@pascal-app/viewer 0.1.12 → 0.2.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/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 +93 -7
- package/dist/components/renderers/node-renderer.d.ts.map +1 -1
- package/dist/components/renderers/node-renderer.js +4 -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 +5 -20
- 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 +1 -1
- package/dist/components/renderers/window/window-renderer.d.ts +5 -0
- package/dist/components/renderers/window/window-renderer.d.ts.map +1 -0
- package/dist/components/renderers/window/window-renderer.js +11 -0
- 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 +55 -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 +229 -106
- package/dist/components/viewer/selection-manager.d.ts.map +1 -1
- package/dist/components/viewer/selection-manager.js +62 -18
- 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-node-events.d.ts +13 -1
- package/dist/hooks/use-node-events.d.ts.map +1 -1
- package/dist/hooks/use-node-events.js +34 -7
- 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 +2 -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 +54 -7
- package/dist/store/use-viewer.d.ts.map +1 -1
- package/dist/store/use-viewer.js +81 -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 +10 -7
- package/dist/hooks/use-grid-events.d.ts +0 -12
- package/dist/hooks/use-grid-events.d.ts.map +0 -1
- package/dist/hooks/use-grid-events.js +0 -33
|
@@ -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
15
|
sliceCount: 1,
|
|
14
|
-
stepCount:
|
|
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
|
-
// TRAA (Temporal Reprojection Anti-Aliasing)
|
|
74
|
-
const traaPass = traa(compositePass, scenePassDepth, scenePassVelocity, camera);
|
|
75
|
-
function generateSelectedOutlinePass() {
|
|
76
|
-
const edgeStrength = uniform(3);
|
|
77
|
-
const edgeGlow = uniform(0);
|
|
78
|
-
const edgeThickness = uniform(1);
|
|
79
|
-
const visibleEdgeColor = uniform(new Color(0xffffff));
|
|
80
|
-
const hiddenEdgeColor = uniform(new Color(0xf3ff47));
|
|
81
|
-
const outlinePass = outline(scene, camera, {
|
|
82
|
-
selectedObjects: useViewer.getState().outliner.selectedObjects,
|
|
83
|
-
edgeGlow,
|
|
84
|
-
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));
|
|
85
118
|
});
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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;
|
|
92
211
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const outlinePass = outline(scene, camera, {
|
|
101
|
-
selectedObjects: useViewer.getState().outliner.hoveredObjects,
|
|
102
|
-
edgeGlow,
|
|
103
|
-
edgeThickness,
|
|
104
|
-
});
|
|
105
|
-
const { visibleEdge, hiddenEdge } = outlinePass;
|
|
106
|
-
const period = time.div(pulsePeriod).mul(2);
|
|
107
|
-
const osc = oscSine(period).mul(0.5).add(0.5); // osc [ 0.5, 1.0 ]
|
|
108
|
-
const outlineColor = visibleEdge
|
|
109
|
-
.mul(visibleEdgeColor)
|
|
110
|
-
.add(hiddenEdge.mul(hiddenEdgeColor))
|
|
111
|
-
.mul(edgeStrength);
|
|
112
|
-
const outlinePulse = pulsePeriod.greaterThan(0).select(outlineColor.mul(osc), outlineColor);
|
|
113
|
-
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;
|
|
114
219
|
}
|
|
115
|
-
// Setup post-processing
|
|
116
|
-
const postProcessing = new PostProcessing(renderer);
|
|
117
|
-
const selectedOutlinePass = generateSelectedOutlinePass();
|
|
118
|
-
const hoverOutlinePass = generateHoverOutlinePass();
|
|
119
|
-
// Combine SSGI output with outlines
|
|
120
|
-
const finalOutput = SSGI_PARAMS.enabled
|
|
121
|
-
? selectedOutlinePass.add(hoverOutlinePass).add(traaPass)
|
|
122
|
-
: selectedOutlinePass.add(hoverOutlinePass).add(scenePassColor);
|
|
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,18 +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)
|
|
165
|
+
// Zone selected -> can select/hover contents (walls, items, slabs, ceilings, roofs, windows, doors)
|
|
148
166
|
return {
|
|
149
|
-
types: ['wall', 'item', 'slab', 'ceiling', 'roof'],
|
|
150
|
-
handleClick: (node) => {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
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
|
+
}
|
|
158
175
|
}
|
|
176
|
+
const { selectedIds } = useViewer.getState().selection;
|
|
177
|
+
useViewer
|
|
178
|
+
.getState()
|
|
179
|
+
.setSelection({ selectedIds: computeNextIds(nodeToSelect, selectedIds, nativeEvent) });
|
|
159
180
|
},
|
|
160
181
|
handleDeselect: () => {
|
|
161
182
|
const { selectedIds } = useViewer.getState().selection;
|
|
@@ -168,7 +189,16 @@ const getStrategy = () => {
|
|
|
168
189
|
}
|
|
169
190
|
},
|
|
170
191
|
isValid: (node) => {
|
|
171
|
-
const validTypes = [
|
|
192
|
+
const validTypes = [
|
|
193
|
+
'wall',
|
|
194
|
+
'item',
|
|
195
|
+
'slab',
|
|
196
|
+
'ceiling',
|
|
197
|
+
'roof',
|
|
198
|
+
'roof-segment',
|
|
199
|
+
'window',
|
|
200
|
+
'door',
|
|
201
|
+
];
|
|
172
202
|
if (!validTypes.includes(node.type))
|
|
173
203
|
return false;
|
|
174
204
|
return isNodeInZone(node, levelId, zoneId);
|
|
@@ -205,12 +235,24 @@ export const SelectionManager = () => {
|
|
|
205
235
|
return;
|
|
206
236
|
event.stopPropagation();
|
|
207
237
|
clickHandledRef.current = true;
|
|
208
|
-
strategy.handleClick(event.node);
|
|
238
|
+
strategy.handleClick(event.node, event.nativeEvent);
|
|
209
239
|
// Clear hover immediately after clicking on building/level/zone
|
|
210
240
|
useViewer.setState({ hoveredId: null });
|
|
211
241
|
};
|
|
212
242
|
// Subscribe to all node types
|
|
213
|
-
const allTypes = [
|
|
243
|
+
const allTypes = [
|
|
244
|
+
'building',
|
|
245
|
+
'level',
|
|
246
|
+
'zone',
|
|
247
|
+
'wall',
|
|
248
|
+
'item',
|
|
249
|
+
'slab',
|
|
250
|
+
'ceiling',
|
|
251
|
+
'roof',
|
|
252
|
+
'roof-segment',
|
|
253
|
+
'window',
|
|
254
|
+
'door',
|
|
255
|
+
];
|
|
214
256
|
for (const type of allTypes) {
|
|
215
257
|
emitter.on(`${type}:enter`, onEnter);
|
|
216
258
|
emitter.on(`${type}:leave`, onLeave);
|
|
@@ -223,14 +265,16 @@ export const SelectionManager = () => {
|
|
|
223
265
|
emitter.off(`${type}:click`, onClick);
|
|
224
266
|
}
|
|
225
267
|
};
|
|
226
|
-
}, [
|
|
268
|
+
}, []);
|
|
227
269
|
return (_jsxs(_Fragment, { children: [_jsx(PointerMissedHandler, { clickHandledRef: clickHandledRef }), _jsx(OutlinerSync, {})] }));
|
|
228
270
|
};
|
|
229
|
-
const PointerMissedHandler = ({ clickHandledRef }) => {
|
|
271
|
+
const PointerMissedHandler = ({ clickHandledRef, }) => {
|
|
230
272
|
const gl = useThree((s) => s.gl);
|
|
231
273
|
useEffect(() => {
|
|
232
274
|
const handleClick = (event) => {
|
|
233
275
|
// Only handle left clicks
|
|
276
|
+
if (useViewer.getState().cameraDragging)
|
|
277
|
+
return;
|
|
234
278
|
if (event.button !== 0)
|
|
235
279
|
return;
|
|
236
280
|
// Use requestAnimationFrame to check after R3F event handlers
|
|
@@ -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
|
};
|
|
@@ -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 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,6 +37,18 @@ type NodeConfig = {
|
|
|
37
37
|
node: RoofNode;
|
|
38
38
|
event: RoofEvent;
|
|
39
39
|
};
|
|
40
|
+
'roof-segment': {
|
|
41
|
+
node: RoofSegmentNode;
|
|
42
|
+
event: RoofSegmentEvent;
|
|
43
|
+
};
|
|
44
|
+
window: {
|
|
45
|
+
node: WindowNode;
|
|
46
|
+
event: WindowEvent;
|
|
47
|
+
};
|
|
48
|
+
door: {
|
|
49
|
+
node: DoorNode;
|
|
50
|
+
event: DoorEvent;
|
|
51
|
+
};
|
|
40
52
|
};
|
|
41
53
|
type NodeType = keyof NodeConfig;
|
|
42
54
|
export declare function useNodeEvents<T extends NodeType>(node: NodeConfig[T]['node'], type: T): {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-node-events.d.ts","sourceRoot":"","sources":["../../src/hooks/use-node-events.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,WAAW,
|
|
1
|
+
{"version":3,"file":"use-node-events.d.ts","sourceRoot":"","sources":["../../src/hooks/use-node-events.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,SAAS,EACd,KAAK,QAAQ,EAGb,KAAK,SAAS,EACd,KAAK,QAAQ,EACb,KAAK,UAAU,EACf,KAAK,SAAS,EACd,KAAK,SAAS,EACd,KAAK,QAAQ,EACb,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,SAAS,EACd,KAAK,QAAQ,EACb,KAAK,SAAS,EACd,KAAK,QAAQ,EACb,KAAK,SAAS,EACd,KAAK,QAAQ,EACb,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,SAAS,EACd,KAAK,QAAQ,EACd,MAAM,kBAAkB,CAAA;AACzB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAGpD,KAAK,UAAU,GAAG;IAChB,IAAI,EAAE;QAAE,IAAI,EAAE,QAAQ,CAAC;QAAC,KAAK,EAAE,SAAS,CAAA;KAAE,CAAA;IAC1C,IAAI,EAAE;QAAE,IAAI,EAAE,QAAQ,CAAC;QAAC,KAAK,EAAE,SAAS,CAAA;KAAE,CAAA;IAC1C,IAAI,EAAE;QAAE,IAAI,EAAE,QAAQ,CAAC;QAAC,KAAK,EAAE,SAAS,CAAA;KAAE,CAAA;IAC1C,QAAQ,EAAE;QAAE,IAAI,EAAE,YAAY,CAAC;QAAC,KAAK,EAAE,aAAa,CAAA;KAAE,CAAA;IACtD,KAAK,EAAE;QAAE,IAAI,EAAE,SAAS,CAAC;QAAC,KAAK,EAAE,UAAU,CAAA;KAAE,CAAA;IAC7C,IAAI,EAAE;QAAE,IAAI,EAAE,QAAQ,CAAC;QAAC,KAAK,EAAE,SAAS,CAAA;KAAE,CAAA;IAC1C,IAAI,EAAE;QAAE,IAAI,EAAE,QAAQ,CAAC;QAAC,KAAK,EAAE,SAAS,CAAA;KAAE,CAAA;IAC1C,OAAO,EAAE;QAAE,IAAI,EAAE,WAAW,CAAC;QAAC,KAAK,EAAE,YAAY,CAAA;KAAE,CAAA;IACnD,IAAI,EAAE;QAAE,IAAI,EAAE,QAAQ,CAAC;QAAC,KAAK,EAAE,SAAS,CAAA;KAAE,CAAA;IAC1C,cAAc,EAAE;QAAE,IAAI,EAAE,eAAe,CAAC;QAAC,KAAK,EAAE,gBAAgB,CAAA;KAAE,CAAA;IAClE,MAAM,EAAE;QAAE,IAAI,EAAE,UAAU,CAAC;QAAC,KAAK,EAAE,WAAW,CAAA;KAAE,CAAA;IAChD,IAAI,EAAE;QAAE,IAAI,EAAE,QAAQ,CAAC;QAAC,KAAK,EAAE,SAAS,CAAA;KAAE,CAAA;CAC3C,CAAA;AAED,KAAK,QAAQ,GAAG,MAAM,UAAU,CAAA;AAEhC,wBAAgB,aAAa,CAAC,CAAC,SAAS,QAAQ,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC;uBAiB/D,UAAU,CAAC,YAAY,CAAC;qBAK1B,UAAU,CAAC,YAAY,CAAC;iBAQ5B,UAAU,CAAC,YAAY,CAAC;wBAIjB,UAAU,CAAC,YAAY,CAAC;wBAIxB,UAAU,CAAC,YAAY,CAAC;uBAIzB,UAAU,CAAC,YAAY,CAAC;uBAIxB,UAAU,CAAC,YAAY,CAAC;uBAIxB,UAAU,CAAC,YAAY,CAAC;EAK9C"}
|