"""Create a separate copy of the conversion disk with six true M4 threads. Run with FreeCAD GUI (headless/hidden window is fine). Original files untouched. """ from pathlib import Path import traceback, math, time import FreeCAD as App, FreeCADGui as Gui, Part, Import root=Path(r'C:\projects\wheel') out=root/'models'/'conversion_disk_m4' out.mkdir(parents=True, exist_ok=True) logs=[] doc=None try: source=root/'scripts'/'build_conversion_disk.py' src=source.read_text(encoding='utf-8') src=src.replace("target = ROOT / 'models' / 'conversion_disk_parametric.FCStd'", "target = ROOT / 'models' / 'conversion_disk_m4' / '转换盘_M4.FCStd'") src=src.replace("('OuterHole','4.234 mm','Outer 6-hole diameter'),", "('OuterHole','3.3 mm','Hole sketch circle for the 6x M4 thread centers; native Hole determines final geometry'),") old=" holes_cut=body.newObject('PartDesign::Pocket',diam+'Through');holes_cut.Profile=holes;holes_cut.Length=8;holes_cut.setExpression('Length','Parameters.Thickness');verify(holes_cut)" new=""" if count==6: holes_cut=body.newObject('PartDesign::Hole','OuterM4Thread');holes_cut.Label='PCD48: 6 x M4 x 0.7, 6H through (modeled)';holes_cut.Profile=holes holes_cut.Threaded=True;holes_cut.ThreadType='ISOMetricProfile';doc.recompute() holes_cut.ThreadSize='M4x0.7';holes_cut.ThreadClass='6H';holes_cut.DepthType='ThroughAll';holes_cut.ModelThread=True;doc.recompute();verify(holes_cut) else: holes_cut=body.newObject('PartDesign::Pocket',diam+'Through');holes_cut.Profile=holes;holes_cut.Length=8;holes_cut.setExpression('Length','Parameters.Thickness');verify(holes_cut)""" assert old in src and "conversion_disk_m4' / '转换盘_M4.FCStd'" in src src=src.replace(old,new) context={'__name__':'__main__','__file__':str(source)} exec(compile(src,str(source),'exec'),context) doc=context['doc'];body=doc.getObject('Body');thread=doc.getObject('OuterM4Thread');final=doc.getObject('InnerHoleThrough') doc.recompute() assert thread.Threaded and thread.ModelThread and thread.ThreadSize=='M4x0.7' and thread.ThreadClass=='6H' assert thread.Shape.isValid() and final.Shape.isValid() and len(final.Shape.Solids)==1 logs.append('Native Hole: threaded=%s modeled=%s size=%s class=%s depth_type=%s valid=%s faces=%d' % (thread.Threaded,thread.ModelThread,thread.ThreadSize,thread.ThreadClass,thread.DepthType,thread.Shape.isValid(),len(thread.Shape.Faces))) logs.append('Final: valid=%s solids=%d volume=%.8f faces=%d' % (final.Shape.isValid(),len(final.Shape.Solids),final.Shape.Volume,len(final.Shape.Faces))) original=App.openDocument(str(root/'models'/'conversion_disk_parametric.FCStd')) reference=original.getObject('InnerHoleThrough').Shape masks=[] for i in range(6): a=math.radians(30+60*i);masks.append(Part.makeCylinder(3.2,8.2,App.Vector(24*math.cos(a),24*math.sin(a),-.1))) mask=masks[0].multiFuse(masks[1:]); mask=mask.removeSplitter() def outside_delta(a,b): delta=a.cut(b) if delta.isNull():return 0.0 try: remaining=delta.cut(mask) except ValueError as exc: if str(exc)=='Null shape':return 0.0 raise return 0.0 if remaining.isNull() else remaining.Volume delta1=outside_delta(reference,final.Shape) delta2=outside_delta(final.Shape,reference) logs.append('Outside six PCD48 masks, old minus new volume=%.9f; new minus old=%.9f mm3' % (delta1,delta2)) assert delta1<1e-5 and delta2<1e-5 App.closeDocument(original.Name) App.setActiveDocument(doc.Name) for obj in doc.Objects: if obj.ViewObject:obj.ViewObject.Visibility=False body.ViewObject.Visibility=True;final.ViewObject.Visibility=True Gui.activeDocument().activeView().viewAxonometric();Gui.activeDocument().activeView().fitAll();Gui.updateGui() Gui.activeDocument().activeView().saveImage(str(out/'转换盘_M4预览.png'),1500,1100,'White') doc.recompute();doc.saveAs(str(out/'转换盘_M4.FCStd')) final_volume=final.Shape.Volume Import.export([final],str(out/'转换盘_M4.step')) name=doc.Name;App.closeDocument(name) reopened=App.openDocument(str(out/'转换盘_M4.FCStd'));reopened.recompute() rt=reopened.getObject('OuterM4Thread');rf=reopened.getObject('InnerHoleThrough') assert rt.ModelThread and rt.Threaded and rt.ThreadSize=='M4x0.7' and rt.ThreadClass=='6H' and rf.Shape.isValid() logs.append('Reopened FCStd: modeled=%s size=%s class=%s final_valid=%s' % (rt.ModelThread,rt.ThreadSize,rt.ThreadClass,rf.Shape.isValid())) App.closeDocument(reopened.Name) stepdoc=App.newDocument('STEP_validation');Import.insert(str(out/'转换盘_M4.step'),stepdoc.Name);stepdoc.recompute() shapes=[o.Shape for o in stepdoc.Objects if hasattr(o,'Shape') and not o.Shape.isNull()] assert len(shapes)>0 and all(s.isValid() for s in shapes) logs.append('STEP objects=%d total_volume=%.8f total_faces=%d valid=True' % (len(shapes),sum(s.Volume for s in shapes),sum(len(s.Faces) for s in shapes))) assert abs(sum(s.Volume for s in shapes)-final_volume)<0.001 App.closeDocument(stepdoc.Name) (out/'validation.txt').write_text('\n'.join(logs)+'\n',encoding='utf-8') (out/'DONE.txt').write_text('ok',encoding='utf-8') except Exception: (out/'ERROR.txt').write_text(traceback.format_exc()+'\n'+'\n'.join(logs),encoding='utf-8') finally: for d in list(App.listDocuments().values()): App.closeDocument(d.Name) Gui.getMainWindow().close()