Files
wheel/scripts/build_shop_sprues.py

111 lines
5.1 KiB
Python

"""Create two genuinely connected eight-washer sprues for shop review.
Run with the FreeCAD bundled Python. The original single-part files are read
only; all lengths and placements below are in millimetres.
"""
from pathlib import Path
import hashlib
import json
import math
import shutil
import zipfile
import FreeCAD as App
import Part
import Mesh
import MeshPart
ROOT = Path(__file__).resolve().parent.parent
SOURCE = ROOT / 'models' / 'pcd50_open_pillar_v1'
OUT = ROOT / 'models' / 'print_shop_joined_eight'
OUT.mkdir(parents=True, exist_ok=True)
V = App.Vector
# STEP washer geometry is centered on (10,10) in these exported files. These
# coordinates are explicit because imported STEP and STL placements differ.
def centered_shape(name):
shape = Part.read(str(SOURCE / name))
if shape.isNull() or not shape.isValid() or len(shape.Solids) != 1:
raise RuntimeError('Invalid source shape ' + name)
shape.translate(V(-10,-10,0))
return shape
def make_sprue(step_name, doc_name, stem, expected_t):
source = centered_shape(step_name)
if abs(source.BoundBox.ZLength-expected_t) > 1e-5:
raise RuntimeError('Unexpected washer thickness')
pads=[]
# Left column is rotated so both inward edges are solid. Two columns and
# four rows give eight washers with seven straight connecting bars.
for col in (0, 1):
for row in range(4):
pad=source.copy()
if col == 0:
pad.rotate(V(),V(0,0,1),180)
pad.translate(V(24*col,16*row,0))
pads.append(pad)
# Each bar is constant 1.8 x 1.8 mm. Ends overlap the outer edge by
# 0.8 mm on the centerline. The root is sacrificial and needs careful
# trimming and smoothing after printing.
rods=[]
for x in (0,24):
for row in range(3):
rods.append(Part.makeBox(1.8,5.6,1.8,V(x-.9,16*row+5.2,0)))
rods.append(Part.makeBox(13.6,1.8,1.8,V(5.2,-.9,0)))
# Quantify the overlap of each end with its two washers. The rod body is
# never necked down, but its contact height is limited by the washer.
overlaps=[]
for col in (0,1):
for row in range(3):
rod=rods[col*3+row]
overlaps.append([rod.common(pads[col*4+row]).Volume,
rod.common(pads[col*4+row+1]).Volume])
overlaps.append([rods[6].common(pads[0]).Volume,
rods[6].common(pads[4]).Volume])
if any(min(pair) < 0.5 for pair in overlaps):
raise RuntimeError('Insufficient rod/pad intersection')
combined=pads[0]
# All parts touch through each rod, but may not be sequentially connected.
# MultiFuse avoids relying on order of the graph during the operation.
combined = pads[0].multiFuse(pads[1:]+rods).removeSplitter()
if combined.isNull() or not combined.isValid() or len(combined.Solids)!=1:
raise RuntimeError('Sprue is not one valid connected solid: '+stem)
# Confirm every washer's complete original geometry is retained.
for pad in pads:
if pad.cut(combined).Volume > 1e-5:
raise RuntimeError('Source washer material was removed')
bounds=combined.BoundBox
doc=App.newDocument(doc_name)
o=doc.addObject('Part::Feature','ConnectedSprue');o.Label=stem;o.Shape=combined
doc.recompute()
doc.saveAs(str(OUT/(stem+'.FCStd')))
Part.export([o],str(OUT/(stem+'.step')))
mesh=MeshPart.meshFromShape(Shape=combined,LinearDeflection=0.03,
AngularDeflection=0.2,Relative=False)
mesh.write(str(OUT/(stem+'.stl')))
App.closeDocument(doc.Name)
check=Mesh.Mesh(str(OUT/(stem+'.stl')))
components=check.getSeparateComponents()
if len(components)!=1 or not components[0].isSolid():
raise RuntimeError('STL is not one closed connected mesh')
return {'file':stem+'.stl','washer_quantity':8,'nominal_washer_thickness_mm':expected_t,
'rod_count':len(rods),'rod_width_mm':1.8,'rod_height_mm':1.8,
'rod_centerline_radial_overlap_mm':0.8,
'rod_end_intersection_volumes_mm3': [[round(v,5) for v in pair] for pair in overlaps],
'closed_components':len(components),'closed':True,
'bounds_mm':[bounds.XMin,bounds.XMax,bounds.YMin,bounds.YMax,bounds.ZMin,bounds.ZMax],
'nominal_washer_volume_mm3':round(source.Volume,5)}
result=[]
result.append(make_sprue('Shim05.step','WasherSprue05','垫片0.5mm_8个拼件',.5))
result.append(make_sprue('Shim10.step','WasherSprue10','垫片1.0mm_8个拼件',1.0))
source=SOURCE/'EncoderBracket.stl'; target=OUT/'编码器支架_单件.stl'
shutil.copyfile(source,target)
digest=hashlib.sha256(source.read_bytes()).hexdigest()
if hashlib.sha256(target.read_bytes()).hexdigest()!=digest: raise RuntimeError('Bracket copy mismatch')
m=Mesh.Mesh(str(target))
if len(m.getSeparateComponents())!=1 or not m.isSolid(): raise RuntimeError('Invalid bracket mesh')
result.insert(0,{'file':target.name,'quantity':1,'closed_components':1,'sha256':digest})
(OUT/'几何检查.json').write_text(json.dumps(result,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
print(json.dumps(result,ensure_ascii=False,indent=2))