@pascal-app/viewer 1.0.6 → 1.1.0

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