@pascal-app/viewer 0.7.0 → 0.9.1

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 (216) hide show
  1. package/dist/components/renderers/node-renderer.d.ts +1 -1
  2. package/dist/components/renderers/node-renderer.d.ts.map +1 -1
  3. package/dist/components/renderers/node-renderer.js +38 -22
  4. package/dist/components/renderers/parametric-node-renderer.d.ts +5 -0
  5. package/dist/components/renderers/parametric-node-renderer.d.ts.map +1 -0
  6. package/dist/components/renderers/parametric-node-renderer.js +34 -0
  7. package/dist/components/renderers/scene-renderer.d.ts +1 -1
  8. package/dist/components/renderers/scene-renderer.d.ts.map +1 -1
  9. package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
  10. package/dist/components/viewer/frame-limiter.js +5 -1
  11. package/dist/components/viewer/ground-occluder.d.ts +1 -1
  12. package/dist/components/viewer/ground-occluder.d.ts.map +1 -1
  13. package/dist/components/viewer/ground-occluder.js +12 -24
  14. package/dist/components/viewer/index.d.ts +38 -1
  15. package/dist/components/viewer/index.d.ts.map +1 -1
  16. package/dist/components/viewer/index.js +183 -60
  17. package/dist/components/viewer/lights.d.ts +1 -1
  18. package/dist/components/viewer/lights.d.ts.map +1 -1
  19. package/dist/components/viewer/lights.js +170 -43
  20. package/dist/components/viewer/perf-monitor.d.ts +1 -1
  21. package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
  22. package/dist/components/viewer/perf-monitor.js +99 -11
  23. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  24. package/dist/components/viewer/post-processing.js +217 -61
  25. package/dist/components/viewer/registered-systems.d.ts +12 -0
  26. package/dist/components/viewer/registered-systems.d.ts.map +1 -0
  27. package/dist/components/viewer/registered-systems.js +47 -0
  28. package/dist/components/viewer/scene-bvh.d.ts +17 -0
  29. package/dist/components/viewer/scene-bvh.d.ts.map +1 -0
  30. package/dist/components/viewer/scene-bvh.js +94 -0
  31. package/dist/components/viewer/selection-manager.d.ts +1 -1
  32. package/dist/components/viewer/selection-manager.d.ts.map +1 -1
  33. package/dist/components/viewer/selection-manager.js +20 -5
  34. package/dist/components/viewer/viewer-camera.d.ts +1 -1
  35. package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
  36. package/dist/components/viewer/walkthrough-controls.d.ts +1 -1
  37. package/dist/components/viewer/walkthrough-controls.d.ts.map +1 -1
  38. package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
  39. package/dist/hooks/use-node-events.d.ts +5 -73
  40. package/dist/hooks/use-node-events.d.ts.map +1 -1
  41. package/dist/hooks/use-node-events.js +19 -7
  42. package/dist/index.d.ts +36 -4
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +67 -3
  45. package/dist/lib/csg-utils.d.ts +16 -0
  46. package/dist/lib/csg-utils.d.ts.map +1 -0
  47. package/dist/lib/csg-utils.js +33 -0
  48. package/dist/lib/drawable-geometry.d.ts +21 -0
  49. package/dist/lib/drawable-geometry.d.ts.map +1 -0
  50. package/dist/lib/drawable-geometry.js +22 -0
  51. package/dist/lib/edge-style.d.ts +3 -0
  52. package/dist/lib/edge-style.d.ts.map +1 -0
  53. package/dist/lib/edge-style.js +11 -0
  54. package/dist/lib/gpu-perf.d.ts +4 -0
  55. package/dist/lib/gpu-perf.d.ts.map +1 -0
  56. package/dist/lib/gpu-perf.js +23 -0
  57. package/dist/lib/ink-edges.d.ts +9 -0
  58. package/dist/lib/ink-edges.d.ts.map +1 -0
  59. package/dist/lib/ink-edges.js +43 -0
  60. package/dist/lib/isolation.d.ts +39 -0
  61. package/dist/lib/isolation.d.ts.map +1 -0
  62. package/dist/lib/isolation.js +103 -0
  63. package/dist/lib/layers.d.ts +18 -0
  64. package/dist/lib/layers.d.ts.map +1 -1
  65. package/dist/lib/layers.js +18 -0
  66. package/dist/lib/materials.d.ts +25 -15
  67. package/dist/lib/materials.d.ts.map +1 -1
  68. package/dist/lib/materials.js +277 -63
  69. package/dist/lib/merged-outline-node.d.ts.map +1 -1
  70. package/dist/lib/merged-outline-node.js +11 -11
  71. package/dist/lib/polygon-union.d.ts +4 -0
  72. package/dist/lib/polygon-union.d.ts.map +1 -0
  73. package/dist/lib/polygon-union.js +289 -0
  74. package/dist/lib/polygon-union.test.d.ts +2 -0
  75. package/dist/lib/polygon-union.test.d.ts.map +1 -0
  76. package/dist/lib/polygon-union.test.js +105 -0
  77. package/dist/lib/scene-themes.d.ts +29 -0
  78. package/dist/lib/scene-themes.d.ts.map +1 -0
  79. package/dist/lib/scene-themes.js +193 -0
  80. package/dist/store/use-viewer.d.ts +42 -4
  81. package/dist/store/use-viewer.d.ts.map +1 -1
  82. package/dist/store/use-viewer.js +26 -3
  83. package/dist/systems/ceiling/ceiling-system.d.ts +6 -2
  84. package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
  85. package/dist/systems/ceiling/ceiling-system.js +62 -12
  86. package/dist/systems/door/door-system.d.ts.map +1 -1
  87. package/dist/systems/door/door-system.js +373 -22
  88. package/dist/systems/elevator/elevator-interaction-system.d.ts +2 -0
  89. package/dist/systems/elevator/elevator-interaction-system.d.ts.map +1 -0
  90. package/dist/systems/elevator/elevator-interaction-system.js +62 -0
  91. package/dist/systems/fence/fence-system.d.ts +3 -0
  92. package/dist/systems/fence/fence-system.d.ts.map +1 -1
  93. package/dist/systems/fence/fence-system.js +10 -6
  94. package/dist/systems/floor-elevation/floor-elevation-system.d.ts +20 -0
  95. package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -0
  96. package/dist/systems/floor-elevation/floor-elevation-system.js +78 -0
  97. package/dist/systems/geometry/geometry-system.d.ts +34 -0
  98. package/dist/systems/geometry/geometry-system.d.ts.map +1 -0
  99. package/dist/systems/geometry/geometry-system.js +279 -0
  100. package/dist/systems/guide/guide-system.d.ts.map +1 -1
  101. package/dist/systems/guide/guide-system.js +1 -26
  102. package/dist/systems/interactive/interactive-system.d.ts +1 -1
  103. package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
  104. package/dist/systems/item/item-system.d.ts +7 -0
  105. package/dist/systems/item/item-system.d.ts.map +1 -1
  106. package/dist/systems/item/item-system.js +8 -11
  107. package/dist/systems/item-light/item-light-system.d.ts +1 -1
  108. package/dist/systems/item-light/item-light-system.d.ts.map +1 -1
  109. package/dist/systems/item-light/item-light-system.js +18 -5
  110. package/dist/systems/roof/roof-materials.d.ts +3 -2
  111. package/dist/systems/roof/roof-materials.d.ts.map +1 -1
  112. package/dist/systems/roof/roof-materials.js +38 -14
  113. package/dist/systems/roof/roof-system.d.ts +27 -0
  114. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  115. package/dist/systems/roof/roof-system.js +411 -71
  116. package/dist/systems/scan/scan-system.d.ts.map +1 -1
  117. package/dist/systems/scan/scan-system.js +1 -26
  118. package/dist/systems/slab/slab-system.d.ts.map +1 -1
  119. package/dist/systems/slab/slab-system.js +167 -49
  120. package/dist/systems/slab/slab-system.test.d.ts +2 -0
  121. package/dist/systems/slab/slab-system.test.d.ts.map +1 -0
  122. package/dist/systems/slab/slab-system.test.js +62 -0
  123. package/dist/systems/stair/stair-materials.d.ts +4 -3
  124. package/dist/systems/stair/stair-materials.d.ts.map +1 -1
  125. package/dist/systems/stair/stair-materials.js +30 -15
  126. package/dist/systems/stair/stair-system.d.ts.map +1 -1
  127. package/dist/systems/stair/stair-system.js +58 -71
  128. package/dist/systems/surface-hole-geometry.d.ts +3 -0
  129. package/dist/systems/surface-hole-geometry.d.ts.map +1 -0
  130. package/dist/systems/surface-hole-geometry.js +4 -0
  131. package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
  132. package/dist/systems/wall/wall-cutout.js +18 -2
  133. package/dist/systems/wall/wall-materials.d.ts +4 -3
  134. package/dist/systems/wall/wall-materials.d.ts.map +1 -1
  135. package/dist/systems/wall/wall-materials.js +52 -28
  136. package/dist/systems/wall/wall-system.d.ts +1 -1
  137. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  138. package/dist/systems/wall/wall-system.js +179 -31
  139. package/dist/systems/window/window-system.d.ts.map +1 -1
  140. package/dist/systems/window/window-system.js +71 -7
  141. package/package.json +4 -5
  142. package/dist/components/renderers/building/building-renderer.d.ts +0 -5
  143. package/dist/components/renderers/building/building-renderer.d.ts.map +0 -1
  144. package/dist/components/renderers/building/building-renderer.js +0 -11
  145. package/dist/components/renderers/ceiling/ceiling-renderer.d.ts +0 -5
  146. package/dist/components/renderers/ceiling/ceiling-renderer.d.ts.map +0 -1
  147. package/dist/components/renderers/ceiling/ceiling-renderer.js +0 -58
  148. package/dist/components/renderers/column/column-renderer.d.ts +0 -5
  149. package/dist/components/renderers/column/column-renderer.d.ts.map +0 -1
  150. package/dist/components/renderers/column/column-renderer.js +0 -594
  151. package/dist/components/renderers/door/door-renderer.d.ts +0 -5
  152. package/dist/components/renderers/door/door-renderer.d.ts.map +0 -1
  153. package/dist/components/renderers/door/door-renderer.js +0 -16
  154. package/dist/components/renderers/fence/fence-renderer.d.ts +0 -5
  155. package/dist/components/renderers/fence/fence-renderer.d.ts.map +0 -1
  156. package/dist/components/renderers/fence/fence-renderer.js +0 -29
  157. package/dist/components/renderers/guide/guide-renderer.d.ts +0 -5
  158. package/dist/components/renderers/guide/guide-renderer.d.ts.map +0 -1
  159. package/dist/components/renderers/guide/guide-renderer.js +0 -38
  160. package/dist/components/renderers/item/item-renderer.d.ts +0 -5
  161. package/dist/components/renderers/item/item-renderer.d.ts.map +0 -1
  162. package/dist/components/renderers/item/item-renderer.js +0 -172
  163. package/dist/components/renderers/level/level-renderer.d.ts +0 -5
  164. package/dist/components/renderers/level/level-renderer.d.ts.map +0 -1
  165. package/dist/components/renderers/level/level-renderer.js +0 -11
  166. package/dist/components/renderers/roof/roof-materials.d.ts +0 -4
  167. package/dist/components/renderers/roof/roof-materials.d.ts.map +0 -1
  168. package/dist/components/renderers/roof/roof-materials.js +0 -16
  169. package/dist/components/renderers/roof/roof-renderer.d.ts +0 -5
  170. package/dist/components/renderers/roof/roof-renderer.d.ts.map +0 -1
  171. package/dist/components/renderers/roof/roof-renderer.js +0 -32
  172. package/dist/components/renderers/roof-segment/roof-segment-renderer.d.ts +0 -5
  173. package/dist/components/renderers/roof-segment/roof-segment-renderer.d.ts.map +0 -1
  174. package/dist/components/renderers/roof-segment/roof-segment-renderer.js +0 -40
  175. package/dist/components/renderers/scan/scan-renderer.d.ts +0 -5
  176. package/dist/components/renderers/scan/scan-renderer.d.ts.map +0 -1
  177. package/dist/components/renderers/scan/scan-renderer.js +0 -54
  178. package/dist/components/renderers/site/site-renderer.d.ts +0 -5
  179. package/dist/components/renderers/site/site-renderer.d.ts.map +0 -1
  180. package/dist/components/renderers/site/site-renderer.js +0 -106
  181. package/dist/components/renderers/slab/slab-renderer.d.ts +0 -5
  182. package/dist/components/renderers/slab/slab-renderer.d.ts.map +0 -1
  183. package/dist/components/renderers/slab/slab-renderer.js +0 -58
  184. package/dist/components/renderers/spawn/spawn-renderer.d.ts +0 -5
  185. package/dist/components/renderers/spawn/spawn-renderer.d.ts.map +0 -1
  186. package/dist/components/renderers/spawn/spawn-renderer.js +0 -32
  187. package/dist/components/renderers/stair/stair-renderer.d.ts +0 -5
  188. package/dist/components/renderers/stair/stair-renderer.d.ts.map +0 -1
  189. package/dist/components/renderers/stair/stair-renderer.js +0 -603
  190. package/dist/components/renderers/stair-segment/stair-segment-renderer.d.ts +0 -5
  191. package/dist/components/renderers/stair-segment/stair-segment-renderer.d.ts.map +0 -1
  192. package/dist/components/renderers/stair-segment/stair-segment-renderer.js +0 -32
  193. package/dist/components/renderers/wall/wall-renderer.d.ts +0 -5
  194. package/dist/components/renderers/wall/wall-renderer.d.ts.map +0 -1
  195. package/dist/components/renderers/wall/wall-renderer.js +0 -16
  196. package/dist/components/renderers/window/window-renderer.d.ts +0 -5
  197. package/dist/components/renderers/window/window-renderer.d.ts.map +0 -1
  198. package/dist/components/renderers/window/window-renderer.js +0 -21
  199. package/dist/components/renderers/zone/zone-renderer.d.ts +0 -5
  200. package/dist/components/renderers/zone/zone-renderer.d.ts.map +0 -1
  201. package/dist/components/renderers/zone/zone-renderer.js +0 -181
  202. package/dist/lib/suppress-three-clock-warning.d.ts +0 -25
  203. package/dist/lib/suppress-three-clock-warning.d.ts.map +0 -1
  204. package/dist/lib/suppress-three-clock-warning.js +0 -51
  205. package/dist/systems/export/export-system.d.ts +0 -2
  206. package/dist/systems/export/export-system.d.ts.map +0 -1
  207. package/dist/systems/export/export-system.js +0 -60
  208. package/dist/systems/item-mesh-metadata/compute-item-mesh-metadata.d.ts +0 -14
  209. package/dist/systems/item-mesh-metadata/compute-item-mesh-metadata.d.ts.map +0 -1
  210. package/dist/systems/item-mesh-metadata/compute-item-mesh-metadata.js +0 -155
  211. package/dist/systems/item-mesh-metadata/item-mesh-metadata-system.d.ts +0 -6
  212. package/dist/systems/item-mesh-metadata/item-mesh-metadata-system.d.ts.map +0 -1
  213. package/dist/systems/item-mesh-metadata/item-mesh-metadata-system.js +0 -75
  214. package/dist/systems/item-mesh-metadata/sync-request.d.ts +0 -7
  215. package/dist/systems/item-mesh-metadata/sync-request.d.ts.map +0 -1
  216. package/dist/systems/item-mesh-metadata/sync-request.js +0 -25
@@ -1,5 +1,5 @@
1
- import { useFrame } from '@react-three/fiber';
2
1
  import { getWallCurveFrameAt, getWallCurveLength, sceneRegistry, useScene, } from '@pascal-app/core';
2
+ import { useFrame } from '@react-three/fiber';
3
3
  import * as THREE from 'three';
4
4
  import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
5
5
  const MIN_CURVE_SEGMENT_LENGTH = 0.18;
@@ -111,6 +111,7 @@ function createFenceParts(fence) {
111
111
  const spacing = Math.max(fence.postSpacing * styleDefaults.spacingFactor, postWidth * 1.2);
112
112
  const edgeInset = Math.max(fence.edgeInset ?? 0.015, 0.005);
113
113
  const isFloating = fence.baseStyle === 'floating';
114
+ const showInfill = fence.showInfill ?? true;
114
115
  const baseY = isFloating ? clearance : 0;
115
116
  const effectiveBaseHeight = baseHeight;
116
117
  const startInsetT = Math.min(0.499, edgeInset / length);
@@ -119,16 +120,17 @@ function createFenceParts(fence) {
119
120
  parts.push(...createFenceCurveSpanParts(fence, 0, 1, baseY + effectiveBaseHeight / 2, effectiveBaseHeight, panelDepth * 1.05));
120
121
  parts.push(...createFenceCurveSpanParts(fence, 0, 1, baseY + effectiveBaseHeight + verticalHeight * 0.15, topRailHeight * 0.8, panelDepth * 0.35));
121
122
  }
122
- const count = Math.max(2, Math.floor((length - edgeInset * 2) / spacing) + 1);
123
+ const count = showInfill ? Math.max(2, Math.floor((length - edgeInset * 2) / spacing) + 1) : 2;
123
124
  const verticalY = baseY + effectiveBaseHeight + verticalHeight / 2;
124
125
  for (let index = 0; index < count; index += 1) {
125
126
  const t = count === 1 ? 0.5 : startInsetT + (endInsetT - startInsetT) * (index / (count - 1));
126
127
  const frame = getFencePointAt(fence, t);
127
128
  const isEdgePost = index === 0 || index === count - 1;
128
- const postHeight = isFloating && isEdgePost
129
+ const fullHeightPost = !showInfill || (isFloating && isEdgePost);
130
+ const postHeight = fullHeightPost
129
131
  ? effectiveBaseHeight + verticalHeight + topRailHeight + clearance
130
132
  : verticalHeight;
131
- const postY = isFloating && isEdgePost ? postHeight / 2 : verticalY;
133
+ const postY = fullHeightPost ? postHeight / 2 : verticalY;
132
134
  parts.push({
133
135
  position: [frame.point.x, postY, frame.point.y],
134
136
  rotationY: -frame.tangentAngle,
@@ -141,11 +143,13 @@ function createFenceParts(fence) {
141
143
  }
142
144
  return parts;
143
145
  }
144
- function generateFenceGeometry(fence) {
146
+ export function generateFenceGeometry(fence) {
145
147
  const parts = createFenceParts(fence);
146
148
  const geometries = parts.map(createFencePartGeometry);
147
149
  const merged = mergeGeometries(geometries, false) ?? new THREE.BufferGeometry();
148
- geometries.forEach((geometry) => geometry.dispose());
150
+ geometries.forEach((geometry) => {
151
+ geometry.dispose();
152
+ });
149
153
  const mergedUv = merged.getAttribute('uv');
150
154
  if (mergedUv) {
151
155
  merged.setAttribute('uv2', new THREE.Float32BufferAttribute(Array.from(mergedUv.array), 2));
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Generic floor-elevation system.
3
+ *
4
+ * Walks `dirtyNodes` and, for any kind that declares
5
+ * `capabilities.floorPlaced`, lifts the registered mesh's Y by whatever
6
+ * slab the footprint overlaps. Items / shelves / etc. that sit directly
7
+ * on a level pick this up automatically — no per-kind elevation logic.
8
+ *
9
+ * Skips nodes whose parent is not a level (items hosted on shelves /
10
+ * tables inherit Y from the parent group), and respects
11
+ * `floorPlaced.applies` so items with `asset.attachTo` (wall / ceiling
12
+ * mounted) are left alone.
13
+ *
14
+ * Runs at priority 1 — before the priority-2 systems (`GeometrySystem`,
15
+ * `ItemSystem`) so the dirty mark survives long enough for those to do
16
+ * their own work. Kinds with no geometry/system have no downstream dirty
17
+ * consumer, so this system clears their dirty mark after applying the lift.
18
+ */
19
+ export declare const FloorElevationSystem: () => null;
20
+ //# sourceMappingURL=floor-elevation-system.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"floor-elevation-system.d.ts","sourceRoot":"","sources":["../../../src/systems/floor-elevation/floor-elevation-system.tsx"],"names":[],"mappings":"AAwCA;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,oBAAoB,YA6ChC,CAAA"}
@@ -0,0 +1,78 @@
1
+ import { getEffectiveNode, getFloorStackedPosition, nodeRegistry, sceneRegistry, useLiveTransforms, useScene, } from '@pascal-app/core';
2
+ import { useFrame } from '@react-three/fiber';
3
+ function withLiveTransform(node, id) {
4
+ const liveTransform = useLiveTransforms.getState().get(id);
5
+ if (!liveTransform)
6
+ return node;
7
+ const currentRotation = node.rotation;
8
+ const rotation = Array.isArray(currentRotation)
9
+ ? [currentRotation[0] ?? 0, liveTransform.rotation, currentRotation[2] ?? 0]
10
+ : typeof currentRotation === 'number'
11
+ ? liveTransform.rotation
12
+ : currentRotation;
13
+ return {
14
+ ...node,
15
+ position: liveTransform.position,
16
+ ...(rotation !== undefined ? { rotation } : {}),
17
+ };
18
+ }
19
+ /**
20
+ * Generic floor-elevation system.
21
+ *
22
+ * Walks `dirtyNodes` and, for any kind that declares
23
+ * `capabilities.floorPlaced`, lifts the registered mesh's Y by whatever
24
+ * slab the footprint overlaps. Items / shelves / etc. that sit directly
25
+ * on a level pick this up automatically — no per-kind elevation logic.
26
+ *
27
+ * Skips nodes whose parent is not a level (items hosted on shelves /
28
+ * tables inherit Y from the parent group), and respects
29
+ * `floorPlaced.applies` so items with `asset.attachTo` (wall / ceiling
30
+ * mounted) are left alone.
31
+ *
32
+ * Runs at priority 1 — before the priority-2 systems (`GeometrySystem`,
33
+ * `ItemSystem`) so the dirty mark survives long enough for those to do
34
+ * their own work. Kinds with no geometry/system have no downstream dirty
35
+ * consumer, so this system clears their dirty mark after applying the lift.
36
+ */
37
+ export const FloorElevationSystem = () => {
38
+ const dirtyNodes = useScene((s) => s.dirtyNodes);
39
+ const clearDirty = useScene((s) => s.clearDirty);
40
+ useFrame(() => {
41
+ if (dirtyNodes.size === 0)
42
+ return;
43
+ const nodes = useScene.getState().nodes;
44
+ dirtyNodes.forEach((id) => {
45
+ const node = nodes[id];
46
+ if (!node)
47
+ return;
48
+ const def = nodeRegistry.get(node.type);
49
+ const floorPlaced = def?.capabilities?.floorPlaced;
50
+ if (!floorPlaced)
51
+ return;
52
+ const mesh = sceneRegistry.nodes.get(id);
53
+ if (!mesh)
54
+ return;
55
+ const effectiveNode = withLiveTransform(getEffectiveNode(node), id);
56
+ const position = effectiveNode.position;
57
+ if (!position)
58
+ return;
59
+ // This system is the single drag-time authority for floor-stack mesh Y:
60
+ // tools publish base positions to live stores, renderers may
61
+ // reconcile that base Y onto the group, then this presentation system
62
+ // reapplies the resolver-derived visual Y before render. Because the
63
+ // override/store position remains base-height, the slab lift is never
64
+ // committed or applied twice.
65
+ const resolverNodes = effectiveNode === node ? nodes : { ...nodes, [effectiveNode.id]: effectiveNode };
66
+ const visualPosition = getFloorStackedPosition({
67
+ node: effectiveNode,
68
+ nodes: resolverNodes,
69
+ position,
70
+ });
71
+ mesh.position.y = visualPosition[1];
72
+ if (!(def.geometry || def.system)) {
73
+ clearDirty(id);
74
+ }
75
+ });
76
+ }, 1);
77
+ return null;
78
+ };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Generic geometry system.
3
+ *
4
+ * For every node in `dirtyNodes` whose definition exposes `def.geometry`,
5
+ * this system:
6
+ * 1. Looks up the registered `Group` from `sceneRegistry` (mounted by the
7
+ * framework's `<ParametricNodeRenderer>`, or a custom renderer that
8
+ * opts into the same mount contract).
9
+ * 2. Builds a `GeometryContext` from the current scene snapshot.
10
+ * 3. Calls `def.geometry(node, ctx)` to get the new `Object3D`.
11
+ * 4. Disposes the registered group's existing children + their geometries
12
+ * and materials.
13
+ * 5. Reparents the returned object's children onto the registered group.
14
+ * 6. Clears the dirty flag.
15
+ *
16
+ * This is the "no per-kind system needed" path documented in
17
+ * `wiki/architecture/node-definitions.md`. A kind that only rebuilds on
18
+ * dirty (shelf, item, fence segment, etc.) ships nothing more than a pure
19
+ * `geometry` function — no `renderer.tsx`, no `system.tsx`.
20
+ *
21
+ * Kinds with `def.system` declared run their own systems *in addition* to
22
+ * this one — animation + cascade + named-mesh material poking stay
23
+ * kind-specific.
24
+ *
25
+ * Frame priority 2 mirrors the per-kind shelf system it replaces. Door
26
+ * animation systems run at priority 2 today too, marking dirty so the
27
+ * geometry rebuild lands at priority 3-4 next frame. Door/window/wall
28
+ * still have their own systems (they need cross-cutting work this system
29
+ * doesn't cover) — they coexist; this system only acts on kinds that
30
+ * declare `def.geometry`.
31
+ */
32
+ export declare const GeometrySystem: () => null;
33
+ export default GeometrySystem;
34
+ //# sourceMappingURL=geometry-system.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geometry-system.d.ts","sourceRoot":"","sources":["../../../src/systems/geometry/geometry-system.tsx"],"names":[],"mappings":"AAsBA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,eAAO,MAAM,cAAc,YAwK1B,CAAA;AAyHD,eAAe,cAAc,CAAA"}
@@ -0,0 +1,279 @@
1
+ 'use client';
2
+ import { getEffectiveNode, nodeRegistry, sceneRegistry, useScene, } from '@pascal-app/core';
3
+ import { useFrame } from '@react-three/fiber';
4
+ import { useEffect, useRef } from 'react';
5
+ import { FrontSide } from 'three';
6
+ import { createSurfaceRoleMaterial, } from '../../lib/materials';
7
+ import useViewer from '../../store/use-viewer';
8
+ /**
9
+ * Generic geometry system.
10
+ *
11
+ * For every node in `dirtyNodes` whose definition exposes `def.geometry`,
12
+ * this system:
13
+ * 1. Looks up the registered `Group` from `sceneRegistry` (mounted by the
14
+ * framework's `<ParametricNodeRenderer>`, or a custom renderer that
15
+ * opts into the same mount contract).
16
+ * 2. Builds a `GeometryContext` from the current scene snapshot.
17
+ * 3. Calls `def.geometry(node, ctx)` to get the new `Object3D`.
18
+ * 4. Disposes the registered group's existing children + their geometries
19
+ * and materials.
20
+ * 5. Reparents the returned object's children onto the registered group.
21
+ * 6. Clears the dirty flag.
22
+ *
23
+ * This is the "no per-kind system needed" path documented in
24
+ * `wiki/architecture/node-definitions.md`. A kind that only rebuilds on
25
+ * dirty (shelf, item, fence segment, etc.) ships nothing more than a pure
26
+ * `geometry` function — no `renderer.tsx`, no `system.tsx`.
27
+ *
28
+ * Kinds with `def.system` declared run their own systems *in addition* to
29
+ * this one — animation + cascade + named-mesh material poking stay
30
+ * kind-specific.
31
+ *
32
+ * Frame priority 2 mirrors the per-kind shelf system it replaces. Door
33
+ * animation systems run at priority 2 today too, marking dirty so the
34
+ * geometry rebuild lands at priority 3-4 next frame. Door/window/wall
35
+ * still have their own systems (they need cross-cutting work this system
36
+ * doesn't cover) — they coexist; this system only acts on kinds that
37
+ * declare `def.geometry`.
38
+ */
39
+ export const GeometrySystem = () => {
40
+ const dirtyNodes = useScene((s) => s.dirtyNodes);
41
+ const clearDirty = useScene((s) => s.clearDirty);
42
+ const shading = useViewer((s) => s.shading);
43
+ const textures = useViewer((s) => s.textures);
44
+ const colorPreset = useViewer((s) => s.colorPreset);
45
+ const sceneTheme = useViewer((s) => s.sceneTheme);
46
+ // Per-node cache of the last-built geometry key (for kinds that declare
47
+ // `def.geometryKey`). Lets us skip a dispose+rebuild when a node is dirty
48
+ // but its geometry inputs are unchanged — e.g. an item reparenting onto a
49
+ // shelf dirties the shelf without altering its boards.
50
+ const builtGeometryKeyRef = useRef(new Map());
51
+ useEffect(() => {
52
+ const nodes = useScene.getState().nodes;
53
+ for (const node of Object.values(nodes)) {
54
+ const def = nodeRegistry.get(node.type);
55
+ if (def?.geometry) {
56
+ useScene.getState().markDirty(node.id);
57
+ }
58
+ }
59
+ }, [shading, textures, colorPreset, sceneTheme]);
60
+ useFrame(() => {
61
+ if (dirtyNodes.size === 0)
62
+ return;
63
+ const nodes = useScene.getState().nodes;
64
+ const batches = new Map();
65
+ const dirtyIds = [];
66
+ dirtyNodes.forEach((id) => {
67
+ const node = nodes[id];
68
+ if (!node)
69
+ return;
70
+ const def = nodeRegistry.get(node.type);
71
+ if (!def?.geometry)
72
+ return;
73
+ dirtyIds.push(id);
74
+ const parentId = (node.parentId ?? null);
75
+ const key = `${node.type}::${parentId ?? ''}`;
76
+ const existing = batches.get(key);
77
+ if (existing)
78
+ existing.ids.push(id);
79
+ else
80
+ batches.set(key, { kind: node.type, parentId, ids: [id] });
81
+ });
82
+ // Phase 2 — for each batch whose kind declares `computeLevelData`,
83
+ // collect every sibling in the level + run the precompute once.
84
+ const levelDataByBatch = new Map();
85
+ for (const [key, batch] of batches) {
86
+ const def = nodeRegistry.get(batch.kind);
87
+ if (!def?.computeLevelData)
88
+ continue;
89
+ const siblings = [];
90
+ if (batch.parentId) {
91
+ const parent = nodes[batch.parentId];
92
+ const childIds = parent?.children;
93
+ if (Array.isArray(childIds)) {
94
+ for (const cid of childIds) {
95
+ const child = nodes[cid];
96
+ if (child?.type === batch.kind)
97
+ siblings.push(child);
98
+ }
99
+ }
100
+ }
101
+ else {
102
+ for (const node of Object.values(nodes)) {
103
+ if (node?.type === batch.kind && !node.parentId)
104
+ siblings.push(node);
105
+ }
106
+ }
107
+ levelDataByBatch.set(key, def.computeLevelData(siblings));
108
+ }
109
+ // Phase 3 — per-node rebuild. Each node receives its batch's
110
+ // precomputed `levelData` in ctx.
111
+ for (const id of dirtyIds) {
112
+ const node = nodes[id];
113
+ if (!node)
114
+ continue;
115
+ const def = nodeRegistry.get(node.type);
116
+ const builder = def?.geometry;
117
+ if (!builder)
118
+ continue;
119
+ const group = sceneRegistry.nodes.get(id);
120
+ if (!group)
121
+ continue; // mount hasn't run — keep dirty for next frame
122
+ // Merge any live drag override into the node so resize arrows /
123
+ // 2D affordances see their in-flight patch on every pointer move
124
+ // — zustand only learns about the final value on commit. Mirrors
125
+ // the WallSystem / DoorSystem / WindowSystem hook-up; any kind
126
+ // that drives its mesh through `def.geometry` (fence, shelf, item)
127
+ // now smooths drags through this single line.
128
+ const effectiveNode = getEffectiveNode(node);
129
+ // Skip the rebuild when the geometry inputs are unchanged (kinds that
130
+ // opt in via `def.geometryKey`). Fold in the global rendering inputs so
131
+ // a theme / shading change — which re-dirties every geometry node — is
132
+ // never skipped. This kills the board remount + pointer enter/leave
133
+ // churn when an item reparents onto a shelf.
134
+ if (def.geometryKey) {
135
+ const builtKey = `${shading}|${textures}|${colorPreset}|${sceneTheme}|${def.geometryKey(effectiveNode)}`;
136
+ if (builtGeometryKeyRef.current.get(id) === builtKey) {
137
+ clearDirty(id);
138
+ continue;
139
+ }
140
+ builtGeometryKeyRef.current.set(id, builtKey);
141
+ }
142
+ const parentId = (node.parentId ?? null);
143
+ const key = `${node.type}::${parentId ?? ''}`;
144
+ const levelData = levelDataByBatch.get(key);
145
+ const ctx = buildGeometryContext(effectiveNode, nodes, levelData);
146
+ // The builder is typed against the kind's specific node — at the
147
+ // generic system level we lose that refinement, so the cast lands
148
+ // here. Builders are responsible for trusting their schema.
149
+ const built = builder(effectiveNode, ctx, shading, textures, colorPreset, sceneTheme);
150
+ if (!textures && def.surfaceRole) {
151
+ applyDefaultSurfaceRole(built, def.surfaceRole, colorPreset, sceneTheme);
152
+ }
153
+ disposeChildren(group);
154
+ for (const child of [...built.children]) {
155
+ // Tag every child the builder produced so a subsequent rebuild
156
+ // can dispose only THIS rebuild's outputs and leave React-
157
+ // mounted siblings (hosted items inside a shelf / slab / etc.)
158
+ // alone. Without this, a parent rebuild triggered by a child
159
+ // event (e.g. an item reparenting onto a shelf calls
160
+ // `dirtyNodes.add(parent)` in `ItemRenderer`'s effect) would
161
+ // wipe ALL of the parent group's children — including the
162
+ // freshly-mounted item — leaving the item in scene state but
163
+ // invisible.
164
+ ;
165
+ child.userData = {
166
+ ...child.userData,
167
+ __fromGeometry: true,
168
+ };
169
+ group.add(child);
170
+ }
171
+ // NOTE: we intentionally do NOT reset `group.position` / `group.rotation`
172
+ // here. The `ParametricNodeRenderer` binds them via JSX (`position={...}`
173
+ // / `rotation={...}`) driven by `useLiveTransforms` during drag and
174
+ // `node.position` / `node.rotation` after commit. Zeroing them out
175
+ // during a rebuild would clobber the React-applied transform — and
176
+ // because the renderer doesn't necessarily re-render on the rebuild
177
+ // tick, R3F wouldn't re-apply the props, leaving the group stuck at
178
+ // origin. Geometry builders are expected to emit local-space children.
179
+ clearDirty(id);
180
+ }
181
+ }, 2);
182
+ return null;
183
+ };
184
+ function buildGeometryContext(node, nodes, levelData) {
185
+ const resolve = (id) => nodes[id];
186
+ const childIds = node.children;
187
+ const children = Array.isArray(childIds)
188
+ ? childIds.map((cid) => nodes[cid]).filter((n) => n !== undefined)
189
+ : [];
190
+ const parentId = node.parentId;
191
+ const parent = parentId ? (nodes[parentId] ?? null) : null;
192
+ // Siblings = same kind, same parent, excluding self. Walks the parent's
193
+ // children array; falls back to scanning the whole scene if the parent
194
+ // doesn't carry a `children` list (rare — most parents do).
195
+ let siblings = [];
196
+ if (parent) {
197
+ const parentChildIds = parent.children;
198
+ if (Array.isArray(parentChildIds)) {
199
+ for (const sid of parentChildIds) {
200
+ if (sid === node.id)
201
+ continue;
202
+ const s = nodes[sid];
203
+ if (s && s.type === node.type)
204
+ siblings.push(s);
205
+ }
206
+ }
207
+ else {
208
+ siblings = Object.values(nodes).filter((n) => n !== node && n.type === node.type && n.parentId === parentId);
209
+ }
210
+ }
211
+ return { resolve, children, siblings, parent, levelData };
212
+ }
213
+ function disposeChildren(group) {
214
+ // Only dispose meshes the geometry builder produced on the previous
215
+ // rebuild (marked via `userData.__fromGeometry`). React-managed
216
+ // children (hosted node renderers) get left in place — they have
217
+ // their own React-driven lifecycle and would lose their meshes /
218
+ // materials if we disposed them here.
219
+ for (const child of [...group.children]) {
220
+ const fromGeometry = child.userData
221
+ ?.__fromGeometry;
222
+ if (!fromGeometry)
223
+ continue;
224
+ group.remove(child);
225
+ const mesh = child;
226
+ if (mesh.geometry?.dispose)
227
+ mesh.geometry.dispose();
228
+ if ('material' in mesh) {
229
+ const m = mesh.material;
230
+ if (Array.isArray(m)) {
231
+ for (const mat of m) {
232
+ if (isCachedMaterial(mat))
233
+ continue;
234
+ if (mat && typeof mat.dispose === 'function') {
235
+ ;
236
+ mat.dispose();
237
+ }
238
+ }
239
+ }
240
+ else if (isCachedMaterial(m)) {
241
+ }
242
+ else if (m && typeof m.dispose === 'function') {
243
+ ;
244
+ m.dispose();
245
+ }
246
+ }
247
+ }
248
+ }
249
+ function applyDefaultSurfaceRole(root, defaultRole, colorPreset, sceneTheme) {
250
+ root.traverse((child) => {
251
+ const mesh = child;
252
+ if (!('material' in mesh) || !mesh.material)
253
+ return;
254
+ const role = getMeshSurfaceRole(mesh.userData.surfaceRole, defaultRole);
255
+ mesh.userData.surfaceRole = role;
256
+ mesh.material = createSurfaceRoleMaterial(role, colorPreset, getMaterialSide(mesh.material), sceneTheme);
257
+ });
258
+ }
259
+ function getMeshSurfaceRole(value, fallback) {
260
+ return typeof value === 'string' && isSurfaceRole(value) ? value : fallback;
261
+ }
262
+ function isSurfaceRole(value) {
263
+ return (value === 'wall' ||
264
+ value === 'floor' ||
265
+ value === 'ceiling' ||
266
+ value === 'roof' ||
267
+ value === 'joinery' ||
268
+ value === 'glazing' ||
269
+ value === 'furnishing');
270
+ }
271
+ function getMaterialSide(material) {
272
+ const source = Array.isArray(material) ? material[0] : material;
273
+ return source?.side ?? FrontSide;
274
+ }
275
+ function isCachedMaterial(value) {
276
+ return Boolean(value?.userData
277
+ ?.__pascalCachedMaterial);
278
+ }
279
+ export default GeometrySystem;
@@ -1 +1 @@
1
- {"version":3,"file":"guide-system.d.ts","sourceRoot":"","sources":["../../../src/systems/guide/guide-system.tsx"],"names":[],"mappings":"AAIA,eAAO,MAAM,WAAW,YAsCvB,CAAA"}
1
+ {"version":3,"file":"guide-system.d.ts","sourceRoot":"","sources":["../../../src/systems/guide/guide-system.tsx"],"names":[],"mappings":"AAIA,eAAO,MAAM,WAAW,YAcvB,CAAA"}
@@ -1,4 +1,4 @@
1
- import { emitter, sceneRegistry } from '@pascal-app/core';
1
+ import { sceneRegistry } from '@pascal-app/core';
2
2
  import { useEffect } from 'react';
3
3
  import useViewer from '../../store/use-viewer';
4
4
  export const GuideSystem = () => {
@@ -12,30 +12,5 @@ export const GuideSystem = () => {
12
12
  }
13
13
  });
14
14
  }, [showGuides]);
15
- useEffect(() => {
16
- const hideForCapture = () => {
17
- const guides = sceneRegistry.byType.guide || new Set();
18
- guides.forEach((guideId) => {
19
- const node = sceneRegistry.nodes.get(guideId);
20
- if (node)
21
- node.visible = false;
22
- });
23
- };
24
- const restoreAfterCapture = () => {
25
- const showGuidesNow = useViewer.getState().showGuides;
26
- const guides = sceneRegistry.byType.guide || new Set();
27
- guides.forEach((guideId) => {
28
- const node = sceneRegistry.nodes.get(guideId);
29
- if (node)
30
- node.visible = showGuidesNow;
31
- });
32
- };
33
- emitter.on('thumbnail:before-capture', hideForCapture);
34
- emitter.on('thumbnail:after-capture', restoreAfterCapture);
35
- return () => {
36
- emitter.off('thumbnail:before-capture', hideForCapture);
37
- emitter.off('thumbnail:after-capture', restoreAfterCapture);
38
- };
39
- }, []);
40
15
  return null;
41
16
  };
@@ -1,2 +1,2 @@
1
- export declare const InteractiveSystem: () => import("react/jsx-runtime").JSX.Element;
1
+ export declare const InteractiveSystem: () => import("react").JSX.Element;
2
2
  //# sourceMappingURL=interactive-system.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"interactive-system.d.ts","sourceRoot":"","sources":["../../../src/systems/interactive/interactive-system.tsx"],"names":[],"mappings":"AAwBA,eAAO,MAAM,iBAAiB,+CAgB7B,CAAA"}
1
+ {"version":3,"file":"interactive-system.d.ts","sourceRoot":"","sources":["../../../src/systems/interactive/interactive-system.tsx"],"names":[],"mappings":"AAwBA,eAAO,MAAM,iBAAiB,mCAgB7B,CAAA"}
@@ -1,2 +1,9 @@
1
+ /**
2
+ * Per-frame wall-side offset for items mounted to wall faces. The slab-
3
+ * elevation lift for floor items lives in the generic
4
+ * `<FloorElevationSystem>` and runs at priority 1 — it has already
5
+ * landed `mesh.position.y` by the time this system clears the dirty
6
+ * mark at priority 2.
7
+ */
1
8
  export declare const ItemSystem: () => null;
2
9
  //# sourceMappingURL=item-system.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"item-system.d.ts","sourceRoot":"","sources":["../../../src/systems/item/item-system.tsx"],"names":[],"mappings":"AAiBA,eAAO,MAAM,UAAU,YA6CtB,CAAA"}
1
+ {"version":3,"file":"item-system.d.ts","sourceRoot":"","sources":["../../../src/systems/item/item-system.tsx"],"names":[],"mappings":"AAcA;;;;;;GAMG;AACH,eAAO,MAAM,UAAU,YA+BtB,CAAA"}
@@ -1,8 +1,15 @@
1
+ import { sceneRegistry, useScene, } from '@pascal-app/core';
1
2
  import { useFrame } from '@react-three/fiber';
2
- import { getScaledDimensions, resolveLevelId, sceneRegistry, spatialGridManager, useScene, } from '@pascal-app/core';
3
3
  // ============================================================================
4
4
  // ITEM SYSTEM
5
5
  // ============================================================================
6
+ /**
7
+ * Per-frame wall-side offset for items mounted to wall faces. The slab-
8
+ * elevation lift for floor items lives in the generic
9
+ * `<FloorElevationSystem>` and runs at priority 1 — it has already
10
+ * landed `mesh.position.y` by the time this system clears the dirty
11
+ * mark at priority 2.
12
+ */
6
13
  export const ItemSystem = () => {
7
14
  const dirtyNodes = useScene((state) => state.dirtyNodes);
8
15
  const clearDirty = useScene((state) => state.clearDirty);
@@ -27,16 +34,6 @@ export const ItemSystem = () => {
27
34
  mesh.position.z = (wallThickness / 2) * side;
28
35
  }
29
36
  }
30
- else if (!item.asset.attachTo) {
31
- // If parented to another item (surface placement), R3F handles positioning via the hierarchy
32
- const parentNode = item.parentId ? nodes[item.parentId] : undefined;
33
- if (parentNode?.type !== 'item') {
34
- // Floor item: elevate by slab height (using full footprint overlap)
35
- const levelId = resolveLevelId(item, nodes);
36
- const slabElevation = spatialGridManager.getSlabElevationForItem(levelId, item.position, getScaledDimensions(item), item.rotation);
37
- mesh.position.y = slabElevation + item.position[1];
38
- }
39
- }
40
37
  clearDirty(id);
41
38
  });
42
39
  }, 2);
@@ -1,2 +1,2 @@
1
- export declare function ItemLightSystem(): import("react/jsx-runtime").JSX.Element;
1
+ export declare function ItemLightSystem(): import("react").JSX.Element;
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":"AAwFA,wBAAgB,eAAe,4CA+M9B"}
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,5 +1,5 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { sceneRegistry, useInteractive, useScene } from '@pascal-app/core';
2
+ import { findLevelAncestorId, sceneRegistry, useInteractive, useScene } from '@pascal-app/core';
3
3
  import { useFrame } from '@react-three/fiber';
4
4
  import { useRef } from 'react';
5
5
  import { MathUtils, Vector3 } from 'three';
@@ -42,8 +42,7 @@ function scoreRegistration(reg, nodes, selectedLevelId, levelMode, interactiveSt
42
42
  // Normalised distance component (assumes scenes < 200 units)
43
43
  const dist = _camPos.distanceTo(_itemPos) / 200;
44
44
  // ── Level factor ──────────────────────────────────────────────────────────
45
- const node = nodes[nodeId];
46
- const itemLevelId = node?.parentId ?? null;
45
+ const itemLevelId = findLevelAncestorId(nodeId, nodes);
47
46
  let levelPenalty = 0;
48
47
  if (selectedLevelId) {
49
48
  if (itemLevelId !== selectedLevelId) {
@@ -97,7 +96,10 @@ export function ItemLightSystem() {
97
96
  }
98
97
  scored.sort((a, b) => a.score - b.score);
99
98
  // Build the desired assignment (top POOL_SIZE keys)
100
- const desired = scored.slice(0, POOL_SIZE).map((s) => s.key);
99
+ const desired = scored
100
+ .filter((s) => Number.isFinite(s.score))
101
+ .slice(0, POOL_SIZE)
102
+ .map((s) => s.key);
101
103
  // Build a map of currently-assigned keys → slot index for hysteresis
102
104
  const currentlyAssigned = new Map();
103
105
  for (let i = 0; i < POOL_SIZE; i++) {
@@ -181,9 +183,11 @@ export function ItemLightSystem() {
181
183
  continue;
182
184
  // Fade-out phase: lerp intensity → 0, then complete the transition
183
185
  if (slot.isFadingOut) {
186
+ light.visible = true;
184
187
  light.intensity = MathUtils.lerp(light.intensity, 0, dt * 12);
185
188
  if (light.intensity < 0.01) {
186
189
  light.intensity = 0;
190
+ light.visible = false;
187
191
  slot.isFadingOut = false;
188
192
  slot.key = slot.pendingKey;
189
193
  slot.pendingKey = null;
@@ -200,12 +204,14 @@ export function ItemLightSystem() {
200
204
  if (!slot.key) {
201
205
  // Idle slot — keep dark
202
206
  light.intensity = 0;
207
+ light.visible = false;
203
208
  continue;
204
209
  }
205
210
  const reg = registrations.get(slot.key);
206
211
  if (!reg) {
207
212
  slot.key = null;
208
213
  light.intensity = 0;
214
+ light.visible = false;
209
215
  continue;
210
216
  }
211
217
  // Snap world position each frame
@@ -226,10 +232,17 @@ export function ItemLightSystem() {
226
232
  const targetIntensity = isOn
227
233
  ? MathUtils.lerp(reg.effect.intensityRange[0], reg.effect.intensityRange[1], t)
228
234
  : reg.effect.intensityRange[0];
235
+ if (targetIntensity > 0) {
236
+ light.visible = true;
237
+ }
229
238
  light.intensity = MathUtils.lerp(light.intensity, targetIntensity, dt * 12);
239
+ if (targetIntensity <= 0 && light.intensity < 0.01) {
240
+ light.intensity = 0;
241
+ light.visible = false;
242
+ }
230
243
  }
231
244
  });
232
- return (_jsx(_Fragment, { children: Array.from({ length: POOL_SIZE }, (_, i) => (_jsx("pointLight", { castShadow: false, intensity: 0, ref: (el) => {
245
+ return (_jsx(_Fragment, { children: Array.from({ length: POOL_SIZE }, (_, i) => (_jsx("pointLight", { castShadow: false, intensity: 0, visible: false, ref: (el) => {
233
246
  lightRefs.current[i] = el;
234
247
  } }, i))) }));
235
248
  }