from pathlib import Path import os, math, json, time, traceback import FreeCAD as A, FreeCADGui as G, Part, Import V=A.Vector root=Path(r'C:\projects\wheel');out=root/'models'/'fan_direct_motor_side_125';out.mkdir(parents=True,exist_ok=True) try: d=A.openDocument(str(root/'models'/'integer_profiles'/'Assembly.FCStd'));d.recompute() rails=[d.getObject('Rail0'),d.getObject('Rail1')]; corners=[d.getObject('MotorBaseCorner0'),d.getObject('MotorBaseCorner1')] xs=[(o.Shape.BoundBox.XMin+o.Shape.BoundBox.XMax)/2 for o in rails];spacing=xs[1]-xs[0] theta=math.degrees(math.acos(spacing/(105*math.sqrt(2))))-45 oldrails=[o.Shape.copy() for o in rails] for i,rail in enumerate(rails): rail.Placement=A.Placement();rail.Shape=Part.makeBox(20,125,20,V(xs[i]-10,-55,0));rail.Label='2020 motor support 125mm' for i,obj in enumerate(corners): orig=obj.Shape.copy();orig.rotate(V(xs[i],0,10),V(0,1,0),180);obj.Placement=A.Placement();obj.Shape=orig;obj.Label='Motor side 2020-3030 connecting corner' d.recompute() visible=[o for o in d.Objects if hasattr(o,'Shape') and not o.Shape.isNull() and o.ViewObject.Visibility] pairs=[];hits=[];skip={'Rail0','Rail1','MotorBaseCorner0','MotorBaseCorner1'} for i,c in enumerate(corners): for o in visible: if o==c:continue if c.Shape.BoundBox.intersect(o.Shape.BoundBox): vol=c.Shape.common(o.Shape).Volume if vol>1e-4:hits.append([c.Name,o.Name,vol]) if o.Name in ('Motor','Rail%d'%i,'NewRail%d'%i,'Corner%d'%(i*2),'Corner%d'%(i*2+1)): pairs.append([c.Name,o.Name,c.Shape.distToShape(o.Shape)[0]]) assert c.Shape.distToShape(rails[i].Shape)[0]<1e-5 assert c.Shape.distToShape(d.getObject('NewRail%d'%i).Shape)[0]<1e-5 for i,rail in enumerate(rails): extra=rail.Shape.cut(oldrails[i]) for o in visible: if o in rails or o in corners:continue if extra.BoundBox.intersect(o.Shape.BoundBox): vol=extra.common(o.Shape).Volume if vol>1e-4:hits.append(['extra_'+rail.Name,o.Name,vol]) fan=Part.makeBox(120,120,25,V(-60,-60,20)).cut(Part.makeCylinder(56,25.04,V(0,0,19.98))) for x in (-52.5,52.5): for y in (-52.5,52.5):fan=fan.cut(Part.makeCylinder(2.2,25.04,V(x,y,19.98))) fan=fan.removeSplitter();fan.rotate(V(0,0,0),V(0,0,1),theta);fan.translate(V(0,7.5,0)) for o in visible: if fan.BoundBox.intersect(o.Shape.BoundBox): vol=fan.common(o.Shape).Volume if vol>1e-4:hits.append(['DirectFan',o.Name,vol]) if hits: raise Exception('COLLISION '+str(hits)) ff=d.addObject('PartDesign::Feature','DirectFan');ff.Shape=fan;ff.Label='120mm fan assumed geometry, rotated';ff.ViewObject.ShapeColor=(.22,.24,.27);ff.ViewObject.Transparency=18 holes=[];alpha=math.radians(theta);bolts=[] for sign in (-1,1): px=sign*52.5*(math.cos(alpha)-math.sin(alpha));py=7.5+sign*52.5*(math.cos(alpha)+math.sin(alpha));idx=0 if sign<0 else 1 assert abs(px-xs[idx])<1e-6 and -551e-4:hits.append(['bolt',idx,o.Name,vol]) b=d.addObject('PartDesign::Feature','FanBolt'+str(idx));b.Shape=screw;b.Label='M4 fastener illustrative, T-nut not modelled';b.ViewObject.ShapeColor=(.65,.66,.68);bolts.append(b) holes.append({'x':px,'y':py,'distance_to_rail_end':min(py+55,70-py)}) if hits:raise Exception('SCREW COLLISION '+str(hits)) d.recompute();d.Label='Motor side corners, direct fan, 125mm profiles' d.saveAs(str(out/'Assembly.FCStd'));obj=[o for o in d.Objects if hasattr(o,'Shape') and not o.Shape.isNull() and o.ViewObject.Visibility];Import.export(obj,str(out/'Assembly.step')) view=G.activeDocument().activeView();view.viewAxonometric();G.updateGui();view.fitAll();time.sleep(.5);G.updateGui();view.saveImage(str(out/'assembly.png'),1800,1300,'White') vis={o.Name:o.ViewObject.Visibility for o in d.Objects if hasattr(o,'Shape')};show={'Rail0','Rail1','NewRail0','NewRail1','MotorBaseCorner0','MotorBaseCorner1','DirectFan','FanBolt0','FanBolt1','Motor','OpenPillarBracket','Holder','Magnet','PCB','Chip','Corner0','Corner1','Corner2','Corner3'} for o in d.Objects: if hasattr(o,'Shape'):o.ViewObject.Visibility=o.Name in show view.viewTop();G.updateGui();view.fitAll();time.sleep(.5);G.updateGui();view.saveImage(str(out/'fan_top.png'),1800,1300,'White') view.viewAxonometric();G.updateGui();view.fitAll();time.sleep(.5);G.updateGui();view.saveImage(str(out/'fan_detail.png'),1800,1300,'White') for o in d.Objects: if hasattr(o,'Shape'):o.ViewObject.Visibility=vis.get(o.Name,False) d.recompute();d.saveAs(str(out/'Assembly.FCStd'));A.closeDocument(d.Name) d=A.openDocument(str(out/'Assembly.FCStd'));d.recompute();assert all(d.getObject(n).Shape.isValid() for n in ['Rail0','Rail1','DirectFan','MotorBaseCorner0','MotorBaseCorner1']);assert all(abs(d.getObject('Rail%d'%i).Shape.BoundBox.YLength-125)<1e-6 for i in (0,1));step=Part.read(str(out/'Assembly.step'));assert step.isValid() and len(step.Solids)>0 data={'rail_length':125,'rail_limits':[-55,70],'fan_center_y':7.5,'motor_center_y':10,'fan_rotation_degrees':theta,'holes':holes,'corner_contact_distance':pairs,'fan_screw_extension_collisions':hits,'note':'Simplified fan, motor geometry; T nut and T slot, actual screw insertion trajectory, tooling, wires and fan hub unmodelled.'} (out/'validation.json').write_text(json.dumps(data,ensure_ascii=False,indent=2),encoding='utf-8');A.closeDocument(d.Name);(out/'DONE.txt').write_text('ok');os._exit(0) except Exception: (out/'ERROR.txt').write_text(traceback.format_exc(),encoding='utf-8');os._exit(1)