"""Build the native FreeCAD feature model for 70转接板.step. Run from the FreeCAD GUI Python console, not FreeCADCmd. Set the project directory first, then execute this script, for example: import os os.chdir(r'C:\projects\wheel') exec(open(r'scripts\build_70.py', encoding='utf-8').read()) GUI execution is required to persist the visible object and camera state. The generated document uses only built-in Part and Spreadsheet objects and expressions; no script is needed to open or edit the generated model. """ import math import os import FreeCAD as App import Part import Import if not App.GuiUp: raise RuntimeError('Run this script from the FreeCAD GUI Python console; ' 'FreeCADCmd cannot persist the required visibility state.') import FreeCADGui as Gui root = os.getcwd() source = os.path.join(root, 'models', '\u0037\u0030\u8f6c\u63a5\u677f.step') target = os.path.join(root, 'models', '\u0037\u0030\u8f6c\u63a5\u677f_\u53c2\u6570\u5316.FCStd') doc = App.newDocument('Adapter70') sheet = doc.addObject('Spreadsheet::Sheet', 'Parameters') sheet.Label = 'Parameters (mm, degrees)' rows = [ ('Thickness', '8 mm', 'Plate thickness'), ('CenterDiameter', '32.5 mm', 'Center through hole diameter'), ('HoleDiameter', '4.234 mm', 'Through hole diameter (all 24 holes)'), ('PCD42', '42 mm', 'Countersunk holes pitch circle diameter'), ('Angle42', '45 deg', 'Countersunk holes first angle'), ('PCD48', '48 mm', 'Six holes pitch circle diameter'), ('Angle48', '0 deg', 'PCD48 first angle'), ('PCD508', '50.8 mm', 'Six holes pitch circle diameter'), ('Angle508', '30 deg', 'PCD50.8 first angle'), ('PCD70', '70 mm', 'Outer six holes pitch circle diameter'), ('Angle70', '30 deg', 'Outer six holes first angle'), ('OutlineScale', '1', 'Overall outline scale relative to PCD70'), ('CountersinkDiameter', '6.8 mm', 'Countersink at bottom face'), ] sheet.set('A1', 'Parameter'); sheet.set('B1', 'Value'); sheet.set('C1', 'Meaning') for i, (alias, val, desc) in enumerate(rows, 2): sheet.set('A%d' % i, alias); sheet.set('B%d' % i, val) sheet.set('C%d' % i, desc); sheet.setAlias('B%d' % i, alias) sheet.set('A16', 'CountersinkDepth'); sheet.set('B16', '= (B14 - B4) / 2') sheet.set('C16', '90 degree included angle, depth derived from diameters') sheet.setAlias('B16', 'CountersinkDepth') sheet.set('A17', 'OuterChamfer'); sheet.set('B17', '1 mm') sheet.set('C17', 'Equal-distance chamfer on both outer edges') sheet.setAlias('B17', 'OuterChamfer') doc.recompute() def setexpr(obj, prop, formula): obj.setExpression(prop, formula) def cylinder(name, label, radius, height, pcd=None, angle=None, index=0): obj = doc.addObject('Part::Cylinder', name) obj.Label = label setexpr(obj, 'Radius', radius) setexpr(obj, 'Height', height) if pcd: phase = 'Parameters.%s + %d deg' % (angle, 60 * index) setexpr(obj, 'Placement.Base.x', 'Parameters.%s / 2 * cos(%s)' % (pcd, phase)) setexpr(obj, 'Placement.Base.y', 'Parameters.%s / 2 * sin(%s)' % (pcd, phase)) return obj original = Part.read(source) outer_wire = max(original.slice(App.Vector(0, 0, 1), 4), key=lambda wire: wire.Length) original_face = Part.Face(outer_wire) original_face.translate(App.Vector(0, 0, -4)) profile = doc.addObject('Part::Feature', 'OriginalOutline') profile.Label = 'Original outline at mid-thickness (reference geometry)' profile.Shape = original_face # A built-in link uniformly scales the original plan view in XY, without # scaling the extrusion height; the local curve dimensions are not editable. scaled = doc.addObject('App::Link', 'ScaledOutline') scaled.Label = 'Original outline - scaled and rotated' scaled.setLink(profile) scaled.LinkTransform = True setexpr(scaled, 'ScaleVector.x', 'Parameters.PCD70 / 70 mm * Parameters.OutlineScale') setexpr(scaled, 'ScaleVector.y', 'Parameters.PCD70 / 70 mm * Parameters.OutlineScale') setexpr(scaled, 'Placement.Rotation.Angle', 'Parameters.Angle70 - 30 deg') outline = doc.addObject('Part::Extrusion', 'Outline') outline.Label = 'Outline - original profile extruded' outline.Base = scaled outline.DirMode = 'Custom' outline.Dir = App.Vector(0, 0, 1) outline.LengthFwd = 8 outline.Solid = True setexpr(outline, 'LengthFwd', 'Parameters.Thickness') doc.recompute() assert not outline.Shape.isNull() and outline.Shape.isValid() and len(outline.Shape.Solids) == 1 # Import the parametric outline into a Body as a linked base feature. Its # PartDesign chamfer has a single native size that accepts an expression. edge_ids = [] for n, edge in enumerate(outline.Shape.Edges, 1): z0, z1 = edge.BoundBox.ZMin, edge.BoundBox.ZMax if abs(z0-z1) < 1e-7 and (abs(z0) < 1e-7 or abs(z0-8) < 1e-7): edge_ids.append(n) body = doc.addObject('PartDesign::Body', 'OutlineBody') body.Label = 'Outline with chamfer' body.BaseFeature = outline doc.recompute() chamfer = body.newObject('PartDesign::Chamfer', 'OuterChamfer') chamfer.Label = 'Outer chamfer (1 mm)' chamfer.Base = (chamfer.BaseFeature, ['Edge%d' % i for i in edge_ids]) setexpr(chamfer, 'Size', 'Parameters.OuterChamfer') doc.recompute() assert not chamfer.Shape.isNull() and chamfer.Shape.isValid(), 'outer chamfer failed' center = cylinder('CenterBore', 'Center through hole', 'Parameters.CenterDiameter / 2', 'Parameters.Thickness') current = doc.addObject('Part::Cut', 'CenterCut') current.Label = 'Center hole' current.Base, current.Tool = body, center doc.recompute() for name, pcd, angle in [('42', 'PCD42', 'Angle42'), ('48', 'PCD48', 'Angle48'), ('508', 'PCD508', 'Angle508'), ('70', 'PCD70', 'Angle70')]: holes = [cylinder('Hole%s_%d' % (name, i), 'PCD %s hole %d' % (name, i+1), 'Parameters.HoleDiameter / 2', 'Parameters.Thickness', pcd, angle, i) for i in range(6)] group = doc.addObject('Part::MultiFuse', 'Bores%s' % name) group.Label = 'PCD %s - six bores' % name group.Shapes = holes cut = doc.addObject('Part::Cut', 'Cut%s' % name) cut.Label = 'PCD %s holes' % name cut.Base, cut.Tool = current, group current = cut doc.recompute() cones = [] for i in range(6): cone = doc.addObject('Part::Cone', 'Countersink%d' % i) cone.Label = 'Bottom countersink %d' % (i+1) setexpr(cone, 'Radius1', 'Parameters.CountersinkDiameter / 2') setexpr(cone, 'Radius2', 'Parameters.HoleDiameter / 2') setexpr(cone, 'Height', 'Parameters.CountersinkDepth') phase = 'Parameters.Angle42 + %d deg' % (i*60) setexpr(cone, 'Placement.Base.x', 'Parameters.PCD42 / 2 * cos(%s)' % phase) setexpr(cone, 'Placement.Base.y', 'Parameters.PCD42 / 2 * sin(%s)' % phase) cones.append(cone) sink_group = doc.addObject('Part::MultiFuse', 'Countersinks') sink_group.Label = 'PCD 42 - six bottom countersinks' sink_group.Shapes = cones final = doc.addObject('Part::Cut', 'FinishedPlate') final.Label = '70 adapter - finished plate' final.Base, final.Tool = current, sink_group doc.recompute() assert not final.Shape.isNull() and final.Shape.isValid() and len(final.Shape.Solids) == 1, 'invalid result' prior = set(doc.Objects) Import.insert(source, doc.Name) reference = list(set(doc.Objects) - prior) assert len(reference) == 1 reference[0].Label = 'Original STEP - reference (hidden)' doc.recompute() for obj in doc.Objects: if obj.ViewObject: obj.ViewObject.Visibility = False final.ViewObject.Visibility = True doc.recompute() Gui.activeDocument().activeView().viewAxonometric() Gui.activeDocument().activeView().fitAll() doc.saveAs(target) print('OUTPUT', target) print('VALID', final.Shape.isValid(), 'VOLUME', final.Shape.Volume, 'FACES', len(final.Shape.Faces))