建立方向盘项目初始版本

This commit is contained in:
2026-10-29 22:04:55 +08:00
commit 9962cbfbce
834 changed files with 1519203 additions and 0 deletions

View File

@@ -0,0 +1,135 @@
"""Place the bracket and both sets of shims on one print plate unchanged.
Run with FreeCAD's Python, for example:
& 'C:\\Users\\billy\\scoop\\apps\\freecad\\current\\bin\\python.exe' scripts/build_pcd50_full_print_plate.py
The plate is one bracket, eight 0.5 mm shims, and eight 1 mm shims.
Output is independent of the input files.
"""
from pathlib import Path
import json
import itertools
import math
import FreeCAD as App
import Mesh
ROOT = Path(__file__).resolve().parent.parent
SOURCE = ROOT / 'models' / 'pcd50_open_pillar_v1'
OUT = ROOT / 'models' / 'pcd50_full_print_plate'
OUT.mkdir(parents=True, exist_ok=True)
V = App.Vector
def load(name):
mesh = Mesh.Mesh(str(SOURCE / name))
if len(mesh.getSeparateComponents()) != 1 or not mesh.isSolid():
raise RuntimeError('Input is not one closed component: ' + name)
return mesh
bracket = load('EncoderBracket.stl')
shim05 = load('Shim05.stl')
shim10 = load('Shim10.stl')
# Two visually distinct 2x4 groups below the bracket. All nominal XY
# center distances are 20 mm, except for the 40 mm gap between groups.
# Every placement uses the original tessellation; no rotations or rescaling.
row05 = [(x, y) for y in (6.0,26.0,46.0,66.0) for x in (6.0,26.0)]
row10 = [(x, y) for y in (6.0,26.0,46.0,66.0) for x in (66.0,86.0)]
bracket_pos = (46.0, 109.5)
def moved(mesh, x, y):
result = mesh.copy()
result.translate(x, y, -mesh.BoundBox.ZMin)
return result
def bounds(m):
b = m.BoundBox
return [b.XMin, b.XMax, b.YMin, b.YMax, b.ZMin, b.ZMax]
def distance_xy(a, b):
a, b = a.BoundBox, b.BoundBox
dx = max(a.XMin - b.XMax, b.XMin - a.XMax, 0.0)
dy = max(a.YMin - b.YMax, b.YMin - a.YMax, 0.0)
return math.hypot(dx, dy)
def equivalent(a, b):
"""Compare vertices and facet topology after removing translation.
Float STL coordinates round slightly when translated/reloaded. A small
tolerance is therefore used rather than comparing quantized text.
"""
if a.CountFacets != b.CountFacets:
return False
pa, fa = a.Topology; pb, fb = b.Topology
if len(pa) != len(pb):
return False
ba, bb = a.BoundBox, b.BoundBox
def norm(p, box): return (p.x-box.XMin,p.y-box.YMin,p.z-box.ZMin)
qa=[norm(p,ba) for p in pa]; qb=[norm(p,bb) for p in pb]
tol=0.0001
def key(v): return tuple(math.floor(x/tol) for x in v)
bins={}
for i,v in enumerate(qa):bins.setdefault(key(v),[]).append(i)
matches=[]
for v in qb:
k=key(v); candidates=[]
for offset in itertools.product((-1,0,1),repeat=3):
for i in bins.get(tuple(k[j]+offset[j] for j in range(3)),[]):
dist=max(abs(v[j]-qa[i][j]) for j in range(3))
if dist<tol:candidates.append((dist,i))
if not candidates:return False
matches.append(min(candidates)[1])
if len(set(matches)) != len(qa):return False
af=sorted(tuple(sorted(tri)) for tri in fa)
bf=sorted(tuple(sorted(matches[i] for i in tri)) for tri in fb)
return af==bf
def plate(stem, specs):
document = App.newDocument(stem)
meshes = []
combined = Mesh.Mesh()
info = []
for name, original, x, y in specs:
m = moved(original, x, y)
if not m.isSolid() or len(m.getSeparateComponents()) != 1:
raise RuntimeError('Invalid input mesh after placement: ' + name)
if not equivalent(original, m):
raise RuntimeError('Geometry changed during placement: ' + name)
obj = document.addObject('Mesh::Feature', name)
obj.Label = name
obj.Mesh = m
combined.addMesh(m)
meshes.append(m)
info.append({'name':name, 'facets':m.CountFacets,
'volume_mm3':m.Volume, 'bounds_mm':bounds(m)})
for i, a in enumerate(meshes):
for b in meshes[i+1:]:
d=distance_xy(a,b)
if d < 8.0:
raise RuntimeError('Parts too close: %.6f mm bounding-box distance' % d)
min_gap=min(distance_xy(a,b) for i,a in enumerate(meshes) for b in meshes[i+1:])
combined.write(str(OUT / (stem+'.stl')))
document.recompute()
document.saveAs(str(OUT / (stem+'.FCStd')))
App.closeDocument(document.Name)
reloaded=Mesh.Mesh(str(OUT / (stem+'.stl')))
parts=reloaded.getSeparateComponents()
if len(parts)!=len(specs) or any(not p.isSolid() for p in parts):
raise RuntimeError('Export does not contain the expected closed components')
for expected in meshes:
candidates=[p for p in parts if abs(p.BoundBox.XMin-expected.BoundBox.XMin)<0.0001
and abs(p.BoundBox.YMin-expected.BoundBox.YMin)<0.0001]
if len(candidates)!=1 or not equivalent(expected,candidates[0]):
raise RuntimeError('Exported mesh differs from the placed original mesh')
if min(p.BoundBox.ZMin for p in parts) < -0.001 or max(abs(p.BoundBox.ZMin) for p in parts) > .001:
raise RuntimeError('Parts are not all at Z=0')
return {'file':stem+'.stl', 'components':len(parts),
'all_closed':True, 'geometry_preserved':True,
'minimum_bounding_box_gap_mm':min_gap,
'bounds_mm':bounds(reloaded), 'objects':info}
main = ([('Bracket',bracket,*bracket_pos)]
+ [('Shim05_%d'%(i+1),shim05,*xy) for i,xy in enumerate(row05)]
+ [('Shim10_%d'%(i+1),shim10,*xy) for i,xy in enumerate(row10)])
result={'main':plate('支架和两种垫片各8个', main)}
(OUT/'布局检查.json').write_text(json.dumps(result,ensure_ascii=False,indent=2),encoding='utf-8')
print(json.dumps(result,ensure_ascii=False,indent=2))