"""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