from pathlib import Path import math, time, traceback import FreeCAD as A, FreeCADGui as G, Part, Import V=A.Vector R=Path(r'C:\projects\wheel'); O=R/'models'/'turn_signal_cam_v2'; O.mkdir(parents=True,exist_ok=True) def add(d,n,label,s,c): o=d.addObject('PartDesign::Feature',n);o.Label=label;o.Shape=s;o.ViewObject.ShapeColor=c;o.ViewObject.LineColor=(.12,.12,.12);return o def cy(r,h,p,v=V(0,0,1)):return Part.makeCylinder(r,h,p,v) def limitrad(s): vertices,_=s.tessellate(.05) return max((math.hypot(p.x,p.y) for p in vertices),default=0) def hit(a,b): return a.common(b).Volume if a.BoundBox.intersect(b.BoundBox) else 0 try: d=A.newDocument('TurnSignalCamV2') # Shaft, with local middle key pocket only. End shaft interfaces are NOT modeled. shaft=cy(6,100,V(0,0,-50)).cut(Part.makeBox(4.1,3,14,V(-2.05,4,-7))) key=Part.makeBox(4,4,12,V(-2,4,-6)) # Two split semicircular pieces with 0.4 mm total clamping gap. ring=cy(15.3,20,V(0,0,-10)).cut(cy(6.1,20.04,V(0,0,-10.02))) upper=ring.common(Part.makeBox(40,20,20,V(-20,.2,-10))) lower=ring.common(Part.makeBox(40,20,20,V(-20,-20.2,-10))) # Square-ended ears, overlap semicircle but fit the Ø34 rotating envelope. for x in (-14.7,8.3): upper=upper.fuse(Part.makeBox(6.4,7.8,20,V(x,.2,-10))) lower=lower.fuse(Part.makeBox(6.4,7.8,20,V(x,-8,-10))) # One radial cam tab, fused; 4x21.3 nominal corner yields Ø43.35 swept. upper=upper.fuse(Part.makeBox(8,7.2,20,V(-4,14.2,-10))) # shaft bore again after fusing + flat-bottom key slot in upper half for name in ('upper','lower'): s=locals()[name].cut(cy(6.1,20.04,V(0,0,-10.02))) if name=='upper': upper=s.cut(Part.makeBox(4.2,4.3,20.04,V(-2.1,4.0,-10.02))) else: lower=s # Two M3 screws across split; cylindrical recesses open at y side. for x in (-11,11): hole=cy(1.7,16.4,V(x,-8.2,0),V(0,1,0)) recessp=cy(3.3,3.02,V(x,5,0),V(0,1,0)) recessn=cy(3.3,3.02,V(x,-8.02,0),V(0,1,0)) upper=upper.cut(hole).cut(recessp) lower=lower.cut(hole).cut(recessn) upper=upper.removeSplitter();lower=lower.removeSplitter() # approximate M3 x 12 screw shaft and 5.5 x 3 head, M3 hex nut. hardware=[] for x in (-11,11): shank=cy(1.5,12,V(x,-7,0),V(0,1,0));head=cy(2.75,3,V(x,5,0),V(0,1,0)); screw=shank.fuse(head).removeSplitter() # hex, across flats = 5.5, axis y, plane x/z rad=5.5/math.sqrt(3);pts=[V(x+rad*math.cos(i*math.pi/3),-7.4,rad*math.sin(i*math.pi/3)) for i in range(6)];pts.append(pts[0]); nut=Part.Face(Part.makePolygon(pts)).extrude(V(0,2.4,0)).cut(cy(1.55,2.42,V(x,-7.41,0),V(0,1,0))) hardware.extend([screw,nut]) for name,s in [('upper',upper),('lower',lower),('shaft',shaft),('key',key)]+[('hardware',s) for s in hardware]: if s.isNull() or not s.isValid() or len(s.Solids)!=1:raise RuntimeError(name+' invalid') res={} for n,s in [('upper',upper),('lower',lower)]+[('hardware%d'%i,s) for i,s in enumerate(hardware)]:res[n]=limitrad(s) checks=[('upper-lower',hit(upper,lower)),('upper-shaft',hit(upper,shaft)),('lower-shaft',hit(lower,shaft)),('key-shaft',hit(key,shaft)),('key-upper',hit(key,upper)),('key-lower',hit(key,lower))] for i,s in enumerate(hardware): checks.extend([('hardware%d-upper'%i,hit(s,upper)),('hardware%d-lower'%i,hit(s,lower)),('hardware%d-shaft'%i,hit(s,shaft))]) if any(v>.001 for _,v in checks):raise RuntimeError('intersections '+str(checks)) if max(res.values())>22.0:raise RuntimeError('whole sweep >44') # cam excluded to check clamping envelope: split top excluding y>15.3 and z 20. clamp=upper.cut(Part.makeBox(50,25,22,V(-25,15.3,-11))).fuse(lower) if max([limitrad(clamp)]+[limitrad(s) for s in hardware])>17:raise RuntimeError('clamp sweep >34') shafto=add(d,'Shaft','12 x 100 shaft; local mid key slot, end features omitted',shaft,(.55,.58,.62)) ko=add(d,'Key','metal key 4 x 4 x 12 assumed',key,(.95,.68,.15)) uo=add(d,'Upper','upper half and integrated tab',upper,(.1,.39,.78)) lo=add(d,'Lower','lower half',lower,(.2,.61,.88)) hs=[] for i,s in enumerate(hardware):hs.append(add(d,'Hardware%d'%i,'M3 schematic fastener %d'%i,s,(.18,.19,.22))) d.recompute();d.saveAs(str(O/'Assembly.FCStd'));Import.export([shafto,ko,uo,lo]+hs,str(O/'Assembly.step')) report=['PRELIMINARY concept only, do not print.','Shaft: 12x100; middle keyway only, shaft end details omitted.', 'Body axial overall 20; cam corner radius %.4f; total swept diameter %.4f'%(limitrad(upper),2*max(res.values())), 'Clamping envelope including screw/nut and ears %.4f'%(2*max([limitrad(clamp)]+[limitrad(s) for s in hardware])), 'key: 4 x 4 x 12 hypothetical; local keyway depth shaft 2mm and hub 2.3mm, width 4.2mm.', 'clamp gap 0.4mm, bore 12.2mm nominal; M3 clearance dia 3.4; cylindrical 6.6mm relief (nut captured only visually, nut rotation restraint not modeled).', 'Calculated positive-volume intersections (mm3):']+[f'{n} {v:.8f}' for n,v in checks]+['Fastener hex socket and actual switch/cam clearance unmodeled; proposed material, deflection, load and tooling clearance not validated.', '100mm shaft position is schematic, module, both bearings, motor connection and wheel are unmodeled; fit cannot be inferred.'] (O/'geometry_check.txt').write_text('\n'.join(report)+'\n',encoding='utf8') view=G.activeDocument().activeView();G.activateWorkbench('PartWorkbench');view.viewAxonometric();G.updateGui();time.sleep(.4);view.fitAll();G.updateGui();time.sleep(.4);view.saveImage(str(O/'full.png'),1600,1200,'White') # close view: temporarily hide shaft ends via a short local reference representation shafto.ViewObject.Visibility=False shaftshort=add(d,'ShortShaft','short shaft visual crop',shaft.common(Part.makeBox(24,24,36,V(-12,-12,-18))),(.55,.58,.62)); d.recompute();view.viewAxonometric();G.updateGui();time.sleep(.3);view.fitAll();G.updateGui();time.sleep(.4);view.saveImage(str(O/'detail.png'),1600,1200,'White') shaftshort.ViewObject.Visibility=False;shafto.ViewObject.Visibility=True;d.recompute();d.saveAs(str(O/'Assembly.FCStd')) (O/'DONE.txt').write_text('ok\n',encoding='utf8') except Exception: (O/'ERROR.txt').write_text(traceback.format_exc(),encoding='utf8');raise