@pascal-app/viewer 0.8.0 → 0.9.2

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 (284) hide show
  1. package/dist/components/error-boundary.d.ts +6 -0
  2. package/dist/components/error-boundary.d.ts.map +1 -1
  3. package/dist/components/error-boundary.js +6 -0
  4. package/dist/components/renderers/node-renderer.d.ts +6 -2
  5. package/dist/components/renderers/node-renderer.d.ts.map +1 -1
  6. package/dist/components/renderers/node-renderer.js +41 -22
  7. package/dist/components/renderers/parametric-node-renderer.d.ts +5 -0
  8. package/dist/components/renderers/parametric-node-renderer.d.ts.map +1 -0
  9. package/dist/components/renderers/parametric-node-renderer.js +37 -0
  10. package/dist/components/renderers/scene-renderer.d.ts +1 -1
  11. package/dist/components/renderers/scene-renderer.d.ts.map +1 -1
  12. package/dist/components/viewer/bvh-ecctrl.d.ts +77 -0
  13. package/dist/components/viewer/bvh-ecctrl.d.ts.map +1 -0
  14. package/dist/components/viewer/bvh-ecctrl.js +552 -0
  15. package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
  16. package/dist/components/viewer/frame-limiter.js +39 -4
  17. package/dist/components/viewer/glb-interactive.d.ts +41 -0
  18. package/dist/components/viewer/glb-interactive.d.ts.map +1 -0
  19. package/dist/components/viewer/glb-interactive.js +394 -0
  20. package/dist/components/viewer/glb-reference-nodes.d.ts +29 -0
  21. package/dist/components/viewer/glb-reference-nodes.d.ts.map +1 -0
  22. package/dist/components/viewer/glb-reference-nodes.js +80 -0
  23. package/dist/components/viewer/glb-replace-instances.d.ts +18 -0
  24. package/dist/components/viewer/glb-replace-instances.d.ts.map +1 -0
  25. package/dist/components/viewer/glb-replace-instances.js +61 -0
  26. package/dist/components/viewer/glb-scene.d.ts +54 -0
  27. package/dist/components/viewer/glb-scene.d.ts.map +1 -0
  28. package/dist/components/viewer/glb-scene.js +920 -0
  29. package/dist/components/viewer/glb-walkthrough-controller.d.ts +12 -0
  30. package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -0
  31. package/dist/components/viewer/glb-walkthrough-controller.js +357 -0
  32. package/dist/components/viewer/index.d.ts +56 -3
  33. package/dist/components/viewer/index.d.ts.map +1 -1
  34. package/dist/components/viewer/index.js +280 -65
  35. package/dist/components/viewer/lights.d.ts +1 -1
  36. package/dist/components/viewer/lights.d.ts.map +1 -1
  37. package/dist/components/viewer/lights.js +201 -43
  38. package/dist/components/viewer/perf-monitor.d.ts +1 -1
  39. package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
  40. package/dist/components/viewer/perf-monitor.js +115 -12
  41. package/dist/components/viewer/post-processing.d.ts +7 -1
  42. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  43. package/dist/components/viewer/post-processing.js +395 -63
  44. package/dist/components/viewer/registered-systems.d.ts +12 -0
  45. package/dist/components/viewer/registered-systems.d.ts.map +1 -0
  46. package/dist/components/viewer/registered-systems.js +49 -0
  47. package/dist/components/viewer/scene-bvh.d.ts +18 -0
  48. package/dist/components/viewer/scene-bvh.d.ts.map +1 -0
  49. package/dist/components/viewer/scene-bvh.js +97 -0
  50. package/dist/components/viewer/scene-bvh.test.d.ts +2 -0
  51. package/dist/components/viewer/scene-bvh.test.d.ts.map +1 -0
  52. package/dist/components/viewer/scene-bvh.test.js +13 -0
  53. package/dist/components/viewer/scene-environment.d.ts +2 -0
  54. package/dist/components/viewer/scene-environment.d.ts.map +1 -0
  55. package/dist/components/viewer/scene-environment.js +79 -0
  56. package/dist/components/viewer/selection-manager.d.ts +1 -1
  57. package/dist/components/viewer/selection-manager.d.ts.map +1 -1
  58. package/dist/components/viewer/selection-manager.js +37 -14
  59. package/dist/components/viewer/viewer-camera.d.ts +1 -1
  60. package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
  61. package/dist/components/viewer/walkthrough-controls.d.ts +2 -1
  62. package/dist/components/viewer/walkthrough-controls.d.ts.map +1 -1
  63. package/dist/components/viewer/walkthrough-controls.js +20 -0
  64. package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
  65. package/dist/hooks/use-gltf-ktx2.js +3 -22
  66. package/dist/hooks/use-node-events.d.ts +6 -74
  67. package/dist/hooks/use-node-events.d.ts.map +1 -1
  68. package/dist/hooks/use-node-events.js +28 -8
  69. package/dist/index.d.ts +48 -5
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.js +82 -4
  72. package/dist/lib/backdrop.d.ts +33 -0
  73. package/dist/lib/backdrop.d.ts.map +1 -0
  74. package/dist/lib/backdrop.js +92 -0
  75. package/dist/lib/box-uv.d.ts +13 -0
  76. package/dist/lib/box-uv.d.ts.map +1 -0
  77. package/dist/lib/box-uv.js +32 -0
  78. package/dist/lib/csg-utils.d.ts +10 -0
  79. package/dist/lib/csg-utils.d.ts.map +1 -0
  80. package/dist/lib/csg-utils.js +93 -0
  81. package/dist/lib/csg-utils.test.d.ts +2 -0
  82. package/dist/lib/csg-utils.test.d.ts.map +1 -0
  83. package/dist/lib/csg-utils.test.js +45 -0
  84. package/dist/lib/drawable-geometry.d.ts +21 -0
  85. package/dist/lib/drawable-geometry.d.ts.map +1 -0
  86. package/dist/lib/drawable-geometry.js +22 -0
  87. package/dist/lib/edge-style.d.ts +4 -0
  88. package/dist/lib/edge-style.d.ts.map +1 -0
  89. package/dist/lib/edge-style.js +31 -0
  90. package/dist/lib/gpu-perf.d.ts +4 -0
  91. package/dist/lib/gpu-perf.d.ts.map +1 -0
  92. package/dist/lib/gpu-perf.js +23 -0
  93. package/dist/lib/ink-edges.d.ts +9 -0
  94. package/dist/lib/ink-edges.d.ts.map +1 -0
  95. package/dist/lib/ink-edges.js +49 -0
  96. package/dist/lib/isolation.d.ts +39 -0
  97. package/dist/lib/isolation.d.ts.map +1 -0
  98. package/dist/lib/isolation.js +103 -0
  99. package/dist/lib/ktx2-loader.d.ts +43 -0
  100. package/dist/lib/ktx2-loader.d.ts.map +1 -0
  101. package/dist/lib/ktx2-loader.js +136 -0
  102. package/dist/lib/layers.d.ts +28 -0
  103. package/dist/lib/layers.d.ts.map +1 -1
  104. package/dist/lib/layers.js +28 -0
  105. package/dist/lib/materials.d.ts +38 -15
  106. package/dist/lib/materials.d.ts.map +1 -1
  107. package/dist/lib/materials.js +364 -72
  108. package/dist/lib/merged-outline-node.d.ts.map +1 -1
  109. package/dist/lib/merged-outline-node.js +7 -0
  110. package/dist/lib/polygon-union.d.ts +4 -0
  111. package/dist/lib/polygon-union.d.ts.map +1 -0
  112. package/dist/lib/polygon-union.js +289 -0
  113. package/dist/lib/polygon-union.test.d.ts +2 -0
  114. package/dist/lib/polygon-union.test.d.ts.map +1 -0
  115. package/dist/lib/polygon-union.test.js +105 -0
  116. package/dist/lib/scene-themes.d.ts +30 -0
  117. package/dist/lib/scene-themes.d.ts.map +1 -0
  118. package/dist/lib/scene-themes.js +202 -0
  119. package/dist/lib/shadow-only.d.ts +4 -0
  120. package/dist/lib/shadow-only.d.ts.map +1 -0
  121. package/dist/lib/shadow-only.js +36 -0
  122. package/dist/lib/tsl-compat.d.ts +12 -0
  123. package/dist/lib/tsl-compat.d.ts.map +1 -0
  124. package/dist/lib/tsl-compat.js +13 -0
  125. package/dist/store/use-viewer.d.ts +92 -12
  126. package/dist/store/use-viewer.d.ts.map +1 -1
  127. package/dist/store/use-viewer.js +201 -6
  128. package/dist/store/use-viewer.test.d.ts +2 -0
  129. package/dist/store/use-viewer.test.d.ts.map +1 -0
  130. package/dist/store/use-viewer.test.js +42 -0
  131. package/dist/systems/ceiling/ceiling-system.d.ts +6 -2
  132. package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
  133. package/dist/systems/ceiling/ceiling-system.js +66 -13
  134. package/dist/systems/door/door-animation.test.d.ts +2 -0
  135. package/dist/systems/door/door-animation.test.d.ts.map +1 -0
  136. package/dist/systems/door/door-animation.test.js +51 -0
  137. package/dist/systems/door/door-floor-alignment.test.d.ts +2 -0
  138. package/dist/systems/door/door-floor-alignment.test.d.ts.map +1 -0
  139. package/dist/systems/door/door-floor-alignment.test.js +50 -0
  140. package/dist/systems/door/door-system.d.ts +24 -0
  141. package/dist/systems/door/door-system.d.ts.map +1 -1
  142. package/dist/systems/door/door-system.js +864 -180
  143. package/dist/systems/elevator/elevator-interaction-system.d.ts +2 -0
  144. package/dist/systems/elevator/elevator-interaction-system.d.ts.map +1 -0
  145. package/dist/systems/elevator/elevator-interaction-system.js +62 -0
  146. package/dist/systems/fence/fence-system.d.ts +20 -0
  147. package/dist/systems/fence/fence-system.d.ts.map +1 -1
  148. package/dist/systems/fence/fence-system.js +229 -57
  149. package/dist/systems/floor-elevation/floor-elevation-system.d.ts +20 -0
  150. package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -0
  151. package/dist/systems/floor-elevation/floor-elevation-system.js +78 -0
  152. package/dist/systems/geometry/geometry-system.d.ts +41 -0
  153. package/dist/systems/geometry/geometry-system.d.ts.map +1 -0
  154. package/dist/systems/geometry/geometry-system.js +348 -0
  155. package/dist/systems/geometry/geometry-system.test.d.ts +2 -0
  156. package/dist/systems/geometry/geometry-system.test.d.ts.map +1 -0
  157. package/dist/systems/geometry/geometry-system.test.js +30 -0
  158. package/dist/systems/interactive/control-widget.d.ts +10 -0
  159. package/dist/systems/interactive/control-widget.d.ts.map +1 -0
  160. package/dist/systems/interactive/control-widget.js +35 -0
  161. package/dist/systems/interactive/interactive-system.d.ts +1 -1
  162. package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
  163. package/dist/systems/interactive/interactive-system.js +55 -56
  164. package/dist/systems/item/item-system.d.ts +7 -0
  165. package/dist/systems/item/item-system.d.ts.map +1 -1
  166. package/dist/systems/item/item-system.js +28 -17
  167. package/dist/systems/item-light/item-light-system.d.ts +1 -1
  168. package/dist/systems/item-light/item-light-system.d.ts.map +1 -1
  169. package/dist/systems/item-light/item-light-system.js +18 -5
  170. package/dist/systems/level/level-system.d.ts.map +1 -1
  171. package/dist/systems/level/level-system.js +28 -4
  172. package/dist/systems/level/level-utils.d.ts +0 -3
  173. package/dist/systems/level/level-utils.d.ts.map +1 -1
  174. package/dist/systems/level/level-utils.js +2 -41
  175. package/dist/systems/roof/roof-materials.d.ts +3 -2
  176. package/dist/systems/roof/roof-materials.d.ts.map +1 -1
  177. package/dist/systems/roof/roof-materials.js +59 -14
  178. package/dist/systems/roof/roof-system.d.ts +46 -6
  179. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  180. package/dist/systems/roof/roof-system.js +1365 -170
  181. package/dist/systems/slab/slab-system.d.ts +5 -3
  182. package/dist/systems/slab/slab-system.d.ts.map +1 -1
  183. package/dist/systems/slab/slab-system.js +204 -68
  184. package/dist/systems/slab/slab-system.test.d.ts +2 -0
  185. package/dist/systems/slab/slab-system.test.d.ts.map +1 -0
  186. package/dist/systems/slab/slab-system.test.js +63 -0
  187. package/dist/systems/stair/stair-materials.d.ts +4 -3
  188. package/dist/systems/stair/stair-materials.d.ts.map +1 -1
  189. package/dist/systems/stair/stair-materials.js +30 -15
  190. package/dist/systems/stair/stair-system.d.ts.map +1 -1
  191. package/dist/systems/stair/stair-system.js +55 -63
  192. package/dist/systems/surface-hole-geometry.d.ts +3 -0
  193. package/dist/systems/surface-hole-geometry.d.ts.map +1 -0
  194. package/dist/systems/surface-hole-geometry.js +4 -0
  195. package/dist/systems/wall/opening-cutout-geometry.d.ts +31 -0
  196. package/dist/systems/wall/opening-cutout-geometry.d.ts.map +1 -0
  197. package/dist/systems/wall/opening-cutout-geometry.js +167 -0
  198. package/dist/systems/wall/opening-cutout-geometry.test.d.ts +2 -0
  199. package/dist/systems/wall/opening-cutout-geometry.test.d.ts.map +1 -0
  200. package/dist/systems/wall/opening-cutout-geometry.test.js +172 -0
  201. package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
  202. package/dist/systems/wall/wall-cutout.js +62 -17
  203. package/dist/systems/wall/wall-materials.d.ts +12 -7
  204. package/dist/systems/wall/wall-materials.d.ts.map +1 -1
  205. package/dist/systems/wall/wall-materials.js +279 -46
  206. package/dist/systems/wall/wall-opening-cutout.test.d.ts +2 -0
  207. package/dist/systems/wall/wall-opening-cutout.test.d.ts.map +1 -0
  208. package/dist/systems/wall/wall-opening-cutout.test.js +59 -0
  209. package/dist/systems/wall/wall-support-extension.test.d.ts +2 -0
  210. package/dist/systems/wall/wall-support-extension.test.d.ts.map +1 -0
  211. package/dist/systems/wall/wall-support-extension.test.js +48 -0
  212. package/dist/systems/wall/wall-system.d.ts +2 -2
  213. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  214. package/dist/systems/wall/wall-system.js +493 -193
  215. package/dist/systems/window/window-animation-system.d.ts +12 -0
  216. package/dist/systems/window/window-animation-system.d.ts.map +1 -1
  217. package/dist/systems/window/window-animation-system.js +16 -5
  218. package/dist/systems/window/window-system.d.ts +7 -0
  219. package/dist/systems/window/window-system.d.ts.map +1 -1
  220. package/dist/systems/window/window-system.js +308 -18
  221. package/package.json +5 -6
  222. package/dist/components/renderers/building/building-renderer.d.ts +0 -5
  223. package/dist/components/renderers/building/building-renderer.d.ts.map +0 -1
  224. package/dist/components/renderers/building/building-renderer.js +0 -11
  225. package/dist/components/renderers/ceiling/ceiling-renderer.d.ts +0 -5
  226. package/dist/components/renderers/ceiling/ceiling-renderer.d.ts.map +0 -1
  227. package/dist/components/renderers/ceiling/ceiling-renderer.js +0 -58
  228. package/dist/components/renderers/column/column-renderer.d.ts +0 -5
  229. package/dist/components/renderers/column/column-renderer.d.ts.map +0 -1
  230. package/dist/components/renderers/column/column-renderer.js +0 -597
  231. package/dist/components/renderers/door/door-renderer.d.ts +0 -5
  232. package/dist/components/renderers/door/door-renderer.d.ts.map +0 -1
  233. package/dist/components/renderers/door/door-renderer.js +0 -16
  234. package/dist/components/renderers/fence/fence-renderer.d.ts +0 -5
  235. package/dist/components/renderers/fence/fence-renderer.d.ts.map +0 -1
  236. package/dist/components/renderers/fence/fence-renderer.js +0 -15
  237. package/dist/components/renderers/guide/guide-renderer.d.ts +0 -5
  238. package/dist/components/renderers/guide/guide-renderer.d.ts.map +0 -1
  239. package/dist/components/renderers/guide/guide-renderer.js +0 -38
  240. package/dist/components/renderers/item/item-renderer.d.ts +0 -5
  241. package/dist/components/renderers/item/item-renderer.d.ts.map +0 -1
  242. package/dist/components/renderers/item/item-renderer.js +0 -172
  243. package/dist/components/renderers/level/level-renderer.d.ts +0 -5
  244. package/dist/components/renderers/level/level-renderer.d.ts.map +0 -1
  245. package/dist/components/renderers/level/level-renderer.js +0 -11
  246. package/dist/components/renderers/roof/roof-materials.d.ts +0 -4
  247. package/dist/components/renderers/roof/roof-materials.d.ts.map +0 -1
  248. package/dist/components/renderers/roof/roof-materials.js +0 -16
  249. package/dist/components/renderers/roof/roof-renderer.d.ts +0 -5
  250. package/dist/components/renderers/roof/roof-renderer.d.ts.map +0 -1
  251. package/dist/components/renderers/roof/roof-renderer.js +0 -32
  252. package/dist/components/renderers/roof-segment/roof-segment-renderer.d.ts +0 -5
  253. package/dist/components/renderers/roof-segment/roof-segment-renderer.d.ts.map +0 -1
  254. package/dist/components/renderers/roof-segment/roof-segment-renderer.js +0 -40
  255. package/dist/components/renderers/scan/scan-renderer.d.ts +0 -5
  256. package/dist/components/renderers/scan/scan-renderer.d.ts.map +0 -1
  257. package/dist/components/renderers/scan/scan-renderer.js +0 -54
  258. package/dist/components/renderers/site/site-renderer.d.ts +0 -5
  259. package/dist/components/renderers/site/site-renderer.d.ts.map +0 -1
  260. package/dist/components/renderers/site/site-renderer.js +0 -106
  261. package/dist/components/renderers/slab/slab-renderer.d.ts +0 -5
  262. package/dist/components/renderers/slab/slab-renderer.d.ts.map +0 -1
  263. package/dist/components/renderers/slab/slab-renderer.js +0 -58
  264. package/dist/components/renderers/spawn/spawn-renderer.d.ts +0 -5
  265. package/dist/components/renderers/spawn/spawn-renderer.d.ts.map +0 -1
  266. package/dist/components/renderers/spawn/spawn-renderer.js +0 -32
  267. package/dist/components/renderers/stair/stair-renderer.d.ts +0 -5
  268. package/dist/components/renderers/stair/stair-renderer.d.ts.map +0 -1
  269. package/dist/components/renderers/stair/stair-renderer.js +0 -603
  270. package/dist/components/renderers/stair-segment/stair-segment-renderer.d.ts +0 -5
  271. package/dist/components/renderers/stair-segment/stair-segment-renderer.d.ts.map +0 -1
  272. package/dist/components/renderers/stair-segment/stair-segment-renderer.js +0 -51
  273. package/dist/components/renderers/wall/wall-renderer.d.ts +0 -5
  274. package/dist/components/renderers/wall/wall-renderer.d.ts.map +0 -1
  275. package/dist/components/renderers/wall/wall-renderer.js +0 -16
  276. package/dist/components/renderers/window/window-renderer.d.ts +0 -5
  277. package/dist/components/renderers/window/window-renderer.d.ts.map +0 -1
  278. package/dist/components/renderers/window/window-renderer.js +0 -21
  279. package/dist/components/renderers/zone/zone-renderer.d.ts +0 -5
  280. package/dist/components/renderers/zone/zone-renderer.d.ts.map +0 -1
  281. package/dist/components/renderers/zone/zone-renderer.js +0 -181
  282. package/dist/components/viewer/ground-occluder.d.ts +0 -2
  283. package/dist/components/viewer/ground-occluder.d.ts.map +0 -1
  284. package/dist/components/viewer/ground-occluder.js +0 -79
@@ -1,13 +1,26 @@
1
1
  import { useFrame, useThree } from '@react-three/fiber';
2
2
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
- import { Color, Layers, UnsignedByteType } from 'three';
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, colorToDirection, diffuseColor, directionToColor, float, mix, mrt, normalView, oscSine, output, pass, sample, time, uniform, vec4, } from 'three/tsl';
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
7
  import { RenderPipeline } from 'three/webgpu';
8
- import { SCENE_LAYER, ZONE_LAYER } from '../../lib/layers';
8
+ import { backdropGradient, deepSkyColor, horizonHazeColor } from '../../lib/backdrop';
9
+ import { edgeColorFor, edgeOpacityScaleFor } from '../../lib/edge-style';
10
+ import { PERF_OVERLAY_ENABLED, pushGpuSample } from '../../lib/gpu-perf';
11
+ import { inkedEdges } from '../../lib/ink-edges';
12
+ import { GRID_LAYER, OVERLAY_LAYER, SCENE_LAYER, ZONE_LAYER } from '../../lib/layers';
9
13
  import { mergedOutline } from '../../lib/merged-outline-node';
14
+ import { getSceneTheme } from '../../lib/scene-themes';
15
+ import { packNormalToRGB, unpackRGBToNormal } from '../../lib/tsl-compat';
10
16
  import useViewer from '../../store/use-viewer';
17
+ // Scene-referred grade applied before the output tone mapping (AgX). AgX rolls
18
+ // highlights off gently but reads flat on its own; a mild mid-gray-pivot
19
+ // contrast + saturation lift restores the punch. Rendered shading only.
20
+ export const GRADE_PARAMS = {
21
+ contrast: 1.05,
22
+ saturation: 1.1,
23
+ };
11
24
  // SSGI Parameters - adjust these to fine-tune global illumination and ambient occlusion
12
25
  export const SSGI_PARAMS = {
13
26
  enabled: true,
@@ -23,10 +36,49 @@ export const SSGI_PARAMS = {
23
36
  useScreenSpaceSampling: true,
24
37
  useTemporalFiltering: false,
25
38
  };
39
+ // Diagnostic toggles for thermal A/B testing. Add `?disable=ao,denoise,outline,postFx`
40
+ // to the URL (any subset) and reload to skip those passes. Each flag prevents
41
+ // allocation + per-frame work for that stage, so device temperature deltas
42
+ // across combos isolate which pass is the actual culprit. Picked up once at
43
+ // pipeline build; reload after changing the URL.
44
+ // - ao: skip SSGI entirely (and denoise, since denoise has nothing to denoise)
45
+ // - denoise: keep SSGI but feed its raw noisy AO straight to the composite
46
+ // - outline: skip the merged-outline node and its 14 internal RTs
47
+ // - postFx: bypass the whole RenderPipeline and use renderer.render(scene, camera)
48
+ // directly — isolates raw scene-render cost from any post-FX overhead
49
+ // - draw: skip the render call entirely — frames still tick (useFrame
50
+ // systems, scene-ready) but no draw is ever submitted. For
51
+ // consumers that only need the built scene graph, never pixels:
52
+ // the headless bake worker renders on SwiftShader (CPU), where
53
+ // per-frame vertex/draw cost dominates the whole capture.
54
+ function readPerfDisableFlags() {
55
+ if (typeof window === 'undefined') {
56
+ return { ao: false, denoise: false, outline: false, postFx: false };
57
+ }
58
+ const raw = new URLSearchParams(window.location.search).get('disable') ?? '';
59
+ const set = new Set(raw
60
+ .split(',')
61
+ .map((s) => s.trim())
62
+ .filter(Boolean));
63
+ return {
64
+ ao: set.has('ao'),
65
+ denoise: set.has('denoise'),
66
+ outline: set.has('outline'),
67
+ postFx: set.has('postFx'),
68
+ };
69
+ }
70
+ const PERF_POST_FX_DISABLED = typeof window !== 'undefined' &&
71
+ new Set((new URLSearchParams(window.location.search).get('disable') ?? '')
72
+ .split(',')
73
+ .map((s) => s.trim())).has('postFx');
74
+ const PERF_DRAW_DISABLED = typeof window !== 'undefined' &&
75
+ new Set((new URLSearchParams(window.location.search).get('disable') ?? '')
76
+ .split(',')
77
+ .map((s) => s.trim())).has('draw');
78
+ // Stand-in scene for `?disable=draw` frames — cleared, never populated.
79
+ const emptyScene = new Scene();
26
80
  const MAX_PIPELINE_RETRIES = 3;
27
81
  const RETRY_DELAY_MS = 500;
28
- const DARK_BG = '#1f2433';
29
- const LIGHT_BG = '#ffffff';
30
82
  const DEFAULT_HOVER_STYLE = {
31
83
  visibleColor: 0x00_aa_ff,
32
84
  hiddenColor: 0xf3_ff_47,
@@ -47,24 +99,69 @@ function sanitizeOutlineObjects(objects) {
47
99
  }
48
100
  objects.length = nextIndex;
49
101
  }
50
- const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, }) => {
51
- const { gl: renderer, invalidate, scene, camera } = useThree();
102
+ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, disablePostFx = false, }) => {
103
+ const { gl: renderer, invalidate, scene, camera, size } = useThree();
52
104
  const renderPipelineRef = useRef(null);
53
105
  const hasPipelineErrorRef = useRef(false);
54
106
  const retryCountRef = useRef(0);
55
107
  const rebuildTimeoutRef = useRef(null);
108
+ const skippedZeroSizeRef = useRef(false);
56
109
  // Background color uniform — updated every frame via lerp, read by the TSL pipeline.
57
- // Initialised from the current theme so there's no flash on first render.
58
- const initBg = useViewer.getState().theme === 'dark' ? DARK_BG : LIGHT_BG;
110
+ // Initialised from the current scene theme so there's no flash on first render.
111
+ const initTheme = getSceneTheme(useViewer.getState().sceneTheme);
112
+ const initBg = initTheme.background;
59
113
  const bgUniform = useRef(uniform(new Color(initBg)));
60
114
  const bgCurrent = useRef(new Color(initBg));
61
115
  const bgTarget = useRef(new Color());
116
+ // Zenith colour of the backdrop gradient (falls back to the flat background).
117
+ const initSky = initTheme.backgroundSky ?? initBg;
118
+ const bgSkyUniform = useRef(uniform(new Color(initSky)));
119
+ const bgSkyCurrent = useRef(new Color(initSky));
120
+ const bgSkyTarget = useRef(new Color());
121
+ // Horizon haze band + deep zenith (derived — see lib/backdrop.ts).
122
+ const initHaze = horizonHazeColor(initSky, initTheme.appearance);
123
+ const bgHazeUniform = useRef(uniform(new Color(initHaze)));
124
+ const bgHazeCurrent = useRef(new Color(initHaze));
125
+ const bgHazeTarget = useRef(new Color());
126
+ const initSkyDeep = deepSkyColor(initSky);
127
+ const bgSkyDeepUniform = useRef(uniform(new Color(initSkyDeep)));
128
+ const bgSkyDeepCurrent = useRef(new Color(initSkyDeep));
129
+ const bgSkyDeepTarget = useRef(new Color());
130
+ // Scene-camera matrices for the backdrop: the pipeline's fullscreen quad has
131
+ // its own camera, so the sky gradient reconstructs each pixel's world-space
132
+ // view ray from these to find the true horizon (dir.y = 0).
133
+ const camProjInvUniform = useRef(uniform(new Matrix4()));
134
+ const camWorldUniform = useRef(uniform(new Matrix4()));
135
+ // Ink-line colour follows the scene-theme background luminance (dark lines on
136
+ // light scenes, light on dark), refreshed each frame like the background.
137
+ // Dark scenes also scale the ink opacity down (see edge-style.ts).
138
+ const inkColorUniform = useRef(uniform(new Color(edgeColorFor(initBg))));
139
+ const inkOpacityScaleUniform = useRef(uniform(edgeOpacityScaleFor(initBg)));
62
140
  const zoneLayers = useMemo(() => {
63
141
  const l = new Layers();
64
142
  l.enable(ZONE_LAYER);
65
143
  l.disable(SCENE_LAYER);
66
144
  return l;
67
145
  }, []);
146
+ // Scene pass renders the main geometry layer plus the grid. The default camera
147
+ // mask also has the overlay layer enabled (custom controls enable it for
148
+ // picking), so without this the gizmos/handles/tool previews land in the
149
+ // depth+normal MRT and get inked / AO'd as if they were geometry. The grid is
150
+ // kept in here (not the overlay pass) so scene geometry depth-occludes it; it's
151
+ // a flat, depth-non-writing plane so the ink never picks it up.
152
+ const sceneOnlyLayers = useMemo(() => {
153
+ const l = new Layers();
154
+ l.set(SCENE_LAYER);
155
+ l.enable(GRID_LAYER);
156
+ return l;
157
+ }, []);
158
+ // Editor overlays render in their own pass, composited on top after the ink
159
+ // and outlines so they read as crisp UI rather than scene geometry.
160
+ const overlayLayers = useMemo(() => {
161
+ const l = new Layers();
162
+ l.set(OVERLAY_LAYER);
163
+ return l;
164
+ }, []);
68
165
  const hoverHighlightMode = useViewer((s) => s.hoverHighlightMode);
69
166
  const hoverVisibleColor = useMemo(() => uniform(new Color(DEFAULT_HOVER_STYLE.visibleColor)), []);
70
167
  const hoverHiddenColor = useMemo(() => uniform(new Color(DEFAULT_HOVER_STYLE.hiddenColor)), []);
@@ -72,6 +169,11 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, }) => {
72
169
  const hoverPulseMix = useMemo(() => uniform(DEFAULT_HOVER_STYLE.pulse ? 0 : 1), []);
73
170
  // Subscribe to projectId so the pipeline rebuilds on project switch
74
171
  const projectId = useViewer((s) => s.projectId);
172
+ const shading = useViewer((s) => s.shading);
173
+ const edges = useViewer((s) => s.edges);
174
+ const inkOpacityOverride = useViewer((s) => s.inkOpacity);
175
+ const transparentBackground = useViewer((s) => s.transparentBackground);
176
+ const lastProjectIdRef = useRef(projectId);
75
177
  // Bump this to force a pipeline rebuild (used by retry logic)
76
178
  const [pipelineVersion, setPipelineVersion] = useState(0);
77
179
  const requestPipelineRebuild = useCallback(() => {
@@ -83,6 +185,9 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, }) => {
83
185
  }, []);
84
186
  // Reset retry state when project changes
85
187
  useEffect(() => {
188
+ if (lastProjectIdRef.current === projectId)
189
+ return;
190
+ lastProjectIdRef.current = projectId;
86
191
  retryCountRef.current = 0;
87
192
  if (rebuildTimeoutRef.current !== null) {
88
193
  clearTimeout(rebuildTimeoutRef.current);
@@ -115,6 +220,8 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, }) => {
115
220
  ]);
116
221
  // Build / rebuild the post-processing pipeline
117
222
  useEffect(() => {
223
+ const width = Math.floor(size.width);
224
+ const height = Math.floor(size.height);
118
225
  if (!(renderer && scene && camera)) {
119
226
  console.warn('[viewer/post-processing] Skipping pipeline build — missing dependency.', {
120
227
  hasRenderer: !!renderer,
@@ -123,14 +230,72 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, }) => {
123
230
  });
124
231
  return;
125
232
  }
233
+ if (width < 1 || height < 1) {
234
+ skippedZeroSizeRef.current = true;
235
+ hasPipelineErrorRef.current = false;
236
+ if (renderPipelineRef.current) {
237
+ renderPipelineRef.current.dispose();
238
+ }
239
+ renderPipelineRef.current = null;
240
+ return;
241
+ }
242
+ if (skippedZeroSizeRef.current) {
243
+ skippedZeroSizeRef.current = false;
244
+ }
245
+ const perfDisable = readPerfDisableFlags();
246
+ // postFx off (host prop or ?disable=postFx): never allocate the pipeline —
247
+ // useFrame's null-pipeline branch direct-renders. Before this check the
248
+ // URL flag only skipped the pipeline at render time; the build still
249
+ // allocated every pass.
250
+ if (disablePostFx || perfDisable.postFx) {
251
+ hasPipelineErrorRef.current = false;
252
+ if (renderPipelineRef.current) {
253
+ renderPipelineRef.current.dispose();
254
+ }
255
+ renderPipelineRef.current = null;
256
+ return;
257
+ }
258
+ const ssgiEnabled = shading === 'rendered' && SSGI_PARAMS.enabled && !perfDisable.ao;
259
+ const denoiseEnabled = ssgiEnabled && !perfDisable.denoise;
260
+ const outlineEnabled = !perfDisable.outline;
261
+ const inkEnabled = edges !== 'off';
262
+ // The depth+normal MRT feeds both SSGI and the screen-space ink pass.
263
+ const needsNormalMRT = ssgiEnabled || inkEnabled;
264
+ // Soft = thin (1px sample radius) + faint (50% opacity); strong = thick
265
+ // (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%.
269
+ const inkRadius = 1;
270
+ const inkOpacity = inkOpacityOverride ?? (edges === 'strong' ? 1 : 0.5);
126
271
  console.log('[viewer/post-processing] Building pipeline', {
127
272
  version: pipelineVersion,
128
- ssgi: SSGI_PARAMS.enabled,
129
- hoverHighlightMode,
273
+ ssgi: ssgiEnabled,
274
+ denoise: denoiseEnabled,
275
+ outline: outlineEnabled,
276
+ perfDisable,
130
277
  projectId,
278
+ shading,
279
+ transparentBackground,
131
280
  rendererCtor: renderer.constructor?.name,
281
+ width,
282
+ height,
132
283
  });
133
284
  hasPipelineErrorRef.current = false;
285
+ // WebGPU availability check: SSGI, denoise, and RenderPipeline are all
286
+ // WebGPU-only APIs. When the browser falls back to WebGL2 (no
287
+ // `navigator.gpu`, or the device couldn't be created), building the
288
+ // pipeline either throws silently or produces a broken output where
289
+ // the scene renders for a few frames and then goes black as the retry
290
+ // loop fights the direct-render fallback path. Short-circuit here so
291
+ // `useFrame` uses the direct `renderer.render(scene, camera)` path
292
+ // exclusively and never attempts the TSL pipeline.
293
+ const hasWebGPU = typeof navigator !== 'undefined' && 'gpu' in navigator;
294
+ if (!hasWebGPU) {
295
+ hasPipelineErrorRef.current = true;
296
+ renderPipelineRef.current = null;
297
+ return;
298
+ }
134
299
  // Clear outliner arrays synchronously to prevent stale Object3D refs
135
300
  // from the previous project leaking into the new pipeline's outline passes.
136
301
  const outliner = useViewer.getState().outliner;
@@ -140,8 +305,15 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, }) => {
140
305
  outliner.hoveredObjects.length = 0;
141
306
  try {
142
307
  const scenePass = pass(scene, camera);
308
+ scenePass.setLayers(sceneOnlyLayers);
143
309
  const zonePass = pass(scene, camera);
144
310
  zonePass.setLayers(zoneLayers);
311
+ // Editor overlays (gizmos, move handles, tool previews, grid) on their own
312
+ // layer, kept out of the depth/normal MRT above so the ink + SSGI ignore
313
+ // them, then composited on top of the final image below.
314
+ const overlayPass = pass(scene, camera);
315
+ overlayPass.setLayers(overlayLayers);
316
+ const overlayColor = overlayPass.getTextureNode('output');
145
317
  const scenePassColor = scenePass.getTextureNode('output');
146
318
  // Background detection via alpha: renderer clears with alpha=0 (setClearAlpha(0) in useFrame),
147
319
  // so background pixels have scenePassColor.a=0 while geometry pixels have output.a=1.
@@ -149,24 +321,32 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, }) => {
149
321
  // is the reliable geometry mask — no normals, no flicker.
150
322
  const hasGeometry = scenePassColor.a;
151
323
  const contentAlpha = hasGeometry.max(zonePass.a);
152
- let sceneColor = scenePassColor;
153
- if (SSGI_PARAMS.enabled) {
154
- // MRT only needed for SSGI (diffuse for GI, normal for SSGI sampling)
324
+ // Composite the zone-pass tint into the base scene so rooms show whether or
325
+ // not SSGI is enabled. When SSGI is on, the branch below overwrites this
326
+ // with its own zone-inclusive composite (no double-add).
327
+ 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;
331
+ let scenePassNormal = null;
332
+ let sceneNormal = null;
333
+ if (needsNormalMRT) {
155
334
  scenePass.setMRT(mrt({
156
335
  output,
157
336
  diffuseColor,
158
- normal: directionToColor(normalView),
337
+ normal: packNormalToRGB(normalView),
159
338
  }));
339
+ scenePassDepth = scenePass.getTextureNode('depth');
340
+ scenePassNormal = scenePass.getTextureNode('normal');
341
+ const normalTexture = scenePass.getTexture('normal');
342
+ normalTexture.type = UnsignedByteType;
343
+ // Extract normal from color-encoded texture (SSGI consumes the node form)
344
+ sceneNormal = sample((uv) => unpackRGBToNormal(scenePassNormal.sample(uv)));
345
+ }
346
+ if (ssgiEnabled) {
160
347
  const scenePassDiffuse = scenePass.getTextureNode('diffuseColor');
161
- const scenePassDepth = scenePass.getTextureNode('depth');
162
- const scenePassNormal = scenePass.getTextureNode('normal');
163
- // Optimize texture bandwidth
164
348
  const diffuseTexture = scenePass.getTexture('diffuseColor');
165
349
  diffuseTexture.type = UnsignedByteType;
166
- const normalTexture = scenePass.getTexture('normal');
167
- normalTexture.type = UnsignedByteType;
168
- // Extract normal from color-encoded texture
169
- const sceneNormal = sample((uv) => colorToDirection(scenePassNormal.sample(uv)));
170
350
  const giPass = ssgi(scenePassColor, scenePassDepth, sceneNormal, camera);
171
351
  giPass.sliceCount.value = SSGI_PARAMS.sliceCount;
172
352
  giPass.stepCount.value = SSGI_PARAMS.stepCount;
@@ -179,50 +359,127 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, }) => {
179
359
  giPass.useLinearThickness.value = SSGI_PARAMS.useLinearThickness;
180
360
  giPass.useScreenSpaceSampling.value = SSGI_PARAMS.useScreenSpaceSampling;
181
361
  giPass.useTemporalFiltering = SSGI_PARAMS.useTemporalFiltering;
182
- const giTexture = giPass.getTextureNode();
183
- // DenoiseNode only denoises RGB — alpha is passed through unchanged.
184
- // SSGI packs AO into alpha, so we remap it into RGB before denoising.
185
- const aoAsRgb = vec4(giTexture.a, giTexture.a, giTexture.a, float(1));
186
- const denoisePass = denoise(aoAsRgb, scenePassDepth, sceneNormal, camera);
187
- denoisePass.index.value = 0;
188
- denoisePass.radius.value = 4;
189
- const gi = giPass.rgb;
190
- const ao = denoisePass.r;
362
+ // r185: SSGI renders AO and GI into two separate textures (R8 + RG11B10)
363
+ // exposed via getAONode()/getGINode() instead of one rgba texture.
364
+ const aoTexture = giPass.getAONode();
365
+ const gi = giPass.getGINode().rgb;
366
+ let ao;
367
+ if (denoiseEnabled) {
368
+ // DenoiseNode only denoises RGB — alpha is passed through unchanged.
369
+ // 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));
371
+ const denoisePass = denoise(aoAsRgb, scenePassDepth, sceneNormal, camera);
372
+ denoisePass.index.value = 0;
373
+ denoisePass.radius.value = 4;
374
+ ao = denoisePass.r;
375
+ }
376
+ else {
377
+ // Diagnostic path: feed raw noisy SSGI AO straight through. Will
378
+ // look grainy — that's the point, it isolates denoise cost.
379
+ ao = aoTexture.r;
380
+ }
381
+ // AO is a near/mid-field cue like the ink: fade it out with raw depth
382
+ // (same ≈150→350 m window as ink-edges' distanceFade) so the horizon
383
+ // disc and the geometry↔sky depth cliff never grow an AO band — that
384
+ // 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);
191
387
  // Composite: scene * AO + diffuse * GI
192
388
  sceneColor = vec4(add(scenePassColor.rgb.mul(ao), add(zonePass.rgb, scenePassDiffuse.rgb.mul(gi))), contentAlpha);
193
389
  }
390
+ // Screen-space ink outline (SketchUp look) — depth/normal edge detection
391
+ // over the composited scene. Topology-agnostic, so it handles CSG-cut
392
+ // walls cleanly. Applied before the selection outline + background mix.
393
+ if (inkEnabled) {
394
+ sceneColor = vec4(inkedEdges({
395
+ sceneRgb: sceneColor.rgb,
396
+ depthTex: scenePassDepth,
397
+ normalTex: scenePassNormal,
398
+ inkColor: inkColorUniform.current,
399
+ radius: inkRadius,
400
+ opacity: float(inkOpacity).mul(inkOpacityScaleUniform.current),
401
+ }), sceneColor.a);
402
+ }
403
+ // Scene-referred grade (contrast around mid-gray + saturation) before the
404
+ // pipeline's output tone mapping. Kept out of solid/schematic shading so
405
+ // the flat presets stay exact. The same transform is applied to the
406
+ // backdrop below so geometry that fades to the background colour (the
407
+ // horizon disc) matches it exactly.
408
+ const gradeRgb = (rgb) => saturation(rgb.div(0.18).pow(vec3(GRADE_PARAMS.contrast)).mul(0.18), GRADE_PARAMS.saturation);
409
+ if (shading === 'rendered') {
410
+ sceneColor = vec4(gradeRgb(sceneColor.rgb), sceneColor.a);
411
+ }
194
412
  // Single merged outline node: one shared depth pass for both selected + hovered groups.
195
413
  const outliner = useViewer.getState().outliner;
196
- const outlineNode = mergedOutline(scene, camera, {
197
- primaryObjects: outliner.selectedObjects,
198
- secondaryObjects: outliner.hoveredObjects,
199
- primaryEdgeThickness: uniform(1),
200
- secondaryEdgeThickness: uniform(1.5),
414
+ let compositeWithOutlines = sceneColor;
415
+ let visualAlpha = contentAlpha;
416
+ if (outlineEnabled) {
417
+ const outlineNode = mergedOutline(scene, camera, {
418
+ primaryObjects: outliner.selectedObjects,
419
+ secondaryObjects: outliner.hoveredObjects,
420
+ primaryEdgeThickness: uniform(1),
421
+ secondaryEdgeThickness: uniform(1.5),
422
+ });
423
+ // Selected: white visible, yellow hidden
424
+ const selectedVisibleColor = uniform(new Color(0xff_ff_ff));
425
+ const selectedHiddenColor = uniform(new Color(0xf3_ff_47));
426
+ const selectedStrength = uniform(3);
427
+ const selectedOutline = outlineNode.primaryVisibleEdge
428
+ .mul(selectedVisibleColor)
429
+ .add(outlineNode.primaryHiddenEdge.mul(selectedHiddenColor))
430
+ .mul(selectedStrength);
431
+ // Hovered: blue visible, yellow hidden, pulsing
432
+ const pulsePeriod = uniform(3);
433
+ const oscillating = oscSine(time.div(pulsePeriod).mul(2)).mul(0.5).add(0.5);
434
+ const osc = mix(oscillating, float(1), hoverPulseMix);
435
+ const hoverOutline = outlineNode.secondaryVisibleEdge
436
+ .mul(hoverVisibleColor)
437
+ .add(outlineNode.secondaryHiddenEdge.mul(hoverHiddenColor))
438
+ .mul(hoverStrength)
439
+ .mul(osc);
440
+ const outlineAlpha = outlineNode.primaryVisibleEdge
441
+ .max(outlineNode.primaryHiddenEdge)
442
+ .max(outlineNode.secondaryVisibleEdge)
443
+ .max(outlineNode.secondaryHiddenEdge);
444
+ visualAlpha = visualAlpha.max(outlineAlpha);
445
+ compositeWithOutlines = vec4(add(sceneColor.rgb, selectedOutline.add(hoverOutline)), sceneColor.a);
446
+ }
447
+ // Backdrop: world-space view ray per pixel → background / horizon haze /
448
+ // sky gradient (shared formula in lib/backdrop.ts). The horizon disc
449
+ // dissolves into the same formula, so backdrop and ground meet
450
+ // 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);
452
+ const viewRay = camProjInvUniform.current.mul(ndc);
453
+ 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,
201
460
  });
202
- // Selected: white visible, yellow hidden
203
- const selectedVisibleColor = uniform(new Color(0xff_ff_ff));
204
- const selectedHiddenColor = uniform(new Color(0xf3_ff_47));
205
- const selectedStrength = uniform(3);
206
- const selectedOutline = outlineNode.primaryVisibleEdge
207
- .mul(selectedVisibleColor)
208
- .add(outlineNode.primaryHiddenEdge.mul(selectedHiddenColor))
209
- .mul(selectedStrength);
210
- // Hovered: blue visible, yellow hidden, pulsing
211
- const pulsePeriod = uniform(3);
212
- const oscillating = oscSine(time.div(pulsePeriod).mul(2)).mul(0.5).add(0.5);
213
- const osc = mix(oscillating, float(1), hoverPulseMix);
214
- const hoverOutline = outlineNode.secondaryVisibleEdge
215
- .mul(hoverVisibleColor)
216
- .add(outlineNode.secondaryHiddenEdge.mul(hoverHiddenColor))
217
- .mul(hoverStrength)
218
- .mul(osc);
219
- const compositeWithOutlines = vec4(add(sceneColor.rgb, selectedOutline.add(hoverOutline)), sceneColor.a);
220
- const finalOutput = vec4(mix(bgUniform.current, compositeWithOutlines.rgb, contentAlpha), float(1));
461
+ if (shading === 'rendered') {
462
+ bgGradient = gradeRgb(bgGradient);
463
+ }
464
+ const composited = mix(bgGradient, compositeWithOutlines.rgb, contentAlpha);
465
+ // Editor overlays painted on top by their own alpha — they never get inked,
466
+ // AO'd, or outlined, and always read crisp regardless of scene depth.
467
+ const withOverlay = mix(composited, overlayColor.rgb, overlayColor.a);
468
+ let finalOutput = vec4(withOverlay, float(1));
469
+ if (transparentBackground) {
470
+ const overlayAlpha = overlayColor.a;
471
+ const alpha = overlayAlpha.add(visualAlpha.mul(overlayAlpha.oneMinus()));
472
+ const straightRgb = overlayColor.rgb
473
+ .mul(overlayAlpha)
474
+ .add(compositeWithOutlines.rgb.mul(visualAlpha).mul(overlayAlpha.oneMinus()))
475
+ .div(alpha.max(float(0.00001)));
476
+ finalOutput = premultiplyAlpha(renderOutput(vec4(straightRgb, alpha)));
477
+ }
221
478
  const renderPipeline = new RenderPipeline(renderer);
479
+ renderPipeline.outputColorTransform = !transparentBackground;
222
480
  renderPipeline.outputNode = finalOutput;
223
481
  renderPipelineRef.current = renderPipeline;
224
482
  retryCountRef.current = 0;
225
- console.log('[viewer/post-processing] Pipeline built OK', { version: pipelineVersion });
226
483
  }
227
484
  catch (error) {
228
485
  hasPipelineErrorRef.current = true;
@@ -243,33 +500,97 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, }) => {
243
500
  renderPipelineRef.current = null;
244
501
  };
245
502
  }, [
503
+ // NOTE: hoverHighlightMode intentionally excluded — the hover style is
504
+ // pushed to uniforms in a separate effect, so a hover must NOT rebuild the
505
+ // whole pipeline. The uniform refs below are stable (useMemo), so they
506
+ // never trigger a rebuild either.
246
507
  camera,
508
+ disablePostFx,
247
509
  hoverHiddenColor,
248
510
  hoverPulseMix,
249
511
  hoverStrength,
250
512
  hoverVisibleColor,
513
+ edges,
514
+ inkOpacityOverride,
251
515
  pipelineVersion,
252
516
  projectId,
253
517
  renderer,
254
518
  scene,
519
+ shading,
520
+ transparentBackground,
521
+ size.height,
522
+ size.width,
255
523
  zoneLayers,
524
+ sceneOnlyLayers,
525
+ overlayLayers,
256
526
  ]);
257
527
  useFrame((_, delta) => {
258
- // Animate background colour toward the current theme target (same lerp as AnimatedBackground)
259
- bgTarget.current.set(useViewer.getState().theme === 'dark' ? DARK_BG : LIGHT_BG);
528
+ if (size.width < 1 || size.height < 1) {
529
+ return;
530
+ }
531
+ // `?disable=draw`: nothing downstream wants pixels — render an EMPTY scene
532
+ // instead of the real one. This is the only render call (positive-priority
533
+ // useFrame subscribers already disable R3F's automatic render), so the real
534
+ // scene is never drawn: per-frame vertex/draw cost drops to a single 64×64
535
+ // clear, which is what makes headless bakes viable on SwiftShader (CPU).
536
+ // Rendering nothing at all is NOT an option — with zero submitted frames
537
+ // Chromium's no-damage scheduler throttles rAF to 1Hz (measured), stalling
538
+ // the useFrame systems the bake still needs.
539
+ if (PERF_DRAW_DISABLED) {
540
+ try {
541
+ ;
542
+ renderer.render(emptyScene, camera);
543
+ }
544
+ catch {
545
+ // A failed empty draw changes nothing — systems keep ticking.
546
+ }
547
+ return;
548
+ }
549
+ // Animate background colour toward the current scene theme target (same lerp as AnimatedBackground)
550
+ const bgTheme = getSceneTheme(useViewer.getState().sceneTheme);
551
+ bgTarget.current.set(bgTheme.background);
260
552
  bgCurrent.current.lerp(bgTarget.current, Math.min(delta, 0.1) * 4);
261
553
  bgUniform.current.value.copy(bgCurrent.current);
554
+ bgSkyTarget.current.set(bgTheme.backgroundSky ?? bgTheme.background);
555
+ bgSkyCurrent.current.lerp(bgSkyTarget.current, Math.min(delta, 0.1) * 4);
556
+ bgSkyUniform.current.value.copy(bgSkyCurrent.current);
557
+ bgHazeTarget.current.set(horizonHazeColor(bgTheme.backgroundSky ?? bgTheme.background, bgTheme.appearance));
558
+ bgHazeCurrent.current.lerp(bgHazeTarget.current, Math.min(delta, 0.1) * 4);
559
+ bgHazeUniform.current.value.copy(bgHazeCurrent.current);
560
+ bgSkyDeepTarget.current.set(deepSkyColor(bgTheme.backgroundSky ?? bgTheme.background));
561
+ bgSkyDeepCurrent.current.lerp(bgSkyDeepTarget.current, Math.min(delta, 0.1) * 4);
562
+ bgSkyDeepUniform.current.value.copy(bgSkyDeepCurrent.current);
563
+ camProjInvUniform.current.value.copy(camera.projectionMatrixInverse);
564
+ camWorldUniform.current.value.copy(camera.matrixWorld);
565
+ // Ink colour follows the (lerping) background luminance — snaps dark↔light.
566
+ const bgHex = `#${bgCurrent.current.getHexString()}`;
567
+ inkColorUniform.current.value.set(edgeColorFor(bgHex));
568
+ inkOpacityScaleUniform.current.value = edgeOpacityScaleFor(bgHex);
262
569
  const outliner = useViewer.getState().outliner;
263
570
  sanitizeOutlineObjects(outliner.selectedObjects);
264
571
  sanitizeOutlineObjects(outliner.hoveredObjects);
265
- if (hasPipelineErrorRef.current || !renderPipelineRef.current) {
572
+ if (disablePostFx ||
573
+ PERF_POST_FX_DISABLED ||
574
+ hasPipelineErrorRef.current ||
575
+ !renderPipelineRef.current) {
266
576
  try {
267
- if (renderer.setClearAlpha) {
577
+ const clearAlpha = transparentBackground ? 0 : 1;
578
+ if (renderer.setClearColor) {
268
579
  ;
269
- renderer.setClearAlpha(1);
580
+ renderer.setClearColor(bgCurrent.current, clearAlpha);
270
581
  }
271
- ;
582
+ else if (renderer.setClearAlpha) {
583
+ ;
584
+ renderer.setClearAlpha(clearAlpha);
585
+ }
586
+ const submittedAt = PERF_OVERLAY_ENABLED ? performance.now() : 0;
272
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
+ }
273
594
  }
274
595
  catch (fallbackError) {
275
596
  console.error('[viewer/post-processing] Fallback render failed.', fallbackError);
@@ -281,7 +602,19 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, }) => {
281
602
  // making scenePassColor.a a reliable geometry mask (geometry pixels write a=1 via output node).
282
603
  ;
283
604
  renderer.setClearAlpha(0);
605
+ const submittedAt = PERF_OVERLAY_ENABLED ? performance.now() : 0;
284
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
+ }
285
618
  }
286
619
  catch (error) {
287
620
  hasPipelineErrorRef.current = true;
@@ -297,7 +630,6 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, }) => {
297
630
  if (retryCountRef.current < MAX_PIPELINE_RETRIES) {
298
631
  // Auto-retry: schedule a pipeline rebuild if we haven't exceeded the retry limit
299
632
  retryCountRef.current++;
300
- console.warn(`[viewer/post-processing] Scheduling pipeline rebuild (attempt ${retryCountRef.current}/${MAX_PIPELINE_RETRIES})`);
301
633
  if (rebuildTimeoutRef.current !== null) {
302
634
  clearTimeout(rebuildTimeoutRef.current);
303
635
  }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Mounts every registered node kind's system component, ordered by
3
+ * `system.priority` (default {@link DEFAULT_PRIORITY}).
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.
10
+ */
11
+ export declare function RegisteredSystems(): import("react").JSX.Element | null;
12
+ //# sourceMappingURL=registered-systems.d.ts.map
@@ -0,0 +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"}