Files
wheel/scripts/build_fan120_mount.py

99 lines
5.9 KiB
Python

"""Build nominal 120 mm fan support; FreeCAD GUI macro entrypoint."""
from pathlib import Path
import math, time, traceback
import FreeCAD as A, FreeCADGui as G, Part, Mesh, Import
from pivy import coin
R=Path(__file__).resolve().parent.parent
O=R/'models'/'fan120_2020_mount_v1'; O.mkdir(parents=True,exist_ok=True)
V=A.Vector
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=(.12,.12,.12);o.ViewObject.Transparency=t
return o
def cyl(r,h,x,y,z):return Part.makeCylinder(r,h,V(x,y,z))
def slot(x,y,z,r,travel,h):
s=Part.makeBox(travel,2*r,h,V(x-travel/2,y-r,z))
return s.fuse(cyl(r,h,x-travel/2,y,z)).fuse(cyl(r,h,x+travel/2,y,z))
def chk(n,s):
if s.isNull() or not s.isValid() or len(s.Solids)!=1 or s.Volume<=0:raise RuntimeError(n+' invalid')
try:
src=A.openDocument(str(R/'models'/'pcd50_open_pillar_v1'/'Assembly.FCStd'))
names=[o.Name for o in src.Objects if hasattr(o,'Shape')]
sh={n:src.getObject(n).Shape.copy() for n in names}
bounds=[sh['Rail%d'%i].BoundBox for i in range(2)]
centers=sorted([(b.XMin+b.XMax)/2 for b in bounds]); A.closeDocument(src.Name)
if abs(centers[0]+centers[1])>.01:raise RuntimeError('rail not symmetric')
zrail=max(b.ZMax for b in bounds); zframe=zrail+10
# Rectangular continuous ring; four supports rest on rail upper faces.
frame=Part.makeBox(120,120,4,V(-60,-60,zframe)).cut(Part.makeBox(96,96,4.04,V(-48,-48,zframe-.02)))
for x in centers:
for y in (-30,30):frame=frame.fuse(Part.makeBox(24,18,14,V(x-12,y-9,zrail)))
frame=frame.removeSplitter()
# Four 4.5 mm through holes for 105 mm standard fan pattern.
for x in (-52.5,52.5):
for y in (-52.5,52.5):frame=frame.cut(cyl(2.25,4.04,x,y,zframe-.02))
# M5 rail screws: x slots provide +/-5 mm adjustment. Counterbores seat heads
# below fan mating face and give access before installing fan.
for x in centers:
for y in (-30,30):
frame=frame.cut(slot(x,y,zrail-.02,2.75,10,14.04))
frame=frame.cut(slot(x,y,zframe-1.5,4.9,10,5.52))
frame=frame.removeSplitter();chk('frame',frame)
# Print fan face down; support legs stand upward with no scaffolding.
printed=frame.copy();printed.translate(V(0,0,-(zframe+4)));printed.rotate(V(),V(1,0,0),180)
pd=A.newDocument('Fan120MountPrint'); po=add(pd,'FanMount','120mm fan support single piece print orientation',printed,(.15,.5,.86));pd.recompute()
Mesh.export([po],str(O/'Fan120Mount.stl'));Import.export([po],str(O/'Fan120Mount.step'))
mesh=Mesh.Mesh(str(O/'Fan120Mount.stl'))
if not mesh.isSolid() or abs(mesh.BoundBox.ZMin)>.001:raise RuntimeError('non-solid STL or off bed')
pd.saveAs(str(O/'Fan120Mount.FCStd'));A.closeDocument(pd.Name)
d=A.newDocument('Fan120Assembly');objects={}
colors={'Motor':(.35,.36,.38),'Holder':(.97,.53,.1),'Magnet':(.8,.8,.8),'PCB':(.03,.65,.19),'Chip':(.1,.1,.1),'OpenPillarBracket':(.12,.43,.88)}
for n in names:
c=colors.get(n,(.82,.49,.15) if n.startswith('Corner') else (.5,.52,.56) if n.startswith('Rail') else (.8,.8,.8))
objects[n]=add(d,n,n,sh[n],c,55 if n=='Motor' else 0)
support=add(d,'FanSupport','PRINT: 120 mm fan mount',frame,(.96,.57,.12))
fan=Part.makeBox(120,120,25,V(-60,-60,zframe+4)).cut(cyl(56,25.04,0,0,zframe+3.98))
for x in (-52.5,52.5):
for y in (-52.5,52.5):fan=fan.cut(cyl(2.2,25.04,x,y,zframe+3.98))
fan=fan.removeSplitter();fo=add(d,'FanEnvelope','120 x 120 x 25 mm FAN envelope, schematic',fan,(.2,.2,.2),45)
# Fan hub/body indicative only (fan-dependent; not included in collision test).
hub=cyl(18,8,0,0,zframe+4+8);add(d,'FanHub','Fan hub schematic only',hub,(.25,.25,.25),35)
d.recompute();hits=[]
for n,s in sh.items():
if frame.BoundBox.intersect(s.BoundBox):
v=frame.common(s).Volume
if v>.001:hits.append('support / %s = %.5f mm3'%(n,v))
if fan.BoundBox.intersect(s.BoundBox):
v=fan.common(s).Volume
if v>.001:hits.append('fan envelope / %s = %.5f mm3'%(n,v))
if hits:raise RuntimeError('Collisions: '+', '.join(hits))
text=['Frame solid=%d volume=%.3f mm3 mesh watertight=%s triangles=%d'%(len(frame.Solids),frame.Volume,mesh.isSolid(),mesh.CountFacets),
'Rail centers X='+str(centers)+' rail top Z=%.3f'%zrail,
'Fan envelope 120x120x25 mm, fan holes 105x105 mm, diameter 4.5 mm.',
'Fan lower face Z=%.1f mm; bracket top Z=%.1f mm; 10 mm gap under cross ring.'%(zframe+4,zframe+4),
'Fan-frame basic opening 96x96 mm with local supports; M5 extrusion slots at y +/-30 mm, transverse travel 10 mm.',
'Nominal clearance from encoder screw heads to fan bottom is %.2f mm.'%((zframe+4)-max(sh[n].BoundBox.ZMax for n in sh if n.startswith('Head50_'))),
'Positive-volume collisions of frame/fan envelope against previous modeled objects:']+(hits or ['none'])+[
'Fan is only a simplified envelope, no real fan structure, grille, leads or cable clearance included.',
'Motor housing is simplified; airflow and real installation are not validated.']
(O/'geometry_check.txt').write_text('\n'.join(text)+'\n',encoding='utf8')
view=G.activeDocument().activeView();G.activateWorkbench('PartWorkbench')
def render(mode,path):
if mode=='top':view.viewTop()
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),1600,1200,'White')
render('axon','assembly.png');render('top','rear.png')
fo.ViewObject.Visibility=False;d.getObject('FanHub').ViewObject.Visibility=False;render('axon','without_fan.png')
for obj in d.Objects:
if hasattr(obj,'Shape'):obj.ViewObject.Visibility=(obj.Name=='FanSupport')
render('axon','support.png')
for obj in d.Objects:
if hasattr(obj,'Shape'):obj.ViewObject.Visibility=True
render('axon','assembly.png');d.recompute();d.saveAs(str(O/'Assembly.FCStd'))
Import.export([o for o in d.Objects if hasattr(o,'Shape')],str(O/'Assembly.step'))
(O/'DONE.txt').write_text('ok\n',encoding='utf8')
except Exception:
(O/'ERROR.txt').write_text(traceback.format_exc(),encoding='utf8');raise