@pascal-app/viewer 0.9.1 → 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/README.md +20 -5
  2. package/dist/components/error-boundary.d.ts +6 -0
  3. package/dist/components/error-boundary.d.ts.map +1 -1
  4. package/dist/components/error-boundary.js +6 -0
  5. package/dist/components/renderers/node-renderer.d.ts +5 -1
  6. package/dist/components/renderers/node-renderer.d.ts.map +1 -1
  7. package/dist/components/renderers/node-renderer.js +5 -2
  8. package/dist/components/renderers/parametric-node-renderer.d.ts.map +1 -1
  9. package/dist/components/renderers/parametric-node-renderer.js +7 -4
  10. package/dist/components/viewer/bvh-ecctrl.d.ts +77 -0
  11. package/dist/components/viewer/bvh-ecctrl.d.ts.map +1 -0
  12. package/dist/components/viewer/bvh-ecctrl.js +552 -0
  13. package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
  14. package/dist/components/viewer/frame-limiter.js +59 -4
  15. package/dist/components/viewer/glb-interactive.d.ts +41 -0
  16. package/dist/components/viewer/glb-interactive.d.ts.map +1 -0
  17. package/dist/components/viewer/glb-interactive.js +394 -0
  18. package/dist/components/viewer/glb-reference-nodes.d.ts +29 -0
  19. package/dist/components/viewer/glb-reference-nodes.d.ts.map +1 -0
  20. package/dist/components/viewer/glb-reference-nodes.js +80 -0
  21. package/dist/components/viewer/glb-replace-instances.d.ts +18 -0
  22. package/dist/components/viewer/glb-replace-instances.d.ts.map +1 -0
  23. package/dist/components/viewer/glb-replace-instances.js +61 -0
  24. package/dist/components/viewer/glb-scene.d.ts +54 -0
  25. package/dist/components/viewer/glb-scene.d.ts.map +1 -0
  26. package/dist/components/viewer/glb-scene.js +920 -0
  27. package/dist/components/viewer/glb-walkthrough-controller.d.ts +22 -0
  28. package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -0
  29. package/dist/components/viewer/glb-walkthrough-controller.js +445 -0
  30. package/dist/components/viewer/index.d.ts +19 -2
  31. package/dist/components/viewer/index.d.ts.map +1 -1
  32. package/dist/components/viewer/index.js +113 -12
  33. package/dist/components/viewer/lights.d.ts.map +1 -1
  34. package/dist/components/viewer/lights.js +36 -5
  35. package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
  36. package/dist/components/viewer/perf-monitor.js +17 -2
  37. package/dist/components/viewer/post-processing.d.ts +7 -1
  38. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  39. package/dist/components/viewer/post-processing.js +180 -24
  40. package/dist/components/viewer/registered-systems.d.ts.map +1 -1
  41. package/dist/components/viewer/registered-systems.js +6 -4
  42. package/dist/components/viewer/scene-bvh.d.ts +2 -1
  43. package/dist/components/viewer/scene-bvh.d.ts.map +1 -1
  44. package/dist/components/viewer/scene-bvh.js +3 -0
  45. package/dist/components/viewer/scene-bvh.test.d.ts +2 -0
  46. package/dist/components/viewer/scene-bvh.test.d.ts.map +1 -0
  47. package/dist/components/viewer/scene-bvh.test.js +13 -0
  48. package/dist/components/viewer/scene-environment.d.ts +2 -0
  49. package/dist/components/viewer/scene-environment.d.ts.map +1 -0
  50. package/dist/components/viewer/scene-environment.js +79 -0
  51. package/dist/components/viewer/selection-manager.d.ts.map +1 -1
  52. package/dist/components/viewer/selection-manager.js +23 -12
  53. package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
  54. package/dist/hooks/use-gltf-ktx2.js +2 -24
  55. package/dist/hooks/use-node-events.d.ts +1 -1
  56. package/dist/hooks/use-node-events.d.ts.map +1 -1
  57. package/dist/hooks/use-node-events.js +22 -14
  58. package/dist/index.d.ts +24 -10
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +29 -13
  61. package/dist/lib/backdrop.d.ts +33 -0
  62. package/dist/lib/backdrop.d.ts.map +1 -0
  63. package/dist/lib/backdrop.js +92 -0
  64. package/dist/lib/box-uv.d.ts +13 -0
  65. package/dist/lib/box-uv.d.ts.map +1 -0
  66. package/dist/lib/box-uv.js +32 -0
  67. package/dist/lib/csg-utils.d.ts +3 -9
  68. package/dist/lib/csg-utils.d.ts.map +1 -1
  69. package/dist/lib/csg-utils.js +65 -5
  70. package/dist/lib/csg-utils.test.d.ts +2 -0
  71. package/dist/lib/csg-utils.test.d.ts.map +1 -0
  72. package/dist/lib/csg-utils.test.js +45 -0
  73. package/dist/lib/edge-style.d.ts +1 -0
  74. package/dist/lib/edge-style.d.ts.map +1 -1
  75. package/dist/lib/edge-style.js +26 -6
  76. package/dist/lib/hero-pose.d.ts +47 -0
  77. package/dist/lib/hero-pose.d.ts.map +1 -0
  78. package/dist/lib/hero-pose.js +227 -0
  79. package/dist/lib/ink-edges.d.ts +1 -1
  80. package/dist/lib/ink-edges.d.ts.map +1 -1
  81. package/dist/lib/ink-edges.js +13 -7
  82. package/dist/lib/ktx2-loader.d.ts +46 -0
  83. package/dist/lib/ktx2-loader.d.ts.map +1 -0
  84. package/dist/lib/ktx2-loader.js +142 -0
  85. package/dist/lib/layers.d.ts +10 -0
  86. package/dist/lib/layers.d.ts.map +1 -1
  87. package/dist/lib/layers.js +10 -0
  88. package/dist/lib/materials.d.ts +16 -1
  89. package/dist/lib/materials.d.ts.map +1 -1
  90. package/dist/lib/materials.js +158 -34
  91. package/dist/lib/materials.test.d.ts +2 -0
  92. package/dist/lib/materials.test.d.ts.map +1 -0
  93. package/dist/lib/materials.test.js +25 -0
  94. package/dist/lib/merged-outline-node.test.d.ts +2 -0
  95. package/dist/lib/merged-outline-node.test.d.ts.map +1 -0
  96. package/dist/lib/merged-outline-node.test.js +22 -0
  97. package/dist/lib/scene-themes.d.ts +1 -0
  98. package/dist/lib/scene-themes.d.ts.map +1 -1
  99. package/dist/lib/scene-themes.js +25 -16
  100. package/dist/lib/shadow-only.d.ts +4 -0
  101. package/dist/lib/shadow-only.d.ts.map +1 -0
  102. package/dist/lib/shadow-only.js +36 -0
  103. package/dist/lib/snapshot-pipeline.d.ts +42 -0
  104. package/dist/lib/snapshot-pipeline.d.ts.map +1 -0
  105. package/dist/lib/snapshot-pipeline.js +254 -0
  106. package/dist/lib/terrain-perimeter-fill.d.ts +7 -0
  107. package/dist/lib/terrain-perimeter-fill.d.ts.map +1 -0
  108. package/dist/lib/terrain-perimeter-fill.js +75 -0
  109. package/dist/lib/texture-node-guard.d.ts +13 -0
  110. package/dist/lib/texture-node-guard.d.ts.map +1 -0
  111. package/dist/lib/texture-node-guard.js +39 -0
  112. package/dist/lib/texture-reference.d.ts +31 -0
  113. package/dist/lib/texture-reference.d.ts.map +1 -0
  114. package/dist/lib/texture-reference.js +139 -0
  115. package/dist/lib/texture-reference.test.d.ts +2 -0
  116. package/dist/lib/texture-reference.test.d.ts.map +1 -0
  117. package/dist/lib/texture-reference.test.js +86 -0
  118. package/dist/lib/tsl-compat.d.ts +12 -0
  119. package/dist/lib/tsl-compat.d.ts.map +1 -0
  120. package/dist/lib/tsl-compat.js +13 -0
  121. package/dist/store/use-viewer.d.ts +73 -18
  122. package/dist/store/use-viewer.d.ts.map +1 -1
  123. package/dist/store/use-viewer.js +191 -4
  124. package/dist/store/use-viewer.test.d.ts +2 -0
  125. package/dist/store/use-viewer.test.d.ts.map +1 -0
  126. package/dist/store/use-viewer.test.js +61 -0
  127. package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
  128. package/dist/systems/ceiling/ceiling-system.js +12 -5
  129. package/dist/systems/door/door-animation.test.d.ts +2 -0
  130. package/dist/systems/door/door-animation.test.d.ts.map +1 -0
  131. package/dist/systems/door/door-animation.test.js +51 -0
  132. package/dist/systems/door/door-floor-alignment.test.d.ts +2 -0
  133. package/dist/systems/door/door-floor-alignment.test.d.ts.map +1 -0
  134. package/dist/systems/door/door-floor-alignment.test.js +50 -0
  135. package/dist/systems/door/door-system.d.ts +24 -0
  136. package/dist/systems/door/door-system.d.ts.map +1 -1
  137. package/dist/systems/door/door-system.js +494 -161
  138. package/dist/systems/fence/fence-system.d.ts +17 -0
  139. package/dist/systems/fence/fence-system.d.ts.map +1 -1
  140. package/dist/systems/fence/fence-system.js +221 -53
  141. package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -1
  142. package/dist/systems/floor-elevation/floor-elevation-system.js +30 -7
  143. package/dist/systems/geometry/geometry-system.d.ts +7 -0
  144. package/dist/systems/geometry/geometry-system.d.ts.map +1 -1
  145. package/dist/systems/geometry/geometry-system.js +95 -26
  146. package/dist/systems/geometry/geometry-system.test.d.ts +2 -0
  147. package/dist/systems/geometry/geometry-system.test.d.ts.map +1 -0
  148. package/dist/systems/geometry/geometry-system.test.js +30 -0
  149. package/dist/systems/interactive/control-widget.d.ts +10 -0
  150. package/dist/systems/interactive/control-widget.d.ts.map +1 -0
  151. package/dist/systems/interactive/control-widget.js +35 -0
  152. package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
  153. package/dist/systems/interactive/interactive-system.js +55 -56
  154. package/dist/systems/item/item-system.d.ts.map +1 -1
  155. package/dist/systems/item/item-system.js +20 -6
  156. package/dist/systems/level/level-system.d.ts.map +1 -1
  157. package/dist/systems/level/level-system.js +34 -9
  158. package/dist/systems/level/level-utils.d.ts +0 -3
  159. package/dist/systems/level/level-utils.d.ts.map +1 -1
  160. package/dist/systems/level/level-utils.js +7 -45
  161. package/dist/systems/roof/roof-materials.d.ts.map +1 -1
  162. package/dist/systems/roof/roof-materials.js +29 -8
  163. package/dist/systems/roof/roof-system.d.ts +21 -8
  164. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  165. package/dist/systems/roof/roof-system.js +1079 -221
  166. package/dist/systems/slab/slab-system.d.ts +7 -4
  167. package/dist/systems/slab/slab-system.d.ts.map +1 -1
  168. package/dist/systems/slab/slab-system.js +31 -83
  169. package/dist/systems/slab/slab-system.test.js +54 -14
  170. package/dist/systems/stair/stair-system.js +7 -4
  171. package/dist/systems/wall/opening-cutout-geometry.d.ts +31 -0
  172. package/dist/systems/wall/opening-cutout-geometry.d.ts.map +1 -0
  173. package/dist/systems/wall/opening-cutout-geometry.js +167 -0
  174. package/dist/systems/wall/opening-cutout-geometry.test.d.ts +2 -0
  175. package/dist/systems/wall/opening-cutout-geometry.test.d.ts.map +1 -0
  176. package/dist/systems/wall/opening-cutout-geometry.test.js +172 -0
  177. package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
  178. package/dist/systems/wall/wall-cutout.js +49 -17
  179. package/dist/systems/wall/wall-materials.d.ts +11 -7
  180. package/dist/systems/wall/wall-materials.d.ts.map +1 -1
  181. package/dist/systems/wall/wall-materials.js +244 -35
  182. package/dist/systems/wall/wall-opening-cutout.test.d.ts +2 -0
  183. package/dist/systems/wall/wall-opening-cutout.test.d.ts.map +1 -0
  184. package/dist/systems/wall/wall-opening-cutout.test.js +59 -0
  185. package/dist/systems/wall/wall-support-extension.test.d.ts +2 -0
  186. package/dist/systems/wall/wall-support-extension.test.d.ts.map +1 -0
  187. package/dist/systems/wall/wall-support-extension.test.js +116 -0
  188. package/dist/systems/wall/wall-system.d.ts +4 -8
  189. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  190. package/dist/systems/wall/wall-system.js +397 -177
  191. package/dist/systems/wall/wall-terrain-bottom.test.d.ts +2 -0
  192. package/dist/systems/wall/wall-terrain-bottom.test.d.ts.map +1 -0
  193. package/dist/systems/wall/wall-terrain-bottom.test.js +34 -0
  194. package/dist/systems/window/window-animation-system.d.ts +12 -0
  195. package/dist/systems/window/window-animation-system.d.ts.map +1 -1
  196. package/dist/systems/window/window-animation-system.js +16 -5
  197. package/dist/systems/window/window-system.d.ts +7 -0
  198. package/dist/systems/window/window-system.d.ts.map +1 -1
  199. package/dist/systems/window/window-system.js +240 -14
  200. package/package.json +4 -4
  201. package/dist/components/viewer/ground-occluder.d.ts +0 -2
  202. package/dist/components/viewer/ground-occluder.d.ts.map +0 -1
  203. package/dist/components/viewer/ground-occluder.js +0 -67
  204. package/dist/components/viewer/walkthrough-controls.d.ts +0 -2
  205. package/dist/components/viewer/walkthrough-controls.d.ts.map +0 -1
  206. package/dist/components/viewer/walkthrough-controls.js +0 -131
@@ -1,17 +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 { edgeColorFor } from '../../lib/edge-style';
8
+ import { backdropGradient, deepSkyColor, horizonHazeColor } from '../../lib/backdrop';
9
+ import { edgeColorFor, edgeOpacityScaleFor } from '../../lib/edge-style';
9
10
  import { PERF_OVERLAY_ENABLED, pushGpuSample } from '../../lib/gpu-perf';
10
11
  import { inkedEdges } from '../../lib/ink-edges';
11
12
  import { GRID_LAYER, OVERLAY_LAYER, SCENE_LAYER, ZONE_LAYER } from '../../lib/layers';
12
13
  import { mergedOutline } from '../../lib/merged-outline-node';
13
14
  import { getSceneTheme } from '../../lib/scene-themes';
15
+ import { packNormalToRGB, unpackRGBToNormal } from '../../lib/tsl-compat';
14
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
+ };
15
24
  // SSGI Parameters - adjust these to fine-tune global illumination and ambient occlusion
16
25
  export const SSGI_PARAMS = {
17
26
  enabled: true,
@@ -37,6 +46,11 @@ export const SSGI_PARAMS = {
37
46
  // - outline: skip the merged-outline node and its 14 internal RTs
38
47
  // - postFx: bypass the whole RenderPipeline and use renderer.render(scene, camera)
39
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.
40
54
  function readPerfDisableFlags() {
41
55
  if (typeof window === 'undefined') {
42
56
  return { ao: false, denoise: false, outline: false, postFx: false };
@@ -57,6 +71,12 @@ const PERF_POST_FX_DISABLED = typeof window !== 'undefined' &&
57
71
  new Set((new URLSearchParams(window.location.search).get('disable') ?? '')
58
72
  .split(',')
59
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();
60
80
  const MAX_PIPELINE_RETRIES = 3;
61
81
  const RETRY_DELAY_MS = 500;
62
82
  const DEFAULT_HOVER_STYLE = {
@@ -79,7 +99,7 @@ function sanitizeOutlineObjects(objects) {
79
99
  }
80
100
  objects.length = nextIndex;
81
101
  }
82
- const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, }) => {
102
+ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, disablePostFx = false, }) => {
83
103
  const { gl: renderer, invalidate, scene, camera, size } = useThree();
84
104
  const renderPipelineRef = useRef(null);
85
105
  const hasPipelineErrorRef = useRef(false);
@@ -88,13 +108,35 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, }) => {
88
108
  const skippedZeroSizeRef = useRef(false);
89
109
  // Background color uniform — updated every frame via lerp, read by the TSL pipeline.
90
110
  // Initialised from the current scene theme so there's no flash on first render.
91
- const initBg = getSceneTheme(useViewer.getState().sceneTheme).background;
111
+ const initTheme = getSceneTheme(useViewer.getState().sceneTheme);
112
+ const initBg = initTheme.background;
92
113
  const bgUniform = useRef(uniform(new Color(initBg)));
93
114
  const bgCurrent = useRef(new Color(initBg));
94
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()));
95
135
  // Ink-line colour follows the scene-theme background luminance (dark lines on
96
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).
97
138
  const inkColorUniform = useRef(uniform(new Color(edgeColorFor(initBg))));
139
+ const inkOpacityScaleUniform = useRef(uniform(edgeOpacityScaleFor(initBg)));
98
140
  const zoneLayers = useMemo(() => {
99
141
  const l = new Layers();
100
142
  l.enable(ZONE_LAYER);
@@ -129,6 +171,8 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, }) => {
129
171
  const projectId = useViewer((s) => s.projectId);
130
172
  const shading = useViewer((s) => s.shading);
131
173
  const edges = useViewer((s) => s.edges);
174
+ const inkOpacityOverride = useViewer((s) => s.inkOpacity);
175
+ const transparentBackground = useViewer((s) => s.transparentBackground);
132
176
  const lastProjectIdRef = useRef(projectId);
133
177
  // Bump this to force a pipeline rebuild (used by retry logic)
134
178
  const [pipelineVersion, setPipelineVersion] = useState(0);
@@ -199,6 +243,18 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, }) => {
199
243
  skippedZeroSizeRef.current = false;
200
244
  }
201
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
+ }
202
258
  const ssgiEnabled = shading === 'rendered' && SSGI_PARAMS.enabled && !perfDisable.ao;
203
259
  const denoiseEnabled = ssgiEnabled && !perfDisable.denoise;
204
260
  const outlineEnabled = !perfDisable.outline;
@@ -211,16 +267,16 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, }) => {
211
267
  // Same 1px line thickness for both (soft's thickness is the nice one);
212
268
  // strong reads heavier purely by being fully solid vs soft's lighter 50%.
213
269
  const inkRadius = 1;
214
- const inkOpacity = edges === 'strong' ? 1 : 0.5;
270
+ const inkOpacity = inkOpacityOverride ?? (edges === 'strong' ? 1 : 0.5);
215
271
  console.log('[viewer/post-processing] Building pipeline', {
216
272
  version: pipelineVersion,
217
273
  ssgi: ssgiEnabled,
218
274
  denoise: denoiseEnabled,
219
275
  outline: outlineEnabled,
220
276
  perfDisable,
221
- hoverHighlightMode,
222
277
  projectId,
223
278
  shading,
279
+ transparentBackground,
224
280
  rendererCtor: renderer.constructor?.name,
225
281
  width,
226
282
  height,
@@ -265,7 +321,10 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, }) => {
265
321
  // is the reliable geometry mask — no normals, no flicker.
266
322
  const hasGeometry = scenePassColor.a;
267
323
  const contentAlpha = hasGeometry.max(zonePass.a);
268
- let sceneColor = scenePassColor;
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);
269
328
  // Depth + normal MRT — shared by SSGI (diffuse/normal) and the ink pass
270
329
  // (depth/normal). Built whenever either is active.
271
330
  let scenePassDepth = null;
@@ -275,14 +334,14 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, }) => {
275
334
  scenePass.setMRT(mrt({
276
335
  output,
277
336
  diffuseColor,
278
- normal: directionToColor(normalView),
337
+ normal: packNormalToRGB(normalView),
279
338
  }));
280
339
  scenePassDepth = scenePass.getTextureNode('depth');
281
340
  scenePassNormal = scenePass.getTextureNode('normal');
282
341
  const normalTexture = scenePass.getTexture('normal');
283
342
  normalTexture.type = UnsignedByteType;
284
343
  // Extract normal from color-encoded texture (SSGI consumes the node form)
285
- sceneNormal = sample((uv) => colorToDirection(scenePassNormal.sample(uv)));
344
+ sceneNormal = sample((uv) => unpackRGBToNormal(scenePassNormal.sample(uv)));
286
345
  }
287
346
  if (ssgiEnabled) {
288
347
  const scenePassDiffuse = scenePass.getTextureNode('diffuseColor');
@@ -300,13 +359,15 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, }) => {
300
359
  giPass.useLinearThickness.value = SSGI_PARAMS.useLinearThickness;
301
360
  giPass.useScreenSpaceSampling.value = SSGI_PARAMS.useScreenSpaceSampling;
302
361
  giPass.useTemporalFiltering = SSGI_PARAMS.useTemporalFiltering;
303
- const giTexture = giPass.getTextureNode();
304
- const gi = giPass.rgb;
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;
305
366
  let ao;
306
367
  if (denoiseEnabled) {
307
368
  // DenoiseNode only denoises RGB — alpha is passed through unchanged.
308
- // SSGI packs AO into alpha, so we remap it into RGB before denoising.
309
- const aoAsRgb = vec4(giTexture.a, giTexture.a, giTexture.a, float(1));
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));
310
371
  const denoisePass = denoise(aoAsRgb, scenePassDepth, sceneNormal, camera);
311
372
  denoisePass.index.value = 0;
312
373
  denoisePass.radius.value = 4;
@@ -315,8 +376,14 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, }) => {
315
376
  else {
316
377
  // Diagnostic path: feed raw noisy SSGI AO straight through. Will
317
378
  // look grainy — that's the point, it isolates denoise cost.
318
- ao = giTexture.a;
379
+ ao = aoTexture.r;
319
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);
320
387
  // Composite: scene * AO + diffuse * GI
321
388
  sceneColor = vec4(add(scenePassColor.rgb.mul(ao), add(zonePass.rgb, scenePassDiffuse.rgb.mul(gi))), contentAlpha);
322
389
  }
@@ -330,12 +397,22 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, }) => {
330
397
  normalTex: scenePassNormal,
331
398
  inkColor: inkColorUniform.current,
332
399
  radius: inkRadius,
333
- opacity: inkOpacity,
400
+ opacity: float(inkOpacity).mul(inkOpacityScaleUniform.current),
334
401
  }), sceneColor.a);
335
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
+ }
336
412
  // Single merged outline node: one shared depth pass for both selected + hovered groups.
337
413
  const outliner = useViewer.getState().outliner;
338
414
  let compositeWithOutlines = sceneColor;
415
+ let visualAlpha = contentAlpha;
339
416
  if (outlineEnabled) {
340
417
  const outlineNode = mergedOutline(scene, camera, {
341
418
  primaryObjects: outliner.selectedObjects,
@@ -360,14 +437,46 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, }) => {
360
437
  .add(outlineNode.secondaryHiddenEdge.mul(hoverHiddenColor))
361
438
  .mul(hoverStrength)
362
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);
363
445
  compositeWithOutlines = vec4(add(sceneColor.rgb, selectedOutline.add(hoverOutline)), sceneColor.a);
364
446
  }
365
- const composited = mix(bgUniform.current, compositeWithOutlines.rgb, contentAlpha);
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,
460
+ });
461
+ if (shading === 'rendered') {
462
+ bgGradient = gradeRgb(bgGradient);
463
+ }
464
+ const composited = mix(bgGradient, compositeWithOutlines.rgb, contentAlpha);
366
465
  // Editor overlays painted on top by their own alpha — they never get inked,
367
466
  // AO'd, or outlined, and always read crisp regardless of scene depth.
368
467
  const withOverlay = mix(composited, overlayColor.rgb, overlayColor.a);
369
- const finalOutput = vec4(withOverlay, float(1));
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
+ }
370
478
  const renderPipeline = new RenderPipeline(renderer);
479
+ renderPipeline.outputColorTransform = !transparentBackground;
371
480
  renderPipeline.outputNode = finalOutput;
372
481
  renderPipelineRef.current = renderPipeline;
373
482
  retryCountRef.current = 0;
@@ -391,18 +500,24 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, }) => {
391
500
  renderPipelineRef.current = null;
392
501
  };
393
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.
394
507
  camera,
508
+ disablePostFx,
395
509
  hoverHiddenColor,
396
- hoverHighlightMode,
397
510
  hoverPulseMix,
398
511
  hoverStrength,
399
512
  hoverVisibleColor,
400
513
  edges,
514
+ inkOpacityOverride,
401
515
  pipelineVersion,
402
516
  projectId,
403
517
  renderer,
404
518
  scene,
405
519
  shading,
520
+ transparentBackground,
406
521
  size.height,
407
522
  size.width,
408
523
  zoneLayers,
@@ -413,20 +528,60 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, }) => {
413
528
  if (size.width < 1 || size.height < 1) {
414
529
  return;
415
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
+ }
416
549
  // Animate background colour toward the current scene theme target (same lerp as AnimatedBackground)
417
- bgTarget.current.set(getSceneTheme(useViewer.getState().sceneTheme).background);
550
+ const bgTheme = getSceneTheme(useViewer.getState().sceneTheme);
551
+ bgTarget.current.set(bgTheme.background);
418
552
  bgCurrent.current.lerp(bgTarget.current, Math.min(delta, 0.1) * 4);
419
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);
420
565
  // Ink colour follows the (lerping) background luminance — snaps dark↔light.
421
- inkColorUniform.current.value.set(edgeColorFor(`#${bgCurrent.current.getHexString()}`));
566
+ const bgHex = `#${bgCurrent.current.getHexString()}`;
567
+ inkColorUniform.current.value.set(edgeColorFor(bgHex));
568
+ inkOpacityScaleUniform.current.value = edgeOpacityScaleFor(bgHex);
422
569
  const outliner = useViewer.getState().outliner;
423
570
  sanitizeOutlineObjects(outliner.selectedObjects);
424
571
  sanitizeOutlineObjects(outliner.hoveredObjects);
425
- if (PERF_POST_FX_DISABLED || hasPipelineErrorRef.current || !renderPipelineRef.current) {
572
+ if (disablePostFx ||
573
+ PERF_POST_FX_DISABLED ||
574
+ hasPipelineErrorRef.current ||
575
+ !renderPipelineRef.current) {
426
576
  try {
427
- if (renderer.setClearAlpha) {
577
+ const clearAlpha = transparentBackground ? 0 : 1;
578
+ if (renderer.setClearColor) {
579
+ ;
580
+ renderer.setClearColor(bgCurrent.current, clearAlpha);
581
+ }
582
+ else if (renderer.setClearAlpha) {
428
583
  ;
429
- renderer.setClearAlpha(1);
584
+ renderer.setClearAlpha(clearAlpha);
430
585
  }
431
586
  const submittedAt = PERF_OVERLAY_ENABLED ? performance.now() : 0;
432
587
  renderer.render(scene, camera);
@@ -463,6 +618,7 @@ const PostProcessingPasses = ({ hoverStyles = DEFAULT_HOVER_STYLES, }) => {
463
618
  }
464
619
  catch (error) {
465
620
  hasPipelineErrorRef.current = true;
621
+ renderer.setRenderTarget?.(null);
466
622
  console.error('[viewer/post-processing] Render pass failed.', {
467
623
  retryCount: retryCountRef.current,
468
624
  rendererCtor: renderer.constructor?.name,
@@ -1 +1 @@
1
- {"version":3,"file":"registered-systems.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/registered-systems.tsx"],"names":[],"mappings":"AAoBA;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,uCAsBhC"}
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,10 +1,8 @@
1
1
  'use client';
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
- import { nodeRegistry } from '@pascal-app/core';
3
+ import { createSceneApi, isNodeKindEnabled, nodeRegistry, useScene, } from '@pascal-app/core';
4
4
  import { lazy, Suspense, useMemo } from 'react';
5
5
  const DEFAULT_PRIORITY = 5;
6
- // Cache lazy components keyed by the module-loader function so React.lazy
7
- // isn't re-invoked across renders.
8
6
  const lazyCache = new WeakMap();
9
7
  function loadSystem(def) {
10
8
  if (!def.system)
@@ -27,6 +25,8 @@ function loadSystem(def) {
27
25
  * guard added to each legacy system.
28
26
  */
29
27
  export function RegisteredSystems() {
28
+ const sceneApi = useMemo(() => createSceneApi(useScene), []);
29
+ const installedPlugins = useScene((state) => state.installedPlugins);
30
30
  const entries = useMemo(() => {
31
31
  return Array.from(nodeRegistry.entries())
32
32
  .filter(([, def]) => def.system != null)
@@ -39,9 +39,11 @@ export function RegisteredSystems() {
39
39
  if (entries.length === 0)
40
40
  return null;
41
41
  return (_jsx(Suspense, { fallback: null, children: entries.map(([kind, def]) => {
42
+ if (!isNodeKindEnabled(kind, installedPlugins))
43
+ return null;
42
44
  const Comp = loadSystem(def);
43
45
  if (!Comp)
44
46
  return null;
45
- return _jsx(Comp, {}, `registered-system:${kind}`);
47
+ return _jsx(Comp, { sceneApi: sceneApi }, `registered-system:${kind}`);
46
48
  }) }));
47
49
  }
@@ -1,5 +1,5 @@
1
1
  import { type ReactNode } from 'react';
2
- import { type Group } from 'three';
2
+ import { type Group, Mesh } from 'three';
3
3
  import { type SplitStrategy } from 'three-mesh-bvh';
4
4
  type SceneBvhProps = {
5
5
  children?: ReactNode;
@@ -12,6 +12,7 @@ type SceneBvhProps = {
12
12
  maxLeafSize?: number;
13
13
  indirect?: boolean;
14
14
  };
15
+ export declare const isSceneBvhExcluded: (object: Mesh) => boolean;
15
16
  export declare const SceneBvh: import("react").ForwardRefExoticComponent<SceneBvhProps & import("react").RefAttributes<Group<import("three").Object3DEventMap>>>;
16
17
  export {};
17
18
  //# sourceMappingURL=scene-bvh.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"scene-bvh.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/scene-bvh.tsx"],"names":[],"mappings":"AACA,OAAO,EAAc,KAAK,SAAS,EAA0C,MAAM,OAAO,CAAA;AAC1F,OAAO,EAAuB,KAAK,KAAK,EAAQ,MAAM,OAAO,CAAA;AAC7D,OAAO,EAKL,KAAK,aAAa,EACnB,MAAM,gBAAgB,CAAA;AAEvB,KAAK,aAAa,GAAG;IACnB,QAAQ,CAAC,EAAE,SAAS,CAAA;IACpB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,QAAQ,CAAC,EAAE,aAAa,CAAA;IACxB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB,CAAA;AAeD,eAAO,MAAM,QAAQ,mIAmGpB,CAAA"}
1
+ {"version":3,"file":"scene-bvh.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/scene-bvh.tsx"],"names":[],"mappings":"AACA,OAAO,EAAc,KAAK,SAAS,EAA0C,MAAM,OAAO,CAAA;AAC1F,OAAO,EAAuB,KAAK,KAAK,EAAE,IAAI,EAAE,MAAM,OAAO,CAAA;AAC7D,OAAO,EAKL,KAAK,aAAa,EACnB,MAAM,gBAAgB,CAAA;AAEvB,KAAK,aAAa,GAAG;IACnB,QAAQ,CAAC,EAAE,SAAS,CAAA;IACpB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,QAAQ,CAAC,EAAE,aAAa,CAAA;IACxB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB,CAAA;AAKD,eAAO,MAAM,kBAAkB,GAAI,QAAQ,IAAI,YAA4C,CAAA;AAY3F,eAAO,MAAM,QAAQ,mIAoGpB,CAAA"}
@@ -4,6 +4,7 @@ import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react';
4
4
  import { Mesh } from 'three';
5
5
  import { acceleratedRaycast, computeBoundsTree, disposeBoundsTree, SAH, } from 'three-mesh-bvh';
6
6
  const isMesh = (object) => !!object && typeof object === 'object' && object.isMesh === true;
7
+ export const isSceneBvhExcluded = (object) => object.userData.excludeFromBvh === true;
7
8
  const hasBvhCompatibleGeometry = (geometry) => {
8
9
  if (!geometry)
9
10
  return false;
@@ -35,6 +36,8 @@ export const SceneBvh = forwardRef(({ children, enabled = true, firstHitOnly = f
35
36
  group.traverse((child) => {
36
37
  if (!isMesh(child))
37
38
  return;
39
+ if (isSceneBvhExcluded(child))
40
+ return;
38
41
  if (child.raycast === Mesh.prototype.raycast) {
39
42
  child.raycast = acceleratedRaycast;
40
43
  acceleratedMeshes.add(child);
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=scene-bvh.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scene-bvh.test.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/scene-bvh.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,13 @@
1
+ // @ts-expect-error — bun:test is provided by the Bun runtime; viewer does not
2
+ // include Bun ambient types in its production declaration build.
3
+ import { describe, expect, test } from 'bun:test';
4
+ import { Mesh } from 'three';
5
+ import { isSceneBvhExcluded } from './scene-bvh';
6
+ describe('scene BVH exclusions', () => {
7
+ test('keeps annotation meshes on their normal raycast path', () => {
8
+ expect(isSceneBvhExcluded(new Mesh())).toBe(false);
9
+ const annotation = new Mesh();
10
+ annotation.userData.excludeFromBvh = true;
11
+ expect(isSceneBvhExcluded(annotation)).toBe(true);
12
+ });
13
+ });
@@ -0,0 +1,2 @@
1
+ export declare function SceneEnvironment(): null;
2
+ //# sourceMappingURL=scene-environment.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scene-environment.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/scene-environment.tsx"],"names":[],"mappings":"AAmEA,wBAAgB,gBAAgB,SAkB/B"}
@@ -0,0 +1,79 @@
1
+ 'use client';
2
+ import { useThree } from '@react-three/fiber';
3
+ import { useEffect, useMemo } from 'react';
4
+ import * as THREE from 'three/webgpu';
5
+ import { getSceneTheme } from '../../lib/scene-themes';
6
+ import useViewer from '../../store/use-viewer';
7
+ /**
8
+ * Scene IBL — a small procedural gradient sky (cool zenith → warm horizon →
9
+ * dim ground bounce) used purely as the environment light source. The visible
10
+ * backdrop stays the flat theme background (composited in post-processing);
11
+ * this texture is never shown. Replaces the old `venice_sunset_1k.hdr` fetch:
12
+ * no network dependency, and the vertical color split means upward-facing
13
+ * surfaces read cooler than vertical ones instead of everything getting the
14
+ * same directionless warm wash. Exported as an opt-in <Viewer> child so
15
+ * embed / thumbnail surfaces that don't want IBL simply don't mount it.
16
+ * Only affects `rendered` shading (Lambert ignores env maps).
17
+ */
18
+ // Linear-space gradient stops.
19
+ const ZENITH = [0.4, 0.56, 0.78];
20
+ const HORIZON = [0.95, 0.84, 0.66];
21
+ const GROUND = [0.38, 0.35, 0.3];
22
+ const ENV_INTENSITY = 0.6;
23
+ // The gradient sky is a daylight source; dark themes only want a whisper of it.
24
+ const ENV_INTENSITY_DARK = 0.2;
25
+ const WIDTH = 64;
26
+ const HEIGHT = 32;
27
+ function buildGradientSky() {
28
+ const data = new Float32Array(WIDTH * HEIGHT * 4);
29
+ for (let y = 0; y < HEIGHT; y++) {
30
+ // Row 0 = v0 = nadir, top row = zenith (equirect v spans -90°..+90°).
31
+ const lat = ((y + 0.5) / HEIGHT) * 2 - 1;
32
+ let r;
33
+ let g;
34
+ let b;
35
+ if (lat <= 0) {
36
+ // Below the horizon: flat ground bounce, slightly darker toward nadir.
37
+ const k = 1 + lat * 0.35;
38
+ r = GROUND[0] * k;
39
+ g = GROUND[1] * k;
40
+ b = GROUND[2] * k;
41
+ }
42
+ else {
43
+ // pow < 1 widens the warm horizon band.
44
+ const t = lat ** 0.65;
45
+ r = HORIZON[0] + (ZENITH[0] - HORIZON[0]) * t;
46
+ g = HORIZON[1] + (ZENITH[1] - HORIZON[1]) * t;
47
+ b = HORIZON[2] + (ZENITH[2] - HORIZON[2]) * t;
48
+ }
49
+ for (let x = 0; x < WIDTH; x++) {
50
+ const i = (y * WIDTH + x) * 4;
51
+ data[i] = r;
52
+ data[i + 1] = g;
53
+ data[i + 2] = b;
54
+ data[i + 3] = 1;
55
+ }
56
+ }
57
+ const texture = new THREE.DataTexture(data, WIDTH, HEIGHT, THREE.RGBAFormat, THREE.FloatType);
58
+ texture.mapping = THREE.EquirectangularReflectionMapping;
59
+ texture.colorSpace = THREE.LinearSRGBColorSpace;
60
+ texture.needsUpdate = true;
61
+ return texture;
62
+ }
63
+ export function SceneEnvironment() {
64
+ const scene = useThree((state) => state.scene);
65
+ const texture = useMemo(buildGradientSky, []);
66
+ const appearance = useViewer((state) => getSceneTheme(state.sceneTheme).appearance);
67
+ useEffect(() => {
68
+ const prevEnvironment = scene.environment;
69
+ const prevIntensity = scene.environmentIntensity;
70
+ scene.environment = texture;
71
+ scene.environmentIntensity = appearance === 'dark' ? ENV_INTENSITY_DARK : ENV_INTENSITY;
72
+ return () => {
73
+ scene.environment = prevEnvironment;
74
+ scene.environmentIntensity = prevIntensity;
75
+ texture.dispose();
76
+ };
77
+ }, [scene, texture, appearance]);
78
+ return null;
79
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"selection-manager.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/selection-manager.tsx"],"names":[],"mappings":"AA0SA,eAAO,MAAM,gBAAgB,mCA0F5B,CAAA"}
1
+ {"version":3,"file":"selection-manager.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/selection-manager.tsx"],"names":[],"mappings":"AAiTA,eAAO,MAAM,gBAAgB,mCAqG5B,CAAA"}