@pascal-app/viewer 1.0.1 → 1.0.4

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 (173) hide show
  1. package/dist/components/renderers/node-renderer.d.ts.map +1 -1
  2. package/dist/components/renderers/node-renderer.js +8 -3
  3. package/dist/components/viewer/frame-limiter.d.ts +1 -0
  4. package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
  5. package/dist/components/viewer/frame-limiter.js +22 -3
  6. package/dist/components/viewer/glb-interactive.d.ts +25 -1
  7. package/dist/components/viewer/glb-interactive.d.ts.map +1 -1
  8. package/dist/components/viewer/glb-interactive.js +252 -85
  9. package/dist/components/viewer/glb-mechanisms.d.ts +10 -0
  10. package/dist/components/viewer/glb-mechanisms.d.ts.map +1 -0
  11. package/dist/components/viewer/glb-mechanisms.js +19 -0
  12. package/dist/components/viewer/glb-scene.d.ts +45 -27
  13. package/dist/components/viewer/glb-scene.d.ts.map +1 -1
  14. package/dist/components/viewer/glb-scene.js +420 -506
  15. package/dist/components/viewer/glb-walkthrough-controller.d.ts +1 -1
  16. package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -1
  17. package/dist/components/viewer/glb-walkthrough-controller.js +12 -1
  18. package/dist/components/viewer/index.d.ts +26 -0
  19. package/dist/components/viewer/index.d.ts.map +1 -1
  20. package/dist/components/viewer/index.js +108 -29
  21. package/dist/components/viewer/lights.d.ts.map +1 -1
  22. package/dist/components/viewer/lights.js +6 -1
  23. package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
  24. package/dist/components/viewer/perf-monitor.js +6 -0
  25. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  26. package/dist/components/viewer/post-processing.js +8 -3
  27. package/dist/components/viewer/registered-systems.d.ts.map +1 -1
  28. package/dist/components/viewer/registered-systems.js +16 -2
  29. package/dist/components/viewer/render-error.d.ts +18 -0
  30. package/dist/components/viewer/render-error.d.ts.map +1 -0
  31. package/dist/components/viewer/render-error.js +29 -0
  32. package/dist/components/viewer/scene-ground-replacement.d.ts +6 -0
  33. package/dist/components/viewer/scene-ground-replacement.d.ts.map +1 -1
  34. package/dist/components/viewer/scene-ground-replacement.js +15 -0
  35. package/dist/components/viewer/selection-manager.d.ts.map +1 -1
  36. package/dist/components/viewer/selection-manager.js +10 -0
  37. package/dist/components/viewer/viewer-camera.d.ts +19 -1
  38. package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
  39. package/dist/components/viewer/viewer-camera.js +53 -5
  40. package/dist/index.d.ts +28 -15
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +26 -18
  43. package/dist/lib/asset-url.d.ts.map +1 -1
  44. package/dist/lib/asset-url.js +7 -1
  45. package/dist/lib/csg-utils.d.ts +1 -1
  46. package/dist/lib/csg-utils.d.ts.map +1 -1
  47. package/dist/lib/csg-utils.js +1 -1
  48. package/dist/lib/display-state.d.ts +25 -0
  49. package/dist/lib/display-state.d.ts.map +1 -0
  50. package/dist/lib/display-state.js +1 -0
  51. package/dist/lib/hero-pose.d.ts +12 -2
  52. package/dist/lib/hero-pose.d.ts.map +1 -1
  53. package/dist/lib/hero-pose.js +72 -5
  54. package/dist/lib/ink-edges.d.ts +4 -2
  55. package/dist/lib/ink-edges.d.ts.map +1 -1
  56. package/dist/lib/ink-edges.js +17 -7
  57. package/dist/lib/key-light-override.d.ts +11 -0
  58. package/dist/lib/key-light-override.d.ts.map +1 -0
  59. package/dist/lib/key-light-override.js +14 -0
  60. package/dist/lib/live-frame-hold.d.ts +14 -0
  61. package/dist/lib/live-frame-hold.d.ts.map +1 -0
  62. package/dist/lib/live-frame-hold.js +28 -0
  63. package/dist/lib/materials.d.ts +14 -7
  64. package/dist/lib/materials.d.ts.map +1 -1
  65. package/dist/lib/materials.js +31 -5
  66. package/dist/lib/node-top-surface-height.d.ts +6 -0
  67. package/dist/lib/node-top-surface-height.d.ts.map +1 -0
  68. package/dist/lib/node-top-surface-height.js +74 -0
  69. package/dist/lib/perf-probe-state.d.ts +39 -0
  70. package/dist/lib/perf-probe-state.d.ts.map +1 -0
  71. package/dist/lib/perf-probe-state.js +17 -0
  72. package/dist/lib/pointer-events.d.ts +8 -0
  73. package/dist/lib/pointer-events.d.ts.map +1 -1
  74. package/dist/lib/pointer-events.js +27 -4
  75. package/dist/lib/procedural-emission.d.ts +7 -0
  76. package/dist/lib/procedural-emission.d.ts.map +1 -0
  77. package/dist/lib/procedural-emission.js +54 -0
  78. package/dist/lib/renderer-capability.d.ts +2 -1
  79. package/dist/lib/renderer-capability.d.ts.map +1 -1
  80. package/dist/lib/renderer-capability.js +2 -2
  81. package/dist/lib/scene-support-height.d.ts +4 -0
  82. package/dist/lib/scene-support-height.d.ts.map +1 -0
  83. package/dist/lib/scene-support-height.js +34 -0
  84. package/dist/lib/scene-visibility.d.ts +6 -0
  85. package/dist/lib/scene-visibility.d.ts.map +1 -1
  86. package/dist/lib/scene-visibility.js +15 -3
  87. package/dist/lib/scripted-opening.d.ts +11 -0
  88. package/dist/lib/scripted-opening.d.ts.map +1 -0
  89. package/dist/lib/scripted-opening.js +20 -0
  90. package/dist/lib/snapshot-pipeline.d.ts +32 -2
  91. package/dist/lib/snapshot-pipeline.d.ts.map +1 -1
  92. package/dist/lib/snapshot-pipeline.js +215 -105
  93. package/dist/lib/terrain-perimeter-fill.d.ts.map +1 -1
  94. package/dist/lib/terrain-perimeter-fill.js +22 -14
  95. package/dist/lib/zone-geometry.d.ts +11 -0
  96. package/dist/lib/zone-geometry.d.ts.map +1 -0
  97. package/dist/lib/zone-geometry.js +46 -0
  98. package/dist/store/use-item-light-pool.d.ts +14 -10
  99. package/dist/store/use-item-light-pool.d.ts.map +1 -1
  100. package/dist/store/use-item-light-pool.js +52 -26
  101. package/dist/store/use-viewer.d.ts +11 -0
  102. package/dist/store/use-viewer.d.ts.map +1 -1
  103. package/dist/store/use-viewer.js +13 -6
  104. package/dist/systems/ceiling/ceiling-system.d.ts +15 -1
  105. package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
  106. package/dist/systems/ceiling/ceiling-system.js +86 -6
  107. package/dist/systems/door/door-system.d.ts.map +1 -1
  108. package/dist/systems/door/door-system.js +73 -22
  109. package/dist/systems/floor-elevation/floor-elevation-system.d.ts +3 -1
  110. package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -1
  111. package/dist/systems/floor-elevation/floor-elevation-system.js +148 -5
  112. package/dist/systems/geometry/geometry-system.d.ts.map +1 -1
  113. package/dist/systems/geometry/geometry-system.js +71 -11
  114. package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
  115. package/dist/systems/interactive/interactive-system.js +40 -15
  116. package/dist/systems/interactive/procedural-controls.d.ts +13 -0
  117. package/dist/systems/interactive/procedural-controls.d.ts.map +1 -0
  118. package/dist/systems/interactive/procedural-controls.js +17 -0
  119. package/dist/systems/interactive/scripted-clips.d.ts +19 -0
  120. package/dist/systems/interactive/scripted-clips.d.ts.map +1 -0
  121. package/dist/systems/interactive/scripted-clips.js +134 -0
  122. package/dist/systems/item/item-system.d.ts.map +1 -1
  123. package/dist/systems/item/item-system.js +9 -3
  124. package/dist/systems/item/object-cut-invalidation.d.ts +3 -0
  125. package/dist/systems/item/object-cut-invalidation.d.ts.map +1 -0
  126. package/dist/systems/item/object-cut-invalidation.js +68 -0
  127. package/dist/systems/item-light/item-light-system.d.ts +1 -1
  128. package/dist/systems/item-light/item-light-system.d.ts.map +1 -1
  129. package/dist/systems/item-light/item-light-system.js +47 -52
  130. package/dist/systems/level/level-system.d.ts.map +1 -1
  131. package/dist/systems/level/level-system.js +17 -19
  132. package/dist/systems/level/level-utils.d.ts +28 -1
  133. package/dist/systems/level/level-utils.d.ts.map +1 -1
  134. package/dist/systems/level/level-utils.js +36 -4
  135. package/dist/systems/level/shadow-only-levels.d.ts +9 -0
  136. package/dist/systems/level/shadow-only-levels.d.ts.map +1 -0
  137. package/dist/systems/level/shadow-only-levels.js +7 -0
  138. package/dist/systems/roof/roof-materials.d.ts +14 -2
  139. package/dist/systems/roof/roof-materials.d.ts.map +1 -1
  140. package/dist/systems/roof/roof-materials.js +54 -15
  141. package/dist/systems/roof/roof-system.d.ts +13 -3
  142. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  143. package/dist/systems/roof/roof-system.js +188 -13
  144. package/dist/systems/slab/slab-system.d.ts +1 -1
  145. package/dist/systems/slab/slab-system.d.ts.map +1 -1
  146. package/dist/systems/slab/slab-system.js +49 -19
  147. package/dist/systems/stair/stair-system.d.ts.map +1 -1
  148. package/dist/systems/stair/stair-system.js +133 -354
  149. package/dist/systems/wall/wall-cutout-cache.d.ts +11 -1
  150. package/dist/systems/wall/wall-cutout-cache.d.ts.map +1 -1
  151. package/dist/systems/wall/wall-cutout-cache.js +58 -20
  152. package/dist/systems/wall/wall-cutout.d.ts +9 -1
  153. package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
  154. package/dist/systems/wall/wall-cutout.js +46 -32
  155. package/dist/systems/wall/wall-finish-data.d.ts +22 -0
  156. package/dist/systems/wall/wall-finish-data.d.ts.map +1 -0
  157. package/dist/systems/wall/wall-finish-data.js +17 -0
  158. package/dist/systems/wall/wall-materials.d.ts +17 -3
  159. package/dist/systems/wall/wall-materials.d.ts.map +1 -1
  160. package/dist/systems/wall/wall-materials.js +109 -156
  161. package/dist/systems/wall/wall-system.d.ts +43 -4
  162. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  163. package/dist/systems/wall/wall-system.js +601 -163
  164. package/dist/systems/window/window-system.d.ts +1 -1
  165. package/dist/systems/window/window-system.d.ts.map +1 -1
  166. package/dist/systems/window/window-system.js +44 -24
  167. package/dist/xr/presentation-context.d.ts +7 -0
  168. package/dist/xr/presentation-context.d.ts.map +1 -0
  169. package/dist/xr/presentation-context.js +10 -0
  170. package/package.json +6 -6
  171. package/dist/systems/fence/fence-system.d.ts +0 -22
  172. package/dist/systems/fence/fence-system.d.ts.map +0 -1
  173. package/dist/systems/fence/fence-system.js +0 -357
@@ -1 +1 @@
1
- {"version":3,"file":"item-system.d.ts","sourceRoot":"","sources":["../../../src/systems/item/item-system.tsx"],"names":[],"mappings":"AAcA;;;;;;GAMG;AACH,eAAO,MAAM,UAAU,YA8CtB,CAAA"}
1
+ {"version":3,"file":"item-system.d.ts","sourceRoot":"","sources":["../../../src/systems/item/item-system.tsx"],"names":[],"mappings":"AAmBA;;;;;;GAMG;AACH,eAAO,MAAM,UAAU,YAsDtB,CAAA"}
@@ -1,5 +1,7 @@
1
- import { sceneRegistry, useScene, } from '@pascal-app/core';
1
+ import { getEffectiveNode, getWallBodyCenterOffset, getWallLocalFaceZ, sceneRegistry, useScene, } from '@pascal-app/core';
2
2
  import { useFrame } from '@react-three/fiber';
3
+ import { useEffect } from 'react';
4
+ import { initializeObjectCutInvalidation } from './object-cut-invalidation';
3
5
  // ============================================================================
4
6
  // ITEM SYSTEM
5
7
  // ============================================================================
@@ -11,6 +13,7 @@ import { useFrame } from '@react-three/fiber';
11
13
  * mark at priority 2.
12
14
  */
13
15
  export const ItemSystem = () => {
16
+ useEffect(initializeObjectCutInvalidation, []);
14
17
  const dirtyNodes = useScene((state) => state.dirtyNodes);
15
18
  const clearDirty = useScene((state) => state.clearDirty);
16
19
  useFrame(() => {
@@ -25,6 +28,10 @@ export const ItemSystem = () => {
25
28
  const mesh = sceneRegistry.nodes.get(id);
26
29
  if (!mesh)
27
30
  return;
31
+ const host = item.parentId ? nodes[item.parentId] : undefined;
32
+ if (item.asset.attachTo === 'wall' && host?.type === 'wall') {
33
+ mesh.position.z = item.position[2] + getWallBodyCenterOffset(getEffectiveNode(host));
34
+ }
28
35
  if (item.asset.attachTo === 'wall-side') {
29
36
  // Wall-attached item: offset Z by half the host wall's thickness.
30
37
  // Roof-segment wall faces share the convention — the face frame's
@@ -37,8 +44,7 @@ export const ItemSystem = () => {
37
44
  ? (parent.wallThickness ?? 0.1)
38
45
  : undefined;
39
46
  if (thickness !== undefined) {
40
- const side = item.side === 'front' ? 1 : -1;
41
- mesh.position.z = (thickness / 2) * side;
47
+ mesh.position.z = getWallLocalFaceZ(parent?.type === 'wall' ? getEffectiveNode(parent) : { thickness }, item.side === 'front' ? 'a' : 'b');
42
48
  }
43
49
  }
44
50
  // Hold the mark until the model settles (loaded, terminally failed, or
@@ -0,0 +1,3 @@
1
+ /** Geometry remains derived; only dirty marks cross into the scene store. */
2
+ export declare function initializeObjectCutInvalidation(): () => void;
3
+ //# sourceMappingURL=object-cut-invalidation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"object-cut-invalidation.d.ts","sourceRoot":"","sources":["../../../src/systems/item/object-cut-invalidation.ts"],"names":[],"mappings":"AAUA,6EAA6E;AAC7E,wBAAgB,+BAA+B,eA6D9C"}
@@ -0,0 +1,68 @@
1
+ import { getEffectiveCutterNode, resolveCutterHost, useLiveNodeOverrides, useLiveTransforms, useScene, } from '@pascal-app/core';
2
+ /** Geometry remains derived; only dirty marks cross into the scene store. */
3
+ export function initializeObjectCutInvalidation() {
4
+ let previous = new Map();
5
+ let cutterIds = [];
6
+ const collect = (nodes) => {
7
+ cutterIds = [];
8
+ for (const node of Object.values(nodes)) {
9
+ if (node.type === 'item' && node.source?.manifest.cutters?.length)
10
+ cutterIds.push(node.id);
11
+ }
12
+ };
13
+ // Drags fire every frame: a scene without cutters pays nothing here.
14
+ const refresh = () => {
15
+ if (!cutterIds.length && !previous.size)
16
+ return;
17
+ const state = useScene.getState();
18
+ const nodes = { ...state.nodes };
19
+ for (const id of new Set([
20
+ ...useLiveNodeOverrides.getState().overrides.keys(),
21
+ ...useLiveTransforms.getState().transforms.keys(),
22
+ ])) {
23
+ if (nodes[id])
24
+ nodes[id] = getEffectiveCutterNode(nodes[id]);
25
+ }
26
+ const owners = new Map();
27
+ for (const id of cutterIds) {
28
+ const node = nodes[id];
29
+ if (node?.type !== 'item' || !node.source?.manifest.cutters?.length)
30
+ continue;
31
+ for (const cutter of node.source.manifest.cutters) {
32
+ const host = resolveCutterHost(node, cutter.host === 'mounted' ? 'cutout' : `cut:${cutter.host}`, nodes);
33
+ if (!host)
34
+ continue;
35
+ const entries = owners.get(host.id) ?? [];
36
+ entries.push([node.id, node.source.artifact, node.position, node.rotation, node.scale]);
37
+ owners.set(host.id, entries);
38
+ }
39
+ }
40
+ const next = new Map([...owners].map(([id, entries]) => [id, JSON.stringify(entries)]));
41
+ const changed = [...new Set([...previous.keys(), ...next.keys()])].filter((id) => previous.get(id) !== next.get(id));
42
+ previous = next;
43
+ for (const id of changed)
44
+ if (nodes[id])
45
+ state.markDirty(id);
46
+ };
47
+ collect(useScene.getState().nodes);
48
+ refresh();
49
+ const unsubscribeScene = useScene.subscribe((state, before) => {
50
+ if (state.nodes === before.nodes)
51
+ return;
52
+ collect(state.nodes);
53
+ refresh();
54
+ });
55
+ const unsubscribeLive = useLiveNodeOverrides.subscribe((state, before) => {
56
+ if (state.overrides !== before.overrides)
57
+ refresh();
58
+ });
59
+ const unsubscribeTransforms = useLiveTransforms.subscribe((state, before) => {
60
+ if (state.transforms !== before.transforms)
61
+ refresh();
62
+ });
63
+ return () => {
64
+ unsubscribeScene();
65
+ unsubscribeLive();
66
+ unsubscribeTransforms();
67
+ };
68
+ }
@@ -1,2 +1,2 @@
1
- export declare function ItemLightSystem(): import("react").JSX.Element;
1
+ export declare function ItemLightSystem(): import("react").JSX.Element | null;
2
2
  //# sourceMappingURL=item-light-system.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"item-light-system.d.ts","sourceRoot":"","sources":["../../../src/systems/item-light/item-light-system.tsx"],"names":[],"mappings":"AAuFA,wBAAgB,eAAe,gCA8N9B"}
1
+ {"version":3,"file":"item-light-system.d.ts","sourceRoot":"","sources":["../../../src/systems/item-light/item-light-system.tsx"],"names":[],"mappings":"AAgGA,wBAAgB,eAAe,uCA0M9B"}
@@ -1,8 +1,9 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { findLevelAncestorId, sceneRegistry, useInteractive, useScene } from '@pascal-app/core';
3
- import { useFrame } from '@react-three/fiber';
2
+ import { findLevelAncestorId, sceneRegistry, useScene } from '@pascal-app/core';
3
+ import { useFrame, useThree } from '@react-three/fiber';
4
4
  import { useRef } from 'react';
5
5
  import { MathUtils, Vector3 } from 'three';
6
+ import { SCENE_LAYER } from '../../lib/layers';
6
7
  import { useItemLightPool } from '../../store/use-item-light-pool';
7
8
  import useViewer from '../../store/use-viewer';
8
9
  const POOL_SIZE = 12;
@@ -19,22 +20,30 @@ const _dir = new Vector3();
19
20
  const _camPos = new Vector3();
20
21
  const _camFwd = new Vector3();
21
22
  const _itemPos = new Vector3();
22
- function scoreRegistration(reg, nodes, selectedLevelId, levelMode, interactiveState) {
23
- // Skip lights that are toggled off — they contribute no illumination
24
- if (reg.toggleIndex >= 0) {
25
- const values = interactiveState.items[reg.nodeId]?.controlValues;
26
- const isOn = Boolean(values?.[reg.toggleIndex]);
27
- if (!isOn)
23
+ function isRendered(nodeId) {
24
+ let object = sceneRegistry.nodes.get(nodeId);
25
+ if (!object?.layers.isEnabled(SCENE_LAYER))
26
+ return false;
27
+ while (object) {
28
+ if (!object.visible)
29
+ return false;
30
+ object = object.parent;
31
+ }
32
+ return true;
33
+ }
34
+ function scoreRegistration(reg, nodes, selectedLevelId, levelMode, hideAbove) {
35
+ if (!reg.isEligible() || !isRendered(reg.nodeId) || !reg.getWorldPosition(_itemPos))
36
+ return Number.POSITIVE_INFINITY;
37
+ const { nodeId } = reg;
38
+ let current = nodes[nodeId];
39
+ while (current) {
40
+ if (current.type !== 'site' && current.visible === false)
28
41
  return Number.POSITIVE_INFINITY;
42
+ current = current.parentId ? nodes[current.parentId] : undefined;
29
43
  }
30
- const { nodeId, effect } = reg;
31
- const obj = sceneRegistry.nodes.get(nodeId);
32
- if (!obj)
44
+ const itemLevelId = findLevelAncestorId(nodeId, nodes);
45
+ if (selectedLevelId && itemLevelId !== selectedLevelId && levelMode === 'solo')
33
46
  return Number.POSITIVE_INFINITY;
34
- obj.getWorldPosition(_itemPos);
35
- _itemPos.x += effect.offset[0];
36
- _itemPos.y += effect.offset[1];
37
- _itemPos.z += effect.offset[2];
38
47
  _dir.copy(_itemPos).sub(_camPos).normalize();
39
48
  const dot = _camFwd.dot(_dir); // 1 = ahead, -1 = behind
40
49
  // Angular component (0 = dead ahead, 2 = directly behind)
@@ -42,12 +51,15 @@ function scoreRegistration(reg, nodes, selectedLevelId, levelMode, interactiveSt
42
51
  // Normalised distance component (assumes scenes < 200 units)
43
52
  const dist = _camPos.distanceTo(_itemPos) / 200;
44
53
  // ── Level factor ──────────────────────────────────────────────────────────
45
- const itemLevelId = findLevelAncestorId(nodeId, nodes);
46
54
  let levelPenalty = 0;
47
55
  if (selectedLevelId) {
48
56
  if (itemLevelId !== selectedLevelId) {
49
- // In solo mode items on other levels are invisible — deprioritize strongly
50
- levelPenalty = levelMode === 'solo' ? 100 : 0.8;
57
+ // Items on hidden levels (every other one in solo, those above the
58
+ // selected one while editing) are invisible — deprioritize strongly
59
+ const itemIndex = nodes[itemLevelId]?.level ?? 0;
60
+ const selectedIndex = nodes[selectedLevelId]?.level ?? 0;
61
+ const hidden = levelMode === 'solo' || (hideAbove && itemIndex > selectedIndex);
62
+ levelPenalty = hidden ? 100 : 0.8;
51
63
  }
52
64
  }
53
65
  else if (itemLevelId) {
@@ -60,6 +72,8 @@ function scoreRegistration(reg, nodes, selectedLevelId, levelMode, interactiveSt
60
72
  return angular * 0.7 + dist * 0.3 + levelPenalty;
61
73
  }
62
74
  export function ItemLightSystem() {
75
+ const scene = useThree((state) => state.scene);
76
+ const bakedOwner = useItemLightPool((state) => state.bakedCanvases.has(scene));
63
77
  const lightRefs = useRef(Array.from({ length: POOL_SIZE }, () => null));
64
78
  const slots = useRef(Array.from({ length: POOL_SIZE }, () => ({ key: null, pendingKey: null, isFadingOut: false })));
65
79
  const reassignTimer = useRef(0);
@@ -67,9 +81,10 @@ export function ItemLightSystem() {
67
81
  const prevReassignCamPos = useRef(new Vector3());
68
82
  const prevReassignCamFwd = useRef(new Vector3(0, 0, -1));
69
83
  useFrame(({ camera }, delta) => {
84
+ if (bakedOwner)
85
+ return;
70
86
  const dt = Math.min(delta, 0.1);
71
87
  const { registrations } = useItemLightPool.getState();
72
- const interactiveState = useInteractive.getState();
73
88
  // ── 1. Throttled priority reassignment ──────────────────────────────────
74
89
  camera.getWorldPosition(_camPos);
75
90
  camera.getWorldDirection(_camFwd);
@@ -86,12 +101,13 @@ export function ItemLightSystem() {
86
101
  const viewerState = useViewer.getState();
87
102
  const selectedLevelId = viewerState.selection.levelId;
88
103
  const levelMode = viewerState.levelMode;
104
+ const hideAbove = viewerState.hideLevelsAboveSelection;
89
105
  // Score every registration
90
106
  const scored = [];
91
107
  for (const [key, reg] of registrations) {
92
108
  scored.push({
93
109
  key,
94
- score: scoreRegistration(reg, nodes, selectedLevelId, levelMode, interactiveState),
110
+ score: scoreRegistration(reg, nodes, selectedLevelId, levelMode, hideAbove),
95
111
  });
96
112
  }
97
113
  scored.sort((a, b) => a.score - b.score);
@@ -155,8 +171,8 @@ export function ItemLightSystem() {
155
171
  const light = lightRefs.current[freeSlot];
156
172
  const reg = registrations.get(key);
157
173
  if (light && reg) {
158
- light.color.set(reg.effect.color);
159
- light.distance = reg.effect.distance ?? 0;
174
+ light.color.set(reg.color);
175
+ light.distance = reg.distance;
160
176
  }
161
177
  }
162
178
  }
@@ -183,19 +199,17 @@ export function ItemLightSystem() {
183
199
  continue;
184
200
  // Fade-out phase: lerp intensity → 0, then complete the transition
185
201
  if (slot.isFadingOut) {
186
- light.visible = true;
187
202
  light.intensity = MathUtils.lerp(light.intensity, 0, dt * 12);
188
203
  if (light.intensity < 0.01) {
189
204
  light.intensity = 0;
190
- light.visible = false;
191
205
  slot.isFadingOut = false;
192
206
  slot.key = slot.pendingKey;
193
207
  slot.pendingKey = null;
194
208
  if (slot.key) {
195
209
  const reg = registrations.get(slot.key);
196
210
  if (reg) {
197
- light.color.set(reg.effect.color);
198
- light.distance = reg.effect.distance ?? 0;
211
+ light.color.set(reg.color);
212
+ light.distance = reg.distance;
199
213
  }
200
214
  }
201
215
  }
@@ -204,45 +218,26 @@ export function ItemLightSystem() {
204
218
  if (!slot.key) {
205
219
  // Idle slot — keep dark
206
220
  light.intensity = 0;
207
- light.visible = false;
208
221
  continue;
209
222
  }
210
223
  const reg = registrations.get(slot.key);
211
224
  if (!reg) {
212
225
  slot.key = null;
213
226
  light.intensity = 0;
214
- light.visible = false;
215
227
  continue;
216
228
  }
217
- // Snap world position each frame
218
- const obj = sceneRegistry.nodes.get(reg.nodeId);
219
- if (obj) {
220
- obj.getWorldPosition(_itemPos);
221
- const [ox, oy, oz] = reg.effect.offset;
222
- light.position.set(_itemPos.x + ox, _itemPos.y + oy, _itemPos.z + oz);
223
- }
224
- // Compute target intensity
225
- const values = interactiveState.items[reg.nodeId]?.controlValues;
226
- const isOn = reg.toggleIndex >= 0 ? Boolean(values?.[reg.toggleIndex]) : true;
227
- let t = 1;
228
- if (reg.hasSlider) {
229
- const raw = values?.[reg.sliderIndex] ?? reg.sliderMin;
230
- t = (raw - reg.sliderMin) / (reg.sliderMax - reg.sliderMin);
231
- }
232
- const targetIntensity = isOn
233
- ? MathUtils.lerp(reg.effect.intensityRange[0], reg.effect.intensityRange[1], t)
234
- : reg.effect.intensityRange[0];
235
- if (targetIntensity > 0) {
236
- light.visible = true;
237
- }
229
+ if (reg.getWorldPosition(_itemPos))
230
+ light.position.copy(_itemPos);
231
+ const targetIntensity = reg.isEligible() && isRendered(reg.nodeId) ? reg.getIntensity() : 0;
238
232
  light.intensity = MathUtils.lerp(light.intensity, targetIntensity, dt * 12);
239
233
  if (targetIntensity <= 0 && light.intensity < 0.01) {
240
234
  light.intensity = 0;
241
- light.visible = false;
242
235
  }
243
236
  }
244
- });
245
- return (_jsx(_Fragment, { children: Array.from({ length: POOL_SIZE }, (_, i) => (_jsx("pointLight", { castShadow: false, intensity: 0, visible: false, ref: (el) => {
237
+ }, 6);
238
+ if (bakedOwner)
239
+ return null;
240
+ return (_jsx(_Fragment, { children: Array.from({ length: POOL_SIZE }, (_, i) => (_jsx("pointLight", { castShadow: false, intensity: 0, visible: true, ref: (el) => {
246
241
  lightRefs.current[i] = el;
247
242
  } }, i))) }));
248
243
  }
@@ -1 +1 @@
1
- {"version":3,"file":"level-system.d.ts","sourceRoot":"","sources":["../../../src/systems/level/level-system.tsx"],"names":[],"mappings":"AAcA,eAAO,MAAM,WAAW,YA6DvB,CAAA"}
1
+ {"version":3,"file":"level-system.d.ts","sourceRoot":"","sources":["../../../src/systems/level/level-system.tsx"],"names":[],"mappings":"AAQA,eAAO,MAAM,WAAW,YA+DvB,CAAA"}
@@ -3,16 +3,13 @@ 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';
7
- // Levels currently in shadow-caster-only mode (solo hides them from the color
8
- // passes but keeps their sun shadows). Tracked so we can restore layer masks
9
- // exactly once on transition; apply re-runs every frame so meshes rebuilt
10
- // while hidden (theme/texture changes) get re-hidden.
11
- const shadowOnlyLevels = new WeakSet();
6
+ import { EXPLODED_GAP, resolveLevelVisibility } from './level-utils';
7
+ import { shadowOnlyLevels } from './shadow-only-levels';
12
8
  export const LevelSystem = () => {
13
9
  useFrame((_, delta) => {
14
10
  const nodes = useScene.getState().nodes;
15
11
  const levelMode = useViewer.getState().levelMode;
12
+ const hideAbove = useViewer.getState().hideLevelsAboveSelection;
16
13
  const selectedLevel = useViewer.getState().selection.levelId;
17
14
  const levelElevations = getLevelElevations(nodes);
18
15
  const entries = [];
@@ -42,23 +39,24 @@ export const LevelSystem = () => {
42
39
  // (blank viewport). Clamping keeps every step a contraction: identical
43
40
  // feel at 60 fps, exact snap instead of overshoot on slow frames.
44
41
  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) {
42
+ const { visible, shadowOnly } = resolveLevelVisibility({
43
+ levelMode,
44
+ hideAbove,
45
+ hasSelectedLevel: Boolean(selectedLevel),
46
+ isSelected: level?.id === selectedLevel,
47
+ index,
48
+ selectedIndex,
49
+ nodeVisible: level?.visible !== false,
50
+ });
51
+ if (shadowOnly) {
51
52
  applyShadowOnly(obj);
52
53
  shadowOnlyLevels.add(obj);
53
- obj.visible = true;
54
54
  }
55
- else {
56
- if (shadowOnlyLevels.has(obj)) {
57
- clearShadowOnly(obj);
58
- shadowOnlyLevels.delete(obj);
59
- }
60
- obj.visible = !hidden;
55
+ else if (shadowOnlyLevels.has(obj)) {
56
+ clearShadowOnly(obj);
57
+ shadowOnlyLevels.delete(obj);
61
58
  }
59
+ obj.visible = visible;
62
60
  }
63
61
  }, 5); // Using a lower priority so it runs after transforms from other systems have settled
64
62
  return null;
@@ -7,9 +7,36 @@ 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
16
+ * presentation hiding — solo mode hides every level but the soloed one, and
17
+ * the editor's level display (`hideAbove`) hides the levels above the
18
+ * selected one. Presentation-hidden levels ABOVE the selected one stay in the
19
+ * shadow map so the sun still shadows the selected floor through them.
20
+ * A level the author hid never enters that shadow-caster branch: its shadows
21
+ * on the floor below would be exactly what hiding it was meant to remove.
22
+ */
23
+ export declare function resolveLevelVisibility({ levelMode, hideAbove, hasSelectedLevel, isSelected, index, selectedIndex, nodeVisible, }: {
24
+ levelMode: 'stacked' | 'exploded' | 'solo' | 'manual';
25
+ hideAbove: boolean;
26
+ hasSelectedLevel: boolean;
27
+ isSelected: boolean;
28
+ index: number;
29
+ selectedIndex: number | undefined;
30
+ nodeVisible: boolean;
31
+ }): {
32
+ visible: boolean;
33
+ shadowOnly: boolean;
34
+ };
10
35
  /**
11
36
  * Instantly snaps all level Objects3D to their true stacked Y positions
12
- * (ignores levelMode — always uses stacked, no exploded gap).
37
+ * (ignores levelMode — always uses stacked, no exploded gap). Presentation
38
+ * hiding is undone with it, but a level the author hid stays hidden: the
39
+ * capture has to match the export, which prunes it.
13
40
  *
14
41
  * Returns a restore function that reverts each level's Y to what it was
15
42
  * 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":"AAIA,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;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CAAC,EACrC,SAAS,EACT,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,SAAS,EAAE,OAAO,CAAA;IAClB,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,CAQ5C;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,yBAAyB,IAAI,MAAM,IAAI,CA+CtD"}
@@ -1,4 +1,6 @@
1
1
  import { getLevelElevations, sceneRegistry, useScene } from '@pascal-app/core';
2
+ import { applyShadowOnly, clearShadowOnly } from '../../lib/shadow-only';
3
+ import { shadowOnlyLevels } from './shadow-only-levels';
2
4
  export const EXPLODED_GAP = 5;
3
5
  /**
4
6
  * The Y a level settles at under the given presentation mode — its stacked
@@ -13,9 +15,32 @@ export function getLevelPresentationY(levelId, nodes, levelMode) {
13
15
  const explodedExtra = levelMode === 'exploded' && level ? level.level * EXPLODED_GAP : 0;
14
16
  return baseY + explodedExtra;
15
17
  }
18
+ /**
19
+ * Whether a level renders this frame, and whether it renders as a
20
+ * shadow-caster only.
21
+ *
22
+ * Two unrelated things hide a level and they do not compose: the author's own
23
+ * `visible` flag (the sidebar eye), which hides the floor outright, and
24
+ * presentation hiding — solo mode hides every level but the soloed one, and
25
+ * the editor's level display (`hideAbove`) hides the levels above the
26
+ * selected one. Presentation-hidden levels ABOVE the selected one stay in the
27
+ * shadow map so the sun still shadows the selected floor through them.
28
+ * A level the author hid never enters that shadow-caster branch: its shadows
29
+ * on the floor below would be exactly what hiding it was meant to remove.
30
+ */
31
+ export function resolveLevelVisibility({ levelMode, hideAbove, hasSelectedLevel, isSelected, index, selectedIndex, nodeVisible, }) {
32
+ if (!nodeVisible)
33
+ return { visible: false, shadowOnly: false };
34
+ const hidden = (levelMode === 'solo' && hasSelectedLevel && !isSelected) ||
35
+ (hideAbove && selectedIndex !== undefined && index > selectedIndex);
36
+ const shadowOnly = hidden && selectedIndex !== undefined && index > selectedIndex;
37
+ return { visible: shadowOnly || !hidden, shadowOnly };
38
+ }
16
39
  /**
17
40
  * Instantly snaps all level Objects3D to their true stacked Y positions
18
- * (ignores levelMode — always uses stacked, no exploded gap).
41
+ * (ignores levelMode — always uses stacked, no exploded gap). Presentation
42
+ * hiding is undone with it, but a level the author hid stays hidden: the
43
+ * capture has to match the export, which prunes it.
19
44
  *
20
45
  * Returns a restore function that reverts each level's Y to what it was
21
46
  * before the snap, so lerp animations in LevelSystem can continue undisturbed.
@@ -35,17 +60,22 @@ export function snapLevelsToTruePositions() {
35
60
  entries.push({
36
61
  levelId,
37
62
  obj,
63
+ nodeVisible: level.visible !== false,
38
64
  });
39
65
  }
40
66
  });
41
67
  const levelElevations = getLevelElevations(nodes);
42
68
  // Snapshot current Y and visibility so we can restore them after the render
43
69
  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) {
70
+ // Snap to true stacked positions and undo presentation hiding — including
71
+ // the levels kept shadow-caster-only above the selected level.
72
+ const shadowOnly = entries.filter(({ obj }) => shadowOnlyLevels.has(obj));
73
+ for (const { levelId, obj, nodeVisible } of entries) {
46
74
  obj.position.y = levelElevations.get(levelId)?.baseY ?? 0;
47
- obj.visible = true;
75
+ obj.visible = nodeVisible;
48
76
  }
77
+ for (const { obj } of shadowOnly)
78
+ clearShadowOnly(obj);
49
79
  return () => {
50
80
  for (const { levelId, obj } of entries) {
51
81
  const saved = snapshot.get(levelId);
@@ -54,5 +84,7 @@ export function snapLevelsToTruePositions() {
54
84
  obj.visible = saved.visible;
55
85
  }
56
86
  }
87
+ for (const { obj } of shadowOnly)
88
+ applyShadowOnly(obj);
57
89
  };
58
90
  }
@@ -0,0 +1,9 @@
1
+ import type { Object3D } from 'three';
2
+ /**
3
+ * Levels currently in shadow-caster-only mode (hidden above the selected level
4
+ * by solo or by the editor's level display). Tracked so layer masks are
5
+ * restored exactly once on transition, and so a full-building capture can
6
+ * show them for its one render.
7
+ */
8
+ export declare const shadowOnlyLevels: WeakSet<Object3D<import("three").Object3DEventMap>>;
9
+ //# sourceMappingURL=shadow-only-levels.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shadow-only-levels.d.ts","sourceRoot":"","sources":["../../../src/systems/level/shadow-only-levels.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAErC;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,qDAA0B,CAAA"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Levels currently in shadow-caster-only mode (hidden above the selected level
3
+ * by solo or by the editor's level display). Tracked so layer masks are
4
+ * restored exactly once on transition, and so a full-building capture can
5
+ * show them for its one render.
6
+ */
7
+ export const shadowOnlyLevels = new WeakSet();
@@ -1,6 +1,18 @@
1
- import { type RoofNode } from '@pascal-app/core';
1
+ import { type AnyNode, type RoofNode, type SceneMaterial, type SceneMaterialId } from '@pascal-app/core';
2
2
  import type * as THREE from 'three';
3
3
  import { type ColorPreset, type RenderShading } from '../../lib/materials';
4
+ type SceneMaterials = Record<SceneMaterialId, SceneMaterial> | undefined;
4
5
  export type RoofMaterialArray = [THREE.Material, THREE.Material, THREE.Material, THREE.Material];
5
- export declare function getRoofMaterialArray(node: RoofNode, shading?: RenderShading, textures?: boolean, colorPreset?: ColorPreset, sceneTheme?: string): RoofMaterialArray | null;
6
+ /**
7
+ * The cladding the walls under a roof declare through their ASSEMBLIES
8
+ * (`wall.assembly.exterior.finish` → catalog ref), so the roof's gable band —
9
+ * the triangle of wall above the plate that the roof kind builds — is skinned
10
+ * like the walls it sits on instead of the bare drywall default. The most
11
+ * common ref among the level's walls wins; null when none declares one.
12
+ */
13
+ export declare function levelWallCladdingRef(nodes: Readonly<Record<string, AnyNode | undefined>>, roof: Pick<RoofNode, 'parentId'>): string | null;
14
+ export declare function getRoofMaterialArray(node: RoofNode, shading?: RenderShading, textures?: boolean, colorPreset?: ColorPreset, sceneTheme?: string,
15
+ /** Catalog ref for the gable/trim band when unpainted — see `levelWallCladdingRef`. */
16
+ wallCladdingRef?: string | null, sceneMaterials?: SceneMaterials): RoofMaterialArray | null;
17
+ export {};
6
18
  //# sourceMappingURL=roof-materials.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"roof-materials.d.ts","sourceRoot":"","sources":["../../../src/systems/roof/roof-materials.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,QAAQ,EAEd,MAAM,kBAAkB,CAAA;AACzB,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAA;AACnC,OAAO,EACL,KAAK,WAAW,EAIhB,KAAK,aAAa,EAEnB,MAAM,qBAAqB,CAAA;AAa5B,MAAM,MAAM,iBAAiB,GAAG,CAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAA;AA6BhG,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,QAAQ,EACd,OAAO,GAAE,aAA0B,EACnC,QAAQ,UAAO,EACf,WAAW,GAAE,WAAoB,EACjC,UAAU,CAAC,EAAE,MAAM,GAClB,iBAAiB,GAAG,IAAI,CAsE1B"}
1
+ {"version":3,"file":"roof-materials.d.ts","sourceRoot":"","sources":["../../../src/systems/roof/roof-materials.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,OAAO,EAGZ,KAAK,QAAQ,EAEb,KAAK,aAAa,EAClB,KAAK,eAAe,EAErB,MAAM,kBAAkB,CAAA;AACzB,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAA;AACnC,OAAO,EACL,KAAK,WAAW,EAGhB,KAAK,aAAa,EAGnB,MAAM,qBAAqB,CAAA;AAE5B,KAAK,cAAc,GAAG,MAAM,CAAC,eAAe,EAAE,aAAa,CAAC,GAAG,SAAS,CAAA;AAaxE,MAAM,MAAM,iBAAiB,GAAG,CAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAA;AAqChG;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,GAAG,SAAS,CAAC,CAAC,EACpD,IAAI,EAAE,IAAI,CAAC,QAAQ,EAAE,UAAU,CAAC,GAC/B,MAAM,GAAG,IAAI,CAkBf;AAED,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,QAAQ,EACd,OAAO,GAAE,aAA0B,EACnC,QAAQ,UAAO,EACf,WAAW,GAAE,WAAoB,EACjC,UAAU,CAAC,EAAE,MAAM;AACnB,uFAAuF;AACvF,eAAe,GAAE,MAAM,GAAG,IAAW,EACrC,cAAc,CAAC,EAAE,cAAc,GAC9B,iBAAiB,GAAG,IAAI,CAyE1B"}