@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.
Files changed (199) hide show
  1. package/dist/components/viewer/daylight-atmosphere.d.ts +9 -0
  2. package/dist/components/viewer/daylight-atmosphere.d.ts.map +1 -0
  3. package/dist/components/viewer/daylight-atmosphere.js +315 -0
  4. package/dist/components/viewer/drone-controller.d.ts +15 -0
  5. package/dist/components/viewer/drone-controller.d.ts.map +1 -0
  6. package/dist/components/viewer/drone-controller.js +194 -0
  7. package/dist/components/viewer/glb-interactive.d.ts +4 -18
  8. package/dist/components/viewer/glb-interactive.d.ts.map +1 -1
  9. package/dist/components/viewer/glb-interactive.js +32 -268
  10. package/dist/components/viewer/glb-scene-motion.d.ts +8 -0
  11. package/dist/components/viewer/glb-scene-motion.d.ts.map +1 -0
  12. package/dist/components/viewer/glb-scene-motion.js +57 -0
  13. package/dist/components/viewer/glb-scene.d.ts +3 -1
  14. package/dist/components/viewer/glb-scene.d.ts.map +1 -1
  15. package/dist/components/viewer/glb-scene.js +289 -49
  16. package/dist/components/viewer/glb-selection-callout.d.ts +17 -0
  17. package/dist/components/viewer/glb-selection-callout.d.ts.map +1 -0
  18. package/dist/components/viewer/glb-selection-callout.js +54 -0
  19. package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -1
  20. package/dist/components/viewer/glb-walkthrough-controller.js +6 -3
  21. package/dist/components/viewer/index.d.ts +5 -0
  22. package/dist/components/viewer/index.d.ts.map +1 -1
  23. package/dist/components/viewer/index.js +30 -6
  24. package/dist/components/viewer/lights.d.ts.map +1 -1
  25. package/dist/components/viewer/lights.js +192 -5
  26. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  27. package/dist/components/viewer/post-processing.js +150 -94
  28. package/dist/components/viewer/scene-environment.js +4 -5
  29. package/dist/components/viewer/selection-callout.d.ts +51 -0
  30. package/dist/components/viewer/selection-callout.d.ts.map +1 -0
  31. package/dist/components/viewer/selection-callout.js +197 -0
  32. package/dist/components/viewer/use-daylight-rooms.d.ts +13 -0
  33. package/dist/components/viewer/use-daylight-rooms.d.ts.map +1 -0
  34. package/dist/components/viewer/use-daylight-rooms.js +236 -0
  35. package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
  36. package/dist/components/viewer/viewer-camera.js +12 -1
  37. package/dist/index.d.ts +21 -7
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +23 -7
  40. package/dist/lib/daylight/ground.d.ts +21 -0
  41. package/dist/lib/daylight/ground.d.ts.map +1 -0
  42. package/dist/lib/daylight/ground.js +53 -0
  43. package/dist/lib/daylight/location.d.ts +32 -0
  44. package/dist/lib/daylight/location.d.ts.map +1 -0
  45. package/dist/lib/daylight/location.js +74 -0
  46. package/dist/lib/daylight/look.d.ts +33 -0
  47. package/dist/lib/daylight/look.d.ts.map +1 -0
  48. package/dist/lib/daylight/look.js +13 -0
  49. package/dist/lib/daylight/material-hooks.d.ts +2 -0
  50. package/dist/lib/daylight/material-hooks.d.ts.map +1 -0
  51. package/dist/lib/daylight/material-hooks.js +150 -0
  52. package/dist/lib/daylight/room-context.d.ts +12 -0
  53. package/dist/lib/daylight/room-context.d.ts.map +1 -0
  54. package/dist/lib/daylight/room-context.js +53 -0
  55. package/dist/lib/daylight/room-scene.d.ts +54 -0
  56. package/dist/lib/daylight/room-scene.d.ts.map +1 -0
  57. package/dist/lib/daylight/room-scene.js +356 -0
  58. package/dist/lib/daylight/rooms.d.ts +262 -0
  59. package/dist/lib/daylight/rooms.d.ts.map +1 -0
  60. package/dist/lib/daylight/rooms.js +1223 -0
  61. package/dist/lib/daylight/runtime.d.ts +33 -0
  62. package/dist/lib/daylight/runtime.d.ts.map +1 -0
  63. package/dist/lib/daylight/runtime.js +42 -0
  64. package/dist/lib/daylight/sky.d.ts +26 -0
  65. package/dist/lib/daylight/sky.d.ts.map +1 -0
  66. package/dist/lib/daylight/sky.js +217 -0
  67. package/dist/lib/daylight/solar.d.ts +46 -0
  68. package/dist/lib/daylight/solar.d.ts.map +1 -0
  69. package/dist/lib/daylight/solar.js +156 -0
  70. package/dist/lib/device-tier.d.ts +29 -0
  71. package/dist/lib/device-tier.d.ts.map +1 -0
  72. package/dist/lib/device-tier.js +71 -0
  73. package/dist/lib/dispose-object3d.d.ts.map +1 -1
  74. package/dist/lib/dispose-object3d.js +4 -2
  75. package/dist/lib/drone-flight.d.ts +44 -0
  76. package/dist/lib/drone-flight.d.ts.map +1 -0
  77. package/dist/lib/drone-flight.js +121 -0
  78. package/dist/lib/editor-highlight.d.ts +16 -0
  79. package/dist/lib/editor-highlight.d.ts.map +1 -0
  80. package/dist/lib/editor-highlight.js +33 -0
  81. package/dist/lib/foundation-materials.d.ts.map +1 -1
  82. package/dist/lib/foundation-materials.js +2 -4
  83. package/dist/lib/glb-merge.d.ts +23 -0
  84. package/dist/lib/glb-merge.d.ts.map +1 -0
  85. package/dist/lib/glb-merge.js +184 -0
  86. package/dist/lib/hardware-geometry.d.ts +21 -0
  87. package/dist/lib/hardware-geometry.d.ts.map +1 -0
  88. package/dist/lib/hardware-geometry.js +283 -0
  89. package/dist/lib/layer-pass.d.ts +2 -0
  90. package/dist/lib/layer-pass.d.ts.map +1 -1
  91. package/dist/lib/layer-pass.js +20 -0
  92. package/dist/lib/materials.d.ts.map +1 -1
  93. package/dist/lib/materials.js +50 -23
  94. package/dist/lib/merged-outline-node.d.ts.map +1 -1
  95. package/dist/lib/merged-outline-node.js +7 -1
  96. package/dist/lib/occlusion-culler.d.ts +34 -0
  97. package/dist/lib/occlusion-culler.d.ts.map +1 -0
  98. package/dist/lib/occlusion-culler.js +698 -0
  99. package/dist/lib/post-processing-resources.d.ts +4 -0
  100. package/dist/lib/post-processing-resources.d.ts.map +1 -1
  101. package/dist/lib/post-processing-resources.js +5 -0
  102. package/dist/lib/readback-rows.d.ts +14 -0
  103. package/dist/lib/readback-rows.d.ts.map +1 -0
  104. package/dist/lib/readback-rows.js +31 -0
  105. package/dist/lib/reveal-pose.d.ts.map +1 -1
  106. package/dist/lib/reveal-pose.js +3 -0
  107. package/dist/lib/scene-matrices.d.ts +12 -0
  108. package/dist/lib/scene-matrices.d.ts.map +1 -0
  109. package/dist/lib/scene-matrices.js +287 -0
  110. package/dist/lib/scene-pass-mrt.d.ts +5 -2
  111. package/dist/lib/scene-pass-mrt.d.ts.map +1 -1
  112. package/dist/lib/scene-pass-mrt.js +7 -2
  113. package/dist/lib/scene-themes.d.ts +0 -2
  114. package/dist/lib/scene-themes.d.ts.map +1 -1
  115. package/dist/lib/scene-themes.js +0 -2
  116. package/dist/lib/scene-visibility.d.ts +2 -0
  117. package/dist/lib/scene-visibility.d.ts.map +1 -1
  118. package/dist/lib/scene-visibility.js +12 -3
  119. package/dist/lib/site-surfaces.d.ts +1 -8
  120. package/dist/lib/site-surfaces.d.ts.map +1 -1
  121. package/dist/lib/site-surfaces.js +0 -19
  122. package/dist/lib/snapshot-pipeline.d.ts.map +1 -1
  123. package/dist/lib/snapshot-pipeline.js +24 -40
  124. package/dist/lib/texture-reference.d.ts.map +1 -1
  125. package/dist/lib/texture-reference.js +10 -8
  126. package/dist/lib/underground-ambiance.d.ts +0 -2
  127. package/dist/lib/underground-ambiance.d.ts.map +1 -1
  128. package/dist/lib/underground-ambiance.js +0 -2
  129. package/dist/store/use-item-light-pool.d.ts +3 -6
  130. package/dist/store/use-item-light-pool.d.ts.map +1 -1
  131. package/dist/store/use-item-light-pool.js +12 -0
  132. package/dist/store/use-viewer.d.ts +72 -15
  133. package/dist/store/use-viewer.d.ts.map +1 -1
  134. package/dist/store/use-viewer.js +103 -18
  135. package/dist/systems/ceiling/ceiling-system.d.ts +5 -0
  136. package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
  137. package/dist/systems/ceiling/ceiling-system.js +11 -1
  138. package/dist/systems/door/door-system.d.ts.map +1 -1
  139. package/dist/systems/door/door-system.js +501 -145
  140. package/dist/systems/door/leaf-profile.d.ts +11 -0
  141. package/dist/systems/door/leaf-profile.d.ts.map +1 -0
  142. package/dist/systems/door/leaf-profile.js +140 -0
  143. package/dist/systems/interactive/callout-actions.d.ts +72 -0
  144. package/dist/systems/interactive/callout-actions.d.ts.map +1 -0
  145. package/dist/systems/interactive/callout-actions.js +212 -0
  146. package/dist/systems/interactive/interactive-system.d.ts +5 -1
  147. package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
  148. package/dist/systems/interactive/interactive-system.js +44 -121
  149. package/dist/systems/interactive/opening-settled.d.ts +16 -0
  150. package/dist/systems/interactive/opening-settled.d.ts.map +1 -0
  151. package/dist/systems/interactive/opening-settled.js +53 -0
  152. package/dist/systems/item-light/item-light-system.d.ts.map +1 -1
  153. package/dist/systems/item-light/item-light-system.js +22 -214
  154. package/dist/systems/item-light/pooled-lights.d.ts +70 -0
  155. package/dist/systems/item-light/pooled-lights.d.ts.map +1 -0
  156. package/dist/systems/item-light/pooled-lights.js +449 -0
  157. package/dist/systems/level/level-system.d.ts.map +1 -1
  158. package/dist/systems/level/level-system.js +19 -1
  159. package/dist/systems/roof/roof-material-slots.d.ts +14 -0
  160. package/dist/systems/roof/roof-material-slots.d.ts.map +1 -0
  161. package/dist/systems/roof/roof-material-slots.js +13 -0
  162. package/dist/systems/roof/roof-materials.d.ts +25 -2
  163. package/dist/systems/roof/roof-materials.d.ts.map +1 -1
  164. package/dist/systems/roof/roof-materials.js +85 -26
  165. package/dist/systems/roof/roof-system.d.ts +34 -1
  166. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  167. package/dist/systems/roof/roof-system.js +477 -105
  168. package/dist/systems/wall/opening-casing-geometry.d.ts +14 -0
  169. package/dist/systems/wall/opening-casing-geometry.d.ts.map +1 -0
  170. package/dist/systems/wall/opening-casing-geometry.js +102 -0
  171. package/dist/systems/wall/opening-cutout-geometry.d.ts +9 -0
  172. package/dist/systems/wall/opening-cutout-geometry.d.ts.map +1 -1
  173. package/dist/systems/wall/opening-cutout-geometry.js +7 -3
  174. package/dist/systems/wall/wall-materials.d.ts.map +1 -1
  175. package/dist/systems/wall/wall-materials.js +28 -35
  176. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  177. package/dist/systems/wall/wall-system.js +34 -19
  178. package/dist/systems/wall/wall-t-junctions.d.ts +15 -0
  179. package/dist/systems/wall/wall-t-junctions.d.ts.map +1 -0
  180. package/dist/systems/wall/wall-t-junctions.js +149 -0
  181. package/dist/systems/window/window-animation-system.d.ts +3 -12
  182. package/dist/systems/window/window-animation-system.d.ts.map +1 -1
  183. package/dist/systems/window/window-animation-system.js +37 -122
  184. package/dist/systems/window/window-pose.d.ts +31 -0
  185. package/dist/systems/window/window-pose.d.ts.map +1 -0
  186. package/dist/systems/window/window-pose.js +152 -0
  187. package/dist/systems/window/window-roller-shutter.d.ts +8 -0
  188. package/dist/systems/window/window-roller-shutter.d.ts.map +1 -0
  189. package/dist/systems/window/window-roller-shutter.js +64 -0
  190. package/dist/systems/window/window-system.d.ts +2 -11
  191. package/dist/systems/window/window-system.d.ts.map +1 -1
  192. package/dist/systems/window/window-system.js +470 -303
  193. package/package.json +6 -6
  194. package/dist/systems/interactive/control-widget.d.ts +0 -10
  195. package/dist/systems/interactive/control-widget.d.ts.map +0 -1
  196. package/dist/systems/interactive/control-widget.js +0 -35
  197. package/dist/systems/interactive/procedural-controls.d.ts +0 -13
  198. package/dist/systems/interactive/procedural-controls.d.ts.map +0 -1
  199. 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, itemInteraction, itemPrompt, operateItem, polygonInteriorPoint, useInteractive, } from '@pascal-app/core';
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
- }, [gltf.scene, textures, sceneTheme]);
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 (!walkthroughMode)
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
- const item = toggleableItem(target.node.userData.pascalId);
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 (!(toggleProcedural(target.hit, target.node) || toggleLoopMechanism(target.node)))
737
- toggleOpenable(target.node);
738
- }, [toggleableItem, toggleLoopMechanism, toggleOpenable, toggleProcedural]);
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 (!walkthroughMode)
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
- canvas?.addEventListener('click', activateWalkDoor);
928
+ const onClick = () => activateWalkDoor(true);
929
+ canvas?.addEventListener('click', onClick);
749
930
  return () => {
750
931
  window.removeEventListener('keydown', onKey);
751
- canvas?.removeEventListener('click', activateWalkDoor);
932
+ canvas?.removeEventListener('click', onClick);
752
933
  };
753
- }, [walkthroughMode, activateWalkDoor]);
754
- // Clear the HUD (and stale targeting) whenever walkthrough turns off.
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 (walkthroughMode)
937
+ if (walkingOnFoot)
757
938
  return;
939
+ setShuttersInView(false);
758
940
  walkDoorRef.current = null;
759
941
  lastWalkKey.current = null;
760
942
  onWalkthroughChange?.(null);
761
- }, [walkthroughMode, onWalkthroughChange]);
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; an openable the click selects (a
783
- // door, a window, an item with an `open` clip, a mechanism) also plays.
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
- if (!pick || !useViewer.getState().selection.selectedIds.includes(pick.nodeId))
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, levelOrder: levelOrder, zones: zoneEntries })) : null, referenceNodes?.length ? (_jsx(GlbReferenceNodes, { identity: identity, nodes: referenceNodes })) : null, replaceNodes?.length ? (_jsx(GlbReplaceInstances, { identity: identity, nodes: replaceNodes })) : null] }));
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;AAiMhC;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,EAAE,GAAG,EAAE,EAAE;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,sCA0PhE"}
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
- if (!mesh.layers.isEnabled(SCENE_LAYER))
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;AAKrC,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AACrE,OAAO,EAAuB,KAAK,uBAAuB,EAAE,MAAM,0BAA0B,CAAA;AAI5F,OAAkB,EAAE,KAAK,aAAa,EAAE,MAAM,wBAAwB,CAAA;AActE,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;AAED,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;;;;;;;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,sGA2RV,CAAA;eAyEa,MAAM"}
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"}