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-pcb-mesh-indexing.py

"""Independently compare native and indexed GLB triangle attribute streams.
Mesh-local coordinates/attributes retain exact encoded bytes; the separate
native model review validates board-world anchors and assembly transforms.
"""
import argparse,hashlib,json,struct
from pathlib import Path
import numpy as np

COMPONENT_BYTES={5120:1,5121:1,5122:2,5123:2,5125:4,5126:4}
ELEMENT_COUNTS={'SCALAR':1,'VEC2':2,'VEC3':3,'VEC4':4,'MAT4':16}
INDEX_DTYPES={5121:'u1',5123:'<u2',5125:'<u4'}

def read_glb(path):
    raw=path.read_bytes()
    if struct.unpack_from('<III',raw)!= (0x46546c67,2,len(raw)):
        raise ValueError('Invalid GLB header')
    length,kind=struct.unpack_from('<II',raw,12)
    if kind!=0x4e4f534a:raise ValueError('Missing JSON chunk')
    document=json.loads(raw[20:20+length]);offset=20+length
    binary_length,kind=struct.unpack_from('<II',raw,offset)
    if kind!=0x004e4942:raise ValueError('Missing binary chunk')
    return document,raw[offset+8:offset+8+binary_length],raw


def accessor_rows(accessor_index,context):
    document,binary,_=context;accessor=document['accessors'][accessor_index]
    if 'sparse' in accessor:raise ValueError('Sparse accessor needs explicit proof support')
    view=document['bufferViews'][accessor['bufferView']]
    if view['buffer']!=0:raise ValueError('External binary buffer')
    width=COMPONENT_BYTES[accessor['componentType']]*ELEMENT_COUNTS[accessor['type']]
    rows=np.ndarray((accessor['count'],width),dtype='u1',buffer=binary,
                    offset=view.get('byteOffset',0)+accessor.get('byteOffset',0),
                    strides=(view.get('byteStride',width),1))
    return accessor,rows


def indices(primitive,context):
    if 'indices' not in primitive:
        accessor,_=accessor_rows(primitive['attributes']['POSITION'],context)
        return np.arange(accessor['count'])
    accessor,rows=accessor_rows(primitive['indices'],context)
    return np.frombuffer(rows.copy().tobytes(),dtype=INDEX_DTYPES[accessor['componentType']])


def image_hashes(context):
    document,binary,_=context;result=[]
    for image in document.get('images',[]):
        if 'bufferView' not in image:raise ValueError('External image needs explicit proof support')
        view=document['bufferViews'][image['bufferView']];start=view.get('byteOffset',0)
        result.append(hashlib.sha256(binary[start:start+view['byteLength']]).hexdigest())
    return sorted(result)


def effective_material(primitive,document):
    material=dict(document.get('materials',[])[primitive['material']]) if 'material' in primitive else {}
    material.setdefault('alphaMode','OPAQUE');material.setdefault('alphaCutoff',.5)
    material.setdefault('doubleSided',False);material.setdefault('emissiveFactor',[0,0,0])
    if material.get('name')=='':material.pop('name')
    pbr=dict(material.get('pbrMetallicRoughness',{}));pbr.setdefault('baseColorFactor',[1,1,1,1])
    pbr.setdefault('metallicFactor',1);pbr.setdefault('roughnessFactor',1);material['pbrMetallicRoughness']=pbr
    return material


def compare(native_path,indexed_path):
    native=read_glb(native_path);indexed=read_glb(indexed_path);a=native[0];b=indexed[0]
    if len(a['meshes'])!=len(b['meshes']):raise ValueError('Mesh coverage changed')
    if image_hashes(native)!=image_hashes(indexed):raise ValueError('Embedded image bytes changed')
    rows=[]
    for before,after in zip(a['meshes'],b['meshes']):
        if before.get('name')!=after.get('name') or len(before['primitives'])!=len(after['primitives']):
            raise ValueError('Mesh identity or primitive coverage changed')
        for first,second in zip(before['primitives'],after['primitives']):
            if first.get('mode',4)!=4 or second.get('mode',4)!=4:raise ValueError('Require triangles')
            if set(first['attributes'])!=set(second['attributes']):raise ValueError('Attribute coverage changed')
            if effective_material(first,a)!=effective_material(second,b):raise ValueError('Effective material changed')
            first_indices=indices(first,native);second_indices=indices(second,indexed)
            if len(first_indices)!=len(second_indices) or len(first_indices)%3:raise ValueError('Triangle count changed')
            streams={}
            for semantic in sorted(first['attributes']):
                first_accessor,first_rows=accessor_rows(first['attributes'][semantic],native)
                second_accessor,second_rows=accessor_rows(second['attributes'][semantic],indexed)
                for field in ['componentType','type']:
                    if first_accessor[field]!=second_accessor[field]:raise ValueError('Vertex encoding changed')
                if first_accessor.get('normalized',False)!=second_accessor.get('normalized',False):raise ValueError('Normalization changed')
                expanded_before=first_rows[first_indices].tobytes();expanded_after=second_rows[second_indices].tobytes()
                if expanded_before!=expanded_after:raise ValueError(f'Triangle attribute bytes changed: {before.get("name")}/{semantic}')
                streams[semantic]=hashlib.sha256(expanded_before).hexdigest()
            rows.append({'mesh':before.get('name'),'triangles':len(first_indices)//3,'expanded_attribute_sha256':streams,'exactly_equal':True})
    return {'native_sha256':hashlib.sha256(native[2]).hexdigest(),'indexed_sha256':hashlib.sha256(indexed[2]).hexdigest(),'native_bytes':len(native[2]),'indexed_bytes':len(indexed[2]),'all_triangle_attribute_streams_exactly_equal':True,'embedded_images_exactly_equal':True,'primitives':rows}


if __name__=='__main__':
    parser=argparse.ArgumentParser();parser.add_argument('--directory',type=Path,required=True);parser.add_argument('--output',type=Path,required=True);args=parser.parse_args()
    reports=[]
    for path in sorted(args.directory.glob('*.native.glb')):
        reports.append({'fragment':path.name,**compare(path,path.with_name(path.name.replace('.native.glb','.indexed.glb')))})
    if len(reports)!=8:raise ValueError('Require all eight actual fragments')
    args.output.write_text(json.dumps({'fragments':reports,'all_exact':True},indent=2)+'\n')
    print('All eight fragments retain every triangle and exact vertex attributes/images')