@pascal-app/viewer 1.0.5 → 1.0.7
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/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-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 +3 -1
- package/dist/components/viewer/glb-scene.d.ts.map +1 -1
- package/dist/components/viewer/glb-scene.js +289 -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 +30 -6
- 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/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/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 +236 -0
- package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
- package/dist/components/viewer/viewer-camera.js +12 -1
- package/dist/index.d.ts +21 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -7
- 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 +53 -0
- package/dist/lib/daylight/room-scene.d.ts +54 -0
- package/dist/lib/daylight/room-scene.d.ts.map +1 -0
- package/dist/lib/daylight/room-scene.js +356 -0
- package/dist/lib/daylight/rooms.d.ts +262 -0
- package/dist/lib/daylight/rooms.d.ts.map +1 -0
- package/dist/lib/daylight/rooms.js +1223 -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/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/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/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 +10 -8
- 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/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 +72 -15
- package/dist/store/use-viewer.d.ts.map +1 -1
- package/dist/store/use-viewer.js +103 -18
- 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/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 +477 -105
- 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.map +1 -1
- package/dist/systems/wall/wall-system.js +34 -19
- 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,7 +123,7 @@ 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);
|
|
@@ -180,6 +193,7 @@ export function GlbScene({ url, interactiveItems, referenceNodes, replaceNodes,
|
|
|
180
193
|
clips: entry.clips,
|
|
181
194
|
spinParts,
|
|
182
195
|
sequence: 0,
|
|
196
|
+
settling: null,
|
|
183
197
|
},
|
|
184
198
|
];
|
|
185
199
|
}));
|
|
@@ -200,8 +214,14 @@ export function GlbScene({ url, interactiveItems, referenceNodes, replaceNodes,
|
|
|
200
214
|
const camera = useThree((state) => state.camera);
|
|
201
215
|
const controls = useThree((state) => state.controls);
|
|
202
216
|
const walkthroughMode = useViewer((s) => s.walkthroughMode);
|
|
217
|
+
// Doors, the reticle prompt and the room HUD belong to the walk; the drone just flies.
|
|
218
|
+
const walkingOnFoot = useViewer((s) => s.walkthroughMode && s.walkthroughMovement === 'walk');
|
|
203
219
|
const textures = useViewer((s) => s.textures);
|
|
204
220
|
const sceneTheme = useViewer((s) => s.sceneTheme);
|
|
221
|
+
// Static merge of the artifact (see lib/glb-merge.ts); `?merge=0` keeps every mesh separate.
|
|
222
|
+
const mergeRef = useRef(null);
|
|
223
|
+
const [mergeVersion, setMergeVersion] = useState(0);
|
|
224
|
+
const [mergeRebuild, setMergeRebuild] = useState(0);
|
|
205
225
|
// Monochrome: strip the baked textures and recolor every building mesh with a
|
|
206
226
|
// flat themed-clay material by surface role — mirrors the parametric viewer's
|
|
207
227
|
// textures-off path. The original baked material is stashed on the mesh
|
|
@@ -223,7 +243,40 @@ export function GlbScene({ url, interactiveItems, referenceNodes, replaceNodes,
|
|
|
223
243
|
: createSurfaceRoleMaterial(role, 'clay', THREE.DoubleSide, sceneTheme);
|
|
224
244
|
});
|
|
225
245
|
});
|
|
226
|
-
|
|
246
|
+
for (const batch of mergeRef.current?.batches ?? [])
|
|
247
|
+
applyBatchLook(batch, textures, sceneTheme);
|
|
248
|
+
}, [gltf.scene, textures, sceneTheme, mergeVersion]);
|
|
249
|
+
// Baked meshes take part in the sun's shadows (the bake carries no shadow flags). Transparent
|
|
250
|
+
// surfaces (glazing) don't cast, so sunlight still reaches the rooms behind them. Foliage ships
|
|
251
|
+
// opaque with cut-out textures (square dark leaves): alpha-test it.
|
|
252
|
+
useEffect(() => {
|
|
253
|
+
gltf.scene.traverse((object) => {
|
|
254
|
+
if (!object.userData.kind?.startsWith('trees:'))
|
|
255
|
+
return;
|
|
256
|
+
object.traverse((child) => {
|
|
257
|
+
const mesh = child;
|
|
258
|
+
if (!mesh.isMesh)
|
|
259
|
+
return;
|
|
260
|
+
for (const material of Array.isArray(mesh.material) ? mesh.material : [mesh.material]) {
|
|
261
|
+
const standard = material;
|
|
262
|
+
if (!standard.map || standard.alphaTest >= 0.5)
|
|
263
|
+
continue;
|
|
264
|
+
standard.alphaTest = 0.5;
|
|
265
|
+
standard.side = THREE.DoubleSide;
|
|
266
|
+
standard.transparent = false;
|
|
267
|
+
standard.needsUpdate = true;
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
gltf.scene.traverse((child) => {
|
|
272
|
+
const mesh = child;
|
|
273
|
+
if (!mesh.isMesh)
|
|
274
|
+
return;
|
|
275
|
+
const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
|
|
276
|
+
mesh.receiveShadow = true;
|
|
277
|
+
mesh.castShadow = !materials.some((material) => material?.transparent);
|
|
278
|
+
});
|
|
279
|
+
}, [gltf.scene]);
|
|
227
280
|
// The baked scene isn't wrapped in <SceneBvh> (the community viewer runs with
|
|
228
281
|
// useBvh={false}), so hover/pick raycasts against the baked building were
|
|
229
282
|
// brute-force triangle tests — dozens of ms per pointer move on a dense scene,
|
|
@@ -339,8 +392,6 @@ export function GlbScene({ url, interactiveItems, referenceNodes, replaceNodes,
|
|
|
339
392
|
levelsWithZones: new Set(zoneList.map((zone) => zone.levelId)),
|
|
340
393
|
};
|
|
341
394
|
}, [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
395
|
// Loop clips no other controller plays (a plugin kind's mechanism) run on
|
|
345
396
|
// click and E. As in the editor they start stopped and never persist.
|
|
346
397
|
const loopMechanisms = useMemo(() => bakedLoopMechanisms(identity, new Set([
|
|
@@ -395,6 +446,80 @@ export function GlbScene({ url, interactiveItems, referenceNodes, replaceNodes,
|
|
|
395
446
|
onObjectsChange?.(identity);
|
|
396
447
|
return () => onObjectsChange?.(new Map());
|
|
397
448
|
}, [identity, onObjectsChange]);
|
|
449
|
+
// Static merge: thousands of baked meshes become a few hundred draws per pass (the enhanced
|
|
450
|
+
// viewer's batching). Rebuilt when the interactive items change, or when a merged mesh is
|
|
451
|
+
// shown or hidden on its own (a plugin taking over a baked kind, an isolation), so a batch
|
|
452
|
+
// never draws something that should be gone.
|
|
453
|
+
const mergeEnabled = useMemo(() => typeof window === 'undefined' ||
|
|
454
|
+
new URLSearchParams(window.location.search).get('merge') !== '0', []);
|
|
455
|
+
useEffect(() => {
|
|
456
|
+
if (!mergeEnabled)
|
|
457
|
+
return;
|
|
458
|
+
const animated = new Set();
|
|
459
|
+
for (const clip of gltf.animations) {
|
|
460
|
+
for (const track of clip.tracks) {
|
|
461
|
+
const { nodeName } = THREE.PropertyBinding.parseTrackName(track.name);
|
|
462
|
+
const node = THREE.PropertyBinding.findNode(gltf.scene, nodeName);
|
|
463
|
+
if (node)
|
|
464
|
+
animated.add(node);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
const merge = mergeBakedStatic(gltf.scene, (kind) => ROLE_BY_KIND[kind ?? ''], {
|
|
468
|
+
keepIds: new Set((interactiveItems ?? []).map((item) => item.pascalId)),
|
|
469
|
+
animated,
|
|
470
|
+
});
|
|
471
|
+
const { textures: texturesNow, sceneTheme: themeNow } = useViewer.getState();
|
|
472
|
+
for (const batch of merge.batches)
|
|
473
|
+
applyBatchLook(batch, texturesNow, themeNow);
|
|
474
|
+
mergeRef.current = merge;
|
|
475
|
+
mergeSignature.current = merge.visibilitySignature();
|
|
476
|
+
setMergeVersion((version) => version + 1);
|
|
477
|
+
return () => {
|
|
478
|
+
for (const batch of merge.batches)
|
|
479
|
+
disposeBatchLook(batch);
|
|
480
|
+
merge.dispose();
|
|
481
|
+
if (mergeRef.current === merge)
|
|
482
|
+
mergeRef.current = null;
|
|
483
|
+
};
|
|
484
|
+
}, [gltf.scene, gltf.animations, interactiveItems, mergeEnabled, mergeRebuild]);
|
|
485
|
+
// Occlusion culling (see lib/occlusion-culler.ts): what the walls, slabs and roofs hide from
|
|
486
|
+
// the eye isn't drawn. Rebuilt with the merge, whose batches are what it mostly culls.
|
|
487
|
+
// `?occlusion=0` draws everything.
|
|
488
|
+
const rootScene = useThree((state) => state.scene);
|
|
489
|
+
const renderer = useThree((state) => state.gl);
|
|
490
|
+
const getThree = useThree((state) => state.get);
|
|
491
|
+
const occlusionEnabled = useMemo(() => typeof window === 'undefined' ||
|
|
492
|
+
new URLSearchParams(window.location.search).get('occlusion') !== '0', []);
|
|
493
|
+
useEffect(() => {
|
|
494
|
+
// three's drawing pipeline (WebGPU renderer) answers which objects a render skips.
|
|
495
|
+
const pipeline = renderer.drawingPipeline;
|
|
496
|
+
if (!occlusionEnabled || !pipeline)
|
|
497
|
+
return;
|
|
498
|
+
const culler = createOcclusionCuller(gltf.scene);
|
|
499
|
+
const detach = attachOcclusionCuller({
|
|
500
|
+
scene: rootScene,
|
|
501
|
+
pipeline,
|
|
502
|
+
culler,
|
|
503
|
+
getCamera: () => getThree().camera,
|
|
504
|
+
});
|
|
505
|
+
return () => {
|
|
506
|
+
detach();
|
|
507
|
+
culler.dispose();
|
|
508
|
+
};
|
|
509
|
+
}, [gltf.scene, rootScene, renderer, getThree, occlusionEnabled, mergeVersion]);
|
|
510
|
+
const mergeSignature = useRef(0);
|
|
511
|
+
const mergeCheck = useRef(0);
|
|
512
|
+
useFrame((_, delta) => {
|
|
513
|
+
const merge = mergeRef.current;
|
|
514
|
+
if (!merge)
|
|
515
|
+
return;
|
|
516
|
+
mergeCheck.current += delta;
|
|
517
|
+
if (mergeCheck.current < 0.25)
|
|
518
|
+
return;
|
|
519
|
+
mergeCheck.current = 0;
|
|
520
|
+
if (merge.visibilitySignature() !== mergeSignature.current)
|
|
521
|
+
setMergeRebuild((n) => n + 1);
|
|
522
|
+
});
|
|
398
523
|
// Apply the viewer's level display to the baked floors each frame, by the
|
|
399
524
|
// parametric LevelSystem's rule (solo, and the levels above the current one
|
|
400
525
|
// hidden when the host asks). Walkthrough always shows the full stacked
|
|
@@ -451,8 +576,16 @@ export function GlbScene({ url, interactiveItems, referenceNodes, replaceNodes,
|
|
|
451
576
|
if (command && command.sequence > entry.sequence) {
|
|
452
577
|
entry.controller.command(command);
|
|
453
578
|
entry.sequence = command.sequence;
|
|
579
|
+
entry.settling = command.sequence;
|
|
454
580
|
}
|
|
455
581
|
const frame = entry.controller.tick(delta);
|
|
582
|
+
if (entry.settling !== null && frame.settled) {
|
|
583
|
+
emitter.emit('procedural:motion-settled', {
|
|
584
|
+
nodeId: nodeId,
|
|
585
|
+
sequence: entry.settling,
|
|
586
|
+
});
|
|
587
|
+
entry.settling = null;
|
|
588
|
+
}
|
|
456
589
|
for (const [clipName, binding] of entry.clips) {
|
|
457
590
|
const action = proceduralPlayback.actions.get(clipName);
|
|
458
591
|
if (!action)
|
|
@@ -526,15 +659,6 @@ export function GlbScene({ url, interactiveItems, referenceNodes, replaceNodes,
|
|
|
526
659
|
// Items the walkthrough operates through their toggles; others fall through to loop clips.
|
|
527
660
|
const toggleableItem = useCallback((pascalId) => interactiveItems?.find((item) => item.pascalId === pascalId &&
|
|
528
661
|
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
662
|
const toggleLoopMechanism = useCallback((identityNode) => {
|
|
539
663
|
const id = identityNode.userData.pascalId;
|
|
540
664
|
if (!(id && loopMechanisms.has(id)))
|
|
@@ -543,7 +667,11 @@ export function GlbScene({ url, interactiveItems, referenceNodes, replaceNodes,
|
|
|
543
667
|
state.setMechanism(id, !state.mechanisms[id]);
|
|
544
668
|
return true;
|
|
545
669
|
}, [loopMechanisms]);
|
|
670
|
+
useEffect(() => bindGlbShutters(identity, actions), [identity, actions]);
|
|
546
671
|
const openIds = useRef(new Set());
|
|
672
|
+
// Bumped on every baked door or window toggle so the selection callout re-reads `openIds`.
|
|
673
|
+
const [, setOpenVersion] = useState(0);
|
|
674
|
+
const isOpen = useCallback((id) => openIds.current.has(id), []);
|
|
547
675
|
const toggleProcedural = useCallback((hit, identityNode) => {
|
|
548
676
|
const extras = identityNode.userData;
|
|
549
677
|
if (extras.kind !== 'procedural-item')
|
|
@@ -588,6 +716,10 @@ export function GlbScene({ url, interactiveItems, referenceNodes, replaceNodes,
|
|
|
588
716
|
openIds.current.add(id);
|
|
589
717
|
else
|
|
590
718
|
openIds.current.delete(id);
|
|
719
|
+
setOpenVersion((version) => version + 1);
|
|
720
|
+
// Played backwards from where it stands, the clip lands after its current time.
|
|
721
|
+
if (!willOpen)
|
|
722
|
+
return new Promise((resolve) => setTimeout(resolve, action.time * 1000));
|
|
591
723
|
}, [actions]);
|
|
592
724
|
// The room whose polygon contains a world point. Resolving by the raycast hit
|
|
593
725
|
// point (rather than a node origin) means any surface inside a room footprint —
|
|
@@ -642,13 +774,15 @@ export function GlbScene({ url, interactiveItems, referenceNodes, replaceNodes,
|
|
|
642
774
|
}
|
|
643
775
|
});
|
|
644
776
|
// ── Walkthrough: first-person HUD + interaction ────────────────────────────
|
|
777
|
+
const [shuttersInView, setShuttersInView] = useState(false);
|
|
645
778
|
const walkDoorRef = useRef(null);
|
|
779
|
+
const walkPromptRef = useRef(null);
|
|
646
780
|
const lastWalkKey = useRef(null);
|
|
647
781
|
// Each frame in walkthrough, report the floor + room the camera stands in and
|
|
648
782
|
// the openable directly ahead (a forward ray from screen centre) so the host
|
|
649
783
|
// can draw the reticle prompt. Fires the callback only when the state changes.
|
|
650
784
|
useFrame(() => {
|
|
651
|
-
if (!
|
|
785
|
+
if (!walkingOnFoot)
|
|
652
786
|
return;
|
|
653
787
|
camera.getWorldPosition(_walkPos);
|
|
654
788
|
let floor = levels[0] ?? null;
|
|
@@ -715,7 +849,14 @@ export function GlbScene({ url, interactiveItems, referenceNodes, replaceNodes,
|
|
|
715
849
|
door = { label: extras.label ?? 'Item', isOpen, verb: isOpen ? 'turn off' : 'turn on' };
|
|
716
850
|
}
|
|
717
851
|
}
|
|
852
|
+
if (hit && !doorNode) {
|
|
853
|
+
const node = findIdentityAncestor(hit.object);
|
|
854
|
+
if (node?.userData?.shutters && node)
|
|
855
|
+
doorNode = { hit: hit.object, node };
|
|
856
|
+
}
|
|
857
|
+
setShuttersInView(Boolean(doorNode?.node.userData?.shutters));
|
|
718
858
|
walkDoorRef.current = doorNode;
|
|
859
|
+
walkPromptRef.current = door;
|
|
719
860
|
const key = `${floor?.id ?? ''}|${zone?.id ?? ''}|${door ? `${doorId}:${door.isOpen}` : ''}`;
|
|
720
861
|
if (key !== lastWalkKey.current) {
|
|
721
862
|
lastWalkKey.current = key;
|
|
@@ -724,41 +865,82 @@ export function GlbScene({ url, interactiveItems, referenceNodes, replaceNodes,
|
|
|
724
865
|
});
|
|
725
866
|
// E or click activates the openable in view. The click also re-locks the
|
|
726
867
|
// pointer through the walkthrough controller; no selection happens.
|
|
727
|
-
const activateWalkDoor = useCallback(() => {
|
|
868
|
+
const activateWalkDoor = useCallback((shutterHit = false) => {
|
|
728
869
|
const target = walkDoorRef.current;
|
|
729
870
|
if (!target)
|
|
730
871
|
return;
|
|
731
|
-
|
|
872
|
+
if (shutterHit && target.node.userData.shutters && isGlbShutterHit(target.hit, target.node)) {
|
|
873
|
+
playShutterSound(target.node);
|
|
874
|
+
toggleGlbShutters(target.node);
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
const prompt = walkPromptRef.current;
|
|
878
|
+
const extras = target.node.userData;
|
|
879
|
+
const switches = prompt?.verb !== undefined && !['open', 'close'].includes(prompt.verb);
|
|
880
|
+
const sound = switches
|
|
881
|
+
? OPEN_SOUNDS.switch
|
|
882
|
+
: openingSound((extras.pascalId && sceneNodes?.[extras.pascalId]) || { type: extras.kind ?? 'door' });
|
|
883
|
+
const play = (settled, as = sound) => {
|
|
884
|
+
if (!prompt)
|
|
885
|
+
return;
|
|
886
|
+
// A garage door's motor runs the whole way, so it starts with the press.
|
|
887
|
+
if (!(prompt.isOpen && settled) || as === OPEN_SOUNDS.garage) {
|
|
888
|
+
emitter.emit('interaction:sound', prompt.isOpen ? as.off : as.on);
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
if (as.swing)
|
|
892
|
+
emitter.emit('interaction:sound', as.swing);
|
|
893
|
+
void settled.then(() => emitter.emit('interaction:sound', as.off));
|
|
894
|
+
};
|
|
895
|
+
const item = toggleableItem(extras.pascalId);
|
|
732
896
|
if (item) {
|
|
897
|
+
play();
|
|
733
898
|
operateItem(item.pascalId, item.interactive);
|
|
734
899
|
return;
|
|
735
900
|
}
|
|
736
|
-
if (
|
|
737
|
-
|
|
738
|
-
|
|
901
|
+
if (toggleProcedural(target.hit, target.node)) {
|
|
902
|
+
// A recipe's door or lid swings, its drawer slides; both land on the motion's end.
|
|
903
|
+
const sliding = findProceduralMotionAncestor(target.hit)?.kind === 'slide';
|
|
904
|
+
if (switches)
|
|
905
|
+
play();
|
|
906
|
+
else
|
|
907
|
+
play(proceduralSettled(extras.pascalId), sliding ? OPEN_SOUNDS.drawer : OPEN_SOUNDS.panel);
|
|
908
|
+
}
|
|
909
|
+
else if (toggleLoopMechanism(target.node))
|
|
910
|
+
play();
|
|
911
|
+
else
|
|
912
|
+
play(toggleOpenable(target.node));
|
|
913
|
+
}, [sceneNodes, toggleableItem, toggleLoopMechanism, toggleOpenable, toggleProcedural]);
|
|
739
914
|
useEffect(() => {
|
|
740
|
-
if (!
|
|
915
|
+
if (!walkingOnFoot)
|
|
741
916
|
return;
|
|
742
917
|
const onKey = (event) => {
|
|
743
918
|
if (event.key.toLowerCase() === 'e')
|
|
744
919
|
activateWalkDoor();
|
|
920
|
+
const shutters = walkDoorRef.current?.node;
|
|
921
|
+
if (event.key.toLowerCase() === 'f' && !event.repeat && shutters?.userData.shutters) {
|
|
922
|
+
playShutterSound(shutters);
|
|
923
|
+
toggleGlbShutters(shutters);
|
|
924
|
+
}
|
|
745
925
|
};
|
|
746
926
|
const canvas = document.querySelector('canvas');
|
|
747
927
|
window.addEventListener('keydown', onKey);
|
|
748
|
-
|
|
928
|
+
const onClick = () => activateWalkDoor(true);
|
|
929
|
+
canvas?.addEventListener('click', onClick);
|
|
749
930
|
return () => {
|
|
750
931
|
window.removeEventListener('keydown', onKey);
|
|
751
|
-
canvas?.removeEventListener('click',
|
|
932
|
+
canvas?.removeEventListener('click', onClick);
|
|
752
933
|
};
|
|
753
|
-
}, [
|
|
754
|
-
// Clear the HUD (and stale targeting) whenever
|
|
934
|
+
}, [walkingOnFoot, activateWalkDoor]);
|
|
935
|
+
// Clear the HUD (and stale targeting) whenever the walk ends, or turns into a drone flight.
|
|
755
936
|
useEffect(() => {
|
|
756
|
-
if (
|
|
937
|
+
if (walkingOnFoot)
|
|
757
938
|
return;
|
|
939
|
+
setShuttersInView(false);
|
|
758
940
|
walkDoorRef.current = null;
|
|
759
941
|
lastWalkKey.current = null;
|
|
760
942
|
onWalkthroughChange?.(null);
|
|
761
|
-
}, [
|
|
943
|
+
}, [walkingOnFoot, onWalkthroughChange]);
|
|
762
944
|
const lastHover = useRef(null);
|
|
763
945
|
const handlePointerMove = useCallback((event) => {
|
|
764
946
|
event.stopPropagation();
|
|
@@ -779,8 +961,8 @@ export function GlbScene({ url, interactiveItems, referenceNodes, replaceNodes,
|
|
|
779
961
|
lastHover.current = null;
|
|
780
962
|
onPick?.({ type: 'hover', pick: null, modifiers: NO_MODIFIERS });
|
|
781
963
|
}, [onPick]);
|
|
782
|
-
// The host's rules decide the selection;
|
|
783
|
-
//
|
|
964
|
+
// The host's rules decide the selection; what the element can do is offered
|
|
965
|
+
// on its selection callout, never run by the click itself.
|
|
784
966
|
const handleClick = useCallback((event) => {
|
|
785
967
|
event.stopPropagation();
|
|
786
968
|
// Walkthrough handles its own door activation (E / canvas click) and never
|
|
@@ -789,24 +971,7 @@ export function GlbScene({ url, interactiveItems, referenceNodes, replaceNodes,
|
|
|
789
971
|
return;
|
|
790
972
|
const pick = resolveGlbPick(event.intersections, identity);
|
|
791
973
|
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
|
-
]);
|
|
974
|
+
}, [identity, onPick, walkthroughMode]);
|
|
810
975
|
// A click on empty space clears the room and the element (the floor stays).
|
|
811
976
|
const handlePointerMissed = useCallback((event) => {
|
|
812
977
|
if (useViewer.getState().walkthroughMode || !onPick)
|
|
@@ -822,7 +987,12 @@ export function GlbScene({ url, interactiveItems, referenceNodes, replaceNodes,
|
|
|
822
987
|
for (const mesh of occluderOwnMeshes)
|
|
823
988
|
clearShadowOnly(mesh);
|
|
824
989
|
}, [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
|
|
990
|
+
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 })) : null, _jsx(GlbDaylightGround, { root: gltf.scene })] }));
|
|
991
|
+
}
|
|
992
|
+
function playShutterSound(node) {
|
|
993
|
+
const id = node.userData.pascalId;
|
|
994
|
+
const open = id && useInteractive.getState().mechanismParts[`${id}:shutters`];
|
|
995
|
+
emitter.emit('interaction:sound', open ? OPEN_SOUNDS.shutters.off : OPEN_SOUNDS.shutters.on);
|
|
826
996
|
}
|
|
827
997
|
const NO_MODIFIERS = {
|
|
828
998
|
alt: false,
|
|
@@ -833,3 +1003,73 @@ const NO_MODIFIERS = {
|
|
|
833
1003
|
function modifiersOf(event) {
|
|
834
1004
|
return { alt: event.altKey, ctrl: event.ctrlKey, meta: event.metaKey, shift: event.shiftKey };
|
|
835
1005
|
}
|
|
1006
|
+
const NO_RAYCAST = () => { };
|
|
1007
|
+
/** A merged batch shows its baked material, or in monochrome the themed clay of its role. */
|
|
1008
|
+
function applyBatchLook(batch, textures, sceneTheme) {
|
|
1009
|
+
const { role, bakedMaterial } = batch.userData;
|
|
1010
|
+
disposeBatchLook(batch);
|
|
1011
|
+
batch.material =
|
|
1012
|
+
textures || !role
|
|
1013
|
+
? bakedMaterial
|
|
1014
|
+
: createSurfaceRoleMaterial(role, 'clay', THREE.DoubleSide, sceneTheme);
|
|
1015
|
+
}
|
|
1016
|
+
function disposeBatchLook(batch) {
|
|
1017
|
+
const material = batch.material;
|
|
1018
|
+
if (material !== batch.userData.bakedMaterial)
|
|
1019
|
+
material.dispose();
|
|
1020
|
+
}
|
|
1021
|
+
/**
|
|
1022
|
+
* The colored appearance's ground under a baked artifact (the bake strips the live horizon
|
|
1023
|
+
* disc): the squared daylight ground just under the baked site fill, so the parcel keeps its own
|
|
1024
|
+
* surface; the atmosphere's fog takes it to the horizon.
|
|
1025
|
+
*/
|
|
1026
|
+
function GlbDaylightGround({ root }) {
|
|
1027
|
+
const atmosphere = useSceneAtmosphere();
|
|
1028
|
+
const daylight = isDaylightAtmosphere(atmosphere);
|
|
1029
|
+
// The baked site fill (the site's own opaque surface) becomes the daylight parcel: a neutral,
|
|
1030
|
+
// slightly lighter surface so the lot reads as its own place on the squared ground.
|
|
1031
|
+
useEffect(() => {
|
|
1032
|
+
if (!daylight)
|
|
1033
|
+
return;
|
|
1034
|
+
const parcel = createDaylightGroundMaterial(false, { color: DAYLIGHT_PARCEL });
|
|
1035
|
+
const swapped = [];
|
|
1036
|
+
root.traverse((object) => {
|
|
1037
|
+
if (object.userData.kind !== 'site')
|
|
1038
|
+
return;
|
|
1039
|
+
for (const child of object.children) {
|
|
1040
|
+
const mesh = child;
|
|
1041
|
+
const material = mesh.material;
|
|
1042
|
+
if (!mesh.isMesh || mesh.userData.pascalId || !material)
|
|
1043
|
+
continue;
|
|
1044
|
+
if (Array.isArray(material) || material.transparent)
|
|
1045
|
+
continue;
|
|
1046
|
+
swapped.push([mesh, mesh.material]);
|
|
1047
|
+
mesh.material = parcel;
|
|
1048
|
+
}
|
|
1049
|
+
});
|
|
1050
|
+
return () => {
|
|
1051
|
+
for (const [mesh, material] of swapped)
|
|
1052
|
+
mesh.material = material;
|
|
1053
|
+
parcel.dispose();
|
|
1054
|
+
};
|
|
1055
|
+
}, [daylight, root]);
|
|
1056
|
+
const solid = useViewer((state) => state.shading === 'solid');
|
|
1057
|
+
const material = useMemo(() => {
|
|
1058
|
+
if (!daylight)
|
|
1059
|
+
return null;
|
|
1060
|
+
const ground = createDaylightGroundMaterial(solid, { horizon: atmosphere?.fogRadiance });
|
|
1061
|
+
ground.polygonOffset = true;
|
|
1062
|
+
ground.polygonOffsetFactor = 2;
|
|
1063
|
+
ground.polygonOffsetUnits = 4;
|
|
1064
|
+
return ground;
|
|
1065
|
+
}, [atmosphere, daylight, solid]);
|
|
1066
|
+
useEffect(() => () => material?.dispose(), [material]);
|
|
1067
|
+
if (!material)
|
|
1068
|
+
return null;
|
|
1069
|
+
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] }) }));
|
|
1070
|
+
}
|
|
1071
|
+
/** Match the walkthrough HUD's secondary key prompt, without requiring host wiring. */
|
|
1072
|
+
function GlbShuttersPrompt() {
|
|
1073
|
+
const i18n = usePascalI18n();
|
|
1074
|
+
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' }) })] }) }) }));
|
|
1075
|
+
}
|
|
@@ -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,EAGd,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;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/viewer/index.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,SAAS,EAGd,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;AAI5F,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,sGA6SV,CAAA;eA+Ea,MAAM"}
|