@pascal-app/viewer 1.0.0-beta.5 → 1.0.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 (208) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +67 -0
  3. package/dist/capture/asset-url.d.ts +2 -0
  4. package/dist/capture/asset-url.d.ts.map +1 -0
  5. package/dist/capture/asset-url.js +13 -0
  6. package/dist/capture/capture-runtime.d.ts +43 -0
  7. package/dist/capture/capture-runtime.d.ts.map +1 -0
  8. package/dist/capture/capture-runtime.js +198 -0
  9. package/dist/capture/frame.d.ts +4 -0
  10. package/dist/capture/frame.d.ts.map +1 -0
  11. package/dist/capture/frame.js +21 -0
  12. package/dist/capture/index.d.ts +12 -0
  13. package/dist/capture/index.d.ts.map +1 -0
  14. package/dist/capture/index.js +11 -0
  15. package/dist/capture/layer-visibility.d.ts +5 -0
  16. package/dist/capture/layer-visibility.d.ts.map +1 -0
  17. package/dist/capture/layer-visibility.js +11 -0
  18. package/dist/capture/layers/clay-matcap.d.ts +3 -0
  19. package/dist/capture/layers/clay-matcap.d.ts.map +1 -0
  20. package/dist/capture/layers/clay-matcap.js +26 -0
  21. package/dist/capture/layers/device-motion-layer.d.ts +7 -0
  22. package/dist/capture/layers/device-motion-layer.d.ts.map +1 -0
  23. package/dist/capture/layers/device-motion-layer.js +76 -0
  24. package/dist/capture/layers/point-cloud-layer.d.ts +15 -0
  25. package/dist/capture/layers/point-cloud-layer.d.ts.map +1 -0
  26. package/dist/capture/layers/point-cloud-layer.js +106 -0
  27. package/dist/capture/layers/room-model-layer.d.ts +9 -0
  28. package/dist/capture/layers/room-model-layer.d.ts.map +1 -0
  29. package/dist/capture/layers/room-model-layer.js +71 -0
  30. package/dist/capture/layers/surface-mesh-data.d.ts +9 -0
  31. package/dist/capture/layers/surface-mesh-data.d.ts.map +1 -0
  32. package/dist/capture/layers/surface-mesh-data.js +74 -0
  33. package/dist/capture/layers/surface-mesh-layer.d.ts +7 -0
  34. package/dist/capture/layers/surface-mesh-layer.d.ts.map +1 -0
  35. package/dist/capture/layers/surface-mesh-layer.js +16 -0
  36. package/dist/capture/preview.d.ts +3 -0
  37. package/dist/capture/preview.d.ts.map +1 -0
  38. package/dist/capture/preview.js +2 -0
  39. package/dist/capture/source-state.d.ts +22 -0
  40. package/dist/capture/source-state.d.ts.map +1 -0
  41. package/dist/capture/source-state.js +182 -0
  42. package/dist/capture/stream-rendering.d.ts +14 -0
  43. package/dist/capture/stream-rendering.d.ts.map +1 -0
  44. package/dist/capture/stream-rendering.js +70 -0
  45. package/dist/capture/trajectory.d.ts +20 -0
  46. package/dist/capture/trajectory.d.ts.map +1 -0
  47. package/dist/capture/trajectory.js +77 -0
  48. package/dist/capture/use-json-artifact.d.ts +2 -0
  49. package/dist/capture/use-json-artifact.d.ts.map +1 -0
  50. package/dist/capture/use-json-artifact.js +42 -0
  51. package/dist/components/viewer/batched-mesh-spike.d.ts +14 -0
  52. package/dist/components/viewer/batched-mesh-spike.d.ts.map +1 -0
  53. package/dist/components/viewer/batched-mesh-spike.js +64 -0
  54. package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
  55. package/dist/components/viewer/frame-limiter.js +4 -3
  56. package/dist/components/viewer/glb-scene.d.ts.map +1 -1
  57. package/dist/components/viewer/glb-scene.js +3 -1
  58. package/dist/components/viewer/index.d.ts.map +1 -1
  59. package/dist/components/viewer/index.js +70 -62
  60. package/dist/components/viewer/lights.d.ts.map +1 -1
  61. package/dist/components/viewer/lights.js +123 -89
  62. package/dist/components/viewer/perf-monitor.d.ts +6 -1
  63. package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
  64. package/dist/components/viewer/perf-monitor.js +307 -116
  65. package/dist/components/viewer/perf-panel.d.ts +3 -0
  66. package/dist/components/viewer/perf-panel.d.ts.map +1 -0
  67. package/dist/components/viewer/perf-panel.js +178 -0
  68. package/dist/components/viewer/pointer-raycast-layers.d.ts +15 -0
  69. package/dist/components/viewer/pointer-raycast-layers.d.ts.map +1 -0
  70. package/dist/components/viewer/pointer-raycast-layers.js +28 -0
  71. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  72. package/dist/components/viewer/post-processing.js +130 -83
  73. package/dist/components/viewer/registered-systems.d.ts +14 -5
  74. package/dist/components/viewer/registered-systems.d.ts.map +1 -1
  75. package/dist/components/viewer/registered-systems.js +22 -10
  76. package/dist/components/viewer/scene-atmosphere.d.ts +30 -0
  77. package/dist/components/viewer/scene-atmosphere.d.ts.map +1 -0
  78. package/dist/components/viewer/scene-atmosphere.js +89 -0
  79. package/dist/components/viewer/scene-environment.d.ts.map +1 -1
  80. package/dist/components/viewer/scene-environment.js +19 -5
  81. package/dist/components/viewer/scene-ground-replacement.d.ts +5 -0
  82. package/dist/components/viewer/scene-ground-replacement.d.ts.map +1 -0
  83. package/dist/components/viewer/scene-ground-replacement.js +35 -0
  84. package/dist/components/viewer/selection-manager.d.ts.map +1 -1
  85. package/dist/components/viewer/selection-manager.js +8 -16
  86. package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
  87. package/dist/components/viewer/viewer-camera.js +4 -1
  88. package/dist/components/viewer/viewer-presentations.d.ts +67 -0
  89. package/dist/components/viewer/viewer-presentations.d.ts.map +1 -0
  90. package/dist/components/viewer/viewer-presentations.js +120 -0
  91. package/dist/hooks/use-library-materials-version.d.ts +7 -0
  92. package/dist/hooks/use-library-materials-version.d.ts.map +1 -0
  93. package/dist/hooks/use-library-materials-version.js +10 -0
  94. package/dist/hooks/use-node-events.d.ts.map +1 -1
  95. package/dist/hooks/use-node-events.js +1 -0
  96. package/dist/index.d.ts +16 -7
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +16 -7
  99. package/dist/lib/csg-utils.d.ts +1 -0
  100. package/dist/lib/csg-utils.d.ts.map +1 -1
  101. package/dist/lib/csg-utils.js +6 -1
  102. package/dist/lib/gpu-perf.d.ts +0 -2
  103. package/dist/lib/gpu-perf.d.ts.map +1 -1
  104. package/dist/lib/gpu-perf.js +7 -21
  105. package/dist/lib/isolation.d.ts +14 -30
  106. package/dist/lib/isolation.d.ts.map +1 -1
  107. package/dist/lib/isolation.js +62 -66
  108. package/dist/lib/layer-pass.d.ts +28 -0
  109. package/dist/lib/layer-pass.d.ts.map +1 -0
  110. package/dist/lib/layer-pass.js +254 -0
  111. package/dist/lib/materials.d.ts +12 -0
  112. package/dist/lib/materials.d.ts.map +1 -1
  113. package/dist/lib/materials.js +52 -17
  114. package/dist/lib/merged-outline-node.d.ts +17 -8
  115. package/dist/lib/merged-outline-node.d.ts.map +1 -1
  116. package/dist/lib/merged-outline-node.js +194 -52
  117. package/dist/lib/perf-actions.d.ts +55 -0
  118. package/dist/lib/perf-actions.d.ts.map +1 -0
  119. package/dist/lib/perf-actions.js +211 -0
  120. package/dist/lib/perf-observers.d.ts +2 -0
  121. package/dist/lib/perf-observers.d.ts.map +1 -0
  122. package/dist/lib/perf-observers.js +17 -0
  123. package/dist/lib/perf-panel-store.d.ts +71 -0
  124. package/dist/lib/perf-panel-store.d.ts.map +1 -0
  125. package/dist/lib/perf-panel-store.js +29 -0
  126. package/dist/lib/perf-tracks.d.ts +43 -0
  127. package/dist/lib/perf-tracks.d.ts.map +1 -0
  128. package/dist/lib/perf-tracks.js +113 -0
  129. package/dist/lib/pointer-events.d.ts +7 -0
  130. package/dist/lib/pointer-events.d.ts.map +1 -0
  131. package/dist/lib/pointer-events.js +680 -0
  132. package/dist/lib/post-processing-resources.d.ts +12 -0
  133. package/dist/lib/post-processing-resources.d.ts.map +1 -0
  134. package/dist/lib/post-processing-resources.js +19 -0
  135. package/dist/lib/renderer-capability.d.ts.map +1 -1
  136. package/dist/lib/renderer-capability.js +4 -2
  137. package/dist/lib/scene-visibility.d.ts +2 -1
  138. package/dist/lib/scene-visibility.d.ts.map +1 -1
  139. package/dist/lib/scene-visibility.js +25 -2
  140. package/dist/lib/snapshot-pipeline.d.ts +3 -1
  141. package/dist/lib/snapshot-pipeline.d.ts.map +1 -1
  142. package/dist/lib/snapshot-pipeline.js +32 -16
  143. package/dist/store/use-viewer.d.ts +5 -3
  144. package/dist/store/use-viewer.d.ts.map +1 -1
  145. package/dist/store/use-viewer.js +2 -0
  146. package/dist/systems/ceiling/ceiling-system.js +1 -1
  147. package/dist/systems/door/door-animation-system.d.ts.map +1 -1
  148. package/dist/systems/door/door-animation-system.js +6 -9
  149. package/dist/systems/door/door-system.d.ts.map +1 -1
  150. package/dist/systems/door/door-system.js +13 -2
  151. package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -1
  152. package/dist/systems/floor-elevation/floor-elevation-system.js +7 -3
  153. package/dist/systems/geometry/geometry-system.d.ts.map +1 -1
  154. package/dist/systems/geometry/geometry-system.js +7 -2
  155. package/dist/systems/level/level-system.d.ts.map +1 -1
  156. package/dist/systems/level/level-system.js +1 -1
  157. package/dist/systems/level/level-utils.d.ts +9 -0
  158. package/dist/systems/level/level-utils.d.ts.map +1 -1
  159. package/dist/systems/level/level-utils.js +14 -0
  160. package/dist/systems/perf-action-settle/perf-action-settle-system.d.ts +8 -0
  161. package/dist/systems/perf-action-settle/perf-action-settle-system.d.ts.map +1 -0
  162. package/dist/systems/perf-action-settle/perf-action-settle-system.js +32 -0
  163. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  164. package/dist/systems/roof/roof-system.js +1196 -58
  165. package/dist/systems/scan/scan-system.js +6 -5
  166. package/dist/systems/slab/slab-system.d.ts.map +1 -1
  167. package/dist/systems/slab/slab-system.js +1 -2
  168. package/dist/systems/stair/stair-system.d.ts.map +1 -1
  169. package/dist/systems/stair/stair-system.js +9 -8
  170. package/dist/systems/surface-hole-geometry.d.ts +1 -1
  171. package/dist/systems/surface-hole-geometry.d.ts.map +1 -1
  172. package/dist/systems/surface-hole-geometry.js +1 -1
  173. package/dist/systems/wall/level-miter-cache.d.ts.map +1 -1
  174. package/dist/systems/wall/wall-build-lifecycle.d.ts +9 -0
  175. package/dist/systems/wall/wall-build-lifecycle.d.ts.map +1 -0
  176. package/dist/systems/wall/wall-build-lifecycle.js +74 -0
  177. package/dist/systems/wall/wall-cutout-cache.d.ts +57 -0
  178. package/dist/systems/wall/wall-cutout-cache.d.ts.map +1 -0
  179. package/dist/systems/wall/wall-cutout-cache.js +304 -0
  180. package/dist/systems/wall/wall-cutout.d.ts +12 -1
  181. package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
  182. package/dist/systems/wall/wall-cutout.js +24 -161
  183. package/dist/systems/wall/wall-material-variant.d.ts +31 -0
  184. package/dist/systems/wall/wall-material-variant.d.ts.map +1 -0
  185. package/dist/systems/wall/wall-material-variant.js +10 -0
  186. package/dist/systems/wall/wall-materials.d.ts +6 -0
  187. package/dist/systems/wall/wall-materials.d.ts.map +1 -1
  188. package/dist/systems/wall/wall-materials.js +29 -13
  189. package/dist/systems/wall/wall-placeholder-sweep.d.ts +50 -0
  190. package/dist/systems/wall/wall-placeholder-sweep.d.ts.map +1 -0
  191. package/dist/systems/wall/wall-placeholder-sweep.js +51 -0
  192. package/dist/systems/wall/wall-rebuild-notifications.d.ts +5 -0
  193. package/dist/systems/wall/wall-rebuild-notifications.d.ts.map +1 -0
  194. package/dist/systems/wall/wall-rebuild-notifications.js +23 -0
  195. package/dist/systems/wall/wall-system.d.ts +11 -4
  196. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  197. package/dist/systems/wall/wall-system.js +405 -153
  198. package/dist/systems/window/window-animation-system.d.ts.map +1 -1
  199. package/dist/systems/window/window-animation-system.js +9 -8
  200. package/dist/systems/window/window-system.d.ts +1 -0
  201. package/dist/systems/window/window-system.d.ts.map +1 -1
  202. package/dist/systems/window/window-system.js +16 -2
  203. package/dist/systems/zone/zone-system.d.ts.map +1 -1
  204. package/dist/systems/zone/zone-system.js +28 -2
  205. package/package.json +17 -4
  206. package/dist/lib/polygon-union.d.ts +0 -4
  207. package/dist/lib/polygon-union.d.ts.map +0 -1
  208. package/dist/lib/polygon-union.js +0 -289
@@ -0,0 +1,28 @@
1
+ 'use client';
2
+ import { useThree } from '@react-three/fiber';
3
+ import { useLayoutEffect } from 'react';
4
+ import { BATCHED_LAYER } from '../../lib/layers';
5
+ /**
6
+ * Lets R3F's pointer raycaster see geometry a collective batch draws.
7
+ *
8
+ * R3F picks with one shared raycaster whose default mask is `SCENE_LAYER`
9
+ * alone. A wall sewn into its level's merged mesh is moved off that layer
10
+ * (`hideBatchedWall`) while staying in the graph with its pointer handlers
11
+ * intact — so without this the wall answers no hover, paint or click the
12
+ * moment it joins a batch, and a floor's walls go dead a fraction of a second
13
+ * after the last edit settles.
14
+ *
15
+ * Additive rather than `setSurfaceRaycastLayers`: that helper resets the mask
16
+ * for the private raycasters callers build per query, and this one is shared.
17
+ */
18
+ export const PointerRaycastLayers = () => {
19
+ const raycaster = useThree((state) => state.raycaster);
20
+ useLayoutEffect(() => {
21
+ const mask = raycaster.layers.mask;
22
+ raycaster.layers.enable(BATCHED_LAYER);
23
+ return () => {
24
+ raycaster.layers.mask = mask;
25
+ };
26
+ }, [raycaster]);
27
+ return null;
28
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"post-processing.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/post-processing.tsx"],"names":[],"mappings":"AAwCA,eAAO,MAAM,YAAY;;;CAGxB,CAAA;AAGD,eAAO,MAAM,WAAW;;;;;;;;;;;;;CAavB,CAAA;AA0DD,MAAM,MAAM,UAAU,GAAG;IACvB,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,OAAO,CAAA;CACf,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,OAAO,EAAE,UAAU,CAAA;CACpB,GAAG,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;AAS9B,eAAO,MAAM,oBAAoB,EAAE,WAElC,CAAA;AAiBD,QAAA,MAAM,oBAAoB,GAAI,iCAG3B;IACD,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,6EAA6E;IAC7E,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB,SAgnBA,CAAA;AAED,eAAe,oBAAoB,CAAA"}
1
+ {"version":3,"file":"post-processing.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/post-processing.tsx"],"names":[],"mappings":"AA8CA,eAAO,MAAM,YAAY;;;CAGxB,CAAA;AAGD,eAAO,MAAM,WAAW;;;;;;;;;;;;;CAavB,CAAA;AAgED,MAAM,MAAM,UAAU,GAAG;IACvB,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,OAAO,CAAA;CACf,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,OAAO,EAAE,UAAU,CAAA;CACpB,GAAG,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;AAS9B,eAAO,MAAM,oBAAoB,EAAE,WAElC,CAAA;AA+CD,QAAA,MAAM,oBAAoB,GAAI,iCAG3B;IACD,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,6EAA6E;IAC7E,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB,SAmnBA,CAAA;AAED,eAAe,oBAAoB,CAAA"}
@@ -3,17 +3,22 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
3
  import { Color, Layers, Matrix4, Scene, UnsignedByteType } from 'three';
4
4
  import { ssgi } from 'three/addons/tsl/display/SSGINode.js';
5
5
  import { denoise } from 'three/examples/jsm/tsl/display/DenoiseNode.js';
6
- import { add, diffuseColor, float, mix, mrt, normalView, oscSine, output, pass, premultiplyAlpha, renderOutput, sample, saturation, screenUV, smoothstep, time, uniform, vec3, vec4, } from 'three/tsl';
7
- import { RenderPipeline } from 'three/webgpu';
6
+ import { add, diffuseColor, mix, mrt, normalView, normalWorldGeometry, oscSine, output, pass, premultiplyAlpha, renderOutput, sample, saturation, screenUV, smoothstep, time, float as tslFloat, uniform, vec3, vec4, } from 'three/tsl';
7
+ import { RenderPipeline, TimestampQuery } from 'three/webgpu';
8
8
  import { backdropGradient, deepSkyColor, horizonHazeColor } from '../../lib/backdrop';
9
9
  import { edgeColorFor, edgeOpacityScaleFor } from '../../lib/edge-style';
10
- import { PERF_OVERLAY_ENABLED, pushGpuSample } from '../../lib/gpu-perf';
10
+ import { PERF_OVERLAY_ENABLED } from '../../lib/gpu-perf';
11
11
  import { inkedEdges } from '../../lib/ink-edges';
12
+ import { refreshIsolation } from '../../lib/isolation';
13
+ import { LayerPassIndex, LayerPassNode } from '../../lib/layer-pass';
12
14
  import { GRID_LAYER, OVERLAY_LAYER, SCENE_LAYER, ZONE_LAYER } from '../../lib/layers';
13
15
  import { mergedOutline } from '../../lib/merged-outline-node';
16
+ import { recordPerfSample, timeSpan } from '../../lib/perf-tracks';
17
+ import { PostProcessingResources } from '../../lib/post-processing-resources';
14
18
  import { getSceneTheme } from '../../lib/scene-themes';
15
19
  import { packNormalToRGB, unpackRGBToNormal } from '../../lib/tsl-compat';
16
20
  import useViewer from '../../store/use-viewer';
21
+ import { useSceneAtmosphere } from './scene-atmosphere';
17
22
  // Scene-referred grade applied before the output tone mapping (AgX). AgX rolls
18
23
  // highlights off gently but reads flat on its own; a mild mid-gray-pivot
19
24
  // contrast + saturation lift restores the punch. Rendered shading only.
@@ -36,7 +41,8 @@ export const SSGI_PARAMS = {
36
41
  useScreenSpaceSampling: true,
37
42
  useTemporalFiltering: false,
38
43
  };
39
- // Diagnostic toggles for thermal A/B testing. Add `?disable=ao,denoise,outline,postFx`
44
+ // Diagnostic toggles for thermal A/B testing. Add
45
+ // `?disable=ao,denoise,outline,postFx`
40
46
  // to the URL (any subset) and reload to skip those passes. Each flag prevents
41
47
  // allocation + per-frame work for that stage, so device temperature deltas
42
48
  // across combos isolate which pass is the actual culprit. Picked up once at
@@ -53,7 +59,12 @@ export const SSGI_PARAMS = {
53
59
  // per-frame vertex/draw cost dominates the whole capture.
54
60
  function readPerfDisableFlags() {
55
61
  if (typeof window === 'undefined') {
56
- return { ao: false, denoise: false, outline: false, postFx: false };
62
+ return {
63
+ ao: false,
64
+ denoise: false,
65
+ outline: false,
66
+ postFx: false,
67
+ };
57
68
  }
58
69
  const raw = new URLSearchParams(window.location.search).get('disable') ?? '';
59
70
  const set = new Set(raw
@@ -99,9 +110,39 @@ function sanitizeOutlineObjects(objects) {
99
110
  }
100
111
  objects.length = nextIndex;
101
112
  }
113
+ // Two independent GPU readings per frame, both `?perf`-only:
114
+ // - `gpu-render`: three's WebGPU timestamp queries — the summed GPU duration of
115
+ // the frame's render passes, measured on the device. The only honest "GPU ms".
116
+ // - `gpu-queue`: submit → `onSubmittedWorkDone()` wall time. That covers queue
117
+ // backlog and CPU work that ran before the microtask got to resume, so it is
118
+ // a backpressure signal, not GPU time.
119
+ // `resolveTimestampsAsync` returns the previous resolve's value while one is in
120
+ // flight, so calling it every frame is safe (and required — the query pool warns
121
+ // once it fills).
122
+ function recordFrameGpuTiming(renderer, submittedAt) {
123
+ const queue = renderer.backend?.device?.queue;
124
+ queue?.onSubmittedWorkDone?.().then(() => {
125
+ recordPerfSample('gpu-queue', performance.now() - submittedAt);
126
+ });
127
+ // Off unless the device advertised 'timestamp-query' at init — the backend
128
+ // clears its own flag when the feature is missing, so this is the truth.
129
+ if (renderer.backend?.trackTimestamp !== true)
130
+ return;
131
+ renderer
132
+ .resolveTimestampsAsync?.(TimestampQuery.RENDER)
133
+ ?.then((ms) => {
134
+ if (typeof ms === 'number' && ms > 0)
135
+ recordPerfSample('gpu-render', ms);
136
+ })
137
+ .catch(() => {
138
+ // Pool disposed mid-flight (pipeline rebuild / unmount) — nothing to report.
139
+ });
140
+ }
102
141
  const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, disablePostFx = false, }) => {
103
142
  const { gl: renderer, invalidate, scene, camera, size } = useThree();
104
- const renderPipelineRef = useRef(null);
143
+ const resourcesRef = useRef(null);
144
+ const atmosphere = useSceneAtmosphere();
145
+ const directSkyNode = useMemo(() => (atmosphere ? atmosphere.skyRadiance(normalWorldGeometry) : null), [atmosphere]);
105
146
  const hasPipelineErrorRef = useRef(false);
106
147
  const retryCountRef = useRef(0);
107
148
  const rebuildTimeoutRef = useRef(null);
@@ -183,6 +224,10 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, disablePostF
183
224
  }
184
225
  setPipelineVersion((v) => v + 1);
185
226
  }, []);
227
+ const disposePipeline = useCallback(() => {
228
+ resourcesRef.current?.dispose();
229
+ resourcesRef.current = null;
230
+ }, []);
186
231
  // Reset retry state when project changes
187
232
  useEffect(() => {
188
233
  if (lastProjectIdRef.current === projectId)
@@ -233,10 +278,7 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, disablePostF
233
278
  if (width < 1 || height < 1) {
234
279
  skippedZeroSizeRef.current = true;
235
280
  hasPipelineErrorRef.current = false;
236
- if (renderPipelineRef.current) {
237
- renderPipelineRef.current.dispose();
238
- }
239
- renderPipelineRef.current = null;
281
+ disposePipeline();
240
282
  return;
241
283
  }
242
284
  if (skippedZeroSizeRef.current) {
@@ -249,23 +291,17 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, disablePostF
249
291
  // allocated every pass.
250
292
  if (disablePostFx || perfDisable.postFx) {
251
293
  hasPipelineErrorRef.current = false;
252
- if (renderPipelineRef.current) {
253
- renderPipelineRef.current.dispose();
254
- }
255
- renderPipelineRef.current = null;
294
+ disposePipeline();
256
295
  return;
257
296
  }
258
297
  const ssgiEnabled = shading === 'rendered' && SSGI_PARAMS.enabled && !perfDisable.ao;
259
298
  const denoiseEnabled = ssgiEnabled && !perfDisable.denoise;
260
299
  const outlineEnabled = !perfDisable.outline;
261
300
  const inkEnabled = edges !== 'off';
262
- // The depth+normal MRT feeds both SSGI and the screen-space ink pass.
263
301
  const needsNormalMRT = ssgiEnabled || inkEnabled;
264
302
  // Soft = thin (1px sample radius) + faint (50% opacity); strong = thick
265
303
  // (2px, ~2× wider detected band) + solid (100%). The edge masks saturate,
266
- // so radius+opacity are what actually separate the two modes — gain wouldn't.
267
- // Same 1px line thickness for both (soft's thickness is the nice one);
268
- // strong reads heavier purely by being fully solid vs soft's lighter 50%.
304
+ // so radius+opacity are what actually separate the two modes.
269
305
  const inkRadius = 1;
270
306
  const inkOpacity = inkOpacityOverride ?? (edges === 'strong' ? 1 : 0.5);
271
307
  console.log('[viewer/post-processing] Building pipeline', {
@@ -293,7 +329,7 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, disablePostF
293
329
  const hasWebGPU = typeof navigator !== 'undefined' && 'gpu' in navigator;
294
330
  if (!hasWebGPU) {
295
331
  hasPipelineErrorRef.current = true;
296
- renderPipelineRef.current = null;
332
+ resourcesRef.current = null;
297
333
  return;
298
334
  }
299
335
  // Clear outliner arrays synchronously to prevent stale Object3D refs
@@ -303,15 +339,22 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, disablePostF
303
339
  sanitizeOutlineObjects(outliner.hoveredObjects);
304
340
  outliner.selectedObjects.length = 0;
305
341
  outliner.hoveredObjects.length = 0;
342
+ const resources = new PostProcessingResources();
343
+ resourcesRef.current = resources;
306
344
  try {
345
+ const layerIndex = new LayerPassIndex(scene, [ZONE_LAYER, OVERLAY_LAYER]);
346
+ resources.layerIndex = layerIndex;
307
347
  const scenePass = pass(scene, camera);
348
+ resources.passes.push(scenePass);
308
349
  scenePass.setLayers(sceneOnlyLayers);
309
- const zonePass = pass(scene, camera);
350
+ const zonePass = new LayerPassNode(layerIndex, camera, ZONE_LAYER, scenePass);
351
+ resources.passes.push(zonePass);
310
352
  zonePass.setLayers(zoneLayers);
311
353
  // Editor overlays (gizmos, move handles, tool previews, grid) on their own
312
354
  // layer, kept out of the depth/normal MRT above so the ink + SSGI ignore
313
355
  // them, then composited on top of the final image below.
314
- const overlayPass = pass(scene, camera);
356
+ const overlayPass = new LayerPassNode(layerIndex, camera, OVERLAY_LAYER, scenePass);
357
+ resources.passes.push(overlayPass);
315
358
  overlayPass.setLayers(overlayLayers);
316
359
  const overlayColor = overlayPass.getTextureNode('output');
317
360
  const scenePassColor = scenePass.getTextureNode('output');
@@ -325,25 +368,28 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, disablePostF
325
368
  // not SSGI is enabled. When SSGI is on, the branch below overwrites this
326
369
  // with its own zone-inclusive composite (no double-add).
327
370
  let sceneColor = vec4(add(scenePassColor.rgb, zonePass.rgb), contentAlpha);
328
- // Depth + normal MRT — shared by SSGI (diffuse/normal) and the ink pass
329
- // (depth/normal). Built whenever either is active.
330
- let scenePassDepth = null;
371
+ // Scene depth is shared by SSGI, ink, and outlines.
372
+ // The normal MRT is only built when SSGI or ink needs it.
373
+ const scenePassDepth = scenePass.getTextureNode('depth');
331
374
  let scenePassNormal = null;
332
- let sceneNormal = null;
333
375
  if (needsNormalMRT) {
334
376
  scenePass.setMRT(mrt({
335
377
  output,
336
378
  diffuseColor,
337
379
  normal: packNormalToRGB(normalView),
338
380
  }));
339
- scenePassDepth = scenePass.getTextureNode('depth');
340
381
  scenePassNormal = scenePass.getTextureNode('normal');
382
+ }
383
+ if (scenePassNormal) {
341
384
  const normalTexture = scenePass.getTexture('normal');
342
385
  normalTexture.type = UnsignedByteType;
343
- // Extract normal from color-encoded texture (SSGI consumes the node form)
344
- sceneNormal = sample((uv) => unpackRGBToNormal(scenePassNormal.sample(uv)));
345
386
  }
346
- if (ssgiEnabled) {
387
+ // Extract normal from color-encoded texture (SSGI consumes the node form).
388
+ const normalNode = scenePassNormal;
389
+ const sceneNormal = normalNode
390
+ ? sample((uv) => unpackRGBToNormal(normalNode.sample(uv)))
391
+ : null;
392
+ if (ssgiEnabled && scenePassDepth && sceneNormal) {
347
393
  const scenePassDiffuse = scenePass.getTextureNode('diffuseColor');
348
394
  const diffuseTexture = scenePass.getTexture('diffuseColor');
349
395
  diffuseTexture.type = UnsignedByteType;
@@ -367,7 +413,7 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, disablePostF
367
413
  if (denoiseEnabled) {
368
414
  // DenoiseNode only denoises RGB — alpha is passed through unchanged.
369
415
  // SSGI's AO is a single red channel, so we remap it into RGB before denoising.
370
- const aoAsRgb = vec4(aoTexture.r, aoTexture.r, aoTexture.r, float(1));
416
+ const aoAsRgb = vec4(aoTexture.r, aoTexture.r, aoTexture.r, tslFloat(1));
371
417
  const denoisePass = denoise(aoAsRgb, scenePassDepth, sceneNormal, camera);
372
418
  denoisePass.index.value = 0;
373
419
  denoisePass.radius.value = 4;
@@ -382,22 +428,22 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, disablePostF
382
428
  // (same ≈150→350 m window as ink-edges' distanceFade) so the horizon
383
429
  // disc and the geometry↔sky depth cliff never grow an AO band — that
384
430
  // band read as a visible line along the horizon.
385
- const aoFarFade = smoothstep(float(0.9994), float(0.9998), scenePassDepth.sample(screenUV).r);
386
- ao = mix(ao, float(1), aoFarFade);
431
+ const aoFarFade = smoothstep(tslFloat(0.9994), tslFloat(0.9998), scenePassDepth.sample(screenUV).r);
432
+ ao = mix(ao, tslFloat(1), aoFarFade);
387
433
  // Composite: scene * AO + diffuse * GI
388
434
  sceneColor = vec4(add(scenePassColor.rgb.mul(ao), add(zonePass.rgb, scenePassDiffuse.rgb.mul(gi))), contentAlpha);
389
435
  }
390
436
  // Screen-space ink outline (SketchUp look) — depth/normal edge detection
391
437
  // over the composited scene. Topology-agnostic, so it handles CSG-cut
392
438
  // walls cleanly. Applied before the selection outline + background mix.
393
- if (inkEnabled) {
439
+ if (inkEnabled && scenePassDepth && scenePassNormal) {
394
440
  sceneColor = vec4(inkedEdges({
395
441
  sceneRgb: sceneColor.rgb,
396
442
  depthTex: scenePassDepth,
397
443
  normalTex: scenePassNormal,
398
444
  inkColor: inkColorUniform.current,
399
445
  radius: inkRadius,
400
- opacity: float(inkOpacity).mul(inkOpacityScaleUniform.current),
446
+ opacity: tslFloat(inkOpacity).mul(inkOpacityScaleUniform.current),
401
447
  }), sceneColor.a);
402
448
  }
403
449
  // Scene-referred grade (contrast around mid-gray + saturation) before the
@@ -409,17 +455,20 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, disablePostF
409
455
  if (shading === 'rendered') {
410
456
  sceneColor = vec4(gradeRgb(sceneColor.rgb), sceneColor.a);
411
457
  }
412
- // Single merged outline node: one shared depth pass for both selected + hovered groups.
458
+ // Reused scene depth lets outlined groups occlude each other; materials
459
+ // with depthWrite=false (including glazing) no longer occlude outlines.
413
460
  const outliner = useViewer.getState().outliner;
414
461
  let compositeWithOutlines = sceneColor;
415
462
  let visualAlpha = contentAlpha;
416
463
  if (outlineEnabled) {
417
464
  const outlineNode = mergedOutline(scene, camera, {
465
+ sceneDepthNode: scenePassDepth,
418
466
  primaryObjects: outliner.selectedObjects,
419
467
  secondaryObjects: outliner.hoveredObjects,
420
468
  primaryEdgeThickness: uniform(1),
421
469
  secondaryEdgeThickness: uniform(1.5),
422
470
  });
471
+ resources.outline = outlineNode;
423
472
  // Selected: white visible, yellow hidden
424
473
  const selectedVisibleColor = uniform(new Color(0xff_ff_ff));
425
474
  const selectedHiddenColor = uniform(new Color(0xf3_ff_47));
@@ -431,7 +480,7 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, disablePostF
431
480
  // Hovered: blue visible, yellow hidden, pulsing
432
481
  const pulsePeriod = uniform(3);
433
482
  const oscillating = oscSine(time.div(pulsePeriod).mul(2)).mul(0.5).add(0.5);
434
- const osc = mix(oscillating, float(1), hoverPulseMix);
483
+ const osc = mix(oscillating, tslFloat(1), hoverPulseMix);
435
484
  const hoverOutline = outlineNode.secondaryVisibleEdge
436
485
  .mul(hoverVisibleColor)
437
486
  .add(outlineNode.secondaryHiddenEdge.mul(hoverHiddenColor))
@@ -448,37 +497,40 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, disablePostF
448
497
  // sky gradient (shared formula in lib/backdrop.ts). The horizon disc
449
498
  // dissolves into the same formula, so backdrop and ground meet
450
499
  // seamlessly exactly where the disc vanishes.
451
- const ndc = vec4(screenUV.x.mul(2).sub(1), float(1).sub(screenUV.y).mul(2).sub(1), 1, 1);
500
+ const ndc = vec4(screenUV.x.mul(2).sub(1), tslFloat(1).sub(screenUV.y).mul(2).sub(1), 1, 1);
452
501
  const viewRay = camProjInvUniform.current.mul(ndc);
453
502
  const worldDir = camWorldUniform.current.mul(vec4(viewRay.xyz, 0)).xyz.normalize();
454
- let bgGradient = backdropGradient({
455
- dirY: worldDir.y,
456
- background: bgUniform.current,
457
- haze: bgHazeUniform.current,
458
- sky: bgSkyUniform.current,
459
- skyDeep: bgSkyDeepUniform.current,
460
- });
503
+ let bgGradient = atmosphere
504
+ ? atmosphere.skyRadiance(worldDir)
505
+ : backdropGradient({
506
+ dirY: worldDir.y,
507
+ background: bgUniform.current,
508
+ haze: bgHazeUniform.current,
509
+ sky: bgSkyUniform.current,
510
+ skyDeep: bgSkyDeepUniform.current,
511
+ });
461
512
  if (shading === 'rendered') {
462
513
  bgGradient = gradeRgb(bgGradient);
463
514
  }
464
- const composited = mix(bgGradient, compositeWithOutlines.rgb, contentAlpha);
515
+ const sceneComposite = compositeWithOutlines.rgb;
516
+ const composited = mix(bgGradient, sceneComposite, contentAlpha);
465
517
  // Editor overlays painted on top by their own alpha — they never get inked,
466
518
  // AO'd, or outlined, and always read crisp regardless of scene depth.
467
519
  const withOverlay = mix(composited, overlayColor.rgb, overlayColor.a);
468
- let finalOutput = vec4(withOverlay, float(1));
520
+ let finalOutput = vec4(withOverlay, tslFloat(1));
469
521
  if (transparentBackground) {
470
522
  const overlayAlpha = overlayColor.a;
471
523
  const alpha = overlayAlpha.add(visualAlpha.mul(overlayAlpha.oneMinus()));
472
524
  const straightRgb = overlayColor.rgb
473
525
  .mul(overlayAlpha)
474
- .add(compositeWithOutlines.rgb.mul(visualAlpha).mul(overlayAlpha.oneMinus()))
475
- .div(alpha.max(float(0.00001)));
526
+ .add(sceneComposite.mul(visualAlpha).mul(overlayAlpha.oneMinus()))
527
+ .div(alpha.max(tslFloat(0.00001)));
476
528
  finalOutput = premultiplyAlpha(renderOutput(vec4(straightRgb, alpha)));
477
529
  }
478
530
  const renderPipeline = new RenderPipeline(renderer);
531
+ resources.pipeline = renderPipeline;
479
532
  renderPipeline.outputColorTransform = !transparentBackground;
480
533
  renderPipeline.outputNode = finalOutput;
481
- renderPipelineRef.current = renderPipeline;
482
534
  retryCountRef.current = 0;
483
535
  }
484
536
  catch (error) {
@@ -488,24 +540,18 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, disablePostF
488
540
  ssgi: SSGI_PARAMS.enabled,
489
541
  rendererCtor: renderer.constructor?.name,
490
542
  }, error);
491
- if (renderPipelineRef.current) {
492
- renderPipelineRef.current.dispose();
493
- }
494
- renderPipelineRef.current = null;
543
+ disposePipeline();
495
544
  }
496
- return () => {
497
- if (renderPipelineRef.current) {
498
- renderPipelineRef.current.dispose();
499
- }
500
- renderPipelineRef.current = null;
501
- };
545
+ return disposePipeline;
502
546
  }, [
503
547
  // NOTE: hoverHighlightMode intentionally excluded — the hover style is
504
548
  // pushed to uniforms in a separate effect, so a hover must NOT rebuild the
505
549
  // whole pipeline. The uniform refs below are stable (useMemo), so they
506
550
  // never trigger a rebuild either.
551
+ atmosphere,
507
552
  camera,
508
553
  disablePostFx,
554
+ disposePipeline,
509
555
  hoverHiddenColor,
510
556
  hoverPulseMix,
511
557
  hoverStrength,
@@ -567,12 +613,16 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, disablePostF
567
613
  inkColorUniform.current.value.set(edgeColorFor(bgHex));
568
614
  inkOpacityScaleUniform.current.value = edgeOpacityScaleFor(bgHex);
569
615
  const outliner = useViewer.getState().outliner;
616
+ const restoreAtmosphere = refreshIsolation(scene);
570
617
  sanitizeOutlineObjects(outliner.selectedObjects);
571
618
  sanitizeOutlineObjects(outliner.hoveredObjects);
572
619
  if (disablePostFx ||
573
620
  PERF_POST_FX_DISABLED ||
574
621
  hasPipelineErrorRef.current ||
575
- !renderPipelineRef.current) {
622
+ !resourcesRef.current?.pipeline) {
623
+ const previousBackgroundNode = scene.backgroundNode;
624
+ if (directSkyNode && !transparentBackground)
625
+ scene.backgroundNode = directSkyNode;
576
626
  try {
577
627
  const clearAlpha = transparentBackground ? 0 : 1;
578
628
  if (renderer.setClearColor) {
@@ -584,37 +634,34 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, disablePostF
584
634
  renderer.setClearAlpha(clearAlpha);
585
635
  }
586
636
  const submittedAt = PERF_OVERLAY_ENABLED ? performance.now() : 0;
587
- renderer.render(scene, camera);
588
- if (PERF_OVERLAY_ENABLED) {
589
- const queue = renderer.backend?.device?.queue;
590
- queue?.onSubmittedWorkDone?.().then(() => {
591
- pushGpuSample(performance.now() - submittedAt);
592
- });
593
- }
637
+ timeSpan('render-encode', () => {
638
+ ;
639
+ renderer.render(scene, camera);
640
+ });
641
+ if (PERF_OVERLAY_ENABLED)
642
+ recordFrameGpuTiming(renderer, submittedAt);
594
643
  }
595
644
  catch (fallbackError) {
596
645
  console.error('[viewer/post-processing] Fallback render failed.', fallbackError);
597
646
  }
647
+ finally {
648
+ scene.backgroundNode = previousBackgroundNode;
649
+ restoreAtmosphere();
650
+ }
598
651
  return;
599
652
  }
653
+ const pipeline = resourcesRef.current.pipeline;
600
654
  try {
601
655
  // Clear alpha=0 so background pixels in the output MRT attachment (index 0) get a=0,
602
656
  // making scenePassColor.a a reliable geometry mask (geometry pixels write a=1 via output node).
603
657
  ;
604
658
  renderer.setClearAlpha(0);
605
659
  const submittedAt = PERF_OVERLAY_ENABLED ? performance.now() : 0;
606
- renderPipelineRef.current.render();
607
- if (PERF_OVERLAY_ENABLED) {
608
- // device.queue.onSubmittedWorkDone() resolves once the GPU has
609
- // finished the work we just submitted — the delta from our submit
610
- // timestamp is a clean per-frame GPU duration. Doesn't block CPU
611
- // (no await) and works for the custom RenderPipeline path that
612
- // bypasses three.js's timestamp-query infrastructure.
613
- const queue = renderer.backend?.device?.queue;
614
- queue?.onSubmittedWorkDone?.().then(() => {
615
- pushGpuSample(performance.now() - submittedAt);
616
- });
617
- }
660
+ timeSpan('render-encode', () => {
661
+ pipeline.render();
662
+ });
663
+ if (PERF_OVERLAY_ENABLED)
664
+ recordFrameGpuTiming(renderer, submittedAt);
618
665
  }
619
666
  catch (error) {
620
667
  hasPipelineErrorRef.current = true;
@@ -624,10 +671,7 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, disablePostF
624
671
  rendererCtor: renderer.constructor?.name,
625
672
  error,
626
673
  });
627
- if (renderPipelineRef.current) {
628
- renderPipelineRef.current.dispose();
629
- }
630
- renderPipelineRef.current = null;
674
+ disposePipeline();
631
675
  if (retryCountRef.current < MAX_PIPELINE_RETRIES) {
632
676
  // Auto-retry: schedule a pipeline rebuild if we haven't exceeded the retry limit
633
677
  retryCountRef.current++;
@@ -640,6 +684,9 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, disablePostF
640
684
  console.error('[viewer/post-processing] Retries exhausted. Rendering without post FX for this session.');
641
685
  }
642
686
  }
687
+ finally {
688
+ restoreAtmosphere();
689
+ }
643
690
  }, 1);
644
691
  return null;
645
692
  };
@@ -2,11 +2,20 @@
2
2
  * Mounts every registered node kind's system component, ordered by
3
3
  * `system.priority` (default {@link DEFAULT_PRIORITY}).
4
4
  *
5
- * Today the registry is empty so this component mounts nothing — coexists
6
- * with legacy `*-System` components in `<Viewer>`. Once kinds register via
7
- * `@pascal-app/nodes`, each kind's registry-driven system takes over and
8
- * its legacy counterpart short-circuits via the `nodeRegistry.has(kind)`
9
- * guard added to each legacy system.
5
+ * Two resilience rules, both learned from a live session in which the wall
6
+ * systems bundle (geometry rebuild + cutout stamps + batching) never ran
7
+ * while everything else did (QA f2 probe6: 24 walls stuck on placeholder
8
+ * geometry, no `wallHidden` stamps, base materials untouched):
9
+ *
10
+ * 1. `entries` re-derives on `useRegistryVersion()` — kinds register
11
+ * asynchronously (plugin discovery, HMR), and a list snapshotted once at
12
+ * mount permanently drops any system whose kind registers later. Same
13
+ * staleness class SelectionManager already guards against ("plugin
14
+ * nodes select-but-never-hover").
15
+ * 2. Each system gets its OWN Suspense boundary. With one shared boundary,
16
+ * ANY lazily-loading (or load-failing) system chunk unmounts every
17
+ * other system while it is pending — one bad chunk must not take the
18
+ * wall pipeline down with it.
10
19
  */
11
20
  export declare function RegisteredSystems(): import("react").JSX.Element | null;
12
21
  //# sourceMappingURL=registered-systems.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"registered-systems.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/registered-systems.tsx"],"names":[],"mappings":"AA8BA;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,uCAyBhC"}
1
+ {"version":3,"file":"registered-systems.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/registered-systems.tsx"],"names":[],"mappings":"AA+BA;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,iBAAiB,uCAgChC"}
@@ -1,6 +1,6 @@
1
1
  'use client';
2
- import { jsx as _jsx } from "react/jsx-runtime";
3
- import { createSceneApi, isNodeKindEnabled, nodeRegistry, useScene, } from '@pascal-app/core';
2
+ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { createSceneApi, isNodeKindEnabled, nodeRegistry, useRegistryVersion, useScene, } from '@pascal-app/core';
4
4
  import { lazy, Suspense, useMemo } from 'react';
5
5
  const DEFAULT_PRIORITY = 5;
6
6
  const lazyCache = new WeakMap();
@@ -18,16 +18,28 @@ function loadSystem(def) {
18
18
  * Mounts every registered node kind's system component, ordered by
19
19
  * `system.priority` (default {@link DEFAULT_PRIORITY}).
20
20
  *
21
- * Today the registry is empty so this component mounts nothing — coexists
22
- * with legacy `*-System` components in `<Viewer>`. Once kinds register via
23
- * `@pascal-app/nodes`, each kind's registry-driven system takes over and
24
- * its legacy counterpart short-circuits via the `nodeRegistry.has(kind)`
25
- * guard added to each legacy system.
21
+ * Two resilience rules, both learned from a live session in which the wall
22
+ * systems bundle (geometry rebuild + cutout stamps + batching) never ran
23
+ * while everything else did (QA f2 probe6: 24 walls stuck on placeholder
24
+ * geometry, no `wallHidden` stamps, base materials untouched):
25
+ *
26
+ * 1. `entries` re-derives on `useRegistryVersion()` — kinds register
27
+ * asynchronously (plugin discovery, HMR), and a list snapshotted once at
28
+ * mount permanently drops any system whose kind registers later. Same
29
+ * staleness class SelectionManager already guards against ("plugin
30
+ * nodes select-but-never-hover").
31
+ * 2. Each system gets its OWN Suspense boundary. With one shared boundary,
32
+ * ANY lazily-loading (or load-failing) system chunk unmounts every
33
+ * other system while it is pending — one bad chunk must not take the
34
+ * wall pipeline down with it.
26
35
  */
27
36
  export function RegisteredSystems() {
28
37
  const sceneApi = useMemo(() => createSceneApi(useScene), []);
29
38
  const installedPlugins = useScene((state) => state.installedPlugins);
39
+ const registryVersion = useRegistryVersion();
30
40
  const entries = useMemo(() => {
41
+ // re-derive when kinds register after mount (async plugin load)
42
+ void registryVersion;
31
43
  return Array.from(nodeRegistry.entries())
32
44
  .filter(([, def]) => def.system != null)
33
45
  .sort(([, a], [, b]) => {
@@ -35,15 +47,15 @@ export function RegisteredSystems() {
35
47
  const pb = b.system?.priority ?? DEFAULT_PRIORITY;
36
48
  return pa - pb;
37
49
  });
38
- }, []);
50
+ }, [registryVersion]);
39
51
  if (entries.length === 0)
40
52
  return null;
41
- return (_jsx(Suspense, { fallback: null, children: entries.map(([kind, def]) => {
53
+ return (_jsx(_Fragment, { children: entries.map(([kind, def]) => {
42
54
  if (!isNodeKindEnabled(kind, installedPlugins))
43
55
  return null;
44
56
  const Comp = loadSystem(def);
45
57
  if (!Comp)
46
58
  return null;
47
- return _jsx(Comp, { sceneApi: sceneApi }, `registered-system:${kind}`);
59
+ return (_jsx(Suspense, { fallback: null, children: _jsx(Comp, { sceneApi: sceneApi }) }, `registered-system:${kind}`));
48
60
  }) }));
49
61
  }
@@ -0,0 +1,30 @@
1
+ import type { Color, Node, Vector3 } from 'three/webgpu';
2
+ export type SceneAtmosphereSource = {
3
+ skyRadiance(direction: Node<'vec3'>): Node<'vec3'>;
4
+ reflectionRadiance(direction: Node<'vec3'>): Node<'vec3'>;
5
+ fogRadiance(direction: Node<'vec3'>): Node<'vec3'>;
6
+ environmentNode: Node<'vec3'>;
7
+ sunDirection: Vector3;
8
+ sunColor: Color;
9
+ sunIntensity: number;
10
+ moonDirection: Vector3;
11
+ moonColor: Color;
12
+ moonIntensity: number;
13
+ skyColor: Color;
14
+ groundColor: Color;
15
+ hemisphereIntensity: number;
16
+ ambientIntensity: number;
17
+ exposure: number;
18
+ fogStart: number;
19
+ fogEnd: number;
20
+ };
21
+ /** Returns the atmosphere currently owning this React Three Fiber scene. */
22
+ export declare function useSceneAtmosphere(): SceneAtmosphereSource | null;
23
+ /**
24
+ * Installs a generic radiance source into the current scene. The source object is
25
+ * expected to remain stable while its colors, vectors, and numbers mutate.
26
+ */
27
+ export declare function SceneAtmosphere({ source }: {
28
+ source: SceneAtmosphereSource;
29
+ }): null;
30
+ //# sourceMappingURL=scene-atmosphere.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scene-atmosphere.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/scene-atmosphere.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,KAAK,EAAE,IAAI,EAAS,OAAO,EAAE,MAAM,cAAc,CAAA;AAI/D,MAAM,MAAM,qBAAqB,GAAG;IAClC,WAAW,CAAC,SAAS,EAAE,IAAI,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,CAAA;IAClD,kBAAkB,CAAC,SAAS,EAAE,IAAI,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,CAAA;IACzD,WAAW,CAAC,SAAS,EAAE,IAAI,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,CAAA;IAClD,eAAe,EAAE,IAAI,CAAC,MAAM,CAAC,CAAA;IAC7B,YAAY,EAAE,OAAO,CAAA;IACrB,QAAQ,EAAE,KAAK,CAAA;IACf,YAAY,EAAE,MAAM,CAAA;IACpB,aAAa,EAAE,OAAO,CAAA;IACtB,SAAS,EAAE,KAAK,CAAA;IAChB,aAAa,EAAE,MAAM,CAAA;IACrB,QAAQ,EAAE,KAAK,CAAA;IACf,WAAW,EAAE,KAAK,CAAA;IAClB,mBAAmB,EAAE,MAAM,CAAA;IAC3B,gBAAgB,EAAE,MAAM,CAAA;IACxB,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;CACf,CAAA;AA6DD,4EAA4E;AAC5E,wBAAgB,kBAAkB,IAAI,qBAAqB,GAAG,IAAI,CAIjE;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,qBAAqB,CAAA;CAAE,QA8C5E"}