"""Build the approximate, editable MAD M9C12 model in FreeCAD. Run via rebuild_mad_m9c12.FCMacro in the FreeCAD GUI. The model and STEP file are overwritten. See models/MAD_M9C12_说明.md for measurement limits. """ import math from pathlib import Path import FreeCAD as App import Part import Import ROOT = Path(__file__).resolve().parent.parent MODEL = ROOT / 'models' / 'MAD_M9C12.FCStd' STEP = ROOT / 'models' / 'MAD_M9C12.step' if not App.GuiUp: raise RuntimeError('Run through the GUI macro to save visibility and camera state') import FreeCADGui as Gui doc = App.newDocument('MAD_M9C12') sheet = doc.addObject('Spreadsheet::Sheet', 'Parameters') sheet.Label = 'Parameters - mm and degrees (verify assumptions)' sheet.set('A1', 'Parameter'); sheet.set('B1', 'Value'); sheet.set('C1', 'Source / status') rows = [ ('BodyDiameter', '120 mm', 'User measurement: body outside diameter'), ('BodyThickness', '30 mm', 'User measurement: body thickness; excludes boss and shaft'), ('FrontPCD', '20 mm', 'User confirmed adapter inner alternating holes; verify with measurements'), ('FrontHoleDiameter', '3 mm', 'Assumed M3 nominal diameter; verify'), ('FrontHoleDepth', '5 mm', 'Placeholder blind depth; verify'), ('FrontAngle', '0 deg', 'Assumed first hole phase; verify'), ('RearInnerPCD', '50 mm', 'User ruler measurement; verify with calipers'), ('RearOuterPCD', '76 mm', 'User ruler measurement; verify with calipers'), ('RearHoleDiameter', '4 mm', 'M4 nominal diameter, no modeled thread'), ('RearHoleDepth', '8 mm', 'Placeholder; M4x8 does not establish thread depth'), ('RearInnerAngle', '45 deg', 'Assumed phase; verify'), ('RearOuterAngle', '45 deg', 'Assumed relative phase; verify'), ('BossDiameter', '26 mm', 'Visual approximation; front mounting holes enter its top face'), ('BossHeight', '8 mm', 'Visual approximation, verify adapter clearance'), ('ShaftDiameter', '8 mm', 'Visual approximation'), ('ShaftHeight', '6 mm', 'Visual approximation, measured above boss'), ('CenterHoleDiameter', '3 mm', 'Axial through hole; smooth nominal M3 diameter'), ] for i, (name, value, meaning) in enumerate(rows, 2): sheet.set('A%d' % i, name); sheet.set('B%d' % i, value) sheet.set('C%d' % i, meaning); sheet.setAlias('B%d' % i, name) doc.recompute() def cylinder(name, label, radius, height, z, x=None, y=None): obj = doc.addObject('Part::Cylinder', name) obj.Label = label obj.setExpression('Radius', radius) obj.setExpression('Height', height) obj.setExpression('Placement.Base.z', z) if x is not None: obj.setExpression('Placement.Base.x', x) obj.setExpression('Placement.Base.y', y) return obj def pattern(name, pcd, angle, diameter, depth, z): holes = [] for i in range(4): phase = 'Parameters.%s + %d deg' % (angle, i * 90) x = 'Parameters.%s / 2 * cos(%s)' % (pcd, phase) y = 'Parameters.%s / 2 * sin(%s)' % (pcd, phase) holes.append(cylinder(name + str(i + 1), name + ' hole ' + str(i + 1), 'Parameters.%s / 2' % diameter, 'Parameters.%s' % depth, z, x, y)) group = doc.addObject('Part::Compound', name + 'Tools') group.Label = name + ' (four hole tools)' group.Links = holes return group def checked(obj): doc.recompute() if obj.Shape.isNull() or not obj.Shape.isValid() or len(obj.Shape.Solids) != 1: raise RuntimeError('Invalid geometry: ' + obj.Name) body = cylinder('MotorBody', 'Motor housing - approximate', 'Parameters.BodyDiameter / 2', 'Parameters.BodyThickness', '0 mm') boss = cylinder('Boss', 'Front boss - approximate', 'Parameters.BossDiameter / 2', 'Parameters.BossHeight', 'Parameters.BodyThickness') shaft = cylinder('Shaft', 'Shaft - approximate', 'Parameters.ShaftDiameter / 2', 'Parameters.ShaftHeight', 'Parameters.BodyThickness + Parameters.BossHeight') housing = doc.addObject('Part::MultiFuse', 'Housing') housing.Label = 'Housing, boss and shaft' housing.Shapes = [body, boss, shaft] checked(housing) front = pattern('Front', 'FrontPCD', 'FrontAngle', 'FrontHoleDiameter', 'FrontHoleDepth', 'Parameters.BodyThickness + Parameters.BossHeight - Parameters.FrontHoleDepth') cut_front = doc.addObject('Part::Cut', 'FrontHoles') cut_front.Label = 'Front 4 holes - phase and depth to verify' cut_front.Base = housing; cut_front.Tool = front checked(cut_front) rear_inner = pattern('RearInner', 'RearInnerPCD', 'RearInnerAngle', 'RearHoleDiameter', 'RearHoleDepth', '0 mm') cut_inner = doc.addObject('Part::Cut', 'RearInnerHoles') cut_inner.Label = 'Rear inner 4 holes - measured approximately' cut_inner.Base = cut_front; cut_inner.Tool = rear_inner checked(cut_inner) rear_outer = pattern('RearOuter', 'RearOuterPCD', 'RearOuterAngle', 'RearHoleDiameter', 'RearHoleDepth', '0 mm') final = doc.addObject('Part::Cut', 'Motor') final.Label = 'MAD M9C12 - approximate motor' final.Base = cut_inner; final.Tool = rear_outer checked(final) center_bore = cylinder('CenterBore', 'Center bore tool', 'Parameters.CenterHoleDiameter / 2', 'Parameters.BodyThickness + Parameters.BossHeight + Parameters.ShaftHeight', '0 mm') center_cut = doc.addObject('Part::Cut', 'CenterThrough') center_cut.Label = 'MAD M9C12 - center through hole' center_cut.Base, center_cut.Tool = final, center_bore checked(center_cut) final = center_cut for obj in doc.Objects: if obj.ViewObject: obj.ViewObject.Visibility = False final.ViewObject.Visibility = True final.ViewObject.ShapeColor = (0.47, 0.50, 0.53) final.ViewObject.Deviation = 0.05 final.ViewObject.AngularDeflection = 10 from pivy import coin view = Gui.activeDocument().activeView() # Set the camera directly so a hidden GUI also saves the intended view. view.getCameraNode().orientation.setValue(coin.SbRotation(0.364705, 0.279848, 0.115917, 0.880476)) view.fitAll() doc.recompute() Gui.updateGui() view.saveImage(str(ROOT / 'models' / 'MAD_M9C12_预览.png'), 1400, 1000, 'White') doc.saveAs(str(MODEL)) Import.export([final], str(STEP)) print('MODEL_OK', MODEL, 'STEP', STEP, 'volume', final.Shape.Volume, 'bounds', final.Shape.BoundBox)