from pathlib import Path import math, time, traceback import FreeCAD as A, FreeCADGui as G, Part, Import from pivy import coin R=Path(r'C:\projects\wheel'); O=R/'models'/'kfl_plate_adjust_concept'; O.mkdir(parents=True,exist_ok=True) V=A.Vector def cylinder(r,h,p,d=V(0,0,1)): return Part.makeCylinder(r,h,p,d) def obj(doc,name,label,shape,color,trans=0): o=doc.addObject('PartDesign::Feature',name);o.Label=label;o.Shape=shape;o.ViewObject.ShapeColor=color;o.ViewObject.LineColor=(.12,.12,.12);o.ViewObject.Transparency=trans;return o def slot_z(x,y1,y2,r,z,h): return Part.makeBox(2*r,y2-y1,h,V(x-r,y1,z)).fuse(cylinder(r,h,V(x,y1,z))).fuse(cylinder(r,h,V(x,y2,z))) def slot_y(x1,x2,z,r,y,h): return Part.makeBox(x2-x1,h,2*r,V(x1,y,z-r)).fuse(cylinder(r,h,V(x1,y,z),V(0,1,0))).fuse(cylinder(r,h,V(x2,y,z),V(0,1,0))) def slot_xz(x1,x2,y,r,z,h): return Part.makeBox(x2-x1,2*r,h,V(x1,y-r,z)).fuse(cylinder(r,h,V(x1,y,z))).fuse(cylinder(r,h,V(x2,y,z))) def hex_prism(radius,length,p,d): return Part.makePolygon([]) if False else Part.makeCylinder(radius,length,p,d,360) def common(a,b): if not a.BoundBox.intersect(b.BoundBox): return 0.0 return a.common(b).Volume try: doc=A.openDocument(str(R/'models'/'kfl_150_switch60_concept'/'Assembly.FCStd')) # Prior rails are solid envelopes; retain their positions but extend forward 10mm. for name in ('Plate0','Plate1','L_0_0','L_0_1','L_1_0','L_1_1'): if doc.getObject(name): doc.removeObject(name) doc.recompute() # Give foreground rails their actual representation as outline envelopes only. for i,x in enumerate((-50.6200577,50.6200577)): rail=doc.getObject('NewRail%d'%i);rail.Shape=Part.makeBox(20,20,235,V(x-10,-75,-215)) rail.Label='2020 rail envelope; slots not modelled';rail.ViewObject.Transparency=25 # KMT KFL001 A0=16, A=11.5, A1=5.5: relative to the mounting # plane, inner insert protrudes 10.5mm and housing protrudes 6mm. # Circular protrusion OD29 is still a geometric placeholder. rear=doc.getObject('KFLHousing0');rear.Shape=rear.Shape.cut(cylinder(20,2.02,V(0,0,-84.01))).removeSplitter();rear.Label='KFL housing 6mm nominal protrusion, OD29 assumed' front=doc.getObject('KFLHousing1');front.Shape=front.Shape.cut(cylinder(20,2.02,V(0,0,-181.01))).removeSplitter();front.Label='KFL housing 6mm nominal protrusion, OD29 assumed' inner=doc.getObject('KFLInner0');s=inner.Shape.copy();s.translate(V(0,0,2));inner.Shape=s;inner.Label='KFL insert 10.5mm nominal protrusion behind mounting plane' inner=doc.getObject('KFLInner1');s=inner.Shape.copy();s.translate(V(0,0,-1));inner.Shape=s;inner.Label='KFL insert 10.5mm nominal protrusion behind mounting plane' doc.recompute() plates=[]; corners=[]; boardfast=[]; railfast=[]; knuts=[] for i,z in enumerate((-84.,-187.)): shape=Part.makeBox(140,86,8,V(-70,-51,z)).cut(cylinder(15.1,8.04,V(0,0,z-.02))) for x in (-24.,24.):shape=shape.cut(cylinder(3.5,8.04,V(x,0,z-.02))) for x in (-50.6200577,50.6200577): shape=shape.cut(slot_xz(x-2,x+2,-42.75,2.75,z-.02,8.04)) plates.append(obj(doc,'AdjustPlate%d'%i,'8mm plate, horizontal cross-slots, mounting aperture provisional',shape.removeSplitter(),(.72,.74,.78))) for j,x in enumerate((-50.6200577,50.6200577)): # Brackets on outer side of each plate, legs exactly meet each other. sign=1 if i==0 else -1 face=z+8 if sign==1 else z zplate=face if sign==1 else face-3 zfoot=face if sign==1 else face-20 leg=Part.makeBox(18,20,3,V(x-9,-55,zplate)) leg=leg.cut(slot_z(x,-45.75,-41.75,2.75,zplate-.02,3.04)) foot=Part.makeBox(18,3,20,V(x-9,-55,zfoot)) zb=face+10 if sign==1 else face-10 foot=foot.cut(cylinder(2.75,3.04,V(x,-55.02,zb),V(0,1,0))) bracket=leg.fuse(foot).removeSplitter() corners.append(obj(doc,'RealCorner%d%d'%(i,j),'2020 L angle 20x20x18: assumed 9.5x5.5 long slot and 5.5 round',bracket,(.88,.48,.14))) # Plate M5 screw shank, head and nut: 5 mm diameter nominal. # 1 mm thick washers and 19 mm shank envelope (not a selected screw length). stem=cylinder(2.45,19,V(x,-42.75,z-4)); if sign==1: head=cylinder(4.2,3,V(x,-42.75,face+4));nut=cylinder(4.3,3,V(x,-42.75,z-4));wash1=cylinder(5,1,V(x,-42.75,face+3));wash2=cylinder(5,1,V(x,-42.75,z-1)) else: head=cylinder(4.2,3,V(x,-42.75,z-7));nut=cylinder(4.3,3,V(x,-42.75,z+9));wash1=cylinder(5,1,V(x,-42.75,z-4));wash2=cylinder(5,1,V(x,-42.75,z+8)) boardfast.append(obj(doc,'BoardFast%d%d'%(i,j),'M5 plate fastener and 10mm washer envelopes',stem.fuse(head).fuse(nut).fuse(wash1).fuse(wash2),(.13,.14,.16))) # Foot screw goes into the rail; T-slot geometry omitted from the rail envelope. # Ø5.5 round hole at profile, low-profile Ø9.5 x 2.75 head, no washer. shank=cylinder(2.45,8,V(x,-60,zb),V(0,1,0));head=cylinder(4.75,2.75,V(x,-52,zb),V(0,1,0)) railfast.append(obj(doc,'RailFast%d%d'%(i,j),'M5 low-profile 9.5x2.75 head at round hole; no washer',shank.fuse(head),(.13,.14,.16))) knuts.append(obj(doc,'Tnut%d%d'%(i,j),'2020 T nut location schematic inside rail',Part.makeBox(8,4,8,V(x-4,-61,zb-4)),(.7,.7,.72))) # Accurate KFL heads and nuts are not dimensioned, add nominal cylindrical envelopes. for i,z in enumerate((-84.,-187.)): for j,x in enumerate((-24.,24.)): if i==0: head=cylinder(5,3,V(x,0,-70.5));nut=cylinder(5,3,V(x,0,-87)) else: head=cylinder(5,3,V(x,0,-195.5));nut=cylinder(5,3,V(x,0,-179)) obj(doc,'KFLFastEnd%d%d'%(i,j),'M6 head / nut envelopes assumed',head.fuse(nut),(.15,.16,.18)) doc.recompute() tests=[] def check(label,a,b): v=common(a.Shape,b.Shape);tests.append((label,v)) for i in range(2): for n in ('KFLHousing%d'%i,'KFLInner%d'%i):check('Plate%d/%s'%(i,n),plates[i],doc.getObject(n)) for j in range(2): k=2*i+j; check('Plate%d/corner%d%d'%(i,i,j),plates[i],corners[k]);check('boardfast%d%d/corner'%(i,j),boardfast[k],corners[k]);check('boardfast%d%d/plate'%(i,j),boardfast[k],plates[i]);check('railfast%d%d/corner'%(i,j),railfast[k],corners[k]) check('boardfast%d%d/railfast'%(i,j),boardfast[k],railfast[k]);check('boardfast%d%d/rail'%(i,j),boardfast[k],doc.getObject('NewRail%d'%j));check('railfast%d%d/plate'%(i,j),railfast[k],plates[i]) for n in ('Motor','SwitchEnvelope','MotorFlange','WheelHub','Wheel70'): if doc.getObject(n): check('corner%d%d/%s'%(i,j,n),corners[k],doc.getObject(n));check('boardfast%d%d/%s'%(i,j,n),boardfast[k],doc.getObject(n));check('railfast%d%d/%s'%(i,j,n),railfast[k],doc.getObject(n)) # Whole solid rail envelopes include a drawn solid area where a T-slot exists. check('corner%d%d/ownRail'%(i,j),corners[k],doc.getObject('NewRail%d'%j)) for i in range(2): for n in ('KFLHousing%d'%i,'KFLInner%d'%i): for other in (('MotorFlange' if i==0 else 'WheelHub'),'CamShaft','SwitchEnvelope'): if doc.getObject(n) and doc.getObject(other):check(n+'/'+other,doc.getObject(n),doc.getObject(other)) for x in (-24,24): nut=doc.getObject('KFLFastEnd%d%d'%(i,0 if x<0 else 1)) for n in ('MotorFlange','WheelHub','SwitchEnvelope'): if doc.getObject(n):check(nut.Name+'/'+n,nut,doc.getObject(n)) traveltests=[] for i in range(4): for dy in (-2.,0.,1.): moved=boardfast[i].Shape.copy();moved.translate(V(0,dy,0)) vol=common(moved,railfast[i].Shape) distance=moved.distToShape(railfast[i].Shape)[0] traveltests.append((boardfast[i].Name,dy,vol,distance)) combined=[] for i in range(4): for dx in (-2.,0.,2.): for dy in (-2.,0.,1.): movedfast=boardfast[i].Shape.copy();movedfast.translate(V(0,dy,0)) movedplate=plates[i//2].Shape.copy();movedplate.translate(V(dx,dy,0)) overlaps=(common(movedfast,railfast[i].Shape),common(movedfast,corners[i].Shape),common(movedfast,movedplate),common(movedplate,corners[i].Shape),common(movedplate,doc.getObject('NewRail%d'%(i%2)).Shape)) combined.append((i,dx,dy,max(overlaps))) doc.recompute() lines=['CONCEPT ONLY: NOT FOR CNC OR PRINT.', 'Prior shaft 12x150, 60mm switch envelope, mirrored KFL and known motor assembly retained.', '8 mm plates: 140x86 envelope; central hole OD30.2 assumed, KFL holes OD7 at +/-24 (48mm pitch).', 'KMT catalogue A0=16, A1=5.5, A=11.5: inner extends 10.5mm and housing 6mm from flange mounting plane. Housing projection OD29 assumed; not catalog-confirmed.', 'Minimum nominal clearance between centre and KFL bolt holes: 24-15.1-3.5=5.4 mm; not a strength calculation.', 'Whole plate height adjusts via 9.5x5.5 long slot in angle leg; initial bolt is 1mm off slot centre, nominal offsets -2 to +1mm. Plate to rail has 4mm nominal gap.', 'Whole plate left/right adjusts via 9.5x5.5 slots on plates, crossing each slotted angle leg, about +/-2mm subject to fit.', 'Axial positioning via sliding T-nuts in the 2020 profile. Brackets 20x20x18x3, 9.5x5.5 slot / 5.5 round as assumed stock.', 'M5 head/nut envelopes include 10mm board washers; round-hole rail fasteners use low-profile 9.5mm head, no washer; T nuts, shape and actual extrusion grooves need review.', 'Centre hole and bearing projection are assumptions: do NOT assume bearing moves in plate opening.', 'Slots geometry offers horizontal +/-2mm, vertical -2..+1mm, but assembled shaft, flange and small gaps constrain safe motion. Loosen and check continuously; do not force alignment.', 'KFL shape, fastener head/nut access, actual motor, electrical wiring, switch shape, stator etc not validated.', 'Bearing locating/floating and structural strength remain unresolved.', 'Positive-volume intersection checks in cubic mm:']+[f'{n}: {v:.6f}' for n,v in tests] lines+=['M5 hardware clearance over candidate vertical travel (cubic mm intersection and mm minimum separation):']+[f'{n} dy={dy:+.1f} intersection={v:.6f} distance={d:.6f}' for n,dy,v,d in traveltests] lines+=['Combined adjustment corners (plate dx/dy and M5s relative bracket; no mounted shaft or surrounding structure included):']+[f'corner={i} dx={dx:+.1f} dy={dy:+.1f} max_intersection={v:.6f}' for i,dx,dy,v in combined] lines+=['Nominal simplified axial clearances (mm): rear housing/flange %.6f; rear inner/flange %.6f; front housing/flange %.6f; front inner/flange %.6f.'%(doc.getObject('KFLHousing0').Shape.distToShape(doc.getObject('MotorFlange').Shape)[0],doc.getObject('KFLInner0').Shape.distToShape(doc.getObject('MotorFlange').Shape)[0],doc.getObject('KFLHousing1').Shape.distToShape(doc.getObject('WheelHub').Shape)[0],doc.getObject('KFLInner1').Shape.distToShape(doc.getObject('WheelHub').Shape)[0])] lines+=['KFL inner axial centre distance 104.5mm, switch envelope to inner end gaps rear15.5mm front14.5mm; computed from nominal adjusted model.'] (O/'geometry_check.txt').write_text('\n'.join(lines)+'\n',encoding='utf8') # Red solids explicitly indicate any detected nonzero intersection. bad=[(n,v) for n,v in tests if v>0.001] (O/'checks_status.txt').write_text('INTERSECTIONS FOUND: '+str(bad)+'\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(path,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/path),1800,1250,'White') save('assembly.png','axon');save('side.png','side') # Bearing/plates context only; hides shaft/end assemblies and green envelope, so holes visible. visible={o.Name:o.ViewObject.Visibility for o in doc.Objects if hasattr(o,'Shape')} show=set([o.Name for o in plates+corners+boardfast+railfast]+['KFLHousing0','KFLHousing1','KFLInner0','KFLInner1','KFLBolt0_0','KFLBolt0_1','KFLBolt1_0','KFLBolt1_1','KFLFastEnd00','KFLFastEnd01','KFLFastEnd10','KFLFastEnd11','NewRail0','NewRail1','CamShaft']) for o in doc.Objects: if hasattr(o,'Shape'):o.ViewObject.Visibility=o.Name in show save('adjustment.png','axon') for o in doc.Objects: if hasattr(o,'Shape'):o.ViewObject.Visibility=visible.get(o.Name,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