feat(export): baked GLB export with identity, clips, cutout fix (phases 0-1)

Promote the client GLB export into the baked-artifact format from
plans/editor-baked-glb-export.md (phases 0 and 1).

- NodeMaterial -> classic MeshStandardMaterial conversion at export. The
  viewer's MeshStandard/LambertNodeMaterial set isNodeMaterial, not
  isMeshStandardMaterial, so GLTFExporter would otherwise drop every
  surface to a blank default. KTX2 (compressed) maps are decompressed via
  WebGPUTextureUtils so the exporter can embed them (PNG for now; KTX2
  re-encode is deferred to the phase-3 bake worker).
- Identity stamping from sceneRegistry: node.name = pascalId and
  extras = { pascalId, kind, label?, openable?, clips? }; all other
  userData stripped so editor/runtime ephemera never reach glTF extras.
- Door/window open clips baked from the build-once + pose-at-t primitives
  (door pascalSwingLeaf marker, window poseWindowMovingParts). Clips named
  by label ("Door 1: open"), carry extras.loop = false (consumers play
  once and hold; dumb glTF players still loop).
- Cutout fix: door/window selection hitboxes hide via material.visible,
  which onlyVisible misses, so the hitbox box plugged the wall opening.
  Non-renderable container meshes now keep their node but lose geometry;
  childless ones are removed.
- Editor-overlay stripping mirrors the thumbnail capture: emit
  thumbnail:before/after-capture so scene-layer affordances (handles,
  ceiling/site brackets) self-hide, and drop anything off SCENE_LAYER
  (gizmos, grid, zone fills).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Wassim SAMAD
2026-06-22 11:55:27 -04:00
co-authored by Claude Opus 4.8
parent b2f1a8432e
commit 06e4cd7748
6 changed files with 624 additions and 38 deletions
@@ -1092,6 +1092,7 @@ function addDoorLeaf(
hingeX,
hingeSide,
swingRotation,
openRotationY,
segments,
contentPadding,
handle,
@@ -1117,6 +1118,10 @@ function addDoorLeaf(
hingeX: number
hingeSide: 'left' | 'right'
swingRotation: number
// Leaf rotation (radians, about the hinge Y axis) at fully-open. The GLB
// exporter reads this off the leaf group to bake an open/close clip; it is
// the kinematic endpoint, independent of the current `swingRotation`.
openRotationY: number
segments: DoorNode['segments']
contentPadding: DoorNode['contentPadding']
handle: boolean
@@ -1147,6 +1152,10 @@ function addDoorLeaf(
const leafGroup = new THREE.Group()
leafGroup.position.set(hingeX, 0, 0)
leafGroup.rotation.y = swingRotation
// Marks this group as the swing leaf and records its fully-open angle so the
// GLB exporter can bake an open/close animation clip from a single pose. The
// exporter strips this marker before writing the file.
leafGroup.userData.pascalSwingLeaf = { axis: 'y', openRotationY }
mesh.add(leafGroup)
const addLeafBox = (
@@ -2461,6 +2470,7 @@ function updateDoorMesh(rawNode: DoorNode, mesh: THREE.Mesh) {
hingeX: -insideWidth / 2,
hingeSide: 'left',
swingRotation: -clampedSwingAngle * swingDirectionSign,
openRotationY: (-Math.PI / 2) * swingDirectionSign,
segments,
contentPadding,
handle,
@@ -2489,6 +2499,7 @@ function updateDoorMesh(rawNode: DoorNode, mesh: THREE.Mesh) {
hingeX: insideWidth / 2,
hingeSide: 'right',
swingRotation: clampedSwingAngle * swingDirectionSign,
openRotationY: (Math.PI / 2) * swingDirectionSign,
segments,
contentPadding,
handle,
@@ -2520,6 +2531,7 @@ function updateDoorMesh(rawNode: DoorNode, mesh: THREE.Mesh) {
hingeX,
hingeSide: hingesSide,
swingRotation: clampedSwingAngle * swingDirectionSign * hingeDirectionSign,
openRotationY: (Math.PI / 2) * swingDirectionSign * hingeDirectionSign,
segments,
contentPadding,
handle,