AnasSarkiz/magnetic-shutter-remote-r8
A USB‑C rechargeable battery-powered ESP32‑C3 wireless controller with Li‑ion charging, protected 3.3‑V buck‑boost regulation, thermal/safety monitoring, status LEDs, buttons, programming UART, crystal clock, and 2.4‑GHz antenna matching network.
- Version
- 0.4.0-prototype
- License
- unset
- Stars
- 0
scripts/review-native-product.py
"""Read actual built GLBs; reject scaled, mirrored, lost or moved PCB geometry."""
import argparse
import hashlib
import json
import itertools
from pathlib import Path
import numpy as np
import trimesh
ROOT = Path(__file__).resolve().parents[1]
def meshes_with_prefix(scene, prefix):
selected = []
for node in scene.graph.nodes_geometry:
if node == prefix or node.startswith(prefix + '_'):
transform, geometry = scene.graph[node]
mesh = scene.geometry[geometry].copy()
mesh.apply_transform(transform)
selected.append(mesh)
if not selected:
raise ValueError(f'Missing native geometry: {prefix}')
return selected
def bounds(meshes):
return np.stack([np.min([m.bounds[0] for m in meshes], axis=0),
np.max([m.bounds[1] for m in meshes], axis=0)])
def compare_triangles(expected, actual):
if expected.shape != actual.shape:
raise ValueError('Mechanical triangle count changed')
cells = {}
cell_size = 0.001
for index, center in enumerate(expected.mean(axis=1)):
key = tuple(np.floor(center/cell_size).astype(int))
cells.setdefault(key, set()).add(index)
maximum_delta = 0.0
# Cyclic order may change; reversed winding must never pass as the same face.
permutations = [(0,1,2),(1,2,0),(2,0,1)]
for triangle in actual:
cell = np.floor(triangle.mean(axis=0)/cell_size).astype(int)
match = None
for offset in itertools.product([-1,0,1], repeat=3):
key = tuple(cell+offset)
for index in list(cells.get(key, set())):
delta = min(float(np.abs(triangle-expected[index][list(order)]).max())
for order in permutations)
if delta <= 1e-4:
match = key, index, delta
break
if match:
break
if not match:
raise ValueError('Imported mechanical triangle differs from canonical geometry')
key, index, delta = match
cells[key].remove(index)
maximum_delta = max(maximum_delta, delta)
return maximum_delta
def review(output):
models = json.loads((ROOT/'product/pcb-model-review.json').read_text())
geometry = json.loads((ROOT/'product/geometry-review.json').read_text())
canonical_path = ROOT/'dist/product.geometry/3d.glb'
canonical_scene = trimesh.load(canonical_path, force='scene')
canonical_circuit = json.loads((canonical_path.parent/'circuit.json').read_text())
rows = []
for view in ['product.assembly', 'product.exploded', 'enclosure']:
path = ROOT/'dist'/view/'3d.glb'
scene = trimesh.load(path, force='scene')
circuit = json.loads((path.parent/'circuit.json').read_text())
for index, asset in enumerate(models['nativeAssets']):
prefix = ('R8QualifiedPCBAndFittedParts1' if index == 0
else f'R8QualifiedFittedParts{index+1}')
component = next(e for e in circuit if e['type']=='source_component' and e['name']==prefix)
cad = next(e for e in circuit if e['type']=='cad_component' and e['source_component_id']==component['source_component_id'])
if cad['rotation'] != {'x':0,'y':0,'z':0} or cad['model_unit_to_mm_scale_factor'] != 1 or cad['model_board_normal_direction'] != 'z+':
raise ValueError('Unexpected native PCB transform')
center = cad['position']
actual_meshes = meshes_with_prefix(scene, prefix)
original = trimesh.load(ROOT/'product/models'/asset['filename'], force='scene')
minimum, maximum = original.bounds
expected = np.array([[-center["x"]-maximum[0], center["z"]+minimum[2], center["y"]+minimum[1]],
[-center["x"]-minimum[0], center["z"]+maximum[2], center["y"]+maximum[1]]])
delta = float(np.abs(bounds(actual_meshes)-expected).max())
original_triangles = sum(len(original.geometry[g].faces)
for g in [original.graph[n][1] for n in original.graph.nodes_geometry])
actual_triangles = sum(len(mesh.faces) for mesh in actual_meshes)
if delta > 1e-4 or actual_triangles != original_triangles:
raise ValueError(f'Native PCB geometry mismatch: {view}/{prefix}: '
f'{delta}mm, {actual_triangles}/{original_triangles} triangles')
rows.append({'view':view,'fragment':asset['filename'],
'bounds_delta_mm':delta, 'triangles':actual_triangles,
'all_source_triangles_present':True})
for part in geometry['parts']:
source = next(e for e in circuit if e['type']=='source_component'
and e['name']==part['name'])
cad = next(e for e in circuit if e['type']=='cad_component'
and e['source_component_id']==source['source_component_id'])
if cad['model_board_normal_direction'] != 'z+':
raise ValueError('Mechanical GLB must use the documented product z+ frame')
lower, upper = np.array(part['localBoundsMm'])
size = upper-lower
center = cad['position']
expected_center = np.array([-center['x'],center['z'],center['y']])
expected_size = size[[0,2,1]]
expected = np.stack([expected_center-expected_size/2,
expected_center+expected_size/2])
actual_meshes = meshes_with_prefix(scene,part['name'])
delta = float(np.abs(bounds(actual_meshes)-expected).max())
if delta > 1e-4:
raise ValueError(f'Native mechanical placement mismatch: {view}/{part["name"]}: {delta}')
canonical_component = next(e for e in canonical_circuit if e['type']=='source_component' and e['name']==part['name'])
canonical_cad = next(e for e in canonical_circuit if e['type']=='cad_component' and e['source_component_id']==canonical_component['source_component_id'])
shift = np.array([-(center['x']-canonical_cad['position']['x']),
center['z']-canonical_cad['position']['z'],
center['y']-canonical_cad['position']['y']])
original_triangles = np.concatenate([m.triangles for m in meshes_with_prefix(canonical_scene,part['name'])])
actual_triangles = np.concatenate([m.triangles for m in actual_meshes])-shift
vertex_delta = compare_triangles(original_triangles, actual_triangles)
rows.append({'view':view,'part':part['name'],'bounds_delta_mm':delta,
'matches_canonical_triangles':True,
'outward_face_winding_preserved':True,
'canonical_triangles':len(original_triangles),
'maximum_vertex_delta_mm':vertex_delta})
report = {'native_views_checked':['product.assembly','product.exploded','enclosure'],
'actual_glb_geometry_review':rows, 'passes':True,
'physical_fit_or_material_testing_claimed':False,
'canonical_native_glb_sha256':hashlib.sha256(canonical_path.read_bytes()).hexdigest(),
'native_glb_sha256':{v:hashlib.sha256((ROOT/'dist'/v/'3d.glb').read_bytes()).hexdigest()
for v in ['product.assembly','product.exploded','enclosure']}}
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(report,indent=2)+'\n')
print(f'Native GLB PCB triangle/bounds and mechanical placement checks: {len(rows)} pass')
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--output',type=Path,required=True)
review(parser.parse_args().output)