Files
wheel/scripts/build_turn_signal_cam_v2.py

78 lines
5.9 KiB
Python

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