@pascal-app/viewer 1.0.0 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +12 -0
  2. package/dist/capture/capture-runtime.js +2 -32
  3. package/dist/capture/layers/device-motion-layer.d.ts.map +1 -1
  4. package/dist/capture/layers/device-motion-layer.js +5 -7
  5. package/dist/capture/stream-rendering.d.ts.map +1 -1
  6. package/dist/capture/stream-rendering.js +2 -1
  7. package/dist/capture/use-json-artifact.d.ts +2 -0
  8. package/dist/capture/use-json-artifact.d.ts.map +1 -0
  9. package/dist/capture/use-json-artifact.js +42 -0
  10. package/dist/components/viewer/glb-scene.d.ts.map +1 -1
  11. package/dist/components/viewer/glb-scene.js +3 -1
  12. package/dist/components/viewer/index.d.ts +12 -0
  13. package/dist/components/viewer/index.d.ts.map +1 -1
  14. package/dist/components/viewer/index.js +28 -7
  15. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  16. package/dist/components/viewer/post-processing.js +6 -0
  17. package/dist/components/viewer/selection-manager.d.ts.map +1 -1
  18. package/dist/components/viewer/selection-manager.js +8 -16
  19. package/dist/components/viewer/viewer-camera.d.ts +15 -1
  20. package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
  21. package/dist/components/viewer/viewer-camera.js +43 -5
  22. package/dist/index.d.ts +8 -6
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +8 -6
  25. package/dist/lib/csg-utils.d.ts +1 -1
  26. package/dist/lib/csg-utils.d.ts.map +1 -1
  27. package/dist/lib/csg-utils.js +1 -1
  28. package/dist/lib/isolation.d.ts +14 -30
  29. package/dist/lib/isolation.d.ts.map +1 -1
  30. package/dist/lib/isolation.js +62 -66
  31. package/dist/lib/materials.d.ts.map +1 -1
  32. package/dist/lib/materials.js +2 -1
  33. package/dist/lib/renderer-capability.d.ts +2 -1
  34. package/dist/lib/renderer-capability.d.ts.map +1 -1
  35. package/dist/lib/renderer-capability.js +2 -2
  36. package/dist/lib/scene-visibility.js +3 -1
  37. package/dist/lib/snapshot-pipeline.d.ts +3 -1
  38. package/dist/lib/snapshot-pipeline.d.ts.map +1 -1
  39. package/dist/lib/snapshot-pipeline.js +32 -16
  40. package/dist/store/use-viewer.d.ts +3 -1
  41. package/dist/store/use-viewer.d.ts.map +1 -1
  42. package/dist/store/use-viewer.js +2 -0
  43. package/dist/systems/door/door-system.d.ts.map +1 -1
  44. package/dist/systems/door/door-system.js +18 -5
  45. package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -1
  46. package/dist/systems/floor-elevation/floor-elevation-system.js +122 -3
  47. package/dist/systems/geometry/geometry-system.d.ts.map +1 -1
  48. package/dist/systems/geometry/geometry-system.js +4 -0
  49. package/dist/systems/level/level-system.js +14 -14
  50. package/dist/systems/level/level-utils.d.ts +25 -1
  51. package/dist/systems/level/level-utils.d.ts.map +1 -1
  52. package/dist/systems/level/level-utils.js +25 -4
  53. package/dist/systems/wall/wall-cutout-cache.d.ts +4 -1
  54. package/dist/systems/wall/wall-cutout-cache.d.ts.map +1 -1
  55. package/dist/systems/wall/wall-cutout-cache.js +16 -7
  56. package/dist/systems/wall/wall-cutout.d.ts +3 -1
  57. package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
  58. package/dist/systems/wall/wall-cutout.js +5 -5
  59. package/dist/systems/wall/wall-materials.d.ts +11 -2
  60. package/dist/systems/wall/wall-materials.d.ts.map +1 -1
  61. package/dist/systems/wall/wall-materials.js +24 -18
  62. package/dist/systems/wall/wall-system.d.ts +15 -2
  63. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  64. package/dist/systems/wall/wall-system.js +20 -21
  65. package/dist/systems/window/window-system.d.ts +1 -1
  66. package/dist/systems/window/window-system.d.ts.map +1 -1
  67. package/dist/systems/window/window-system.js +3 -2
  68. package/dist/systems/zone/zone-system.d.ts.map +1 -1
  69. package/dist/systems/zone/zone-system.js +28 -2
  70. package/dist/xr/presentation-context.d.ts +7 -0
  71. package/dist/xr/presentation-context.d.ts.map +1 -0
  72. package/dist/xr/presentation-context.js +10 -0
  73. package/package.json +4 -4
@@ -1,5 +1,7 @@
1
1
  import { getEffectiveNode, getFloorStackedPosition, nodeRegistry, sceneRegistry, useLiveNodeOverrides, useLiveTransforms, useScene, } from '@pascal-app/core';
2
2
  import { useFrame } from '@react-three/fiber';
3
+ import { useEffect, useMemo } from 'react';
4
+ import { Euler, Matrix4, Quaternion, Vector3 } from 'three';
3
5
  function withLiveTransform(node, liveTransform) {
4
6
  if (!liveTransform)
5
7
  return node;
@@ -15,6 +17,16 @@ function withLiveTransform(node, liveTransform) {
15
17
  ...(rotation !== undefined ? { rotation } : {}),
16
18
  };
17
19
  }
20
+ function restoreMountedExitPose(saved) {
21
+ saved.mesh.matrixAutoUpdate = saved.autoUpdate;
22
+ saved.mesh.scale.copy(saved.scale);
23
+ if (!saved.mesh.parent || saved.mesh.parent === saved.parent) {
24
+ saved.mesh.position.copy(saved.position);
25
+ saved.mesh.quaternion.copy(saved.quaternion);
26
+ }
27
+ saved.mesh.updateMatrix();
28
+ saved.mesh.updateMatrixWorld(true);
29
+ }
18
30
  /**
19
31
  * Generic floor-elevation system.
20
32
  *
@@ -36,6 +48,56 @@ function withLiveTransform(node, liveTransform) {
36
48
  export const FloorElevationSystem = () => {
37
49
  const dirtyNodes = useScene((s) => s.dirtyNodes);
38
50
  const clearDirty = useScene((s) => s.clearDirty);
51
+ const preview = useMemo(() => ({
52
+ local: new Matrix4(),
53
+ target: new Matrix4(),
54
+ position: new Vector3(),
55
+ rotation: new Euler(),
56
+ quaternion: new Quaternion(),
57
+ unit: new Vector3(1, 1, 1),
58
+ saved: new Map(),
59
+ }), []);
60
+ useEffect(() => {
61
+ const restore = (id) => {
62
+ const saved = preview.saved.get(id);
63
+ if (!saved)
64
+ return;
65
+ restoreMountedExitPose(saved);
66
+ preview.saved.delete(id);
67
+ };
68
+ const sync = () => {
69
+ const { nodes } = useScene.getState();
70
+ const overrides = useLiveNodeOverrides.getState().overrides;
71
+ for (const id of preview.saved.keys()) {
72
+ const node = nodes[id];
73
+ const patch = overrides.get(id);
74
+ if (!node || !patch?.parentId || patch.parentId === node.parentId)
75
+ restore(id);
76
+ }
77
+ for (const [id, patch] of overrides) {
78
+ const node = nodes[id];
79
+ if (!node || !patch.parentId || patch.parentId === node.parentId || preview.saved.has(id))
80
+ continue;
81
+ const mesh = sceneRegistry.nodes.get(id);
82
+ if (!mesh?.parent)
83
+ continue;
84
+ preview.saved.set(id, {
85
+ mesh,
86
+ parent: mesh.parent,
87
+ position: mesh.position.clone(),
88
+ quaternion: mesh.quaternion.clone(),
89
+ scale: mesh.scale.clone(),
90
+ autoUpdate: mesh.matrixAutoUpdate,
91
+ });
92
+ }
93
+ };
94
+ const unsubscribe = useLiveNodeOverrides.subscribe(sync);
95
+ return () => {
96
+ unsubscribe();
97
+ for (const id of preview.saved.keys())
98
+ restore(id);
99
+ };
100
+ }, [preview]);
39
101
  useFrame(() => {
40
102
  // Nodes with a live preview (override / transform) are reapplied EVERY
41
103
  // frame, not only while dirty: the React commit that rebinds the group's
@@ -64,6 +126,11 @@ export const FloorElevationSystem = () => {
64
126
  const position = effectiveNode.position;
65
127
  if (!position)
66
128
  return;
129
+ if (effectiveNode.parentId !== node.parentId)
130
+ return;
131
+ if (!(def.geometry || def.system) && dirtyNodes.has(id)) {
132
+ clearDirty(id);
133
+ }
67
134
  // `applies === false` means the kind opts OUT of floor stacking for this
68
135
  // node: its Y belongs to a host frame (a wall/ceiling-mounted item, a
69
136
  // cabinet module inside a run, a wall duct terminal). `getFloorPlacedElevation`
@@ -73,6 +140,10 @@ export const FloorElevationSystem = () => {
73
140
  // the host frame's own elevation.
74
141
  if (floorPlaced.applies && !floorPlaced.applies(effectiveNode))
75
142
  return;
143
+ // Hosted meshes inherit elevation from their parent (and possibly a surface group).
144
+ // Their live transform can be world-space, so it cannot replace the mesh's local Y.
145
+ if (!effectiveNode.parentId || nodes[effectiveNode.parentId]?.type !== 'level')
146
+ return;
76
147
  // This system is the single drag-time authority for floor-stack mesh Y:
77
148
  // tools publish base positions to live stores, renderers may
78
149
  // reconcile that base Y onto the group, then this presentation system
@@ -90,9 +161,6 @@ export const FloorElevationSystem = () => {
90
161
  maxElevation: liveTransform?.supportElevationCap,
91
162
  });
92
163
  mesh.position.y = visualPosition[1];
93
- if (!(def.geometry || def.system) && dirtyNodes.has(id)) {
94
- clearDirty(id);
95
- }
96
164
  };
97
165
  dirtyNodes.forEach((id) => {
98
166
  applyLift(id);
@@ -106,5 +174,56 @@ export const FloorElevationSystem = () => {
106
174
  applyLift(id);
107
175
  });
108
176
  }, 1);
177
+ // PostProcessing draws at priority 1 after this system; later callbacks would show one wrong frame per move.
178
+ useFrame(() => {
179
+ const nodes = useScene.getState().nodes;
180
+ for (const [id, patch] of useLiveNodeOverrides.getState().overrides) {
181
+ const node = nodes[id];
182
+ if (!node || !patch.parentId || patch.parentId === node.parentId)
183
+ continue;
184
+ const effective = getEffectiveNode(node);
185
+ if (effective.parentId === node.parentId || !effective.position)
186
+ continue;
187
+ if (!effective.parentId || nodes[effective.parentId]?.type !== 'level')
188
+ continue;
189
+ const mesh = sceneRegistry.nodes.get(id);
190
+ const level = sceneRegistry.nodes.get(effective.parentId);
191
+ if (!mesh?.parent || !level)
192
+ continue;
193
+ const previous = preview.saved.get(id);
194
+ if (previous?.mesh !== mesh) {
195
+ if (previous)
196
+ restoreMountedExitPose(previous);
197
+ preview.saved.set(id, {
198
+ mesh,
199
+ parent: mesh.parent,
200
+ position: mesh.position.clone(),
201
+ quaternion: mesh.quaternion.clone(),
202
+ scale: mesh.scale.clone(),
203
+ autoUpdate: mesh.matrixAutoUpdate,
204
+ });
205
+ }
206
+ const position = getFloorStackedPosition({
207
+ node: effective,
208
+ nodes,
209
+ position: effective.position,
210
+ });
211
+ const rotation = typeof effective.rotation === 'number'
212
+ ? [0, effective.rotation, 0]
213
+ : (effective.rotation ?? [0, 0, 0]);
214
+ level.updateWorldMatrix(true, false);
215
+ mesh.parent.updateWorldMatrix(true, false);
216
+ // The override's parent is logical; the mesh still inherits the mounted host and surface wrapper.
217
+ preview.local
218
+ .copy(mesh.parent.matrixWorld)
219
+ .invert()
220
+ .multiply(level.matrixWorld)
221
+ .multiply(preview.target.compose(preview.position.fromArray(position), preview.quaternion.setFromEuler(preview.rotation.set(rotation[0], rotation[1], rotation[2])), preview.unit));
222
+ // Keep the full matrix: a rotated child beneath nonuniform scale can require shear.
223
+ mesh.matrixAutoUpdate = false;
224
+ mesh.matrix.copy(preview.local);
225
+ mesh.updateMatrixWorld(true);
226
+ }
227
+ }, 1);
109
228
  return null;
110
229
  };
@@ -1 +1 @@
1
- {"version":3,"file":"geometry-system.d.ts","sourceRoot":"","sources":["../../../src/systems/geometry/geometry-system.tsx"],"names":[],"mappings":"AAkBA,OAAO,EAAa,KAAK,KAAK,EAA4B,KAAK,QAAQ,EAAE,MAAM,OAAO,CAAA;AAUtF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,eAAO,MAAM,cAAc,YAuM1B,CAAA;AA8ID,eAAe,cAAc,CAAA;AAE7B,MAAM,MAAM,uBAAuB,GAAG;IACpC,KAAK,EAAE,KAAK,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;CACZ,CAAA;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI,CAU7D;AAED,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,uBAAuB,CAAC,EAC3C,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,KAAK,EACZ,GAAG,EAAE,MAAM,GACV,OAAO,CAKT"}
1
+ {"version":3,"file":"geometry-system.d.ts","sourceRoot":"","sources":["../../../src/systems/geometry/geometry-system.tsx"],"names":[],"mappings":"AAkBA,OAAO,EAAa,KAAK,KAAK,EAA4B,KAAK,QAAQ,EAAE,MAAM,OAAO,CAAA;AAUtF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,eAAO,MAAM,cAAc,YAuM1B,CAAA;AAiJD,eAAe,cAAc,CAAA;AAE7B,MAAM,MAAM,uBAAuB,GAAG;IACpC,KAAK,EAAE,KAAK,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;CACZ,CAAA;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI,CAU7D;AAED,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,uBAAuB,CAAC,EAC3C,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,KAAK,EACZ,GAAG,EAAE,MAAM,GACV,OAAO,CAKT"}
@@ -212,7 +212,11 @@ function liveChildOverrideKey(node) {
212
212
  return '';
213
213
  const overrides = useLiveNodeOverrides.getState().overrides;
214
214
  const entries = [];
215
+ const childTypes = nodeRegistry.get(node.type)?.geometryChildTypes;
216
+ const nodes = useScene.getState().nodes;
215
217
  for (const childId of childIds) {
218
+ if (childTypes && !childTypes.includes(nodes[childId]?.type ?? ''))
219
+ continue;
216
220
  const override = overrides.get(childId);
217
221
  if (override)
218
222
  entries.push([childId, override]);
@@ -3,7 +3,7 @@ import { useFrame } from '@react-three/fiber';
3
3
  import { lerp } from 'three/src/math/MathUtils.js';
4
4
  import { applyShadowOnly, clearShadowOnly } from '../../lib/shadow-only';
5
5
  import useViewer from '../../store/use-viewer';
6
- import { EXPLODED_GAP } from './level-utils';
6
+ import { EXPLODED_GAP, resolveLevelVisibility } from './level-utils';
7
7
  // Levels currently in shadow-caster-only mode (solo hides them from the color
8
8
  // passes but keeps their sun shadows). Tracked so we can restore layer masks
9
9
  // exactly once on transition; apply re-runs every frame so meshes rebuilt
@@ -42,23 +42,23 @@ export const LevelSystem = () => {
42
42
  // (blank viewport). Clamping keeps every step a contraction: identical
43
43
  // feel at 60 fps, exact snap instead of overshoot on slow frames.
44
44
  obj.position.y = lerp(obj.position.y, targetY, Math.min(1, delta * 12));
45
- // Solo: hidden levels ABOVE the soloed one stay in the shadow map
46
- // (shadow-caster-only) so the sun still shadows the soloed floor through
47
- // them; levels below can't block the sun, so they plain-hide.
48
- const hidden = levelMode === 'solo' && Boolean(selectedLevel) && level?.id !== selectedLevel;
49
- const castsWhileHidden = hidden && selectedIndex !== undefined && index > selectedIndex;
50
- if (castsWhileHidden) {
45
+ const { visible, shadowOnly } = resolveLevelVisibility({
46
+ levelMode,
47
+ hasSelectedLevel: Boolean(selectedLevel),
48
+ isSelected: level?.id === selectedLevel,
49
+ index,
50
+ selectedIndex,
51
+ nodeVisible: level?.visible !== false,
52
+ });
53
+ if (shadowOnly) {
51
54
  applyShadowOnly(obj);
52
55
  shadowOnlyLevels.add(obj);
53
- obj.visible = true;
54
56
  }
55
- else {
56
- if (shadowOnlyLevels.has(obj)) {
57
- clearShadowOnly(obj);
58
- shadowOnlyLevels.delete(obj);
59
- }
60
- obj.visible = !hidden;
57
+ else if (shadowOnlyLevels.has(obj)) {
58
+ clearShadowOnly(obj);
59
+ shadowOnlyLevels.delete(obj);
61
60
  }
61
+ obj.visible = visible;
62
62
  }
63
63
  }, 5); // Using a lower priority so it runs after transforms from other systems have settled
64
64
  return null;
@@ -7,9 +7,33 @@ export declare const EXPLODED_GAP = 5;
7
7
  * code framing a level must aim at the destination, not the moving target.
8
8
  */
9
9
  export declare function getLevelPresentationY(levelId: string, nodes: Record<string, unknown>, levelMode: 'stacked' | 'exploded' | 'solo' | 'manual'): number;
10
+ /**
11
+ * Whether a level renders this frame, and whether it renders as a
12
+ * shadow-caster only.
13
+ *
14
+ * Two unrelated things hide a level and they do not compose: the author's own
15
+ * `visible` flag (the sidebar eye), which hides the floor outright, and solo
16
+ * mode, which hides every level but the soloed one — keeping the levels ABOVE
17
+ * it in the shadow map so the sun still shadows the soloed floor through them.
18
+ * A level the author hid never enters that shadow-caster branch: its shadows
19
+ * on the floor below would be exactly what hiding it was meant to remove.
20
+ */
21
+ export declare function resolveLevelVisibility({ levelMode, hasSelectedLevel, isSelected, index, selectedIndex, nodeVisible, }: {
22
+ levelMode: 'stacked' | 'exploded' | 'solo' | 'manual';
23
+ hasSelectedLevel: boolean;
24
+ isSelected: boolean;
25
+ index: number;
26
+ selectedIndex: number | undefined;
27
+ nodeVisible: boolean;
28
+ }): {
29
+ visible: boolean;
30
+ shadowOnly: boolean;
31
+ };
10
32
  /**
11
33
  * Instantly snaps all level Objects3D to their true stacked Y positions
12
- * (ignores levelMode — always uses stacked, no exploded gap).
34
+ * (ignores levelMode — always uses stacked, no exploded gap). Presentation
35
+ * hiding is undone with it, but a level the author hid stays hidden: the
36
+ * capture has to match the export, which prunes it.
13
37
  *
14
38
  * Returns a restore function that reverts each level's Y to what it was
15
39
  * before the snap, so lerp animations in LevelSystem can continue undisturbed.
@@ -1 +1 @@
1
- {"version":3,"file":"level-utils.d.ts","sourceRoot":"","sources":["../../../src/systems/level/level-utils.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,YAAY,IAAI,CAAA;AAE7B;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,SAAS,EAAE,SAAS,GAAG,UAAU,GAAG,MAAM,GAAG,QAAQ,GACpD,MAAM,CAKR;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,yBAAyB,IAAI,MAAM,IAAI,CAyCtD"}
1
+ {"version":3,"file":"level-utils.d.ts","sourceRoot":"","sources":["../../../src/systems/level/level-utils.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,YAAY,IAAI,CAAA;AAE7B;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,SAAS,EAAE,SAAS,GAAG,UAAU,GAAG,MAAM,GAAG,QAAQ,GACpD,MAAM,CAKR;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,CAAC,EACrC,SAAS,EACT,gBAAgB,EAChB,UAAU,EACV,KAAK,EACL,aAAa,EACb,WAAW,GACZ,EAAE;IACD,SAAS,EAAE,SAAS,GAAG,UAAU,GAAG,MAAM,GAAG,QAAQ,CAAA;IACrD,gBAAgB,EAAE,OAAO,CAAA;IACzB,UAAU,EAAE,OAAO,CAAA;IACnB,KAAK,EAAE,MAAM,CAAA;IACb,aAAa,EAAE,MAAM,GAAG,SAAS,CAAA;IACjC,WAAW,EAAE,OAAO,CAAA;CACrB,GAAG;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,UAAU,EAAE,OAAO,CAAA;CAAE,CAM5C;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,yBAAyB,IAAI,MAAM,IAAI,CA2CtD"}
@@ -13,9 +13,29 @@ export function getLevelPresentationY(levelId, nodes, levelMode) {
13
13
  const explodedExtra = levelMode === 'exploded' && level ? level.level * EXPLODED_GAP : 0;
14
14
  return baseY + explodedExtra;
15
15
  }
16
+ /**
17
+ * Whether a level renders this frame, and whether it renders as a
18
+ * shadow-caster only.
19
+ *
20
+ * Two unrelated things hide a level and they do not compose: the author's own
21
+ * `visible` flag (the sidebar eye), which hides the floor outright, and solo
22
+ * mode, which hides every level but the soloed one — keeping the levels ABOVE
23
+ * it in the shadow map so the sun still shadows the soloed floor through them.
24
+ * A level the author hid never enters that shadow-caster branch: its shadows
25
+ * on the floor below would be exactly what hiding it was meant to remove.
26
+ */
27
+ export function resolveLevelVisibility({ levelMode, hasSelectedLevel, isSelected, index, selectedIndex, nodeVisible, }) {
28
+ if (!nodeVisible)
29
+ return { visible: false, shadowOnly: false };
30
+ const hidden = levelMode === 'solo' && hasSelectedLevel && !isSelected;
31
+ const shadowOnly = hidden && selectedIndex !== undefined && index > selectedIndex;
32
+ return { visible: shadowOnly || !hidden, shadowOnly };
33
+ }
16
34
  /**
17
35
  * Instantly snaps all level Objects3D to their true stacked Y positions
18
- * (ignores levelMode — always uses stacked, no exploded gap).
36
+ * (ignores levelMode — always uses stacked, no exploded gap). Presentation
37
+ * hiding is undone with it, but a level the author hid stays hidden: the
38
+ * capture has to match the export, which prunes it.
19
39
  *
20
40
  * Returns a restore function that reverts each level's Y to what it was
21
41
  * before the snap, so lerp animations in LevelSystem can continue undisturbed.
@@ -35,16 +55,17 @@ export function snapLevelsToTruePositions() {
35
55
  entries.push({
36
56
  levelId,
37
57
  obj,
58
+ nodeVisible: level.visible !== false,
38
59
  });
39
60
  }
40
61
  });
41
62
  const levelElevations = getLevelElevations(nodes);
42
63
  // Snapshot current Y and visibility so we can restore them after the render
43
64
  const snapshot = new Map(entries.map(({ levelId, obj }) => [levelId, { y: obj.position.y, visible: obj.visible }]));
44
- // Snap to true stacked positions and make all levels visible
45
- for (const { levelId, obj } of entries) {
65
+ // Snap to true stacked positions and undo presentation hiding
66
+ for (const { levelId, obj, nodeVisible } of entries) {
46
67
  obj.position.y = levelElevations.get(levelId)?.baseY ?? 0;
47
- obj.visible = true;
68
+ obj.visible = nodeVisible;
48
69
  }
49
70
  return () => {
50
71
  for (const { levelId, obj } of entries) {
@@ -2,6 +2,7 @@ import { type WallNode } from '@pascal-app/core';
2
2
  import { type Camera, type Material, Matrix4, type Mesh, Vector3 } from 'three';
3
3
  import useViewer, { type WallMode } from '../../store/use-viewer';
4
4
  import { type WallMaterialVariant } from './wall-material-variant';
5
+ import { type WallMaterialsResolver } from './wall-materials';
5
6
  export declare function sameMaterialArray(a: Material | Material[], b: Material[]): boolean;
6
7
  type Variant = {
7
8
  key: WallMaterialVariant;
@@ -29,6 +30,7 @@ export type WallCutoutViewerStore = {
29
30
  };
30
31
  export declare class WallCutoutCache {
31
32
  private readonly viewerStore;
33
+ private readonly materialResolver;
32
34
  readonly walls: Map<string, CachedWall>;
33
35
  readonly rebuilt: Set<string>;
34
36
  private viewer;
@@ -46,7 +48,8 @@ export declare class WallCutoutCache {
46
48
  private selected;
47
49
  private highlightKey;
48
50
  private transformed;
49
- constructor(viewerStore?: WallCutoutViewerStore);
51
+ private overrides;
52
+ constructor(viewerStore?: WallCutoutViewerStore, materialResolver?: WallMaterialsResolver);
50
53
  subscribeLiveTransforms(): () => void;
51
54
  update(camera: Camera, time: number): void;
52
55
  private refreshAppearance;
@@ -1 +1 @@
1
- {"version":3,"file":"wall-cutout-cache.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-cutout-cache.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"wall-cutout-cache.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-cutout-cache.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  // New kind-specific modules belong in nodes; viewer must not depend on nodes.
2
2
  // This extends the existing viewer-owned wall cutout and material implementation.
3
- import { getLibraryMaterialsVersion, getWallFaceBandConfig, getWallPlaneTop, resolveLevelId, resolveWallEffectiveHeight, sceneRegistry, spatialGridManager, useLiveTransforms, useScene, } from '@pascal-app/core';
3
+ import { getEffectiveNode, getLibraryMaterialsVersion, getWallFaceBandConfig, getWallPlaneTop, resolveLevelId, resolveWallEffectiveHeight, sceneRegistry, spatialGridManager, useLiveNodeOverrides, useLiveTransforms, useScene, } from '@pascal-app/core';
4
4
  import { Matrix4, Vector3 } from 'three';
5
5
  import { getMaterialTextureVersion } from '../../lib/materials';
6
6
  import useViewer, {} from '../../store/use-viewer';
@@ -59,6 +59,7 @@ export function wallFacingNegative(dot, previous) {
59
59
  }
60
60
  export class WallCutoutCache {
61
61
  viewerStore;
62
+ materialResolver;
62
63
  walls = new Map();
63
64
  rebuilt = new Set();
64
65
  viewer;
@@ -76,8 +77,10 @@ export class WallCutoutCache {
76
77
  selected = new Set();
77
78
  highlightKey = '';
78
79
  transformed = new Set();
79
- constructor(viewerStore = useViewer) {
80
+ overrides = useLiveNodeOverrides.getState().overrides;
81
+ constructor(viewerStore = useViewer, materialResolver = getMaterialsForWall) {
80
82
  this.viewerStore = viewerStore;
83
+ this.materialResolver = materialResolver;
81
84
  }
82
85
  subscribeLiveTransforms() {
83
86
  return useLiveTransforms.subscribe((state, previous) => {
@@ -98,6 +101,8 @@ export class WallCutoutCache {
98
101
  const libraryVersion = getLibraryMaterialsVersion();
99
102
  const textureVersion = getMaterialTextureVersion();
100
103
  const previous = this.viewer;
104
+ const overrides = useLiveNodeOverrides.getState().overrides;
105
+ const overridesChanged = this.overrides !== overrides;
101
106
  const nodesChanged = this.nodes !== scene.nodes;
102
107
  const registryChanged = this.registryRevision !== sceneRegistry.revision || this.wallCount !== wallIds.size;
103
108
  let highlightChanged = false;
@@ -128,7 +133,8 @@ export class WallCutoutCache {
128
133
  : null;
129
134
  const releasedPreview = previewId(previous) !== previewId(viewer) ? previewId(previous) : null;
130
135
  this.viewer = viewer;
131
- const invalidated = appearanceChanged ||
136
+ const invalidated = overridesChanged ||
137
+ appearanceChanged ||
132
138
  nodesChanged ||
133
139
  registryChanged ||
134
140
  this.rebuilt.size > 0 ||
@@ -195,16 +201,18 @@ export class WallCutoutCache {
195
201
  const rebuilt = this.rebuilt.has(id);
196
202
  if (added || changed || rebuilt)
197
203
  this.refreshNormal(wall, visited);
198
- wall.node = node;
199
- if (added || appearanceChanged || (nodesChanged && changed))
204
+ const overrideChanged = this.overrides.get(id) !== overrides.get(id);
205
+ wall.node = getEffectiveNode(node);
206
+ if (added || appearanceChanged || overrideChanged || (nodesChanged && changed))
200
207
  this.refreshAppearance(wall);
201
208
  if (added ||
202
209
  appearanceChanged ||
210
+ overrideChanged ||
203
211
  changed ||
204
212
  rebuilt ||
205
213
  releasedPreview === id ||
206
214
  cameraChanged) {
207
- this.apply(wall, viewer.wallMode, added || appearanceChanged || (nodesChanged && changed));
215
+ this.apply(wall, viewer.wallMode, added || appearanceChanged || overrideChanged || (nodesChanged && changed));
208
216
  }
209
217
  }
210
218
  }
@@ -214,6 +222,7 @@ export class WallCutoutCache {
214
222
  }
215
223
  this.rebuilt.clear();
216
224
  this.transformed.clear();
225
+ this.overrides = overrides;
217
226
  this.nodes = scene.nodes;
218
227
  this.materials = scene.materials;
219
228
  this.registryRevision = sceneRegistry.revision;
@@ -238,7 +247,7 @@ export class WallCutoutCache {
238
247
  const height = resolveWallEffectiveHeight(node, getWallPlaneTop(node, levelId, scene.nodes), support.elevation);
239
248
  selectionHighlighted = !getWallFaceBandConfig(node, height).enabled;
240
249
  }
241
- const materials = getMaterialsForWall(node, viewer.shading, viewer.textures, viewer.colorPreset, viewer.sceneTheme, scene.materials);
250
+ const materials = this.materialResolver(node, viewer.shading, viewer.textures, viewer.colorPreset, viewer.sceneTheme, scene.materials);
242
251
  const variant = (hidden) => {
243
252
  const key = resolveWallMaterialVariant({
244
253
  translucentMode: viewer.wallMode === 'translucent',
@@ -3,6 +3,7 @@ import type { Camera } from 'three';
3
3
  import { type Mesh, Vector3 } from 'three/webgpu';
4
4
  import { type WallMode } from '../../store/use-viewer';
5
5
  import { WallCutoutCache, type WallCutoutViewerStore } from './wall-cutout-cache';
6
+ import { type WallMaterialsResolver } from './wall-materials';
6
7
  export declare const WALL_CUTOUT_FRAME_PRIORITY = 0;
7
8
  export declare function runWallCutoutFrame(cache: WallCutoutCache, { camera, clock }: {
8
9
  camera: Camera;
@@ -19,7 +20,8 @@ export declare function runWallCutoutFrame(cache: WallCutoutCache, { camera, clo
19
20
  * from the assigned material variant.
20
21
  */
21
22
  export declare function getWallHideState(wallNode: WallNode, wallMesh: Mesh, wallMode: WallMode, cameraDir: Vector3): boolean;
22
- export declare const WallCutout: ({ viewerStore, }: {
23
+ export declare const WallCutout: ({ viewerStore, materialResolver, }: {
23
24
  viewerStore?: WallCutoutViewerStore;
25
+ materialResolver?: WallMaterialsResolver;
24
26
  }) => null;
25
27
  //# sourceMappingURL=wall-cutout.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"wall-cutout.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-cutout.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAoD,KAAK,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAGlG,OAAO,KAAK,EAAE,MAAM,EAAY,MAAM,OAAO,CAAA;AAC7C,OAAO,EAAE,KAAK,IAAI,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AACjD,OAAkB,EAAE,KAAK,QAAQ,EAAE,MAAM,wBAAwB,CAAA;AACjE,OAAO,EAEL,eAAe,EACf,KAAK,qBAAqB,EAE3B,MAAM,qBAAqB,CAAA;AAM5B,eAAO,MAAM,0BAA0B,IAAI,CAAA;AAE3C,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,eAAe,EACtB,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE;QAAE,WAAW,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,QAGtE;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,QAAQ,EAClB,QAAQ,EAAE,IAAI,EACd,QAAQ,EAAE,QAAQ,EAClB,SAAS,EAAE,OAAO,GACjB,OAAO,CAKT;AAED,eAAO,MAAM,UAAU,GAAI,kBAExB;IACD,WAAW,CAAC,EAAE,qBAAqB,CAAA;CACpC,SA2DA,CAAA"}
1
+ {"version":3,"file":"wall-cutout.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-cutout.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAoD,KAAK,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAGlG,OAAO,KAAK,EAAE,MAAM,EAAY,MAAM,OAAO,CAAA;AAC7C,OAAO,EAAE,KAAK,IAAI,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AACjD,OAAkB,EAAE,KAAK,QAAQ,EAAE,MAAM,wBAAwB,CAAA;AACjE,OAAO,EAEL,eAAe,EACf,KAAK,qBAAqB,EAE3B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAGL,KAAK,qBAAqB,EAC3B,MAAM,kBAAkB,CAAA;AAKzB,eAAO,MAAM,0BAA0B,IAAI,CAAA;AAE3C,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,eAAe,EACtB,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE;QAAE,WAAW,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,QAGtE;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,QAAQ,EAClB,QAAQ,EAAE,IAAI,EACd,QAAQ,EAAE,QAAQ,EAClB,SAAS,EAAE,OAAO,GACjB,OAAO,CAKT;AAED,eAAO,MAAM,UAAU,GAAI,oCAGxB;IACD,WAAW,CAAC,EAAE,qBAAqB,CAAA;IACnC,gBAAgB,CAAC,EAAE,qBAAqB,CAAA;CACzC,SA8DA,CAAA"}
@@ -4,7 +4,7 @@ import { useEffect, useMemo } from 'react';
4
4
  import { Vector3 } from 'three/webgpu';
5
5
  import useViewer, {} from '../../store/use-viewer';
6
6
  import { sameMaterialArray, WallCutoutCache, wallHiddenFromFacing, } from './wall-cutout-cache';
7
- import { getMaterialsForWall, getSelectionHighlightMaterials } from './wall-materials';
7
+ import { getMaterialsForWall, getSelectionHighlightMaterials, } from './wall-materials';
8
8
  import { subscribeWallRebuilds } from './wall-rebuild-notifications';
9
9
  const v = new Vector3();
10
10
  export const WALL_CUTOUT_FRAME_PRIORITY = 0;
@@ -27,8 +27,8 @@ export function getWallHideState(wallNode, wallMesh, wallMode, cameraDir) {
27
27
  wallMesh.getWorldDirection(v);
28
28
  return wallHiddenFromFacing(wallNode, wallMode, v.dot(cameraDir) < 0);
29
29
  }
30
- export const WallCutout = ({ viewerStore = useViewer, }) => {
31
- const cache = useMemo(() => new WallCutoutCache(viewerStore), [viewerStore]);
30
+ export const WallCutout = ({ viewerStore = useViewer, materialResolver = getMaterialsForWall, }) => {
31
+ const cache = useMemo(() => new WallCutoutCache(viewerStore, materialResolver), [viewerStore, materialResolver]);
32
32
  useEffect(() => subscribeWallRebuilds((id) => cache.rebuilt.add(id)), [cache]);
33
33
  useEffect(() => cache.subscribeLiveTransforms(), [cache]);
34
34
  // Camera changes reach PostProcessing (1) in this frame. WallSystem (4)
@@ -44,7 +44,7 @@ export const WallCutout = ({ viewerStore = useViewer, }) => {
44
44
  const wallNode = useScene.getState().nodes[wallId];
45
45
  if (wallNode?.type !== 'wall')
46
46
  return;
47
- const mats = getMaterialsForWall(wallNode, viewerStore.getState().shading, viewerStore.getState().textures, viewerStore.getState().colorPreset, viewerStore.getState().sceneTheme, useScene.getState().materials);
47
+ const mats = materialResolver(wallNode, viewerStore.getState().shading, viewerStore.getState().textures, viewerStore.getState().colorPreset, viewerStore.getState().sceneTheme, useScene.getState().materials);
48
48
  const current = wallMesh.material;
49
49
  snapshot.set(wallMesh, current);
50
50
  if (current === mats.deleteVisible) {
@@ -71,6 +71,6 @@ export const WallCutout = ({ viewerStore = useViewer, }) => {
71
71
  emitter.off('thumbnail:before-capture', restoreForCapture);
72
72
  emitter.off('thumbnail:after-capture', reapplyAfterCapture);
73
73
  };
74
- }, [viewerStore]);
74
+ }, [viewerStore, materialResolver]);
75
75
  return null;
76
76
  };
@@ -11,7 +11,16 @@ export interface WallMaterials {
11
11
  deleteInvisible: WallMaterialArray;
12
12
  deleteTranslucent: WallMaterialArray;
13
13
  materialHash: string;
14
+ ownedVisible?: Material[];
14
15
  }
16
+ export type WallMaterialOverride = {
17
+ hash: string;
18
+ create: () => {
19
+ visible: WallMaterialArray;
20
+ owned?: Material[];
21
+ };
22
+ };
23
+ export type WallMaterialsResolver = (wallNode: WallNode, shading?: RenderShading, textures?: boolean, colorPreset?: ColorPreset, sceneTheme?: string, sceneMaterials?: SceneMaterials) => WallMaterials;
15
24
  /** Lazy light-emissive selection variant of a wall's material array (keeps texture). */
16
25
  export declare function getSelectionHighlightMaterials(materials: WallMaterialArray): WallMaterialArray;
17
26
  /**
@@ -20,8 +29,8 @@ export declare function getSelectionHighlightMaterials(materials: WallMaterialAr
20
29
  * film so the wall the click would select reads under the cursor).
21
30
  */
22
31
  export declare function getHoverHighlightMaterials(materials: WallMaterialArray): WallMaterialArray;
23
- export declare function getWallMaterialHash(wallNode: WallNode, shading: RenderShading, sceneMaterials?: SceneMaterials): string;
24
- export declare function getMaterialsForWall(wallNode: WallNode, shading?: RenderShading, textures?: boolean, colorPreset?: ColorPreset, sceneTheme?: string, sceneMaterials?: SceneMaterials): WallMaterials;
32
+ export declare function getWallMaterialHash(wallNode: WallNode, shading: RenderShading, sceneMaterials?: SceneMaterials, overrideHash?: string): string;
33
+ export declare function getMaterialsForWall(wallNode: WallNode, shading?: RenderShading, textures?: boolean, colorPreset?: ColorPreset, sceneTheme?: string, sceneMaterials?: SceneMaterials, override?: WallMaterialOverride): WallMaterials;
25
34
  export declare function getVisibleWallMaterials(wallNode: WallNode, shading?: RenderShading, textures?: boolean, colorPreset?: ColorPreset, sceneTheme?: string, sceneMaterials?: SceneMaterials): WallMaterialArray;
26
35
  export {};
27
36
  //# sourceMappingURL=wall-materials.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"wall-materials.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-materials.ts"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,aAAa,EAClB,KAAK,eAAe,EAGpB,KAAK,QAAQ,EAId,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAS,KAAK,QAAQ,EAAE,MAAM,OAAO,CAAA;AAG5C,OAAO,EAEL,KAAK,WAAW,EAMhB,KAAK,aAAa,EAGnB,MAAM,qBAAqB,CAAA;AAE5B,KAAK,cAAc,GAAG,MAAM,CAAC,eAAe,EAAE,aAAa,CAAC,GAAG,SAAS,CAAA;AAexE,MAAM,MAAM,iBAAiB,GAAG,QAAQ,EAAE,CAAA;AAE1C,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,iBAAiB,CAAA;IAC1B,SAAS,EAAE,iBAAiB,CAAA;IAC5B,WAAW,EAAE,iBAAiB,CAAA;IAC9B,aAAa,EAAE,iBAAiB,CAAA;IAChC,eAAe,EAAE,iBAAiB,CAAA;IAClC,iBAAiB,EAAE,iBAAiB,CAAA;IACpC,YAAY,EAAE,MAAM,CAAA;CACrB;AA+SD,wFAAwF;AACxF,wBAAgB,8BAA8B,CAAC,SAAS,EAAE,iBAAiB,GAAG,iBAAiB,CAS9F;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,SAAS,EAAE,iBAAiB,GAAG,iBAAiB,CAS1F;AA4DD,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,aAAa,EACtB,cAAc,CAAC,EAAE,cAAc,GAC9B,MAAM,CAcR;AAED,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,QAAQ,EAClB,OAAO,GAAE,aAA0B,EACnC,QAAQ,UAAO,EACf,WAAW,GAAE,WAAoB,EACjC,UAAU,CAAC,EAAE,MAAM,EACnB,cAAc,CAAC,EAAE,cAAc,GAC9B,aAAa,CAgIf;AAED,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,QAAQ,EAClB,OAAO,GAAE,aAA0B,EACnC,QAAQ,UAAO,EACf,WAAW,GAAE,WAAoB,EACjC,UAAU,CAAC,EAAE,MAAM,EACnB,cAAc,CAAC,EAAE,cAAc,GAC9B,iBAAiB,CAGnB"}
1
+ {"version":3,"file":"wall-materials.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-materials.ts"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,aAAa,EAClB,KAAK,eAAe,EAGpB,KAAK,QAAQ,EAId,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAS,KAAK,QAAQ,EAAE,MAAM,OAAO,CAAA;AAG5C,OAAO,EAEL,KAAK,WAAW,EAMhB,KAAK,aAAa,EAGnB,MAAM,qBAAqB,CAAA;AAE5B,KAAK,cAAc,GAAG,MAAM,CAAC,eAAe,EAAE,aAAa,CAAC,GAAG,SAAS,CAAA;AAexE,MAAM,MAAM,iBAAiB,GAAG,QAAQ,EAAE,CAAA;AAE1C,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,iBAAiB,CAAA;IAC1B,SAAS,EAAE,iBAAiB,CAAA;IAC5B,WAAW,EAAE,iBAAiB,CAAA;IAC9B,aAAa,EAAE,iBAAiB,CAAA;IAChC,eAAe,EAAE,iBAAiB,CAAA;IAClC,iBAAiB,EAAE,iBAAiB,CAAA;IACpC,YAAY,EAAE,MAAM,CAAA;IACpB,YAAY,CAAC,EAAE,QAAQ,EAAE,CAAA;CAC1B;AAED,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM;QAAE,OAAO,EAAE,iBAAiB,CAAC;QAAC,KAAK,CAAC,EAAE,QAAQ,EAAE,CAAA;KAAE,CAAA;CACjE,CAAA;AAED,MAAM,MAAM,qBAAqB,GAAG,CAClC,QAAQ,EAAE,QAAQ,EAClB,OAAO,CAAC,EAAE,aAAa,EACvB,QAAQ,CAAC,EAAE,OAAO,EAClB,WAAW,CAAC,EAAE,WAAW,EACzB,UAAU,CAAC,EAAE,MAAM,EACnB,cAAc,CAAC,EAAE,cAAc,KAC5B,aAAa,CAAA;AA+SlB,wFAAwF;AACxF,wBAAgB,8BAA8B,CAAC,SAAS,EAAE,iBAAiB,GAAG,iBAAiB,CAS9F;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,SAAS,EAAE,iBAAiB,GAAG,iBAAiB,CAS1F;AA4DD,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,aAAa,EACtB,cAAc,CAAC,EAAE,cAAc,EAC/B,YAAY,CAAC,EAAE,MAAM,GACpB,MAAM,CAeR;AAED,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,QAAQ,EAClB,OAAO,GAAE,aAA0B,EACnC,QAAQ,UAAO,EACf,WAAW,GAAE,WAAoB,EACjC,UAAU,CAAC,EAAE,MAAM,EACnB,cAAc,CAAC,EAAE,cAAc,EAC/B,QAAQ,CAAC,EAAE,oBAAoB,GAC9B,aAAa,CAqIf;AAED,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,QAAQ,EAClB,OAAO,GAAE,aAA0B,EACnC,QAAQ,UAAO,EACf,WAAW,GAAE,WAAoB,EACjC,UAAU,CAAC,EAAE,MAAM,EACnB,cAAc,CAAC,EAAE,cAAc,GAC9B,iBAAiB,CAGnB"}
@@ -286,9 +286,10 @@ function disposeOwnedMaterials(materials) {
286
286
  material.dispose();
287
287
  });
288
288
  }
289
- export function getWallMaterialHash(wallNode, shading, sceneMaterials) {
289
+ export function getWallMaterialHash(wallNode, shading, sceneMaterials, overrideHash) {
290
290
  return JSON.stringify({
291
291
  shading,
292
+ overrideHash,
292
293
  interior: wallFaceMaterialSignature(wallNode, 'interior', sceneMaterials),
293
294
  exterior: wallFaceMaterialSignature(wallNode, 'exterior', sceneMaterials),
294
295
  lowerInterior: wallSlotMaterialSignature(wallNode, 'lowerInterior', sceneMaterials),
@@ -301,10 +302,10 @@ export function getWallMaterialHash(wallNode, shading, sceneMaterials) {
301
302
  topExterior: wallSlotMaterialSignature(wallNode, 'topExterior', sceneMaterials),
302
303
  });
303
304
  }
304
- export function getMaterialsForWall(wallNode, shading = 'rendered', textures = true, colorPreset = 'clay', sceneTheme, sceneMaterials) {
305
+ export function getMaterialsForWall(wallNode, shading = 'rendered', textures = true, colorPreset = 'clay', sceneTheme, sceneMaterials, override) {
305
306
  const cacheKey = `${wallNode.id}-${shading}-${textures}-${colorPreset}-${sceneTheme ?? 'base'}`;
306
307
  const materialHash = textures
307
- ? getWallMaterialHash(wallNode, shading, sceneMaterials)
308
+ ? getWallMaterialHash(wallNode, shading, sceneMaterials, override?.hash)
308
309
  : JSON.stringify({ textures, colorPreset, sceneTheme });
309
310
  const existing = wallMaterialCache.get(cacheKey);
310
311
  if (existing && existing.materialHash === materialHash) {
@@ -312,6 +313,7 @@ export function getMaterialsForWall(wallNode, shading = 'rendered', textures = t
312
313
  }
313
314
  if (existing) {
314
315
  disposeOwnedMaterials([
316
+ ...(existing.ownedVisible ? [existing.ownedVisible] : []),
315
317
  existing.invisible,
316
318
  existing.translucent,
317
319
  existing.deleteVisible,
@@ -320,25 +322,28 @@ export function getMaterialsForWall(wallNode, shading = 'rendered', textures = t
320
322
  ]);
321
323
  }
322
324
  const wallRoleMaterial = createSurfaceRoleMaterial('wall', colorPreset, undefined, sceneTheme);
325
+ const resolvedOverride = textures ? override?.create() : undefined;
323
326
  // Colored mode: each face resolves slot-first (node.slots ref → legacy inline
324
327
  // fields → declared slot default, parity with the retired DEFAULT_WALL_MATERIAL).
325
328
  // Textures-off collapses every face to the themed wall role (the guaranteed
326
329
  // escape hatch). The edge/cap slot (index 0) stays role-based.
327
- const visible = textures
328
- ? [
329
- wallRoleMaterial,
330
- resolveWallFaceMaterial(wallNode, 'interior', shading, sceneMaterials),
331
- resolveWallFaceMaterial(wallNode, 'exterior', shading, sceneMaterials),
332
- resolveWallSlotMaterial(wallNode, 'lowerInterior', shading, sceneMaterials),
333
- resolveWallSlotMaterial(wallNode, 'middleInterior', shading, sceneMaterials),
334
- resolveWallSlotMaterial(wallNode, 'upperInterior', shading, sceneMaterials),
335
- resolveWallSlotMaterial(wallNode, 'topInterior', shading, sceneMaterials),
336
- resolveWallSlotMaterial(wallNode, 'lowerExterior', shading, sceneMaterials),
337
- resolveWallSlotMaterial(wallNode, 'middleExterior', shading, sceneMaterials),
338
- resolveWallSlotMaterial(wallNode, 'upperExterior', shading, sceneMaterials),
339
- resolveWallSlotMaterial(wallNode, 'topExterior', shading, sceneMaterials),
340
- ]
341
- : Array.from({ length: 11 }, () => wallRoleMaterial);
330
+ const visible = resolvedOverride
331
+ ? resolvedOverride.visible
332
+ : textures
333
+ ? [
334
+ wallRoleMaterial,
335
+ resolveWallFaceMaterial(wallNode, 'interior', shading, sceneMaterials),
336
+ resolveWallFaceMaterial(wallNode, 'exterior', shading, sceneMaterials),
337
+ resolveWallSlotMaterial(wallNode, 'lowerInterior', shading, sceneMaterials),
338
+ resolveWallSlotMaterial(wallNode, 'middleInterior', shading, sceneMaterials),
339
+ resolveWallSlotMaterial(wallNode, 'upperInterior', shading, sceneMaterials),
340
+ resolveWallSlotMaterial(wallNode, 'topInterior', shading, sceneMaterials),
341
+ resolveWallSlotMaterial(wallNode, 'lowerExterior', shading, sceneMaterials),
342
+ resolveWallSlotMaterial(wallNode, 'middleExterior', shading, sceneMaterials),
343
+ resolveWallSlotMaterial(wallNode, 'upperExterior', shading, sceneMaterials),
344
+ resolveWallSlotMaterial(wallNode, 'topExterior', shading, sceneMaterials),
345
+ ]
346
+ : Array.from({ length: 11 }, () => wallRoleMaterial);
342
347
  const wallRoleColor = resolveSurfaceColor('wall', colorPreset, sceneTheme);
343
348
  const invisible = [
344
349
  createInvisibleWallMaterial(wallRoleColor, textures ? shading : 'solid'),
@@ -387,6 +392,7 @@ export function getMaterialsForWall(wallNode, shading = 'rendered', textures = t
387
392
  deleteInvisible,
388
393
  deleteTranslucent,
389
394
  materialHash,
395
+ ownedVisible: resolvedOverride?.owned,
390
396
  };
391
397
  wallMaterialCache.set(cacheKey, result);
392
398
  return result;