@pascal-app/viewer 1.0.0-beta.4 → 1.0.0

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 (265) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +107 -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 +228 -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 +78 -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 +69 -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/components/viewer/batched-mesh-spike.d.ts +14 -0
  49. package/dist/components/viewer/batched-mesh-spike.d.ts.map +1 -0
  50. package/dist/components/viewer/batched-mesh-spike.js +64 -0
  51. package/dist/components/viewer/frame-limiter.d.ts +11 -0
  52. package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
  53. package/dist/components/viewer/frame-limiter.js +59 -18
  54. package/dist/components/viewer/index.d.ts +15 -0
  55. package/dist/components/viewer/index.d.ts.map +1 -1
  56. package/dist/components/viewer/index.js +74 -82
  57. package/dist/components/viewer/lights.d.ts.map +1 -1
  58. package/dist/components/viewer/lights.js +123 -89
  59. package/dist/components/viewer/perf-monitor.d.ts +6 -1
  60. package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
  61. package/dist/components/viewer/perf-monitor.js +306 -115
  62. package/dist/components/viewer/perf-panel.d.ts +3 -0
  63. package/dist/components/viewer/perf-panel.d.ts.map +1 -0
  64. package/dist/components/viewer/perf-panel.js +178 -0
  65. package/dist/components/viewer/pointer-raycast-layers.d.ts +15 -0
  66. package/dist/components/viewer/pointer-raycast-layers.d.ts.map +1 -0
  67. package/dist/components/viewer/pointer-raycast-layers.js +28 -0
  68. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  69. package/dist/components/viewer/post-processing.js +124 -83
  70. package/dist/components/viewer/registered-systems.d.ts +14 -5
  71. package/dist/components/viewer/registered-systems.d.ts.map +1 -1
  72. package/dist/components/viewer/registered-systems.js +22 -10
  73. package/dist/components/viewer/scene-atmosphere.d.ts +30 -0
  74. package/dist/components/viewer/scene-atmosphere.d.ts.map +1 -0
  75. package/dist/components/viewer/scene-atmosphere.js +89 -0
  76. package/dist/components/viewer/scene-environment.d.ts.map +1 -1
  77. package/dist/components/viewer/scene-environment.js +19 -5
  78. package/dist/components/viewer/scene-ground-replacement.d.ts +5 -0
  79. package/dist/components/viewer/scene-ground-replacement.d.ts.map +1 -0
  80. package/dist/components/viewer/scene-ground-replacement.js +35 -0
  81. package/dist/components/viewer/selection-manager.d.ts.map +1 -1
  82. package/dist/components/viewer/selection-manager.js +7 -2
  83. package/dist/components/viewer/unsupported-gpu-fallback.d.ts +2 -0
  84. package/dist/components/viewer/unsupported-gpu-fallback.d.ts.map +1 -0
  85. package/dist/components/viewer/unsupported-gpu-fallback.js +4 -0
  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 +20 -8
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +19 -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/dispose-object3d.d.ts +4 -0
  103. package/dist/lib/dispose-object3d.d.ts.map +1 -0
  104. package/dist/lib/dispose-object3d.js +27 -0
  105. package/dist/lib/geometry-groups.d.ts +17 -0
  106. package/dist/lib/geometry-groups.d.ts.map +1 -0
  107. package/dist/lib/geometry-groups.js +53 -0
  108. package/dist/lib/gpu-perf.d.ts +0 -2
  109. package/dist/lib/gpu-perf.d.ts.map +1 -1
  110. package/dist/lib/gpu-perf.js +7 -21
  111. package/dist/lib/isolation.d.ts +8 -8
  112. package/dist/lib/isolation.d.ts.map +1 -1
  113. package/dist/lib/isolation.js +18 -30
  114. package/dist/lib/layer-pass.d.ts +28 -0
  115. package/dist/lib/layer-pass.d.ts.map +1 -0
  116. package/dist/lib/layer-pass.js +254 -0
  117. package/dist/lib/layers.d.ts +16 -0
  118. package/dist/lib/layers.d.ts.map +1 -1
  119. package/dist/lib/layers.js +18 -0
  120. package/dist/lib/materials.d.ts +12 -0
  121. package/dist/lib/materials.d.ts.map +1 -1
  122. package/dist/lib/materials.js +51 -17
  123. package/dist/lib/merged-outline-node.d.ts +17 -8
  124. package/dist/lib/merged-outline-node.d.ts.map +1 -1
  125. package/dist/lib/merged-outline-node.js +194 -52
  126. package/dist/lib/perf-actions.d.ts +55 -0
  127. package/dist/lib/perf-actions.d.ts.map +1 -0
  128. package/dist/lib/perf-actions.js +211 -0
  129. package/dist/lib/perf-observers.d.ts +2 -0
  130. package/dist/lib/perf-observers.d.ts.map +1 -0
  131. package/dist/lib/perf-observers.js +17 -0
  132. package/dist/lib/perf-panel-store.d.ts +71 -0
  133. package/dist/lib/perf-panel-store.d.ts.map +1 -0
  134. package/dist/lib/perf-panel-store.js +29 -0
  135. package/dist/lib/perf-tracks.d.ts +43 -0
  136. package/dist/lib/perf-tracks.d.ts.map +1 -0
  137. package/dist/lib/perf-tracks.js +113 -0
  138. package/dist/lib/pointer-events.d.ts +7 -0
  139. package/dist/lib/pointer-events.d.ts.map +1 -0
  140. package/dist/lib/pointer-events.js +680 -0
  141. package/dist/lib/post-processing-resources.d.ts +12 -0
  142. package/dist/lib/post-processing-resources.d.ts.map +1 -0
  143. package/dist/lib/post-processing-resources.js +19 -0
  144. package/dist/lib/renderer-capability.d.ts +56 -0
  145. package/dist/lib/renderer-capability.d.ts.map +1 -0
  146. package/dist/lib/renderer-capability.js +116 -0
  147. package/dist/lib/scene-visibility.d.ts +15 -0
  148. package/dist/lib/scene-visibility.d.ts.map +1 -0
  149. package/dist/lib/scene-visibility.js +69 -0
  150. package/dist/lib/shadow-only.d.ts +16 -0
  151. package/dist/lib/shadow-only.d.ts.map +1 -1
  152. package/dist/lib/shadow-only.js +8 -17
  153. package/dist/lib/snapshot-pipeline.d.ts +9 -0
  154. package/dist/lib/snapshot-pipeline.d.ts.map +1 -1
  155. package/dist/lib/snapshot-pipeline.js +33 -9
  156. package/dist/store/use-viewer.d.ts +7 -6
  157. package/dist/store/use-viewer.d.ts.map +1 -1
  158. package/dist/systems/ceiling/ceiling-system.js +1 -1
  159. package/dist/systems/door/door-animation-system.d.ts.map +1 -1
  160. package/dist/systems/door/door-animation-system.js +6 -9
  161. package/dist/systems/door/door-system.d.ts.map +1 -1
  162. package/dist/systems/door/door-system.js +13 -2
  163. package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -1
  164. package/dist/systems/floor-elevation/floor-elevation-system.js +9 -0
  165. package/dist/systems/geometry/geometry-system.d.ts.map +1 -1
  166. package/dist/systems/geometry/geometry-system.js +5 -28
  167. package/dist/systems/level/level-system.d.ts.map +1 -1
  168. package/dist/systems/level/level-system.js +8 -2
  169. package/dist/systems/level/level-utils.d.ts +9 -0
  170. package/dist/systems/level/level-utils.d.ts.map +1 -1
  171. package/dist/systems/level/level-utils.js +14 -0
  172. package/dist/systems/perf-action-settle/perf-action-settle-system.d.ts +8 -0
  173. package/dist/systems/perf-action-settle/perf-action-settle-system.d.ts.map +1 -0
  174. package/dist/systems/perf-action-settle/perf-action-settle-system.js +32 -0
  175. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  176. package/dist/systems/roof/roof-system.js +1196 -58
  177. package/dist/systems/scan/scan-system.js +6 -5
  178. package/dist/systems/slab/slab-system.d.ts.map +1 -1
  179. package/dist/systems/slab/slab-system.js +1 -2
  180. package/dist/systems/stair/stair-system.d.ts.map +1 -1
  181. package/dist/systems/stair/stair-system.js +9 -8
  182. package/dist/systems/surface-hole-geometry.d.ts +1 -1
  183. package/dist/systems/surface-hole-geometry.d.ts.map +1 -1
  184. package/dist/systems/surface-hole-geometry.js +1 -1
  185. package/dist/systems/wall/level-miter-cache.d.ts +10 -0
  186. package/dist/systems/wall/level-miter-cache.d.ts.map +1 -0
  187. package/dist/systems/wall/level-miter-cache.js +40 -0
  188. package/dist/systems/wall/wall-build-lifecycle.d.ts +9 -0
  189. package/dist/systems/wall/wall-build-lifecycle.d.ts.map +1 -0
  190. package/dist/systems/wall/wall-build-lifecycle.js +74 -0
  191. package/dist/systems/wall/wall-cutout-cache.d.ts +57 -0
  192. package/dist/systems/wall/wall-cutout-cache.d.ts.map +1 -0
  193. package/dist/systems/wall/wall-cutout-cache.js +304 -0
  194. package/dist/systems/wall/wall-cutout.d.ts +24 -1
  195. package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
  196. package/dist/systems/wall/wall-cutout.js +33 -140
  197. package/dist/systems/wall/wall-material-variant.d.ts +31 -0
  198. package/dist/systems/wall/wall-material-variant.d.ts.map +1 -0
  199. package/dist/systems/wall/wall-material-variant.js +10 -0
  200. package/dist/systems/wall/wall-materials.d.ts +6 -0
  201. package/dist/systems/wall/wall-materials.d.ts.map +1 -1
  202. package/dist/systems/wall/wall-materials.js +29 -13
  203. package/dist/systems/wall/wall-placeholder-sweep.d.ts +50 -0
  204. package/dist/systems/wall/wall-placeholder-sweep.d.ts.map +1 -0
  205. package/dist/systems/wall/wall-placeholder-sweep.js +51 -0
  206. package/dist/systems/wall/wall-rebuild-notifications.d.ts +5 -0
  207. package/dist/systems/wall/wall-rebuild-notifications.d.ts.map +1 -0
  208. package/dist/systems/wall/wall-rebuild-notifications.js +23 -0
  209. package/dist/systems/wall/wall-system.d.ts +13 -2
  210. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  211. package/dist/systems/wall/wall-system.js +413 -148
  212. package/dist/systems/window/window-animation-system.d.ts.map +1 -1
  213. package/dist/systems/window/window-animation-system.js +9 -8
  214. package/dist/systems/window/window-system.d.ts +1 -0
  215. package/dist/systems/window/window-system.d.ts.map +1 -1
  216. package/dist/systems/window/window-system.js +16 -2
  217. package/package.json +18 -3
  218. package/dist/components/viewer/scene-bvh.test.d.ts +0 -2
  219. package/dist/components/viewer/scene-bvh.test.d.ts.map +0 -1
  220. package/dist/components/viewer/scene-bvh.test.js +0 -13
  221. package/dist/lib/csg-utils.test.d.ts +0 -2
  222. package/dist/lib/csg-utils.test.d.ts.map +0 -1
  223. package/dist/lib/csg-utils.test.js +0 -45
  224. package/dist/lib/materials.test.d.ts +0 -2
  225. package/dist/lib/materials.test.d.ts.map +0 -1
  226. package/dist/lib/materials.test.js +0 -25
  227. package/dist/lib/merged-outline-node.test.d.ts +0 -2
  228. package/dist/lib/merged-outline-node.test.d.ts.map +0 -1
  229. package/dist/lib/merged-outline-node.test.js +0 -22
  230. package/dist/lib/polygon-union.d.ts +0 -4
  231. package/dist/lib/polygon-union.d.ts.map +0 -1
  232. package/dist/lib/polygon-union.js +0 -289
  233. package/dist/lib/polygon-union.test.d.ts +0 -2
  234. package/dist/lib/polygon-union.test.d.ts.map +0 -1
  235. package/dist/lib/polygon-union.test.js +0 -105
  236. package/dist/lib/texture-reference.test.d.ts +0 -2
  237. package/dist/lib/texture-reference.test.d.ts.map +0 -1
  238. package/dist/lib/texture-reference.test.js +0 -86
  239. package/dist/store/use-viewer.test.d.ts +0 -2
  240. package/dist/store/use-viewer.test.d.ts.map +0 -1
  241. package/dist/store/use-viewer.test.js +0 -61
  242. package/dist/systems/door/door-animation.test.d.ts +0 -2
  243. package/dist/systems/door/door-animation.test.d.ts.map +0 -1
  244. package/dist/systems/door/door-animation.test.js +0 -51
  245. package/dist/systems/door/door-floor-alignment.test.d.ts +0 -2
  246. package/dist/systems/door/door-floor-alignment.test.d.ts.map +0 -1
  247. package/dist/systems/door/door-floor-alignment.test.js +0 -50
  248. package/dist/systems/geometry/geometry-system.test.d.ts +0 -2
  249. package/dist/systems/geometry/geometry-system.test.d.ts.map +0 -1
  250. package/dist/systems/geometry/geometry-system.test.js +0 -30
  251. package/dist/systems/slab/slab-system.test.d.ts +0 -2
  252. package/dist/systems/slab/slab-system.test.d.ts.map +0 -1
  253. package/dist/systems/slab/slab-system.test.js +0 -102
  254. package/dist/systems/wall/opening-cutout-geometry.test.d.ts +0 -2
  255. package/dist/systems/wall/opening-cutout-geometry.test.d.ts.map +0 -1
  256. package/dist/systems/wall/opening-cutout-geometry.test.js +0 -172
  257. package/dist/systems/wall/wall-opening-cutout.test.d.ts +0 -2
  258. package/dist/systems/wall/wall-opening-cutout.test.d.ts.map +0 -1
  259. package/dist/systems/wall/wall-opening-cutout.test.js +0 -59
  260. package/dist/systems/wall/wall-support-extension.test.d.ts +0 -2
  261. package/dist/systems/wall/wall-support-extension.test.d.ts.map +0 -1
  262. package/dist/systems/wall/wall-support-extension.test.js +0 -116
  263. package/dist/systems/wall/wall-terrain-bottom.test.d.ts +0 -2
  264. package/dist/systems/wall/wall-terrain-bottom.test.d.ts.map +0 -1
  265. package/dist/systems/wall/wall-terrain-bottom.test.js +0 -34
@@ -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":"AA6CA,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,SA+mBA,CAAA;AAED,eAAe,oBAAoB,CAAA"}
@@ -3,17 +3,21 @@ 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 { LayerPassIndex, LayerPassNode } from '../../lib/layer-pass';
12
13
  import { GRID_LAYER, OVERLAY_LAYER, SCENE_LAYER, ZONE_LAYER } from '../../lib/layers';
13
14
  import { mergedOutline } from '../../lib/merged-outline-node';
15
+ import { recordPerfSample, timeSpan } from '../../lib/perf-tracks';
16
+ import { PostProcessingResources } from '../../lib/post-processing-resources';
14
17
  import { getSceneTheme } from '../../lib/scene-themes';
15
18
  import { packNormalToRGB, unpackRGBToNormal } from '../../lib/tsl-compat';
16
19
  import useViewer from '../../store/use-viewer';
20
+ import { useSceneAtmosphere } from './scene-atmosphere';
17
21
  // Scene-referred grade applied before the output tone mapping (AgX). AgX rolls
18
22
  // highlights off gently but reads flat on its own; a mild mid-gray-pivot
19
23
  // contrast + saturation lift restores the punch. Rendered shading only.
@@ -36,7 +40,8 @@ export const SSGI_PARAMS = {
36
40
  useScreenSpaceSampling: true,
37
41
  useTemporalFiltering: false,
38
42
  };
39
- // Diagnostic toggles for thermal A/B testing. Add `?disable=ao,denoise,outline,postFx`
43
+ // Diagnostic toggles for thermal A/B testing. Add
44
+ // `?disable=ao,denoise,outline,postFx`
40
45
  // to the URL (any subset) and reload to skip those passes. Each flag prevents
41
46
  // allocation + per-frame work for that stage, so device temperature deltas
42
47
  // across combos isolate which pass is the actual culprit. Picked up once at
@@ -53,7 +58,12 @@ export const SSGI_PARAMS = {
53
58
  // per-frame vertex/draw cost dominates the whole capture.
54
59
  function readPerfDisableFlags() {
55
60
  if (typeof window === 'undefined') {
56
- return { ao: false, denoise: false, outline: false, postFx: false };
61
+ return {
62
+ ao: false,
63
+ denoise: false,
64
+ outline: false,
65
+ postFx: false,
66
+ };
57
67
  }
58
68
  const raw = new URLSearchParams(window.location.search).get('disable') ?? '';
59
69
  const set = new Set(raw
@@ -99,9 +109,39 @@ function sanitizeOutlineObjects(objects) {
99
109
  }
100
110
  objects.length = nextIndex;
101
111
  }
112
+ // Two independent GPU readings per frame, both `?perf`-only:
113
+ // - `gpu-render`: three's WebGPU timestamp queries — the summed GPU duration of
114
+ // the frame's render passes, measured on the device. The only honest "GPU ms".
115
+ // - `gpu-queue`: submit → `onSubmittedWorkDone()` wall time. That covers queue
116
+ // backlog and CPU work that ran before the microtask got to resume, so it is
117
+ // a backpressure signal, not GPU time.
118
+ // `resolveTimestampsAsync` returns the previous resolve's value while one is in
119
+ // flight, so calling it every frame is safe (and required — the query pool warns
120
+ // once it fills).
121
+ function recordFrameGpuTiming(renderer, submittedAt) {
122
+ const queue = renderer.backend?.device?.queue;
123
+ queue?.onSubmittedWorkDone?.().then(() => {
124
+ recordPerfSample('gpu-queue', performance.now() - submittedAt);
125
+ });
126
+ // Off unless the device advertised 'timestamp-query' at init — the backend
127
+ // clears its own flag when the feature is missing, so this is the truth.
128
+ if (renderer.backend?.trackTimestamp !== true)
129
+ return;
130
+ renderer
131
+ .resolveTimestampsAsync?.(TimestampQuery.RENDER)
132
+ ?.then((ms) => {
133
+ if (typeof ms === 'number' && ms > 0)
134
+ recordPerfSample('gpu-render', ms);
135
+ })
136
+ .catch(() => {
137
+ // Pool disposed mid-flight (pipeline rebuild / unmount) — nothing to report.
138
+ });
139
+ }
102
140
  const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, disablePostFx = false, }) => {
103
141
  const { gl: renderer, invalidate, scene, camera, size } = useThree();
104
- const renderPipelineRef = useRef(null);
142
+ const resourcesRef = useRef(null);
143
+ const atmosphere = useSceneAtmosphere();
144
+ const directSkyNode = useMemo(() => (atmosphere ? atmosphere.skyRadiance(normalWorldGeometry) : null), [atmosphere]);
105
145
  const hasPipelineErrorRef = useRef(false);
106
146
  const retryCountRef = useRef(0);
107
147
  const rebuildTimeoutRef = useRef(null);
@@ -183,6 +223,10 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, disablePostF
183
223
  }
184
224
  setPipelineVersion((v) => v + 1);
185
225
  }, []);
226
+ const disposePipeline = useCallback(() => {
227
+ resourcesRef.current?.dispose();
228
+ resourcesRef.current = null;
229
+ }, []);
186
230
  // Reset retry state when project changes
187
231
  useEffect(() => {
188
232
  if (lastProjectIdRef.current === projectId)
@@ -233,10 +277,7 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, disablePostF
233
277
  if (width < 1 || height < 1) {
234
278
  skippedZeroSizeRef.current = true;
235
279
  hasPipelineErrorRef.current = false;
236
- if (renderPipelineRef.current) {
237
- renderPipelineRef.current.dispose();
238
- }
239
- renderPipelineRef.current = null;
280
+ disposePipeline();
240
281
  return;
241
282
  }
242
283
  if (skippedZeroSizeRef.current) {
@@ -249,23 +290,17 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, disablePostF
249
290
  // allocated every pass.
250
291
  if (disablePostFx || perfDisable.postFx) {
251
292
  hasPipelineErrorRef.current = false;
252
- if (renderPipelineRef.current) {
253
- renderPipelineRef.current.dispose();
254
- }
255
- renderPipelineRef.current = null;
293
+ disposePipeline();
256
294
  return;
257
295
  }
258
296
  const ssgiEnabled = shading === 'rendered' && SSGI_PARAMS.enabled && !perfDisable.ao;
259
297
  const denoiseEnabled = ssgiEnabled && !perfDisable.denoise;
260
298
  const outlineEnabled = !perfDisable.outline;
261
299
  const inkEnabled = edges !== 'off';
262
- // The depth+normal MRT feeds both SSGI and the screen-space ink pass.
263
300
  const needsNormalMRT = ssgiEnabled || inkEnabled;
264
301
  // Soft = thin (1px sample radius) + faint (50% opacity); strong = thick
265
302
  // (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%.
303
+ // so radius+opacity are what actually separate the two modes.
269
304
  const inkRadius = 1;
270
305
  const inkOpacity = inkOpacityOverride ?? (edges === 'strong' ? 1 : 0.5);
271
306
  console.log('[viewer/post-processing] Building pipeline', {
@@ -293,7 +328,7 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, disablePostF
293
328
  const hasWebGPU = typeof navigator !== 'undefined' && 'gpu' in navigator;
294
329
  if (!hasWebGPU) {
295
330
  hasPipelineErrorRef.current = true;
296
- renderPipelineRef.current = null;
331
+ resourcesRef.current = null;
297
332
  return;
298
333
  }
299
334
  // Clear outliner arrays synchronously to prevent stale Object3D refs
@@ -303,15 +338,22 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, disablePostF
303
338
  sanitizeOutlineObjects(outliner.hoveredObjects);
304
339
  outliner.selectedObjects.length = 0;
305
340
  outliner.hoveredObjects.length = 0;
341
+ const resources = new PostProcessingResources();
342
+ resourcesRef.current = resources;
306
343
  try {
344
+ const layerIndex = new LayerPassIndex(scene, [ZONE_LAYER, OVERLAY_LAYER]);
345
+ resources.layerIndex = layerIndex;
307
346
  const scenePass = pass(scene, camera);
347
+ resources.passes.push(scenePass);
308
348
  scenePass.setLayers(sceneOnlyLayers);
309
- const zonePass = pass(scene, camera);
349
+ const zonePass = new LayerPassNode(layerIndex, camera, ZONE_LAYER, scenePass);
350
+ resources.passes.push(zonePass);
310
351
  zonePass.setLayers(zoneLayers);
311
352
  // Editor overlays (gizmos, move handles, tool previews, grid) on their own
312
353
  // layer, kept out of the depth/normal MRT above so the ink + SSGI ignore
313
354
  // them, then composited on top of the final image below.
314
- const overlayPass = pass(scene, camera);
355
+ const overlayPass = new LayerPassNode(layerIndex, camera, OVERLAY_LAYER, scenePass);
356
+ resources.passes.push(overlayPass);
315
357
  overlayPass.setLayers(overlayLayers);
316
358
  const overlayColor = overlayPass.getTextureNode('output');
317
359
  const scenePassColor = scenePass.getTextureNode('output');
@@ -325,25 +367,28 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, disablePostF
325
367
  // not SSGI is enabled. When SSGI is on, the branch below overwrites this
326
368
  // with its own zone-inclusive composite (no double-add).
327
369
  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;
370
+ // Scene depth is shared by SSGI, ink, and outlines.
371
+ // The normal MRT is only built when SSGI or ink needs it.
372
+ const scenePassDepth = scenePass.getTextureNode('depth');
331
373
  let scenePassNormal = null;
332
- let sceneNormal = null;
333
374
  if (needsNormalMRT) {
334
375
  scenePass.setMRT(mrt({
335
376
  output,
336
377
  diffuseColor,
337
378
  normal: packNormalToRGB(normalView),
338
379
  }));
339
- scenePassDepth = scenePass.getTextureNode('depth');
340
380
  scenePassNormal = scenePass.getTextureNode('normal');
381
+ }
382
+ if (scenePassNormal) {
341
383
  const normalTexture = scenePass.getTexture('normal');
342
384
  normalTexture.type = UnsignedByteType;
343
- // Extract normal from color-encoded texture (SSGI consumes the node form)
344
- sceneNormal = sample((uv) => unpackRGBToNormal(scenePassNormal.sample(uv)));
345
385
  }
346
- if (ssgiEnabled) {
386
+ // Extract normal from color-encoded texture (SSGI consumes the node form).
387
+ const normalNode = scenePassNormal;
388
+ const sceneNormal = normalNode
389
+ ? sample((uv) => unpackRGBToNormal(normalNode.sample(uv)))
390
+ : null;
391
+ if (ssgiEnabled && scenePassDepth && sceneNormal) {
347
392
  const scenePassDiffuse = scenePass.getTextureNode('diffuseColor');
348
393
  const diffuseTexture = scenePass.getTexture('diffuseColor');
349
394
  diffuseTexture.type = UnsignedByteType;
@@ -367,7 +412,7 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, disablePostF
367
412
  if (denoiseEnabled) {
368
413
  // DenoiseNode only denoises RGB — alpha is passed through unchanged.
369
414
  // 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));
415
+ const aoAsRgb = vec4(aoTexture.r, aoTexture.r, aoTexture.r, tslFloat(1));
371
416
  const denoisePass = denoise(aoAsRgb, scenePassDepth, sceneNormal, camera);
372
417
  denoisePass.index.value = 0;
373
418
  denoisePass.radius.value = 4;
@@ -382,22 +427,22 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, disablePostF
382
427
  // (same ≈150→350 m window as ink-edges' distanceFade) so the horizon
383
428
  // disc and the geometry↔sky depth cliff never grow an AO band — that
384
429
  // 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);
430
+ const aoFarFade = smoothstep(tslFloat(0.9994), tslFloat(0.9998), scenePassDepth.sample(screenUV).r);
431
+ ao = mix(ao, tslFloat(1), aoFarFade);
387
432
  // Composite: scene * AO + diffuse * GI
388
433
  sceneColor = vec4(add(scenePassColor.rgb.mul(ao), add(zonePass.rgb, scenePassDiffuse.rgb.mul(gi))), contentAlpha);
389
434
  }
390
435
  // Screen-space ink outline (SketchUp look) — depth/normal edge detection
391
436
  // over the composited scene. Topology-agnostic, so it handles CSG-cut
392
437
  // walls cleanly. Applied before the selection outline + background mix.
393
- if (inkEnabled) {
438
+ if (inkEnabled && scenePassDepth && scenePassNormal) {
394
439
  sceneColor = vec4(inkedEdges({
395
440
  sceneRgb: sceneColor.rgb,
396
441
  depthTex: scenePassDepth,
397
442
  normalTex: scenePassNormal,
398
443
  inkColor: inkColorUniform.current,
399
444
  radius: inkRadius,
400
- opacity: float(inkOpacity).mul(inkOpacityScaleUniform.current),
445
+ opacity: tslFloat(inkOpacity).mul(inkOpacityScaleUniform.current),
401
446
  }), sceneColor.a);
402
447
  }
403
448
  // Scene-referred grade (contrast around mid-gray + saturation) before the
@@ -409,17 +454,20 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, disablePostF
409
454
  if (shading === 'rendered') {
410
455
  sceneColor = vec4(gradeRgb(sceneColor.rgb), sceneColor.a);
411
456
  }
412
- // Single merged outline node: one shared depth pass for both selected + hovered groups.
457
+ // Reused scene depth lets outlined groups occlude each other; materials
458
+ // with depthWrite=false (including glazing) no longer occlude outlines.
413
459
  const outliner = useViewer.getState().outliner;
414
460
  let compositeWithOutlines = sceneColor;
415
461
  let visualAlpha = contentAlpha;
416
462
  if (outlineEnabled) {
417
463
  const outlineNode = mergedOutline(scene, camera, {
464
+ sceneDepthNode: scenePassDepth,
418
465
  primaryObjects: outliner.selectedObjects,
419
466
  secondaryObjects: outliner.hoveredObjects,
420
467
  primaryEdgeThickness: uniform(1),
421
468
  secondaryEdgeThickness: uniform(1.5),
422
469
  });
470
+ resources.outline = outlineNode;
423
471
  // Selected: white visible, yellow hidden
424
472
  const selectedVisibleColor = uniform(new Color(0xff_ff_ff));
425
473
  const selectedHiddenColor = uniform(new Color(0xf3_ff_47));
@@ -431,7 +479,7 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, disablePostF
431
479
  // Hovered: blue visible, yellow hidden, pulsing
432
480
  const pulsePeriod = uniform(3);
433
481
  const oscillating = oscSine(time.div(pulsePeriod).mul(2)).mul(0.5).add(0.5);
434
- const osc = mix(oscillating, float(1), hoverPulseMix);
482
+ const osc = mix(oscillating, tslFloat(1), hoverPulseMix);
435
483
  const hoverOutline = outlineNode.secondaryVisibleEdge
436
484
  .mul(hoverVisibleColor)
437
485
  .add(outlineNode.secondaryHiddenEdge.mul(hoverHiddenColor))
@@ -448,37 +496,40 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, disablePostF
448
496
  // sky gradient (shared formula in lib/backdrop.ts). The horizon disc
449
497
  // dissolves into the same formula, so backdrop and ground meet
450
498
  // 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);
499
+ const ndc = vec4(screenUV.x.mul(2).sub(1), tslFloat(1).sub(screenUV.y).mul(2).sub(1), 1, 1);
452
500
  const viewRay = camProjInvUniform.current.mul(ndc);
453
501
  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
- });
502
+ let bgGradient = atmosphere
503
+ ? atmosphere.skyRadiance(worldDir)
504
+ : backdropGradient({
505
+ dirY: worldDir.y,
506
+ background: bgUniform.current,
507
+ haze: bgHazeUniform.current,
508
+ sky: bgSkyUniform.current,
509
+ skyDeep: bgSkyDeepUniform.current,
510
+ });
461
511
  if (shading === 'rendered') {
462
512
  bgGradient = gradeRgb(bgGradient);
463
513
  }
464
- const composited = mix(bgGradient, compositeWithOutlines.rgb, contentAlpha);
514
+ const sceneComposite = compositeWithOutlines.rgb;
515
+ const composited = mix(bgGradient, sceneComposite, contentAlpha);
465
516
  // Editor overlays painted on top by their own alpha — they never get inked,
466
517
  // AO'd, or outlined, and always read crisp regardless of scene depth.
467
518
  const withOverlay = mix(composited, overlayColor.rgb, overlayColor.a);
468
- let finalOutput = vec4(withOverlay, float(1));
519
+ let finalOutput = vec4(withOverlay, tslFloat(1));
469
520
  if (transparentBackground) {
470
521
  const overlayAlpha = overlayColor.a;
471
522
  const alpha = overlayAlpha.add(visualAlpha.mul(overlayAlpha.oneMinus()));
472
523
  const straightRgb = overlayColor.rgb
473
524
  .mul(overlayAlpha)
474
- .add(compositeWithOutlines.rgb.mul(visualAlpha).mul(overlayAlpha.oneMinus()))
475
- .div(alpha.max(float(0.00001)));
525
+ .add(sceneComposite.mul(visualAlpha).mul(overlayAlpha.oneMinus()))
526
+ .div(alpha.max(tslFloat(0.00001)));
476
527
  finalOutput = premultiplyAlpha(renderOutput(vec4(straightRgb, alpha)));
477
528
  }
478
529
  const renderPipeline = new RenderPipeline(renderer);
530
+ resources.pipeline = renderPipeline;
479
531
  renderPipeline.outputColorTransform = !transparentBackground;
480
532
  renderPipeline.outputNode = finalOutput;
481
- renderPipelineRef.current = renderPipeline;
482
533
  retryCountRef.current = 0;
483
534
  }
484
535
  catch (error) {
@@ -488,24 +539,18 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, disablePostF
488
539
  ssgi: SSGI_PARAMS.enabled,
489
540
  rendererCtor: renderer.constructor?.name,
490
541
  }, error);
491
- if (renderPipelineRef.current) {
492
- renderPipelineRef.current.dispose();
493
- }
494
- renderPipelineRef.current = null;
542
+ disposePipeline();
495
543
  }
496
- return () => {
497
- if (renderPipelineRef.current) {
498
- renderPipelineRef.current.dispose();
499
- }
500
- renderPipelineRef.current = null;
501
- };
544
+ return disposePipeline;
502
545
  }, [
503
546
  // NOTE: hoverHighlightMode intentionally excluded — the hover style is
504
547
  // pushed to uniforms in a separate effect, so a hover must NOT rebuild the
505
548
  // whole pipeline. The uniform refs below are stable (useMemo), so they
506
549
  // never trigger a rebuild either.
550
+ atmosphere,
507
551
  camera,
508
552
  disablePostFx,
553
+ disposePipeline,
509
554
  hoverHiddenColor,
510
555
  hoverPulseMix,
511
556
  hoverStrength,
@@ -572,7 +617,10 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, disablePostF
572
617
  if (disablePostFx ||
573
618
  PERF_POST_FX_DISABLED ||
574
619
  hasPipelineErrorRef.current ||
575
- !renderPipelineRef.current) {
620
+ !resourcesRef.current?.pipeline) {
621
+ const previousBackgroundNode = scene.backgroundNode;
622
+ if (directSkyNode && !transparentBackground)
623
+ scene.backgroundNode = directSkyNode;
576
624
  try {
577
625
  const clearAlpha = transparentBackground ? 0 : 1;
578
626
  if (renderer.setClearColor) {
@@ -584,37 +632,33 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, disablePostF
584
632
  renderer.setClearAlpha(clearAlpha);
585
633
  }
586
634
  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
- }
635
+ timeSpan('render-encode', () => {
636
+ ;
637
+ renderer.render(scene, camera);
638
+ });
639
+ if (PERF_OVERLAY_ENABLED)
640
+ recordFrameGpuTiming(renderer, submittedAt);
594
641
  }
595
642
  catch (fallbackError) {
596
643
  console.error('[viewer/post-processing] Fallback render failed.', fallbackError);
597
644
  }
645
+ finally {
646
+ scene.backgroundNode = previousBackgroundNode;
647
+ }
598
648
  return;
599
649
  }
650
+ const pipeline = resourcesRef.current.pipeline;
600
651
  try {
601
652
  // Clear alpha=0 so background pixels in the output MRT attachment (index 0) get a=0,
602
653
  // making scenePassColor.a a reliable geometry mask (geometry pixels write a=1 via output node).
603
654
  ;
604
655
  renderer.setClearAlpha(0);
605
656
  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
- }
657
+ timeSpan('render-encode', () => {
658
+ pipeline.render();
659
+ });
660
+ if (PERF_OVERLAY_ENABLED)
661
+ recordFrameGpuTiming(renderer, submittedAt);
618
662
  }
619
663
  catch (error) {
620
664
  hasPipelineErrorRef.current = true;
@@ -624,10 +668,7 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, disablePostF
624
668
  rendererCtor: renderer.constructor?.name,
625
669
  error,
626
670
  });
627
- if (renderPipelineRef.current) {
628
- renderPipelineRef.current.dispose();
629
- }
630
- renderPipelineRef.current = null;
671
+ disposePipeline();
631
672
  if (retryCountRef.current < MAX_PIPELINE_RETRIES) {
632
673
  // Auto-retry: schedule a pipeline rebuild if we haven't exceeded the retry limit
633
674
  retryCountRef.current++;
@@ -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"}