@pascal-app/viewer 1.0.3 → 1.0.5

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 (276) hide show
  1. package/dist/capture/capture-runtime.js +1 -1
  2. package/dist/components/error-boundary.d.ts.map +1 -1
  3. package/dist/components/renderers/node-renderer.d.ts +1 -1
  4. package/dist/components/renderers/node-renderer.d.ts.map +1 -1
  5. package/dist/components/renderers/node-renderer.js +12 -4
  6. package/dist/components/renderers/parametric-node-renderer.d.ts.map +1 -1
  7. package/dist/components/viewer/bvh-ecctrl.d.ts.map +1 -1
  8. package/dist/components/viewer/frame-limiter.d.ts +1 -0
  9. package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
  10. package/dist/components/viewer/frame-limiter.js +22 -3
  11. package/dist/components/viewer/glb-interactive.d.ts +25 -1
  12. package/dist/components/viewer/glb-interactive.d.ts.map +1 -1
  13. package/dist/components/viewer/glb-interactive.js +253 -86
  14. package/dist/components/viewer/glb-mechanisms.d.ts +10 -0
  15. package/dist/components/viewer/glb-mechanisms.d.ts.map +1 -0
  16. package/dist/components/viewer/glb-mechanisms.js +19 -0
  17. package/dist/components/viewer/glb-reference-nodes.js +1 -1
  18. package/dist/components/viewer/glb-replace-instances.js +1 -1
  19. package/dist/components/viewer/glb-scene.d.ts +45 -27
  20. package/dist/components/viewer/glb-scene.d.ts.map +1 -1
  21. package/dist/components/viewer/glb-scene.js +421 -508
  22. package/dist/components/viewer/glb-walkthrough-controller.d.ts +1 -1
  23. package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -1
  24. package/dist/components/viewer/glb-walkthrough-controller.js +13 -2
  25. package/dist/components/viewer/index.d.ts +24 -1
  26. package/dist/components/viewer/index.d.ts.map +1 -1
  27. package/dist/components/viewer/index.js +111 -37
  28. package/dist/components/viewer/lights.d.ts.map +1 -1
  29. package/dist/components/viewer/lights.js +113 -22
  30. package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
  31. package/dist/components/viewer/perf-monitor.js +6 -0
  32. package/dist/components/viewer/perf-panel.d.ts.map +1 -1
  33. package/dist/components/viewer/perf-panel.js +1 -1
  34. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  35. package/dist/components/viewer/post-processing.js +104 -36
  36. package/dist/components/viewer/registered-systems.d.ts.map +1 -1
  37. package/dist/components/viewer/registered-systems.js +17 -3
  38. package/dist/components/viewer/render-error.d.ts +18 -0
  39. package/dist/components/viewer/render-error.d.ts.map +1 -0
  40. package/dist/components/viewer/render-error.js +29 -0
  41. package/dist/components/viewer/scene-bvh.d.ts.map +1 -1
  42. package/dist/components/viewer/scene-bvh.js +1 -1
  43. package/dist/components/viewer/scene-environment.d.ts.map +1 -1
  44. package/dist/components/viewer/scene-environment.js +31 -11
  45. package/dist/components/viewer/scene-ground-replacement.d.ts +6 -0
  46. package/dist/components/viewer/scene-ground-replacement.d.ts.map +1 -1
  47. package/dist/components/viewer/scene-ground-replacement.js +15 -0
  48. package/dist/components/viewer/selection-manager.d.ts.map +1 -1
  49. package/dist/components/viewer/selection-manager.js +25 -7
  50. package/dist/components/viewer/underground-ambiance.d.ts +9 -0
  51. package/dist/components/viewer/underground-ambiance.d.ts.map +1 -0
  52. package/dist/components/viewer/underground-ambiance.js +105 -0
  53. package/dist/components/viewer/viewer-camera.d.ts +4 -0
  54. package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
  55. package/dist/components/viewer/viewer-camera.js +12 -2
  56. package/dist/components/viewer/viewer-presentations.d.ts.map +1 -1
  57. package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
  58. package/dist/hooks/use-gltf-ktx2.js +2 -0
  59. package/dist/hooks/use-node-events.d.ts.map +1 -1
  60. package/dist/index.d.ts +36 -16
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +34 -19
  63. package/dist/lib/asset-url.d.ts.map +1 -1
  64. package/dist/lib/asset-url.js +7 -1
  65. package/dist/lib/backdrop.d.ts +17 -0
  66. package/dist/lib/backdrop.d.ts.map +1 -1
  67. package/dist/lib/backdrop.js +18 -1
  68. package/dist/lib/construction-dust.d.ts +57 -0
  69. package/dist/lib/construction-dust.d.ts.map +1 -0
  70. package/dist/lib/construction-dust.js +187 -0
  71. package/dist/lib/csg-utils.js +1 -1
  72. package/dist/lib/display-state.d.ts +25 -0
  73. package/dist/lib/display-state.d.ts.map +1 -0
  74. package/dist/lib/display-state.js +1 -0
  75. package/dist/lib/foundation-materials.d.ts +26 -0
  76. package/dist/lib/foundation-materials.d.ts.map +1 -0
  77. package/dist/lib/foundation-materials.js +65 -0
  78. package/dist/lib/gltf-unlit.d.ts +10 -0
  79. package/dist/lib/gltf-unlit.d.ts.map +1 -0
  80. package/dist/lib/gltf-unlit.js +23 -0
  81. package/dist/lib/ground-window.d.ts +33 -0
  82. package/dist/lib/ground-window.d.ts.map +1 -0
  83. package/dist/lib/ground-window.js +114 -0
  84. package/dist/lib/hero-pose.d.ts +13 -3
  85. package/dist/lib/hero-pose.d.ts.map +1 -1
  86. package/dist/lib/hero-pose.js +72 -5
  87. package/dist/lib/ink-edges.d.ts +6 -3
  88. package/dist/lib/ink-edges.d.ts.map +1 -1
  89. package/dist/lib/ink-edges.js +17 -7
  90. package/dist/lib/key-light-override.d.ts +11 -0
  91. package/dist/lib/key-light-override.d.ts.map +1 -0
  92. package/dist/lib/key-light-override.js +14 -0
  93. package/dist/lib/ktx2-loader.d.ts +7 -0
  94. package/dist/lib/ktx2-loader.d.ts.map +1 -1
  95. package/dist/lib/ktx2-loader.js +35 -0
  96. package/dist/lib/layer-pass.d.ts.map +1 -1
  97. package/dist/lib/layers.d.ts +11 -7
  98. package/dist/lib/layers.d.ts.map +1 -1
  99. package/dist/lib/layers.js +16 -6
  100. package/dist/lib/live-frame-hold.d.ts +14 -0
  101. package/dist/lib/live-frame-hold.d.ts.map +1 -0
  102. package/dist/lib/live-frame-hold.js +28 -0
  103. package/dist/lib/materials.d.ts +19 -7
  104. package/dist/lib/materials.d.ts.map +1 -1
  105. package/dist/lib/materials.js +65 -9
  106. package/dist/lib/merged-outline-node.d.ts +5 -0
  107. package/dist/lib/merged-outline-node.d.ts.map +1 -1
  108. package/dist/lib/merged-outline-node.js +80 -2
  109. package/dist/lib/node-top-surface-height.d.ts +6 -0
  110. package/dist/lib/node-top-surface-height.d.ts.map +1 -0
  111. package/dist/lib/node-top-surface-height.js +74 -0
  112. package/dist/lib/perf-probe-state.d.ts +39 -0
  113. package/dist/lib/perf-probe-state.d.ts.map +1 -0
  114. package/dist/lib/perf-probe-state.js +17 -0
  115. package/dist/lib/pointer-events.d.ts +26 -2
  116. package/dist/lib/pointer-events.d.ts.map +1 -1
  117. package/dist/lib/pointer-events.js +99 -12
  118. package/dist/lib/post-processing-resources.d.ts.map +1 -1
  119. package/dist/lib/procedural-emission.d.ts +7 -0
  120. package/dist/lib/procedural-emission.d.ts.map +1 -0
  121. package/dist/lib/procedural-emission.js +54 -0
  122. package/dist/lib/reveal-candidates.d.ts +80 -0
  123. package/dist/lib/reveal-candidates.d.ts.map +1 -0
  124. package/dist/lib/reveal-candidates.js +524 -0
  125. package/dist/lib/reveal-ghost.d.ts +9 -0
  126. package/dist/lib/reveal-ghost.d.ts.map +1 -0
  127. package/dist/lib/reveal-ghost.js +62 -0
  128. package/dist/lib/reveal-pending.d.ts +12 -0
  129. package/dist/lib/reveal-pending.d.ts.map +1 -0
  130. package/dist/lib/reveal-pending.js +12 -0
  131. package/dist/lib/reveal-pose.d.ts +70 -0
  132. package/dist/lib/reveal-pose.d.ts.map +1 -0
  133. package/dist/lib/reveal-pose.js +248 -0
  134. package/dist/lib/reveal-schedule.d.ts +208 -0
  135. package/dist/lib/reveal-schedule.d.ts.map +1 -0
  136. package/dist/lib/reveal-schedule.js +410 -0
  137. package/dist/lib/reveal-texture-scale.d.ts +5 -0
  138. package/dist/lib/reveal-texture-scale.d.ts.map +1 -0
  139. package/dist/lib/reveal-texture-scale.js +113 -0
  140. package/dist/lib/reveal-windows.d.ts +23 -0
  141. package/dist/lib/reveal-windows.d.ts.map +1 -0
  142. package/dist/lib/reveal-windows.js +31 -0
  143. package/dist/lib/reveal-wipe.d.ts +71 -0
  144. package/dist/lib/reveal-wipe.d.ts.map +1 -0
  145. package/dist/lib/reveal-wipe.js +105 -0
  146. package/dist/lib/scene-pass-mrt.d.ts +12 -0
  147. package/dist/lib/scene-pass-mrt.d.ts.map +1 -0
  148. package/dist/lib/scene-pass-mrt.js +23 -0
  149. package/dist/lib/scene-support-height.d.ts +4 -0
  150. package/dist/lib/scene-support-height.d.ts.map +1 -0
  151. package/dist/lib/scene-support-height.js +34 -0
  152. package/dist/lib/scene-themes.d.ts +30 -8
  153. package/dist/lib/scene-themes.d.ts.map +1 -1
  154. package/dist/lib/scene-themes.js +36 -0
  155. package/dist/lib/scene-visibility.d.ts +17 -1
  156. package/dist/lib/scene-visibility.d.ts.map +1 -1
  157. package/dist/lib/scene-visibility.js +53 -3
  158. package/dist/lib/scripted-opening.d.ts +11 -0
  159. package/dist/lib/scripted-opening.d.ts.map +1 -0
  160. package/dist/lib/scripted-opening.js +20 -0
  161. package/dist/lib/site-surfaces.d.ts +32 -0
  162. package/dist/lib/site-surfaces.d.ts.map +1 -0
  163. package/dist/lib/site-surfaces.js +70 -0
  164. package/dist/lib/snapshot-pipeline.d.ts +34 -3
  165. package/dist/lib/snapshot-pipeline.d.ts.map +1 -1
  166. package/dist/lib/snapshot-pipeline.js +247 -117
  167. package/dist/lib/terrain-perimeter-fill.d.ts.map +1 -1
  168. package/dist/lib/terrain-perimeter-fill.js +22 -14
  169. package/dist/lib/tsl-compat.d.ts +2 -2
  170. package/dist/lib/tsl-compat.d.ts.map +1 -1
  171. package/dist/lib/underground-ambiance.d.ts +168 -0
  172. package/dist/lib/underground-ambiance.d.ts.map +1 -0
  173. package/dist/lib/underground-ambiance.js +334 -0
  174. package/dist/lib/zone-geometry.d.ts +11 -0
  175. package/dist/lib/zone-geometry.d.ts.map +1 -0
  176. package/dist/lib/zone-geometry.js +46 -0
  177. package/dist/store/use-item-light-pool.d.ts +14 -10
  178. package/dist/store/use-item-light-pool.d.ts.map +1 -1
  179. package/dist/store/use-item-light-pool.js +52 -26
  180. package/dist/store/use-viewer.d.ts +194 -26
  181. package/dist/store/use-viewer.d.ts.map +1 -1
  182. package/dist/store/use-viewer.js +164 -21
  183. package/dist/systems/ceiling/ceiling-system.d.ts +15 -1
  184. package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
  185. package/dist/systems/ceiling/ceiling-system.js +88 -6
  186. package/dist/systems/construction-reveal/construction-dust.d.ts +16 -0
  187. package/dist/systems/construction-reveal/construction-dust.d.ts.map +1 -0
  188. package/dist/systems/construction-reveal/construction-dust.js +92 -0
  189. package/dist/systems/construction-reveal/construction-reveal.d.ts +276 -0
  190. package/dist/systems/construction-reveal/construction-reveal.d.ts.map +1 -0
  191. package/dist/systems/construction-reveal/construction-reveal.js +2383 -0
  192. package/dist/systems/construction-reveal/dust-material.d.ts +10 -0
  193. package/dist/systems/construction-reveal/dust-material.d.ts.map +1 -0
  194. package/dist/systems/construction-reveal/dust-material.js +17 -0
  195. package/dist/systems/construction-reveal/ghost-dress.d.ts +10 -0
  196. package/dist/systems/construction-reveal/ghost-dress.d.ts.map +1 -0
  197. package/dist/systems/construction-reveal/ghost-dress.js +24 -0
  198. package/dist/systems/door/door-system.d.ts.map +1 -1
  199. package/dist/systems/door/door-system.js +55 -17
  200. package/dist/systems/elevator/elevator-interaction-system.d.ts +7 -0
  201. package/dist/systems/elevator/elevator-interaction-system.d.ts.map +1 -1
  202. package/dist/systems/elevator/elevator-interaction-system.js +18 -1
  203. package/dist/systems/floor-elevation/floor-elevation-system.d.ts +3 -1
  204. package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -1
  205. package/dist/systems/floor-elevation/floor-elevation-system.js +33 -5
  206. package/dist/systems/geometry/geometry-system.d.ts.map +1 -1
  207. package/dist/systems/geometry/geometry-system.js +71 -11
  208. package/dist/systems/interactive/control-widget.d.ts.map +1 -1
  209. package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
  210. package/dist/systems/interactive/interactive-system.js +52 -17
  211. package/dist/systems/interactive/procedural-controls.d.ts +13 -0
  212. package/dist/systems/interactive/procedural-controls.d.ts.map +1 -0
  213. package/dist/systems/interactive/procedural-controls.js +17 -0
  214. package/dist/systems/interactive/scripted-clips.d.ts +19 -0
  215. package/dist/systems/interactive/scripted-clips.d.ts.map +1 -0
  216. package/dist/systems/interactive/scripted-clips.js +134 -0
  217. package/dist/systems/item/item-system.d.ts.map +1 -1
  218. package/dist/systems/item/item-system.js +9 -3
  219. package/dist/systems/item/object-cut-invalidation.d.ts +3 -0
  220. package/dist/systems/item/object-cut-invalidation.d.ts.map +1 -0
  221. package/dist/systems/item/object-cut-invalidation.js +68 -0
  222. package/dist/systems/item-light/item-light-system.d.ts +1 -1
  223. package/dist/systems/item-light/item-light-system.d.ts.map +1 -1
  224. package/dist/systems/item-light/item-light-system.js +48 -53
  225. package/dist/systems/level/level-hold.d.ts +4 -0
  226. package/dist/systems/level/level-hold.d.ts.map +1 -0
  227. package/dist/systems/level/level-hold.js +14 -0
  228. package/dist/systems/level/level-system.d.ts.map +1 -1
  229. package/dist/systems/level/level-system.js +19 -6
  230. package/dist/systems/level/level-utils.d.ts +19 -6
  231. package/dist/systems/level/level-utils.d.ts.map +1 -1
  232. package/dist/systems/level/level-utils.js +68 -9
  233. package/dist/systems/level/shadow-only-levels.d.ts +9 -0
  234. package/dist/systems/level/shadow-only-levels.d.ts.map +1 -0
  235. package/dist/systems/level/shadow-only-levels.js +7 -0
  236. package/dist/systems/roof/__fixtures__/roof-plane-cases.d.ts +103 -0
  237. package/dist/systems/roof/__fixtures__/roof-plane-cases.d.ts.map +1 -0
  238. package/dist/systems/roof/__fixtures__/roof-plane-cases.js +317 -0
  239. package/dist/systems/roof/roof-materials.d.ts +16 -2
  240. package/dist/systems/roof/roof-materials.d.ts.map +1 -1
  241. package/dist/systems/roof/roof-materials.js +64 -31
  242. package/dist/systems/roof/roof-system.d.ts +21 -4
  243. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  244. package/dist/systems/roof/roof-system.js +374 -49
  245. package/dist/systems/slab/slab-system.d.ts +1 -1
  246. package/dist/systems/slab/slab-system.d.ts.map +1 -1
  247. package/dist/systems/slab/slab-system.js +49 -19
  248. package/dist/systems/stair/stair-system.d.ts.map +1 -1
  249. package/dist/systems/stair/stair-system.js +133 -354
  250. package/dist/systems/wall/wall-cutout-cache.d.ts +39 -3
  251. package/dist/systems/wall/wall-cutout-cache.d.ts.map +1 -1
  252. package/dist/systems/wall/wall-cutout-cache.js +141 -19
  253. package/dist/systems/wall/wall-cutout.d.ts +6 -0
  254. package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
  255. package/dist/systems/wall/wall-cutout.js +42 -28
  256. package/dist/systems/wall/wall-finish-data.d.ts +22 -0
  257. package/dist/systems/wall/wall-finish-data.d.ts.map +1 -0
  258. package/dist/systems/wall/wall-finish-data.js +17 -0
  259. package/dist/systems/wall/wall-material-variant.d.ts.map +1 -1
  260. package/dist/systems/wall/wall-materials.d.ts +9 -4
  261. package/dist/systems/wall/wall-materials.d.ts.map +1 -1
  262. package/dist/systems/wall/wall-materials.js +135 -153
  263. package/dist/systems/wall/wall-placeholder-sweep.d.ts.map +1 -1
  264. package/dist/systems/wall/wall-system.d.ts +33 -2
  265. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  266. package/dist/systems/wall/wall-system.js +621 -159
  267. package/dist/systems/window/corner-frame.d.ts +10 -0
  268. package/dist/systems/window/corner-frame.d.ts.map +1 -0
  269. package/dist/systems/window/corner-frame.js +17 -0
  270. package/dist/systems/window/window-system.d.ts +1 -1
  271. package/dist/systems/window/window-system.d.ts.map +1 -1
  272. package/dist/systems/window/window-system.js +71 -27
  273. package/package.json +10 -10
  274. package/dist/systems/fence/fence-system.d.ts +0 -22
  275. package/dist/systems/fence/fence-system.d.ts.map +0 -1
  276. package/dist/systems/fence/fence-system.js +0 -357
@@ -1,27 +1,27 @@
1
1
  'use client';
2
- import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { pointInPolygon, sceneRegistry, useInteractive, useScene, } from '@pascal-app/core';
2
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
3
+ import { isScriptedNode, pointInPolygon, sceneRegistry, scriptedOrigin, scriptedSize, scriptInteractive, useInteractive, useScene, } from '@pascal-app/core';
4
+ import { evaluateRecipe, operableParts, } from '@pascal-app/core/procedural-items';
4
5
  import { Html } from '@react-three/drei';
5
6
  import { createPortal, useFrame } from '@react-three/fiber';
6
- import { useEffect, useState } from 'react';
7
+ import { useEffect, useMemo, useState } from 'react';
7
8
  import { Vector3 } from 'three';
8
9
  import { useShallow } from 'zustand/react/shallow';
9
10
  import useViewer from '../../store/use-viewer';
10
11
  import { ControlWidget } from './control-widget';
12
+ import { proceduralControlDescriptors } from './procedural-controls';
11
13
  const _tempVec = new Vector3();
12
- // ---- Parent: one overlay per interactive item inside the selected zone ----
14
+ // ---- Parent: overlays for selected interactive items and the selected zone ----
13
15
  //
14
- // The <Html> overlays only exist while a zone is selected and the item sits
15
- // inside it. Mounting them unconditionally is not an option: each drei <Html>
16
+ // Mounting <Html> overlays unconditionally is not an option: each drei <Html>
16
17
  // repositions and re-sorts its DOM element on every camera-move frame, and
17
18
  // with `occlude` it also raycasts the entire scene per overlay per frame. On
18
19
  // large scenes (hundreds of interactive items) that starves the frame budget
19
20
  // and the display/z-index churn makes the whole DOM UI flicker.
20
21
  //
21
- // The child components stay rendered (returning null) so an overlay can fade
22
- // out before its <Html> unmounts.
23
22
  export const InteractiveSystem = () => {
24
23
  const zoneId = useViewer((s) => s.selection.zoneId);
24
+ const selectedIds = useViewer((s) => s.selection.selectedIds);
25
25
  const zonePolygon = useScene((s) => {
26
26
  if (!zoneId)
27
27
  return null;
@@ -29,13 +29,18 @@ export const InteractiveSystem = () => {
29
29
  return z?.polygon ?? null;
30
30
  });
31
31
  const interactiveNodeIds = useScene(useShallow((state) => Object.values(state.nodes)
32
- .filter((n) => n.type === 'item' && n.asset.interactive != null)
32
+ .filter((n) => (isScriptedNode(n) && scriptInteractive(n.source.manifest) != null) ||
33
+ (n.type === 'item' && n.asset.interactive != null) ||
34
+ (n.type === 'procedural-item' &&
35
+ (operableParts(n.recipe).length > 0 || n.recipe.parts.some((part) => part.light))))
33
36
  .map((n) => n.id)));
34
- return (_jsx(_Fragment, { children: interactiveNodeIds.map((id) => (_jsx(ItemControlsOverlay, { nodeId: id, zonePolygon: zonePolygon }, id))) }));
37
+ return (_jsx(_Fragment, { children: interactiveNodeIds
38
+ .filter((id) => zonePolygon?.length || selectedIds.includes(id))
39
+ .map((id) => (_jsx(ItemControlsOverlay, { isSelected: selectedIds.includes(id), nodeId: id, zonePolygon: zonePolygon }, id))) }));
35
40
  };
36
41
  // ---- Child: polls sceneRegistry then portals controls into the item group ----
37
42
  const FADE_MS = 300;
38
- const ItemControlsOverlay = ({ nodeId, zonePolygon, }) => {
43
+ const ItemControlsOverlay = ({ nodeId, zonePolygon, isSelected, }) => {
39
44
  const node = useScene((state) => state.nodes[nodeId]);
40
45
  const [itemObj, setItemObj] = useState(null);
41
46
  useFrame(() => {
@@ -46,11 +51,43 @@ const ItemControlsOverlay = ({ nodeId, zonePolygon, }) => {
46
51
  setItemObj(obj);
47
52
  });
48
53
  const controlValues = useInteractive(useShallow((state) => state.items[nodeId]?.controlValues));
54
+ const proceduralState = useInteractive((state) => state.procedural[nodeId]);
55
+ const lampDefault = useInteractive((state) => state.lampDefault);
49
56
  const setControlValue = useInteractive((state) => state.setControlValue);
50
- let visible = false;
57
+ const togglePart = useInteractive((state) => state.toggleProceduralPart);
58
+ const toggleLights = useInteractive((state) => state.toggleProceduralLights);
59
+ const proceduralHeight = useMemo(() => node?.type === 'procedural-item' ? evaluateRecipe(node.recipe, node.parameters).max[1] : 0, [
60
+ node?.type === 'procedural-item' ? node.recipe : null,
61
+ node?.type === 'procedural-item' ? node.parameters : null,
62
+ ]);
63
+ let descriptors = [];
64
+ let height = 0;
65
+ const interactive = isScriptedNode(node)
66
+ ? scriptInteractive(node.source.manifest, scriptedOrigin(node))
67
+ : node?.type === 'item'
68
+ ? node.asset.interactive
69
+ : undefined;
70
+ if (interactive && controlValues) {
71
+ descriptors = interactive.controls.map((control, i) => ({
72
+ key: String(i),
73
+ control,
74
+ value: controlValues[i] ?? false,
75
+ onChange: (value) => setControlValue(nodeId, i, value),
76
+ }));
77
+ height = isScriptedNode(node)
78
+ ? scriptedSize(node.source.manifest)[1] + scriptedOrigin(node)[1]
79
+ : node?.type === 'item'
80
+ ? node.asset.dimensions[1]
81
+ : 0;
82
+ }
83
+ else if (node?.type === 'procedural-item') {
84
+ descriptors = proceduralControlDescriptors(node.recipe, proceduralState, (partId) => togglePart(nodeId, partId), () => toggleLights(nodeId), lampDefault);
85
+ height = proceduralHeight;
86
+ }
87
+ let visible = isSelected;
51
88
  if (itemObj && zonePolygon?.length) {
52
89
  itemObj.getWorldPosition(_tempVec);
53
- visible = pointInPolygon(_tempVec.x, _tempVec.z, zonePolygon);
90
+ visible = visible || pointInPolygon(_tempVec.x, _tempVec.z, zonePolygon);
54
91
  }
55
92
  // Fade in on mount and fade out before unmounting the <Html>.
56
93
  const [mounted, setMounted] = useState(false);
@@ -73,10 +110,8 @@ const ItemControlsOverlay = ({ nodeId, zonePolygon, }) => {
73
110
  const timeout = setTimeout(() => setMounted(false), FADE_MS);
74
111
  return () => clearTimeout(timeout);
75
112
  }, [visible]);
76
- if (!(mounted && itemObj && controlValues && node?.asset.interactive))
113
+ if (!(mounted && itemObj && descriptors.length))
77
114
  return null;
78
- const { controls } = node.asset.interactive;
79
- const [, height] = node.asset.dimensions;
80
115
  return createPortal(_jsx(Html, { center: true, distanceFactor: 8, eps: -1, position: [0, height + 0.3, 0], zIndexRange: [20, 0], children: _jsx("div", { style: {
81
116
  display: 'flex',
82
117
  flexDirection: 'column',
@@ -90,5 +125,5 @@ const ItemControlsOverlay = ({ nodeId, zonePolygon, }) => {
90
125
  userSelect: 'none',
91
126
  opacity: shown ? 1 : 0,
92
127
  transition: `opacity ${FADE_MS}ms ease`,
93
- }, children: controls.map((control, i) => (_jsx(ControlWidget, { control: control, onChange: (v) => setControlValue(nodeId, i, v), value: controlValues[i] ?? false }, i))) }) }), itemObj);
128
+ }, children: descriptors.map((descriptor) => (_jsx(ControlWidget, { control: descriptor.control, onChange: descriptor.onChange, value: descriptor.value }, descriptor.key))) }) }), itemObj);
94
129
  };
@@ -0,0 +1,13 @@
1
+ import type { Control, ControlValue } from '@pascal-app/core';
2
+ import { type ProceduralItemNode } from '@pascal-app/core/procedural-items';
3
+ export type ControlDescriptor = {
4
+ key: string;
5
+ control: Control;
6
+ value: ControlValue;
7
+ onChange: (value: ControlValue) => void;
8
+ };
9
+ export declare function proceduralControlDescriptors(recipe: Pick<ProceduralItemNode['recipe'], 'parts' | 'joints'>, state: {
10
+ parts: Record<string, boolean>;
11
+ lightsOn: boolean;
12
+ } | undefined, togglePart: (partId: string) => void, toggleLights: () => void, lampDefault?: boolean): ControlDescriptor[];
13
+ //# sourceMappingURL=procedural-controls.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"procedural-controls.d.ts","sourceRoot":"","sources":["../../../src/systems/interactive/procedural-controls.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAC7D,OAAO,EAAiB,KAAK,kBAAkB,EAAE,MAAM,mCAAmC,CAAA;AAE1F,MAAM,MAAM,iBAAiB,GAAG;IAC9B,GAAG,EAAE,MAAM,CAAA;IACX,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,YAAY,CAAA;IACnB,QAAQ,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAA;CACxC,CAAA;AAED,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,IAAI,CAAC,kBAAkB,CAAC,QAAQ,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC,EAC9D,KAAK,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,GAAG,SAAS,EACxE,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,EACpC,YAAY,EAAE,MAAM,IAAI,EACxB,WAAW,UAAQ,GAClB,iBAAiB,EAAE,CAerB"}
@@ -0,0 +1,17 @@
1
+ import { operableParts } from '@pascal-app/core/procedural-items';
2
+ export function proceduralControlDescriptors(recipe, state, togglePart, toggleLights, lampDefault = false) {
3
+ const controls = operableParts(recipe).map((part) => ({
4
+ key: part.id,
5
+ control: { kind: 'toggle', label: part.label },
6
+ value: state?.parts[part.id] ?? part.kind === 'spin',
7
+ onChange: () => togglePart(part.id),
8
+ }));
9
+ if (recipe.parts.some((part) => part.light))
10
+ controls.push({
11
+ key: 'lights',
12
+ control: { kind: 'toggle', label: 'Lights' },
13
+ value: state?.lightsOn ?? lampDefault,
14
+ onChange: toggleLights,
15
+ });
16
+ return controls;
17
+ }
@@ -0,0 +1,19 @@
1
+ import { type AnyNodeId, type Interactive } from '@pascal-app/core';
2
+ import { type RefObject } from 'react';
3
+ import { type AnimationAction, type AnimationClip, Object3D } from 'three';
4
+ /** Actions keyed by the clip names the script wrote (`open`, `loop`, `Princess twirl`). */
5
+ export type ScriptedClipActions = Record<string, AnimationAction | null | undefined>;
6
+ /** Own the action cache so replacing clips cannot retain bindings to an old artifact. */
7
+ export declare function useClipActions(clips: AnimationClip[], root: RefObject<Object3D | null>): Record<string, AnimationAction | null>;
8
+ /**
9
+ * An authored object's clips, each driven by its own toggle: an open-close
10
+ * effect plays `open` once and holds, closing plays `close` or `open` reversed;
11
+ * an ambient effect plays its clip while its toggle is on (always, for `loop`).
12
+ * Shared by the live item renderer and the baked viewer.
13
+ */
14
+ export declare function ScriptedClips({ nodeId, interactive, actions, }: {
15
+ nodeId: AnyNodeId;
16
+ interactive: Interactive;
17
+ actions: ScriptedClipActions;
18
+ }): import("react").JSX.Element;
19
+ //# sourceMappingURL=scripted-clips.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scripted-clips.d.ts","sourceRoot":"","sources":["../../../src/systems/interactive/scripted-clips.tsx"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,SAAS,EACd,KAAK,WAAW,EAEjB,MAAM,kBAAkB,CAAA;AAEzB,OAAO,EAAE,KAAK,SAAS,EAA+C,MAAM,OAAO,CAAA;AACnF,OAAO,EACL,KAAK,eAAe,EACpB,KAAK,aAAa,EAIlB,QAAQ,EACT,MAAM,OAAO,CAAA;AAEd,2FAA2F;AAC3F,MAAM,MAAM,mBAAmB,GAAG,MAAM,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI,GAAG,SAAS,CAAC,CAAA;AAEpF,yFAAyF;AACzF,wBAAgB,cAAc,CAAC,KAAK,EAAE,aAAa,EAAE,EAAE,IAAI,EAAE,SAAS,CAAC,QAAQ,GAAG,IAAI,CAAC,0CAoCtF;AAQD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,EAC5B,MAAM,EACN,WAAW,EACX,OAAO,GACR,EAAE;IACD,MAAM,EAAE,SAAS,CAAA;IACjB,WAAW,EAAE,WAAW,CAAA;IACxB,OAAO,EAAE,mBAAmB,CAAA;CAC7B,+BAoBA"}
@@ -0,0 +1,134 @@
1
+ 'use client';
2
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
3
+ import { useInteractive, } from '@pascal-app/core';
4
+ import { useFrame } from '@react-three/fiber';
5
+ import { useEffect, useLayoutEffect, useMemo, useRef } from 'react';
6
+ import { AnimationMixer, LoopOnce, LoopRepeat, Object3D, } from 'three';
7
+ /** Own the action cache so replacing clips cannot retain bindings to an old artifact. */
8
+ export function useClipActions(clips, root) {
9
+ const { mixer, actions, cached, lifecycle } = useMemo(() => {
10
+ const mixer = new AnimationMixer(new Object3D());
11
+ const cached = new Map();
12
+ const lifecycle = { active: true };
13
+ const actions = {};
14
+ for (const clip of clips) {
15
+ Object.defineProperty(actions, clip.name, {
16
+ enumerable: true,
17
+ configurable: true,
18
+ get: () => {
19
+ if (!lifecycle.active || !root.current)
20
+ return null;
21
+ let action = cached.get(clip.name);
22
+ if (!action) {
23
+ action = mixer.clipAction(clip, root.current);
24
+ cached.set(clip.name, action);
25
+ }
26
+ return action;
27
+ },
28
+ });
29
+ }
30
+ return { mixer, actions, cached, lifecycle };
31
+ }, [clips, root]);
32
+ useFrame((_, delta) => mixer.update(delta));
33
+ useLayoutEffect(() => {
34
+ lifecycle.active = true;
35
+ return () => {
36
+ // React's development prop diff can read old getters after cleanup.
37
+ lifecycle.active = false;
38
+ mixer.stopAllAction();
39
+ const roots = new Set([...cached.values()].map((action) => action.getRoot()));
40
+ cached.clear();
41
+ for (const object of roots)
42
+ mixer.uncacheRoot(object);
43
+ };
44
+ }, [mixer, cached, lifecycle]);
45
+ return actions;
46
+ }
47
+ /** Whether an effect's toggle is on; an effect without one always runs. */
48
+ const useEffectControl = (nodeId, control) => useInteractive((s) => control === undefined ? true : Boolean(s.items[nodeId]?.controlValues[control]));
49
+ /**
50
+ * An authored object's clips, each driven by its own toggle: an open-close
51
+ * effect plays `open` once and holds, closing plays `close` or `open` reversed;
52
+ * an ambient effect plays its clip while its toggle is on (always, for `loop`).
53
+ * Shared by the live item renderer and the baked viewer.
54
+ */
55
+ export function ScriptedClips({ nodeId, interactive, actions, }) {
56
+ const effects = interactive.effects.filter((effect) => effect.kind === 'animation');
57
+ return (_jsx(_Fragment, { children: effects.map((effect) => effect.mode === 'open-close' ? (_jsx(OpenCloseClip, { actions: actions, effect: effect, nodeId: nodeId }, effect.clips.on)) : (_jsx(PlayClip, { actions: actions, effect: effect, nodeId: nodeId }, effect.clips.on ?? effect.clips.loop))) }));
58
+ }
59
+ const PlayClip = ({ nodeId, effect, actions, }) => {
60
+ const on = useEffectControl(nodeId, effect.control);
61
+ const name = effect.clips.on ?? effect.clips.loop;
62
+ useEffect(() => {
63
+ const action = name ? actions[name] : undefined;
64
+ return () => {
65
+ action?.stop();
66
+ };
67
+ }, [actions, name]);
68
+ useEffect(() => {
69
+ const action = name ? actions[name] : undefined;
70
+ if (!action)
71
+ return;
72
+ if (on) {
73
+ action.setLoop(LoopRepeat, Number.POSITIVE_INFINITY);
74
+ action.clampWhenFinished = false;
75
+ action.paused = false;
76
+ action.play();
77
+ }
78
+ else {
79
+ // Hold the pose where it was, like pausing a music box.
80
+ action.paused = true;
81
+ }
82
+ }, [actions, name, on]);
83
+ return null;
84
+ };
85
+ const OpenCloseClip = ({ nodeId, effect, actions, }) => {
86
+ const isOpen = useEffectControl(nodeId, effect.control);
87
+ const mounted = useRef(false);
88
+ useEffect(() => {
89
+ const open = effect.clips.on ? actions[effect.clips.on] : undefined;
90
+ const close = effect.clips.off ? actions[effect.clips.off] : undefined;
91
+ return () => {
92
+ open?.stop();
93
+ close?.stop();
94
+ mounted.current = false;
95
+ };
96
+ }, [actions, effect.clips.on, effect.clips.off]);
97
+ useEffect(() => {
98
+ const open = effect.clips.on ? actions[effect.clips.on] : undefined;
99
+ const close = effect.clips.off ? actions[effect.clips.off] : undefined;
100
+ if (!open)
101
+ return;
102
+ const first = !mounted.current;
103
+ mounted.current = true;
104
+ for (const action of [open, close]) {
105
+ if (!action)
106
+ continue;
107
+ action.setLoop(LoopOnce, 1);
108
+ action.clampWhenFinished = true;
109
+ }
110
+ if (isOpen) {
111
+ close?.stop();
112
+ open.paused = false;
113
+ open.timeScale = 1;
114
+ if (!open.isRunning())
115
+ open.reset();
116
+ open.play();
117
+ // Already open when the scene loads: hold the open pose, no swing.
118
+ if (first)
119
+ open.time = open.getClip().duration;
120
+ return;
121
+ }
122
+ if (first)
123
+ return;
124
+ if (close) {
125
+ open.stop();
126
+ close.reset().play();
127
+ return;
128
+ }
129
+ open.paused = false;
130
+ open.timeScale = -1;
131
+ open.play();
132
+ }, [actions, effect.clips.on, effect.clips.off, isOpen]);
133
+ return null;
134
+ };
@@ -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
- 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';
1
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
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
  }
@@ -0,0 +1,4 @@
1
+ export declare const setHeldLevelY: (levelId: string, y: number) => void;
2
+ export declare const heldLevelY: (levelId: string) => number | undefined;
3
+ export declare const releaseLevel: (levelId: string) => void;
4
+ //# sourceMappingURL=level-hold.d.ts.map