abse/smartwatch-flex-top-only
Three-panel flex smartwatch with two parallel R3.9 bends, all components flat-top, low-profile JST connector harnesses, split heartbeat optical windows and screw-retained PCB mounting. Modeled 39 × 50.5 × 14.5mm prototype; 100% routing, zero native DRC and shorts. Hardware qualification pending.
- Version
- 0.2.20
- License
- unset
- Stars
- 0
scripts/audit-pcb-mounts.py
from pathlib import Path
import base64, io, json, hashlib
import numpy as np
import trimesh
from shapely.geometry import Point, LineString, MultiPoint, Polygon
from shapely.ops import polygonize, unary_union
root=Path(__file__).resolve().parent.parent
sha=lambda p:hashlib.sha256((root/p).read_bytes()).hexdigest()
body=trimesh.collision.CollisionManager()
body_bounds={}
def add_body(name,mesh):
body_bounds[name]=mesh.bounds.copy();body.add_object(name,mesh)
for filename,shift in [('upright-pcb.glb',[0,0,0]),('assembly-display.glb',[0,0,0]),('relocated-motor.glb',[0,0,0])]:
scene=trimesh.load(root/'mechanical'/filename,force='scene')
for node in scene.graph.nodes_geometry:
T,name=scene.graph[node];mesh=scene.geometry[name].copy();mesh.apply_transform(T);mesh.apply_translation(shift)
add_body(filename+'/'+node,mesh)
for name,shift in [('battery-envelope',[3,-.19159126281738,-3.6]),('ferrite-envelope',[0,-8.99159126281738,-6.78])]:
data=base64.b64decode((root/f'mechanical/{name}.ts').read_text().split('base64,')[1].split('"')[0])
scene=trimesh.load(io.BytesIO(data),file_type='glb',force='scene')
for node in scene.graph.nodes_geometry:
T,n=scene.graph[node];mesh=scene.geometry[n].copy();mesh.apply_transform(T);mesh.apply_translation(shift);add_body(name+'/'+node,mesh)
case_fixtures=trimesh.load(root/'mechanical/optical-enclosure.glb',force='scene')
for node in case_fixtures.graph.nodes_geometry:
T,name=case_fixtures.graph[node]
if name.startswith('Original outside') or 'shell' in name.lower() or name=='ORIGINAL_SIZE_CASE_WITH_SPLIT_OPTICAL_TUNNEL':continue
mesh=case_fixtures.geometry[name].copy();mesh.apply_transform(T);add_body('existing-fixture/'+name,mesh)
contacts=[];failures=[];meshes=[];removable=set()
for file in ['pcb-supports','pcb-cradle','pcb-retainers','pcb-mount-screws']:
scene=trimesh.load(root/f'mechanical/{file}.glb',force='scene')
for node in scene.graph.nodes_geometry:
T,name=scene.graph[node];mesh=scene.geometry[name].copy();mesh.apply_transform(T);meshes.append((name,mesh))
if file!='pcb-supports':removable.add(name)
hit,names,data=body.in_collision_single(mesh,return_names=True,return_data=True)
for contact in data:
other=next(n for n in contact.names if n!='__external')
bb=body_bounds[other]
# A separating AABB independently excludes volume penetration;
# FCL can report a false depth at triangulated support faces.
if np.any(np.maximum(bb[0]-mesh.bounds[1],mesh.bounds[0]-bb[1])>=-0.000005):continue
record={'mount':name,'body':sorted(contact.names),'penetrationMM':float(contact.depth)}
contacts.append(record)
# Exact face contact is intentional at the support and capture pads;
# 5um covers float32 export resolution, not a design clearance waiver.
if contact.depth>0.000005:failures.append(record)
# Integrated posts must lie inside the original exterior at every level. The
# base intentionally enters the existing floor by 0.1mm to attach to the shell.
case=trimesh.load(root/'mechanical/original-enclosure.glb',force='scene')
shell=next(g for n,g in case.geometry.items() if n.startswith('Rolled'))
shell_manager=trimesh.collision.CollisionManager();shell_manager.add_object('original-shell',shell)
for name,mesh in meshes:
if name in removable:
hit,names,data=shell_manager.in_collision_single(mesh,return_names=True,return_data=True)
for c in data:
if c.depth>0.000005:failures.append({'mount':name,'body':['original-shell'],'penetrationMM':float(c.depth)})
outside=[];wall_overlap=[]
for z in np.arange(min(m.bounds[0,2] for _,m in meshes),max(m.bounds[1,2] for _,m in meshes)+.00001,.1):
segments=trimesh.intersections.mesh_plane(shell,[0,0,1],[0,0,float(z)])
polys=list(polygonize(unary_union([LineString(np.round(s[:,:2],5)) for s in segments if np.linalg.norm(s[1,:2]-s[0,:2])>1e-6])))
assert polys, float(z)
# The original side-button aperture opens the inner and outer contours.
# Its convex outer silhouette still defines the unchanged case envelope.
outer=MultiPoint(np.vstack(segments)[:,:2]).convex_hull
for name,mesh in meshes:
if mesh.bounds[0,2]-1e-6<=z<=mesh.bounds[1,2]+1e-6:
# Convex XY projection is conservative, including annular screw eyes.
projection=MultiPoint(mesh.vertices[:,:2]).convex_hull
if not outer.buffer(.00002).covers(projection):
outside.append({'mount':name,'z':float(z),'outsideAreaMM2':projection.difference(outer).area})
if name in removable:
center=[float(mesh.centroid[0]),float(mesh.centroid[1])]
inner=[p for p in polys if p.covers(Point(0,0))]
if inner:
cavity=min(inner,key=lambda p:p.area)
if not cavity.buffer(.00002).covers(projection):wall_overlap.append({'mount':name,'z':float(z),'wallOverlapAreaMM2':projection.difference(cavity).area})
report={'status':'PASS' if not failures and not outside and not wall_overlap else 'FAIL',
'method':'FCL mesh contact detection against every emitted populated PCB, original display, maximum battery/ferrite and relocated motor mesh. Original exterior and removable-piece cavity containment checked at 0.1mm slices with conservative XY hulls.',
'mountMeshesChecked':len(meshes),'penetrations':failures,'outsideOriginalExterior':outside,'removablePiecesOverlapFixedWall':wall_overlap,
'intentionalZeroDepthSupportContacts':len(contacts),'mountAssetsSha256':{f:sha(f'mechanical/{f}.glb') for f in ['pcb-supports','pcb-cradle','pcb-retainers','pcb-mount-screws']},
'qualification':'Nominal CAD only; screw threads, preload, print tolerances, material strength and durability need prototype verification.'}
(root/'reports/pcb-mount-clearance.json').write_text(json.dumps(report,indent=2)+'\n')
print(json.dumps(report,indent=2))
assert report['status']=='PASS', 'Mount clearance failed'