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/export-product-mechanics.py

"""Lossless imported mechanical models from the canonical native JSCAD assembly."""
import argparse
import hashlib
import json
from pathlib import Path
import numpy as np
import trimesh

ROOT = Path(__file__).resolve().parents[1]


def export_models(options):
    source = ROOT/'dist'/options.source_view/'3d.glb'
    scene = trimesh.load(source, force='scene')
    review = json.loads((ROOT/'product/geometry-review.json').read_text())
    circuit = json.loads((source.parent/'circuit.json').read_text())
    reports = []
    for part in review['parts']:
        name = part['name']
        component = next(row for row in circuit if row['type']=='source_component' and row['name']==name)
        cad = next(row for row in circuit if row['type']=='cad_component' and row['source_component_id']==component['source_component_id'])
        if 'model_jscad' not in cad:
            raise ValueError('Mechanical export must originate from canonical native plans')
        colored_plan = cad['model_jscad']
        while colored_plan['type'] == 'translate':
            colored_plan = colored_plan['shape']
        if colored_plan['type'] != 'colorize' or len(colored_plan['color']) != 3:
            raise ValueError(f'Missing canonical presentation material: {name}')
        rgba = [*colored_plan['color'], 0.45 if part['referenceOnly'] else 1.0]
        material = trimesh.visual.material.PBRMaterial(
            name=name, baseColorFactor=rgba, roughnessFactor=0.85, metallicFactor=0,
            alphaMode='BLEND' if part['referenceOnly'] else 'OPAQUE')
        lower, upper = np.asarray(part['boundsMm'])
        center = (lower+upper)/2
        if any(abs(cad['position'][axis]-center[index]) > 1e-5 for index,axis in enumerate(['x','y','z'])):
            raise ValueError(f'Canonical mechanical center changed: {name}')
        product_to_model = np.eye(4)
        # Canonical native GLB is (-X, Z, Y); documented z+ models are
        # product (X, Y, Z). This proper rotation has determinant +1 and
        # preserves triangle orientation without face edits or negative scale.
        product_to_model[:3,:3] = [[-1,0,0],[0,0,1],[0,1,0]]
        product_to_model[:3,3] = -center
        selected = trimesh.Scene()
        triangles = 0
        for node in scene.graph.nodes_geometry:
            if node != name and not node.startswith(name+'_'):
                continue
            transform, geometry = scene.graph[node]
            mesh = scene.geometry[geometry].copy()
            model_transform = product_to_model @ transform
            if abs(np.linalg.det(model_transform[:3,:3])-1) > 1e-6:
                raise ValueError(f'Mechanical export requires a proper rigid transform: {name}')
            mesh.apply_transform(model_transform)
            mesh.visual = trimesh.visual.TextureVisuals(material=material)
            selected.add_geometry(mesh, node_name=node, geom_name=node)
            triangles += len(mesh.faces)
        if not triangles:
            raise ValueError(f'Missing canonical mechanical triangles: {name}')
        target = ROOT/'product/models/mechanical'/f'{name}.glb'
        target.parent.mkdir(parents=True, exist_ok=True)
        target.write_bytes(selected.export(file_type='glb'))
        actual = trimesh.load(target, force='scene')
        actual_triangles = sum(len(actual.geometry[actual.graph[node][1]].faces) for node in actual.graph.nodes_geometry)
        bounds_delta = float(np.abs(selected.bounds-actual.bounds).max())
        if actual_triangles != triangles or bounds_delta > 1e-4:
            raise ValueError(f'Lossy mechanical model export: {name}')
        reports.append({'part':name,'file':str(target.relative_to(ROOT)),
                        'sha256':hashlib.sha256(target.read_bytes()).hexdigest(),
                        'bytes':target.stat().st_size,'triangles':triangles,
                        'exported_triangles':actual_triangles,'bounds_delta_mm':bounds_delta,
                        'canonical_base_color_rgba':rgba,
                        'no_mesh_repair_or_simplification':True})
    report = {'source_view':options.source_view,
              'canonical_native_glb_sha256':hashlib.sha256(source.read_bytes()).hexdigest(),
              'source_plans_sha256':hashlib.sha256((ROOT/'product/print-plans.json').read_bytes()).hexdigest(),
              'geometry_review_sha256':hashlib.sha256((ROOT/'product/geometry-review.json').read_bytes()).hexdigest(),
              'model_coordinate_frame':'product XYZ, board normal z+, millimeters',
              'proper_rigid_transforms_only':True,
              'models':reports,'passes':True}
    (ROOT/'product/mechanical-model-review.json').write_text(json.dumps(report,indent=2)+'\n')
    print(f'{len(reports)} lossless native mechanical GLBs exported; {sum(p["bytes"] for p in reports)} bytes')


if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('--source-view', required=True, choices=['product.geometry'])
    export_models(parser.parse_args())