@pascal-app/viewer 1.0.7 → 1.1.1

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 (270) hide show
  1. package/README.md +209 -19
  2. package/dist/components/viewer/drone-controller.d.ts.map +1 -1
  3. package/dist/components/viewer/drone-controller.js +1 -5
  4. package/dist/components/viewer/first-person/build-collider-world.d.ts +14 -0
  5. package/dist/components/viewer/first-person/build-collider-world.d.ts.map +1 -0
  6. package/dist/components/viewer/first-person/build-collider-world.js +426 -0
  7. package/dist/components/viewer/first-person/ground-holes.d.ts +22 -0
  8. package/dist/components/viewer/first-person/ground-holes.d.ts.map +1 -0
  9. package/dist/components/viewer/first-person/ground-holes.js +116 -0
  10. package/dist/components/viewer/first-person/mechanism-targets.d.ts +27 -0
  11. package/dist/components/viewer/first-person/mechanism-targets.d.ts.map +1 -0
  12. package/dist/components/viewer/first-person/mechanism-targets.js +79 -0
  13. package/dist/components/viewer/first-person/terrain-collider.d.ts +32 -0
  14. package/dist/components/viewer/first-person/terrain-collider.d.ts.map +1 -0
  15. package/dist/components/viewer/first-person/terrain-collider.js +175 -0
  16. package/dist/components/viewer/glb-replace-instances.d.ts +6 -3
  17. package/dist/components/viewer/glb-replace-instances.d.ts.map +1 -1
  18. package/dist/components/viewer/glb-replace-instances.js +5 -4
  19. package/dist/components/viewer/glb-scene.d.ts +3 -1
  20. package/dist/components/viewer/glb-scene.d.ts.map +1 -1
  21. package/dist/components/viewer/glb-scene.js +2 -1
  22. package/dist/components/viewer/glb-walkthrough-controller.d.ts +1 -1
  23. package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -1
  24. package/dist/components/viewer/glb-walkthrough-controller.js +3 -4
  25. package/dist/components/viewer/index.d.ts.map +1 -1
  26. package/dist/components/viewer/index.js +8 -4
  27. package/dist/components/viewer/live-walkthrough-controller.d.ts +17 -0
  28. package/dist/components/viewer/live-walkthrough-controller.d.ts.map +1 -0
  29. package/dist/components/viewer/live-walkthrough-controller.js +1449 -0
  30. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  31. package/dist/components/viewer/post-processing.js +22 -1
  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/unsupported-gpu-fallback.d.ts.map +1 -1
  35. package/dist/components/viewer/unsupported-gpu-fallback.js +21 -1
  36. package/dist/components/viewer/use-daylight-rooms.d.ts.map +1 -1
  37. package/dist/components/viewer/use-daylight-rooms.js +75 -33
  38. package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
  39. package/dist/hooks/use-gltf-ktx2.js +2 -1
  40. package/dist/index.d.ts +8 -1
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +8 -1
  43. package/dist/lib/asset-url.d.ts +0 -1
  44. package/dist/lib/asset-url.d.ts.map +1 -1
  45. package/dist/lib/asset-url.js +3 -3
  46. package/dist/lib/daylight/room-context.d.ts.map +1 -1
  47. package/dist/lib/daylight/room-context.js +3 -2
  48. package/dist/lib/daylight/room-map.d.ts +149 -0
  49. package/dist/lib/daylight/room-map.d.ts.map +1 -0
  50. package/dist/lib/daylight/room-map.js +813 -0
  51. package/dist/lib/daylight/room-scene.d.ts +4 -2
  52. package/dist/lib/daylight/room-scene.d.ts.map +1 -1
  53. package/dist/lib/daylight/room-scene.js +81 -40
  54. package/dist/lib/daylight/rooms.d.ts +71 -32
  55. package/dist/lib/daylight/rooms.d.ts.map +1 -1
  56. package/dist/lib/daylight/rooms.js +371 -286
  57. package/dist/lib/ktx2-loader.d.ts.map +1 -1
  58. package/dist/lib/ktx2-loader.js +2 -1
  59. package/dist/lib/measurement-label.d.ts +4 -0
  60. package/dist/lib/measurement-label.d.ts.map +1 -0
  61. package/dist/lib/measurement-label.js +54 -0
  62. package/dist/lib/measurement-presentation.d.ts +7 -0
  63. package/dist/lib/measurement-presentation.d.ts.map +1 -0
  64. package/dist/lib/measurement-presentation.js +14 -0
  65. package/dist/lib/pointer-events.d.ts.map +1 -1
  66. package/dist/lib/pointer-events.js +3 -3
  67. package/dist/lib/texture-reference.d.ts.map +1 -1
  68. package/dist/lib/texture-reference.js +4 -3
  69. package/dist/lib/typing-target.d.ts +2 -0
  70. package/dist/lib/typing-target.d.ts.map +1 -0
  71. package/dist/lib/typing-target.js +7 -0
  72. package/dist/lib/viewer-config.d.ts +27 -0
  73. package/dist/lib/viewer-config.d.ts.map +1 -0
  74. package/dist/lib/viewer-config.js +23 -0
  75. package/dist/lib/viewer-preference-storage.d.ts +7 -0
  76. package/dist/lib/viewer-preference-storage.d.ts.map +1 -0
  77. package/dist/lib/viewer-preference-storage.js +23 -0
  78. package/dist/sfx/audio-settings.d.ts +81 -0
  79. package/dist/sfx/audio-settings.d.ts.map +1 -0
  80. package/dist/sfx/audio-settings.js +166 -0
  81. package/dist/sfx/index.d.ts +6 -0
  82. package/dist/sfx/index.d.ts.map +1 -0
  83. package/dist/sfx/index.js +5 -0
  84. package/dist/sfx/interaction-sounds.d.ts +7 -0
  85. package/dist/sfx/interaction-sounds.d.ts.map +1 -0
  86. package/dist/sfx/interaction-sounds.js +39 -0
  87. package/dist/sfx/player.d.ts +31 -0
  88. package/dist/sfx/player.d.ts.map +1 -0
  89. package/dist/sfx/player.js +255 -0
  90. package/dist/sfx/sounds.d.ts +60 -0
  91. package/dist/sfx/sounds.d.ts.map +1 -0
  92. package/dist/sfx/sounds.js +25 -0
  93. package/dist/sfx/use-viewer-ui-sounds.d.ts +8 -0
  94. package/dist/sfx/use-viewer-ui-sounds.d.ts.map +1 -0
  95. package/dist/sfx/use-viewer-ui-sounds.js +42 -0
  96. package/dist/store/use-first-person-hud.d.ts +13 -0
  97. package/dist/store/use-first-person-hud.d.ts.map +1 -0
  98. package/dist/store/use-first-person-hud.js +21 -0
  99. package/dist/store/use-viewer.d.ts +2 -56
  100. package/dist/store/use-viewer.d.ts.map +1 -1
  101. package/dist/store/use-viewer.js +3 -1
  102. package/dist/systems/elevator/elevator-interaction-system.d.ts +16 -2
  103. package/dist/systems/elevator/elevator-interaction-system.d.ts.map +1 -1
  104. package/dist/systems/elevator/elevator-interaction-system.js +41 -8
  105. package/dist/systems/geometry/geometry-status.d.ts +8 -0
  106. package/dist/systems/geometry/geometry-status.d.ts.map +1 -0
  107. package/dist/systems/geometry/geometry-status.js +24 -0
  108. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  109. package/dist/systems/roof/roof-system.js +134 -78
  110. package/dist/systems/wall/wall-system.d.ts +11 -0
  111. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  112. package/dist/systems/wall/wall-system.js +56 -10
  113. package/dist/ui/components/action-button.d.ts +14 -0
  114. package/dist/ui/components/action-button.d.ts.map +1 -0
  115. package/dist/ui/components/action-button.js +21 -0
  116. package/dist/ui/components/action-menu-placement.d.ts +7 -0
  117. package/dist/ui/components/action-menu-placement.d.ts.map +1 -0
  118. package/dist/ui/components/action-menu-placement.js +10 -0
  119. package/dist/ui/components/floorplan-compass-button.d.ts +10 -0
  120. package/dist/ui/components/floorplan-compass-button.d.ts.map +1 -0
  121. package/dist/ui/components/floorplan-compass-button.js +22 -0
  122. package/dist/ui/components/follow-pascal-toggle.d.ts +6 -0
  123. package/dist/ui/components/follow-pascal-toggle.d.ts.map +1 -0
  124. package/dist/ui/components/follow-pascal-toggle.js +21 -0
  125. package/dist/ui/components/localized-html.d.ts +9 -0
  126. package/dist/ui/components/localized-html.d.ts.map +1 -0
  127. package/dist/ui/components/localized-html.js +16 -0
  128. package/dist/ui/components/render-settings-menu.d.ts +9 -0
  129. package/dist/ui/components/render-settings-menu.d.ts.map +1 -0
  130. package/dist/ui/components/render-settings-menu.js +145 -0
  131. package/dist/ui/components/rooms/room-highlight.d.ts +10 -0
  132. package/dist/ui/components/rooms/room-highlight.d.ts.map +1 -0
  133. package/dist/ui/components/rooms/room-highlight.js +56 -0
  134. package/dist/ui/components/rooms/room-labels.d.ts +278 -0
  135. package/dist/ui/components/rooms/room-labels.d.ts.map +1 -0
  136. package/dist/ui/components/rooms/room-labels.js +1295 -0
  137. package/dist/ui/components/rooms/viewer-rooms.d.ts +14 -0
  138. package/dist/ui/components/rooms/viewer-rooms.d.ts.map +1 -0
  139. package/dist/ui/components/rooms/viewer-rooms.js +123 -0
  140. package/dist/ui/components/scene-loader.d.ts +18 -0
  141. package/dist/ui/components/scene-loader.d.ts.map +1 -0
  142. package/dist/ui/components/scene-loader.js +29 -0
  143. package/dist/ui/components/sky-control.d.ts +37 -0
  144. package/dist/ui/components/sky-control.d.ts.map +1 -0
  145. package/dist/ui/components/sky-control.js +382 -0
  146. package/dist/ui/components/use-viewer-camera-navigation-sync.d.ts +4 -0
  147. package/dist/ui/components/use-viewer-camera-navigation-sync.d.ts.map +1 -0
  148. package/dist/ui/components/use-viewer-camera-navigation-sync.js +117 -0
  149. package/dist/ui/components/viewer-controls-bar.d.ts +20 -0
  150. package/dist/ui/components/viewer-controls-bar.d.ts.map +1 -0
  151. package/dist/ui/components/viewer-controls-bar.js +130 -0
  152. package/dist/ui/components/viewer-hover-label.d.ts +14 -0
  153. package/dist/ui/components/viewer-hover-label.d.ts.map +1 -0
  154. package/dist/ui/components/viewer-hover-label.js +28 -0
  155. package/dist/ui/components/viewer-scene-header.d.ts +34 -0
  156. package/dist/ui/components/viewer-scene-header.d.ts.map +1 -0
  157. package/dist/ui/components/viewer-scene-header.js +92 -0
  158. package/dist/ui/components/viewer-selection-manager.d.ts +7 -0
  159. package/dist/ui/components/viewer-selection-manager.d.ts.map +1 -0
  160. package/dist/ui/components/viewer-selection-manager.js +116 -0
  161. package/dist/ui/components/viewer-stage-modes.d.ts +7 -0
  162. package/dist/ui/components/viewer-stage-modes.d.ts.map +1 -0
  163. package/dist/ui/components/viewer-stage-modes.js +42 -0
  164. package/dist/ui/components/viewer-stage-switcher.d.ts +10 -0
  165. package/dist/ui/components/viewer-stage-switcher.d.ts.map +1 -0
  166. package/dist/ui/components/viewer-stage-switcher.js +16 -0
  167. package/dist/ui/components/viewer-stage.d.ts +25 -0
  168. package/dist/ui/components/viewer-stage.d.ts.map +1 -0
  169. package/dist/ui/components/viewer-stage.js +107 -0
  170. package/dist/ui/components/walkthrough-hud.d.ts +29 -0
  171. package/dist/ui/components/walkthrough-hud.d.ts.map +1 -0
  172. package/dist/ui/components/walkthrough-hud.js +55 -0
  173. package/dist/ui/floorplan/floorplan-camera-sync.d.ts +22 -0
  174. package/dist/ui/floorplan/floorplan-camera-sync.d.ts.map +1 -0
  175. package/dist/ui/floorplan/floorplan-camera-sync.js +157 -0
  176. package/dist/ui/floorplan/floorplan-dimension-renderer.d.ts +61 -0
  177. package/dist/ui/floorplan/floorplan-dimension-renderer.d.ts.map +1 -0
  178. package/dist/ui/floorplan/floorplan-dimension-renderer.js +280 -0
  179. package/dist/ui/floorplan/floorplan-geometry-renderer.d.ts +54 -0
  180. package/dist/ui/floorplan/floorplan-geometry-renderer.d.ts.map +1 -0
  181. package/dist/ui/floorplan/floorplan-geometry-renderer.js +317 -0
  182. package/dist/ui/floorplan/floorplan-label-angle.d.ts +24 -0
  183. package/dist/ui/floorplan/floorplan-label-angle.d.ts.map +1 -0
  184. package/dist/ui/floorplan/floorplan-label-angle.js +60 -0
  185. package/dist/ui/floorplan/floorplan-navigation-sync.d.ts +18 -0
  186. package/dist/ui/floorplan/floorplan-navigation-sync.d.ts.map +1 -0
  187. package/dist/ui/floorplan/floorplan-navigation-sync.js +41 -0
  188. package/dist/ui/floorplan/floorplan-preview-geometry.d.ts +26 -0
  189. package/dist/ui/floorplan/floorplan-preview-geometry.d.ts.map +1 -0
  190. package/dist/ui/floorplan/floorplan-preview-geometry.js +173 -0
  191. package/dist/ui/floorplan/floorplan-preview-navigation.d.ts +22 -0
  192. package/dist/ui/floorplan/floorplan-preview-navigation.d.ts.map +1 -0
  193. package/dist/ui/floorplan/floorplan-preview-navigation.js +41 -0
  194. package/dist/ui/floorplan/floorplan-preview-visibility.d.ts +8 -0
  195. package/dist/ui/floorplan/floorplan-preview-visibility.d.ts.map +1 -0
  196. package/dist/ui/floorplan/floorplan-preview-visibility.js +21 -0
  197. package/dist/ui/floorplan/floorplan-preview.d.ts +19 -0
  198. package/dist/ui/floorplan/floorplan-preview.d.ts.map +1 -0
  199. package/dist/ui/floorplan/floorplan-preview.js +700 -0
  200. package/dist/ui/hooks/use-mobile.d.ts +2 -0
  201. package/dist/ui/hooks/use-mobile.d.ts.map +1 -0
  202. package/dist/ui/hooks/use-mobile.js +14 -0
  203. package/dist/ui/index.d.ts +49 -0
  204. package/dist/ui/index.d.ts.map +1 -0
  205. package/dist/ui/index.js +48 -0
  206. package/dist/ui/lib/below-grade.d.ts +38 -0
  207. package/dist/ui/lib/below-grade.d.ts.map +1 -0
  208. package/dist/ui/lib/below-grade.js +102 -0
  209. package/dist/ui/lib/camera-pose.d.ts +19 -0
  210. package/dist/ui/lib/camera-pose.d.ts.map +1 -0
  211. package/dist/ui/lib/camera-pose.js +113 -0
  212. package/dist/ui/lib/cn.d.ts +3 -0
  213. package/dist/ui/lib/cn.d.ts.map +1 -0
  214. package/dist/ui/lib/cn.js +5 -0
  215. package/dist/ui/lib/level-name.d.ts +10 -0
  216. package/dist/ui/lib/level-name.d.ts.map +1 -0
  217. package/dist/ui/lib/level-name.js +33 -0
  218. package/dist/ui/lib/node-display-label.d.ts +8 -0
  219. package/dist/ui/lib/node-display-label.d.ts.map +1 -0
  220. package/dist/ui/lib/node-display-label.js +36 -0
  221. package/dist/ui/lib/room-area.d.ts +3 -0
  222. package/dist/ui/lib/room-area.d.ts.map +1 -0
  223. package/dist/ui/lib/room-area.js +8 -0
  224. package/dist/ui/lib/room-assembly-overlay.d.ts +46 -0
  225. package/dist/ui/lib/room-assembly-overlay.d.ts.map +1 -0
  226. package/dist/ui/lib/room-assembly-overlay.js +222 -0
  227. package/dist/ui/lib/room-name-messages.d.ts +28 -0
  228. package/dist/ui/lib/room-name-messages.d.ts.map +1 -0
  229. package/dist/ui/lib/room-name-messages.js +112 -0
  230. package/dist/ui/lib/room-selection.d.ts +58 -0
  231. package/dist/ui/lib/room-selection.d.ts.map +1 -0
  232. package/dist/ui/lib/room-selection.js +297 -0
  233. package/dist/ui/lib/scene-theme-name.d.ts +4 -0
  234. package/dist/ui/lib/scene-theme-name.d.ts.map +1 -0
  235. package/dist/ui/lib/scene-theme-name.js +22 -0
  236. package/dist/ui/lib/selection-modifiers.d.ts +20 -0
  237. package/dist/ui/lib/selection-modifiers.d.ts.map +1 -0
  238. package/dist/ui/lib/selection-modifiers.js +15 -0
  239. package/dist/ui/lib/viewer-selection.d.ts +132 -0
  240. package/dist/ui/lib/viewer-selection.d.ts.map +1 -0
  241. package/dist/ui/lib/viewer-selection.js +462 -0
  242. package/dist/ui/lib/zone-content.d.ts +6 -0
  243. package/dist/ui/lib/zone-content.d.ts.map +1 -0
  244. package/dist/ui/lib/zone-content.js +96 -0
  245. package/dist/ui/primitives/button.d.ts +11 -0
  246. package/dist/ui/primitives/button.d.ts.map +1 -0
  247. package/dist/ui/primitives/button.js +35 -0
  248. package/dist/ui/primitives/dropdown-menu.d.ts +26 -0
  249. package/dist/ui/primitives/dropdown-menu.d.ts.map +1 -0
  250. package/dist/ui/primitives/dropdown-menu.js +53 -0
  251. package/dist/ui/primitives/popover.d.ts +8 -0
  252. package/dist/ui/primitives/popover.d.ts.map +1 -0
  253. package/dist/ui/primitives/popover.js +17 -0
  254. package/dist/ui/primitives/switch.d.ts +5 -0
  255. package/dist/ui/primitives/switch.d.ts.map +1 -0
  256. package/dist/ui/primitives/switch.js +8 -0
  257. package/dist/ui/primitives/tooltip.d.ts +8 -0
  258. package/dist/ui/primitives/tooltip.d.ts.map +1 -0
  259. package/dist/ui/primitives/tooltip.js +17 -0
  260. package/dist/ui/store/camera-pose-store.d.ts +17 -0
  261. package/dist/ui/store/camera-pose-store.d.ts.map +1 -0
  262. package/dist/ui/store/camera-pose-store.js +15 -0
  263. package/dist/ui/store/navigation-sync-pose-store.d.ts +26 -0
  264. package/dist/ui/store/navigation-sync-pose-store.d.ts.map +1 -0
  265. package/dist/ui/store/navigation-sync-pose-store.js +16 -0
  266. package/dist/ui/store/use-follow-pascal.d.ts +30 -0
  267. package/dist/ui/store/use-follow-pascal.d.ts.map +1 -0
  268. package/dist/ui/store/use-follow-pascal.js +28 -0
  269. package/dist/ui/theme.css +339 -0
  270. package/package.json +34 -7
@@ -0,0 +1,700 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { buildFloorplanContext, FLOORPLAN_VIEW_ROTATION_DEG, floorplanLayerRank, floorplanLocalToWorldPoint, isNodeKindEnabled, nodeRegistry, useLocale, usePascalI18n, useScene, worldToFloorplanLocalPoint, } from '@pascal-app/core';
4
+ import { AnyNode as AnyNodeSchema } from '@pascal-app/core/schema';
5
+ import { Maximize2, Minus, Plus } from 'lucide-react';
6
+ import { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, } from 'react';
7
+ import { createPortal } from 'react-dom';
8
+ import useViewer from '../../store/use-viewer';
9
+ import { FloorplanCompassButton } from '../components/floorplan-compass-button';
10
+ import { localizedLevelName } from '../lib/level-name';
11
+ import { publishNavigationSyncPose, subscribeNavigationSyncPose, } from '../store/navigation-sync-pose-store';
12
+ import { subscribeFloorplanCameraNavigation, useFloorplanCameraSyncBridge, } from './floorplan-camera-sync';
13
+ import { FloorplanGeometryRenderer } from './floorplan-geometry-renderer';
14
+ import { createFloorplanNavigationSyncScheduler, setFloorplanCompassRotation, } from './floorplan-navigation-sync';
15
+ import { getFloorplanBounds, padFloorplanBounds, panFloorplanViewBox, scaleFloorplanViewBox, scaleFloorplanViewBoxBetweenClients, } from './floorplan-preview-geometry';
16
+ import { cameraAzimuthFromFloorplanRotation, floorplanRotationFromCameraAzimuth, floorplanViewBoxFromNavigationPose, nearestEquivalentDegrees, rotateFloorplanPoint, visibleFloorplanViewWidth, } from './floorplan-preview-navigation';
17
+ import { isVisibleInFloorplan } from './floorplan-preview-visibility';
18
+ const READ_ONLY_PALETTE = {
19
+ selectedStroke: '#4f46e5',
20
+ selectedFill: '#e0e7ff',
21
+ selectedHatch: '#818cf8',
22
+ wallHoverStroke: '#64748b',
23
+ endpointHandleFill: '#ffffff',
24
+ endpointHandleStroke: '#f97316',
25
+ endpointHandleHoverStroke: '#fb923c',
26
+ endpointHandleActiveFill: '#fed7aa',
27
+ endpointHandleActiveStroke: '#ea580c',
28
+ curveHandleFill: '#ffffff',
29
+ curveHandleStroke: '#0d9488',
30
+ curveHandleHoverStroke: '#14b8a6',
31
+ measurementStroke: '#475569',
32
+ measurementLabelBackground: '#ffffff',
33
+ measurementLabelText: '#0f172a',
34
+ };
35
+ const EMPTY_PREVIEW_NODES = {};
36
+ const EMPTY_INSTALLED_PLUGINS = [];
37
+ const useClientLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;
38
+ function FloorplanCameraSyncMount() {
39
+ useFloorplanCameraSyncBridge();
40
+ return null;
41
+ }
42
+ function boundsToViewBox(bounds) {
43
+ return {
44
+ x: bounds.minX,
45
+ y: bounds.minY,
46
+ width: Math.max(bounds.maxX - bounds.minX, 1),
47
+ height: Math.max(bounds.maxY - bounds.minY, 1),
48
+ };
49
+ }
50
+ function collectLevelTree(root, nodes) {
51
+ const result = [root];
52
+ const seen = new Set();
53
+ const queue = [...(root.children ?? [])];
54
+ let index = 0;
55
+ while (index < queue.length) {
56
+ const id = queue[index++];
57
+ if (!id || seen.has(id))
58
+ continue;
59
+ seen.add(id);
60
+ const node = nodes[id];
61
+ if (!node)
62
+ continue;
63
+ result.push(node);
64
+ queue.push(...(node.children ?? []));
65
+ }
66
+ return result;
67
+ }
68
+ export function normalizeFloorplanPreviewNodes(nodes) {
69
+ const normalized = {};
70
+ for (const [id, node] of Object.entries(nodes)) {
71
+ const builtin = AnyNodeSchema.safeParse(node);
72
+ if (builtin.success) {
73
+ normalized[builtin.data.id] = builtin.data;
74
+ continue;
75
+ }
76
+ const type = node && typeof node === 'object' && !Array.isArray(node)
77
+ ? node.type
78
+ : null;
79
+ const registered = typeof type === 'string' ? nodeRegistry.get(type)?.schema.safeParse(node) : null;
80
+ if (registered?.success) {
81
+ const parsed = registered.data;
82
+ normalized[parsed.id] = parsed;
83
+ }
84
+ else {
85
+ console.warn(`[floorplan-preview] Skipping invalid node ${id}`, builtin.error.issues);
86
+ }
87
+ }
88
+ return normalized;
89
+ }
90
+ function buildFloorplanGeometries(nodes, installedPlugins, level, unit, metricNotation) {
91
+ const building = level.parentId ? nodes[level.parentId] : undefined;
92
+ const levelTree = collectLevelTree(level, nodes);
93
+ const entries = levelTree.map((node) => ({ node }));
94
+ const entryIds = new Set(entries.map((entry) => entry.node.id));
95
+ if (building) {
96
+ for (const candidate of Object.values(nodes)) {
97
+ const definition = nodeRegistry.get(candidate.type);
98
+ if (definition?.floorplanScope === 'building' &&
99
+ candidate.parentId === building.id &&
100
+ !entryIds.has(candidate.id)) {
101
+ entries.push({
102
+ node: candidate,
103
+ contextOverrides: { children: [], siblings: [], parent: level },
104
+ });
105
+ entryIds.add(candidate.id);
106
+ }
107
+ }
108
+ }
109
+ for (const candidate of Object.values(nodes)) {
110
+ const definition = nodeRegistry.get(candidate.type);
111
+ const linkedLevelIds = definition?.floorplanLinkedLevelIds;
112
+ if (definition?.floorplan &&
113
+ linkedLevelIds?.(candidate).includes(level.id) &&
114
+ !entryIds.has(candidate.id)) {
115
+ const childIds = candidate.children;
116
+ const children = Array.isArray(childIds)
117
+ ? childIds.map((id) => nodes[id]).filter((child) => child !== undefined)
118
+ : [];
119
+ entries.push({
120
+ node: candidate,
121
+ contextOverrides: { children, siblings: [], parent: level },
122
+ });
123
+ entryIds.add(candidate.id);
124
+ }
125
+ }
126
+ const renderable = entries
127
+ .filter((entry) => isVisibleInFloorplan(entry.node, nodes))
128
+ .filter((entry) => isNodeKindEnabled(entry.node.type, installedPlugins))
129
+ .filter((entry) => nodeRegistry.get(entry.node.type)?.floorplan)
130
+ .sort((a, b) => floorplanLayerRank(a.node.type) - floorplanLayerRank(b.node.type));
131
+ const byType = new Map();
132
+ for (const node of levelTree) {
133
+ if (!isNodeKindEnabled(node.type, installedPlugins))
134
+ continue;
135
+ if (!nodeRegistry.get(node.type)?.computeFloorplanLevelData)
136
+ continue;
137
+ const siblings = byType.get(node.type) ?? [];
138
+ siblings.push(node);
139
+ byType.set(node.type, siblings);
140
+ }
141
+ const levelData = new Map();
142
+ for (const [type, siblings] of byType) {
143
+ const definition = nodeRegistry.get(type);
144
+ if (definition?.computeFloorplanLevelData) {
145
+ levelData.set(type, definition.computeFloorplanLevelData({ siblings, nodes }));
146
+ }
147
+ }
148
+ const geometries = [];
149
+ for (const { node, contextOverrides } of renderable) {
150
+ const definition = nodeRegistry.get(node.type);
151
+ if (!definition?.floorplan)
152
+ continue;
153
+ const baseContext = buildFloorplanContext(node, nodes, {
154
+ automaticDimensions: false,
155
+ selected: false,
156
+ unit,
157
+ metricNotation,
158
+ purpose: 'document',
159
+ highlighted: false,
160
+ hovered: false,
161
+ moving: false,
162
+ palette: READ_ONLY_PALETTE,
163
+ }, levelData.get(node.type));
164
+ const context = contextOverrides ? { ...baseContext, ...contextOverrides } : baseContext;
165
+ try {
166
+ const geometry = definition.floorplan(node, context);
167
+ if (geometry) {
168
+ geometries.push({
169
+ id: node.id,
170
+ geometry,
171
+ includeInInitialFit: definition.category !== 'furnish',
172
+ });
173
+ }
174
+ }
175
+ catch (error) {
176
+ console.error(`[floorplan-preview] Failed to render ${node.type}:${node.id}`, error);
177
+ }
178
+ }
179
+ return geometries;
180
+ }
181
+ export function FloorplanPreview({ className, compassHost, levelId, navigationVisible = true, onLevelChange, scene, showCompass = true, showLevelSelector = true, synchronizeNavigation = false, }) {
182
+ const i18n = usePascalI18n();
183
+ const locale = useLocale();
184
+ const storeNodes = useScene((state) => (scene ? EMPTY_PREVIEW_NODES : state.nodes));
185
+ const storeInstalledPlugins = useScene((state) => scene ? EMPTY_INSTALLED_PLUGINS : state.installedPlugins);
186
+ const unit = useViewer((state) => state.unit);
187
+ const metricNotation = useViewer((state) => state.metricNotation);
188
+ const externalNodes = useMemo(() => (scene?.nodes ? normalizeFloorplanPreviewNodes(scene.nodes) : null), [scene?.nodes]);
189
+ const nodes = externalNodes ?? storeNodes;
190
+ const installedPlugins = scene ? scene.installedPlugins : storeInstalledPlugins;
191
+ const levels = useMemo(() => Object.values(nodes)
192
+ .filter((node) => node.type === 'level')
193
+ .sort((a, b) => (a.level ?? 0) - (b.level ?? 0)), [nodes]);
194
+ const [internalLevelId, setInternalLevelId] = useState(null);
195
+ const activeLevelId = (levelId && levels.some((level) => level.id === levelId) ? levelId : null) ??
196
+ (internalLevelId && levels.some((level) => level.id === internalLevelId)
197
+ ? internalLevelId
198
+ : null) ??
199
+ levels[0]?.id ??
200
+ null;
201
+ const activeLevel = activeLevelId ? nodes[activeLevelId] : undefined;
202
+ const activeBuilding = activeLevel?.parentId
203
+ ? nodes[activeLevel.parentId]
204
+ : undefined;
205
+ const buildingPosition = useMemo(() => (activeBuilding?.type === 'building' ? activeBuilding.position : [0, 0, 0]), [activeBuilding]);
206
+ const buildingRotationY = activeBuilding?.type === 'building' ? activeBuilding.rotation[1] : 0;
207
+ const buildingRotationDeg = (buildingRotationY * 180) / Math.PI;
208
+ const renderEntries = useMemo(() => activeLevel
209
+ ? buildFloorplanGeometries(nodes, installedPlugins, activeLevel, unit, metricNotation)
210
+ : [], [activeLevel, installedPlugins, metricNotation, nodes, unit]);
211
+ const framingEntries = useMemo(() => {
212
+ const structural = renderEntries.filter((entry) => entry.includeInInitialFit);
213
+ return structural.length > 0 ? structural : renderEntries;
214
+ }, [renderEntries]);
215
+ const fitKey = `${activeLevelId ?? 'none'}:${framingEntries.map((entry) => entry.id).join('|')}`;
216
+ const geometricFitBounds = useMemo(() => getFloorplanBounds(framingEntries.map((entry) => entry.geometry)), [framingEntries]);
217
+ const [measuredFit, setMeasuredFit] = useState(null);
218
+ const fittedViewBox = useMemo(() => {
219
+ const bounds = measuredFit?.key === fitKey ? measuredFit.bounds : geometricFitBounds;
220
+ return bounds
221
+ ? boundsToViewBox(padFloorplanBounds(bounds))
222
+ : { x: -5, y: -5, width: 10, height: 10 };
223
+ }, [fitKey, geometricFitBounds, measuredFit]);
224
+ const [viewBox, setViewBox] = useState(fittedViewBox);
225
+ const viewBoxRef = useRef(viewBox);
226
+ const interactionRef = useRef(null);
227
+ const svgRef = useRef(null);
228
+ const gridRef = useRef(null);
229
+ const sceneRef = useRef(null);
230
+ const compassNeedleRef = useRef(null);
231
+ const fitElementRefs = useRef(new Map());
232
+ const pointersRef = useRef(new Map());
233
+ const dragRef = useRef(null);
234
+ const pinchRef = useRef(null);
235
+ const [isPanning, setIsPanning] = useState(false);
236
+ const [isRotating, setIsRotating] = useState(false);
237
+ const [viewportSize, setViewportSize] = useState({ width: 1000, height: 1000 });
238
+ const [rotationDeg, setRotationDeg] = useState(0);
239
+ const rotationDegRef = useRef(rotationDeg);
240
+ const latestNavigationPoseRef = useRef(null);
241
+ const gridPatternId = useId().replaceAll(':', '');
242
+ const updateViewBox = useCallback((update) => {
243
+ setViewBox((current) => {
244
+ const next = typeof update === 'function' ? update(current) : update;
245
+ viewBoxRef.current = next;
246
+ return next;
247
+ });
248
+ }, []);
249
+ const presentViewBox = useCallback((next) => {
250
+ viewBoxRef.current = next;
251
+ const value = `${next.x} ${next.y} ${next.width} ${next.height}`;
252
+ svgRef.current?.setAttribute('viewBox', value);
253
+ const grid = gridRef.current;
254
+ if (grid) {
255
+ grid.setAttribute('x', String(next.x));
256
+ grid.setAttribute('y', String(next.y));
257
+ grid.setAttribute('width', String(next.width));
258
+ grid.setAttribute('height', String(next.height));
259
+ }
260
+ }, []);
261
+ const presentRotation = useCallback((nextRotationDeg) => {
262
+ rotationDegRef.current = nextRotationDeg;
263
+ const sceneRotationDeg = FLOORPLAN_VIEW_ROTATION_DEG + nextRotationDeg - buildingRotationDeg;
264
+ const sceneElement = sceneRef.current;
265
+ if (sceneElement) {
266
+ if (sceneRotationDeg === 0)
267
+ sceneElement.removeAttribute('transform');
268
+ else
269
+ sceneElement.setAttribute('transform', `rotate(${sceneRotationDeg})`);
270
+ }
271
+ setFloorplanCompassRotation(compassNeedleRef.current, nextRotationDeg);
272
+ }, [buildingRotationDeg]);
273
+ const commitPresentation = useCallback((nextViewBox, nextRotationDeg) => {
274
+ presentViewBox(nextViewBox);
275
+ presentRotation(nextRotationDeg);
276
+ setViewBox(nextViewBox);
277
+ setRotationDeg(nextRotationDeg);
278
+ }, [presentRotation, presentViewBox]);
279
+ const publishNavigation = useCallback((nextViewBox, nextRotationDeg) => {
280
+ if (!synchronizeNavigation)
281
+ return;
282
+ const sceneRotationDeg = FLOORPLAN_VIEW_ROTATION_DEG + nextRotationDeg - buildingRotationDeg;
283
+ const displayedCenter = {
284
+ x: nextViewBox.x + nextViewBox.width / 2,
285
+ y: nextViewBox.y + nextViewBox.height / 2,
286
+ };
287
+ const localCenter = rotateFloorplanPoint(displayedCenter, -sceneRotationDeg);
288
+ const worldCenter = floorplanLocalToWorldPoint(localCenter, buildingPosition, buildingRotationY);
289
+ publishNavigationSyncPose({
290
+ source: '2d',
291
+ target: [
292
+ worldCenter.x,
293
+ latestNavigationPoseRef.current?.target[1] ?? buildingPosition[1],
294
+ worldCenter.z,
295
+ ],
296
+ azimuth: cameraAzimuthFromFloorplanRotation(nextRotationDeg),
297
+ viewWidth: visibleFloorplanViewWidth(nextViewBox, viewportSize),
298
+ });
299
+ }, [buildingPosition, buildingRotationDeg, buildingRotationY, synchronizeNavigation, viewportSize]);
300
+ const applyNavigationPresentationRef = useRef(() => { });
301
+ const commitNavigationPresentationRef = useRef(() => { });
302
+ const navigationSchedulerRef = useRef(null);
303
+ if (!navigationSchedulerRef.current) {
304
+ navigationSchedulerRef.current = createFloorplanNavigationSyncScheduler({
305
+ applyPresentation: (pose) => applyNavigationPresentationRef.current(pose),
306
+ commit: (pose) => commitNavigationPresentationRef.current(pose),
307
+ });
308
+ }
309
+ applyNavigationPresentationRef.current = (pose) => {
310
+ latestNavigationPoseRef.current = pose;
311
+ const nextRotationDeg = floorplanRotationFromCameraAzimuth(pose.azimuth, rotationDegRef.current);
312
+ presentRotation(nextRotationDeg);
313
+ if (!navigationVisible)
314
+ return;
315
+ const localCenter = worldToFloorplanLocalPoint(pose.target[0], pose.target[2], buildingPosition, buildingRotationY);
316
+ presentViewBox(floorplanViewBoxFromNavigationPose(pose, localCenter, FLOORPLAN_VIEW_ROTATION_DEG + nextRotationDeg - buildingRotationDeg, viewportSize));
317
+ };
318
+ commitNavigationPresentationRef.current = (_pose) => {
319
+ if (!navigationVisible)
320
+ return;
321
+ setViewBox(viewBoxRef.current);
322
+ setRotationDeg(rotationDegRef.current);
323
+ };
324
+ useClientLayoutEffect(() => {
325
+ let bounds = null;
326
+ for (const entry of framingEntries) {
327
+ const element = fitElementRefs.current.get(entry.id);
328
+ if (!element || typeof element.getBBox !== 'function')
329
+ continue;
330
+ let box;
331
+ try {
332
+ box = element.getBBox();
333
+ }
334
+ catch {
335
+ continue;
336
+ }
337
+ if (![box.x, box.y, box.width, box.height].every(Number.isFinite))
338
+ continue;
339
+ if (box.width === 0 && box.height === 0)
340
+ continue;
341
+ const next = {
342
+ minX: box.x,
343
+ minY: box.y,
344
+ maxX: box.x + box.width,
345
+ maxY: box.y + box.height,
346
+ };
347
+ bounds = bounds
348
+ ? {
349
+ minX: Math.min(bounds.minX, next.minX),
350
+ minY: Math.min(bounds.minY, next.minY),
351
+ maxX: Math.max(bounds.maxX, next.maxX),
352
+ maxY: Math.max(bounds.maxY, next.maxY),
353
+ }
354
+ : next;
355
+ }
356
+ if (bounds)
357
+ setMeasuredFit({ key: fitKey, bounds });
358
+ }, [fitKey, framingEntries]);
359
+ useClientLayoutEffect(() => {
360
+ if (synchronizeNavigation && latestNavigationPoseRef.current)
361
+ return;
362
+ updateViewBox(fittedViewBox);
363
+ pointersRef.current.clear();
364
+ dragRef.current = null;
365
+ pinchRef.current = null;
366
+ setIsPanning(false);
367
+ setIsRotating(false);
368
+ }, [fittedViewBox, synchronizeNavigation, updateViewBox]);
369
+ useEffect(() => {
370
+ const svg = svgRef.current;
371
+ if (!svg)
372
+ return;
373
+ const updateSize = () => {
374
+ const rect = svg.getBoundingClientRect();
375
+ setViewportSize({ width: Math.max(rect.width, 1), height: Math.max(rect.height, 1) });
376
+ };
377
+ updateSize();
378
+ if (typeof ResizeObserver === 'undefined') {
379
+ window.addEventListener('resize', updateSize);
380
+ return () => window.removeEventListener('resize', updateSize);
381
+ }
382
+ const observer = new ResizeObserver(updateSize);
383
+ observer.observe(svg);
384
+ return () => observer.disconnect();
385
+ }, []);
386
+ useClientLayoutEffect(() => {
387
+ presentRotation(rotationDegRef.current);
388
+ }, [presentRotation]);
389
+ useEffect(() => {
390
+ if (!synchronizeNavigation)
391
+ return;
392
+ const scheduler = navigationSchedulerRef.current;
393
+ if (!scheduler)
394
+ return;
395
+ const receivePose = (pose) => {
396
+ latestNavigationPoseRef.current = pose;
397
+ if (navigationVisible)
398
+ scheduler.update(pose);
399
+ else
400
+ applyNavigationPresentationRef.current(pose);
401
+ };
402
+ const unsubscribeCamera = subscribeFloorplanCameraNavigation(receivePose);
403
+ const unsubscribeStored = subscribeNavigationSyncPose((pose) => {
404
+ if (pose.source === '2d' && !navigationVisible)
405
+ receivePose(pose);
406
+ });
407
+ return () => {
408
+ unsubscribeCamera();
409
+ unsubscribeStored();
410
+ scheduler.discard();
411
+ };
412
+ }, [navigationVisible, synchronizeNavigation]);
413
+ useEffect(() => {
414
+ if (!(synchronizeNavigation && navigationVisible && latestNavigationPoseRef.current))
415
+ return;
416
+ navigationSchedulerRef.current?.update(latestNavigationPoseRef.current);
417
+ }, [navigationVisible, synchronizeNavigation]);
418
+ const chooseLevel = useCallback((nextLevelId) => {
419
+ setInternalLevelId(nextLevelId);
420
+ onLevelChange?.(nextLevelId);
421
+ }, [onLevelChange]);
422
+ const updateLocalViewBox = useCallback((update, commit = true) => {
423
+ const current = viewBoxRef.current;
424
+ const next = typeof update === 'function' ? update(current) : update;
425
+ presentViewBox(next);
426
+ if (commit)
427
+ setViewBox(next);
428
+ publishNavigation(next, rotationDegRef.current);
429
+ return next;
430
+ }, [presentViewBox, publishNavigation]);
431
+ const zoom = useCallback((factor, anchorX = 0.5, anchorY = 0.5) => {
432
+ updateLocalViewBox((current) => scaleFloorplanViewBox(current, factor, anchorX, anchorY));
433
+ }, [updateLocalViewBox]);
434
+ const onWheel = useCallback((event) => {
435
+ event.preventDefault();
436
+ const rect = event.currentTarget.getBoundingClientRect();
437
+ updateLocalViewBox((current) => scaleFloorplanViewBoxBetweenClients(current, event.deltaY > 0 ? 1.12 : 0.88, rect, [event.clientX, event.clientY], [event.clientX, event.clientY]));
438
+ }, [updateLocalViewBox]);
439
+ const onPointerDown = useCallback((event) => {
440
+ if (event.pointerType === 'mouse' && event.button !== 0 && event.button !== 2)
441
+ return;
442
+ event.preventDefault();
443
+ interactionRef.current?.focus();
444
+ try {
445
+ event.currentTarget.setPointerCapture(event.pointerId);
446
+ }
447
+ catch {
448
+ // Pointer capture is an enhancement; the gesture still works while events stay over the SVG.
449
+ }
450
+ const point = { x: event.clientX, y: event.clientY };
451
+ pointersRef.current.set(event.pointerId, point);
452
+ const pointers = Array.from(pointersRef.current.entries());
453
+ if (pointers.length === 1) {
454
+ dragRef.current = {
455
+ mode: event.pointerType === 'mouse' && event.button === 2 ? 'rotate' : 'pan',
456
+ pointerId: event.pointerId,
457
+ point,
458
+ rotationDeg: rotationDegRef.current,
459
+ viewBox: viewBoxRef.current,
460
+ };
461
+ pinchRef.current = null;
462
+ }
463
+ else {
464
+ const [first, second] = pointers;
465
+ if (!first || !second)
466
+ return;
467
+ const dx = second[1].x - first[1].x;
468
+ const dy = second[1].y - first[1].y;
469
+ pinchRef.current = {
470
+ pointerIds: [first[0], second[0]],
471
+ midpoint: { x: (first[1].x + second[1].x) / 2, y: (first[1].y + second[1].y) / 2 },
472
+ distance: Math.max(Math.hypot(dx, dy), 1),
473
+ viewBox: viewBoxRef.current,
474
+ };
475
+ dragRef.current = null;
476
+ }
477
+ setIsPanning(true);
478
+ setIsRotating(dragRef.current?.mode === 'rotate');
479
+ }, []);
480
+ const onPointerMove = useCallback((event) => {
481
+ if (!pointersRef.current.has(event.pointerId))
482
+ return;
483
+ pointersRef.current.set(event.pointerId, { x: event.clientX, y: event.clientY });
484
+ const rect = event.currentTarget.getBoundingClientRect();
485
+ const pinch = pinchRef.current;
486
+ if (pinch) {
487
+ const first = pointersRef.current.get(pinch.pointerIds[0]);
488
+ const second = pointersRef.current.get(pinch.pointerIds[1]);
489
+ if (!(first && second))
490
+ return;
491
+ const dx = second.x - first.x;
492
+ const dy = second.y - first.y;
493
+ const midpoint = { x: (first.x + second.x) / 2, y: (first.y + second.y) / 2 };
494
+ updateLocalViewBox(scaleFloorplanViewBoxBetweenClients(pinch.viewBox, pinch.distance / Math.max(Math.hypot(dx, dy), 1), rect, [pinch.midpoint.x, pinch.midpoint.y], [midpoint.x, midpoint.y]), false);
495
+ return;
496
+ }
497
+ const drag = dragRef.current;
498
+ if (!drag || drag.pointerId !== event.pointerId)
499
+ return;
500
+ if (drag.mode === 'rotate') {
501
+ const nextRotationDeg = drag.rotationDeg + (event.clientX - drag.point.x) * 0.35;
502
+ const initialSceneRotationDeg = FLOORPLAN_VIEW_ROTATION_DEG + drag.rotationDeg - buildingRotationDeg;
503
+ const nextSceneRotationDeg = FLOORPLAN_VIEW_ROTATION_DEG + nextRotationDeg - buildingRotationDeg;
504
+ const displayedCenter = {
505
+ x: drag.viewBox.x + drag.viewBox.width / 2,
506
+ y: drag.viewBox.y + drag.viewBox.height / 2,
507
+ };
508
+ const localCenter = rotateFloorplanPoint(displayedCenter, -initialSceneRotationDeg);
509
+ const nextCenter = rotateFloorplanPoint(localCenter, nextSceneRotationDeg);
510
+ const nextViewBox = {
511
+ ...drag.viewBox,
512
+ x: nextCenter.x - drag.viewBox.width / 2,
513
+ y: nextCenter.y - drag.viewBox.height / 2,
514
+ };
515
+ presentRotation(nextRotationDeg);
516
+ presentViewBox(nextViewBox);
517
+ publishNavigation(nextViewBox, nextRotationDeg);
518
+ return;
519
+ }
520
+ updateLocalViewBox(panFloorplanViewBox(drag.viewBox, rect, [drag.point.x, drag.point.y], [event.clientX, event.clientY]), false);
521
+ }, [buildingRotationDeg, presentRotation, presentViewBox, publishNavigation, updateLocalViewBox]);
522
+ const onPointerUp = useCallback((event) => {
523
+ pointersRef.current.delete(event.pointerId);
524
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) {
525
+ event.currentTarget.releasePointerCapture(event.pointerId);
526
+ }
527
+ pinchRef.current = null;
528
+ const [remaining] = pointersRef.current.entries();
529
+ if (remaining) {
530
+ dragRef.current = {
531
+ mode: 'pan',
532
+ pointerId: remaining[0],
533
+ point: remaining[1],
534
+ rotationDeg: rotationDegRef.current,
535
+ viewBox: viewBoxRef.current,
536
+ };
537
+ setIsRotating(false);
538
+ }
539
+ else {
540
+ dragRef.current = null;
541
+ setIsPanning(false);
542
+ setIsRotating(false);
543
+ setViewBox(viewBoxRef.current);
544
+ setRotationDeg(rotationDegRef.current);
545
+ }
546
+ }, []);
547
+ const onKeyDown = useCallback((event) => {
548
+ const panStep = 0.08;
549
+ switch (event.key) {
550
+ case '+':
551
+ case '=':
552
+ event.preventDefault();
553
+ zoom(0.8);
554
+ return;
555
+ case '-':
556
+ case '_':
557
+ event.preventDefault();
558
+ zoom(1.2);
559
+ return;
560
+ case '0':
561
+ case 'f':
562
+ case 'F':
563
+ event.preventDefault();
564
+ updateLocalViewBox(fittedViewBox);
565
+ return;
566
+ case 'ArrowLeft':
567
+ event.preventDefault();
568
+ updateLocalViewBox((current) => ({
569
+ ...current,
570
+ x: current.x - current.width * panStep,
571
+ }));
572
+ return;
573
+ case 'ArrowRight':
574
+ event.preventDefault();
575
+ updateLocalViewBox((current) => ({
576
+ ...current,
577
+ x: current.x + current.width * panStep,
578
+ }));
579
+ return;
580
+ case 'ArrowUp':
581
+ event.preventDefault();
582
+ updateLocalViewBox((current) => ({
583
+ ...current,
584
+ y: current.y - current.height * panStep,
585
+ }));
586
+ return;
587
+ case 'ArrowDown':
588
+ event.preventDefault();
589
+ updateLocalViewBox((current) => ({
590
+ ...current,
591
+ y: current.y + current.height * panStep,
592
+ }));
593
+ }
594
+ }, [fittedViewBox, updateLocalViewBox, zoom]);
595
+ const alignToNorth = useCallback(() => {
596
+ const currentRotationDeg = rotationDegRef.current;
597
+ const nextRotationDeg = nearestEquivalentDegrees(0, currentRotationDeg);
598
+ if (!navigationVisible) {
599
+ const pose = latestNavigationPoseRef.current;
600
+ if (!pose)
601
+ return;
602
+ publishNavigationSyncPose({
603
+ source: '2d',
604
+ target: [...pose.target],
605
+ azimuth: cameraAzimuthFromFloorplanRotation(nextRotationDeg),
606
+ viewWidth: pose.viewWidth,
607
+ });
608
+ return;
609
+ }
610
+ const currentViewBox = viewBoxRef.current;
611
+ const currentSceneRotationDeg = FLOORPLAN_VIEW_ROTATION_DEG + currentRotationDeg - buildingRotationDeg;
612
+ const nextSceneRotationDeg = FLOORPLAN_VIEW_ROTATION_DEG + nextRotationDeg - buildingRotationDeg;
613
+ const currentCenter = {
614
+ x: currentViewBox.x + currentViewBox.width / 2,
615
+ y: currentViewBox.y + currentViewBox.height / 2,
616
+ };
617
+ const localCenter = rotateFloorplanPoint(currentCenter, -currentSceneRotationDeg);
618
+ const nextCenter = rotateFloorplanPoint(localCenter, nextSceneRotationDeg);
619
+ const nextViewBox = {
620
+ ...currentViewBox,
621
+ x: nextCenter.x - currentViewBox.width / 2,
622
+ y: nextCenter.y - currentViewBox.height / 2,
623
+ };
624
+ commitPresentation(nextViewBox, nextRotationDeg);
625
+ publishNavigation(nextViewBox, nextRotationDeg);
626
+ }, [buildingRotationDeg, commitPresentation, navigationVisible, publishNavigation]);
627
+ const screenUnitsPerPixel = Math.max(viewBox.width / Math.max(viewportSize.width, 1), viewBox.height / Math.max(viewportSize.height, 1));
628
+ const compassControl = (_jsx(FloorplanCompassButton, { needleRef: compassNeedleRef, northRotationDeg: rotationDeg, onAlignNorth: alignToNorth }));
629
+ if (levels.length === 0) {
630
+ return (_jsx("div", { className: className, style: { display: 'grid', placeItems: 'center', background: '#f8fafc', color: '#64748b' }, children: i18n._({
631
+ id: 'viewer.floorplan.empty',
632
+ message: 'No floor plans are available for this scene.',
633
+ }) }));
634
+ }
635
+ return (_jsxs("div", { className: className, style: { position: 'relative', overflow: 'hidden', background: '#f8fafc' }, children: [synchronizeNavigation ? _jsx(FloorplanCameraSyncMount, {}) : null, _jsx("div", { "aria-keyshortcuts": "ArrowUp ArrowDown ArrowLeft ArrowRight + - 0 F", "aria-label": i18n._({
636
+ id: 'viewer.floorplan.viewLabel',
637
+ message: '{floor} 2D view',
638
+ values: {
639
+ floor: activeLevel
640
+ ? localizedLevelName(activeLevel, i18n, locale)
641
+ : i18n._({ id: 'viewer.floorplan.floorPlan', message: 'Floor plan' }),
642
+ },
643
+ }), onKeyDown: onKeyDown, role: "application", ref: interactionRef, style: { width: '100%', height: '100%' },
644
+ // biome-ignore lint/a11y/noNoninteractiveTabindex: The plan canvas supports keyboard pan, zoom, and fit controls.
645
+ tabIndex: 0, children: _jsxs("svg", { "aria-hidden": "true", "data-floorplan-preview": "", onContextMenu: (event) => event.preventDefault(), onPointerCancel: onPointerUp, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: onPointerUp, onWheel: onWheel, preserveAspectRatio: "xMidYMid meet", ref: svgRef, style: {
646
+ width: '100%',
647
+ height: '100%',
648
+ cursor: isRotating ? 'ew-resize' : isPanning ? 'grabbing' : 'grab',
649
+ touchAction: 'none',
650
+ }, viewBox: `${viewBox.x} ${viewBox.y} ${viewBox.width} ${viewBox.height}`, children: [_jsx("defs", { children: _jsx("pattern", { height: "1", id: gridPatternId, patternUnits: "userSpaceOnUse", width: "1", children: _jsx("path", { d: "M 1 0 L 0 0 0 1", fill: "none", stroke: "#cbd5e1", strokeOpacity: "0.32", strokeWidth: "0.012" }) }) }), _jsx("rect", { fill: `url(#${gridPatternId})`, height: viewBox.height, ref: gridRef, width: viewBox.width, x: viewBox.x, y: viewBox.y }), _jsx("g", { pointerEvents: "none", ref: sceneRef, transform: FLOORPLAN_VIEW_ROTATION_DEG + rotationDeg - buildingRotationDeg === 0
651
+ ? undefined
652
+ : `rotate(${FLOORPLAN_VIEW_ROTATION_DEG + rotationDeg - buildingRotationDeg})`, children: renderEntries.map((entry) => (_jsx("g", { "data-floorplan-node-id": entry.id, ref: (element) => {
653
+ if (element)
654
+ fitElementRefs.current.set(entry.id, element);
655
+ else
656
+ fitElementRefs.current.delete(entry.id);
657
+ }, children: _jsx(FloorplanGeometryRenderer, { geometry: entry.geometry, pointerEventsOverride: "none", screenUnitsPerPixel: screenUnitsPerPixel }) }, entry.id))) })] }) }), showLevelSelector && levels.length > 1 ? (_jsxs("label", { style: {
658
+ position: 'absolute',
659
+ bottom: 16,
660
+ left: 64,
661
+ display: 'grid',
662
+ gap: 4,
663
+ color: '#475569',
664
+ fontSize: 11,
665
+ fontWeight: 600,
666
+ }, children: [i18n._({ id: 'viewer.floorplan.floor', message: 'Floor' }), _jsx("select", { "aria-label": i18n._({ id: 'viewer.floorplan.floor', message: 'Floor' }), onChange: (event) => chooseLevel(event.target.value), style: {
667
+ border: '1px solid rgba(148,163,184,.55)',
668
+ borderRadius: 999,
669
+ background: 'rgba(255,255,255,.94)',
670
+ padding: '8px 30px 8px 12px',
671
+ color: '#0f172a',
672
+ boxShadow: '0 8px 24px rgba(15,23,42,.10)',
673
+ }, value: activeLevelId ?? '', children: levels.map((level) => (_jsx("option", { value: level.id, children: localizedLevelName(level, i18n, locale) }, level.id))) })] })) : null, showCompass
674
+ ? compassHost
675
+ ? createPortal(compassControl, compassHost)
676
+ : compassControl
677
+ : null, _jsxs("div", { style: {
678
+ position: 'absolute',
679
+ right: 16,
680
+ bottom: 16,
681
+ display: 'flex',
682
+ gap: 4,
683
+ border: '1px solid rgba(148,163,184,.45)',
684
+ borderRadius: 999,
685
+ background: 'rgba(255,255,255,.94)',
686
+ padding: 4,
687
+ boxShadow: '0 8px 24px rgba(15,23,42,.10)',
688
+ }, children: [_jsx("button", { "aria-label": i18n._({ id: 'viewer.floorplan.zoomOut', message: 'Zoom out' }), onClick: () => zoom(1.2), style: controlStyle, title: i18n._({ id: 'viewer.floorplan.zoomOut', message: 'Zoom out' }), type: "button", children: _jsx(Minus, { size: 16 }) }), _jsx("button", { "aria-label": i18n._({ id: 'viewer.floorplan.fit', message: 'Fit floor plan' }), onClick: () => updateLocalViewBox(fittedViewBox), style: controlStyle, title: i18n._({ id: 'viewer.floorplan.fit', message: 'Fit floor plan' }), type: "button", children: _jsx(Maximize2, { size: 15 }) }), _jsx("button", { "aria-label": i18n._({ id: 'viewer.floorplan.zoomIn', message: 'Zoom in' }), onClick: () => zoom(0.8), style: controlStyle, title: i18n._({ id: 'viewer.floorplan.zoomIn', message: 'Zoom in' }), type: "button", children: _jsx(Plus, { size: 16 }) })] })] }));
689
+ }
690
+ const controlStyle = {
691
+ display: 'grid',
692
+ width: 32,
693
+ height: 32,
694
+ placeItems: 'center',
695
+ border: 0,
696
+ borderRadius: 999,
697
+ background: 'transparent',
698
+ color: '#334155',
699
+ cursor: 'pointer',
700
+ };