@pascal-app/viewer 0.9.1 → 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 (176) 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 +5 -1
  5. package/dist/components/renderers/node-renderer.d.ts.map +1 -1
  6. package/dist/components/renderers/node-renderer.js +5 -2
  7. package/dist/components/renderers/parametric-node-renderer.d.ts.map +1 -1
  8. package/dist/components/renderers/parametric-node-renderer.js +7 -4
  9. package/dist/components/viewer/bvh-ecctrl.d.ts +77 -0
  10. package/dist/components/viewer/bvh-ecctrl.d.ts.map +1 -0
  11. package/dist/components/viewer/bvh-ecctrl.js +552 -0
  12. package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
  13. package/dist/components/viewer/frame-limiter.js +39 -4
  14. package/dist/components/viewer/glb-interactive.d.ts +41 -0
  15. package/dist/components/viewer/glb-interactive.d.ts.map +1 -0
  16. package/dist/components/viewer/glb-interactive.js +394 -0
  17. package/dist/components/viewer/glb-reference-nodes.d.ts +29 -0
  18. package/dist/components/viewer/glb-reference-nodes.d.ts.map +1 -0
  19. package/dist/components/viewer/glb-reference-nodes.js +80 -0
  20. package/dist/components/viewer/glb-replace-instances.d.ts +18 -0
  21. package/dist/components/viewer/glb-replace-instances.d.ts.map +1 -0
  22. package/dist/components/viewer/glb-replace-instances.js +61 -0
  23. package/dist/components/viewer/glb-scene.d.ts +54 -0
  24. package/dist/components/viewer/glb-scene.d.ts.map +1 -0
  25. package/dist/components/viewer/glb-scene.js +920 -0
  26. package/dist/components/viewer/glb-walkthrough-controller.d.ts +12 -0
  27. package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -0
  28. package/dist/components/viewer/glb-walkthrough-controller.js +357 -0
  29. package/dist/components/viewer/index.d.ts +19 -2
  30. package/dist/components/viewer/index.d.ts.map +1 -1
  31. package/dist/components/viewer/index.js +104 -12
  32. package/dist/components/viewer/lights.d.ts.map +1 -1
  33. package/dist/components/viewer/lights.js +36 -5
  34. package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
  35. package/dist/components/viewer/perf-monitor.js +17 -2
  36. package/dist/components/viewer/post-processing.d.ts +7 -1
  37. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  38. package/dist/components/viewer/post-processing.js +179 -24
  39. package/dist/components/viewer/registered-systems.d.ts.map +1 -1
  40. package/dist/components/viewer/registered-systems.js +6 -4
  41. package/dist/components/viewer/scene-bvh.d.ts +2 -1
  42. package/dist/components/viewer/scene-bvh.d.ts.map +1 -1
  43. package/dist/components/viewer/scene-bvh.js +3 -0
  44. package/dist/components/viewer/scene-bvh.test.d.ts +2 -0
  45. package/dist/components/viewer/scene-bvh.test.d.ts.map +1 -0
  46. package/dist/components/viewer/scene-bvh.test.js +13 -0
  47. package/dist/components/viewer/scene-environment.d.ts +2 -0
  48. package/dist/components/viewer/scene-environment.d.ts.map +1 -0
  49. package/dist/components/viewer/scene-environment.js +79 -0
  50. package/dist/components/viewer/selection-manager.d.ts.map +1 -1
  51. package/dist/components/viewer/selection-manager.js +18 -10
  52. package/dist/components/viewer/walkthrough-controls.d.ts +1 -0
  53. package/dist/components/viewer/walkthrough-controls.d.ts.map +1 -1
  54. package/dist/components/viewer/walkthrough-controls.js +20 -0
  55. package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
  56. package/dist/hooks/use-gltf-ktx2.js +3 -22
  57. package/dist/hooks/use-node-events.d.ts +1 -1
  58. package/dist/hooks/use-node-events.d.ts.map +1 -1
  59. package/dist/hooks/use-node-events.js +22 -14
  60. package/dist/index.d.ts +19 -8
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +22 -8
  63. package/dist/lib/backdrop.d.ts +33 -0
  64. package/dist/lib/backdrop.d.ts.map +1 -0
  65. package/dist/lib/backdrop.js +92 -0
  66. package/dist/lib/box-uv.d.ts +13 -0
  67. package/dist/lib/box-uv.d.ts.map +1 -0
  68. package/dist/lib/box-uv.js +32 -0
  69. package/dist/lib/csg-utils.d.ts +3 -9
  70. package/dist/lib/csg-utils.d.ts.map +1 -1
  71. package/dist/lib/csg-utils.js +65 -5
  72. package/dist/lib/csg-utils.test.d.ts +2 -0
  73. package/dist/lib/csg-utils.test.d.ts.map +1 -0
  74. package/dist/lib/csg-utils.test.js +45 -0
  75. package/dist/lib/edge-style.d.ts +1 -0
  76. package/dist/lib/edge-style.d.ts.map +1 -1
  77. package/dist/lib/edge-style.js +26 -6
  78. package/dist/lib/ink-edges.d.ts +1 -1
  79. package/dist/lib/ink-edges.d.ts.map +1 -1
  80. package/dist/lib/ink-edges.js +13 -7
  81. package/dist/lib/ktx2-loader.d.ts +43 -0
  82. package/dist/lib/ktx2-loader.d.ts.map +1 -0
  83. package/dist/lib/ktx2-loader.js +136 -0
  84. package/dist/lib/layers.d.ts +10 -0
  85. package/dist/lib/layers.d.ts.map +1 -1
  86. package/dist/lib/layers.js +10 -0
  87. package/dist/lib/materials.d.ts +14 -1
  88. package/dist/lib/materials.d.ts.map +1 -1
  89. package/dist/lib/materials.js +103 -26
  90. package/dist/lib/scene-themes.d.ts +1 -0
  91. package/dist/lib/scene-themes.d.ts.map +1 -1
  92. package/dist/lib/scene-themes.js +25 -16
  93. package/dist/lib/shadow-only.d.ts +4 -0
  94. package/dist/lib/shadow-only.d.ts.map +1 -0
  95. package/dist/lib/shadow-only.js +36 -0
  96. package/dist/lib/tsl-compat.d.ts +12 -0
  97. package/dist/lib/tsl-compat.d.ts.map +1 -0
  98. package/dist/lib/tsl-compat.js +13 -0
  99. package/dist/store/use-viewer.d.ts +60 -18
  100. package/dist/store/use-viewer.d.ts.map +1 -1
  101. package/dist/store/use-viewer.js +175 -3
  102. package/dist/store/use-viewer.test.d.ts +2 -0
  103. package/dist/store/use-viewer.test.d.ts.map +1 -0
  104. package/dist/store/use-viewer.test.js +42 -0
  105. package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
  106. package/dist/systems/ceiling/ceiling-system.js +4 -1
  107. package/dist/systems/door/door-animation.test.d.ts +2 -0
  108. package/dist/systems/door/door-animation.test.d.ts.map +1 -0
  109. package/dist/systems/door/door-animation.test.js +51 -0
  110. package/dist/systems/door/door-floor-alignment.test.d.ts +2 -0
  111. package/dist/systems/door/door-floor-alignment.test.d.ts.map +1 -0
  112. package/dist/systems/door/door-floor-alignment.test.js +50 -0
  113. package/dist/systems/door/door-system.d.ts +24 -0
  114. package/dist/systems/door/door-system.d.ts.map +1 -1
  115. package/dist/systems/door/door-system.js +494 -161
  116. package/dist/systems/fence/fence-system.d.ts +17 -0
  117. package/dist/systems/fence/fence-system.d.ts.map +1 -1
  118. package/dist/systems/fence/fence-system.js +221 -53
  119. package/dist/systems/geometry/geometry-system.d.ts +7 -0
  120. package/dist/systems/geometry/geometry-system.d.ts.map +1 -1
  121. package/dist/systems/geometry/geometry-system.js +95 -26
  122. package/dist/systems/geometry/geometry-system.test.d.ts +2 -0
  123. package/dist/systems/geometry/geometry-system.test.d.ts.map +1 -0
  124. package/dist/systems/geometry/geometry-system.test.js +30 -0
  125. package/dist/systems/interactive/control-widget.d.ts +10 -0
  126. package/dist/systems/interactive/control-widget.d.ts.map +1 -0
  127. package/dist/systems/interactive/control-widget.js +35 -0
  128. package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
  129. package/dist/systems/interactive/interactive-system.js +55 -56
  130. package/dist/systems/item/item-system.d.ts.map +1 -1
  131. package/dist/systems/item/item-system.js +20 -6
  132. package/dist/systems/level/level-system.d.ts.map +1 -1
  133. package/dist/systems/level/level-system.js +28 -4
  134. package/dist/systems/level/level-utils.d.ts +0 -3
  135. package/dist/systems/level/level-utils.d.ts.map +1 -1
  136. package/dist/systems/level/level-utils.js +2 -41
  137. package/dist/systems/roof/roof-materials.d.ts.map +1 -1
  138. package/dist/systems/roof/roof-materials.js +29 -8
  139. package/dist/systems/roof/roof-system.d.ts +21 -8
  140. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  141. package/dist/systems/roof/roof-system.js +1079 -221
  142. package/dist/systems/slab/slab-system.d.ts +5 -3
  143. package/dist/systems/slab/slab-system.d.ts.map +1 -1
  144. package/dist/systems/slab/slab-system.js +41 -23
  145. package/dist/systems/slab/slab-system.test.js +3 -2
  146. package/dist/systems/stair/stair-system.js +6 -3
  147. package/dist/systems/wall/opening-cutout-geometry.d.ts +31 -0
  148. package/dist/systems/wall/opening-cutout-geometry.d.ts.map +1 -0
  149. package/dist/systems/wall/opening-cutout-geometry.js +167 -0
  150. package/dist/systems/wall/opening-cutout-geometry.test.d.ts +2 -0
  151. package/dist/systems/wall/opening-cutout-geometry.test.d.ts.map +1 -0
  152. package/dist/systems/wall/opening-cutout-geometry.test.js +172 -0
  153. package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
  154. package/dist/systems/wall/wall-cutout.js +46 -17
  155. package/dist/systems/wall/wall-materials.d.ts +11 -7
  156. package/dist/systems/wall/wall-materials.d.ts.map +1 -1
  157. package/dist/systems/wall/wall-materials.js +244 -35
  158. package/dist/systems/wall/wall-opening-cutout.test.d.ts +2 -0
  159. package/dist/systems/wall/wall-opening-cutout.test.d.ts.map +1 -0
  160. package/dist/systems/wall/wall-opening-cutout.test.js +59 -0
  161. package/dist/systems/wall/wall-support-extension.test.d.ts +2 -0
  162. package/dist/systems/wall/wall-support-extension.test.d.ts.map +1 -0
  163. package/dist/systems/wall/wall-support-extension.test.js +48 -0
  164. package/dist/systems/wall/wall-system.d.ts +2 -2
  165. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  166. package/dist/systems/wall/wall-system.js +329 -174
  167. package/dist/systems/window/window-animation-system.d.ts +12 -0
  168. package/dist/systems/window/window-animation-system.d.ts.map +1 -1
  169. package/dist/systems/window/window-animation-system.js +16 -5
  170. package/dist/systems/window/window-system.d.ts +7 -0
  171. package/dist/systems/window/window-system.d.ts.map +1 -1
  172. package/dist/systems/window/window-system.js +240 -14
  173. package/package.json +4 -4
  174. package/dist/components/viewer/ground-occluder.d.ts +0 -2
  175. package/dist/components/viewer/ground-occluder.d.ts.map +0 -1
  176. package/dist/components/viewer/ground-occluder.js +0 -67
@@ -3,6 +3,7 @@ import { sceneRegistry } from '@pascal-app/core';
3
3
  import { useFrame } from '@react-three/fiber';
4
4
  import { useMemo, useRef } from 'react';
5
5
  import * as THREE from 'three/webgpu';
6
+ import { SHADOW_ONLY_LAYER } from '../../lib/layers';
6
7
  import { getSceneTheme } from '../../lib/scene-themes';
7
8
  import useViewer from '../../store/use-viewer';
8
9
  // Diagnostic toggle: `?disable=shadows` skips the shadow-map render pass
@@ -12,10 +13,27 @@ const SHADOWS_DISABLED = typeof window !== 'undefined' &&
12
13
  new Set((new URLSearchParams(window.location.search).get('disable') ?? '')
13
14
  .split(',')
14
15
  .map((s) => s.trim())).has('shadows');
16
+ // Diagnostic toggle: `?debug=shadowcamera` draws a CameraHelper for each
17
+ // shadow camera so the building-fit frustum (and thus shadow texel density)
18
+ // can be inspected while tuning margins/bias.
19
+ const SHADOW_CAMERA_DEBUG = typeof window !== 'undefined' &&
20
+ new Set((new URLSearchParams(window.location.search).get('debug') ?? '')
21
+ .split(',')
22
+ .map((s) => s.trim())).has('shadowcamera');
15
23
  // Shadow darkness for the bright key lights (themes drive most lights past
16
- // intensity 1). The aesthetic prototype runs these near-black (≈1.0); this is a
17
- // deliberate middle ground — present, but not the heavy contact shadow there.
18
- const MAX_SHADOW_INTENSITY = 0.55;
24
+ // intensity 1). Runs high so shadowed areas actually lose the sun's
25
+ // contribution — the ambient/hemisphere/IBL stack provides the fill. The old
26
+ // 0.55 clamp leaked 45% of the key light into shadow and flattened interiors;
27
+ // 0.9 read too heavy in review.
28
+ const MAX_SHADOW_INTENSITY = 0.75;
29
+ // `normalBias` is measured in world units. The previous 0.3 moved shadow
30
+ // lookups 30 cm off their surfaces, visibly detaching wall shadows at the
31
+ // floor; 0.07 and below still acnes on the building-fit 1024 map (large
32
+ // texels), so 0.08 is the smallest acne-free value at that resolution — even
33
+ // with the depth bias at -0.0005 (which is itself needed: 0.08 alone still
34
+ // showed faint acne at -0.0001).
35
+ const SHADOW_DEPTH_BIAS = -0.0005;
36
+ const SHADOW_NORMAL_BIAS = 0.08;
19
37
  // Shadow frustum framing. The frustum is fit to the BUILDING geometry (not the
20
38
  // camera): we union the bounds of all registered scene nodes, fit a sphere, and
21
39
  // size the directional light's ortho shadow camera to that sphere plus a margin.
@@ -54,6 +72,7 @@ export function Lights() {
54
72
  const boundsBox = useRef(new THREE.Box3()); // scratch: union AABB
55
73
  const boundsSphere = useRef(new THREE.Sphere()); // scratch: fitted sphere
56
74
  const lastBoundsTime = useRef(-1); // last refresh timestamp (-1 = never)
75
+ const shadowHelpers = useRef([]);
57
76
  const hemiRef = useRef(null);
58
77
  const ambientRef = useRef(null);
59
78
  const initialized = useRef(false);
@@ -129,6 +148,9 @@ export function Lights() {
129
148
  // the <orthographicCamera attach="shadow-camera"> below.
130
149
  const cam = light.shadow?.camera;
131
150
  if (cam) {
151
+ // Shadow-caster-only geometry (hidden roofs/levels in cutaway views)
152
+ // is visible to the shadow pass alone — see lib/shadow-only.ts.
153
+ cam.layers.enable(SHADOW_ONLY_LAYER);
132
154
  cam.left = -size;
133
155
  cam.right = size;
134
156
  cam.top = size;
@@ -136,6 +158,15 @@ export function Lights() {
136
158
  cam.near = near;
137
159
  cam.far = far;
138
160
  cam.updateProjectionMatrix();
161
+ if (SHADOW_CAMERA_DEBUG) {
162
+ let helper = shadowHelpers.current[index];
163
+ if (!helper) {
164
+ helper = new THREE.CameraHelper(cam);
165
+ shadowHelpers.current[index] = helper;
166
+ state.scene.add(helper);
167
+ }
168
+ helper.update();
169
+ }
139
170
  }
140
171
  }
141
172
  }
@@ -195,7 +226,7 @@ export function Lights() {
195
226
  ambientRef.current.color.lerp(targets.ambColor, dt);
196
227
  }
197
228
  });
198
- return (_jsxs(_Fragment, { children: [theme.lights.map((light, index) => (_jsx("directionalLight", { castShadow: Boolean(light.castShadow) && !SHADOWS_DISABLED && shadows, position: light.position, ref: (ref) => {
229
+ return (_jsxs(_Fragment, { children: [theme.lights.map((light, index) => (_jsx("directionalLight", { castShadow: Boolean(light.castShadow) && !SHADOWS_DISABLED, position: light.position, ref: (ref) => {
199
230
  lightRefs.current[index] = ref;
200
- }, "shadow-bias": -0.002, "shadow-mapSize": [1024, 1024], "shadow-normalBias": 0.3, "shadow-radius": 1.5, children: light.castShadow && !SHADOWS_DISABLED && shadows ? (_jsx("orthographicCamera", { attach: "shadow-camera", bottom: -shadowCameraSize, far: 400, left: -shadowCameraSize, near: 1, ref: shadowCamera, right: shadowCameraSize, top: shadowCameraSize })) : null }, `${index}-${light.position.join(',')}`))), theme.hemi ? _jsx("hemisphereLight", { ref: hemiRef }) : null, _jsx("ambientLight", { ref: ambientRef })] }));
231
+ }, "shadow-bias": SHADOW_DEPTH_BIAS, "shadow-mapSize": [1024, 1024], "shadow-normalBias": SHADOW_NORMAL_BIAS, "shadow-radius": 2, children: light.castShadow && !SHADOWS_DISABLED ? (_jsx("orthographicCamera", { attach: "shadow-camera", bottom: -shadowCameraSize, far: 400, left: -shadowCameraSize, near: 1, ref: shadowCamera, right: shadowCameraSize, top: shadowCameraSize })) : null }, `${index}-${light.position.join(',')}`))), theme.hemi ? _jsx("hemisphereLight", { ref: hemiRef }) : null, _jsx("ambientLight", { ref: ambientRef })] }));
201
232
  }
@@ -1 +1 @@
1
- {"version":3,"file":"perf-monitor.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/perf-monitor.tsx"],"names":[],"mappings":"AAQA,eAAO,MAAM,WAAW,mCAuIvB,CAAA"}
1
+ {"version":3,"file":"perf-monitor.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/perf-monitor.tsx"],"names":[],"mappings":"AAQA,eAAO,MAAM,WAAW,mCAsJvB,CAAA"}
@@ -14,6 +14,7 @@ export const PerfMonitor = () => {
14
14
  drawCalls: 0,
15
15
  triangles: 0,
16
16
  dirty: 0,
17
+ dirtyDetail: '',
17
18
  meshes: 0,
18
19
  lines: 0,
19
20
  sprites: 0,
@@ -57,7 +58,20 @@ export const PerfMonitor = () => {
57
58
  const drawCalls = Math.round(totalCalls / Math.max(1, frameCount.current));
58
59
  const triangles = totalTriangles / Math.max(1, frameCount.current);
59
60
  info.reset();
60
- const dirty = useScene.getState().dirtyNodes.size;
61
+ const sceneState = useScene.getState();
62
+ const dirty = sceneState.dirtyNodes.size;
63
+ let dirtyDetail = '';
64
+ if (dirty > 0) {
65
+ const counts = new Map();
66
+ for (const id of sceneState.dirtyNodes) {
67
+ const type = sceneState.nodes[id]?.type ?? 'missing';
68
+ counts.set(type, (counts.get(type) ?? 0) + 1);
69
+ }
70
+ dirtyDetail = [...counts.entries()]
71
+ .sort((a, b) => b[1] - a[1])
72
+ .map(([type, count]) => `${count} ${type}`)
73
+ .join(', ');
74
+ }
61
75
  // Count visible drawables by type so we can match scene contents
62
76
  // against the renderer's draw count and find hidden contributors.
63
77
  let meshes = 0;
@@ -99,6 +113,7 @@ export const PerfMonitor = () => {
99
113
  drawCalls,
100
114
  triangles,
101
115
  dirty,
116
+ dirtyDetail,
102
117
  meshes,
103
118
  lines,
104
119
  sprites,
@@ -122,7 +137,7 @@ export const PerfMonitor = () => {
122
137
  GPU ${stats.gpuMs > 0 ? `${stats.gpuMs.toFixed(1)}ms (max ${stats.gpuMaxMs.toFixed(1)})` : '—'}
123
138
  DRAW ${stats.drawCalls}
124
139
  TRI ${(stats.triangles / 1000).toFixed(1)}k
125
- DIRTY ${stats.dirty}
140
+ DIRTY ${stats.dirty}${stats.dirtyDetail ? ` (${stats.dirtyDetail})` : ''}
126
141
  MESH ${stats.meshes}
127
142
  LINE ${stats.lines}
128
143
  SPRITE ${stats.sprites}
@@ -1,3 +1,7 @@
1
+ export declare const GRADE_PARAMS: {
2
+ contrast: number;
3
+ saturation: number;
4
+ };
1
5
  export declare const SSGI_PARAMS: {
2
6
  enabled: boolean;
3
7
  sliceCount: number;
@@ -22,8 +26,10 @@ export type HoverStyles = {
22
26
  default: HoverStyle;
23
27
  } & Record<string, HoverStyle>;
24
28
  export declare const DEFAULT_HOVER_STYLES: HoverStyles;
25
- declare const PostProcessingPasses: ({ hoverStyles, }: {
29
+ declare const PostProcessingPasses: ({ hoverStyles, disablePostFx, }: {
26
30
  hoverStyles?: HoverStyles;
31
+ /** Host-controlled equivalent of `?disable=postFx` — see the Viewer prop. */
32
+ disablePostFx?: boolean;
27
33
  }) => null;
28
34
  export default PostProcessingPasses;
29
35
  //# sourceMappingURL=post-processing.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"post-processing.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/post-processing.tsx"],"names":[],"mappings":"AAgCA,eAAO,MAAM,WAAW;;;;;;;;;;;;;CAavB,CAAA;AA0CD,MAAM,MAAM,UAAU,GAAG;IACvB,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,OAAO,CAAA;CACf,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,OAAO,EAAE,UAAU,CAAA;CACpB,GAAG,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;AAS9B,eAAO,MAAM,oBAAoB,EAAE,WAElC,CAAA;AAiBD,QAAA,MAAM,oBAAoB,GAAI,kBAE3B;IACD,WAAW,CAAC,EAAE,WAAW,CAAA;CAC1B,SAgdA,CAAA;AAED,eAAe,oBAAoB,CAAA"}
1
+ {"version":3,"file":"post-processing.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/post-processing.tsx"],"names":[],"mappings":"AAwCA,eAAO,MAAM,YAAY;;;CAGxB,CAAA;AAGD,eAAO,MAAM,WAAW;;;;;;;;;;;;;CAavB,CAAA;AA0DD,MAAM,MAAM,UAAU,GAAG;IACvB,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,OAAO,CAAA;CACf,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,OAAO,EAAE,UAAU,CAAA;CACpB,GAAG,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;AAS9B,eAAO,MAAM,oBAAoB,EAAE,WAElC,CAAA;AAiBD,QAAA,MAAM,oBAAoB,GAAI,iCAG3B;IACD,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,6EAA6E;IAC7E,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB,SA8mBA,CAAA;AAED,eAAe,oBAAoB,CAAA"}
@@ -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);
@@ -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