@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.
- package/dist/components/error-boundary.d.ts +6 -0
- package/dist/components/error-boundary.d.ts.map +1 -1
- package/dist/components/error-boundary.js +6 -0
- package/dist/components/renderers/node-renderer.d.ts +5 -1
- package/dist/components/renderers/node-renderer.d.ts.map +1 -1
- package/dist/components/renderers/node-renderer.js +5 -2
- package/dist/components/renderers/parametric-node-renderer.d.ts.map +1 -1
- package/dist/components/renderers/parametric-node-renderer.js +7 -4
- package/dist/components/viewer/bvh-ecctrl.d.ts +77 -0
- package/dist/components/viewer/bvh-ecctrl.d.ts.map +1 -0
- package/dist/components/viewer/bvh-ecctrl.js +552 -0
- package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
- package/dist/components/viewer/frame-limiter.js +39 -4
- package/dist/components/viewer/glb-interactive.d.ts +41 -0
- package/dist/components/viewer/glb-interactive.d.ts.map +1 -0
- package/dist/components/viewer/glb-interactive.js +394 -0
- package/dist/components/viewer/glb-reference-nodes.d.ts +29 -0
- package/dist/components/viewer/glb-reference-nodes.d.ts.map +1 -0
- package/dist/components/viewer/glb-reference-nodes.js +80 -0
- package/dist/components/viewer/glb-replace-instances.d.ts +18 -0
- package/dist/components/viewer/glb-replace-instances.d.ts.map +1 -0
- package/dist/components/viewer/glb-replace-instances.js +61 -0
- package/dist/components/viewer/glb-scene.d.ts +54 -0
- package/dist/components/viewer/glb-scene.d.ts.map +1 -0
- package/dist/components/viewer/glb-scene.js +920 -0
- package/dist/components/viewer/glb-walkthrough-controller.d.ts +12 -0
- package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -0
- package/dist/components/viewer/glb-walkthrough-controller.js +357 -0
- package/dist/components/viewer/index.d.ts +19 -2
- package/dist/components/viewer/index.d.ts.map +1 -1
- package/dist/components/viewer/index.js +104 -12
- package/dist/components/viewer/lights.d.ts.map +1 -1
- package/dist/components/viewer/lights.js +36 -5
- package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
- package/dist/components/viewer/perf-monitor.js +17 -2
- package/dist/components/viewer/post-processing.d.ts +7 -1
- package/dist/components/viewer/post-processing.d.ts.map +1 -1
- package/dist/components/viewer/post-processing.js +179 -24
- package/dist/components/viewer/registered-systems.d.ts.map +1 -1
- package/dist/components/viewer/registered-systems.js +6 -4
- package/dist/components/viewer/scene-bvh.d.ts +2 -1
- package/dist/components/viewer/scene-bvh.d.ts.map +1 -1
- package/dist/components/viewer/scene-bvh.js +3 -0
- package/dist/components/viewer/scene-bvh.test.d.ts +2 -0
- package/dist/components/viewer/scene-bvh.test.d.ts.map +1 -0
- package/dist/components/viewer/scene-bvh.test.js +13 -0
- package/dist/components/viewer/scene-environment.d.ts +2 -0
- package/dist/components/viewer/scene-environment.d.ts.map +1 -0
- package/dist/components/viewer/scene-environment.js +79 -0
- package/dist/components/viewer/selection-manager.d.ts.map +1 -1
- package/dist/components/viewer/selection-manager.js +18 -10
- package/dist/components/viewer/walkthrough-controls.d.ts +1 -0
- package/dist/components/viewer/walkthrough-controls.d.ts.map +1 -1
- package/dist/components/viewer/walkthrough-controls.js +20 -0
- package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
- package/dist/hooks/use-gltf-ktx2.js +3 -22
- package/dist/hooks/use-node-events.d.ts +1 -1
- package/dist/hooks/use-node-events.d.ts.map +1 -1
- package/dist/hooks/use-node-events.js +22 -14
- package/dist/index.d.ts +19 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -8
- package/dist/lib/backdrop.d.ts +33 -0
- package/dist/lib/backdrop.d.ts.map +1 -0
- package/dist/lib/backdrop.js +92 -0
- package/dist/lib/box-uv.d.ts +13 -0
- package/dist/lib/box-uv.d.ts.map +1 -0
- package/dist/lib/box-uv.js +32 -0
- package/dist/lib/csg-utils.d.ts +3 -9
- package/dist/lib/csg-utils.d.ts.map +1 -1
- package/dist/lib/csg-utils.js +65 -5
- package/dist/lib/csg-utils.test.d.ts +2 -0
- package/dist/lib/csg-utils.test.d.ts.map +1 -0
- package/dist/lib/csg-utils.test.js +45 -0
- package/dist/lib/edge-style.d.ts +1 -0
- package/dist/lib/edge-style.d.ts.map +1 -1
- package/dist/lib/edge-style.js +26 -6
- package/dist/lib/ink-edges.d.ts +1 -1
- package/dist/lib/ink-edges.d.ts.map +1 -1
- package/dist/lib/ink-edges.js +13 -7
- package/dist/lib/ktx2-loader.d.ts +43 -0
- package/dist/lib/ktx2-loader.d.ts.map +1 -0
- package/dist/lib/ktx2-loader.js +136 -0
- package/dist/lib/layers.d.ts +10 -0
- package/dist/lib/layers.d.ts.map +1 -1
- package/dist/lib/layers.js +10 -0
- package/dist/lib/materials.d.ts +14 -1
- package/dist/lib/materials.d.ts.map +1 -1
- package/dist/lib/materials.js +103 -26
- package/dist/lib/scene-themes.d.ts +1 -0
- package/dist/lib/scene-themes.d.ts.map +1 -1
- package/dist/lib/scene-themes.js +25 -16
- package/dist/lib/shadow-only.d.ts +4 -0
- package/dist/lib/shadow-only.d.ts.map +1 -0
- package/dist/lib/shadow-only.js +36 -0
- package/dist/lib/tsl-compat.d.ts +12 -0
- package/dist/lib/tsl-compat.d.ts.map +1 -0
- package/dist/lib/tsl-compat.js +13 -0
- package/dist/store/use-viewer.d.ts +60 -18
- package/dist/store/use-viewer.d.ts.map +1 -1
- package/dist/store/use-viewer.js +175 -3
- package/dist/store/use-viewer.test.d.ts +2 -0
- package/dist/store/use-viewer.test.d.ts.map +1 -0
- package/dist/store/use-viewer.test.js +42 -0
- package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
- package/dist/systems/ceiling/ceiling-system.js +4 -1
- package/dist/systems/door/door-animation.test.d.ts +2 -0
- package/dist/systems/door/door-animation.test.d.ts.map +1 -0
- package/dist/systems/door/door-animation.test.js +51 -0
- package/dist/systems/door/door-floor-alignment.test.d.ts +2 -0
- package/dist/systems/door/door-floor-alignment.test.d.ts.map +1 -0
- package/dist/systems/door/door-floor-alignment.test.js +50 -0
- package/dist/systems/door/door-system.d.ts +24 -0
- package/dist/systems/door/door-system.d.ts.map +1 -1
- package/dist/systems/door/door-system.js +494 -161
- package/dist/systems/fence/fence-system.d.ts +17 -0
- package/dist/systems/fence/fence-system.d.ts.map +1 -1
- package/dist/systems/fence/fence-system.js +221 -53
- package/dist/systems/geometry/geometry-system.d.ts +7 -0
- package/dist/systems/geometry/geometry-system.d.ts.map +1 -1
- package/dist/systems/geometry/geometry-system.js +95 -26
- package/dist/systems/geometry/geometry-system.test.d.ts +2 -0
- package/dist/systems/geometry/geometry-system.test.d.ts.map +1 -0
- package/dist/systems/geometry/geometry-system.test.js +30 -0
- package/dist/systems/interactive/control-widget.d.ts +10 -0
- package/dist/systems/interactive/control-widget.d.ts.map +1 -0
- package/dist/systems/interactive/control-widget.js +35 -0
- package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
- package/dist/systems/interactive/interactive-system.js +55 -56
- package/dist/systems/item/item-system.d.ts.map +1 -1
- package/dist/systems/item/item-system.js +20 -6
- package/dist/systems/level/level-system.d.ts.map +1 -1
- package/dist/systems/level/level-system.js +28 -4
- package/dist/systems/level/level-utils.d.ts +0 -3
- package/dist/systems/level/level-utils.d.ts.map +1 -1
- package/dist/systems/level/level-utils.js +2 -41
- package/dist/systems/roof/roof-materials.d.ts.map +1 -1
- package/dist/systems/roof/roof-materials.js +29 -8
- package/dist/systems/roof/roof-system.d.ts +21 -8
- package/dist/systems/roof/roof-system.d.ts.map +1 -1
- package/dist/systems/roof/roof-system.js +1079 -221
- package/dist/systems/slab/slab-system.d.ts +5 -3
- package/dist/systems/slab/slab-system.d.ts.map +1 -1
- package/dist/systems/slab/slab-system.js +41 -23
- package/dist/systems/slab/slab-system.test.js +3 -2
- package/dist/systems/stair/stair-system.js +6 -3
- package/dist/systems/wall/opening-cutout-geometry.d.ts +31 -0
- package/dist/systems/wall/opening-cutout-geometry.d.ts.map +1 -0
- package/dist/systems/wall/opening-cutout-geometry.js +167 -0
- package/dist/systems/wall/opening-cutout-geometry.test.d.ts +2 -0
- package/dist/systems/wall/opening-cutout-geometry.test.d.ts.map +1 -0
- package/dist/systems/wall/opening-cutout-geometry.test.js +172 -0
- package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
- package/dist/systems/wall/wall-cutout.js +46 -17
- package/dist/systems/wall/wall-materials.d.ts +11 -7
- package/dist/systems/wall/wall-materials.d.ts.map +1 -1
- package/dist/systems/wall/wall-materials.js +244 -35
- package/dist/systems/wall/wall-opening-cutout.test.d.ts +2 -0
- package/dist/systems/wall/wall-opening-cutout.test.d.ts.map +1 -0
- package/dist/systems/wall/wall-opening-cutout.test.js +59 -0
- package/dist/systems/wall/wall-support-extension.test.d.ts +2 -0
- package/dist/systems/wall/wall-support-extension.test.d.ts.map +1 -0
- package/dist/systems/wall/wall-support-extension.test.js +48 -0
- package/dist/systems/wall/wall-system.d.ts +2 -2
- package/dist/systems/wall/wall-system.d.ts.map +1 -1
- package/dist/systems/wall/wall-system.js +329 -174
- package/dist/systems/window/window-animation-system.d.ts +12 -0
- package/dist/systems/window/window-animation-system.d.ts.map +1 -1
- package/dist/systems/window/window-animation-system.js +16 -5
- package/dist/systems/window/window-system.d.ts +7 -0
- package/dist/systems/window/window-system.d.ts.map +1 -1
- package/dist/systems/window/window-system.js +240 -14
- package/package.json +4 -4
- package/dist/components/viewer/ground-occluder.d.ts +0 -2
- package/dist/components/viewer/ground-occluder.d.ts.map +0 -1
- 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).
|
|
17
|
-
//
|
|
18
|
-
|
|
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
|
|
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":
|
|
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,
|
|
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
|
|
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":"
|
|
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,
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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:
|
|
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) =>
|
|
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
|
-
|
|
304
|
-
|
|
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
|
|
309
|
-
const aoAsRgb = vec4(
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
572
|
+
if (disablePostFx ||
|
|
573
|
+
PERF_POST_FX_DISABLED ||
|
|
574
|
+
hasPipelineErrorRef.current ||
|
|
575
|
+
!renderPipelineRef.current) {
|
|
426
576
|
try {
|
|
427
|
-
|
|
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(
|
|
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":"
|
|
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,
|
|
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 @@
|
|
|
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
|
+
});
|