@pascal-app/viewer 1.0.6 → 1.1.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/README.md +54 -21
- package/dist/components/viewer/daylight-atmosphere.d.ts +9 -0
- package/dist/components/viewer/daylight-atmosphere.d.ts.map +1 -0
- package/dist/components/viewer/daylight-atmosphere.js +315 -0
- package/dist/components/viewer/drone-controller.d.ts +15 -0
- package/dist/components/viewer/drone-controller.d.ts.map +1 -0
- package/dist/components/viewer/drone-controller.js +194 -0
- package/dist/components/viewer/glb-interactive.d.ts +4 -18
- package/dist/components/viewer/glb-interactive.d.ts.map +1 -1
- package/dist/components/viewer/glb-interactive.js +32 -268
- package/dist/components/viewer/glb-replace-instances.d.ts +6 -3
- package/dist/components/viewer/glb-replace-instances.d.ts.map +1 -1
- package/dist/components/viewer/glb-replace-instances.js +5 -4
- package/dist/components/viewer/glb-scene-motion.d.ts +8 -0
- package/dist/components/viewer/glb-scene-motion.d.ts.map +1 -0
- package/dist/components/viewer/glb-scene-motion.js +57 -0
- package/dist/components/viewer/glb-scene.d.ts +5 -1
- package/dist/components/viewer/glb-scene.d.ts.map +1 -1
- package/dist/components/viewer/glb-scene.js +290 -49
- package/dist/components/viewer/glb-selection-callout.d.ts +17 -0
- package/dist/components/viewer/glb-selection-callout.d.ts.map +1 -0
- package/dist/components/viewer/glb-selection-callout.js +54 -0
- package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -1
- package/dist/components/viewer/glb-walkthrough-controller.js +6 -3
- package/dist/components/viewer/index.d.ts +5 -0
- package/dist/components/viewer/index.d.ts.map +1 -1
- package/dist/components/viewer/index.js +38 -10
- package/dist/components/viewer/lights.d.ts.map +1 -1
- package/dist/components/viewer/lights.js +192 -5
- package/dist/components/viewer/post-processing.d.ts.map +1 -1
- package/dist/components/viewer/post-processing.js +150 -94
- package/dist/components/viewer/registered-systems.d.ts.map +1 -1
- package/dist/components/viewer/registered-systems.js +2 -2
- package/dist/components/viewer/scene-environment.js +4 -5
- package/dist/components/viewer/selection-callout.d.ts +51 -0
- package/dist/components/viewer/selection-callout.d.ts.map +1 -0
- package/dist/components/viewer/selection-callout.js +197 -0
- package/dist/components/viewer/unsupported-gpu-fallback.d.ts.map +1 -1
- package/dist/components/viewer/unsupported-gpu-fallback.js +21 -1
- package/dist/components/viewer/use-daylight-rooms.d.ts +13 -0
- package/dist/components/viewer/use-daylight-rooms.d.ts.map +1 -0
- package/dist/components/viewer/use-daylight-rooms.js +278 -0
- package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
- package/dist/components/viewer/viewer-camera.js +12 -1
- package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
- package/dist/hooks/use-gltf-ktx2.js +2 -1
- package/dist/index.d.ts +26 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -8
- package/dist/lib/asset-url.d.ts +0 -1
- package/dist/lib/asset-url.d.ts.map +1 -1
- package/dist/lib/asset-url.js +3 -3
- package/dist/lib/daylight/ground.d.ts +21 -0
- package/dist/lib/daylight/ground.d.ts.map +1 -0
- package/dist/lib/daylight/ground.js +53 -0
- package/dist/lib/daylight/location.d.ts +32 -0
- package/dist/lib/daylight/location.d.ts.map +1 -0
- package/dist/lib/daylight/location.js +74 -0
- package/dist/lib/daylight/look.d.ts +33 -0
- package/dist/lib/daylight/look.d.ts.map +1 -0
- package/dist/lib/daylight/look.js +13 -0
- package/dist/lib/daylight/material-hooks.d.ts +2 -0
- package/dist/lib/daylight/material-hooks.d.ts.map +1 -0
- package/dist/lib/daylight/material-hooks.js +150 -0
- package/dist/lib/daylight/room-context.d.ts +12 -0
- package/dist/lib/daylight/room-context.d.ts.map +1 -0
- package/dist/lib/daylight/room-context.js +54 -0
- package/dist/lib/daylight/room-map.d.ts +124 -0
- package/dist/lib/daylight/room-map.d.ts.map +1 -0
- package/dist/lib/daylight/room-map.js +630 -0
- package/dist/lib/daylight/room-scene.d.ts +56 -0
- package/dist/lib/daylight/room-scene.d.ts.map +1 -0
- package/dist/lib/daylight/room-scene.js +391 -0
- package/dist/lib/daylight/rooms.d.ts +292 -0
- package/dist/lib/daylight/rooms.d.ts.map +1 -0
- package/dist/lib/daylight/rooms.js +1268 -0
- package/dist/lib/daylight/runtime.d.ts +33 -0
- package/dist/lib/daylight/runtime.d.ts.map +1 -0
- package/dist/lib/daylight/runtime.js +42 -0
- package/dist/lib/daylight/sky.d.ts +26 -0
- package/dist/lib/daylight/sky.d.ts.map +1 -0
- package/dist/lib/daylight/sky.js +217 -0
- package/dist/lib/daylight/solar.d.ts +46 -0
- package/dist/lib/daylight/solar.d.ts.map +1 -0
- package/dist/lib/daylight/solar.js +156 -0
- package/dist/lib/device-tier.d.ts +29 -0
- package/dist/lib/device-tier.d.ts.map +1 -0
- package/dist/lib/device-tier.js +71 -0
- package/dist/lib/dispose-object3d.d.ts.map +1 -1
- package/dist/lib/dispose-object3d.js +4 -2
- package/dist/lib/drone-flight.d.ts +44 -0
- package/dist/lib/drone-flight.d.ts.map +1 -0
- package/dist/lib/drone-flight.js +121 -0
- package/dist/lib/editor-highlight.d.ts +16 -0
- package/dist/lib/editor-highlight.d.ts.map +1 -0
- package/dist/lib/editor-highlight.js +33 -0
- package/dist/lib/foundation-materials.d.ts.map +1 -1
- package/dist/lib/foundation-materials.js +2 -4
- package/dist/lib/glb-merge.d.ts +23 -0
- package/dist/lib/glb-merge.d.ts.map +1 -0
- package/dist/lib/glb-merge.js +184 -0
- package/dist/lib/hardware-geometry.d.ts +21 -0
- package/dist/lib/hardware-geometry.d.ts.map +1 -0
- package/dist/lib/hardware-geometry.js +283 -0
- package/dist/lib/ktx2-loader.d.ts.map +1 -1
- package/dist/lib/ktx2-loader.js +2 -1
- package/dist/lib/layer-pass.d.ts +2 -0
- package/dist/lib/layer-pass.d.ts.map +1 -1
- package/dist/lib/layer-pass.js +20 -0
- package/dist/lib/materials.d.ts.map +1 -1
- package/dist/lib/materials.js +50 -23
- package/dist/lib/measurement-label.d.ts +4 -0
- package/dist/lib/measurement-label.d.ts.map +1 -0
- package/dist/lib/measurement-label.js +54 -0
- package/dist/lib/measurement-presentation.d.ts +5 -0
- package/dist/lib/measurement-presentation.d.ts.map +1 -0
- package/dist/lib/measurement-presentation.js +8 -0
- package/dist/lib/merged-outline-node.d.ts.map +1 -1
- package/dist/lib/merged-outline-node.js +7 -1
- package/dist/lib/occlusion-culler.d.ts +34 -0
- package/dist/lib/occlusion-culler.d.ts.map +1 -0
- package/dist/lib/occlusion-culler.js +698 -0
- package/dist/lib/pointer-events.d.ts.map +1 -1
- package/dist/lib/pointer-events.js +3 -3
- package/dist/lib/post-processing-resources.d.ts +4 -0
- package/dist/lib/post-processing-resources.d.ts.map +1 -1
- package/dist/lib/post-processing-resources.js +5 -0
- package/dist/lib/readback-rows.d.ts +14 -0
- package/dist/lib/readback-rows.d.ts.map +1 -0
- package/dist/lib/readback-rows.js +31 -0
- package/dist/lib/reveal-pose.d.ts.map +1 -1
- package/dist/lib/reveal-pose.js +3 -0
- package/dist/lib/scene-matrices.d.ts +12 -0
- package/dist/lib/scene-matrices.d.ts.map +1 -0
- package/dist/lib/scene-matrices.js +287 -0
- package/dist/lib/scene-pass-mrt.d.ts +5 -2
- package/dist/lib/scene-pass-mrt.d.ts.map +1 -1
- package/dist/lib/scene-pass-mrt.js +7 -2
- package/dist/lib/scene-themes.d.ts +0 -2
- package/dist/lib/scene-themes.d.ts.map +1 -1
- package/dist/lib/scene-themes.js +0 -2
- package/dist/lib/scene-visibility.d.ts +2 -0
- package/dist/lib/scene-visibility.d.ts.map +1 -1
- package/dist/lib/scene-visibility.js +12 -3
- package/dist/lib/site-surfaces.d.ts +1 -8
- package/dist/lib/site-surfaces.d.ts.map +1 -1
- package/dist/lib/site-surfaces.js +0 -19
- package/dist/lib/snapshot-pipeline.d.ts.map +1 -1
- package/dist/lib/snapshot-pipeline.js +24 -40
- package/dist/lib/texture-reference.d.ts.map +1 -1
- package/dist/lib/texture-reference.js +13 -10
- package/dist/lib/underground-ambiance.d.ts +0 -2
- package/dist/lib/underground-ambiance.d.ts.map +1 -1
- package/dist/lib/underground-ambiance.js +0 -2
- package/dist/lib/viewer-config.d.ts +21 -0
- package/dist/lib/viewer-config.d.ts.map +1 -0
- package/dist/lib/viewer-config.js +21 -0
- package/dist/lib/viewer-preference-storage.d.ts +7 -0
- package/dist/lib/viewer-preference-storage.d.ts.map +1 -0
- package/dist/lib/viewer-preference-storage.js +23 -0
- package/dist/store/use-item-light-pool.d.ts +3 -6
- package/dist/store/use-item-light-pool.d.ts.map +1 -1
- package/dist/store/use-item-light-pool.js +12 -0
- package/dist/store/use-viewer.d.ts +62 -59
- package/dist/store/use-viewer.d.ts.map +1 -1
- package/dist/store/use-viewer.js +106 -19
- package/dist/systems/ceiling/ceiling-system.d.ts +5 -0
- package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
- package/dist/systems/ceiling/ceiling-system.js +11 -1
- package/dist/systems/door/door-system.d.ts.map +1 -1
- package/dist/systems/door/door-system.js +501 -145
- package/dist/systems/door/leaf-profile.d.ts +11 -0
- package/dist/systems/door/leaf-profile.d.ts.map +1 -0
- package/dist/systems/door/leaf-profile.js +140 -0
- package/dist/systems/elevator/elevator-interaction-system.d.ts +16 -2
- package/dist/systems/elevator/elevator-interaction-system.d.ts.map +1 -1
- package/dist/systems/elevator/elevator-interaction-system.js +41 -8
- package/dist/systems/geometry/geometry-status.d.ts +8 -0
- package/dist/systems/geometry/geometry-status.d.ts.map +1 -0
- package/dist/systems/geometry/geometry-status.js +24 -0
- package/dist/systems/interactive/callout-actions.d.ts +72 -0
- package/dist/systems/interactive/callout-actions.d.ts.map +1 -0
- package/dist/systems/interactive/callout-actions.js +212 -0
- package/dist/systems/interactive/interactive-system.d.ts +5 -1
- package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
- package/dist/systems/interactive/interactive-system.js +44 -121
- package/dist/systems/interactive/opening-settled.d.ts +16 -0
- package/dist/systems/interactive/opening-settled.d.ts.map +1 -0
- package/dist/systems/interactive/opening-settled.js +53 -0
- package/dist/systems/item-light/item-light-system.d.ts.map +1 -1
- package/dist/systems/item-light/item-light-system.js +22 -214
- package/dist/systems/item-light/pooled-lights.d.ts +70 -0
- package/dist/systems/item-light/pooled-lights.d.ts.map +1 -0
- package/dist/systems/item-light/pooled-lights.js +449 -0
- package/dist/systems/level/level-system.d.ts.map +1 -1
- package/dist/systems/level/level-system.js +19 -1
- package/dist/systems/roof/roof-material-slots.d.ts +14 -0
- package/dist/systems/roof/roof-material-slots.d.ts.map +1 -0
- package/dist/systems/roof/roof-material-slots.js +13 -0
- package/dist/systems/roof/roof-materials.d.ts +25 -2
- package/dist/systems/roof/roof-materials.d.ts.map +1 -1
- package/dist/systems/roof/roof-materials.js +85 -26
- package/dist/systems/roof/roof-system.d.ts +34 -1
- package/dist/systems/roof/roof-system.d.ts.map +1 -1
- package/dist/systems/roof/roof-system.js +582 -168
- package/dist/systems/wall/opening-casing-geometry.d.ts +14 -0
- package/dist/systems/wall/opening-casing-geometry.d.ts.map +1 -0
- package/dist/systems/wall/opening-casing-geometry.js +102 -0
- package/dist/systems/wall/opening-cutout-geometry.d.ts +9 -0
- package/dist/systems/wall/opening-cutout-geometry.d.ts.map +1 -1
- package/dist/systems/wall/opening-cutout-geometry.js +7 -3
- package/dist/systems/wall/wall-materials.d.ts.map +1 -1
- package/dist/systems/wall/wall-materials.js +28 -35
- package/dist/systems/wall/wall-system.d.ts +11 -0
- package/dist/systems/wall/wall-system.d.ts.map +1 -1
- package/dist/systems/wall/wall-system.js +86 -25
- package/dist/systems/wall/wall-t-junctions.d.ts +15 -0
- package/dist/systems/wall/wall-t-junctions.d.ts.map +1 -0
- package/dist/systems/wall/wall-t-junctions.js +149 -0
- package/dist/systems/window/window-animation-system.d.ts +3 -12
- package/dist/systems/window/window-animation-system.d.ts.map +1 -1
- package/dist/systems/window/window-animation-system.js +37 -122
- package/dist/systems/window/window-pose.d.ts +31 -0
- package/dist/systems/window/window-pose.d.ts.map +1 -0
- package/dist/systems/window/window-pose.js +152 -0
- package/dist/systems/window/window-roller-shutter.d.ts +8 -0
- package/dist/systems/window/window-roller-shutter.d.ts.map +1 -0
- package/dist/systems/window/window-roller-shutter.js +64 -0
- package/dist/systems/window/window-system.d.ts +2 -11
- package/dist/systems/window/window-system.d.ts.map +1 -1
- package/dist/systems/window/window-system.js +470 -303
- package/package.json +6 -6
- package/dist/systems/interactive/control-widget.d.ts +0 -10
- package/dist/systems/interactive/control-widget.d.ts.map +0 -1
- package/dist/systems/interactive/control-widget.js +0 -35
- package/dist/systems/interactive/procedural-controls.d.ts +0 -13
- package/dist/systems/interactive/procedural-controls.d.ts.map +0 -1
- package/dist/systems/interactive/procedural-controls.js +0 -17
|
@@ -1,22 +1,33 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { bakePolicyOf, containsPoint, distanceToBoundary, EXPLODED_GAP,
|
|
3
|
+
import { bakePolicyOf, containsPoint, distanceToBoundary, EXPLODED_GAP, emitter, itemPrompt, operateItem, polygonInteriorPoint, useInteractive, usePascalI18n, } from '@pascal-app/core';
|
|
4
4
|
import { operableParts, ProceduralMotionController, } from '@pascal-app/core/procedural-items';
|
|
5
|
+
import { Html } from '@react-three/drei';
|
|
5
6
|
import { useFrame, useThree } from '@react-three/fiber';
|
|
6
|
-
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
|
7
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
7
8
|
import * as THREE from 'three';
|
|
8
9
|
import { lerp } from 'three/src/math/MathUtils.js';
|
|
9
10
|
import { acceleratedRaycast, computeBoundsTree, disposeBoundsTree } from 'three-mesh-bvh';
|
|
10
11
|
import { useGLTFKTX2 } from '../../hooks/use-gltf-ktx2';
|
|
12
|
+
import { createDaylightGroundMaterial, DAYLIGHT_PARCEL } from '../../lib/daylight/ground';
|
|
13
|
+
import { isDaylightAtmosphere } from '../../lib/daylight/look';
|
|
14
|
+
import { mergeBakedStatic } from '../../lib/glb-merge';
|
|
15
|
+
import { setSurfaceRaycastLayers } from '../../lib/layers';
|
|
11
16
|
import { createSurfaceRoleMaterial } from '../../lib/materials';
|
|
17
|
+
import { attachOcclusionCuller, createOcclusionCuller, } from '../../lib/occlusion-culler';
|
|
12
18
|
import { applyShadowOnly, clearShadowOnly } from '../../lib/shadow-only';
|
|
13
19
|
import useViewer from '../../store/use-viewer';
|
|
20
|
+
import { OPEN_SOUNDS, openingSound } from '../../systems/interactive/callout-actions';
|
|
21
|
+
import { proceduralSettled } from '../../systems/interactive/opening-settled';
|
|
14
22
|
import { useClipActions } from '../../systems/interactive/scripted-clips';
|
|
15
23
|
import { resolveLevelVisibility } from '../../systems/level/level-utils';
|
|
16
24
|
import { GlbInteractive } from './glb-interactive';
|
|
17
25
|
import { bakedLoopMechanisms } from './glb-mechanisms';
|
|
18
26
|
import { GlbReferenceNodes } from './glb-reference-nodes';
|
|
19
27
|
import { GlbReplaceInstances } from './glb-replace-instances';
|
|
28
|
+
import { bindGlbShutters, isGlbShutterHit, toggleGlbShutters } from './glb-scene-motion';
|
|
29
|
+
import { GlbSelectionCallout } from './glb-selection-callout';
|
|
30
|
+
import { useSceneAtmosphere } from './scene-atmosphere';
|
|
20
31
|
/** Baked `kind` → surface role, so monochrome can recolor by role like the
|
|
21
32
|
* parametric viewer (textures-off collapses each face to its themed clay). */
|
|
22
33
|
const ROLE_BY_KIND = {
|
|
@@ -68,6 +79,8 @@ const _camSize = new THREE.Vector3();
|
|
|
68
79
|
const _walkPos = new THREE.Vector3();
|
|
69
80
|
const _reticleNdc = new THREE.Vector2(0, 0);
|
|
70
81
|
const _reticleRaycaster = new THREE.Raycaster();
|
|
82
|
+
// Merged originals sit on the batched layer; the reticle still aims at them (batches never hit).
|
|
83
|
+
setSurfaceRaycastLayers(_reticleRaycaster.layers);
|
|
71
84
|
/** How far ahead (metres) an interactive part counts as "in view" for activation. */
|
|
72
85
|
const WALK_REACH = 3;
|
|
73
86
|
const ZONE_FOOTPRINT_EPSILON = 0.05;
|
|
@@ -110,10 +123,11 @@ function pointInZone(x, z, zone) {
|
|
|
110
123
|
* when clicked. The host disables the parametric `SelectionManager`
|
|
111
124
|
* (`selectionManager="custom"`).
|
|
112
125
|
*/
|
|
113
|
-
export function GlbScene({ url, interactiveItems, referenceNodes, replaceNodes, onObjectsChange, onPick, onWalkthroughChange, }) {
|
|
126
|
+
export function GlbScene({ url, interactiveItems, referenceNodes, replaceNodes, onObjectsChange, onPick, onWalkthroughChange, sceneNodes, }) {
|
|
114
127
|
const gltf = useGLTFKTX2(url);
|
|
115
128
|
const rootRef = useRef(null);
|
|
116
129
|
const actions = useClipActions(gltf.animations, rootRef);
|
|
130
|
+
const resolveNode = useMemo(() => sceneNodes ? (id) => sceneNodes[id] : undefined, [sceneNodes]);
|
|
117
131
|
const proceduralPlayback = useMemo(() => {
|
|
118
132
|
const byNode = new Map();
|
|
119
133
|
gltf.scene.traverse((object) => {
|
|
@@ -180,6 +194,7 @@ export function GlbScene({ url, interactiveItems, referenceNodes, replaceNodes,
|
|
|
180
194
|
clips: entry.clips,
|
|
181
195
|
spinParts,
|
|
182
196
|
sequence: 0,
|
|
197
|
+
settling: null,
|
|
183
198
|
},
|
|
184
199
|
];
|
|
185
200
|
}));
|
|
@@ -200,8 +215,14 @@ export function GlbScene({ url, interactiveItems, referenceNodes, replaceNodes,
|
|
|
200
215
|
const camera = useThree((state) => state.camera);
|
|
201
216
|
const controls = useThree((state) => state.controls);
|
|
202
217
|
const walkthroughMode = useViewer((s) => s.walkthroughMode);
|
|
218
|
+
// Doors, the reticle prompt and the room HUD belong to the walk; the drone just flies.
|
|
219
|
+
const walkingOnFoot = useViewer((s) => s.walkthroughMode && s.walkthroughMovement === 'walk');
|
|
203
220
|
const textures = useViewer((s) => s.textures);
|
|
204
221
|
const sceneTheme = useViewer((s) => s.sceneTheme);
|
|
222
|
+
// Static merge of the artifact (see lib/glb-merge.ts); `?merge=0` keeps every mesh separate.
|
|
223
|
+
const mergeRef = useRef(null);
|
|
224
|
+
const [mergeVersion, setMergeVersion] = useState(0);
|
|
225
|
+
const [mergeRebuild, setMergeRebuild] = useState(0);
|
|
205
226
|
// Monochrome: strip the baked textures and recolor every building mesh with a
|
|
206
227
|
// flat themed-clay material by surface role — mirrors the parametric viewer's
|
|
207
228
|
// textures-off path. The original baked material is stashed on the mesh
|
|
@@ -223,7 +244,40 @@ export function GlbScene({ url, interactiveItems, referenceNodes, replaceNodes,
|
|
|
223
244
|
: createSurfaceRoleMaterial(role, 'clay', THREE.DoubleSide, sceneTheme);
|
|
224
245
|
});
|
|
225
246
|
});
|
|
226
|
-
|
|
247
|
+
for (const batch of mergeRef.current?.batches ?? [])
|
|
248
|
+
applyBatchLook(batch, textures, sceneTheme);
|
|
249
|
+
}, [gltf.scene, textures, sceneTheme, mergeVersion]);
|
|
250
|
+
// Baked meshes take part in the sun's shadows (the bake carries no shadow flags). Transparent
|
|
251
|
+
// surfaces (glazing) don't cast, so sunlight still reaches the rooms behind them. Foliage ships
|
|
252
|
+
// opaque with cut-out textures (square dark leaves): alpha-test it.
|
|
253
|
+
useEffect(() => {
|
|
254
|
+
gltf.scene.traverse((object) => {
|
|
255
|
+
if (!object.userData.kind?.startsWith('trees:'))
|
|
256
|
+
return;
|
|
257
|
+
object.traverse((child) => {
|
|
258
|
+
const mesh = child;
|
|
259
|
+
if (!mesh.isMesh)
|
|
260
|
+
return;
|
|
261
|
+
for (const material of Array.isArray(mesh.material) ? mesh.material : [mesh.material]) {
|
|
262
|
+
const standard = material;
|
|
263
|
+
if (!standard.map || standard.alphaTest >= 0.5)
|
|
264
|
+
continue;
|
|
265
|
+
standard.alphaTest = 0.5;
|
|
266
|
+
standard.side = THREE.DoubleSide;
|
|
267
|
+
standard.transparent = false;
|
|
268
|
+
standard.needsUpdate = true;
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
gltf.scene.traverse((child) => {
|
|
273
|
+
const mesh = child;
|
|
274
|
+
if (!mesh.isMesh)
|
|
275
|
+
return;
|
|
276
|
+
const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
|
|
277
|
+
mesh.receiveShadow = true;
|
|
278
|
+
mesh.castShadow = !materials.some((material) => material?.transparent);
|
|
279
|
+
});
|
|
280
|
+
}, [gltf.scene]);
|
|
227
281
|
// The baked scene isn't wrapped in <SceneBvh> (the community viewer runs with
|
|
228
282
|
// useBvh={false}), so hover/pick raycasts against the baked building were
|
|
229
283
|
// brute-force triangle tests — dozens of ms per pointer move on a dense scene,
|
|
@@ -339,8 +393,6 @@ export function GlbScene({ url, interactiveItems, referenceNodes, replaceNodes,
|
|
|
339
393
|
levelsWithZones: new Set(zoneList.map((zone) => zone.levelId)),
|
|
340
394
|
};
|
|
341
395
|
}, [gltf.scene]);
|
|
342
|
-
// Level pascalIds bottom-to-top, for the interactive light pool's level factor.
|
|
343
|
-
const levelOrder = useMemo(() => levels.map((entry) => entry.id), [levels]);
|
|
344
396
|
// Loop clips no other controller plays (a plugin kind's mechanism) run on
|
|
345
397
|
// click and E. As in the editor they start stopped and never persist.
|
|
346
398
|
const loopMechanisms = useMemo(() => bakedLoopMechanisms(identity, new Set([
|
|
@@ -395,6 +447,80 @@ export function GlbScene({ url, interactiveItems, referenceNodes, replaceNodes,
|
|
|
395
447
|
onObjectsChange?.(identity);
|
|
396
448
|
return () => onObjectsChange?.(new Map());
|
|
397
449
|
}, [identity, onObjectsChange]);
|
|
450
|
+
// Static merge: thousands of baked meshes become a few hundred draws per pass (the enhanced
|
|
451
|
+
// viewer's batching). Rebuilt when the interactive items change, or when a merged mesh is
|
|
452
|
+
// shown or hidden on its own (a plugin taking over a baked kind, an isolation), so a batch
|
|
453
|
+
// never draws something that should be gone.
|
|
454
|
+
const mergeEnabled = useMemo(() => typeof window === 'undefined' ||
|
|
455
|
+
new URLSearchParams(window.location.search).get('merge') !== '0', []);
|
|
456
|
+
useEffect(() => {
|
|
457
|
+
if (!mergeEnabled)
|
|
458
|
+
return;
|
|
459
|
+
const animated = new Set();
|
|
460
|
+
for (const clip of gltf.animations) {
|
|
461
|
+
for (const track of clip.tracks) {
|
|
462
|
+
const { nodeName } = THREE.PropertyBinding.parseTrackName(track.name);
|
|
463
|
+
const node = THREE.PropertyBinding.findNode(gltf.scene, nodeName);
|
|
464
|
+
if (node)
|
|
465
|
+
animated.add(node);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
const merge = mergeBakedStatic(gltf.scene, (kind) => ROLE_BY_KIND[kind ?? ''], {
|
|
469
|
+
keepIds: new Set((interactiveItems ?? []).map((item) => item.pascalId)),
|
|
470
|
+
animated,
|
|
471
|
+
});
|
|
472
|
+
const { textures: texturesNow, sceneTheme: themeNow } = useViewer.getState();
|
|
473
|
+
for (const batch of merge.batches)
|
|
474
|
+
applyBatchLook(batch, texturesNow, themeNow);
|
|
475
|
+
mergeRef.current = merge;
|
|
476
|
+
mergeSignature.current = merge.visibilitySignature();
|
|
477
|
+
setMergeVersion((version) => version + 1);
|
|
478
|
+
return () => {
|
|
479
|
+
for (const batch of merge.batches)
|
|
480
|
+
disposeBatchLook(batch);
|
|
481
|
+
merge.dispose();
|
|
482
|
+
if (mergeRef.current === merge)
|
|
483
|
+
mergeRef.current = null;
|
|
484
|
+
};
|
|
485
|
+
}, [gltf.scene, gltf.animations, interactiveItems, mergeEnabled, mergeRebuild]);
|
|
486
|
+
// Occlusion culling (see lib/occlusion-culler.ts): what the walls, slabs and roofs hide from
|
|
487
|
+
// the eye isn't drawn. Rebuilt with the merge, whose batches are what it mostly culls.
|
|
488
|
+
// `?occlusion=0` draws everything.
|
|
489
|
+
const rootScene = useThree((state) => state.scene);
|
|
490
|
+
const renderer = useThree((state) => state.gl);
|
|
491
|
+
const getThree = useThree((state) => state.get);
|
|
492
|
+
const occlusionEnabled = useMemo(() => typeof window === 'undefined' ||
|
|
493
|
+
new URLSearchParams(window.location.search).get('occlusion') !== '0', []);
|
|
494
|
+
useEffect(() => {
|
|
495
|
+
// three's drawing pipeline (WebGPU renderer) answers which objects a render skips.
|
|
496
|
+
const pipeline = renderer.drawingPipeline;
|
|
497
|
+
if (!occlusionEnabled || !pipeline)
|
|
498
|
+
return;
|
|
499
|
+
const culler = createOcclusionCuller(gltf.scene);
|
|
500
|
+
const detach = attachOcclusionCuller({
|
|
501
|
+
scene: rootScene,
|
|
502
|
+
pipeline,
|
|
503
|
+
culler,
|
|
504
|
+
getCamera: () => getThree().camera,
|
|
505
|
+
});
|
|
506
|
+
return () => {
|
|
507
|
+
detach();
|
|
508
|
+
culler.dispose();
|
|
509
|
+
};
|
|
510
|
+
}, [gltf.scene, rootScene, renderer, getThree, occlusionEnabled, mergeVersion]);
|
|
511
|
+
const mergeSignature = useRef(0);
|
|
512
|
+
const mergeCheck = useRef(0);
|
|
513
|
+
useFrame((_, delta) => {
|
|
514
|
+
const merge = mergeRef.current;
|
|
515
|
+
if (!merge)
|
|
516
|
+
return;
|
|
517
|
+
mergeCheck.current += delta;
|
|
518
|
+
if (mergeCheck.current < 0.25)
|
|
519
|
+
return;
|
|
520
|
+
mergeCheck.current = 0;
|
|
521
|
+
if (merge.visibilitySignature() !== mergeSignature.current)
|
|
522
|
+
setMergeRebuild((n) => n + 1);
|
|
523
|
+
});
|
|
398
524
|
// Apply the viewer's level display to the baked floors each frame, by the
|
|
399
525
|
// parametric LevelSystem's rule (solo, and the levels above the current one
|
|
400
526
|
// hidden when the host asks). Walkthrough always shows the full stacked
|
|
@@ -451,8 +577,16 @@ export function GlbScene({ url, interactiveItems, referenceNodes, replaceNodes,
|
|
|
451
577
|
if (command && command.sequence > entry.sequence) {
|
|
452
578
|
entry.controller.command(command);
|
|
453
579
|
entry.sequence = command.sequence;
|
|
580
|
+
entry.settling = command.sequence;
|
|
454
581
|
}
|
|
455
582
|
const frame = entry.controller.tick(delta);
|
|
583
|
+
if (entry.settling !== null && frame.settled) {
|
|
584
|
+
emitter.emit('procedural:motion-settled', {
|
|
585
|
+
nodeId: nodeId,
|
|
586
|
+
sequence: entry.settling,
|
|
587
|
+
});
|
|
588
|
+
entry.settling = null;
|
|
589
|
+
}
|
|
456
590
|
for (const [clipName, binding] of entry.clips) {
|
|
457
591
|
const action = proceduralPlayback.actions.get(clipName);
|
|
458
592
|
if (!action)
|
|
@@ -526,15 +660,6 @@ export function GlbScene({ url, interactiveItems, referenceNodes, replaceNodes,
|
|
|
526
660
|
// Items the walkthrough operates through their toggles; others fall through to loop clips.
|
|
527
661
|
const toggleableItem = useCallback((pascalId) => interactiveItems?.find((item) => item.pascalId === pascalId &&
|
|
528
662
|
item.interactive.controls.some((control) => control.kind === 'toggle')), [interactiveItems]);
|
|
529
|
-
// A click opens and closes an item with an `open` clip (an authored object,
|
|
530
|
-
// a scripted window or door), like a door.
|
|
531
|
-
const toggleOpenControl = useCallback((identityNode) => {
|
|
532
|
-
const item = toggleableItem(identityNode.userData.pascalId);
|
|
533
|
-
if (!item || itemInteraction(item.interactive).kind !== 'open')
|
|
534
|
-
return false;
|
|
535
|
-
operateItem(item.pascalId, item.interactive);
|
|
536
|
-
return true;
|
|
537
|
-
}, [toggleableItem]);
|
|
538
663
|
const toggleLoopMechanism = useCallback((identityNode) => {
|
|
539
664
|
const id = identityNode.userData.pascalId;
|
|
540
665
|
if (!(id && loopMechanisms.has(id)))
|
|
@@ -543,7 +668,11 @@ export function GlbScene({ url, interactiveItems, referenceNodes, replaceNodes,
|
|
|
543
668
|
state.setMechanism(id, !state.mechanisms[id]);
|
|
544
669
|
return true;
|
|
545
670
|
}, [loopMechanisms]);
|
|
671
|
+
useEffect(() => bindGlbShutters(identity, actions), [identity, actions]);
|
|
546
672
|
const openIds = useRef(new Set());
|
|
673
|
+
// Bumped on every baked door or window toggle so the selection callout re-reads `openIds`.
|
|
674
|
+
const [, setOpenVersion] = useState(0);
|
|
675
|
+
const isOpen = useCallback((id) => openIds.current.has(id), []);
|
|
547
676
|
const toggleProcedural = useCallback((hit, identityNode) => {
|
|
548
677
|
const extras = identityNode.userData;
|
|
549
678
|
if (extras.kind !== 'procedural-item')
|
|
@@ -588,6 +717,10 @@ export function GlbScene({ url, interactiveItems, referenceNodes, replaceNodes,
|
|
|
588
717
|
openIds.current.add(id);
|
|
589
718
|
else
|
|
590
719
|
openIds.current.delete(id);
|
|
720
|
+
setOpenVersion((version) => version + 1);
|
|
721
|
+
// Played backwards from where it stands, the clip lands after its current time.
|
|
722
|
+
if (!willOpen)
|
|
723
|
+
return new Promise((resolve) => setTimeout(resolve, action.time * 1000));
|
|
591
724
|
}, [actions]);
|
|
592
725
|
// The room whose polygon contains a world point. Resolving by the raycast hit
|
|
593
726
|
// point (rather than a node origin) means any surface inside a room footprint —
|
|
@@ -642,13 +775,15 @@ export function GlbScene({ url, interactiveItems, referenceNodes, replaceNodes,
|
|
|
642
775
|
}
|
|
643
776
|
});
|
|
644
777
|
// ── Walkthrough: first-person HUD + interaction ────────────────────────────
|
|
778
|
+
const [shuttersInView, setShuttersInView] = useState(false);
|
|
645
779
|
const walkDoorRef = useRef(null);
|
|
780
|
+
const walkPromptRef = useRef(null);
|
|
646
781
|
const lastWalkKey = useRef(null);
|
|
647
782
|
// Each frame in walkthrough, report the floor + room the camera stands in and
|
|
648
783
|
// the openable directly ahead (a forward ray from screen centre) so the host
|
|
649
784
|
// can draw the reticle prompt. Fires the callback only when the state changes.
|
|
650
785
|
useFrame(() => {
|
|
651
|
-
if (!
|
|
786
|
+
if (!walkingOnFoot)
|
|
652
787
|
return;
|
|
653
788
|
camera.getWorldPosition(_walkPos);
|
|
654
789
|
let floor = levels[0] ?? null;
|
|
@@ -715,7 +850,14 @@ export function GlbScene({ url, interactiveItems, referenceNodes, replaceNodes,
|
|
|
715
850
|
door = { label: extras.label ?? 'Item', isOpen, verb: isOpen ? 'turn off' : 'turn on' };
|
|
716
851
|
}
|
|
717
852
|
}
|
|
853
|
+
if (hit && !doorNode) {
|
|
854
|
+
const node = findIdentityAncestor(hit.object);
|
|
855
|
+
if (node?.userData?.shutters && node)
|
|
856
|
+
doorNode = { hit: hit.object, node };
|
|
857
|
+
}
|
|
858
|
+
setShuttersInView(Boolean(doorNode?.node.userData?.shutters));
|
|
718
859
|
walkDoorRef.current = doorNode;
|
|
860
|
+
walkPromptRef.current = door;
|
|
719
861
|
const key = `${floor?.id ?? ''}|${zone?.id ?? ''}|${door ? `${doorId}:${door.isOpen}` : ''}`;
|
|
720
862
|
if (key !== lastWalkKey.current) {
|
|
721
863
|
lastWalkKey.current = key;
|
|
@@ -724,41 +866,82 @@ export function GlbScene({ url, interactiveItems, referenceNodes, replaceNodes,
|
|
|
724
866
|
});
|
|
725
867
|
// E or click activates the openable in view. The click also re-locks the
|
|
726
868
|
// pointer through the walkthrough controller; no selection happens.
|
|
727
|
-
const activateWalkDoor = useCallback(() => {
|
|
869
|
+
const activateWalkDoor = useCallback((shutterHit = false) => {
|
|
728
870
|
const target = walkDoorRef.current;
|
|
729
871
|
if (!target)
|
|
730
872
|
return;
|
|
731
|
-
|
|
873
|
+
if (shutterHit && target.node.userData.shutters && isGlbShutterHit(target.hit, target.node)) {
|
|
874
|
+
playShutterSound(target.node);
|
|
875
|
+
toggleGlbShutters(target.node);
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
const prompt = walkPromptRef.current;
|
|
879
|
+
const extras = target.node.userData;
|
|
880
|
+
const switches = prompt?.verb !== undefined && !['open', 'close'].includes(prompt.verb);
|
|
881
|
+
const sound = switches
|
|
882
|
+
? OPEN_SOUNDS.switch
|
|
883
|
+
: openingSound((extras.pascalId && sceneNodes?.[extras.pascalId]) || { type: extras.kind ?? 'door' });
|
|
884
|
+
const play = (settled, as = sound) => {
|
|
885
|
+
if (!prompt)
|
|
886
|
+
return;
|
|
887
|
+
// A garage door's motor runs the whole way, so it starts with the press.
|
|
888
|
+
if (!(prompt.isOpen && settled) || as === OPEN_SOUNDS.garage) {
|
|
889
|
+
emitter.emit('interaction:sound', prompt.isOpen ? as.off : as.on);
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
if (as.swing)
|
|
893
|
+
emitter.emit('interaction:sound', as.swing);
|
|
894
|
+
void settled.then(() => emitter.emit('interaction:sound', as.off));
|
|
895
|
+
};
|
|
896
|
+
const item = toggleableItem(extras.pascalId);
|
|
732
897
|
if (item) {
|
|
898
|
+
play();
|
|
733
899
|
operateItem(item.pascalId, item.interactive);
|
|
734
900
|
return;
|
|
735
901
|
}
|
|
736
|
-
if (
|
|
737
|
-
|
|
738
|
-
|
|
902
|
+
if (toggleProcedural(target.hit, target.node)) {
|
|
903
|
+
// A recipe's door or lid swings, its drawer slides; both land on the motion's end.
|
|
904
|
+
const sliding = findProceduralMotionAncestor(target.hit)?.kind === 'slide';
|
|
905
|
+
if (switches)
|
|
906
|
+
play();
|
|
907
|
+
else
|
|
908
|
+
play(proceduralSettled(extras.pascalId), sliding ? OPEN_SOUNDS.drawer : OPEN_SOUNDS.panel);
|
|
909
|
+
}
|
|
910
|
+
else if (toggleLoopMechanism(target.node))
|
|
911
|
+
play();
|
|
912
|
+
else
|
|
913
|
+
play(toggleOpenable(target.node));
|
|
914
|
+
}, [sceneNodes, toggleableItem, toggleLoopMechanism, toggleOpenable, toggleProcedural]);
|
|
739
915
|
useEffect(() => {
|
|
740
|
-
if (!
|
|
916
|
+
if (!walkingOnFoot)
|
|
741
917
|
return;
|
|
742
918
|
const onKey = (event) => {
|
|
743
919
|
if (event.key.toLowerCase() === 'e')
|
|
744
920
|
activateWalkDoor();
|
|
921
|
+
const shutters = walkDoorRef.current?.node;
|
|
922
|
+
if (event.key.toLowerCase() === 'f' && !event.repeat && shutters?.userData.shutters) {
|
|
923
|
+
playShutterSound(shutters);
|
|
924
|
+
toggleGlbShutters(shutters);
|
|
925
|
+
}
|
|
745
926
|
};
|
|
746
927
|
const canvas = document.querySelector('canvas');
|
|
747
928
|
window.addEventListener('keydown', onKey);
|
|
748
|
-
|
|
929
|
+
const onClick = () => activateWalkDoor(true);
|
|
930
|
+
canvas?.addEventListener('click', onClick);
|
|
749
931
|
return () => {
|
|
750
932
|
window.removeEventListener('keydown', onKey);
|
|
751
|
-
canvas?.removeEventListener('click',
|
|
933
|
+
canvas?.removeEventListener('click', onClick);
|
|
752
934
|
};
|
|
753
|
-
}, [
|
|
754
|
-
// Clear the HUD (and stale targeting) whenever
|
|
935
|
+
}, [walkingOnFoot, activateWalkDoor]);
|
|
936
|
+
// Clear the HUD (and stale targeting) whenever the walk ends, or turns into a drone flight.
|
|
755
937
|
useEffect(() => {
|
|
756
|
-
if (
|
|
938
|
+
if (walkingOnFoot)
|
|
757
939
|
return;
|
|
940
|
+
setShuttersInView(false);
|
|
758
941
|
walkDoorRef.current = null;
|
|
759
942
|
lastWalkKey.current = null;
|
|
760
943
|
onWalkthroughChange?.(null);
|
|
761
|
-
}, [
|
|
944
|
+
}, [walkingOnFoot, onWalkthroughChange]);
|
|
762
945
|
const lastHover = useRef(null);
|
|
763
946
|
const handlePointerMove = useCallback((event) => {
|
|
764
947
|
event.stopPropagation();
|
|
@@ -779,8 +962,8 @@ export function GlbScene({ url, interactiveItems, referenceNodes, replaceNodes,
|
|
|
779
962
|
lastHover.current = null;
|
|
780
963
|
onPick?.({ type: 'hover', pick: null, modifiers: NO_MODIFIERS });
|
|
781
964
|
}, [onPick]);
|
|
782
|
-
// The host's rules decide the selection;
|
|
783
|
-
//
|
|
965
|
+
// The host's rules decide the selection; what the element can do is offered
|
|
966
|
+
// on its selection callout, never run by the click itself.
|
|
784
967
|
const handleClick = useCallback((event) => {
|
|
785
968
|
event.stopPropagation();
|
|
786
969
|
// Walkthrough handles its own door activation (E / canvas click) and never
|
|
@@ -789,24 +972,7 @@ export function GlbScene({ url, interactiveItems, referenceNodes, replaceNodes,
|
|
|
789
972
|
return;
|
|
790
973
|
const pick = resolveGlbPick(event.intersections, identity);
|
|
791
974
|
onPick({ type: 'click', pick, modifiers: modifiersOf(event.nativeEvent) });
|
|
792
|
-
|
|
793
|
-
return;
|
|
794
|
-
const object = identity.get(pick.nodeId);
|
|
795
|
-
if (!object)
|
|
796
|
-
return;
|
|
797
|
-
if (!(toggleOpenControl(object) ||
|
|
798
|
-
toggleProcedural(pick.hitObject, object) ||
|
|
799
|
-
toggleLoopMechanism(object)))
|
|
800
|
-
toggleOpenable(object);
|
|
801
|
-
}, [
|
|
802
|
-
identity,
|
|
803
|
-
onPick,
|
|
804
|
-
toggleLoopMechanism,
|
|
805
|
-
toggleOpenControl,
|
|
806
|
-
toggleOpenable,
|
|
807
|
-
toggleProcedural,
|
|
808
|
-
walkthroughMode,
|
|
809
|
-
]);
|
|
975
|
+
}, [identity, onPick, walkthroughMode]);
|
|
810
976
|
// A click on empty space clears the room and the element (the floor stays).
|
|
811
977
|
const handlePointerMissed = useCallback((event) => {
|
|
812
978
|
if (useViewer.getState().walkthroughMode || !onPick)
|
|
@@ -822,7 +988,12 @@ export function GlbScene({ url, interactiveItems, referenceNodes, replaceNodes,
|
|
|
822
988
|
for (const mesh of occluderOwnMeshes)
|
|
823
989
|
clearShadowOnly(mesh);
|
|
824
990
|
}, [occluderOwnMeshes]);
|
|
825
|
-
return (_jsxs("group", { ref: rootRef, children: [_jsx("primitive", { object: gltf.scene, onClick: handleClick, onPointerMissed: handlePointerMissed, onPointerMove: handlePointerMove, onPointerOut: handlePointerOut }), interactiveItems?.length ? (_jsx(GlbInteractive, { actions: actions, identity: identity, items: interactiveItems
|
|
991
|
+
return (_jsxs("group", { ref: rootRef, children: [walkingOnFoot && shuttersInView ? _jsx(GlbShuttersPrompt, {}) : null, _jsx("primitive", { object: gltf.scene, onClick: handleClick, onPointerMissed: handlePointerMissed, onPointerMove: handlePointerMove, onPointerOut: handlePointerOut }), _jsx(GlbSelectionCallout, { identity: identity, isOpen: isOpen, items: interactiveItems, loopMechanisms: loopMechanisms, sceneNodes: sceneNodes, toggleOpen: toggleOpenable }), interactiveItems?.length ? (_jsx(GlbInteractive, { actions: actions, identity: identity, items: interactiveItems })) : null, referenceNodes?.length ? (_jsx(GlbReferenceNodes, { identity: identity, nodes: referenceNodes })) : null, replaceNodes?.length ? (_jsx(GlbReplaceInstances, { identity: identity, nodes: replaceNodes, resolve: resolveNode })) : null, _jsx(GlbDaylightGround, { root: gltf.scene })] }));
|
|
992
|
+
}
|
|
993
|
+
function playShutterSound(node) {
|
|
994
|
+
const id = node.userData.pascalId;
|
|
995
|
+
const open = id && useInteractive.getState().mechanismParts[`${id}:shutters`];
|
|
996
|
+
emitter.emit('interaction:sound', open ? OPEN_SOUNDS.shutters.off : OPEN_SOUNDS.shutters.on);
|
|
826
997
|
}
|
|
827
998
|
const NO_MODIFIERS = {
|
|
828
999
|
alt: false,
|
|
@@ -833,3 +1004,73 @@ const NO_MODIFIERS = {
|
|
|
833
1004
|
function modifiersOf(event) {
|
|
834
1005
|
return { alt: event.altKey, ctrl: event.ctrlKey, meta: event.metaKey, shift: event.shiftKey };
|
|
835
1006
|
}
|
|
1007
|
+
const NO_RAYCAST = () => { };
|
|
1008
|
+
/** A merged batch shows its baked material, or in monochrome the themed clay of its role. */
|
|
1009
|
+
function applyBatchLook(batch, textures, sceneTheme) {
|
|
1010
|
+
const { role, bakedMaterial } = batch.userData;
|
|
1011
|
+
disposeBatchLook(batch);
|
|
1012
|
+
batch.material =
|
|
1013
|
+
textures || !role
|
|
1014
|
+
? bakedMaterial
|
|
1015
|
+
: createSurfaceRoleMaterial(role, 'clay', THREE.DoubleSide, sceneTheme);
|
|
1016
|
+
}
|
|
1017
|
+
function disposeBatchLook(batch) {
|
|
1018
|
+
const material = batch.material;
|
|
1019
|
+
if (material !== batch.userData.bakedMaterial)
|
|
1020
|
+
material.dispose();
|
|
1021
|
+
}
|
|
1022
|
+
/**
|
|
1023
|
+
* The colored appearance's ground under a baked artifact (the bake strips the live horizon
|
|
1024
|
+
* disc): the squared daylight ground just under the baked site fill, so the parcel keeps its own
|
|
1025
|
+
* surface; the atmosphere's fog takes it to the horizon.
|
|
1026
|
+
*/
|
|
1027
|
+
function GlbDaylightGround({ root }) {
|
|
1028
|
+
const atmosphere = useSceneAtmosphere();
|
|
1029
|
+
const daylight = isDaylightAtmosphere(atmosphere);
|
|
1030
|
+
// The baked site fill (the site's own opaque surface) becomes the daylight parcel: a neutral,
|
|
1031
|
+
// slightly lighter surface so the lot reads as its own place on the squared ground.
|
|
1032
|
+
useEffect(() => {
|
|
1033
|
+
if (!daylight)
|
|
1034
|
+
return;
|
|
1035
|
+
const parcel = createDaylightGroundMaterial(false, { color: DAYLIGHT_PARCEL });
|
|
1036
|
+
const swapped = [];
|
|
1037
|
+
root.traverse((object) => {
|
|
1038
|
+
if (object.userData.kind !== 'site')
|
|
1039
|
+
return;
|
|
1040
|
+
for (const child of object.children) {
|
|
1041
|
+
const mesh = child;
|
|
1042
|
+
const material = mesh.material;
|
|
1043
|
+
if (!mesh.isMesh || mesh.userData.pascalId || !material)
|
|
1044
|
+
continue;
|
|
1045
|
+
if (Array.isArray(material) || material.transparent)
|
|
1046
|
+
continue;
|
|
1047
|
+
swapped.push([mesh, mesh.material]);
|
|
1048
|
+
mesh.material = parcel;
|
|
1049
|
+
}
|
|
1050
|
+
});
|
|
1051
|
+
return () => {
|
|
1052
|
+
for (const [mesh, material] of swapped)
|
|
1053
|
+
mesh.material = material;
|
|
1054
|
+
parcel.dispose();
|
|
1055
|
+
};
|
|
1056
|
+
}, [daylight, root]);
|
|
1057
|
+
const solid = useViewer((state) => state.shading === 'solid');
|
|
1058
|
+
const material = useMemo(() => {
|
|
1059
|
+
if (!daylight)
|
|
1060
|
+
return null;
|
|
1061
|
+
const ground = createDaylightGroundMaterial(solid, { horizon: atmosphere?.fogRadiance });
|
|
1062
|
+
ground.polygonOffset = true;
|
|
1063
|
+
ground.polygonOffsetFactor = 2;
|
|
1064
|
+
ground.polygonOffsetUnits = 4;
|
|
1065
|
+
return ground;
|
|
1066
|
+
}, [atmosphere, daylight, solid]);
|
|
1067
|
+
useEffect(() => () => material?.dispose(), [material]);
|
|
1068
|
+
if (!material)
|
|
1069
|
+
return null;
|
|
1070
|
+
return (_jsx("mesh", { material: material, position: [0, -0.07, 0], raycast: NO_RAYCAST, receiveShadow: true, rotation: [-Math.PI / 2, 0, 0], children: _jsx("planeGeometry", { args: [12_000, 12_000] }) }));
|
|
1071
|
+
}
|
|
1072
|
+
/** Match the walkthrough HUD's secondary key prompt, without requiring host wiring. */
|
|
1073
|
+
function GlbShuttersPrompt() {
|
|
1074
|
+
const i18n = usePascalI18n();
|
|
1075
|
+
return (_jsx(Html, { fullscreen: true, style: { pointerEvents: 'none' }, children: _jsx("div", { className: "dark pointer-events-none absolute top-1/2 left-1/2 mt-16 -translate-x-1/2 whitespace-nowrap text-foreground", children: _jsxs("div", { className: "corner-smooth flex items-center gap-1.5 rounded-full border border-border/40 bg-background/80 px-3 py-1 text-xs shadow-elevation-3 backdrop-blur-xl", children: [_jsx("kbd", { className: "rounded border border-border/60 bg-white/10 px-1.5 py-0.5 font-mono text-[10px]", children: "F" }), _jsx("span", { className: "font-medium", children: i18n._({ id: 'viewer.walkthrough.shutters', message: 'Shutters' }) })] }) }) }));
|
|
1076
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type AnyNode } from '@pascal-app/core';
|
|
2
|
+
import type { Object3D } from 'three';
|
|
3
|
+
import type { GlbInteractiveItem } from './glb-interactive';
|
|
4
|
+
/**
|
|
5
|
+
* The baked viewer's callout on the one selected element. The controls drive
|
|
6
|
+
* the same baked clips and interactive state the walkthrough does.
|
|
7
|
+
*/
|
|
8
|
+
export declare function GlbSelectionCallout({ identity, items, sceneNodes, loopMechanisms, isOpen, toggleOpen, }: {
|
|
9
|
+
identity: ReadonlyMap<string, Object3D>;
|
|
10
|
+
items: GlbInteractiveItem[] | undefined;
|
|
11
|
+
sceneNodes: Readonly<Record<string, AnyNode>> | undefined;
|
|
12
|
+
loopMechanisms: ReadonlyMap<string, unknown>;
|
|
13
|
+
/** Baked doors and windows open through their `: open` clip, tracked by the scene. */
|
|
14
|
+
isOpen: (id: string) => boolean;
|
|
15
|
+
toggleOpen: (object: Object3D) => void | Promise<void>;
|
|
16
|
+
}): import("react").JSX.Element | null;
|
|
17
|
+
//# sourceMappingURL=glb-selection-callout.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"glb-selection-callout.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/glb-selection-callout.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,OAAO,EAAkC,MAAM,kBAAkB,CAAA;AAE/E,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAarC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AAa3D;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,QAAQ,EACR,KAAK,EACL,UAAU,EACV,cAAc,EACd,MAAM,EACN,UAAU,GACX,EAAE;IACD,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAA;IACvC,KAAK,EAAE,kBAAkB,EAAE,GAAG,SAAS,CAAA;IACvC,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,SAAS,CAAA;IACzD,cAAc,EAAE,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC5C,sFAAsF;IACtF,MAAM,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAA;IAC/B,UAAU,EAAE,CAAC,MAAM,EAAE,QAAQ,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACvD,sCA0FA"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useInteractive } from '@pascal-app/core';
|
|
4
|
+
import { useCallback } from 'react';
|
|
5
|
+
import useViewer from '../../store/use-viewer';
|
|
6
|
+
import { calloutSubtitle, calloutToggle, hasCallout, interactiveActions, OPEN_SOUNDS, openingSound, proceduralActions, SHUTTERS, } from '../../systems/interactive/callout-actions';
|
|
7
|
+
import { toggleGlbShutters } from './glb-scene-motion';
|
|
8
|
+
import { SelectionCallout } from './selection-callout';
|
|
9
|
+
/**
|
|
10
|
+
* The baked viewer's callout on the one selected element. The controls drive
|
|
11
|
+
* the same baked clips and interactive state the walkthrough does.
|
|
12
|
+
*/
|
|
13
|
+
export function GlbSelectionCallout({ identity, items, sceneNodes, loopMechanisms, isOpen, toggleOpen, }) {
|
|
14
|
+
const selectedIds = useViewer((s) => s.selection.selectedIds);
|
|
15
|
+
const walkthrough = useViewer((s) => s.walkthroughMode);
|
|
16
|
+
const id = selectedIds.length === 1 ? selectedIds[0] : null;
|
|
17
|
+
const object = id ? identity.get(id) : undefined;
|
|
18
|
+
const interactive = useInteractive();
|
|
19
|
+
const dismiss = useCallback(() => {
|
|
20
|
+
const { selection, setSelection } = useViewer.getState();
|
|
21
|
+
if (id && selection.selectedIds.includes(id))
|
|
22
|
+
setSelection({ selectedIds: [] });
|
|
23
|
+
}, [id]);
|
|
24
|
+
if (walkthrough || !id || !object)
|
|
25
|
+
return null;
|
|
26
|
+
const extras = object.userData;
|
|
27
|
+
if (!hasCallout(extras.kind))
|
|
28
|
+
return null;
|
|
29
|
+
const node = sceneNodes?.[id];
|
|
30
|
+
const title = extras.label ?? node?.name ?? 'Item';
|
|
31
|
+
const opening = (node ?? { type: extras.kind ?? 'item' });
|
|
32
|
+
const actions = [];
|
|
33
|
+
const item = items?.find((candidate) => candidate.pascalId === id);
|
|
34
|
+
if (item?.procedural) {
|
|
35
|
+
actions.push(...proceduralActions(id, title, item.procedural.recipe ?? { parts: item.procedural.parts }, interactive));
|
|
36
|
+
}
|
|
37
|
+
else if (item) {
|
|
38
|
+
actions.push(...interactiveActions(title, item.interactive, interactive.items[item.pascalId]?.controlValues, (index, value) => useInteractive.getState().setControlValue(item.pascalId, index, value), opening));
|
|
39
|
+
}
|
|
40
|
+
else if (extras.openable && extras.clips?.some((clip) => clip.endsWith(': open'))) {
|
|
41
|
+
actions.push(calloutToggle('open', extras.kind === 'window' ? 'window' : 'door', isOpen(id), title, openingSound(opening), () => toggleOpen(object)));
|
|
42
|
+
}
|
|
43
|
+
else if (loopMechanisms.has(id)) {
|
|
44
|
+
actions.push(calloutToggle('mechanism', 'power', Boolean(interactive.mechanisms[id]), title, OPEN_SOUNDS.switch, () => {
|
|
45
|
+
const state = useInteractive.getState();
|
|
46
|
+
state.setMechanism(id, !state.mechanisms[id]);
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
if (extras.shutters)
|
|
50
|
+
actions.push(calloutToggle('shutters', 'shutters', Boolean(interactive.mechanismParts[`${id}:shutters`]), title, OPEN_SOUNDS.shutters, () => {
|
|
51
|
+
toggleGlbShutters(object);
|
|
52
|
+
}, SHUTTERS));
|
|
53
|
+
return (_jsx(SelectionCallout, { actions: actions, object: object, onDismiss: dismiss, subtitle: calloutSubtitle(node ?? { type: extras.kind }, title), title: title }, id));
|
|
54
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"glb-walkthrough-controller.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/glb-walkthrough-controller.tsx"],"names":[],"mappings":"AAkCA,eAAO,MAAM,eAAe,KAAK,CAAA;AAkBjC,eAAO,MAAM,aAAa,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAqB,CAAA;AAChF,eAAO,MAAM,cAAc,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAqB,CAAA;AACjF,eAAO,MAAM,kBAAkB,MAAM,CAAA;AACrC,eAAO,MAAM,mBAAmB,OAAO,CAAA;AACvC,eAAO,MAAM,iBAAiB,MAAM,CAAA;AACpC,eAAO,MAAM,iBAAiB,IAAI,CAAA;AAClC,eAAO,MAAM,gBAAgB,MAAM,CAAA;AAInC,eAAO,MAAM,eAAe,OAAO,CAAA;AACnC,eAAO,MAAM,cAAc,KAAK,CAAA;
|
|
1
|
+
{"version":3,"file":"glb-walkthrough-controller.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/glb-walkthrough-controller.tsx"],"names":[],"mappings":"AAkCA,eAAO,MAAM,eAAe,KAAK,CAAA;AAkBjC,eAAO,MAAM,aAAa,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAqB,CAAA;AAChF,eAAO,MAAM,cAAc,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAqB,CAAA;AACjF,eAAO,MAAM,kBAAkB,MAAM,CAAA;AACrC,eAAO,MAAM,mBAAmB,OAAO,CAAA;AACvC,eAAO,MAAM,iBAAiB,MAAM,CAAA;AACpC,eAAO,MAAM,iBAAiB,IAAI,CAAA;AAClC,eAAO,MAAM,gBAAgB,MAAM,CAAA;AAInC,eAAO,MAAM,eAAe,OAAO,CAAA;AACnC,eAAO,MAAM,cAAc,KAAK,CAAA;AAmMhC;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,EAAE,GAAG,EAAE,EAAE;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,sCA0PhE"}
|
|
@@ -7,7 +7,7 @@ import { Box3, BoxGeometry, BufferGeometry, Euler, Float32BufferAttribute, Matri
|
|
|
7
7
|
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
|
|
8
8
|
import { acceleratedRaycast, computeBoundsTree, disposeBoundsTree } from 'three-mesh-bvh';
|
|
9
9
|
import { useGLTFKTX2 } from '../../hooks/use-gltf-ktx2';
|
|
10
|
-
import { SCENE_LAYER } from '../../lib/layers';
|
|
10
|
+
import { BATCHED_LAYER, SCENE_LAYER } from '../../lib/layers';
|
|
11
11
|
import useViewer from '../../store/use-viewer';
|
|
12
12
|
import BVHEcctrl, {} from './bvh-ecctrl';
|
|
13
13
|
import { applyWalkthroughCameraClipping } from './viewer-camera';
|
|
@@ -141,8 +141,11 @@ function buildGlbColliderWorld(scene) {
|
|
|
141
141
|
const mesh = object;
|
|
142
142
|
if (!mesh.isMesh)
|
|
143
143
|
return;
|
|
144
|
-
// Zone fills live on a separate layer (and never collide).
|
|
145
|
-
|
|
144
|
+
// Zone fills live on a separate layer (and never collide). Merged originals sit on the
|
|
145
|
+
// batched layer and still collide; their batches don't (each original keeps its kind rules).
|
|
146
|
+
if (mesh.userData.glbBatch)
|
|
147
|
+
return;
|
|
148
|
+
if (!mesh.layers.isEnabled(SCENE_LAYER) && !mesh.layers.isEnabled(BATCHED_LAYER))
|
|
146
149
|
return;
|
|
147
150
|
if (!isEffectivelyVisible(mesh))
|
|
148
151
|
return;
|
|
@@ -42,6 +42,11 @@ interface ViewerProps {
|
|
|
42
42
|
};
|
|
43
43
|
/** Saved site appearance when rendering a baked asset without its graph. Null ignores it. */
|
|
44
44
|
presentation?: SitePresentation | null;
|
|
45
|
+
/**
|
|
46
|
+
* The colored appearance's built-in daylight (sun, sky, ground, time and weather presets).
|
|
47
|
+
* Default on; canonical renders (bake) turn it off and keep the neutral studio look.
|
|
48
|
+
*/
|
|
49
|
+
daylight?: boolean;
|
|
45
50
|
/**
|
|
46
51
|
* Visibility filter on the live canvas. When non-null, every registered
|
|
47
52
|
* node group whose id is not in `isolate` (or in the isolated set's
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/index.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,SAAS,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/index.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,SAAS,EAId,KAAK,gBAAgB,EAKtB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAkB,KAAK,YAAY,EAAsB,MAAM,oBAAoB,CAAA;AAC1F,OAAO,EACL,KAAK,aAAa,EAOnB,MAAM,OAAO,CAAA;AACd,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AAMrC,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AACrE,OAAO,EAAuB,KAAK,uBAAuB,EAAE,MAAM,0BAA0B,CAAA;AAK5F,OAAkB,EAAE,KAAK,aAAa,EAAsB,MAAM,wBAAwB,CAAA;AAe1F,OAAuB,EAAwB,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAa1F,OAAO,QAAQ,oBAAoB,CAAC;IAOlC,UAAU,aAAa;QACrB,qBAAqB,EAAE,YAAY,CAAC,OAAO,KAAK,CAAC,qBAAqB,CAAC,CAAA;KACxE;CACF;AAsPD;;;;;GAKG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,CAS/C;AAsED,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,aAAa,CAAC;QAAE,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;KAAE,CAAC,CAAA;IACrD,KAAK,EAAE,aAAa,CAAC;QAAE,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;KAAE,CAAC,CAAA;IACnD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;CACnC;AASD,UAAU,WAAW;IACnB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC1B,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,gBAAgB,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAA;IACvC,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,aAAa,CAAC,EAAE,aAAa,CAAA;IAC7B,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,aAAa,CAAC,EAAE;QACd,OAAO,CAAC,EAAE,aAAa,CAAA;QACvB,QAAQ,CAAC,EAAE,OAAO,CAAA;QAClB,WAAW,CAAC,EAAE,WAAW,CAAA;KAC1B,CAAA;IACD,6FAA6F;IAC7F,YAAY,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAA;IACtC;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,SAAS,EAAE,GAAG,IAAI,CAAA;IAC5B;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;IACtC,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;IAC7C;;;;;OAKG;IACH,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;IACxC;;;;OAIG;IACH,YAAY,CAAC,EAAE,uBAAuB,CAAA;IACtC;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,6EAA6E;IAC7E,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,sEAAsE;IACtE,SAAS,CAAC,EAAE,sBAAsB,CAAA;CACnC;AAED,yDAAyD;AACzD,MAAM,MAAM,YAAY,GAAG;IACzB;;;;;OAKG;IACH,WAAW,CAAC,GAAG,EAAE,SAAS,EAAE,GAAG,IAAI,GAAG,IAAI,CAAA;CAC3C,CAAA;AAED,QAAA,MAAM,MAAM,sGA+SV,CAAA;eA+Ea,MAAM"}
|