The glTF writer now groups products by representation step_id and
emits one shared mesh + one node per group with EXT_mesh_gpu_instancing
(per-instance TRS attributes). Multi-fragment products and rep-unique
singletons fall through to the existing baked path so backwards-
compat is preserved for viewers that don't read the extension.
Plan:
- Classify products: single-fragment + non-empty mesh + rep shared
with ≥2 others → instance group; everything else → baked.
- Per group: pack the first member's local_vertices/local_indices
as the shared mesh, then TRS-decompose each member's
world_transform * instance_transform via glam's
to_scale_rotation_translation. Write TRANSLATION (VEC3), ROTATION
(VEC4 quat xyzw), SCALE (VEC3) accessors per group.
- Per-instance identity goes into node.extras.instances[] as a
parallel array indexed by glTF instance order — viewers map
picked instance_id → guid/entity/segments without a side channel.
- Materials: baked path keeps per-product GUID materials (legacy
pick-to-BIM hook); instanced path uses one entity-type material
per group (per-instance materials aren't supported by the
extension).
File size measurements (real Skiplum client files):
- LBK_RIBp_C 41 MB (structural, 4.84 instances/rep): 118.5 MB
→ 72.9 MB (38% smaller). 804 instance groups with 25,565
total instances; 8,464 baked singletons.
- LBK_ARK_C 192 MB (architecture, 1.13 instances/rep): 86.9 MB
→ 86.8 MB (no harm). Most reps unique → very little to dedup.
- Bundled minimal fixture: 296 bytes both ways (no dedup possible).
Structural validation (custom Python checker on the .glb): all
accessor/bufferView refs valid, T/R/S accessor counts matched
per group, ifcfast-bench / all 13 mesh_reveal / 7 cut_openings_
integration / 76 Python tests pass.
Bumps to v0.4.23. No cache schema change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>