from pathlib import Path import math, time, traceback import FreeCAD as A, FreeCADGui as G, Part, Import from pivy import coin V=A.Vector R=Path(r'C:\projects\wheel'); O=R/'models'/'kfl_compact80_concept'; O.mkdir(parents=True,exist_ok=True) def box(x,y,z,dx,dy,dz): return Part.makeBox(dx,dy,dz,V(x,y,z)) def c(r,h,x,y,z,d=V(0,0,1)): return Part.makeCylinder(r,h,V(x,y,z),d) def slotx(x,y,z,r,h):return box(x-2,y-r,z,4,2*r,h).fuse(c(r,h,x-2,y,z)).fuse(c(r,h,x+2,y,z)) def sloty(x,y,z,r,h):return box(x-r,y-2,z,2*r,4,h).fuse(c(r,h,x,y-2,z)).fuse(c(r,h,x,y+2,z)) def add(d,n,s,col,trans=0): o=d.addObject('PartDesign::Feature',n);o.Label=n;o.Shape=s.removeSplitter();o.ViewObject.ShapeColor=col;o.ViewObject.LineColor=(.1,.1,.1);o.ViewObject.Transparency=trans;return o def cv(a,b): return a.common(b).Volume if a.BoundBox.intersect(b.BoundBox) else 0. try: d=A.openDocument(str(R/'models'/'kfl_plate_b38_concept'/'Assembly.FCStd'));d.Label='Compact 80x80 plates and one transverse beam' for i in range(2): for j in range(2): for prefix in ('RealCorner','BoardFast','RailFast','Tnut'): n='%s%d%d'%(prefix,i,j) if d.getObject(n):d.removeObject(n) if d.getObject('AdjustPlate%d'%i):d.removeObject('AdjustPlate%d'%i) d.recompute() plates=[]; angles=[]; bolts=[] for i,z in enumerate((-84.,-187.)): p=box(-38,-51,z,76,80,8).cut(c(19.5,8.04,0,0,z-.02)) for x in (-24.,24.):p=p.cut(c(3.5,8.04,x,0,z-.02)) for x in (-27.5,27.5):p=p.cut(slotx(x,-44,z-.02,2.75,8.04)) p=add(d,'CompactPlate%d'%i,p,(.7,.72,.76));plates.append(p) face=z+8 if i==0 else z sign=1 if i==0 else -1 lpz=face if sign==1 else face-3 lsz=face if sign==1 else face-20 zrail=face+10 if sign==1 else face-10 for j,side in enumerate((-1,1)): # Rail side leg is against the inner x face; plate leg is on its axial face. xmin=-40.6200577 if side==-1 else 20.6200577 plateleg=box(xmin,-74,lpz,20,18,3) sx=side*27.5 plateleg=plateleg.cut(slotx(sx,-64,lpz-.02,2.75,3.04)) side_x=-40.6200577 if side==-1 else 37.6200577 sideleg=box(side_x,-74,lsz,3,18,20) sideleg=sideleg.cut(c(2.75,3.04,side_x-.02,-65,zrail,V(1,0,0))) angle=add(d,'SideAngle%d%d'%(i,j),plateleg.fuse(sideleg),(.9,.5,.15));angles.append(angle) # Flat strip and spacer bridge normal 18mm angle to the compact plate. linkz=face+3 if i==0 else face-6 link=box(sx-9,-70,linkz,18,40,3) link=link.cut(c(2.75,3.04,sx,-64,linkz-.02)) link=link.cut(sloty(sx,-44,linkz-.02,2.75,3.04)) angles.append(add(d,'LinkPlate%d%d'%(i,j),link,(.92,.65,.23))) spacerz=face if i==0 else face-3 spacer=c(5,3,sx,-44,spacerz).cut(c(2.75,3.04,sx,-44,spacerz-.02)) angles.append(add(d,'LinkSpacer%d%d'%(i,j),spacer,(.7,.72,.75))) stem=c(2.45,22,sx,-44,z-10 if i==1 else z-4) if i==0: head=c(4.25,2.75,sx,-44,face+7);nut=c(4.25,3,sx,-44,z-3);wz=face+6 else: head=c(4.25,2.75,sx,-44,z-9.75);nut=c(4.25,3,sx,-44,z+8);wz=z-7 washer=c(5,1,sx,-44,wz).cut(c(2.75,1.02,sx,-44,wz-.01)) fast=add(d,'PlateM5_%d%d'%(i,j),stem.fuse(head).fuse(nut).fuse(washer),(.12,.13,.15));bolts.append(fast) linkshank=c(2.45,13,sx,-64,face-3 if i==0 else face-10) if i==0: linkhead=c(4.25,2.75,sx,-64,face+7);linknut=c(4.25,3,sx,-64,face-3);linkwz=face+6 else: linkhead=c(4.25,2.75,sx,-64,face-9.75);linknut=c(4.25,3,sx,-64,face);linkwz=face-7 linkwasher=c(5,1,sx,-64,linkwz).cut(c(2.75,1.02,sx,-64,linkwz-.01)) bolts.append(add(d,'LinkM5_%d%d'%(i,j),linkshank.fuse(linkhead).fuse(linknut).fuse(linkwasher),(.12,.13,.15))) # Side bolt enters the T-slot on the inner face of the longitudinal rail. if side==-1: shank=c(2.45,11,-46,-65,zrail,V(1,0,0));head=c(4.25,2.75,-37.6200577,-65,zrail,V(1,0,0));nut=box(-45,-69,zrail-4,4,8,8) else: shank=c(2.45,11,35,-65,zrail,V(1,0,0));head=c(4.25,2.75,37.6200577,-65,zrail,V(-1,0,0));nut=box(41,-69,zrail-4,4,8,8) b=add(d,'RailM5_%d%d'%(i,j),shank.fuse(head),(.14,.15,.17));bolts.append(b) add(d,'RailTnut_%d%d'%(i,j),nut,(.67,.67,.69)) # Single X-directed cross beam: below both lengthwise rails, halfway between plates. cross=add(d,'SingleCrossbeam',box(-80,-95,-146,160,20,20),(.46,.49,.53)) crossangles=[] for j,side in enumerate((-1,1)): # Two ordinary right angles outside the long beams, attaching the crossmember to each. x0=-80 if side==-1 else 60.6200577 horiz=box(x0,-75,-145,19.3799423,3,18) vertx=-63.6200577 if side==-1 else 60.6200577 vert=box(vertx,-75,-145,3,20,18) hx=-70 if side==-1 else 70 horiz=horiz.cut(c(2.75,3.04,hx,-75.02,-136,V(0,1,0))) vert=vert.cut(c(2.75,3.04,vertx-.02,-65,-136,V(1,0,0))) ca=add(d,'CrossAngle%d'%j,horiz.fuse(vert),(.85,.48,.15));crossangles.append(ca) # Low-head M5 fastener indications; no T-slot channels modeled. ba=c(2.4,10,hx,-82,-136,V(0,1,0)).fuse(c(4,2,hx,-72,-136,V(0,1,0))) if side==-1: bb=c(2.4,10,-67,-65,-136,V(1,0,0)).fuse(c(4,2,-63.6200577,-65,-136,V(-1,0,0))) else: bb=c(2.4,10,57,-65,-136,V(1,0,0)).fuse(c(4,2,63.6200577,-65,-136,V(1,0,0))) bolts.append(add(d,'CrossFastTop%d'%j,ba,(.14,.15,.17))) bolts.append(add(d,'CrossFastSide%d'%j,bb,(.14,.15,.17))) d.recompute() tests=[] def check(a,b): ao=d.getObject(a);bo=d.getObject(b) if ao and bo:tests.append((a,b,cv(ao.Shape,bo.Shape))) for i in range(2): for n in ('KFLHousing%d'%i,'KFLInner%d'%i,'Motor','MotorFlange','WheelHub','Wheel70','SwitchEnvelope','NewRail0','NewRail1','SingleCrossbeam'): check('CompactPlate%d'%i,n) for j in range(2): for n in ('CompactPlate%d'%i,'NewRail%d'%j,'Motor','MotorFlange','WheelHub','Wheel70','SwitchEnvelope','SingleCrossbeam','PlateM5_%d%d'%(i,j),'RailM5_%d%d'%(i,j)): check('SideAngle%d%d'%(i,j),n) check('PlateM5_%d%d'%(i,j),'RailM5_%d%d'%(i,j)) check('LinkM5_%d%d'%(i,j),'RailM5_%d%d'%(i,j)) check('LinkM5_%d%d'%(i,j),'LinkPlate%d%d'%(i,j)) check('PlateM5_%d%d'%(i,j),'LinkPlate%d%d'%(i,j)) check('PlateM5_%d%d'%(i,j),'CompactPlate%d'%i) check('LinkPlate%d%d'%(i,j),'CompactPlate%d'%i) check('LinkSpacer%d%d'%(i,j),'CompactPlate%d'%i) for n in ('Motor','MotorFlange','WheelHub','Wheel70','SwitchEnvelope','NewRail%d'%j):check('LinkPlate%d%d'%(i,j),n) for j in range(2): for n in ('NewRail%d'%j,'SingleCrossbeam','SwitchEnvelope','Motor'): check('CrossAngle%d'%j,n) check('CrossFastTop%d'%j,'CrossFastSide%d'%j) for n in ('NewRail0','NewRail1','SwitchEnvelope','Motor','CamShaft','CamUpper','CamLower'): check('SingleCrossbeam',n) motion=[] for i in range(2): for j in range(2): a=d.getObject('SideAngle%d%d'%(i,j)).Shape rail=d.getObject('NewRail%d'%j).Shape railbolt=d.getObject('RailM5_%d%d'%(i,j)).Shape for dx in (-2.,0.,2.): for dy in (-2.,0.,2.): pl=plates[i].Shape.copy();pl.translate(V(dx,dy,0)) link=d.getObject('LinkPlate%d%d'%(i,j)).Shape.copy();link.translate(V(dx,0,0)) lb=d.getObject('LinkM5_%d%d'%(i,j)).Shape.copy();lb.translate(V(dx,0,0)) pb=d.getObject('PlateM5_%d%d'%(i,j)).Shape.copy();pb.translate(V(dx,dy,0)) vals=[cv(pl,rail),cv(lb,a),cv(lb,railbolt),cv(pb,link),cv(pb,railbolt),cv(link,rail)] motion.append((i,j,dx,dy,max(vals))) d.recompute() lines=['CONCEPT ONLY; NOT FOR CNC, PRINT, FINAL CUT OR SAFE-FIT APPROVAL.', 'Two 76x80x8 plates span x=-38..38,y=-51..29; 39 bore, 48 bolt spacing, 7 mounting holes.', 'Longitudinal profiles are solid schematic 2020, inner x=+/-40.6200577; plate-to-rail lateral nominal gap 2.6200577 mm.', 'Each plate connected to both longitudinal rails by side angles. Crossbeam is ONE 2020 below both rails between plates, reinforcing frame; it does NOT directly support both plates.', 'Crossbeam extent 160x20x20 schematic; stock angles approximated 20x20x18x3, M5 hex-head envelopes and T nuts illustrative; no T slot geometry.', 'Four additional 18x40x3 flat links and 3mm spacers bridge standard 18mm-wide side angles to the plates; the side angle T-slot bolt is centred on rail at y=-65.', 'Compact plate slot 9.5x5.5 along X crosses the flat link upper slot 9.5x5.5 along Y: nominal ~2mm adjustment each direction assuming nominal M5, subject to hardware and shaft constraints. Standard angle oval leg slot follows leg length X. Plate-to-rail margin 2.62mm.', 'Crossbeam fastening T nuts and head/tool accessibility, hardware tolerances, structural stiffness and angular adjuster remain unvalidated; switch geometry and axis are approximate.', 'KFL cylindrical protrusion OD38 is assumed b envelope, actual shape / bore OD39 must be verified; M6 washer OD12 partly over central hole. No tolerance or strength conclusion.', 'Selected positive volume intersection checks (mm3), any rail versus fastener may represent intentionally undrawn T-slot; inspect individually:']+[f'{a} / {b} = {v:.6f}' for a,b,v in tests]+['Sampled adjustments, max selected overlap mm3:']+[f'{i}{j} dx={dx:+.1f} dy={dy:+.1f}: {v:.6f}' for i,j,dx,dy,v in motion] (O/'geometry_check.txt').write_text('\n'.join(lines)+'\n',encoding='utf8') (O/'bad.txt').write_text('\n'.join([f'{a}/{b}: {v:.6f}' for a,b,v in tests if v>.001]+[f'motion {i}{j} dx={dx} dy={dy}: {v:.6f}' for i,j,dx,dy,v in motion if v>.001]) or 'none',encoding='utf8') d.saveAs(str(O/'Assembly.FCStd'));Import.export([o for o in d.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() elif mode=='front':view.viewBottom() else:view.viewAxonometric();G.updateGui();view.getCameraNode().orientation.setValue(coin.SbRotation(.880476,.115917,-.279848,-.364705)) G.updateGui();time.sleep(.5);view.fitAll();G.updateGui();time.sleep(.5);view.saveImage(str(O/name),1800,1250,'White') save('assembly.png','axon');save('side.png','side') visible={o.Name:o.ViewObject.Visibility for o in d.Objects if hasattr(o,'Shape')} show=set(['CompactPlate0','CompactPlate1','SingleCrossbeam','NewRail0','NewRail1','KFLHousing0','KFLHousing1','KFLInner0','KFLInner1','CamShaft']+[o.Name for o in angles+crossangles+bolts]) for o in d.Objects: if hasattr(o,'Shape'):o.ViewObject.Visibility=o.Name in show save('frame.png','axon');save('plate.png','front') for o in d.Objects: if hasattr(o,'Shape'):o.ViewObject.Visibility=visible.get(o.Name,True) d.recompute();d.saveAs(str(O/'Assembly.FCStd')) if (O/'ERROR.txt').exists(): (O/'ERROR.txt').unlink() (O/'DONE.txt').write_text('ok',encoding='utf8') except Exception: (O/'ERROR.txt').write_text(traceback.format_exc(),encoding='utf8');raise