Files
wheel/scripts/build_70.py

173 lines
7.6 KiB
Python

"""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))