"""Place existing PCD50 STL meshes on two print plates without changing shapes. Run with FreeCAD's Python, for example: & 'C:\\Users\\billy\\scoop\\apps\\freecad\\current\\bin\\python.exe' scripts/build_pcd50_print_plate.py The main plate is one bracket and four 0.5 mm shims. The second plate holds four optional 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_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') # X positions are chosen to give at least 8 mm between the shims' actual # bounding boxes. The 49 mm bracket sits 8 mm above their actual Y maximum. # Every placement uses the original tessellation; no rotations or rescaling. row = [(6 + 20*i, 6.0) for i in range(4)] bracket_pos = (35.77, 44.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 .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(row)] spare=[('Shim10_%d'%(i+1),shim10,*xy) for i,xy in enumerate(row)] result={'main':plate('支架和四个0.5mm垫片',main), 'spare':plate('四个1mm备用垫片',spare)} (OUT/'布局检查.json').write_text(json.dumps(result,ensure_ascii=False,indent=2),encoding='utf-8') print(json.dumps(result,ensure_ascii=False,indent=2))