@pascal-app/viewer 1.0.1 → 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 (41) hide show
  1. package/dist/components/viewer/index.d.ts +12 -0
  2. package/dist/components/viewer/index.d.ts.map +1 -1
  3. package/dist/components/viewer/index.js +28 -7
  4. package/dist/components/viewer/viewer-camera.d.ts +15 -1
  5. package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
  6. package/dist/components/viewer/viewer-camera.js +43 -5
  7. package/dist/index.d.ts +7 -5
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +7 -5
  10. package/dist/lib/csg-utils.d.ts +1 -1
  11. package/dist/lib/csg-utils.d.ts.map +1 -1
  12. package/dist/lib/csg-utils.js +1 -1
  13. package/dist/lib/renderer-capability.d.ts +2 -1
  14. package/dist/lib/renderer-capability.d.ts.map +1 -1
  15. package/dist/lib/renderer-capability.js +2 -2
  16. package/dist/systems/door/door-system.d.ts.map +1 -1
  17. package/dist/systems/door/door-system.js +18 -5
  18. package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -1
  19. package/dist/systems/floor-elevation/floor-elevation-system.js +115 -0
  20. package/dist/systems/level/level-system.js +14 -14
  21. package/dist/systems/level/level-utils.d.ts +25 -1
  22. package/dist/systems/level/level-utils.d.ts.map +1 -1
  23. package/dist/systems/level/level-utils.js +25 -4
  24. package/dist/systems/wall/wall-cutout-cache.d.ts +4 -1
  25. package/dist/systems/wall/wall-cutout-cache.d.ts.map +1 -1
  26. package/dist/systems/wall/wall-cutout-cache.js +16 -7
  27. package/dist/systems/wall/wall-cutout.d.ts +3 -1
  28. package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
  29. package/dist/systems/wall/wall-cutout.js +5 -5
  30. package/dist/systems/wall/wall-materials.d.ts +11 -2
  31. package/dist/systems/wall/wall-materials.d.ts.map +1 -1
  32. package/dist/systems/wall/wall-materials.js +24 -18
  33. package/dist/systems/wall/wall-system.d.ts +15 -2
  34. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  35. package/dist/systems/wall/wall-system.js +20 -21
  36. package/dist/systems/window/window-system.d.ts.map +1 -1
  37. package/dist/systems/window/window-system.js +3 -2
  38. package/dist/xr/presentation-context.d.ts +7 -0
  39. package/dist/xr/presentation-context.d.ts.map +1 -0
  40. package/dist/xr/presentation-context.js +10 -0
  41. package/package.json +4 -4
@@ -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;
@@ -11,8 +11,21 @@ export declare function mergeWallCutoutBrushes(brushes: readonly Brush[]): {
11
11
  export declare function shouldDeferWallRebuild(wallId: string, nodes: Record<AnyNodeId, AnyNode>, rebuiltThisFrame: number, elapsedMs: number): boolean;
12
12
  /** Rebuilds this system still owes — neighbours deferred during a drag. */
13
13
  export declare function getPendingWallRebuildCount(): number;
14
- export declare const WallSystem: () => null;
15
- export declare function runWallBuildFrame(): void;
14
+ export type WallGeometryAdapterContext = {
15
+ isLive: (id: AnyNodeId) => boolean;
16
+ };
17
+ export type WallGeometryAdapter = {
18
+ prepareChildren?: (wall: WallNode, children: readonly AnyNode[], context: WallGeometryAdapterContext) => {
19
+ envelopeChildren: AnyNode[];
20
+ renderChildren: AnyNode[];
21
+ };
22
+ buildGeometry?: (wall: WallNode, envelope: THREE.BufferGeometry, children: readonly AnyNode[]) => THREE.BufferGeometry;
23
+ syncAuxiliaryGeometry?: (wall: WallNode, mesh: THREE.Mesh, geometry: THREE.BufferGeometry) => void;
24
+ };
25
+ export declare const WallSystem: ({ geometryAdapter }?: {
26
+ geometryAdapter?: WallGeometryAdapter;
27
+ }) => null;
28
+ export declare function runWallBuildFrame(geometryAdapter?: WallGeometryAdapter): void;
16
29
  type WallTerrainBottomSampler = (x: number, z: number) => number | null;
17
30
  export declare function generateExtrudedWall(wallNode: WallNode, childrenNodes: AnyNode[], miterData: WallMiterData, slabElevation?: number, baseElevation?: number, baseSegments?: readonly WallSlabSupportSegment[], storeyHeight?: number, terrainBottomAt?: WallTerrainBottomSampler): THREE.BufferGeometry;
18
31
  //# sourceMappingURL=wall-system.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"wall-system.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-system.tsx"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,OAAO,EACZ,KAAK,SAAS,EAyBd,KAAK,aAAa,EAClB,KAAK,QAAQ,EACb,KAAK,sBAAsB,EAI5B,MAAM,kBAAkB,CAAA;AAGzB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAY,KAAK,EAA0B,MAAM,eAAe,CAAA;AAsBvE,OAAO,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAA;AACjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAA;AA2DhE,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,SAAS,KAAK,EAAE,GAAG;IACjE,MAAM,EAAE,KAAK,GAAG,IAAI,CAAA;IACpB,eAAe,EAAE,KAAK,EAAE,CAAA;IACxB,YAAY,EAAE,MAAM,CAAA;CACrB,CA4FA;AAmbD,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,EACjC,gBAAgB,EAAE,MAAM,EACxB,SAAS,EAAE,MAAM,GAChB,OAAO,CAET;AAED,2EAA2E;AAC3E,wBAAgB,0BAA0B,IAAI,MAAM,CAEnD;AAID,eAAO,MAAM,UAAU,YAMtB,CAAA;AAED,wBAAgB,iBAAiB,SAahC;AA6bD,KAAK,wBAAwB,GAAG,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAA;AAgEvE,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,QAAQ,EAClB,aAAa,EAAE,OAAO,EAAE,EACxB,SAAS,EAAE,aAAa,EACxB,aAAa,SAAI,EACjB,aAAa,SAAgB,EAC7B,YAAY,GAAE,SAAS,sBAAsB,EAE5C,EACD,YAAY,SAAuB,EACnC,eAAe,CAAC,EAAE,wBAAwB,GACzC,KAAK,CAAC,cAAc,CAwPtB"}
1
+ {"version":3,"file":"wall-system.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-system.tsx"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,OAAO,EACZ,KAAK,SAAS,EAyBd,KAAK,aAAa,EAClB,KAAK,QAAQ,EACb,KAAK,sBAAsB,EAI5B,MAAM,kBAAkB,CAAA;AAGzB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAY,KAAK,EAA0B,MAAM,eAAe,CAAA;AAsBvE,OAAO,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAA;AACjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAA;AA2DhE,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,SAAS,KAAK,EAAE,GAAG;IACjE,MAAM,EAAE,KAAK,GAAG,IAAI,CAAA;IACpB,eAAe,EAAE,KAAK,EAAE,CAAA;IACxB,YAAY,EAAE,MAAM,CAAA;CACrB,CA4FA;AAmbD,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,EACjC,gBAAgB,EAAE,MAAM,EACxB,SAAS,EAAE,MAAM,GAChB,OAAO,CAET;AAED,2EAA2E;AAC3E,wBAAgB,0BAA0B,IAAI,MAAM,CAEnD;AAID,MAAM,MAAM,0BAA0B,GAAG;IACvC,MAAM,EAAE,CAAC,EAAE,EAAE,SAAS,KAAK,OAAO,CAAA;CACnC,CAAA;AAED,MAAM,MAAM,mBAAmB,GAAG;IAChC,eAAe,CAAC,EAAE,CAChB,IAAI,EAAE,QAAQ,EACd,QAAQ,EAAE,SAAS,OAAO,EAAE,EAC5B,OAAO,EAAE,0BAA0B,KAChC;QAAE,gBAAgB,EAAE,OAAO,EAAE,CAAC;QAAC,cAAc,EAAE,OAAO,EAAE,CAAA;KAAE,CAAA;IAC/D,aAAa,CAAC,EAAE,CACd,IAAI,EAAE,QAAQ,EACd,QAAQ,EAAE,KAAK,CAAC,cAAc,EAC9B,QAAQ,EAAE,SAAS,OAAO,EAAE,KACzB,KAAK,CAAC,cAAc,CAAA;IACzB,qBAAqB,CAAC,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,CAAC,cAAc,KAAK,IAAI,CAAA;CACnG,CAAA;AAED,eAAO,MAAM,UAAU,GAAI,sBAAqB;IAAE,eAAe,CAAC,EAAE,mBAAmB,CAAA;CAAO,SAM7F,CAAA;AAED,wBAAgB,iBAAiB,CAAC,eAAe,CAAC,EAAE,mBAAmB,QAatE;AAmcD,KAAK,wBAAwB,GAAG,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAA;AAgEvE,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,QAAQ,EAClB,aAAa,EAAE,OAAO,EAAE,EACxB,SAAS,EAAE,aAAa,EACxB,aAAa,SAAI,EACjB,aAAa,SAAgB,EAC7B,YAAY,GAAE,SAAS,sBAAsB,EAE5C,EACD,YAAY,SAAuB,EACnC,eAAe,CAAC,EAAE,wBAAwB,GACzC,KAAK,CAAC,cAAc,CAwPtB"}
@@ -496,14 +496,14 @@ export function getPendingWallRebuildCount() {
496
496
  return drainStats.pendingNeighbours;
497
497
  }
498
498
  let placeholderSweepCountdown = WALL_PLACEHOLDER_SWEEP_INTERVAL;
499
- export const WallSystem = () => {
499
+ export const WallSystem = ({ geometryAdapter } = {}) => {
500
500
  useScene((state) => state.dirtyNodes);
501
501
  useLiveNodeOverrides((s) => s.overrides);
502
502
  useEffect(() => () => clearLevelMiterCache(), []);
503
- useFrame(runWallBuildFrame, 4);
503
+ useFrame(() => runWallBuildFrame(geometryAdapter), 4);
504
504
  return null;
505
505
  };
506
- export function runWallBuildFrame() {
506
+ export function runWallBuildFrame(geometryAdapter) {
507
507
  const initialBuild = isWallInitialBuildActive();
508
508
  const token = useScene.getState().hydrationToken;
509
509
  if (token !== stalledHydrationToken) {
@@ -512,13 +512,13 @@ export function runWallBuildFrame() {
512
512
  }
513
513
  drainStats.wallsConsumedThisFrame = 0;
514
514
  try {
515
- consumeWallBuildFrame(initialBuild);
515
+ consumeWallBuildFrame(initialBuild, geometryAdapter);
516
516
  }
517
517
  finally {
518
518
  publishWallDrainStats();
519
519
  }
520
520
  }
521
- function consumeWallBuildFrame(initialBuild) {
521
+ function consumeWallBuildFrame(initialBuild, geometryAdapter) {
522
522
  const clearDirty = useScene.getState().clearDirty;
523
523
  // Self-heal: any registered wall still on its mount-time placeholder
524
524
  // geometry with NO dirty mark gets re-marked, so a lost mark (system
@@ -600,7 +600,7 @@ function consumeWallBuildFrame(initialBuild) {
600
600
  }
601
601
  const mesh = sceneRegistry.nodes.get(wallId);
602
602
  if (mesh) {
603
- timeSpan('wall-rebuild', () => updateWallGeometry(wallId, miterData), {
603
+ timeSpan('wall-rebuild', () => updateWallGeometry(wallId, miterData, geometryAdapter), {
604
604
  properties: [['node', wallId]],
605
605
  });
606
606
  clearDirty(wallId);
@@ -677,7 +677,7 @@ function consumeWallBuildFrame(initialBuild) {
677
677
  }
678
678
  const mesh = sceneRegistry.nodes.get(wallId);
679
679
  if (mesh) {
680
- timeSpan('wall-rebuild', () => updateWallGeometry(wallId, miterData), {
680
+ timeSpan('wall-rebuild', () => updateWallGeometry(wallId, miterData, geometryAdapter), {
681
681
  properties: [['node', wallId]],
682
682
  });
683
683
  notifyWallRebuilt(wallId);
@@ -766,7 +766,7 @@ function getLevelWalls(levelId) {
766
766
  * (override-merged) so a 2D drag visibly moves the 3D mesh without
767
767
  * having touched `useScene` mid-drag.
768
768
  */
769
- function updateWallGeometry(wallId, miterData) {
769
+ function updateWallGeometry(wallId, miterData, geometryAdapter) {
770
770
  const nodes = useScene.getState().nodes;
771
771
  const sceneNode = nodes[wallId];
772
772
  if (sceneNode?.type !== 'wall')
@@ -785,38 +785,37 @@ function updateWallGeometry(wallId, miterData) {
785
785
  ? (x, z) => terrainSupportLift(nodes, levelId, x, z)
786
786
  : undefined;
787
787
  const childrenIds = node.children || [];
788
- // Merge live overrides into door / window children so cutouts track an
789
- // in-flight resize drag (door width arrow, window height arrow, etc.)
790
- // without waiting on the scene store. Non-cutout children pass through
791
- // unchanged.
792
788
  const childrenNodes = childrenIds
793
789
  .map((childId) => nodes[childId])
794
790
  .filter((n) => n !== undefined)
795
791
  .map((child) => {
796
792
  if (child.type !== 'door' && child.type !== 'window')
797
793
  return child;
798
- // `getEffectiveNode` folds in resize overrides (width/height arrows).
799
- // Position moves publish to `useLiveTransforms` instead, so fold that
800
- // in too — opening cutout brushes are rebuilt directly from the
801
- // effective node position rather than from the rendered proxy mesh.
802
794
  const effective = getEffectiveNode(child);
803
795
  const live = useLiveTransforms.getState().get(child.id);
804
- if (!live?.position)
805
- return effective;
806
- return { ...effective, position: live.position };
796
+ return live?.position ? { ...effective, position: live.position } : effective;
807
797
  });
808
- const builtGeo = generateExtrudedWall(node, childrenNodes, miterData, slabElevation, slabSupport.baseElevation, slabSupport.baseSegments, planeTop, terrainBottomAt);
798
+ const prepared = geometryAdapter?.prepareChildren?.(node, childrenNodes, {
799
+ isLive: (id) => useLiveNodeOverrides.getState().get(id) !== undefined ||
800
+ useLiveTransforms.getState().get(id) !== undefined,
801
+ }) ?? {
802
+ envelopeChildren: childrenNodes,
803
+ renderChildren: childrenNodes,
804
+ };
805
+ const builtGeo = generateExtrudedWall(node, prepared.envelopeChildren, miterData, slabElevation, slabSupport.baseElevation, slabSupport.baseSegments, planeTop, terrainBottomAt);
809
806
  const wallAngle = Math.atan2(node.end[1] - node.start[1], node.end[0] - node.start[0]);
810
807
  // World transform the render mesh will apply (position + Y-rotation below).
811
808
  // Reproduce it here so the UVs can be projected in WORLD space — see
812
809
  // `applyWorldPlanarWallUVs`.
813
810
  const wallWorldMatrix = new THREE.Matrix4().compose(new THREE.Vector3(node.start[0], slabElevation, node.start[1]), new THREE.Quaternion().setFromAxisAngle(WALL_UV_Y_AXIS, -wallAngle), WALL_UV_UNIT_SCALE);
814
- const newGeo = applyWorldPlanarWallUVs(builtGeo, wallWorldMatrix);
811
+ const renderedGeo = geometryAdapter?.buildGeometry?.(node, builtGeo, prepared.renderChildren) ?? builtGeo;
812
+ const newGeo = applyWorldPlanarWallUVs(renderedGeo, wallWorldMatrix);
815
813
  mesh.geometry.dispose();
816
814
  // A degenerate rebuild (zero-length or fully cut wall) yields as few vertices
817
815
  // as the mount-time placeholder; the stamp keeps the sweep from re-marking it.
818
816
  newGeo.userData.built = true;
819
817
  mesh.geometry = newGeo;
818
+ geometryAdapter?.syncAuxiliaryGeometry?.(node, mesh, newGeo);
820
819
  // Update collision mesh
821
820
  const collisionMesh = mesh.getObjectByName('collision-mesh');
822
821
  if (collisionMesh) {
@@ -1 +1 @@
1
- {"version":3,"file":"window-system.d.ts","sourceRoot":"","sources":["../../../src/systems/window/window-system.tsx"],"names":[],"mappings":"AAAA,OAAO,EAYL,KAAK,UAAU,EAChB,MAAM,kBAAkB,CAAA;AAGzB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAyB9B,eAAO,MAAM,yBAAyB,yBAAyB,CAAA;AAC/D,eAAO,MAAM,8BAA8B,8BAA8B,CAAA;AACzE,eAAO,MAAM,+BAA+B,+BAA+B,CAAA;AAC3E,eAAO,MAAM,gCAAgC,gCAAgC,CAAA;AAC7E,eAAO,MAAM,4BAA4B,4BAA4B,CAAA;AACrE,eAAO,MAAM,yBAAyB,yBAAyB,CAAA;AAC/D,eAAO,MAAM,4BAA4B,4BAA4B,CAAA;AACrE,eAAO,MAAM,0BAA0B,0BAA0B,CAAA;AACjE,eAAO,MAAM,uBAAuB,uBAAuB,CAAA;AAC3D,eAAO,MAAM,uBAAuB,uBAAuB,CAAA;AAS3D,eAAO,MAAM,8BAA8B,uoCAAuB,CAAA;AAElE,eAAO,MAAM,YAAY,YAoHxB,CAAA;AA28GD;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,UAAU,GAAG,KAAK,CAAC,IAAI,CAInE"}
1
+ {"version":3,"file":"window-system.d.ts","sourceRoot":"","sources":["../../../src/systems/window/window-system.tsx"],"names":[],"mappings":"AAAA,OAAO,EAYL,KAAK,UAAU,EAChB,MAAM,kBAAkB,CAAA;AAGzB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAyB9B,eAAO,MAAM,yBAAyB,yBAAyB,CAAA;AAC/D,eAAO,MAAM,8BAA8B,8BAA8B,CAAA;AACzE,eAAO,MAAM,+BAA+B,+BAA+B,CAAA;AAC3E,eAAO,MAAM,gCAAgC,gCAAgC,CAAA;AAC7E,eAAO,MAAM,4BAA4B,4BAA4B,CAAA;AACrE,eAAO,MAAM,yBAAyB,yBAAyB,CAAA;AAC/D,eAAO,MAAM,4BAA4B,4BAA4B,CAAA;AACrE,eAAO,MAAM,0BAA0B,0BAA0B,CAAA;AACjE,eAAO,MAAM,uBAAuB,uBAAuB,CAAA;AAC3D,eAAO,MAAM,uBAAuB,uBAAuB,CAAA;AAS3D,eAAO,MAAM,8BAA8B,uoCAAuB,CAAA;AAElE,eAAO,MAAM,YAAY,YAqHxB,CAAA;AA28GD;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,UAAU,GAAG,KAAK,CAAC,IAAI,CAInE"}
@@ -136,8 +136,9 @@ export const WindowSystem = () => {
136
136
  // Rebuild the parent wall so its cutout reflects the updated window geometry
137
137
  // Avoid triggering expensive wall CSG rebuilds while the window is being interactively moved/duplicated.
138
138
  // The editor tools will request a final wall rebuild on commit.
139
- const isTransient = !!node.metadata?.isTransient;
140
- if (!isTransient && effectiveNode.parentId) {
139
+ const metadata = effectiveNode.metadata;
140
+ const deferParentRebuild = !!metadata?.isTransient || !!metadata?.deferParentRebuild;
141
+ if (!deferParentRebuild && effectiveNode.parentId) {
141
142
  useScene.getState().dirtyNodes.add(effectiveNode.parentId);
142
143
  }
143
144
  }
@@ -0,0 +1,7 @@
1
+ import { type ReactNode } from 'react';
2
+ export declare function ImmersiveXRPresentationProvider({ children, enabled, }: {
3
+ children: ReactNode;
4
+ enabled: boolean;
5
+ }): import("react").JSX.Element;
6
+ export declare function useImmersiveXRPresentation(): boolean;
7
+ //# sourceMappingURL=presentation-context.d.ts.map