@pascal-app/viewer 0.1.13 → 0.3.0
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 +18 -0
- package/dist/components/error-boundary.d.ts.map +1 -0
- package/dist/components/error-boundary.js +11 -0
- package/dist/components/renderers/ceiling/ceiling-renderer.d.ts.map +1 -1
- package/dist/components/renderers/ceiling/ceiling-renderer.js +16 -9
- package/dist/components/renderers/door/door-renderer.d.ts +5 -0
- package/dist/components/renderers/door/door-renderer.d.ts.map +1 -0
- package/dist/components/renderers/door/door-renderer.js +11 -0
- package/dist/components/renderers/guide/guide-renderer.d.ts.map +1 -1
- package/dist/components/renderers/guide/guide-renderer.js +4 -2
- package/dist/components/renderers/item/item-renderer.d.ts.map +1 -1
- package/dist/components/renderers/item/item-renderer.js +98 -7
- package/dist/components/renderers/node-renderer.d.ts.map +1 -1
- package/dist/components/renderers/node-renderer.js +3 -1
- package/dist/components/renderers/roof/roof-materials.d.ts +4 -0
- package/dist/components/renderers/roof/roof-materials.d.ts.map +1 -0
- package/dist/components/renderers/roof/roof-materials.js +16 -0
- package/dist/components/renderers/roof/roof-renderer.d.ts.map +1 -1
- package/dist/components/renderers/roof/roof-renderer.js +5 -1
- package/dist/components/renderers/roof-segment/roof-segment-renderer.d.ts +5 -0
- package/dist/components/renderers/roof-segment/roof-segment-renderer.d.ts.map +1 -0
- package/dist/components/renderers/roof-segment/roof-segment-renderer.js +13 -0
- package/dist/components/renderers/scan/scan-renderer.d.ts.map +1 -1
- package/dist/components/renderers/scan/scan-renderer.js +3 -1
- package/dist/components/renderers/scene-renderer.d.ts.map +1 -1
- package/dist/components/renderers/scene-renderer.js +3 -3
- package/dist/components/renderers/site/site-renderer.d.ts.map +1 -1
- package/dist/components/renderers/site/site-renderer.js +4 -19
- package/dist/components/renderers/slab/slab-renderer.js +1 -1
- package/dist/components/renderers/wall/wall-renderer.d.ts.map +1 -1
- package/dist/components/renderers/wall/wall-renderer.js +7 -3
- package/dist/components/renderers/window/window-renderer.d.ts.map +1 -1
- package/dist/components/renderers/window/window-renderer.js +2 -1
- package/dist/components/renderers/zone/zone-renderer.d.ts.map +1 -1
- package/dist/components/renderers/zone/zone-renderer.js +33 -13
- package/dist/components/viewer/ground-occluder.d.ts +2 -0
- package/dist/components/viewer/ground-occluder.d.ts.map +1 -0
- package/dist/components/viewer/ground-occluder.js +75 -0
- package/dist/components/viewer/index.d.ts +1 -0
- package/dist/components/viewer/index.d.ts.map +1 -1
- package/dist/components/viewer/index.js +59 -6
- package/dist/components/viewer/lights.d.ts.map +1 -1
- package/dist/components/viewer/lights.js +69 -5
- package/dist/components/viewer/perf-monitor.d.ts +2 -0
- package/dist/components/viewer/perf-monitor.d.ts.map +1 -0
- package/dist/components/viewer/perf-monitor.js +42 -0
- package/dist/components/viewer/post-processing.d.ts.map +1 -1
- package/dist/components/viewer/post-processing.js +230 -107
- package/dist/components/viewer/selection-manager.d.ts.map +1 -1
- package/dist/components/viewer/selection-manager.js +47 -17
- package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
- package/dist/components/viewer/viewer-camera.js +2 -2
- package/dist/hooks/use-gltf-ktx2.d.ts +1 -1
- package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
- package/dist/hooks/use-gltf-ktx2.js +25 -7
- package/dist/hooks/use-grid-events.d.ts +12 -0
- package/dist/hooks/use-grid-events.d.ts.map +1 -0
- package/dist/hooks/use-grid-events.js +33 -0
- package/dist/hooks/use-node-events.d.ts +9 -1
- package/dist/hooks/use-node-events.d.ts.map +1 -1
- package/dist/hooks/use-node-events.js +5 -5
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/lib/asset-url.d.ts +1 -1
- package/dist/lib/asset-url.d.ts.map +1 -1
- package/dist/lib/asset-url.js +1 -1
- package/dist/lib/layers.d.ts +5 -0
- package/dist/lib/layers.d.ts.map +1 -0
- package/dist/lib/layers.js +4 -0
- package/dist/store/use-item-light-pool.d.ts +18 -0
- package/dist/store/use-item-light-pool.d.ts.map +1 -0
- package/dist/store/use-item-light-pool.js +30 -0
- package/dist/store/use-viewer.d.ts +56 -7
- package/dist/store/use-viewer.d.ts.map +1 -1
- package/dist/store/use-viewer.js +82 -17
- package/dist/systems/interactive/interactive-system.d.ts +2 -0
- package/dist/systems/interactive/interactive-system.d.ts.map +1 -0
- package/dist/systems/interactive/interactive-system.js +95 -0
- package/dist/systems/item-light/item-light-system.d.ts +2 -0
- package/dist/systems/item-light/item-light-system.d.ts.map +1 -0
- package/dist/systems/item-light/item-light-system.js +235 -0
- package/dist/systems/level/level-system.d.ts.map +1 -1
- package/dist/systems/level/level-system.js +19 -8
- package/dist/systems/level/level-utils.d.ts +17 -0
- package/dist/systems/level/level-utils.d.ts.map +1 -0
- package/dist/systems/level/level-utils.js +82 -0
- package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
- package/dist/systems/wall/wall-cutout.js +2 -4
- package/dist/systems/zone/zone-system.d.ts.map +1 -1
- package/dist/systems/zone/zone-system.js +29 -3
- package/package.json +7 -5
|
@@ -1,137 +1,260 @@
|
|
|
1
1
|
import { useFrame, useThree } from '@react-three/fiber';
|
|
2
|
-
import { useEffect, useRef } from 'react';
|
|
3
|
-
import { Color, UnsignedByteType } from 'three';
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { Color, Layers, UnsignedByteType } from 'three';
|
|
4
4
|
import { outline } from 'three/addons/tsl/display/OutlineNode.js';
|
|
5
5
|
import { ssgi } from 'three/addons/tsl/display/SSGINode.js';
|
|
6
6
|
import { traa } from 'three/addons/tsl/display/TRAANode.js';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { denoise } from 'three/examples/jsm/tsl/display/DenoiseNode.js';
|
|
8
|
+
import { add, colorToDirection, diffuseColor, directionToColor, float, mix, mrt, normalView, oscSine, output, pass, sample, time, uniform, vec4, velocity, } from 'three/tsl';
|
|
9
|
+
import { RenderPipeline } from 'three/webgpu';
|
|
10
|
+
import { SCENE_LAYER, ZONE_LAYER } from '../../lib/layers';
|
|
9
11
|
import useViewer from '../../store/use-viewer';
|
|
10
12
|
// SSGI Parameters - adjust these to fine-tune global illumination and ambient occlusion
|
|
11
13
|
export const SSGI_PARAMS = {
|
|
12
14
|
enabled: true,
|
|
13
|
-
sliceCount:
|
|
14
|
-
stepCount:
|
|
15
|
+
sliceCount: 1,
|
|
16
|
+
stepCount: 4,
|
|
15
17
|
radius: 1,
|
|
16
18
|
expFactor: 1.5,
|
|
17
19
|
thickness: 0.5,
|
|
18
20
|
backfaceLighting: 0.5,
|
|
19
21
|
aoIntensity: 1.5,
|
|
20
|
-
giIntensity: 0
|
|
22
|
+
giIntensity: 0,
|
|
21
23
|
useLinearThickness: false,
|
|
22
24
|
useScreenSpaceSampling: true,
|
|
23
|
-
useTemporalFiltering:
|
|
25
|
+
useTemporalFiltering: false,
|
|
24
26
|
};
|
|
27
|
+
const MAX_PIPELINE_RETRIES = 3;
|
|
28
|
+
const RETRY_DELAY_MS = 500;
|
|
29
|
+
const DARK_BG = '#1f2433';
|
|
30
|
+
const LIGHT_BG = '#ffffff';
|
|
25
31
|
const PostProcessingPasses = () => {
|
|
26
32
|
const { gl: renderer, scene, camera } = useThree();
|
|
27
|
-
const
|
|
33
|
+
const renderPipelineRef = useRef(null);
|
|
34
|
+
const hasPipelineErrorRef = useRef(false);
|
|
35
|
+
const retryCountRef = useRef(0);
|
|
36
|
+
const [isInitialized, setIsInitialized] = useState(false);
|
|
37
|
+
// Background color uniform — updated every frame via lerp, read by the TSL pipeline.
|
|
38
|
+
// Initialised from the current theme so there's no flash on first render.
|
|
39
|
+
const initBg = useViewer.getState().theme === 'dark' ? DARK_BG : LIGHT_BG;
|
|
40
|
+
const bgUniform = useRef(uniform(new Color(initBg)));
|
|
41
|
+
const bgCurrent = useRef(new Color(initBg));
|
|
42
|
+
const bgTarget = useRef(new Color());
|
|
43
|
+
const zoneLayers = useMemo(() => {
|
|
44
|
+
const l = new Layers();
|
|
45
|
+
l.enable(ZONE_LAYER);
|
|
46
|
+
l.disable(SCENE_LAYER);
|
|
47
|
+
return l;
|
|
48
|
+
}, []);
|
|
49
|
+
// Subscribe to projectId so the pipeline rebuilds on project switch
|
|
50
|
+
const projectId = useViewer((s) => s.projectId);
|
|
51
|
+
// Bump this to force a pipeline rebuild (used by retry logic)
|
|
52
|
+
const [pipelineVersion, setPipelineVersion] = useState(0);
|
|
53
|
+
const requestPipelineRebuild = useCallback(() => {
|
|
54
|
+
setPipelineVersion((v) => v + 1);
|
|
55
|
+
}, []);
|
|
56
|
+
// Renderer initialization
|
|
28
57
|
useEffect(() => {
|
|
29
|
-
|
|
58
|
+
let mounted = true;
|
|
59
|
+
const initRenderer = async () => {
|
|
60
|
+
try {
|
|
61
|
+
if (renderer && renderer.init) {
|
|
62
|
+
await renderer.init();
|
|
63
|
+
}
|
|
64
|
+
if (mounted) {
|
|
65
|
+
setIsInitialized(true);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
console.error('[viewer] Failed to initialize renderer for post-processing.', error);
|
|
70
|
+
if (mounted) {
|
|
71
|
+
setIsInitialized(false);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
initRenderer();
|
|
76
|
+
return () => {
|
|
77
|
+
mounted = false;
|
|
78
|
+
};
|
|
79
|
+
}, [renderer]);
|
|
80
|
+
// Reset retry count when project changes
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
retryCountRef.current = 0;
|
|
83
|
+
}, []);
|
|
84
|
+
// Build / rebuild the post-processing pipeline
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (!(renderer && scene && camera && isInitialized)) {
|
|
30
87
|
return;
|
|
31
88
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
giPass.thickness.value = SSGI_PARAMS.thickness;
|
|
62
|
-
giPass.backfaceLighting.value = SSGI_PARAMS.backfaceLighting;
|
|
63
|
-
giPass.aoIntensity.value = SSGI_PARAMS.aoIntensity;
|
|
64
|
-
giPass.giIntensity.value = SSGI_PARAMS.giIntensity;
|
|
65
|
-
giPass.useLinearThickness.value = SSGI_PARAMS.useLinearThickness;
|
|
66
|
-
giPass.useScreenSpaceSampling.value = SSGI_PARAMS.useScreenSpaceSampling;
|
|
67
|
-
giPass.useTemporalFiltering = SSGI_PARAMS.useTemporalFiltering;
|
|
68
|
-
// Extract GI and AO from SSGI pass
|
|
69
|
-
const gi = giPass.rgb;
|
|
70
|
-
const ao = giPass.a;
|
|
71
|
-
// Composite: scene * AO + diffuse * GI
|
|
72
|
-
const compositePass = vec4(add(scenePassColor.rgb.mul(ao), scenePassDiffuse.rgb.mul(gi)), scenePassColor.a);
|
|
73
|
-
function generateSelectedOutlinePass() {
|
|
74
|
-
const edgeStrength = uniform(3);
|
|
75
|
-
const edgeGlow = uniform(0);
|
|
76
|
-
const edgeThickness = uniform(1);
|
|
77
|
-
const visibleEdgeColor = uniform(new Color(0xffffff));
|
|
78
|
-
const hiddenEdgeColor = uniform(new Color(0xf3ff47));
|
|
79
|
-
const outlinePass = outline(scene, camera, {
|
|
80
|
-
selectedObjects: useViewer.getState().outliner.selectedObjects,
|
|
81
|
-
edgeGlow,
|
|
82
|
-
edgeThickness,
|
|
89
|
+
hasPipelineErrorRef.current = false;
|
|
90
|
+
// Clear outliner arrays synchronously to prevent stale Object3D refs
|
|
91
|
+
// from the previous project leaking into the new pipeline's outline passes.
|
|
92
|
+
const outliner = useViewer.getState().outliner;
|
|
93
|
+
outliner.selectedObjects.length = 0;
|
|
94
|
+
outliner.hoveredObjects.length = 0;
|
|
95
|
+
try {
|
|
96
|
+
// Scene pass with MRT for SSGI
|
|
97
|
+
const scenePass = pass(scene, camera);
|
|
98
|
+
scenePass.setMRT(mrt({
|
|
99
|
+
output,
|
|
100
|
+
diffuseColor,
|
|
101
|
+
normal: directionToColor(normalView),
|
|
102
|
+
velocity,
|
|
103
|
+
}));
|
|
104
|
+
// Get texture outputs
|
|
105
|
+
const scenePassColor = scenePass.getTextureNode('output');
|
|
106
|
+
const scenePassDiffuse = scenePass.getTextureNode('diffuseColor');
|
|
107
|
+
const scenePassDepth = scenePass.getTextureNode('depth');
|
|
108
|
+
const scenePassNormal = scenePass.getTextureNode('normal');
|
|
109
|
+
const scenePassVelocity = scenePass.getTextureNode('velocity');
|
|
110
|
+
// Optimize texture bandwidth
|
|
111
|
+
const diffuseTexture = scenePass.getTexture('diffuseColor');
|
|
112
|
+
diffuseTexture.type = UnsignedByteType;
|
|
113
|
+
const normalTexture = scenePass.getTexture('normal');
|
|
114
|
+
normalTexture.type = UnsignedByteType;
|
|
115
|
+
// Extract normal from color-encoded texture
|
|
116
|
+
const sceneNormal = sample((uv) => {
|
|
117
|
+
return colorToDirection(scenePassNormal.sample(uv));
|
|
83
118
|
});
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
119
|
+
const zonePass = pass(scene, camera);
|
|
120
|
+
zonePass.setLayers(zoneLayers);
|
|
121
|
+
// SSGI Pass (cast to PerspectiveCamera for SSGI)
|
|
122
|
+
const giPass = ssgi(scenePassColor, scenePassDepth, sceneNormal, camera);
|
|
123
|
+
giPass.sliceCount.value = SSGI_PARAMS.sliceCount;
|
|
124
|
+
giPass.stepCount.value = SSGI_PARAMS.stepCount;
|
|
125
|
+
giPass.radius.value = SSGI_PARAMS.radius;
|
|
126
|
+
giPass.expFactor.value = SSGI_PARAMS.expFactor;
|
|
127
|
+
giPass.thickness.value = SSGI_PARAMS.thickness;
|
|
128
|
+
giPass.backfaceLighting.value = SSGI_PARAMS.backfaceLighting;
|
|
129
|
+
giPass.aoIntensity.value = SSGI_PARAMS.aoIntensity;
|
|
130
|
+
giPass.giIntensity.value = SSGI_PARAMS.giIntensity;
|
|
131
|
+
giPass.useLinearThickness.value = SSGI_PARAMS.useLinearThickness;
|
|
132
|
+
giPass.useScreenSpaceSampling.value = SSGI_PARAMS.useScreenSpaceSampling;
|
|
133
|
+
giPass.useTemporalFiltering = SSGI_PARAMS.useTemporalFiltering;
|
|
134
|
+
const giTexture = giPass.getTextureNode();
|
|
135
|
+
// DenoiseNode only denoises RGB — alpha is passed through unchanged.
|
|
136
|
+
// SSGI packs AO into alpha, so we remap it into RGB before denoising.
|
|
137
|
+
// convertToTexture() inside denoise() will call rtt() on this vec4 node automatically.
|
|
138
|
+
const aoAsRgb = vec4(giTexture.a, giTexture.a, giTexture.a, float(1));
|
|
139
|
+
const denoisePass = denoise(aoAsRgb, scenePassDepth, sceneNormal, camera);
|
|
140
|
+
denoisePass.index.value = 0;
|
|
141
|
+
denoisePass.radius.value = 4;
|
|
142
|
+
const gi = giPass.rgb;
|
|
143
|
+
const ao = denoisePass.r;
|
|
144
|
+
// const gi = giPass.rgb;
|
|
145
|
+
// const ao = giPass.a;
|
|
146
|
+
// Background detection via alpha: renderer clears with alpha=0 (setClearAlpha(0) in useFrame),
|
|
147
|
+
// so background pixels have scenePassColor.a=0 while geometry pixels have output.a=1.
|
|
148
|
+
// WebGPU only applies clearColorValue to MRT attachment 0 (output), so scenePassColor.a
|
|
149
|
+
// is the reliable geometry mask — no normals, no flicker.
|
|
150
|
+
const hasGeometry = scenePassColor.a;
|
|
151
|
+
const contentAlpha = hasGeometry.max(zonePass.a);
|
|
152
|
+
// Composite: scene * AO + diffuse * GI
|
|
153
|
+
const compositePass = vec4(add(scenePassColor.rgb.mul(ao), add(zonePass.rgb, scenePassDiffuse.rgb.mul(gi))), contentAlpha);
|
|
154
|
+
function generateSelectedOutlinePass() {
|
|
155
|
+
const edgeStrength = uniform(3);
|
|
156
|
+
const edgeGlow = uniform(0);
|
|
157
|
+
const edgeThickness = uniform(1);
|
|
158
|
+
const visibleEdgeColor = uniform(new Color(0xff_ff_ff));
|
|
159
|
+
const hiddenEdgeColor = uniform(new Color(0xf3_ff_47));
|
|
160
|
+
const outlinePass = outline(scene, camera, {
|
|
161
|
+
selectedObjects: useViewer.getState().outliner.selectedObjects,
|
|
162
|
+
edgeGlow,
|
|
163
|
+
edgeThickness,
|
|
164
|
+
});
|
|
165
|
+
const { visibleEdge, hiddenEdge } = outlinePass;
|
|
166
|
+
const outlineColor = visibleEdge
|
|
167
|
+
.mul(visibleEdgeColor)
|
|
168
|
+
.add(hiddenEdge.mul(hiddenEdgeColor))
|
|
169
|
+
.mul(edgeStrength);
|
|
170
|
+
return outlineColor;
|
|
171
|
+
}
|
|
172
|
+
function generateHoverOutlinePass() {
|
|
173
|
+
const edgeStrength = uniform(5);
|
|
174
|
+
const edgeGlow = uniform(0.5);
|
|
175
|
+
const edgeThickness = uniform(1.5);
|
|
176
|
+
const pulsePeriod = uniform(3);
|
|
177
|
+
const visibleEdgeColor = uniform(new Color(0x00_aa_ff));
|
|
178
|
+
const hiddenEdgeColor = uniform(new Color(0xf3_ff_47));
|
|
179
|
+
const outlinePass = outline(scene, camera, {
|
|
180
|
+
selectedObjects: useViewer.getState().outliner.hoveredObjects,
|
|
181
|
+
edgeGlow,
|
|
182
|
+
edgeThickness,
|
|
183
|
+
});
|
|
184
|
+
const { visibleEdge, hiddenEdge } = outlinePass;
|
|
185
|
+
const period = time.div(pulsePeriod).mul(2);
|
|
186
|
+
const osc = oscSine(period).mul(0.5).add(0.5); // osc [ 0.5, 1.0 ]
|
|
187
|
+
const outlineColor = visibleEdge
|
|
188
|
+
.mul(visibleEdgeColor)
|
|
189
|
+
.add(hiddenEdge.mul(hiddenEdgeColor))
|
|
190
|
+
.mul(edgeStrength);
|
|
191
|
+
const outlinePulse = pulsePeriod.greaterThan(0).select(outlineColor.mul(osc), outlineColor);
|
|
192
|
+
return outlinePulse;
|
|
193
|
+
}
|
|
194
|
+
const selectedOutlinePass = generateSelectedOutlinePass();
|
|
195
|
+
const hoverOutlinePass = generateHoverOutlinePass();
|
|
196
|
+
// Combine composite with outlines BEFORE applying TRAA
|
|
197
|
+
const compositeWithOutlines = SSGI_PARAMS.enabled
|
|
198
|
+
? vec4(add(compositePass.rgb, selectedOutlinePass.add(hoverOutlinePass)), compositePass.a)
|
|
199
|
+
: vec4(add(scenePassColor.rgb, selectedOutlinePass.add(hoverOutlinePass)), scenePassColor.a);
|
|
200
|
+
// TRAA (Temporal Reprojection Anti-Aliasing) - applied AFTER combining everything
|
|
201
|
+
const traaOutput = traa(compositeWithOutlines, scenePassDepth, scenePassVelocity, camera);
|
|
202
|
+
// For zone-over-background pixels, scenePassDepth=1.0 (no scene geometry) causes TRAA
|
|
203
|
+
// to output black. Use hasGeometry to blend: geometry pixels use traaRgb, all others
|
|
204
|
+
// (zones over background, pure background) use compositePass.rgb directly.
|
|
205
|
+
const traaRgb = traaOutput.rgb;
|
|
206
|
+
const colorSource = mix(compositePass.rgb, traaRgb, hasGeometry);
|
|
207
|
+
const finalOutput = vec4(mix(bgUniform.current, colorSource, contentAlpha), float(1));
|
|
208
|
+
const renderPipeline = new RenderPipeline(renderer);
|
|
209
|
+
renderPipeline.outputNode = finalOutput;
|
|
210
|
+
renderPipelineRef.current = renderPipeline;
|
|
90
211
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const outlinePass = outline(scene, camera, {
|
|
99
|
-
selectedObjects: useViewer.getState().outliner.hoveredObjects,
|
|
100
|
-
edgeGlow,
|
|
101
|
-
edgeThickness,
|
|
102
|
-
});
|
|
103
|
-
const { visibleEdge, hiddenEdge } = outlinePass;
|
|
104
|
-
const period = time.div(pulsePeriod).mul(2);
|
|
105
|
-
const osc = oscSine(period).mul(0.5).add(0.5); // osc [ 0.5, 1.0 ]
|
|
106
|
-
const outlineColor = visibleEdge
|
|
107
|
-
.mul(visibleEdgeColor)
|
|
108
|
-
.add(hiddenEdge.mul(hiddenEdgeColor))
|
|
109
|
-
.mul(edgeStrength);
|
|
110
|
-
const outlinePulse = pulsePeriod.greaterThan(0).select(outlineColor.mul(osc), outlineColor);
|
|
111
|
-
return outlinePulse;
|
|
212
|
+
catch (error) {
|
|
213
|
+
hasPipelineErrorRef.current = true;
|
|
214
|
+
console.error('[viewer] Failed to set up post-processing pipeline. Rendering without post FX.', error);
|
|
215
|
+
if (renderPipelineRef.current) {
|
|
216
|
+
renderPipelineRef.current.dispose();
|
|
217
|
+
}
|
|
218
|
+
renderPipelineRef.current = null;
|
|
112
219
|
}
|
|
113
|
-
// Setup post-processing
|
|
114
|
-
const postProcessing = new PostProcessing(renderer);
|
|
115
|
-
const selectedOutlinePass = generateSelectedOutlinePass();
|
|
116
|
-
const hoverOutlinePass = generateHoverOutlinePass();
|
|
117
|
-
// Combine composite with outlines BEFORE applying TRAA
|
|
118
|
-
const compositeWithOutlines = SSGI_PARAMS.enabled
|
|
119
|
-
? vec4(add(compositePass.rgb, selectedOutlinePass.add(hoverOutlinePass)), compositePass.a)
|
|
120
|
-
: vec4(add(scenePassColor.rgb, selectedOutlinePass.add(hoverOutlinePass)), scenePassColor.a);
|
|
121
|
-
// TRAA (Temporal Reprojection Anti-Aliasing) - applied AFTER combining everything
|
|
122
|
-
const finalOutput = traa(compositeWithOutlines, scenePassDepth, scenePassVelocity, camera);
|
|
123
|
-
postProcessing.outputNode = finalOutput;
|
|
124
|
-
postProcessingRef.current = postProcessing;
|
|
125
220
|
return () => {
|
|
126
|
-
if (
|
|
127
|
-
|
|
221
|
+
if (renderPipelineRef.current) {
|
|
222
|
+
renderPipelineRef.current.dispose();
|
|
128
223
|
}
|
|
129
|
-
|
|
224
|
+
renderPipelineRef.current = null;
|
|
130
225
|
};
|
|
131
|
-
}, [renderer, scene, camera]);
|
|
132
|
-
useFrame(() => {
|
|
133
|
-
|
|
134
|
-
|
|
226
|
+
}, [renderer, scene, camera, isInitialized, zoneLayers]);
|
|
227
|
+
useFrame((_, delta) => {
|
|
228
|
+
// Animate background colour toward the current theme target (same lerp as AnimatedBackground)
|
|
229
|
+
bgTarget.current.set(useViewer.getState().theme === 'dark' ? DARK_BG : LIGHT_BG);
|
|
230
|
+
bgCurrent.current.lerp(bgTarget.current, Math.min(delta, 0.1) * 4);
|
|
231
|
+
bgUniform.current.value.copy(bgCurrent.current);
|
|
232
|
+
if (hasPipelineErrorRef.current || !renderPipelineRef.current) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
// Clear alpha=0 so background pixels in the output MRT attachment (index 0) get a=0,
|
|
237
|
+
// making scenePassColor.a a reliable geometry mask (geometry pixels write a=1 via output node).
|
|
238
|
+
;
|
|
239
|
+
renderer.setClearAlpha(0);
|
|
240
|
+
renderPipelineRef.current.render();
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
hasPipelineErrorRef.current = true;
|
|
244
|
+
console.error('[viewer] Post-processing render pass failed.', error);
|
|
245
|
+
if (renderPipelineRef.current) {
|
|
246
|
+
renderPipelineRef.current.dispose();
|
|
247
|
+
}
|
|
248
|
+
renderPipelineRef.current = null;
|
|
249
|
+
if (retryCountRef.current < MAX_PIPELINE_RETRIES) {
|
|
250
|
+
// Auto-retry: schedule a pipeline rebuild if we haven't exceeded the retry limit
|
|
251
|
+
retryCountRef.current++;
|
|
252
|
+
console.warn(`[viewer] Scheduling post-processing rebuild (attempt ${retryCountRef.current}/${MAX_PIPELINE_RETRIES})`);
|
|
253
|
+
setTimeout(requestPipelineRebuild, RETRY_DELAY_MS);
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
console.error('[viewer] Post-processing retries exhausted. Rendering without post FX for this session.');
|
|
257
|
+
}
|
|
135
258
|
}
|
|
136
259
|
}, 1);
|
|
137
260
|
return null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"selection-manager.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/selection-manager.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"selection-manager.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/selection-manager.tsx"],"names":[],"mappings":"AAwQA,eAAO,MAAM,gBAAgB,+CAsE5B,CAAA"}
|
|
@@ -46,12 +46,19 @@ const isNodeOnLevel = (node, levelId) => {
|
|
|
46
46
|
// Direct child of level
|
|
47
47
|
if (node.parentId === levelId)
|
|
48
48
|
return true;
|
|
49
|
-
// Wall-attached
|
|
50
|
-
if (node.type === 'item' && node.parentId) {
|
|
49
|
+
// Wall-attached nodes (window/door/item): check if parent wall is on the level
|
|
50
|
+
if ((node.type === 'item' || node.type === 'window' || node.type === 'door') && node.parentId) {
|
|
51
51
|
const parentNode = nodes[node.parentId];
|
|
52
52
|
if (parentNode?.type === 'wall' && parentNode.parentId === levelId) {
|
|
53
53
|
return true;
|
|
54
54
|
}
|
|
55
|
+
// Ceiling/slab/roof-attached items: check if parent structure is on the level
|
|
56
|
+
if ((parentNode?.type === 'ceiling' ||
|
|
57
|
+
parentNode?.type === 'slab' ||
|
|
58
|
+
parentNode?.type === 'roof') &&
|
|
59
|
+
parentNode.parentId === levelId) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
55
62
|
}
|
|
56
63
|
return false;
|
|
57
64
|
};
|
|
@@ -97,7 +104,7 @@ const isNodeInZone = (node, levelId, zoneId) => {
|
|
|
97
104
|
}
|
|
98
105
|
return false;
|
|
99
106
|
}
|
|
100
|
-
if (node.type === 'roof') {
|
|
107
|
+
if (node.type === 'roof' || node.type === 'roof-segment') {
|
|
101
108
|
// Roofs on the same level are valid when zone is selected
|
|
102
109
|
return true;
|
|
103
110
|
}
|
|
@@ -105,6 +112,17 @@ const isNodeInZone = (node, levelId, zoneId) => {
|
|
|
105
112
|
};
|
|
106
113
|
const getStrategy = () => {
|
|
107
114
|
const { buildingId, levelId, zoneId } = useViewer.getState().selection;
|
|
115
|
+
const computeNextIds = (node, selectedIds, event) => {
|
|
116
|
+
const isMeta = event?.metaKey || event?.nativeEvent?.metaKey;
|
|
117
|
+
const isCtrl = event?.ctrlKey || event?.nativeEvent?.ctrlKey;
|
|
118
|
+
if (isMeta || isCtrl) {
|
|
119
|
+
if (selectedIds.includes(node.id)) {
|
|
120
|
+
return selectedIds.filter((id) => id !== node.id);
|
|
121
|
+
}
|
|
122
|
+
return [...selectedIds, node.id];
|
|
123
|
+
}
|
|
124
|
+
return [node.id];
|
|
125
|
+
};
|
|
108
126
|
// No building selected -> can select buildings
|
|
109
127
|
if (!buildingId) {
|
|
110
128
|
return {
|
|
@@ -144,20 +162,21 @@ const getStrategy = () => {
|
|
|
144
162
|
isValid: (node) => node.type === 'zone' && node.parentId === levelId,
|
|
145
163
|
};
|
|
146
164
|
}
|
|
147
|
-
// Zone selected -> can select/hover contents (walls, items, slabs, ceilings, roofs, windows)
|
|
165
|
+
// Zone selected -> can select/hover contents (walls, items, slabs, ceilings, roofs, windows, doors)
|
|
148
166
|
return {
|
|
149
|
-
types: ['wall', 'item', 'slab', 'ceiling', 'roof', 'window'],
|
|
150
|
-
handleClick: (node) => {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
else {
|
|
159
|
-
useViewer.getState().setSelection({ selectedIds: [node.id] });
|
|
167
|
+
types: ['wall', 'item', 'slab', 'ceiling', 'roof', 'roof-segment', 'window', 'door'],
|
|
168
|
+
handleClick: (node, nativeEvent) => {
|
|
169
|
+
let nodeToSelect = node;
|
|
170
|
+
if (node.type === 'roof-segment' && node.parentId) {
|
|
171
|
+
const parentNode = useScene.getState().nodes[node.parentId];
|
|
172
|
+
if (parentNode && parentNode.type === 'roof') {
|
|
173
|
+
nodeToSelect = parentNode;
|
|
174
|
+
}
|
|
160
175
|
}
|
|
176
|
+
const { selectedIds } = useViewer.getState().selection;
|
|
177
|
+
useViewer
|
|
178
|
+
.getState()
|
|
179
|
+
.setSelection({ selectedIds: computeNextIds(nodeToSelect, selectedIds, nativeEvent) });
|
|
161
180
|
},
|
|
162
181
|
handleDeselect: () => {
|
|
163
182
|
const { selectedIds } = useViewer.getState().selection;
|
|
@@ -170,7 +189,16 @@ const getStrategy = () => {
|
|
|
170
189
|
}
|
|
171
190
|
},
|
|
172
191
|
isValid: (node) => {
|
|
173
|
-
const validTypes = [
|
|
192
|
+
const validTypes = [
|
|
193
|
+
'wall',
|
|
194
|
+
'item',
|
|
195
|
+
'slab',
|
|
196
|
+
'ceiling',
|
|
197
|
+
'roof',
|
|
198
|
+
'roof-segment',
|
|
199
|
+
'window',
|
|
200
|
+
'door',
|
|
201
|
+
];
|
|
174
202
|
if (!validTypes.includes(node.type))
|
|
175
203
|
return false;
|
|
176
204
|
return isNodeInZone(node, levelId, zoneId);
|
|
@@ -207,7 +235,7 @@ export const SelectionManager = () => {
|
|
|
207
235
|
return;
|
|
208
236
|
event.stopPropagation();
|
|
209
237
|
clickHandledRef.current = true;
|
|
210
|
-
strategy.handleClick(event.node);
|
|
238
|
+
strategy.handleClick(event.node, event.nativeEvent);
|
|
211
239
|
// Clear hover immediately after clicking on building/level/zone
|
|
212
240
|
useViewer.setState({ hoveredId: null });
|
|
213
241
|
};
|
|
@@ -221,7 +249,9 @@ export const SelectionManager = () => {
|
|
|
221
249
|
'slab',
|
|
222
250
|
'ceiling',
|
|
223
251
|
'roof',
|
|
252
|
+
'roof-segment',
|
|
224
253
|
'window',
|
|
254
|
+
'door',
|
|
225
255
|
];
|
|
226
256
|
for (const type of allTypes) {
|
|
227
257
|
emitter.on(`${type}:enter`, onEnter);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"viewer-camera.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/viewer-camera.tsx"],"names":[],"mappings":"AAGA,eAAO,MAAM,YAAY,+
|
|
1
|
+
{"version":3,"file":"viewer-camera.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/viewer-camera.tsx"],"names":[],"mappings":"AAGA,eAAO,MAAM,YAAY,+CAQxB,CAAA"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { OrthographicCamera, PerspectiveCamera } from
|
|
3
|
-
import useViewer from
|
|
2
|
+
import { OrthographicCamera, PerspectiveCamera } from '@react-three/drei';
|
|
3
|
+
import useViewer from '../../store/use-viewer';
|
|
4
4
|
export const ViewerCamera = () => {
|
|
5
5
|
const cameraMode = useViewer((state) => state.cameraMode);
|
|
6
6
|
return cameraMode === 'perspective' ? (_jsx(PerspectiveCamera, { far: 1000, fov: 50, makeDefault: true, near: 0.1, position: [10, 10, 10] })) : (_jsx(OrthographicCamera, { far: 1000, makeDefault: true, near: -1000, position: [10, 10, 10], zoom: 20 }));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-gltf-ktx2.d.ts","sourceRoot":"","sources":["../../src/hooks/use-gltf-ktx2.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAA;
|
|
1
|
+
{"version":3,"file":"use-gltf-ktx2.d.ts","sourceRoot":"","sources":["../../src/hooks/use-gltf-ktx2.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAA;AAU3C,QAAA,MAAM,WAAW,GAAI,MAAM,MAAM,KAAG,UAAU,CAAC,OAAO,OAAO,CA2B5D,CAAA;AACD,OAAO,EAAE,WAAW,EAAE,CAAA"}
|
|
@@ -1,15 +1,33 @@
|
|
|
1
|
-
import { useGLTF } from
|
|
2
|
-
import { useThree } from
|
|
3
|
-
import { KTX2Loader } from
|
|
4
|
-
import { MeshoptDecoder } from
|
|
1
|
+
import { useGLTF } from '@react-three/drei';
|
|
2
|
+
import { useThree } from '@react-three/fiber';
|
|
3
|
+
import { KTX2Loader } from 'three/examples/jsm/Addons.js';
|
|
4
|
+
import { MeshoptDecoder } from 'three/examples/jsm/libs/meshopt_decoder.module.js';
|
|
5
5
|
const ktx2LoaderInstance = new KTX2Loader();
|
|
6
6
|
ktx2LoaderInstance.setTranscoderPath('https://cdn.jsdelivr.net/gh/pmndrs/drei-assets@master/basis/');
|
|
7
|
+
const ktx2ConfiguredRenderers = new WeakSet();
|
|
8
|
+
const ktx2WarningLoggedRenderers = new WeakSet();
|
|
7
9
|
const useGLTFKTX2 = (path) => {
|
|
8
10
|
const gl = useThree((state) => state.gl);
|
|
9
11
|
return useGLTF(path, true, true, (loader) => {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
const renderer = gl;
|
|
13
|
+
if (!ktx2ConfiguredRenderers.has(renderer)) {
|
|
14
|
+
try {
|
|
15
|
+
ktx2LoaderInstance.detectSupport(gl);
|
|
16
|
+
ktx2ConfiguredRenderers.add(renderer);
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
// Some WebGPU flows can transiently call this before backend init.
|
|
20
|
+
// Avoid crashing the whole scene; scans may render without KTX2 on this pass.
|
|
21
|
+
if (!ktx2WarningLoggedRenderers.has(renderer)) {
|
|
22
|
+
console.warn('[viewer] Skipping KTX2 support detection for now.', error);
|
|
23
|
+
ktx2WarningLoggedRenderers.add(renderer);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (ktx2ConfiguredRenderers.has(renderer)) {
|
|
28
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
29
|
+
loader.setKTX2Loader(ktx2LoaderInstance);
|
|
30
|
+
}
|
|
13
31
|
loader.setMeshoptDecoder(MeshoptDecoder);
|
|
14
32
|
});
|
|
15
33
|
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ThreeEvent } from "@react-three/fiber";
|
|
2
|
+
export declare function useGridEvents(): {
|
|
3
|
+
onPointerDown: (e: ThreeEvent<PointerEvent>) => void;
|
|
4
|
+
onPointerUp: (e: ThreeEvent<PointerEvent>) => void;
|
|
5
|
+
onClick: (e: ThreeEvent<PointerEvent>) => void;
|
|
6
|
+
onPointerEnter: (e: ThreeEvent<PointerEvent>) => void;
|
|
7
|
+
onPointerLeave: (e: ThreeEvent<PointerEvent>) => void;
|
|
8
|
+
onPointerMove: (e: ThreeEvent<PointerEvent>) => void;
|
|
9
|
+
onDoubleClick: (e: ThreeEvent<PointerEvent>) => void;
|
|
10
|
+
onContextMenu: (e: ThreeEvent<PointerEvent>) => void;
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=use-grid-events.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-grid-events.d.ts","sourceRoot":"","sources":["../../src/hooks/use-grid-events.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAErD,wBAAgB,aAAa;uBAYN,UAAU,CAAC,YAAY,CAAC;qBAI1B,UAAU,CAAC,YAAY,CAAC;iBAI5B,UAAU,CAAC,YAAY,CAAC;wBAIjB,UAAU,CAAC,YAAY,CAAC;wBACxB,UAAU,CAAC,YAAY,CAAC;uBACzB,UAAU,CAAC,YAAY,CAAC;uBACxB,UAAU,CAAC,YAAY,CAAC;uBACxB,UAAU,CAAC,YAAY,CAAC;EAE9C"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { emitter } from "@pascal-app/core";
|
|
2
|
+
export function useGridEvents() {
|
|
3
|
+
const emit = (suffix, e) => {
|
|
4
|
+
const eventKey = `grid:${suffix}`;
|
|
5
|
+
const payload = {
|
|
6
|
+
position: [e.point.x, e.point.y, e.point.z],
|
|
7
|
+
nativeEvent: e,
|
|
8
|
+
};
|
|
9
|
+
emitter.emit(eventKey, payload);
|
|
10
|
+
};
|
|
11
|
+
return {
|
|
12
|
+
onPointerDown: (e) => {
|
|
13
|
+
if (e.button !== 0)
|
|
14
|
+
return;
|
|
15
|
+
emit("pointerdown", e);
|
|
16
|
+
},
|
|
17
|
+
onPointerUp: (e) => {
|
|
18
|
+
if (e.button !== 0)
|
|
19
|
+
return;
|
|
20
|
+
emit("pointerup", e);
|
|
21
|
+
},
|
|
22
|
+
onClick: (e) => {
|
|
23
|
+
if (e.button !== 0)
|
|
24
|
+
return;
|
|
25
|
+
emit("click", e);
|
|
26
|
+
},
|
|
27
|
+
onPointerEnter: (e) => emit("enter", e),
|
|
28
|
+
onPointerLeave: (e) => emit("leave", e),
|
|
29
|
+
onPointerMove: (e) => emit("move", e),
|
|
30
|
+
onDoubleClick: (e) => emit("double-click", e),
|
|
31
|
+
onContextMenu: (e) => emit("context-menu", e),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type BuildingEvent, type BuildingNode, type CeilingEvent, type CeilingNode, type ItemEvent, type ItemNode, type LevelEvent, type LevelNode, type RoofEvent, type RoofNode, type SiteEvent, type SiteNode, type SlabEvent, type SlabNode, type WallEvent, type WallNode, type WindowEvent, type WindowNode, type ZoneEvent, type ZoneNode } from '@pascal-app/core';
|
|
1
|
+
import { type BuildingEvent, type BuildingNode, type CeilingEvent, type CeilingNode, type DoorEvent, type DoorNode, type ItemEvent, type ItemNode, type LevelEvent, type LevelNode, type RoofEvent, type RoofNode, type RoofSegmentEvent, type RoofSegmentNode, type SiteEvent, type SiteNode, type SlabEvent, type SlabNode, type WallEvent, type WallNode, type WindowEvent, type WindowNode, type ZoneEvent, type ZoneNode } from '@pascal-app/core';
|
|
2
2
|
import type { ThreeEvent } from '@react-three/fiber';
|
|
3
3
|
type NodeConfig = {
|
|
4
4
|
site: {
|
|
@@ -37,10 +37,18 @@ type NodeConfig = {
|
|
|
37
37
|
node: RoofNode;
|
|
38
38
|
event: RoofEvent;
|
|
39
39
|
};
|
|
40
|
+
'roof-segment': {
|
|
41
|
+
node: RoofSegmentNode;
|
|
42
|
+
event: RoofSegmentEvent;
|
|
43
|
+
};
|
|
40
44
|
window: {
|
|
41
45
|
node: WindowNode;
|
|
42
46
|
event: WindowEvent;
|
|
43
47
|
};
|
|
48
|
+
door: {
|
|
49
|
+
node: DoorNode;
|
|
50
|
+
event: DoorEvent;
|
|
51
|
+
};
|
|
44
52
|
};
|
|
45
53
|
type NodeType = keyof NodeConfig;
|
|
46
54
|
export declare function useNodeEvents<T extends NodeType>(node: NodeConfig[T]['node'], type: T): {
|