@pascal-app/viewer 0.9.1 → 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -5
- 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 +59 -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 +22 -0
- package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -0
- package/dist/components/viewer/glb-walkthrough-controller.js +445 -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 +113 -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 +180 -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 +23 -12
- package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
- package/dist/hooks/use-gltf-ktx2.js +2 -24
- 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 +24 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +29 -13
- 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/hero-pose.d.ts +47 -0
- package/dist/lib/hero-pose.d.ts.map +1 -0
- package/dist/lib/hero-pose.js +227 -0
- 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 +46 -0
- package/dist/lib/ktx2-loader.d.ts.map +1 -0
- package/dist/lib/ktx2-loader.js +142 -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 +16 -1
- package/dist/lib/materials.d.ts.map +1 -1
- package/dist/lib/materials.js +158 -34
- package/dist/lib/materials.test.d.ts +2 -0
- package/dist/lib/materials.test.d.ts.map +1 -0
- package/dist/lib/materials.test.js +25 -0
- package/dist/lib/merged-outline-node.test.d.ts +2 -0
- package/dist/lib/merged-outline-node.test.d.ts.map +1 -0
- package/dist/lib/merged-outline-node.test.js +22 -0
- 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/snapshot-pipeline.d.ts +42 -0
- package/dist/lib/snapshot-pipeline.d.ts.map +1 -0
- package/dist/lib/snapshot-pipeline.js +254 -0
- package/dist/lib/terrain-perimeter-fill.d.ts +7 -0
- package/dist/lib/terrain-perimeter-fill.d.ts.map +1 -0
- package/dist/lib/terrain-perimeter-fill.js +75 -0
- package/dist/lib/texture-node-guard.d.ts +13 -0
- package/dist/lib/texture-node-guard.d.ts.map +1 -0
- package/dist/lib/texture-node-guard.js +39 -0
- package/dist/lib/texture-reference.d.ts +31 -0
- package/dist/lib/texture-reference.d.ts.map +1 -0
- package/dist/lib/texture-reference.js +139 -0
- package/dist/lib/texture-reference.test.d.ts +2 -0
- package/dist/lib/texture-reference.test.d.ts.map +1 -0
- package/dist/lib/texture-reference.test.js +86 -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 +73 -18
- package/dist/store/use-viewer.d.ts.map +1 -1
- package/dist/store/use-viewer.js +191 -4
- 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 +61 -0
- package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
- package/dist/systems/ceiling/ceiling-system.js +12 -5
- 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/floor-elevation/floor-elevation-system.d.ts.map +1 -1
- package/dist/systems/floor-elevation/floor-elevation-system.js +30 -7
- 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 +34 -9
- 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 +7 -45
- 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 +7 -4
- package/dist/systems/slab/slab-system.d.ts.map +1 -1
- package/dist/systems/slab/slab-system.js +31 -83
- package/dist/systems/slab/slab-system.test.js +54 -14
- package/dist/systems/stair/stair-system.js +7 -4
- 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 +49 -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 +116 -0
- package/dist/systems/wall/wall-system.d.ts +4 -8
- package/dist/systems/wall/wall-system.d.ts.map +1 -1
- package/dist/systems/wall/wall-system.js +397 -177
- package/dist/systems/wall/wall-terrain-bottom.test.d.ts +2 -0
- package/dist/systems/wall/wall-terrain-bottom.test.d.ts.map +1 -0
- package/dist/systems/wall/wall-terrain-bottom.test.js +34 -0
- 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
- package/dist/components/viewer/walkthrough-controls.d.ts +0 -2
- package/dist/components/viewer/walkthrough-controls.d.ts.map +0 -1
- package/dist/components/viewer/walkthrough-controls.js +0 -131
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { nodeRegistry, StairOpeningSystem, sceneRegistry, useScene, } from '@pascal-app/core';
|
|
4
4
|
import { Canvas, extend, useFrame, useThree } from '@react-three/fiber';
|
|
5
|
-
import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react';
|
|
5
|
+
import { forwardRef, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState, } from 'react';
|
|
6
6
|
import * as THREE from 'three/webgpu';
|
|
7
7
|
import { hasDrawableGeometry } from '../../lib/drawable-geometry';
|
|
8
8
|
import { PERF_OVERLAY_ENABLED, pushGpuSample } from '../../lib/gpu-perf';
|
|
9
9
|
import { applyIsolation, clearIsolation } from '../../lib/isolation';
|
|
10
|
+
import { ensureKtx2Support } from '../../lib/ktx2-loader';
|
|
10
11
|
import { getSceneTheme } from '../../lib/scene-themes';
|
|
12
|
+
import { installTextureNodeNullGuard } from '../../lib/texture-node-guard';
|
|
11
13
|
import useViewer, {} from '../../store/use-viewer';
|
|
12
14
|
import { FloorElevationSystem } from '../../systems/floor-elevation/floor-elevation-system';
|
|
13
15
|
import { GeometrySystem } from '../../systems/geometry/geometry-system';
|
|
@@ -21,6 +23,9 @@ import { RegisteredSystems } from './registered-systems';
|
|
|
21
23
|
import { SceneBvh } from './scene-bvh';
|
|
22
24
|
import { SelectionManager } from './selection-manager';
|
|
23
25
|
import { ViewerCamera } from './viewer-camera';
|
|
26
|
+
// Must be in place before any node material builds — a null texture pulled by
|
|
27
|
+
// a shared override-material pass otherwise kills the render pass outright.
|
|
28
|
+
installTextureNodeNullGuard();
|
|
24
29
|
extend(THREE);
|
|
25
30
|
// R3F's <Canvas> useLayoutEffect has no deps, so any re-render (theme switch,
|
|
26
31
|
// parent re-render, StrictMode double-mount) re-invokes `configure()`. With a
|
|
@@ -51,6 +56,27 @@ const DIRTY_BUILD_KINDS = new Set([
|
|
|
51
56
|
'window',
|
|
52
57
|
]);
|
|
53
58
|
const warnedEmptyDraw = process.env.NODE_ENV === 'production' ? null : new WeakSet();
|
|
59
|
+
function canCreateWebGLContext() {
|
|
60
|
+
if (typeof document === 'undefined')
|
|
61
|
+
return false;
|
|
62
|
+
const canvas = document.createElement('canvas');
|
|
63
|
+
try {
|
|
64
|
+
return Boolean(canvas.getContext('webgl2') ?? canvas.getContext('webgl'));
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function canMountGpuViewer() {
|
|
71
|
+
if (typeof window === 'undefined')
|
|
72
|
+
return false;
|
|
73
|
+
if (!('gpu' in navigator) && !canCreateWebGLContext())
|
|
74
|
+
return false;
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
function UnsupportedGpuViewerFallback() {
|
|
78
|
+
return (_jsx("div", { className: "flex h-full min-h-64 w-full items-center justify-center bg-[#fafafa] p-6 text-center text-neutral-900", children: _jsxs("div", { className: "max-w-md rounded-2xl border border-neutral-200 bg-white p-6 shadow-sm", children: [_jsx("h2", { className: "font-semibold text-lg", children: "3D viewer unavailable" }), _jsx("p", { className: "mt-2 text-neutral-600 text-sm", children: "This browser or environment does not expose WebGPU or WebGL, so Pascal cannot render the 3D scene here. Try opening the editor in a browser with hardware acceleration enabled." })] }) }));
|
|
79
|
+
}
|
|
54
80
|
/**
|
|
55
81
|
* Renderer-level safety net against the empty-vertex-buffer crash.
|
|
56
82
|
*
|
|
@@ -83,6 +109,10 @@ function installEmptyDrawGuard(renderer) {
|
|
|
83
109
|
function GPUDeviceWatcher() {
|
|
84
110
|
const gl = useThree((s) => s.gl);
|
|
85
111
|
useEffect(() => {
|
|
112
|
+
// Detect KTX2 transcode support as soon as the renderer exists, so catalog
|
|
113
|
+
// `.ktx2` finish textures load even in scenes with no GLB items (whose
|
|
114
|
+
// loader would otherwise be the only thing to call this).
|
|
115
|
+
ensureKtx2Support(gl);
|
|
86
116
|
const backend = gl.backend;
|
|
87
117
|
const device = backend?.device;
|
|
88
118
|
if (!device) {
|
|
@@ -123,11 +153,23 @@ function ToneMappingExposure() {
|
|
|
123
153
|
return null;
|
|
124
154
|
}
|
|
125
155
|
function hasPendingSceneBuildWork() {
|
|
126
|
-
const { dirtyNodes, nodes } = useScene.getState();
|
|
156
|
+
const { dirtyNodes, nodes, rootNodeIds } = useScene.getState();
|
|
127
157
|
for (const id of dirtyNodes) {
|
|
128
158
|
const node = nodes[id];
|
|
129
159
|
if (!node)
|
|
130
160
|
continue;
|
|
161
|
+
// Unreachable nodes (orphaned by a broken detach: the parent doesn't list
|
|
162
|
+
// them in `children`, or no parent and not a root) never render — every
|
|
163
|
+
// renderer enumerates the parent's `children` array — so no system will
|
|
164
|
+
// ever build them or clear their mark. They must not hold scene-ready
|
|
165
|
+
// hostage (observed in prod scene data: two dangling windows kept every
|
|
166
|
+
// bake of that scene waiting out the full readiness cap).
|
|
167
|
+
const parent = node.parentId ? nodes[node.parentId] : undefined;
|
|
168
|
+
const reachable = parent
|
|
169
|
+
? parent.children?.includes(id) === true
|
|
170
|
+
: rootNodeIds.includes(id);
|
|
171
|
+
if (!reachable)
|
|
172
|
+
continue;
|
|
131
173
|
const def = nodeRegistry.get(node.type);
|
|
132
174
|
if (def?.geometry || def?.capabilities?.floorPlaced || DIRTY_BUILD_KINDS.has(node.type)) {
|
|
133
175
|
return true;
|
|
@@ -141,10 +183,12 @@ function hasCommittedSceneRoot() {
|
|
|
141
183
|
return Object.keys(nodes).length === 0;
|
|
142
184
|
return rootNodeIds.some((id) => sceneRegistry.nodes.has(id));
|
|
143
185
|
}
|
|
144
|
-
function SceneReadyTracker({ onSceneReadyChange, sceneReadyKey, }) {
|
|
186
|
+
function SceneReadyTracker({ onSceneReadyChange, sceneReadyKey, sceneReadyMaxWaitMs, }) {
|
|
187
|
+
const invalidate = useThree((state) => state.invalidate);
|
|
145
188
|
const readyRef = useRef(false);
|
|
146
189
|
const settledFramesRef = useRef(0);
|
|
147
190
|
const waitedFramesRef = useRef(0);
|
|
191
|
+
const waitStartRef = useRef(null);
|
|
148
192
|
const onSceneReadyChangeRef = useRef(onSceneReadyChange);
|
|
149
193
|
useEffect(() => {
|
|
150
194
|
onSceneReadyChangeRef.current = onSceneReadyChange;
|
|
@@ -154,26 +198,44 @@ function SceneReadyTracker({ onSceneReadyChange, sceneReadyKey, }) {
|
|
|
154
198
|
readyRef.current = false;
|
|
155
199
|
settledFramesRef.current = 0;
|
|
156
200
|
waitedFramesRef.current = 0;
|
|
201
|
+
waitStartRef.current = null;
|
|
157
202
|
onSceneReadyChangeRef.current?.(false);
|
|
158
|
-
|
|
203
|
+
invalidate();
|
|
204
|
+
}, [invalidate, sceneReadyKey]);
|
|
159
205
|
useFrame(() => {
|
|
160
206
|
if (!(onSceneReadyChangeRef.current && !readyRef.current))
|
|
161
207
|
return;
|
|
162
208
|
waitedFramesRef.current += 1;
|
|
163
|
-
|
|
164
|
-
|
|
209
|
+
waitStartRef.current ??= performance.now();
|
|
210
|
+
// Give-up cap so a permanently-dirty node can't block readiness forever.
|
|
211
|
+
// The frame-count default assumes display-rate frames; a host whose frame
|
|
212
|
+
// cadence is decoupled from wall time (the headless bake page's timer-driven
|
|
213
|
+
// loop runs 180 frames in 3.6s — faster than a cold item download) passes
|
|
214
|
+
// `sceneReadyMaxWaitMs` to make the cap wall-clock instead.
|
|
215
|
+
const capReached = sceneReadyMaxWaitMs
|
|
216
|
+
? performance.now() - waitStartRef.current >= sceneReadyMaxWaitMs
|
|
217
|
+
: waitedFramesRef.current >= SCENE_READY_MAX_WAIT_FRAMES;
|
|
218
|
+
if (!capReached && (!hasCommittedSceneRoot() || hasPendingSceneBuildWork())) {
|
|
165
219
|
settledFramesRef.current = 0;
|
|
220
|
+
invalidate();
|
|
166
221
|
return;
|
|
167
222
|
}
|
|
168
223
|
settledFramesRef.current += 1;
|
|
169
|
-
if (settledFramesRef.current < SCENE_READY_SETTLED_FRAMES)
|
|
224
|
+
if (settledFramesRef.current < SCENE_READY_SETTLED_FRAMES) {
|
|
225
|
+
invalidate();
|
|
170
226
|
return;
|
|
227
|
+
}
|
|
171
228
|
readyRef.current = true;
|
|
172
229
|
onSceneReadyChangeRef.current(true);
|
|
173
230
|
}, 10);
|
|
174
231
|
return null;
|
|
175
232
|
}
|
|
176
|
-
const Viewer = forwardRef(function Viewer({ children, hoverStyles = DEFAULT_HOVER_STYLES, selectionManager = 'default', perf = false, useBvh = true, renderContext = 'editor', defaultRender, isolate, sceneReadyKey, onSceneReadyChange, }, ref) {
|
|
233
|
+
const Viewer = forwardRef(function Viewer({ children, hoverStyles = DEFAULT_HOVER_STYLES, selectionManager = 'default', perf = false, useBvh = true, renderContext = 'editor', transparent, defaultRender, isolate, sceneReadyKey, onSceneReadyChange, sceneReadyMaxWaitMs, disablePostFx = false, }, ref) {
|
|
234
|
+
useEffect(() => {
|
|
235
|
+
if (nodeRegistry.size === 0) {
|
|
236
|
+
console.warn('[viewer] Node registry is empty. Install @pascal-app/nodes and call await loadPlugin(builtinPlugin) before mounting <Viewer>.');
|
|
237
|
+
}
|
|
238
|
+
}, []);
|
|
177
239
|
useImperativeHandle(ref, () => ({
|
|
178
240
|
setIsolated: (ids) => applyIsolation(ids),
|
|
179
241
|
}), []);
|
|
@@ -191,7 +253,34 @@ const Viewer = forwardRef(function Viewer({ children, hoverStyles = DEFAULT_HOVE
|
|
|
191
253
|
clearIsolation();
|
|
192
254
|
};
|
|
193
255
|
}, [isolate]);
|
|
256
|
+
const [rendererInitFailed, setRendererInitFailed] = useState(false);
|
|
257
|
+
// Capability detection runs after mount. We start optimistic (true) so the
|
|
258
|
+
// server-rendered markup and the first client render agree (no hydration
|
|
259
|
+
// mismatch); the effect flips it to false only on environments that expose
|
|
260
|
+
// neither WebGPU nor WebGL.
|
|
261
|
+
const [canMountViewer, setCanMountViewer] = useState(true);
|
|
262
|
+
useEffect(() => {
|
|
263
|
+
if (!canMountGpuViewer())
|
|
264
|
+
setCanMountViewer(false);
|
|
265
|
+
}, []);
|
|
194
266
|
const isDark = useViewer((state) => getSceneTheme(state.sceneTheme).appearance === 'dark');
|
|
267
|
+
const transparentBackground = useViewer((state) => state.transparentBackground);
|
|
268
|
+
// The shadows toggle drives `renderer.shadowMap.enabled` (via the Canvas
|
|
269
|
+
// `shadows` prop) rather than the lights' `castShadow`: toggling castShadow
|
|
270
|
+
// off disposes the shadow map's GPU texture but three r184's WebGPU node
|
|
271
|
+
// cache keeps the shadows-on builder state that references it, so toggling
|
|
272
|
+
// back on reuses destroyed resources and every frame submit fails with a
|
|
273
|
+
// GPUValidationError. Disabling at the renderer level rebuilds materials
|
|
274
|
+
// without disposing anything, so the round-trip is safe.
|
|
275
|
+
const shadowsEnabled = useViewer((state) => state.shadows);
|
|
276
|
+
useLayoutEffect(() => {
|
|
277
|
+
if (transparent === undefined)
|
|
278
|
+
return;
|
|
279
|
+
useViewer.getState().setTransparentBackground(transparent);
|
|
280
|
+
return () => {
|
|
281
|
+
useViewer.getState().setTransparentBackground(false);
|
|
282
|
+
};
|
|
283
|
+
}, [transparent]);
|
|
195
284
|
const defaultShading = defaultRender?.shading;
|
|
196
285
|
const defaultTextures = defaultRender?.textures;
|
|
197
286
|
const defaultColorPreset = defaultRender?.colorPreset;
|
|
@@ -228,14 +317,25 @@ const Viewer = forwardRef(function Viewer({ children, hoverStyles = DEFAULT_HOVE
|
|
|
228
317
|
// fragment-shader cost down — saves another ~30% over 1.5x on high-DPI mobile.
|
|
229
318
|
// Desktops (fine pointer) keep the original 1.5 cap.
|
|
230
319
|
const maxDpr = typeof window !== 'undefined' && window.matchMedia('(pointer: coarse)').matches ? 1.25 : 1.5;
|
|
231
|
-
|
|
320
|
+
const showGpuFallback = !canMountViewer || rendererInitFailed;
|
|
321
|
+
// When we can't mount the GPU canvas, the SceneReadyTracker never mounts and
|
|
322
|
+
// the host editor would otherwise wait on its scene-readiness timeout. Signal
|
|
323
|
+
// readiness explicitly so the host can drop its loader immediately.
|
|
324
|
+
useEffect(() => {
|
|
325
|
+
if (showGpuFallback)
|
|
326
|
+
onSceneReadyChange?.(true);
|
|
327
|
+
}, [showGpuFallback, onSceneReadyChange]);
|
|
328
|
+
if (showGpuFallback) {
|
|
329
|
+
return _jsx(UnsupportedGpuViewerFallback, {});
|
|
330
|
+
}
|
|
331
|
+
return (_jsxs(Canvas, { camera: { position: [50, 50, 50], fov: 50 }, className: `transition-colors duration-700 ${transparentBackground ? 'bg-transparent' : isDark ? 'bg-[#1f2433]' : 'bg-[#fafafa]'}`, dpr: [1, maxDpr], frameloop: "never", gl: ((props) => {
|
|
232
332
|
const canvas = props.canvas;
|
|
233
333
|
const cached = canvas ? WEBGPU_RENDERER_CACHE.get(canvas) : undefined;
|
|
234
334
|
if (cached)
|
|
235
335
|
return cached;
|
|
236
336
|
const promise = (async () => {
|
|
237
337
|
try {
|
|
238
|
-
const renderer = new THREE.WebGPURenderer(props);
|
|
338
|
+
const renderer = new THREE.WebGPURenderer({ ...props, alpha: true });
|
|
239
339
|
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
|
240
340
|
renderer.toneMappingExposure = getSceneTheme(useViewer.getState().sceneTheme).toneMappingExposure;
|
|
241
341
|
await renderer.init();
|
|
@@ -249,6 +349,7 @@ const Viewer = forwardRef(function Viewer({ children, hoverStyles = DEFAULT_HOVE
|
|
|
249
349
|
if (canvas)
|
|
250
350
|
WEBGPU_RENDERER_CACHE.delete(canvas);
|
|
251
351
|
console.error('[viewer] WebGPURenderer init failed', err);
|
|
352
|
+
setRendererInitFailed(true);
|
|
252
353
|
throw err;
|
|
253
354
|
}
|
|
254
355
|
})();
|
|
@@ -259,8 +360,8 @@ const Viewer = forwardRef(function Viewer({ children, hoverStyles = DEFAULT_HOVE
|
|
|
259
360
|
debounce: 100,
|
|
260
361
|
}, shadows: {
|
|
261
362
|
type: THREE.PCFShadowMap,
|
|
262
|
-
enabled:
|
|
263
|
-
}, children: [_jsx(FrameLimiter, { fps: 50 }), _jsx(ViewerCamera, {}), _jsx(GPUDeviceWatcher, {}), _jsx(ToneMappingExposure, {}), _jsx(SceneReadyTracker, { onSceneReadyChange: onSceneReadyChange, sceneReadyKey: sceneReadyKey }), _jsxs(ErrorBoundary, { fallback: null, scope: "viewer-scene", children: [_jsx(Lights, {}), useBvh ? (_jsx(SceneBvh, { children: _jsx(SceneRenderer, {}) })) : (_jsx(SceneRenderer, {})), _jsx(FloorElevationSystem, {}), _jsx(GeometrySystem, {}), _jsx(StairOpeningSystem, {}), _jsx(RegisteredSystems, {}), _jsx(PostProcessing, { hoverStyles: hoverStyles }), selectionManager === 'default' && _jsx(SelectionManager, {}), (perf || PERF_OVERLAY_ENABLED) && _jsx(PerfMonitor, {}), children] })] }));
|
|
363
|
+
enabled: shadowsEnabled,
|
|
364
|
+
}, children: [_jsx(FrameLimiter, { fps: 50 }), _jsx(ViewerCamera, {}), _jsx(GPUDeviceWatcher, {}), _jsx(ToneMappingExposure, {}), _jsx(SceneReadyTracker, { onSceneReadyChange: onSceneReadyChange, sceneReadyKey: sceneReadyKey, sceneReadyMaxWaitMs: sceneReadyMaxWaitMs }), _jsxs(ErrorBoundary, { fallback: null, scope: "viewer-scene", children: [_jsx(Lights, {}), useBvh ? (_jsx(SceneBvh, { children: _jsx(SceneRenderer, {}) })) : (_jsx(SceneRenderer, {})), _jsx(FloorElevationSystem, {}), _jsx(GeometrySystem, {}), _jsx(StairOpeningSystem, {}), _jsx(RegisteredSystems, {}), _jsx(PostProcessing, { disablePostFx: disablePostFx, hoverStyles: hoverStyles }), selectionManager === 'default' && _jsx(SelectionManager, {}), (perf || PERF_OVERLAY_ENABLED) && _jsx(PerfMonitor, {}), children] })] }));
|
|
264
365
|
});
|
|
265
366
|
const DebugRenderer = () => {
|
|
266
367
|
useFrame(({ gl, scene, camera }) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lights.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/lights.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"lights.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/lights.tsx"],"names":[],"mappings":"AA4EA,wBAAgB,MAAM,gCAgPrB"}
|
|
@@ -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,SAgnBA,CAAA;AAED,eAAe,oBAAoB,CAAA"}
|