173 lines
7.6 KiB
Python
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))
|