from pathlib import Path import os, json, math, traceback import FreeCAD as App import FreeCADGui as Gui import Import, Part root = Path(r'C:\projects\wheel') out = root / 'models' / '70_m4_drill_quote' out.mkdir(parents=True, exist_ok=True) try: os.chdir(str(root)) base = (root / 'scripts' / 'build_70.py').read_text(encoding='utf-8') base = base.replace("target = os.path.join(root, 'models', '\\u0037\\u0030\\u8f6c\\u63a5\\u677f_\\u53c2\\u6570\\u5316.FCStd')", "target = str(out / '70_M4_drill.FCStd')") base = base.replace("('HoleDiameter', '4.234 mm', 'Through hole diameter (all 24 holes)')", "('HoleDiameter', '3.3 mm', 'M4 tapping pilot diameter for all 24 holes')") base = base.replace("print('OUTPUT', target)", "Import.export([final], str(out / '70_M4_drill.step'))\nGui.activeDocument().activeView().saveImage(str(out / '70_M4_drill.png'), 1600, 1200, 'White')\nprint('OUTPUT', target)") if "target = str(out / '70_M4_drill.FCStd')" not in base or "'3.3 mm'" not in base: raise RuntimeError('Failed to patch baseline; original file not modified') exec(compile(base, str(root/'scripts'/'build_70.py'), 'exec'), {'out':out, '__name__':'__main__'}) for name in list(App.listDocuments()): App.closeDocument(name) f=App.openDocument(str(out/'70_M4_drill.FCStd')); f.recompute() plate=f.getObject('FinishedPlate'); shape=plate.Shape assert shape.isValid() and len(shape.Solids)==1 assert abs(f.getObject('Hole42_0').Radius.Value-1.65)<1e-8 assert abs(f.getObject('Countersink0').Height.Value-1.75)<1e-8 step=App.newDocument('VerifyExport'); Import.insert(str(out/'70_M4_drill.step'), step.Name); step.recompute() stepShapes=[o.Shape for o in step.Objects if hasattr(o,'Shape') and not o.Shape.isNull()] assert len(stepShapes)==1 and stepShapes[0].isValid() and len(stepShapes[0].Solids)==1 cylinder_faces=[] for face in stepShapes[0].Faces: surf=face.Surface if isinstance(surf,Part.Cylinder) and abs(surf.Radius-1.65)<1e-6 and abs(abs(surf.Axis.z)-1)<1e-7: center=surf.Center cylinder_faces.append({'x':round(center.x,6),'y':round(center.y,6),'z_min':round(face.BoundBox.ZMin,6),'z_max':round(face.BoundBox.ZMax,6),'r':surf.Radius}) assert len(cylinder_faces)==24, 'Expected 24 pilot cylinders, found '+str(len(cylinder_faces)) radii=sorted(round(math.hypot(c['x'],c['y']),4) for c in cylinder_faces) assert radii == [21.0]*6+[24.0]*6+[25.4]*6+[35.0]*6, str(radii) pcd42=[c for c in cylinder_faces if abs(math.hypot(c['x'],c['y'])-21)<0.001] assert all(abs(c['z_min']-1.75)<1e-6 and abs(c['z_max']-8)<1e-6 for c in pcd42) assert all(abs(c['z_min'])<1e-6 and abs(c['z_max']-8)<1e-6 for c in cylinder_faces if c not in pcd42) result={'fcstd_valid':True,'fcstd_solids':1,'step_valid':True,'step_solids':1,'step_volume':stepShapes[0].Volume,'pilot_diameter_mm':3.3,'number_pilot_cylinders':len(cylinder_faces),'positions_and_extents':cylinder_faces,'countersink_diameter_mm':6.8,'countersink_angle_deg':90,'countersink_depth_mm':1.75,'pcd42_straight_segment_mm':6.25,'other_straight_segment_mm':8.0,'interpretation':'These are smooth pilot holes, not semantic or geometric threads. Automatic quotation recognition untested.'} (out/'validation.json').write_text(json.dumps(result,ensure_ascii=False,indent=2),encoding='utf-8') (out/'status.txt').write_text('OK',encoding='utf-8') except: (out/'status.txt').write_text(traceback.format_exc(),encoding='utf-8') for name in list(App.listDocuments()): App.closeDocument(name) Gui.getMainWindow().close()