@pascal-app/viewer 0.8.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/error-boundary.d.ts +6 -0
- package/dist/components/error-boundary.d.ts.map +1 -1
- package/dist/components/error-boundary.js +6 -0
- package/dist/components/renderers/node-renderer.d.ts +6 -2
- package/dist/components/renderers/node-renderer.d.ts.map +1 -1
- package/dist/components/renderers/node-renderer.js +41 -22
- package/dist/components/renderers/parametric-node-renderer.d.ts +5 -0
- package/dist/components/renderers/parametric-node-renderer.d.ts.map +1 -0
- package/dist/components/renderers/parametric-node-renderer.js +37 -0
- package/dist/components/renderers/scene-renderer.d.ts +1 -1
- package/dist/components/renderers/scene-renderer.d.ts.map +1 -1
- package/dist/components/viewer/bvh-ecctrl.d.ts +77 -0
- package/dist/components/viewer/bvh-ecctrl.d.ts.map +1 -0
- package/dist/components/viewer/bvh-ecctrl.js +552 -0
- package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
- package/dist/components/viewer/frame-limiter.js +39 -4
- package/dist/components/viewer/glb-interactive.d.ts +41 -0
- package/dist/components/viewer/glb-interactive.d.ts.map +1 -0
- package/dist/components/viewer/glb-interactive.js +394 -0
- package/dist/components/viewer/glb-reference-nodes.d.ts +29 -0
- package/dist/components/viewer/glb-reference-nodes.d.ts.map +1 -0
- package/dist/components/viewer/glb-reference-nodes.js +80 -0
- package/dist/components/viewer/glb-replace-instances.d.ts +18 -0
- package/dist/components/viewer/glb-replace-instances.d.ts.map +1 -0
- package/dist/components/viewer/glb-replace-instances.js +61 -0
- package/dist/components/viewer/glb-scene.d.ts +54 -0
- package/dist/components/viewer/glb-scene.d.ts.map +1 -0
- package/dist/components/viewer/glb-scene.js +920 -0
- package/dist/components/viewer/glb-walkthrough-controller.d.ts +12 -0
- package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -0
- package/dist/components/viewer/glb-walkthrough-controller.js +357 -0
- package/dist/components/viewer/index.d.ts +56 -3
- package/dist/components/viewer/index.d.ts.map +1 -1
- package/dist/components/viewer/index.js +280 -65
- package/dist/components/viewer/lights.d.ts +1 -1
- package/dist/components/viewer/lights.d.ts.map +1 -1
- package/dist/components/viewer/lights.js +201 -43
- package/dist/components/viewer/perf-monitor.d.ts +1 -1
- package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
- package/dist/components/viewer/perf-monitor.js +115 -12
- 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 +395 -63
- package/dist/components/viewer/registered-systems.d.ts +12 -0
- package/dist/components/viewer/registered-systems.d.ts.map +1 -0
- package/dist/components/viewer/registered-systems.js +49 -0
- package/dist/components/viewer/scene-bvh.d.ts +18 -0
- package/dist/components/viewer/scene-bvh.d.ts.map +1 -0
- package/dist/components/viewer/scene-bvh.js +97 -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 +1 -1
- package/dist/components/viewer/selection-manager.d.ts.map +1 -1
- package/dist/components/viewer/selection-manager.js +37 -14
- package/dist/components/viewer/viewer-camera.d.ts +1 -1
- package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
- package/dist/components/viewer/walkthrough-controls.d.ts +2 -1
- package/dist/components/viewer/walkthrough-controls.d.ts.map +1 -1
- package/dist/components/viewer/walkthrough-controls.js +20 -0
- package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
- package/dist/hooks/use-gltf-ktx2.js +3 -22
- package/dist/hooks/use-node-events.d.ts +6 -74
- package/dist/hooks/use-node-events.d.ts.map +1 -1
- package/dist/hooks/use-node-events.js +28 -8
- package/dist/index.d.ts +48 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +82 -4
- 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 +10 -0
- package/dist/lib/csg-utils.d.ts.map +1 -0
- package/dist/lib/csg-utils.js +93 -0
- 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/drawable-geometry.d.ts +21 -0
- package/dist/lib/drawable-geometry.d.ts.map +1 -0
- package/dist/lib/drawable-geometry.js +22 -0
- package/dist/lib/edge-style.d.ts +4 -0
- package/dist/lib/edge-style.d.ts.map +1 -0
- package/dist/lib/edge-style.js +31 -0
- package/dist/lib/gpu-perf.d.ts +4 -0
- package/dist/lib/gpu-perf.d.ts.map +1 -0
- package/dist/lib/gpu-perf.js +23 -0
- package/dist/lib/ink-edges.d.ts +9 -0
- package/dist/lib/ink-edges.d.ts.map +1 -0
- package/dist/lib/ink-edges.js +49 -0
- package/dist/lib/isolation.d.ts +39 -0
- package/dist/lib/isolation.d.ts.map +1 -0
- package/dist/lib/isolation.js +103 -0
- package/dist/lib/ktx2-loader.d.ts +43 -0
- package/dist/lib/ktx2-loader.d.ts.map +1 -0
- package/dist/lib/ktx2-loader.js +136 -0
- package/dist/lib/layers.d.ts +28 -0
- package/dist/lib/layers.d.ts.map +1 -1
- package/dist/lib/layers.js +28 -0
- package/dist/lib/materials.d.ts +38 -15
- package/dist/lib/materials.d.ts.map +1 -1
- package/dist/lib/materials.js +364 -72
- package/dist/lib/merged-outline-node.d.ts.map +1 -1
- package/dist/lib/merged-outline-node.js +7 -0
- package/dist/lib/polygon-union.d.ts +4 -0
- package/dist/lib/polygon-union.d.ts.map +1 -0
- package/dist/lib/polygon-union.js +289 -0
- package/dist/lib/polygon-union.test.d.ts +2 -0
- package/dist/lib/polygon-union.test.d.ts.map +1 -0
- package/dist/lib/polygon-union.test.js +105 -0
- package/dist/lib/scene-themes.d.ts +30 -0
- package/dist/lib/scene-themes.d.ts.map +1 -0
- package/dist/lib/scene-themes.js +202 -0
- package/dist/lib/shadow-only.d.ts +4 -0
- package/dist/lib/shadow-only.d.ts.map +1 -0
- package/dist/lib/shadow-only.js +36 -0
- package/dist/lib/tsl-compat.d.ts +12 -0
- package/dist/lib/tsl-compat.d.ts.map +1 -0
- package/dist/lib/tsl-compat.js +13 -0
- package/dist/store/use-viewer.d.ts +92 -12
- package/dist/store/use-viewer.d.ts.map +1 -1
- package/dist/store/use-viewer.js +201 -6
- package/dist/store/use-viewer.test.d.ts +2 -0
- package/dist/store/use-viewer.test.d.ts.map +1 -0
- package/dist/store/use-viewer.test.js +42 -0
- package/dist/systems/ceiling/ceiling-system.d.ts +6 -2
- package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
- package/dist/systems/ceiling/ceiling-system.js +66 -13
- 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 +864 -180
- package/dist/systems/elevator/elevator-interaction-system.d.ts +2 -0
- package/dist/systems/elevator/elevator-interaction-system.d.ts.map +1 -0
- package/dist/systems/elevator/elevator-interaction-system.js +62 -0
- package/dist/systems/fence/fence-system.d.ts +20 -0
- package/dist/systems/fence/fence-system.d.ts.map +1 -1
- package/dist/systems/fence/fence-system.js +229 -57
- package/dist/systems/floor-elevation/floor-elevation-system.d.ts +20 -0
- package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -0
- package/dist/systems/floor-elevation/floor-elevation-system.js +78 -0
- package/dist/systems/geometry/geometry-system.d.ts +41 -0
- package/dist/systems/geometry/geometry-system.d.ts.map +1 -0
- package/dist/systems/geometry/geometry-system.js +348 -0
- 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 +1 -1
- 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 +7 -0
- package/dist/systems/item/item-system.d.ts.map +1 -1
- package/dist/systems/item/item-system.js +28 -17
- package/dist/systems/item-light/item-light-system.d.ts +1 -1
- package/dist/systems/item-light/item-light-system.d.ts.map +1 -1
- package/dist/systems/item-light/item-light-system.js +18 -5
- package/dist/systems/level/level-system.d.ts.map +1 -1
- package/dist/systems/level/level-system.js +28 -4
- package/dist/systems/level/level-utils.d.ts +0 -3
- package/dist/systems/level/level-utils.d.ts.map +1 -1
- package/dist/systems/level/level-utils.js +2 -41
- package/dist/systems/roof/roof-materials.d.ts +3 -2
- package/dist/systems/roof/roof-materials.d.ts.map +1 -1
- package/dist/systems/roof/roof-materials.js +59 -14
- package/dist/systems/roof/roof-system.d.ts +46 -6
- package/dist/systems/roof/roof-system.d.ts.map +1 -1
- package/dist/systems/roof/roof-system.js +1365 -170
- package/dist/systems/slab/slab-system.d.ts +5 -3
- package/dist/systems/slab/slab-system.d.ts.map +1 -1
- package/dist/systems/slab/slab-system.js +204 -68
- package/dist/systems/slab/slab-system.test.d.ts +2 -0
- package/dist/systems/slab/slab-system.test.d.ts.map +1 -0
- package/dist/systems/slab/slab-system.test.js +63 -0
- package/dist/systems/stair/stair-materials.d.ts +4 -3
- package/dist/systems/stair/stair-materials.d.ts.map +1 -1
- package/dist/systems/stair/stair-materials.js +30 -15
- package/dist/systems/stair/stair-system.d.ts.map +1 -1
- package/dist/systems/stair/stair-system.js +55 -63
- package/dist/systems/surface-hole-geometry.d.ts +3 -0
- package/dist/systems/surface-hole-geometry.d.ts.map +1 -0
- package/dist/systems/surface-hole-geometry.js +4 -0
- 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 +62 -17
- package/dist/systems/wall/wall-materials.d.ts +12 -7
- package/dist/systems/wall/wall-materials.d.ts.map +1 -1
- package/dist/systems/wall/wall-materials.js +279 -46
- package/dist/systems/wall/wall-opening-cutout.test.d.ts +2 -0
- package/dist/systems/wall/wall-opening-cutout.test.d.ts.map +1 -0
- package/dist/systems/wall/wall-opening-cutout.test.js +59 -0
- package/dist/systems/wall/wall-support-extension.test.d.ts +2 -0
- package/dist/systems/wall/wall-support-extension.test.d.ts.map +1 -0
- package/dist/systems/wall/wall-support-extension.test.js +48 -0
- package/dist/systems/wall/wall-system.d.ts +2 -2
- package/dist/systems/wall/wall-system.d.ts.map +1 -1
- package/dist/systems/wall/wall-system.js +493 -193
- 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 +308 -18
- package/package.json +5 -6
- package/dist/components/renderers/building/building-renderer.d.ts +0 -5
- package/dist/components/renderers/building/building-renderer.d.ts.map +0 -1
- package/dist/components/renderers/building/building-renderer.js +0 -11
- package/dist/components/renderers/ceiling/ceiling-renderer.d.ts +0 -5
- package/dist/components/renderers/ceiling/ceiling-renderer.d.ts.map +0 -1
- package/dist/components/renderers/ceiling/ceiling-renderer.js +0 -58
- package/dist/components/renderers/column/column-renderer.d.ts +0 -5
- package/dist/components/renderers/column/column-renderer.d.ts.map +0 -1
- package/dist/components/renderers/column/column-renderer.js +0 -597
- package/dist/components/renderers/door/door-renderer.d.ts +0 -5
- package/dist/components/renderers/door/door-renderer.d.ts.map +0 -1
- package/dist/components/renderers/door/door-renderer.js +0 -16
- package/dist/components/renderers/fence/fence-renderer.d.ts +0 -5
- package/dist/components/renderers/fence/fence-renderer.d.ts.map +0 -1
- package/dist/components/renderers/fence/fence-renderer.js +0 -15
- package/dist/components/renderers/guide/guide-renderer.d.ts +0 -5
- package/dist/components/renderers/guide/guide-renderer.d.ts.map +0 -1
- package/dist/components/renderers/guide/guide-renderer.js +0 -38
- package/dist/components/renderers/item/item-renderer.d.ts +0 -5
- package/dist/components/renderers/item/item-renderer.d.ts.map +0 -1
- package/dist/components/renderers/item/item-renderer.js +0 -172
- package/dist/components/renderers/level/level-renderer.d.ts +0 -5
- package/dist/components/renderers/level/level-renderer.d.ts.map +0 -1
- package/dist/components/renderers/level/level-renderer.js +0 -11
- package/dist/components/renderers/roof/roof-materials.d.ts +0 -4
- package/dist/components/renderers/roof/roof-materials.d.ts.map +0 -1
- package/dist/components/renderers/roof/roof-materials.js +0 -16
- package/dist/components/renderers/roof/roof-renderer.d.ts +0 -5
- package/dist/components/renderers/roof/roof-renderer.d.ts.map +0 -1
- package/dist/components/renderers/roof/roof-renderer.js +0 -32
- package/dist/components/renderers/roof-segment/roof-segment-renderer.d.ts +0 -5
- package/dist/components/renderers/roof-segment/roof-segment-renderer.d.ts.map +0 -1
- package/dist/components/renderers/roof-segment/roof-segment-renderer.js +0 -40
- package/dist/components/renderers/scan/scan-renderer.d.ts +0 -5
- package/dist/components/renderers/scan/scan-renderer.d.ts.map +0 -1
- package/dist/components/renderers/scan/scan-renderer.js +0 -54
- package/dist/components/renderers/site/site-renderer.d.ts +0 -5
- package/dist/components/renderers/site/site-renderer.d.ts.map +0 -1
- package/dist/components/renderers/site/site-renderer.js +0 -106
- package/dist/components/renderers/slab/slab-renderer.d.ts +0 -5
- package/dist/components/renderers/slab/slab-renderer.d.ts.map +0 -1
- package/dist/components/renderers/slab/slab-renderer.js +0 -58
- package/dist/components/renderers/spawn/spawn-renderer.d.ts +0 -5
- package/dist/components/renderers/spawn/spawn-renderer.d.ts.map +0 -1
- package/dist/components/renderers/spawn/spawn-renderer.js +0 -32
- package/dist/components/renderers/stair/stair-renderer.d.ts +0 -5
- package/dist/components/renderers/stair/stair-renderer.d.ts.map +0 -1
- package/dist/components/renderers/stair/stair-renderer.js +0 -603
- package/dist/components/renderers/stair-segment/stair-segment-renderer.d.ts +0 -5
- package/dist/components/renderers/stair-segment/stair-segment-renderer.d.ts.map +0 -1
- package/dist/components/renderers/stair-segment/stair-segment-renderer.js +0 -51
- package/dist/components/renderers/wall/wall-renderer.d.ts +0 -5
- package/dist/components/renderers/wall/wall-renderer.d.ts.map +0 -1
- package/dist/components/renderers/wall/wall-renderer.js +0 -16
- package/dist/components/renderers/window/window-renderer.d.ts +0 -5
- package/dist/components/renderers/window/window-renderer.d.ts.map +0 -1
- package/dist/components/renderers/window/window-renderer.js +0 -21
- package/dist/components/renderers/zone/zone-renderer.d.ts +0 -5
- package/dist/components/renderers/zone/zone-renderer.d.ts.map +0 -1
- package/dist/components/renderers/zone/zone-renderer.js +0 -181
- 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 -79
|
@@ -1,56 +1,27 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import {
|
|
3
|
+
import { nodeRegistry, StairOpeningSystem, sceneRegistry, useScene, } from '@pascal-app/core';
|
|
4
4
|
import { Canvas, extend, useFrame, useThree } from '@react-three/fiber';
|
|
5
|
-
import { useEffect,
|
|
5
|
+
import { forwardRef, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState, } from 'react';
|
|
6
6
|
import * as THREE from 'three/webgpu';
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import { LevelSystem } from '../../systems/level/level-system';
|
|
16
|
-
import { RoofSystem } from '../../systems/roof/roof-system';
|
|
17
|
-
import { ScanSystem } from '../../systems/scan/scan-system';
|
|
18
|
-
import { SlabSystem } from '../../systems/slab/slab-system';
|
|
19
|
-
import { StairSystem } from '../../systems/stair/stair-system';
|
|
20
|
-
import { WallCutout } from '../../systems/wall/wall-cutout';
|
|
21
|
-
import { WallSystem } from '../../systems/wall/wall-system';
|
|
22
|
-
import { WindowAnimationSystem } from '../../systems/window/window-animation-system';
|
|
23
|
-
import { WindowSystem } from '../../systems/window/window-system';
|
|
24
|
-
import { ZoneSystem } from '../../systems/zone/zone-system';
|
|
7
|
+
import { hasDrawableGeometry } from '../../lib/drawable-geometry';
|
|
8
|
+
import { PERF_OVERLAY_ENABLED, pushGpuSample } from '../../lib/gpu-perf';
|
|
9
|
+
import { applyIsolation, clearIsolation } from '../../lib/isolation';
|
|
10
|
+
import { ensureKtx2Support } from '../../lib/ktx2-loader';
|
|
11
|
+
import { getSceneTheme } from '../../lib/scene-themes';
|
|
12
|
+
import useViewer, {} from '../../store/use-viewer';
|
|
13
|
+
import { FloorElevationSystem } from '../../systems/floor-elevation/floor-elevation-system';
|
|
14
|
+
import { GeometrySystem } from '../../systems/geometry/geometry-system';
|
|
25
15
|
import { ErrorBoundary } from '../error-boundary';
|
|
26
16
|
import { SceneRenderer } from '../renderers/scene-renderer';
|
|
27
17
|
import FrameLimiter from './frame-limiter';
|
|
28
18
|
import { Lights } from './lights';
|
|
29
19
|
import { PerfMonitor } from './perf-monitor';
|
|
30
20
|
import PostProcessing, { DEFAULT_HOVER_STYLES } from './post-processing';
|
|
21
|
+
import { RegisteredSystems } from './registered-systems';
|
|
22
|
+
import { SceneBvh } from './scene-bvh';
|
|
31
23
|
import { SelectionManager } from './selection-manager';
|
|
32
24
|
import { ViewerCamera } from './viewer-camera';
|
|
33
|
-
function AnimatedBackground({ isDark }) {
|
|
34
|
-
const targetColor = useMemo(() => new THREE.Color(), []);
|
|
35
|
-
const initialized = useRef(false);
|
|
36
|
-
useFrame(({ scene }, delta) => {
|
|
37
|
-
const dt = Math.min(delta, 0.1) * 4;
|
|
38
|
-
const targetHex = isDark ? '#1f2433' : '#ffffff';
|
|
39
|
-
if (!(scene.background && scene.background instanceof THREE.Color)) {
|
|
40
|
-
scene.background = new THREE.Color(targetHex);
|
|
41
|
-
initialized.current = true;
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
if (!initialized.current) {
|
|
45
|
-
scene.background.set(targetHex);
|
|
46
|
-
initialized.current = true;
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
targetColor.set(targetHex);
|
|
50
|
-
scene.background.lerp(targetColor, dt);
|
|
51
|
-
});
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
25
|
extend(THREE);
|
|
55
26
|
// R3F's <Canvas> useLayoutEffect has no deps, so any re-render (theme switch,
|
|
56
27
|
// parent re-render, StrictMode double-mount) re-invokes `configure()`. With a
|
|
@@ -67,9 +38,77 @@ extend(THREE);
|
|
|
67
38
|
// concurrent configure() calls await the same init instead of creating two
|
|
68
39
|
// renderers in parallel and only caching the second.
|
|
69
40
|
const WEBGPU_RENDERER_CACHE = new WeakMap();
|
|
41
|
+
const SCENE_READY_SETTLED_FRAMES = 2;
|
|
42
|
+
const SCENE_READY_MAX_WAIT_FRAMES = 180;
|
|
43
|
+
const DIRTY_BUILD_KINDS = new Set([
|
|
44
|
+
'ceiling',
|
|
45
|
+
'door',
|
|
46
|
+
'item',
|
|
47
|
+
'roof',
|
|
48
|
+
'roof-segment',
|
|
49
|
+
'stair',
|
|
50
|
+
'stair-segment',
|
|
51
|
+
'wall',
|
|
52
|
+
'window',
|
|
53
|
+
]);
|
|
54
|
+
const warnedEmptyDraw = process.env.NODE_ENV === 'production' ? null : new WeakSet();
|
|
55
|
+
function canCreateWebGLContext() {
|
|
56
|
+
if (typeof document === 'undefined')
|
|
57
|
+
return false;
|
|
58
|
+
const canvas = document.createElement('canvas');
|
|
59
|
+
try {
|
|
60
|
+
return Boolean(canvas.getContext('webgl2') ?? canvas.getContext('webgl'));
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function canMountGpuViewer() {
|
|
67
|
+
if (typeof window === 'undefined')
|
|
68
|
+
return false;
|
|
69
|
+
if (!('gpu' in navigator) && !canCreateWebGLContext())
|
|
70
|
+
return false;
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
function UnsupportedGpuViewerFallback() {
|
|
74
|
+
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." })] }) }));
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Renderer-level safety net against the empty-vertex-buffer crash.
|
|
78
|
+
*
|
|
79
|
+
* Wraps the per-object render function so any draw whose geometry has a count-0
|
|
80
|
+
* `position` attribute is skipped instead of submitted. One such draw leaves
|
|
81
|
+
* WebGPU vertex buffer slot 0 unbound, which the validator rejects and which
|
|
82
|
+
* poisons the *whole* command encoder — so a single stray empty mesh (e.g. a
|
|
83
|
+
* transient placeholder, or a derived edge/outline geometry) flickers the entire
|
|
84
|
+
* canvas, not just itself. See `hasDrawableGeometry`.
|
|
85
|
+
*
|
|
86
|
+
* The custom render-object function is the documented three.js hook for this
|
|
87
|
+
* (`Renderer.setRenderObjectFunction`); it must call `renderObject()` for
|
|
88
|
+
* everything it keeps. `MergedOutlineNode` captures and restores this function
|
|
89
|
+
* around its passes, so the guard survives outline rendering (its own passes
|
|
90
|
+
* carry the same check inline).
|
|
91
|
+
*/
|
|
92
|
+
function installEmptyDrawGuard(renderer) {
|
|
93
|
+
renderer.setRenderObjectFunction((object, scene, camera, geometry, material, group, lightsNode, clippingContext, passId) => {
|
|
94
|
+
if (!hasDrawableGeometry(geometry)) {
|
|
95
|
+
if (warnedEmptyDraw && !warnedEmptyDraw.has(geometry ?? object)) {
|
|
96
|
+
warnedEmptyDraw.add(geometry ?? object);
|
|
97
|
+
console.warn('[viewer] skipped a draw with an empty position buffer (would poison the WebGPU command encoder)', { name: object?.name, type: object?.type, material: material?.name });
|
|
98
|
+
}
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
;
|
|
102
|
+
renderer.renderObject(object, scene, camera, geometry, material, group, lightsNode, clippingContext, passId);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
70
105
|
function GPUDeviceWatcher() {
|
|
71
106
|
const gl = useThree((s) => s.gl);
|
|
72
107
|
useEffect(() => {
|
|
108
|
+
// Detect KTX2 transcode support as soon as the renderer exists, so catalog
|
|
109
|
+
// `.ktx2` finish textures load even in scenes with no GLB items (whose
|
|
110
|
+
// loader would otherwise be the only thing to call this).
|
|
111
|
+
ensureKtx2Support(gl);
|
|
73
112
|
const backend = gl.backend;
|
|
74
113
|
const device = backend?.device;
|
|
75
114
|
if (!device) {
|
|
@@ -84,46 +123,214 @@ function GPUDeviceWatcher() {
|
|
|
84
123
|
features: Array.from(device.features ?? []),
|
|
85
124
|
});
|
|
86
125
|
device.lost.then((info) => {
|
|
87
|
-
console.error(`[viewer] WebGPU device lost: reason="${info.reason}", message="${info.message}". ` +
|
|
126
|
+
console.error(`[viewer] WebGPU device lost: reason="${info.reason ?? 'unknown'}", message="${info.message ?? ''}". ` +
|
|
88
127
|
'The page must be reloaded to recover the GPU context.');
|
|
89
128
|
});
|
|
90
129
|
// Uncaptured errors are normally silent (only console-warned by Chrome at
|
|
91
130
|
// best). Pipe them to console.error so silent mobile crashes show up.
|
|
92
131
|
const onUncapturedError = (event) => {
|
|
93
|
-
console.error('[viewer] WebGPU uncaptured error:', event
|
|
132
|
+
console.error('[viewer] WebGPU uncaptured error:', event?.error?.message, event?.error);
|
|
94
133
|
};
|
|
95
|
-
device.addEventListener('uncapturederror', onUncapturedError);
|
|
134
|
+
device.addEventListener?.('uncapturederror', onUncapturedError);
|
|
96
135
|
return () => {
|
|
97
|
-
device.removeEventListener('uncapturederror', onUncapturedError);
|
|
136
|
+
device.removeEventListener?.('uncapturederror', onUncapturedError);
|
|
98
137
|
};
|
|
99
138
|
}, [gl]);
|
|
100
139
|
return null;
|
|
101
140
|
}
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
|
|
141
|
+
function ToneMappingExposure() {
|
|
142
|
+
const sceneTheme = useViewer((state) => state.sceneTheme);
|
|
143
|
+
const gl = useThree((state) => state.gl);
|
|
144
|
+
const invalidate = useThree((state) => state.invalidate);
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
gl.toneMappingExposure = getSceneTheme(sceneTheme).toneMappingExposure;
|
|
147
|
+
invalidate();
|
|
148
|
+
}, [gl, invalidate, sceneTheme]);
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
function hasPendingSceneBuildWork() {
|
|
152
|
+
const { dirtyNodes, nodes, rootNodeIds } = useScene.getState();
|
|
153
|
+
for (const id of dirtyNodes) {
|
|
154
|
+
const node = nodes[id];
|
|
155
|
+
if (!node)
|
|
156
|
+
continue;
|
|
157
|
+
// Unreachable nodes (orphaned by a broken detach: the parent doesn't list
|
|
158
|
+
// them in `children`, or no parent and not a root) never render — every
|
|
159
|
+
// renderer enumerates the parent's `children` array — so no system will
|
|
160
|
+
// ever build them or clear their mark. They must not hold scene-ready
|
|
161
|
+
// hostage (observed in prod scene data: two dangling windows kept every
|
|
162
|
+
// bake of that scene waiting out the full readiness cap).
|
|
163
|
+
const parent = node.parentId ? nodes[node.parentId] : undefined;
|
|
164
|
+
const reachable = parent
|
|
165
|
+
? parent.children?.includes(id) === true
|
|
166
|
+
: rootNodeIds.includes(id);
|
|
167
|
+
if (!reachable)
|
|
168
|
+
continue;
|
|
169
|
+
const def = nodeRegistry.get(node.type);
|
|
170
|
+
if (def?.geometry || def?.capabilities?.floorPlaced || DIRTY_BUILD_KINDS.has(node.type)) {
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
function hasCommittedSceneRoot() {
|
|
177
|
+
const { nodes, rootNodeIds } = useScene.getState();
|
|
178
|
+
if (rootNodeIds.length === 0)
|
|
179
|
+
return Object.keys(nodes).length === 0;
|
|
180
|
+
return rootNodeIds.some((id) => sceneRegistry.nodes.has(id));
|
|
181
|
+
}
|
|
182
|
+
function SceneReadyTracker({ onSceneReadyChange, sceneReadyKey, sceneReadyMaxWaitMs, }) {
|
|
183
|
+
const invalidate = useThree((state) => state.invalidate);
|
|
184
|
+
const readyRef = useRef(false);
|
|
185
|
+
const settledFramesRef = useRef(0);
|
|
186
|
+
const waitedFramesRef = useRef(0);
|
|
187
|
+
const waitStartRef = useRef(null);
|
|
188
|
+
const onSceneReadyChangeRef = useRef(onSceneReadyChange);
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
onSceneReadyChangeRef.current = onSceneReadyChange;
|
|
191
|
+
}, [onSceneReadyChange]);
|
|
192
|
+
useEffect(() => {
|
|
193
|
+
void sceneReadyKey;
|
|
194
|
+
readyRef.current = false;
|
|
195
|
+
settledFramesRef.current = 0;
|
|
196
|
+
waitedFramesRef.current = 0;
|
|
197
|
+
waitStartRef.current = null;
|
|
198
|
+
onSceneReadyChangeRef.current?.(false);
|
|
199
|
+
invalidate();
|
|
200
|
+
}, [invalidate, sceneReadyKey]);
|
|
201
|
+
useFrame(() => {
|
|
202
|
+
if (!(onSceneReadyChangeRef.current && !readyRef.current))
|
|
203
|
+
return;
|
|
204
|
+
waitedFramesRef.current += 1;
|
|
205
|
+
waitStartRef.current ??= performance.now();
|
|
206
|
+
// Give-up cap so a permanently-dirty node can't block readiness forever.
|
|
207
|
+
// The frame-count default assumes display-rate frames; a host whose frame
|
|
208
|
+
// cadence is decoupled from wall time (the headless bake page's timer-driven
|
|
209
|
+
// loop runs 180 frames in 3.6s — faster than a cold item download) passes
|
|
210
|
+
// `sceneReadyMaxWaitMs` to make the cap wall-clock instead.
|
|
211
|
+
const capReached = sceneReadyMaxWaitMs
|
|
212
|
+
? performance.now() - waitStartRef.current >= sceneReadyMaxWaitMs
|
|
213
|
+
: waitedFramesRef.current >= SCENE_READY_MAX_WAIT_FRAMES;
|
|
214
|
+
if (!capReached && (!hasCommittedSceneRoot() || hasPendingSceneBuildWork())) {
|
|
215
|
+
settledFramesRef.current = 0;
|
|
216
|
+
invalidate();
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
settledFramesRef.current += 1;
|
|
220
|
+
if (settledFramesRef.current < SCENE_READY_SETTLED_FRAMES) {
|
|
221
|
+
invalidate();
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
readyRef.current = true;
|
|
225
|
+
onSceneReadyChangeRef.current(true);
|
|
226
|
+
}, 10);
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
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) {
|
|
230
|
+
useImperativeHandle(ref, () => ({
|
|
231
|
+
setIsolated: (ids) => applyIsolation(ids),
|
|
232
|
+
}), []);
|
|
233
|
+
// Track the most recently-applied isolation so the cleanup path can
|
|
234
|
+
// restore visibility even if the prop is removed while the component is
|
|
235
|
+
// still mounted. `clearIsolation()` is a no-op when nothing was applied.
|
|
236
|
+
const isolateRef = useRef(undefined);
|
|
237
|
+
useEffect(() => {
|
|
238
|
+
isolateRef.current = isolate ?? null;
|
|
239
|
+
applyIsolation(isolate ?? null);
|
|
240
|
+
return () => {
|
|
241
|
+
// Only clear if this effect was the one that applied — protects
|
|
242
|
+
// against a parent unmount racing with a setIsolated() consumer.
|
|
243
|
+
if (isolateRef.current === isolate)
|
|
244
|
+
clearIsolation();
|
|
245
|
+
};
|
|
246
|
+
}, [isolate]);
|
|
247
|
+
const [rendererInitFailed, setRendererInitFailed] = useState(false);
|
|
248
|
+
// Capability detection runs after mount. We start optimistic (true) so the
|
|
249
|
+
// server-rendered markup and the first client render agree (no hydration
|
|
250
|
+
// mismatch); the effect flips it to false only on environments that expose
|
|
251
|
+
// neither WebGPU nor WebGL.
|
|
252
|
+
const [canMountViewer, setCanMountViewer] = useState(true);
|
|
253
|
+
useEffect(() => {
|
|
254
|
+
if (!canMountGpuViewer())
|
|
255
|
+
setCanMountViewer(false);
|
|
256
|
+
}, []);
|
|
257
|
+
const isDark = useViewer((state) => getSceneTheme(state.sceneTheme).appearance === 'dark');
|
|
258
|
+
const transparentBackground = useViewer((state) => state.transparentBackground);
|
|
259
|
+
// The shadows toggle drives `renderer.shadowMap.enabled` (via the Canvas
|
|
260
|
+
// `shadows` prop) rather than the lights' `castShadow`: toggling castShadow
|
|
261
|
+
// off disposes the shadow map's GPU texture but three r184's WebGPU node
|
|
262
|
+
// cache keeps the shadows-on builder state that references it, so toggling
|
|
263
|
+
// back on reuses destroyed resources and every frame submit fails with a
|
|
264
|
+
// GPUValidationError. Disabling at the renderer level rebuilds materials
|
|
265
|
+
// without disposing anything, so the round-trip is safe.
|
|
266
|
+
const shadowsEnabled = useViewer((state) => state.shadows);
|
|
267
|
+
useLayoutEffect(() => {
|
|
268
|
+
if (transparent === undefined)
|
|
269
|
+
return;
|
|
270
|
+
useViewer.getState().setTransparentBackground(transparent);
|
|
271
|
+
return () => {
|
|
272
|
+
useViewer.getState().setTransparentBackground(false);
|
|
273
|
+
};
|
|
274
|
+
}, [transparent]);
|
|
275
|
+
const defaultShading = defaultRender?.shading;
|
|
276
|
+
const defaultTextures = defaultRender?.textures;
|
|
277
|
+
const defaultColorPreset = defaultRender?.colorPreset;
|
|
278
|
+
const hasDefaultRender = defaultRender != null;
|
|
279
|
+
useEffect(() => {
|
|
280
|
+
const ctx = renderContext;
|
|
281
|
+
useViewer.getState().setRenderContext(ctx);
|
|
282
|
+
const { shading, shadingByContext, setShading } = useViewer.getState();
|
|
283
|
+
setShading(shadingByContext[ctx] ?? defaultShading ?? shading);
|
|
284
|
+
if (!hasDefaultRender || typeof window === 'undefined')
|
|
285
|
+
return;
|
|
286
|
+
let persistedState = {};
|
|
287
|
+
const rawPreferences = window.localStorage.getItem('viewer-preferences');
|
|
288
|
+
if (rawPreferences) {
|
|
289
|
+
try {
|
|
290
|
+
const parsed = JSON.parse(rawPreferences);
|
|
291
|
+
if (parsed &&
|
|
292
|
+
typeof parsed === 'object' &&
|
|
293
|
+
parsed.state &&
|
|
294
|
+
typeof parsed.state === 'object') {
|
|
295
|
+
persistedState = parsed.state;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
catch { }
|
|
299
|
+
}
|
|
300
|
+
if (defaultTextures !== undefined && !('textures' in persistedState)) {
|
|
301
|
+
useViewer.getState().setTextures(defaultTextures);
|
|
302
|
+
}
|
|
303
|
+
if (defaultColorPreset && !('colorPreset' in persistedState)) {
|
|
304
|
+
useViewer.getState().setColorPreset(defaultColorPreset);
|
|
305
|
+
}
|
|
306
|
+
}, [defaultColorPreset, defaultShading, defaultTextures, hasDefaultRender, renderContext]);
|
|
307
|
+
// Coarse-pointer devices (phones/tablets) get a tighter DPR ceiling to keep
|
|
308
|
+
// fragment-shader cost down — saves another ~30% over 1.5x on high-DPI mobile.
|
|
309
|
+
// Desktops (fine pointer) keep the original 1.5 cap.
|
|
310
|
+
const maxDpr = typeof window !== 'undefined' && window.matchMedia('(pointer: coarse)').matches ? 1.25 : 1.5;
|
|
311
|
+
const showGpuFallback = !canMountViewer || rendererInitFailed;
|
|
312
|
+
// When we can't mount the GPU canvas, the SceneReadyTracker never mounts and
|
|
313
|
+
// the host editor would otherwise wait on its scene-readiness timeout. Signal
|
|
314
|
+
// readiness explicitly so the host can drop its loader immediately.
|
|
315
|
+
useEffect(() => {
|
|
316
|
+
if (showGpuFallback)
|
|
317
|
+
onSceneReadyChange?.(true);
|
|
318
|
+
}, [showGpuFallback, onSceneReadyChange]);
|
|
319
|
+
if (showGpuFallback) {
|
|
320
|
+
return _jsx(UnsupportedGpuViewerFallback, {});
|
|
321
|
+
}
|
|
322
|
+
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) => {
|
|
105
323
|
const canvas = props.canvas;
|
|
106
324
|
const cached = canvas ? WEBGPU_RENDERER_CACHE.get(canvas) : undefined;
|
|
107
325
|
if (cached)
|
|
108
326
|
return cached;
|
|
109
|
-
// Surface the env we're about to ask WebGPU for — catches "no
|
|
110
|
-
// navigator.gpu" / "adapter request failed" silently failing in
|
|
111
|
-
// mobile WebViews where WebGPU is gated behind flags.
|
|
112
|
-
const hasGpu = typeof navigator !== 'undefined' && 'gpu' in navigator;
|
|
113
|
-
console.log('[viewer] Creating WebGPURenderer', {
|
|
114
|
-
hasNavigatorGPU: hasGpu,
|
|
115
|
-
ua: typeof navigator !== 'undefined' ? navigator.userAgent : 'n/a',
|
|
116
|
-
});
|
|
117
327
|
const promise = (async () => {
|
|
118
328
|
try {
|
|
119
|
-
const renderer = new THREE.WebGPURenderer(props);
|
|
329
|
+
const renderer = new THREE.WebGPURenderer({ ...props, alpha: true });
|
|
120
330
|
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
|
121
|
-
renderer.toneMappingExposure =
|
|
331
|
+
renderer.toneMappingExposure = getSceneTheme(useViewer.getState().sceneTheme).toneMappingExposure;
|
|
122
332
|
await renderer.init();
|
|
123
|
-
|
|
124
|
-
backend: renderer.backend?.constructor?.name,
|
|
125
|
-
isWebGPU: renderer.isWebGPURenderer === true,
|
|
126
|
-
});
|
|
333
|
+
installEmptyDrawGuard(renderer);
|
|
127
334
|
return renderer;
|
|
128
335
|
}
|
|
129
336
|
catch (err) {
|
|
@@ -133,6 +340,7 @@ const Viewer = ({ children, hoverStyles = DEFAULT_HOVER_STYLES, selectionManager
|
|
|
133
340
|
if (canvas)
|
|
134
341
|
WEBGPU_RENDERER_CACHE.delete(canvas);
|
|
135
342
|
console.error('[viewer] WebGPURenderer init failed', err);
|
|
343
|
+
setRendererInitFailed(true);
|
|
136
344
|
throw err;
|
|
137
345
|
}
|
|
138
346
|
})();
|
|
@@ -143,12 +351,19 @@ const Viewer = ({ children, hoverStyles = DEFAULT_HOVER_STYLES, selectionManager
|
|
|
143
351
|
debounce: 100,
|
|
144
352
|
}, shadows: {
|
|
145
353
|
type: THREE.PCFShadowMap,
|
|
146
|
-
enabled:
|
|
147
|
-
}, children: [_jsx(FrameLimiter, { fps: 50 }), _jsx(ViewerCamera, {}), _jsx(GPUDeviceWatcher, {}), _jsxs(ErrorBoundary, { fallback: null, scope: "viewer-scene", children: [_jsx(Lights, {}), useBvh ? (_jsx(
|
|
148
|
-
};
|
|
354
|
+
enabled: shadowsEnabled,
|
|
355
|
+
}, 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] })] }));
|
|
356
|
+
});
|
|
149
357
|
const DebugRenderer = () => {
|
|
150
358
|
useFrame(({ gl, scene, camera }) => {
|
|
359
|
+
const submittedAt = PERF_OVERLAY_ENABLED ? performance.now() : 0;
|
|
151
360
|
gl.render(scene, camera);
|
|
361
|
+
if (PERF_OVERLAY_ENABLED) {
|
|
362
|
+
const queue = gl.backend?.device?.queue;
|
|
363
|
+
queue?.onSubmittedWorkDone?.().then(() => {
|
|
364
|
+
pushGpuSample(performance.now() - submittedAt);
|
|
365
|
+
});
|
|
366
|
+
}
|
|
152
367
|
});
|
|
153
368
|
return null;
|
|
154
369
|
};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare function Lights(): import("react
|
|
1
|
+
export declare function Lights(): import("react").JSX.Element;
|
|
2
2
|
//# sourceMappingURL=lights.d.ts.map
|
|
@@ -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"}
|