← Commit history

Fix: keep the insertion animation in the rebuilt page GLBs

John Lauer ·50c09a4951 ·20d ago ·parent cde8bd5
4 files changed +20−8
IN-S42ATG.glb
⋯ 1 unchanged line ⋯
IN-S42ATG.insertion.glb
⋯ 1 unchanged line ⋯
provenance/in-s42atg-led-pass.provenance.json+6−5
@@ -85,8 +85,8 @@   "meaning": "<MPN>.glb and <MPN>.insertion.glb are the page's composite LED scenes (FR4 board, pads, silk, solder fillets, the LED, an emissive LED_lit skin, FOV cone and a point light). The README's inline viewer (Colby's viewer + Static / Pick&Place additions) loads <MPN>.insertion.glb and animates the LED being inserted (placed) into its footprint; <MPN>.glb is the older static composite with the earlier silkscreen. Only the LED geometry/colours were replaced; every other node is copied unchanged.",   "files": {    "IN-S42ATG.glb": {-    "sha256": "1c37e73db8be7f14cb39852a94d54071c9f494dbd1944829f603c26f66fb0d7b",-    "bytes": 88096,+    "sha256": "b999c788de1037c9d14a51ba30de186a5eabd1971d82f9fde2695e0c7783baf1",+    "bytes": 88424,     "bytes_before": 281312,     "tris_before": 3226,     "tris_after": 1172,@@ -128,8 +128,8 @@     }    },    "IN-S42ATG.insertion.glb": {-    "sha256": "77778ec454ab3fc27e96ca3a637b770b89af6d3a850168f8a7c708c2a19ab633",-    "bytes": 54248,+    "sha256": "5c8412facd5d02c54fde39d8afb595b5fece1e1149f28542cd9ad8913eeb7fbc",+    "bytes": 54576,     "bytes_before": 304976,     "tris_before": 3506,     "tris_after": 1452,@@ -170,7 +170,8 @@      }     }    }-  }+  },+  "animation_fix": "2026-09-30: the pick-and-place insertion animation (sampler keyframe accessors) is carried over unchanged; the earlier rebuilds of this pass had dropped those accessors, leaving the animation pointing at geometry data"  },  "hero_glb": {   "file": "in-s42atg-hero.glb",
provenance/tools/gltfio.py+14−3
@@ -49,12 +49,17 @@ def save(path, d, arrays):         k = 1 if arr.ndim == 1 else arr.shape[1]         acc = {'bufferView': len(d['bufferViews']) - 1, 'componentType': ct, 'count': int(arr.shape[0]),                'type': {1: 'SCALAR', 2: 'VEC2', 3: 'VEC3', 4: 'VEC4'}[k]}-        if pos or key.endswith('POSITION'): acc['min'] = [float(v) for v in arr.min(0)]; acc['max'] = [float(v) for v in arr.max(0)]+        if pos or key.endswith('POSITION'):+            mn, mx = arr.min(0), arr.max(0)+            acc['min'] = [float(v) for v in np.atleast_1d(mn)]; acc['max'] = [float(v) for v in np.atleast_1d(mx)]         d['accessors'].append(acc); idx[key] = len(d['accessors']) - 1; return idx[key]     for m in d['meshes']:         for p in m['primitives']:             p['attributes'] = {k: add(v, k == 'POSITION') for k, v in p['attributes'].items()}             if 'indices' in p: p['indices'] = add(p['indices'])+    for an in d.get('animations', []):             # keyframe times need min/max (glTF requires it on sampler input)+        for sm in an['samplers']:+            sm['input'] = add(sm['input'], True); sm['output'] = add(sm['output'])     while len(blob) % 4: blob.append(0)     d['buffers'] = [{'byteLength': len(blob)}]     j = json.dumps(d, separators=(',', ':')).encode(); j += b' ' * ((-len(j)) % 4)@@ -70,6 +75,10 @@ def explode(d, binc):                 key = f'a{a}_{k}'; arrays[key] = (accessor(d, binc, a), 34962); p['attributes'][k] = key             if 'indices' in p:                 key = f'a{p["indices"]}_IDX'; arrays[key] = (accessor(d, binc, p['indices']), 34963); p['indices'] = key+    for an in d.get('animations', []):             # animation keyframes are accessors too: carry them (bug fixed 2026-09-30)+        for sm in an['samplers']:+            for k in ('input', 'output'):+                key = f'a{sm[k]}_ANIM'; arrays[key] = (accessor(d, binc, sm[k]), None); sm[k] = key     for k in ('accessors', 'bufferViews', 'buffers'): d.pop(k, None)     return arrays @@ -84,8 +93,10 @@ def flatten(d, binc):             R = W[:3, :3]; Nm = np.linalg.inv(R).T             for p in d['meshes'][n['mesh']]['primitives']:                 P = accessor(d, binc, p['attributes']['POSITION']).astype(np.float64) @ R.T + W[:3, 3]-                N = accessor(d, binc, p['attributes']['NORMAL']).astype(np.float64) @ Nm.T-                N /= np.maximum(np.linalg.norm(N, axis=1, keepdims=True), 1e-12)+                N = None+                if 'NORMAL' in p['attributes']:+                    N = accessor(d, binc, p['attributes']['NORMAL']).astype(np.float64) @ Nm.T+                    N /= np.maximum(np.linalg.norm(N, axis=1, keepdims=True), 1e-12)                 out.append((p.get('material'), P, N, accessor(d, binc, p['indices']).astype(np.int64)))         for c in n.get('children', []): walk(c, W)     for r in d['scenes'][d.get('scene', 0)]['nodes']: walk(r, np.eye(4))