161 lines
10 KiB
Python
161 lines
10 KiB
Python
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
|