from pathlib import Path import math, time, traceback, os import FreeCAD as A, FreeCADGui as G, Part, Import from pivy import coin V=A.Vector R=Path(r'C:\projects\wheel'); variant=os.environ.get('KFL_HUB_VARIANT')=='narrow';O=R/'models'/('kfl_100_narrow_concept' if variant else 'kfl_100_concept');O.mkdir(parents=True,exist_ok=True) def cy(r,h,x=0,y=0,z=0):return Part.makeCylinder(r,h,V(x,y,z)) def add(d,n,label,s,c,t=0): o=d.addObject('PartDesign::Feature',n);o.Label=label;o.Shape=s;o.ViewObject.ShapeColor=c;o.ViewObject.LineColor=(.08,.08,.08);o.ViewObject.Transparency=t;return o def booleanvol(a,b): return a.common(b).Volume if a.BoundBox.intersect(b.BoundBox) else 0 try: src=A.openDocument(str(R/'models'/'pcd50_open_pillar_v1'/'Assembly.FCStd')) existing={o.Name:(o.Shape.copy(),tuple(o.ViewObject.ShapeColor),o.ViewObject.Transparency) for o in src.Objects if hasattr(o,'Shape') and not o.Shape.isNull()} A.closeDocument(src.Name) src=A.openDocument(str(R/'models'/'turn_signal_cam_v2'/'Assembly.FCStd')) cam={n:src.getObject(n).Shape.copy() for n in ['Shaft','Key','Upper','Lower','Hardware0','Hardware1','Hardware2','Hardware3']} A.closeDocument(src.Name) for s in cam.values():s.translate(V(0,0,-102)) doc=A.newDocument('KFL100_Schematic'); objs={} for n,(s,c,t) in existing.items(): objs[n]=add(doc,n,'Prior existing '+n,s,c,55 if n=='Motor' else t) for i,x in enumerate([-50.6200577,50.6200577]): objs['NewRail%d'%i]=add(doc,'NewRail%d'%i,'Proposed 2020 longitudinal rail schematic',Part.makeBox(20,20,190,V(x-10,-75,-170)),(.48,.51,.55)) # Two vertical plates parallel to XY. The 30.2 mm bore is placeholder, # with dummy bearing protrusion 29 mm OD; NOT a production bore. plates=[] for i,z in enumerate([-74,-140]): s=Part.makeBox(140,90,8,V(-70,-55,z)).cut(cy(15.1,8.02,0,0,z-.01)) for x in (-24,24):s=s.cut(cy(3.5,8.02,x,0,z-.01)) o=add(doc,'Plate%d'%i,'8mm plate: hole 30.2 assumed',s,(.66,.69,.74));plates.append(o) # Simplified illustrative corner brackets, contacting plate and rail only. for j,x in enumerate([-50.6200577,50.6200577]): a=Part.makeBox(16,12,3,V(x-8,-55,z-3)) b=Part.makeBox(16,3,11,V(x-8,-55,z-11)) o=add(doc,'L_%d_%d'%(i,j),'90deg bracket placeholder, fasteners omitted',a.fuse(b).removeSplitter(),(.88,.49,.16)) # Approximate KFL outside dimensions: 63 x 38 mm, pitch 48, flange # ears 5.5, insert 14.5; protrusion 29mm OD and position are placeholders. bearings=[] for i,(plate_z,inner_z,sign) in enumerate([(-74,-78.5,1),(-140,-143,-1)]): # face of plate towards outside axle area, mirrored. if sign==1: face=plate_z+8; flangez=face; insertz=face-8 else: face=plate_z; flangez=face-5.5; insertz=face ring=cy(19,5.5,0,0,flangez).cut(cy(9.5,5.52,0,0,flangez-.01)) for x in (-24,24): ear=cy(7.5,5.5,x,0,flangez).fuse(Part.makeBox(12,15,5.5,V(min(x,0),-7.5,flangez))) ring=ring.fuse(ear) for x in (-24,24):ring=ring.cut(cy(3.5,5.52,x,0,flangez-.01)) ring=ring.cut(cy(9.5,5.52,0,0,flangez-.01)) ring=ring.fuse(cy(14.5,8,0,0,insertz).cut(cy(9.5,8.02,0,0,insertz-.01))).removeSplitter() outer=add(doc,'KFLHousing%d'%i,'KFL001 housing: 29 protrusion ASSUMED',ring,(.45,.49,.53));bearings.append(outer) inner=cy(9.2,14.5,0,0,inner_z).cut(cy(6.1,14.52,0,0,inner_z-.01)) bearings.append(add(doc,'KFLInner%d'%i,'KFL inner 14.5mm rotating schematic',inner,(.77,.77,.79))) # M6 screws only illustrative straight 5.8mm shank; heads omitted. for j,x in enumerate((-24,24)): z0=plate_z-1 if sign==1 else flangez-1 o=add(doc,'KFLBolt%d_%d'%(i,j),'M6 shank schematic, heads omitted',cy(2.9,15.5,x,0,z0),(.18,.19,.20));objs[o.Name]=o # Real shaft/cam from existing project. colors={'Shaft':(.65,.66,.68),'Key':(.98,.65,.1),'Upper':(.05,.40,.80),'Lower':(.16,.62,.88)} for n,s in cam.items():objs['Cam'+n]=add(doc,'Cam'+n,'Existing cam V2 '+n,s,colors.get(n,(.2,.2,.21))) # Actual small conversion disk STEP: mirrored to front face at z=-38. conv=Part.read(str(R/'models'/'转换盘1.step'));conv.rotate(V(),V(1,0,0),180);conv.translate(V(0,0,-38)) objs['Conversion']=add(doc,'Conversion','Existing conversion disk STEP; face position approx',conv,(.82,.83,.85)) # Six-hole keyed flange is a placeholder, the hub overlaps 12mm of shaft. flange=cy(27,4,0,0,-50).cut(cy(6.2,4.02,0,0,-50.01)) for j in range(6): a=2*math.pi*j/6+math.pi/6;flange=flange.cut(cy(2.2,4.02,24*math.cos(a),24*math.sin(a),-50.01)) hubradius=9.2 if variant else 10 hub=cy(hubradius,14,0,0,-64).cut(cy(6.2,14.02,0,0,-64.01));flange=flange.fuse(hub).removeSplitter() objs['MotorFlange']=add(doc,'MotorFlange','MOTOR hub placeholder: 12mm shaft engagement assumed',flange,(.9,.66,.20)) # Wheel hub is deliberately modelled with only 9mm shaft engagement. wh=cy(hubradius,9,0,0,-152).cut(cy(6.2,9.02,0,0,-152.01)) whplate=cy(27,3,0,0,-155).cut(cy(6.2,3.02,0,0,-155.01)) for j in range(6): a=2*math.pi*j/6;whplate=whplate.cut(cy(2.2,3.02,24*math.cos(a),24*math.sin(a),-155.01)) wh=wh.fuse(whplate).removeSplitter() objs['WheelHub']=add(doc,'WheelHub','Wheel flange schematic: only 9mm engagement unverified',wh,(.9,.66,.20)) wheel70=Part.read(str(R/'models'/'70转接板.step'));wheel70.rotate(V(),V(1,0,0),180);wheel70.translate(V(0,0,-155)) objs['Wheel70']=add(doc,'Wheel70','Existing 70 adapter, nominal assumed position, bolt access unchecked',wheel70,(.82,.83,.85)) # Switch envelope is deliberately only transparent rectangular with generic # centre bore; 50mm depth includes off-axis connector. Actual contours unknown. sw=Part.makeBox(110,80,50,V(-55,-40,-128.5)).cut(cy(23.5,50.02,0,0,-128.51)) objs['SwitchEnvelope']=add(doc,'SwitchEnvelope','50mm switch bounding placeholder; 47mm bore ASSUMED',sw,(.45,.73,.43),80) doc.recompute() # Restrict checks to shapes actually modelled. Intentional shaft-to-hub clearance. checks=[] pairs=[('Plate%d'%i,'KFLHousing%d'%i,plates[i].Shape,bearings[2*i].Shape) for i in range(2)] pairs += [('Plate%d'%i,'KFLInner%d'%i,plates[i].Shape,bearings[2*i+1].Shape) for i in range(2)] pairs += [('SwitchEnvelope','Cam'+n,sw,cam[n]) for n in ('Upper','Lower','Hardware0','Hardware1','Hardware2','Hardware3')] pairs += [('MotorFlange','Shaft',flange,cam['Shaft']),('WheelHub','Shaft',wh,cam['Shaft']),('WheelHub','70Adapter',wh,wheel70),('Plate0','Plate1',plates[0].Shape,plates[1].Shape)] pairs += [('MotorFlange','RearBearing',flange,bearings[0].Shape),('WheelHub','FrontBearing',wh,bearings[2].Shape)] for a,b,sa,sb in pairs: checks.append('%s / %s = %.6f mm3'%(a,b,booleanvol(sa,sb))) for n,s in [('RearConflict',flange.common(bearings[0].Shape)),('FrontConflict',wh.common(bearings[2].Shape))]: if not s.isNull() and s.Volume>.001: add(doc,n,'RED: nominal collision '+n,s,(1.0,.03,.03)) lines=['CONCEPT ONLY; NOT ASSEMBLY FIT VALIDATION.', 'Schematic hub outside diameter %.1fmm, not measured; variant narrowed only to explore geometry.'%(hubradius*2), 'Z decreases from motor toward steering wheel; motor rear z0; wheel shaft z=-52..-152 length100.', 'KFL actual nominal 63x38 housing envelope, 48 bolt centers, 7 hole, 5.5 ears, inner 14.5; protrusion OD29 and position assumed.', '8mm plates central hole OD30.2 assumed, not actual production spec.', '50mm switch opaque-enclosing envelope z=-128.5..-78.5; opening OD47 ASSUMED only; real rotary features and contacts unknown.', 'Allocation by shaft: motor hub12 + bearing14.5 + switch50 + bearing14.5 + wheel hub9 = 100 mm. No axial gaps. 9mm hub engagement NOT VALIDATED.', 'Sleeve20 sits inside switch envelope and is not additional 20mm.', 'Motor mounting boss, shaft end keys, original M3 nut, lights/white contacts, bearing set screws, pin fastening/head/tool and adjuster are not modelled.', 'Because ideal 100mm budget has zero clearance and unknown wheel hub engagement, this is not a workable fit verification; real assembly remains unverified. Narrow hub has no verified keyway strength or real KFL clearance.', 'Checked positive-volume intersections (other pairs, loaded operation, exact bearing and switch shape not tested):']+checks (O/'geometry_check.txt').write_text('\n'.join(lines)+'\n',encoding='utf8') doc.saveAs(str(O/'Assembly.FCStd'));Import.export([o for o in doc.Objects if hasattr(o,'Shape')],str(O/'Assembly.step')) view=G.activeDocument().activeView();G.activateWorkbench('PartWorkbench') def save(name,mode): if mode=='side':view.viewFront() else: view.viewAxonometric();G.updateGui();view.getCameraNode().orientation.setValue(coin.SbRotation(.880476,.115917,-.279848,-.364705)) G.updateGui();time.sleep(.7);view.fitAll();G.updateGui();time.sleep(.7);view.saveImage(str(O/name),1800,1250,'White') save('assembly.png','axon');save('side.png','side') # Simplified focus hides rear encoder/rails so bearing through plate is visible. for n,o in objs.items(): if n in existing or n.startswith('NewRail') or n=='Conversion':o.ViewObject.Visibility=False for o in doc.Objects: if o.Name.startswith('L_'):o.ViewObject.Visibility=False save('focus.png','axon') for o in doc.Objects: if hasattr(o,'Shape'):o.ViewObject.Visibility=True doc.recompute();doc.saveAs(str(O/'Assembly.FCStd')) (O/'DONE.txt').write_text('ok',encoding='utf8') except Exception: (O/'ERROR.txt').write_text(traceback.format_exc(),encoding='utf8');raise