# Rebuild through rebuild_conversion_disk.FCMacro in FreeCAD's GUI to save the # same visibility and view as the delivered file. Running this .py directly # with FreeCADCmd also builds the geometry, but has no GUI view to save. # Rebuilding overwrites models/conversion_disk_parametric.FCStd. import FreeCAD as App import Part, Sketcher, math, pathlib ROOT = pathlib.Path(__file__).resolve().parent.parent source = ROOT / 'models' / '转换盘1.step' target = ROOT / 'models' / 'conversion_disk_parametric.FCStd' doc = App.newDocument('ConversionDisk') params = doc.addObject('Spreadsheet::Sheet', 'Parameters') params.Label = 'Parameters (mm)' rows = [ ('Thickness','8 mm','Plate thickness'), ('OuterPCD','48 mm','Outer 6-hole pitch circle diameter'), ('OuterHole','4.234 mm','Outer 6-hole diameter'), ('CenterHole','15 mm','Center through hole diameter'), ('InnerPCD','20 mm','Inner 8-hole pitch circle diameter'), ('InnerHole','3 mm','Inner 8-hole diameter'), ('RecessDiameter','28 mm','Bottom recess diameter'), ('RecessDepth','2 mm','Bottom recess depth'), ('OutlineScale','1','Overall outline scale relative to outer PCD'), ('EdgeChamfer','1.5 mm','Outer edges chamfer'), ('OuterAngle','30 deg','Outer 6-hole start angle'), ('InnerAngle','0 deg','Inner 8-hole start angle'), ] params.set('A1','Parameter'); params.set('B1','Value'); params.set('C1','Description') for i,(key,val,desc) in enumerate(rows,2): params.set('A%d'%i,key);params.set('B%d'%i,val);params.set('C%d'%i,desc);params.setAlias('B%d'%i,key) doc.recompute() reference=doc.addObject('PartDesign::Feature','OriginalReference') reference.Label='Original STEP reference' reference.Shape=Part.read(str(source)) body=doc.addObject('PartDesign::Body','Body') body.Label='Conversion disk' def sketch(name, z=None): s=doc.addObject('Sketcher::SketchObject',name);body.addObject(s) if z is not None: s.Placement.Base.z=z s.setExpression('Placement.Base.z','Parameters.Thickness') return s def circle(s,x,y,r,exprx=None,expry=None,exprr=None): k=s.addGeometry(Part.Circle(App.Vector(x,y,0),App.Vector(0,0,1),r),False) if abs(x)<1e-8 and abs(y)<1e-8: s.addConstraint(Sketcher.Constraint('Coincident',k,3,-1,1)) else: if abs(x)<1e-8: s.addConstraint(Sketcher.Constraint('PointOnObject',k,3,-2)) else: ci=s.addConstraint(Sketcher.Constraint('DistanceX',k,3,x)) if exprx: s.setExpression('Constraints[%d]'%ci,exprx) if abs(y)<1e-8: s.addConstraint(Sketcher.Constraint('PointOnObject',k,3,-1)) else: ci=s.addConstraint(Sketcher.Constraint('DistanceY',k,3,y)) if expry: s.setExpression('Constraints[%d]'%ci,expry) ci=s.addConstraint(Sketcher.Constraint('Radius',k,r)) if exprr:s.setExpression('Constraints[%d]'%ci,exprr) def verify(obj): doc.recompute() print('CHECK',obj.Name, 'shape', obj.Shape.isNull(), 'valid', False if obj.Shape.isNull() else obj.Shape.isValid(), 'status',obj.State) if obj.Shape.isNull() or not obj.Shape.isValid() or 'Invalid' in obj.State:raise RuntimeError('Invalid feature '+obj.Name) # Preserve the true STEP mid-plane perimeter. A native link scales the whole # profile in XY and rotates it with the outer hole group; the individual arcs # are fixed source geometry rather than independently editable constraints. wire=max(reference.Shape.slice(App.Vector(0,0,1),4),key=lambda w:w.Length) face=Part.Face(wire);face.translate(App.Vector(0,0,-4)) profile=doc.addObject('Part::Feature','OriginalOutline') profile.Label='Original outline at mid-thickness (reference geometry)';profile.Shape=face scaled=doc.addObject('App::Link','ScaledOutline');scaled.Label='Original outline - scaled and rotated' scaled.setLink(profile);scaled.LinkTransform=True scaled.setExpression('ScaleVector.x','Parameters.OuterPCD / 48 mm * Parameters.OutlineScale') scaled.setExpression('ScaleVector.y','Parameters.OuterPCD / 48 mm * Parameters.OutlineScale') scaled.setExpression('Placement.Rotation.Angle','Parameters.OuterAngle - 30 deg') outline=doc.addObject('Part::Extrusion','Outline');outline.Label='Original profile extruded' outline.Base=scaled;outline.DirMode='Custom';outline.Dir=App.Vector(0,0,1);outline.LengthFwd=8;outline.Solid=True outline.setExpression('LengthFwd','Parameters.Thickness');verify(outline) body.BaseFeature=outline;doc.recompute() edges=[] for i,e in enumerate(outline.Shape.Edges,1): b=e.BoundBox if abs(b.ZMax-b.ZMin)<1e-7 and (abs(b.ZMin)<1e-7 or abs(b.ZMin-8)<1e-7):edges.append('Edge%d'%i) print('CHAMFER edges',edges) ch=body.newObject('PartDesign::Chamfer','PerimeterChamfer');ch.Base=(ch.BaseFeature,edges);ch.Size=1.5;ch.setExpression('Size','Parameters.EdgeChamfer');verify(ch) recesssk=sketch('RecessProfile');circle(recesssk,0,0,14,exprr='Parameters.RecessDiameter / 2');doc.recompute() recess=body.newObject('PartDesign::Pocket','BottomRecess');recess.Profile=recesssk;recess.Length=2;recess.Reversed=True;recess.setExpression('Length','Parameters.RecessDepth');verify(recess) center=sketch('CenterProfile',8);center.Label='Center through hole' circle(center,0,0,7.5,exprr='Parameters.CenterHole / 2');doc.recompute() center_cut=body.newObject('PartDesign::Pocket','CenterHole');center_cut.Profile=center;center_cut.Length=8;center_cut.setExpression('Length','Parameters.Thickness');verify(center_cut) for count,pcd,r,diam,angle in [(6,'OuterPCD',2.117,'OuterHole','OuterAngle'),(8,'InnerPCD',1.5,'InnerHole','InnerAngle')]: holes=sketch(diam+'Profile',8);holes.Label=diam+' pattern' holes.setExpression('Placement.Rotation.Angle','Parameters.'+angle) for i in range(count): a=math.radians(i*360/count);c=math.cos(a);s=math.sin(a);d=24 if count==6 else 10 circle(holes,d*c,d*s,r,'(%s) * Parameters.%s / 2'%(c,pcd),'(%s) * Parameters.%s / 2'%(s,pcd),'Parameters.%s / 2'%diam) doc.recompute() 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) print('FINAL volume',holes_cut.Shape.Volume,'bounds',holes_cut.Shape.BoundBox) doc.recompute() if App.GuiUp: import FreeCADGui as Gui for obj in doc.Objects: if obj.ViewObject: obj.ViewObject.Visibility = False body.ViewObject.Visibility = True holes_cut.ViewObject.Visibility = True Gui.activeDocument().activeView().viewAxonometric() Gui.activeDocument().activeView().fitAll() doc.saveAs(str(target)) print('SAVED',str(target))