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_145' 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')] 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.0 * math.sqrt(2.0)))) - 45.0 corners_before = [d.getObject('MotorBaseCorner%d' % i).Shape.copy() for i in range(2)] old_rails = [o.Shape.copy() for o in rails] for i, o in enumerate(rails): o.Placement = A.Placement() o.Shape = Part.makeBox(20, 145, 20, V(xs[i] - 10, -55, 0)) o.Label = '2020 motor profile - 145 mm' d.recompute() src = 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): src = src.cut(Part.makeCylinder(2.2, 25.04, V(x,y,19.98))) fan = src.removeSplitter() fan.rotate(V(0,0,0), V(0,0,1), theta) fan.translate(V(0,28,0)) assert fan.isValid() and len(fan.Solids) == 1 original = [o for o in d.Objects if hasattr(o,'Shape') and not o.Shape.isNull() and o.ViewObject.Visibility] fan_hits = [] for o in original: if fan.BoundBox.intersect(o.Shape.BoundBox): volume = fan.common(o.Shape).Volume if volume > 1e-4: fan_hits.append([o.Name, volume]) assert not fan_hits, 'fan collisions: ' + str(fan_hits) extra_hits = [] for i, rail in enumerate(rails): extra = rail.Shape.cut(old_rails[i]) for o in original: if o == rail or o in rails: continue if extra.BoundBox.intersect(o.Shape.BoundBox): volume = extra.common(o.Shape).Volume if volume > 1e-4: extra_hits.append([rail.Name, o.Name, volume]) assert old_rails[i].cut(rail.Shape).Volume < 1e-5 assert not extra_hits, 'rail extension collisions: ' + str(extra_hits) for i in range(2): assert corners_before[i].cut(d.getObject('MotorBaseCorner%d' % i).Shape).Volume < 1e-7 assert d.getObject('MotorBaseCorner%d' % i).Shape.cut(corners_before[i]).Volume < 1e-7 o = d.addObject('PartDesign::Feature', 'DirectFan') o.Label = '120 mm fan envelope - rotated, two diagonal fixings; user fan assumed' o.Shape = fan o.ViewObject.ShapeColor = (.22,.24,.27) o.ViewObject.Transparency = 18 alpha = math.radians(theta) fasteners=[] holes=[] for sign in (-1,1): px = sign * 52.5 * (math.cos(alpha) - math.sin(alpha)) py = 28 + sign * 52.5 * (math.cos(alpha) + math.sin(alpha)) idx=0 if sign<0 else 1 assert abs(px-xs[idx]) < 1e-6 assert -55 < py < 90 shaft=Part.makeCylinder(2,25,V(px,py,20)) head=Part.makeCylinder(3.5,3,V(px,py,45)) screw=shaft.fuse(head).removeSplitter() assert screw.common(fan).Volume < 1e-5 f=d.addObject('PartDesign::Feature','FanBolt'+str(idx)) f.Shape=screw; f.Label='Illustrative M4 fastener, nut not modeled'; f.ViewObject.ShapeColor=(.65,.66,.68) fasteners.append(f) holes.append({'x':px,'y':py,'rail_center_x':xs[idx], 'distance_to_end_mm': min(py+55,90-py)}) d.recompute() for obj in [o]+fasteners: assert not obj.Shape.isNull() and obj.Shape.isValid() d.Label='120 mm fan on 145 mm profiles - 2 diagonal screws, no bracket' d.saveAs(str(out/'Assembly.FCStd')) visible=[a for a in d.Objects if hasattr(a,'Shape') and not a.Shape.isNull() and a.ViewObject.Visibility] Import.export(visible, 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={a.Name:a.ViewObject.Visibility for a in d.Objects if hasattr(a,'Shape')} names={'Rail0','Rail1','NewRail0','NewRail1','MotorBaseCorner0','MotorBaseCorner1','DirectFan','FanBolt0','FanBolt1','Motor','OpenPillarBracket','Holder','Magnet','PCB','Chip'} for a in d.Objects: if hasattr(a,'Shape'):a.ViewObject.Visibility=a.Name in names 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 a in d.Objects: if hasattr(a,'Shape'):a.ViewObject.Visibility=vis.get(a.Name,False) d.recompute();d.saveAs(str(out/'Assembly.FCStd')) expected=[a.Name for a in visible] A.closeDocument(d.Name) d2=A.openDocument(str(out/'Assembly.FCStd'));d2.recompute() assert all(d2.getObject(x) and d2.getObject(x).Shape.isValid() for x in ['Rail0','Rail1','DirectFan','FanBolt0','FanBolt1']) assert all(abs(d2.getObject('Rail%d'%i).Shape.BoundBox.YLength-145)<1e-7 for i in (0,1)) st=Part.read(str(out/'Assembly.step'));assert st.isValid() and len(st.Solids)>0 data={'motor_2020_length_mm':145,'2020_longitudinal_limits_mm':[-55,90], 'fan_envelope_mm':[120,120,25], 'fan_mount_pitch_mm':105,'fan_center_y_mm':28,'motor_center_y_mm':10,'fan_offset_from_motor_mm':18,'rotation_deg':theta,'fastener_centers':holes,'fan_collisions':fan_hits,'extension_collisions':extra_hits,'original_corners_unchanged':True,'fcstd_reopened':True,'step_valid':True,'assumptions':'Fan solid frame and holes schematic; no blade, hub, wires, real T-slot or nut. Shaft of screw only above extrusion surface.'} (out/'validation.json').write_text(json.dumps(data,ensure_ascii=False,indent=2),encoding='utf8') A.closeDocument(d2.Name) (out/'DONE.txt').write_text('ok',encoding='utf8') os._exit(0) except Exception: (out/'ERROR.txt').write_text(traceback.format_exc(),encoding='utf8') os._exit(1)