@pascal-app/viewer 1.1.0 → 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 (224) hide show
  1. package/README.md +157 -0
  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-walkthrough-controller.d.ts +1 -1
  17. package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -1
  18. package/dist/components/viewer/glb-walkthrough-controller.js +3 -4
  19. package/dist/components/viewer/live-walkthrough-controller.d.ts +17 -0
  20. package/dist/components/viewer/live-walkthrough-controller.d.ts.map +1 -0
  21. package/dist/components/viewer/live-walkthrough-controller.js +1449 -0
  22. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  23. package/dist/components/viewer/post-processing.js +22 -1
  24. package/dist/index.d.ts +3 -0
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +3 -0
  27. package/dist/lib/daylight/room-map.d.ts +29 -4
  28. package/dist/lib/daylight/room-map.d.ts.map +1 -1
  29. package/dist/lib/daylight/room-map.js +219 -36
  30. package/dist/lib/daylight/room-scene.d.ts.map +1 -1
  31. package/dist/lib/daylight/room-scene.js +9 -3
  32. package/dist/lib/daylight/rooms.d.ts +15 -6
  33. package/dist/lib/daylight/rooms.d.ts.map +1 -1
  34. package/dist/lib/daylight/rooms.js +49 -9
  35. package/dist/lib/measurement-presentation.d.ts +2 -0
  36. package/dist/lib/measurement-presentation.d.ts.map +1 -1
  37. package/dist/lib/measurement-presentation.js +6 -0
  38. package/dist/lib/typing-target.d.ts +2 -0
  39. package/dist/lib/typing-target.d.ts.map +1 -0
  40. package/dist/lib/typing-target.js +7 -0
  41. package/dist/lib/viewer-config.d.ts +6 -0
  42. package/dist/lib/viewer-config.d.ts.map +1 -1
  43. package/dist/lib/viewer-config.js +2 -0
  44. package/dist/sfx/audio-settings.d.ts +81 -0
  45. package/dist/sfx/audio-settings.d.ts.map +1 -0
  46. package/dist/sfx/audio-settings.js +166 -0
  47. package/dist/sfx/index.d.ts +6 -0
  48. package/dist/sfx/index.d.ts.map +1 -0
  49. package/dist/sfx/index.js +5 -0
  50. package/dist/sfx/interaction-sounds.d.ts +7 -0
  51. package/dist/sfx/interaction-sounds.d.ts.map +1 -0
  52. package/dist/sfx/interaction-sounds.js +39 -0
  53. package/dist/sfx/player.d.ts +31 -0
  54. package/dist/sfx/player.d.ts.map +1 -0
  55. package/dist/sfx/player.js +255 -0
  56. package/dist/sfx/sounds.d.ts +60 -0
  57. package/dist/sfx/sounds.d.ts.map +1 -0
  58. package/dist/sfx/sounds.js +25 -0
  59. package/dist/sfx/use-viewer-ui-sounds.d.ts +8 -0
  60. package/dist/sfx/use-viewer-ui-sounds.d.ts.map +1 -0
  61. package/dist/sfx/use-viewer-ui-sounds.js +42 -0
  62. package/dist/store/use-first-person-hud.d.ts +13 -0
  63. package/dist/store/use-first-person-hud.d.ts.map +1 -0
  64. package/dist/store/use-first-person-hud.js +21 -0
  65. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  66. package/dist/systems/roof/roof-system.js +27 -13
  67. package/dist/ui/components/action-button.d.ts +14 -0
  68. package/dist/ui/components/action-button.d.ts.map +1 -0
  69. package/dist/ui/components/action-button.js +21 -0
  70. package/dist/ui/components/action-menu-placement.d.ts +7 -0
  71. package/dist/ui/components/action-menu-placement.d.ts.map +1 -0
  72. package/dist/ui/components/action-menu-placement.js +10 -0
  73. package/dist/ui/components/floorplan-compass-button.d.ts +10 -0
  74. package/dist/ui/components/floorplan-compass-button.d.ts.map +1 -0
  75. package/dist/ui/components/floorplan-compass-button.js +22 -0
  76. package/dist/ui/components/follow-pascal-toggle.d.ts +6 -0
  77. package/dist/ui/components/follow-pascal-toggle.d.ts.map +1 -0
  78. package/dist/ui/components/follow-pascal-toggle.js +21 -0
  79. package/dist/ui/components/localized-html.d.ts +9 -0
  80. package/dist/ui/components/localized-html.d.ts.map +1 -0
  81. package/dist/ui/components/localized-html.js +16 -0
  82. package/dist/ui/components/render-settings-menu.d.ts +9 -0
  83. package/dist/ui/components/render-settings-menu.d.ts.map +1 -0
  84. package/dist/ui/components/render-settings-menu.js +145 -0
  85. package/dist/ui/components/rooms/room-highlight.d.ts +10 -0
  86. package/dist/ui/components/rooms/room-highlight.d.ts.map +1 -0
  87. package/dist/ui/components/rooms/room-highlight.js +56 -0
  88. package/dist/ui/components/rooms/room-labels.d.ts +278 -0
  89. package/dist/ui/components/rooms/room-labels.d.ts.map +1 -0
  90. package/dist/ui/components/rooms/room-labels.js +1295 -0
  91. package/dist/ui/components/rooms/viewer-rooms.d.ts +14 -0
  92. package/dist/ui/components/rooms/viewer-rooms.d.ts.map +1 -0
  93. package/dist/ui/components/rooms/viewer-rooms.js +123 -0
  94. package/dist/ui/components/scene-loader.d.ts +18 -0
  95. package/dist/ui/components/scene-loader.d.ts.map +1 -0
  96. package/dist/ui/components/scene-loader.js +29 -0
  97. package/dist/ui/components/sky-control.d.ts +37 -0
  98. package/dist/ui/components/sky-control.d.ts.map +1 -0
  99. package/dist/ui/components/sky-control.js +382 -0
  100. package/dist/ui/components/use-viewer-camera-navigation-sync.d.ts +4 -0
  101. package/dist/ui/components/use-viewer-camera-navigation-sync.d.ts.map +1 -0
  102. package/dist/ui/components/use-viewer-camera-navigation-sync.js +117 -0
  103. package/dist/ui/components/viewer-controls-bar.d.ts +20 -0
  104. package/dist/ui/components/viewer-controls-bar.d.ts.map +1 -0
  105. package/dist/ui/components/viewer-controls-bar.js +130 -0
  106. package/dist/ui/components/viewer-hover-label.d.ts +14 -0
  107. package/dist/ui/components/viewer-hover-label.d.ts.map +1 -0
  108. package/dist/ui/components/viewer-hover-label.js +28 -0
  109. package/dist/ui/components/viewer-scene-header.d.ts +34 -0
  110. package/dist/ui/components/viewer-scene-header.d.ts.map +1 -0
  111. package/dist/ui/components/viewer-scene-header.js +92 -0
  112. package/dist/ui/components/viewer-selection-manager.d.ts +7 -0
  113. package/dist/ui/components/viewer-selection-manager.d.ts.map +1 -0
  114. package/dist/ui/components/viewer-selection-manager.js +116 -0
  115. package/dist/ui/components/viewer-stage-modes.d.ts +7 -0
  116. package/dist/ui/components/viewer-stage-modes.d.ts.map +1 -0
  117. package/dist/ui/components/viewer-stage-modes.js +42 -0
  118. package/dist/ui/components/viewer-stage-switcher.d.ts +10 -0
  119. package/dist/ui/components/viewer-stage-switcher.d.ts.map +1 -0
  120. package/dist/ui/components/viewer-stage-switcher.js +16 -0
  121. package/dist/ui/components/viewer-stage.d.ts +25 -0
  122. package/dist/ui/components/viewer-stage.d.ts.map +1 -0
  123. package/dist/ui/components/viewer-stage.js +107 -0
  124. package/dist/ui/components/walkthrough-hud.d.ts +29 -0
  125. package/dist/ui/components/walkthrough-hud.d.ts.map +1 -0
  126. package/dist/ui/components/walkthrough-hud.js +55 -0
  127. package/dist/ui/floorplan/floorplan-camera-sync.d.ts +22 -0
  128. package/dist/ui/floorplan/floorplan-camera-sync.d.ts.map +1 -0
  129. package/dist/ui/floorplan/floorplan-camera-sync.js +157 -0
  130. package/dist/ui/floorplan/floorplan-dimension-renderer.d.ts +61 -0
  131. package/dist/ui/floorplan/floorplan-dimension-renderer.d.ts.map +1 -0
  132. package/dist/ui/floorplan/floorplan-dimension-renderer.js +280 -0
  133. package/dist/ui/floorplan/floorplan-geometry-renderer.d.ts +54 -0
  134. package/dist/ui/floorplan/floorplan-geometry-renderer.d.ts.map +1 -0
  135. package/dist/ui/floorplan/floorplan-geometry-renderer.js +317 -0
  136. package/dist/ui/floorplan/floorplan-label-angle.d.ts +24 -0
  137. package/dist/ui/floorplan/floorplan-label-angle.d.ts.map +1 -0
  138. package/dist/ui/floorplan/floorplan-label-angle.js +60 -0
  139. package/dist/ui/floorplan/floorplan-navigation-sync.d.ts +18 -0
  140. package/dist/ui/floorplan/floorplan-navigation-sync.d.ts.map +1 -0
  141. package/dist/ui/floorplan/floorplan-navigation-sync.js +41 -0
  142. package/dist/ui/floorplan/floorplan-preview-geometry.d.ts +26 -0
  143. package/dist/ui/floorplan/floorplan-preview-geometry.d.ts.map +1 -0
  144. package/dist/ui/floorplan/floorplan-preview-geometry.js +173 -0
  145. package/dist/ui/floorplan/floorplan-preview-navigation.d.ts +22 -0
  146. package/dist/ui/floorplan/floorplan-preview-navigation.d.ts.map +1 -0
  147. package/dist/ui/floorplan/floorplan-preview-navigation.js +41 -0
  148. package/dist/ui/floorplan/floorplan-preview-visibility.d.ts +8 -0
  149. package/dist/ui/floorplan/floorplan-preview-visibility.d.ts.map +1 -0
  150. package/dist/ui/floorplan/floorplan-preview-visibility.js +21 -0
  151. package/dist/ui/floorplan/floorplan-preview.d.ts +19 -0
  152. package/dist/ui/floorplan/floorplan-preview.d.ts.map +1 -0
  153. package/dist/ui/floorplan/floorplan-preview.js +700 -0
  154. package/dist/ui/hooks/use-mobile.d.ts +2 -0
  155. package/dist/ui/hooks/use-mobile.d.ts.map +1 -0
  156. package/dist/ui/hooks/use-mobile.js +14 -0
  157. package/dist/ui/index.d.ts +49 -0
  158. package/dist/ui/index.d.ts.map +1 -0
  159. package/dist/ui/index.js +48 -0
  160. package/dist/ui/lib/below-grade.d.ts +38 -0
  161. package/dist/ui/lib/below-grade.d.ts.map +1 -0
  162. package/dist/ui/lib/below-grade.js +102 -0
  163. package/dist/ui/lib/camera-pose.d.ts +19 -0
  164. package/dist/ui/lib/camera-pose.d.ts.map +1 -0
  165. package/dist/ui/lib/camera-pose.js +113 -0
  166. package/dist/ui/lib/cn.d.ts +3 -0
  167. package/dist/ui/lib/cn.d.ts.map +1 -0
  168. package/dist/ui/lib/cn.js +5 -0
  169. package/dist/ui/lib/level-name.d.ts +10 -0
  170. package/dist/ui/lib/level-name.d.ts.map +1 -0
  171. package/dist/ui/lib/level-name.js +33 -0
  172. package/dist/ui/lib/node-display-label.d.ts +8 -0
  173. package/dist/ui/lib/node-display-label.d.ts.map +1 -0
  174. package/dist/ui/lib/node-display-label.js +36 -0
  175. package/dist/ui/lib/room-area.d.ts +3 -0
  176. package/dist/ui/lib/room-area.d.ts.map +1 -0
  177. package/dist/ui/lib/room-area.js +8 -0
  178. package/dist/ui/lib/room-assembly-overlay.d.ts +46 -0
  179. package/dist/ui/lib/room-assembly-overlay.d.ts.map +1 -0
  180. package/dist/ui/lib/room-assembly-overlay.js +222 -0
  181. package/dist/ui/lib/room-name-messages.d.ts +28 -0
  182. package/dist/ui/lib/room-name-messages.d.ts.map +1 -0
  183. package/dist/ui/lib/room-name-messages.js +112 -0
  184. package/dist/ui/lib/room-selection.d.ts +58 -0
  185. package/dist/ui/lib/room-selection.d.ts.map +1 -0
  186. package/dist/ui/lib/room-selection.js +297 -0
  187. package/dist/ui/lib/scene-theme-name.d.ts +4 -0
  188. package/dist/ui/lib/scene-theme-name.d.ts.map +1 -0
  189. package/dist/ui/lib/scene-theme-name.js +22 -0
  190. package/dist/ui/lib/selection-modifiers.d.ts +20 -0
  191. package/dist/ui/lib/selection-modifiers.d.ts.map +1 -0
  192. package/dist/ui/lib/selection-modifiers.js +15 -0
  193. package/dist/ui/lib/viewer-selection.d.ts +132 -0
  194. package/dist/ui/lib/viewer-selection.d.ts.map +1 -0
  195. package/dist/ui/lib/viewer-selection.js +462 -0
  196. package/dist/ui/lib/zone-content.d.ts +6 -0
  197. package/dist/ui/lib/zone-content.d.ts.map +1 -0
  198. package/dist/ui/lib/zone-content.js +96 -0
  199. package/dist/ui/primitives/button.d.ts +11 -0
  200. package/dist/ui/primitives/button.d.ts.map +1 -0
  201. package/dist/ui/primitives/button.js +35 -0
  202. package/dist/ui/primitives/dropdown-menu.d.ts +26 -0
  203. package/dist/ui/primitives/dropdown-menu.d.ts.map +1 -0
  204. package/dist/ui/primitives/dropdown-menu.js +53 -0
  205. package/dist/ui/primitives/popover.d.ts +8 -0
  206. package/dist/ui/primitives/popover.d.ts.map +1 -0
  207. package/dist/ui/primitives/popover.js +17 -0
  208. package/dist/ui/primitives/switch.d.ts +5 -0
  209. package/dist/ui/primitives/switch.d.ts.map +1 -0
  210. package/dist/ui/primitives/switch.js +8 -0
  211. package/dist/ui/primitives/tooltip.d.ts +8 -0
  212. package/dist/ui/primitives/tooltip.d.ts.map +1 -0
  213. package/dist/ui/primitives/tooltip.js +17 -0
  214. package/dist/ui/store/camera-pose-store.d.ts +17 -0
  215. package/dist/ui/store/camera-pose-store.d.ts.map +1 -0
  216. package/dist/ui/store/camera-pose-store.js +15 -0
  217. package/dist/ui/store/navigation-sync-pose-store.d.ts +26 -0
  218. package/dist/ui/store/navigation-sync-pose-store.d.ts.map +1 -0
  219. package/dist/ui/store/navigation-sync-pose-store.js +16 -0
  220. package/dist/ui/store/use-follow-pascal.d.ts +30 -0
  221. package/dist/ui/store/use-follow-pascal.d.ts.map +1 -0
  222. package/dist/ui/store/use-follow-pascal.js +28 -0
  223. package/dist/ui/theme.css +339 -0
  224. package/package.json +34 -7
@@ -0,0 +1,14 @@
1
+ import { type ViewerSource } from '../../lib/viewer-selection';
2
+ /**
3
+ * Rooms on every viewer surface, as the editor shows them: the room pills on
4
+ * the current floor while nothing but the floor is selected, the room
5
+ * highlight on the selected or hovered room (or a focused unit's rooms), and
6
+ * the selected room's callout. Zones draw no tinted volume here, and their
7
+ * meshes are not mounted at all, so they never catch a click.
8
+ */
9
+ export declare function ViewerRooms({ source }: {
10
+ source: ViewerSource;
11
+ }): import("react").JSX.Element;
12
+ /** `ViewerRooms` over the parametric scene store. */
13
+ export declare function ParametricViewerRooms(): import("react").JSX.Element;
14
+ //# sourceMappingURL=viewer-rooms.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"viewer-rooms.d.ts","sourceRoot":"","sources":["../../../../src/ui/components/rooms/viewer-rooms.tsx"],"names":[],"mappings":"AAqBA,OAAO,EAEL,KAAK,YAAY,EAGlB,MAAM,4BAA4B,CAAA;AAanC;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,YAAY,CAAA;CAAE,+BAqI/D;AAsCD,qDAAqD;AACrD,wBAAgB,qBAAqB,gCAIpC"}
@@ -0,0 +1,123 @@
1
+ 'use client';
2
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { buildUnitReport, polygonInteriorPoint, roomFloorElevation, sceneRegistry, useLocale, usePascalI18n, useScene, } from '@pascal-app/core';
4
+ import { useCallback, useEffect, useMemo } from 'react';
5
+ import { Vector3 } from 'three';
6
+ import { useShallow } from 'zustand/react/shallow';
7
+ import { SelectionCallout } from '../../../components/viewer/selection-callout';
8
+ import useViewer from '../../../store/use-viewer';
9
+ import { formatRoomArea } from '../../lib/room-area';
10
+ import { resolveRoomAssemblyHeights } from '../../lib/room-assembly-overlay';
11
+ import { describeZoneOutline } from '../../lib/room-selection';
12
+ import { selectViewerRoom, viewerRooms, zoneLabel, } from '../../lib/viewer-selection';
13
+ import { RoomAssemblyMesh } from './room-highlight';
14
+ import { RoomLabelAnchor3D, RoomPillChip, RoomPillDetail, RoomPillsLayer3D, roomLabelEntries, roomLabelsVisible, useFadePresence, } from './room-labels';
15
+ /**
16
+ * Rooms on every viewer surface, as the editor shows them: the room pills on
17
+ * the current floor while nothing but the floor is selected, the room
18
+ * highlight on the selected or hovered room (or a focused unit's rooms), and
19
+ * the selected room's callout. Zones draw no tinted volume here, and their
20
+ * meshes are not mounted at all, so they never catch a click.
21
+ */
22
+ export function ViewerRooms({ source }) {
23
+ const i18n = usePascalI18n();
24
+ useEffect(() => {
25
+ const before = useViewer.getState().showZones;
26
+ useViewer.getState().setShowZones(false);
27
+ return () => useViewer.getState().setShowZones(before);
28
+ }, []);
29
+ const { levelId, zoneId, selectedIds } = useViewer(useShallow((s) => ({
30
+ levelId: s.selection.levelId,
31
+ zoneId: s.selection.zoneId,
32
+ selectedIds: s.selection.selectedIds,
33
+ })));
34
+ const focusedUnitId = useViewer((s) => s.focusedUnitId);
35
+ const hoveredId = useViewer((s) => s.hoveredId);
36
+ const walkthrough = useViewer((s) => s.walkthroughMode);
37
+ const { nodes } = source;
38
+ const pillsShown = !!levelId &&
39
+ !walkthrough &&
40
+ roomLabelsVisible({
41
+ zoneId,
42
+ focusedUnitId,
43
+ selectedTypes: selectedIds.map((id) => nodes[id]?.type),
44
+ });
45
+ // The layer stays mounted through its fade-out.
46
+ const pillsPresent = useFadePresence(pillsShown);
47
+ const rooms = useMemo(() => (levelId ? viewerRooms(levelId, nodes) : []), [levelId, nodes]);
48
+ const entries = useMemo(() => {
49
+ if (!levelId)
50
+ return [];
51
+ const zones = Object.values(nodes).filter((node) => node.type === 'zone' && node.parentId === levelId && node.visible !== false);
52
+ return roomLabelEntries(rooms.filter((room) => nodes[room.zoneId]?.visible !== false), zones);
53
+ }, [levelId, nodes, rooms]);
54
+ const highlighted = useMemo(() => {
55
+ if (walkthrough)
56
+ return [];
57
+ const ids = new Set();
58
+ if (zoneId)
59
+ ids.add(zoneId);
60
+ if (hoveredId && nodes[hoveredId]?.type === 'zone')
61
+ ids.add(hoveredId);
62
+ const unit = focusedUnitId ? nodes[focusedUnitId] : undefined;
63
+ if (unit?.type === 'unit') {
64
+ for (const member of buildUnitReport(unit, nodes).members)
65
+ if (member.levelId === levelId)
66
+ ids.add(member.zoneId);
67
+ }
68
+ return [...ids].flatMap((id) => {
69
+ const zone = nodes[id];
70
+ if (zone?.type !== 'zone' || !zone.parentId)
71
+ return [];
72
+ const detected = viewerRooms(zone.parentId, nodes).find((room) => room.zoneId === id);
73
+ return [detected ?? describeZoneOutline(zone.parentId, zone)];
74
+ });
75
+ }, [focusedUnitId, hoveredId, levelId, nodes, walkthrough, zoneId]);
76
+ return (_jsxs(_Fragment, { children: [pillsPresent && levelId ? (_jsx(RoomPillsLayer3D, { entries: entries, levelObject: () => source.objectFor(levelId), renderDetail: (entry) => {
77
+ const zone = nodes[entry.zoneId];
78
+ if (zone?.type !== 'zone')
79
+ return null;
80
+ return (_jsx(RoomPillDetail, { areaM2: entry.area, color: zone.color, name: zoneLabel(zone, i18n), view: "3d", zoneId: zone.id }));
81
+ }, renderLabel: (entry, anchors, elements) => {
82
+ const zone = nodes[entry.zoneId];
83
+ if (zone?.type !== 'zone')
84
+ return null;
85
+ return (_jsx(RoomLabelAnchor3D, { anchors: anchors, entry: entry, floorY: roomFloorElevation(nodes, entry.zoneId), children: _jsx(RoomPillChip, { areaM2: entry.area, color: zone.color, elements: elements, name: zoneLabel(zone, i18n), onClick: () => selectViewerRoom(zone.id, source), onHover: () => useViewer.getState().setHoveredId(zone.id), onLeave: () => {
86
+ if (useViewer.getState().hoveredId === zone.id)
87
+ useViewer.getState().setHoveredId(null);
88
+ }, view: "3d", zoneId: zone.id }) }));
89
+ }, shown: pillsShown }, levelId)) : null, highlighted.map((room) => (_jsx(ViewerRoomHighlight, { room: room, source: source }, room.zoneId))), zoneId && levelId && !walkthrough && selectedIds.length === 0
90
+ ? (() => {
91
+ const entry = entries.find((candidate) => candidate.zoneId === zoneId);
92
+ const zone = nodes[zoneId];
93
+ const level = source.objectFor(levelId);
94
+ return entry && zone?.type === 'zone' && level ? (_jsx(ViewerRoomCallout, { entry: entry, floorY: roomFloorElevation(nodes, zoneId), level: level, name: zoneLabel(zone, i18n) }, zoneId)) : null;
95
+ })()
96
+ : null] }));
97
+ }
98
+ /** The selected room's callout: its name and area at a point inside it, and the close button. */
99
+ function ViewerRoomCallout({ entry, floorY, level, name, }) {
100
+ const unit = useViewer((s) => s.unit);
101
+ const locale = useLocale();
102
+ const anchor = useMemo(() => {
103
+ const [x, z] = polygonInteriorPoint({ polygon: entry.polygon, holes: entry.holes });
104
+ return new Vector3(x, floorY + 0.05, z);
105
+ }, [entry.polygon, entry.holes, floorY]);
106
+ const dismiss = useCallback(() => {
107
+ const { selection, setSelection } = useViewer.getState();
108
+ if (selection.zoneId === entry.zoneId)
109
+ setSelection({ zoneId: null });
110
+ }, [entry.zoneId]);
111
+ return (_jsx(SelectionCallout, { actions: [], anchor: anchor, object: level, onDismiss: dismiss, subtitle: formatRoomArea(entry.area, unit, locale), title: name }));
112
+ }
113
+ const parametricObject = (id) => sceneRegistry.nodes.get(id);
114
+ /** `ViewerRooms` over the parametric scene store. */
115
+ export function ParametricViewerRooms() {
116
+ const nodes = useScene((s) => s.nodes);
117
+ const source = useMemo(() => ({ nodes, objectFor: parametricObject }), [nodes]);
118
+ return _jsx(ViewerRooms, { source: source });
119
+ }
120
+ function ViewerRoomHighlight({ room, source, }) {
121
+ const heights = useMemo(() => resolveRoomAssemblyHeights(room, source.nodes), [room, source.nodes]);
122
+ return (_jsx(RoomAssemblyMesh, { heights: heights, levelObject: () => source.objectFor(room.key.levelId), room: room }));
123
+ }
@@ -0,0 +1,18 @@
1
+ interface SceneLoaderProps {
2
+ className?: string;
3
+ fullScreen?: boolean;
4
+ }
5
+ export declare function SceneLoader({ className, fullScreen }: SceneLoaderProps): import("react").JSX.Element;
6
+ interface SceneLoadFailedProps {
7
+ className?: string;
8
+ onRetry: () => void;
9
+ }
10
+ /**
11
+ * Replaces the loader when the host could not deliver the scene. Rendered
12
+ * INSTEAD of falling back to an empty default scene: a session that shows
13
+ * scaffold nodes after a failed load autosaves that scaffold over the real
14
+ * project (prod scene-wipe class, 2026-09-02).
15
+ */
16
+ export declare function SceneLoadFailed({ className, onRetry }: SceneLoadFailedProps): import("react").JSX.Element;
17
+ export {};
18
+ //# sourceMappingURL=scene-loader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scene-loader.d.ts","sourceRoot":"","sources":["../../../src/ui/components/scene-loader.tsx"],"names":[],"mappings":"AAcA,UAAU,gBAAgB;IACxB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,OAAO,CAAA;CACrB;AAED,wBAAgB,WAAW,CAAC,EAAE,SAAS,EAAE,UAAkB,EAAE,EAAE,gBAAgB,+BAmB9E;AAED,UAAU,oBAAoB;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,EAAE,oBAAoB,+BAqB3E"}
@@ -0,0 +1,29 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useState } from 'react';
4
+ import { cn } from '../lib/cn';
5
+ import { Button } from '../primitives/button';
6
+ const LOADERS = [
7
+ 'pascal-loader-1',
8
+ 'pascal-loader-2',
9
+ 'pascal-loader-3',
10
+ 'pascal-loader-4',
11
+ 'pascal-loader-5',
12
+ ];
13
+ export function SceneLoader({ className, fullScreen = false }) {
14
+ const [loaderClass, setLoaderClass] = useState(LOADERS[0]);
15
+ useEffect(() => {
16
+ // Pick a random loader on mount
17
+ setLoaderClass(LOADERS[Math.floor(Math.random() * LOADERS.length)] ?? LOADERS[0]);
18
+ }, []);
19
+ return (_jsx("div", { className: cn('z-100 flex items-center justify-center bg-background/80 backdrop-blur-md transition-opacity duration-300', fullScreen ? 'fixed inset-0' : 'absolute inset-0', className), children: _jsx("div", { className: cn(loaderClass, 'text-foreground opacity-80') }) }));
20
+ }
21
+ /**
22
+ * Replaces the loader when the host could not deliver the scene. Rendered
23
+ * INSTEAD of falling back to an empty default scene: a session that shows
24
+ * scaffold nodes after a failed load autosaves that scaffold over the real
25
+ * project (prod scene-wipe class, 2026-09-02).
26
+ */
27
+ export function SceneLoadFailed({ className, onRetry }) {
28
+ return (_jsxs("div", { className: cn('z-100 flex flex-col items-center justify-center gap-4 bg-background/90 px-6 text-center backdrop-blur-md', 'absolute inset-0', className), role: "alert", children: [_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("p", { className: "font-medium text-foreground text-sm", children: "This project couldn't be loaded" }), _jsx("p", { className: "text-muted-foreground text-sm", children: "Nothing was changed. Check your connection and try again." })] }), _jsx(Button, { className: "rounded-full", onClick: onRetry, size: "sm", type: "button", children: "Try again" })] }));
29
+ }
@@ -0,0 +1,37 @@
1
+ import { type PascalI18n } from '@pascal-app/core';
2
+ import { type CSSProperties } from 'react';
3
+ import { type Weather } from '../../lib/daylight/sky';
4
+ /**
5
+ * The sky (time of day and weather) as a chip beside the compass, opening a panel: the sun's
6
+ * arc to drag through the day, the presets, the live clock, and the cloud cover. While a chip is
7
+ * mounted the display menus point here instead of carrying their own time and weather menus.
8
+ */
9
+ export declare const useSkyPanel: import("zustand").UseBoundStore<import("zustand").StoreApi<{
10
+ open: boolean;
11
+ hosts: number;
12
+ }>>;
13
+ export declare function openSkyPanel(): void;
14
+ /**
15
+ * The bottom docks the chip lines up with (measured): the editor's tool dock and the viewer's
16
+ * controls bar, both 24 px up. The compass beside the chip centres on them.
17
+ */
18
+ export declare const SKY_DOCK_BOTTOM = 24;
19
+ export declare const SKY_DOCK_HEIGHT: {
20
+ readonly editor: 58;
21
+ readonly viewer: 56;
22
+ };
23
+ export declare const SKY_COMPASS_BOTTOM = 36;
24
+ export declare function weatherIcon(weather: Weather): import("lucide-react").LucideIcon;
25
+ export declare function weatherName(i18n: PascalI18n, id: Weather): string;
26
+ /**
27
+ * The chip and its panel, absolutely placed by the caller (the scene's bottom-left corner, beside
28
+ * the compass). `dark` for the viewer's dark chrome.
29
+ */
30
+ export declare function SkyControl({ className, dark, height, style, }: {
31
+ className?: string;
32
+ dark?: boolean;
33
+ /** The dock's height beside it, so the two line up. */
34
+ height: number;
35
+ style?: CSSProperties;
36
+ }): import("react").JSX.Element;
37
+ //# sourceMappingURL=sky-control.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sky-control.d.ts","sourceRoot":"","sources":["../../../src/ui/components/sky-control.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,UAAU,EAA2B,MAAM,kBAAkB,CAAA;AAa3E,OAAO,EACL,KAAK,aAAa,EAOnB,MAAM,OAAO,CAAA;AAGd,OAAO,EAAiB,KAAK,OAAO,EAAE,MAAM,wBAAwB,CAAA;AAgBpE;;;;GAIG;AACH,eAAO,MAAM,WAAW;UAAkB,OAAO;WAAS,MAAM;GAG7D,CAAA;AAEH,wBAAgB,YAAY,SAE3B;AAED;;;GAGG;AACH,eAAO,MAAM,eAAe,KAAK,CAAA;AACjC,eAAO,MAAM,eAAe;aAAK,MAAM,EAAE,EAAE;aAAE,MAAM,EAAE,EAAE;CAAW,CAAA;AAClE,eAAO,MAAM,kBAAkB,KAAK,CAAA;AAqDpC,wBAAgB,WAAW,CAAC,OAAO,EAAE,OAAO,qCAE3C;AAgCD,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,EAAE,EAAE,EAAE,OAAO,UAWxD;AAkFD;;;GAGG;AACH,wBAAgB,UAAU,CAAC,EACzB,SAAS,EACT,IAAY,EACZ,MAAM,EACN,KAAK,GACN,EAAE;IACD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,uDAAuD;IACvD,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,aAAa,CAAA;CACtB,+BAmFA"}
@@ -0,0 +1,382 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { usePascalI18n, useScene } from '@pascal-app/core';
4
+ import { Cloud, CloudSun, Cloudy, LocateFixed, Moon, RotateCcw, Sun, Sunrise, Sunset, X, } from 'lucide-react';
5
+ import { useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
6
+ import { create } from 'zustand';
7
+ import { requestDeviceLocation, resolveLocation } from '../../lib/daylight/location';
8
+ import { WEATHER_COVER } from '../../lib/daylight/sky';
9
+ import { presetInstant, skyPhase, solarClockInstant, solarClockMinutes, solarPosition, sunEvents, } from '../../lib/daylight/solar';
10
+ import useViewer from '../../store/use-viewer';
11
+ import { cn } from '../lib/cn';
12
+ import { Popover, PopoverAnchor, PopoverContent } from '../primitives/popover';
13
+ import { Switch } from '../primitives/switch';
14
+ /**
15
+ * The sky (time of day and weather) as a chip beside the compass, opening a panel: the sun's
16
+ * arc to drag through the day, the presets, the live clock, and the cloud cover. While a chip is
17
+ * mounted the display menus point here instead of carrying their own time and weather menus.
18
+ */
19
+ export const useSkyPanel = create(() => ({
20
+ open: false,
21
+ hosts: 0,
22
+ }));
23
+ export function openSkyPanel() {
24
+ useSkyPanel.setState({ open: true });
25
+ }
26
+ /**
27
+ * The bottom docks the chip lines up with (measured): the editor's tool dock and the viewer's
28
+ * controls bar, both 24 px up. The compass beside the chip centres on them.
29
+ */
30
+ export const SKY_DOCK_BOTTOM = 24;
31
+ export const SKY_DOCK_HEIGHT = { editor: 58, viewer: 56 };
32
+ export const SKY_COMPASS_BOTTOM = 36;
33
+ /**
34
+ * A preset or reset eases the sun to its time the short way round the clock, slowly enough to
35
+ * watch the light change: 1.5 s for a short hop, up to 3 s for half a day, easing in and out.
36
+ */
37
+ const TWEEN_MIN_MS = 1500;
38
+ const TWEEN_MAX_MS = 3000;
39
+ let tween = 0;
40
+ function stopTween() {
41
+ cancelAnimationFrame(tween);
42
+ tween = 0;
43
+ }
44
+ function easeTimeTo(from, to, done) {
45
+ stopTween();
46
+ const reduce = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
47
+ const delta = ((((to - from + 2160) % 1440) + 1440) % 1440) - 720;
48
+ if (reduce || Math.abs(delta) < 1) {
49
+ done();
50
+ return;
51
+ }
52
+ const start = performance.now();
53
+ const duration = TWEEN_MIN_MS + (TWEEN_MAX_MS - TWEEN_MIN_MS) * (Math.abs(delta) / 720);
54
+ const step = (now) => {
55
+ const t = Math.min(1, (now - start) / duration);
56
+ const eased = t < 0.5 ? 4 * t ** 3 : 1 - (-2 * t + 2) ** 3 / 2;
57
+ if (t >= 1) {
58
+ tween = 0;
59
+ done();
60
+ return;
61
+ }
62
+ useViewer.getState().setTimeOfDayMinutes((((from + delta * eased) % 1440) + 1440) % 1440);
63
+ tween = requestAnimationFrame(step);
64
+ };
65
+ tween = requestAnimationFrame(step);
66
+ }
67
+ const PRESETS = [
68
+ { id: 'midday', icon: Sun },
69
+ { id: 'golden', icon: Sunset },
70
+ { id: 'blue', icon: Sunrise },
71
+ { id: 'night', icon: Moon },
72
+ ];
73
+ const WEATHERS = [
74
+ { id: 'clear', icon: Sun },
75
+ { id: 'fair', icon: CloudSun },
76
+ { id: 'cloudy', icon: Cloudy },
77
+ { id: 'overcast', icon: Cloud },
78
+ ];
79
+ export function weatherIcon(weather) {
80
+ return WEATHERS.find((option) => option.id === weather)?.icon ?? CloudSun;
81
+ }
82
+ function presetName(i18n, id) {
83
+ switch (id) {
84
+ case 'golden':
85
+ return i18n._({ id: 'viewer.daylight.time.golden', message: 'Golden hour' });
86
+ case 'midday':
87
+ return i18n._({ id: 'viewer.daylight.time.midday', message: 'Midday' });
88
+ case 'blue':
89
+ return i18n._({ id: 'viewer.daylight.time.blue', message: 'Blue hour' });
90
+ case 'night':
91
+ return i18n._({ id: 'viewer.daylight.time.night', message: 'Night' });
92
+ }
93
+ }
94
+ function phaseName(i18n, phase) {
95
+ switch (phase) {
96
+ case 'night':
97
+ return presetName(i18n, 'night');
98
+ case 'blue':
99
+ return presetName(i18n, 'blue');
100
+ case 'golden':
101
+ return presetName(i18n, 'golden');
102
+ case 'midday':
103
+ return presetName(i18n, 'midday');
104
+ case 'morning':
105
+ return i18n._({ id: 'viewer.daylight.phase.morning', message: 'Morning' });
106
+ case 'afternoon':
107
+ return i18n._({ id: 'viewer.daylight.phase.afternoon', message: 'Afternoon' });
108
+ }
109
+ }
110
+ export function weatherName(i18n, id) {
111
+ switch (id) {
112
+ case 'clear':
113
+ return i18n._({ id: 'viewer.daylight.weather.clear', message: 'Clear' });
114
+ case 'fair':
115
+ return i18n._({ id: 'viewer.daylight.weather.fair', message: 'Fair' });
116
+ case 'cloudy':
117
+ return i18n._({ id: 'viewer.daylight.weather.cloudy', message: 'Cloudy' });
118
+ case 'overcast':
119
+ return i18n._({ id: 'viewer.daylight.weather.overcast', message: 'Overcast' });
120
+ }
121
+ }
122
+ /** Sky colours (zenith, horizon) per phase, for the chip swatch and the arc's backdrop. */
123
+ const PHASE_SKY = {
124
+ night: ['#121a36', '#36406c'],
125
+ blue: ['#263465', '#8c88b4'],
126
+ golden: ['#8e98c6', '#f2cfb2'],
127
+ morning: ['#6f9dd6', '#dbe7f2'],
128
+ midday: ['#5a93d5', '#d3e4f3'],
129
+ afternoon: ['#6a98d0', '#e6e2da'],
130
+ };
131
+ /** Minutes after midnight, as the locale writes a time of day. */
132
+ const formatMinutes = (() => {
133
+ let format = null;
134
+ return (minutes) => {
135
+ format ??= new Intl.DateTimeFormat(undefined, {
136
+ hour: 'numeric',
137
+ minute: '2-digit',
138
+ timeZone: 'UTC',
139
+ });
140
+ return format.format(new Date(Math.round(minutes) * 60_000));
141
+ };
142
+ })();
143
+ /** The sky the viewer shows right now: its time on the site's sun clock, phase and events. */
144
+ function useSky() {
145
+ const timeOfDay = useViewer((s) => s.timeOfDay);
146
+ const timeOfDayMinutes = useViewer((s) => s.timeOfDayMinutes);
147
+ const skyLive = useViewer((s) => s.skyLive);
148
+ const weather = useViewer((s) => s.weather);
149
+ const deviceLocation = useViewer((s) => s.deviceLocation);
150
+ const site = useScene((state) => {
151
+ for (const id of state.rootNodeIds) {
152
+ const node = state.nodes[id];
153
+ if (node?.type === 'site')
154
+ return node;
155
+ }
156
+ return undefined;
157
+ });
158
+ // The live clock moves the chip on once a minute.
159
+ const [now, setNow] = useState(() => new Date());
160
+ useEffect(() => {
161
+ if (!skyLive)
162
+ return;
163
+ setNow(new Date());
164
+ const timer = setInterval(() => setNow(new Date()), 60_000);
165
+ return () => clearInterval(timer);
166
+ }, [skyLive]);
167
+ const { location, source } = resolveLocation(site, deviceLocation);
168
+ const { lat, lng } = location;
169
+ const day = useMemo(() => {
170
+ const today = new Date();
171
+ const presets = Object.fromEntries(PRESETS.map(({ id }) => [id, solarClockMinutes(presetInstant(today, lat, lng, id), lng)]));
172
+ return { today, presets, events: sunEvents(today, lat, lng) };
173
+ }, [lat, lng]);
174
+ const minutes = skyLive
175
+ ? solarClockMinutes(now, location.lng)
176
+ : (timeOfDayMinutes ?? day.presets[timeOfDay]);
177
+ const elevation = solarPosition(solarClockInstant(day.today, location.lng, minutes), location.lat, location.lng).elevation;
178
+ return {
179
+ minutes,
180
+ // The live sky reads the wall clock the person knows; set times read the site's sun clock.
181
+ clock: skyLive ? now.getHours() * 60 + now.getMinutes() : minutes,
182
+ phase: skyPhase(elevation, minutes),
183
+ activePreset: skyLive || timeOfDayMinutes !== null ? null : timeOfDay,
184
+ presets: day.presets,
185
+ events: day.events ?? { sunrise: 6 * 60, sunset: 18 * 60 },
186
+ skyLive,
187
+ weather,
188
+ source,
189
+ };
190
+ }
191
+ /**
192
+ * The chip and its panel, absolutely placed by the caller (the scene's bottom-left corner, beside
193
+ * the compass). `dark` for the viewer's dark chrome.
194
+ */
195
+ export function SkyControl({ className, dark = false, height, style, }) {
196
+ const i18n = usePascalI18n();
197
+ const open = useSkyPanel((s) => s.open);
198
+ const sky = useSky();
199
+ const WeatherIcon = weatherIcon(sky.weather);
200
+ const nextWeather = WEATHERS[(WEATHERS.findIndex((w) => w.id === sky.weather) + 1) % WEATHERS.length];
201
+ const chip = useRef(null);
202
+ const compact = useClearOfDock(chip);
203
+ useEffect(() => {
204
+ useSkyPanel.setState((s) => ({ hosts: s.hosts + 1 }));
205
+ return () => useSkyPanel.setState((s) => ({ hosts: s.hosts - 1, open: false }));
206
+ }, []);
207
+ return (_jsxs(Popover, { onOpenChange: (next) => useSkyPanel.setState({ open: next }), open: open, children: [_jsx(PopoverAnchor, { asChild: true, children: _jsxs("div", { className: cn('corner-smooth pointer-events-auto absolute z-30 flex items-stretch gap-0.5 rounded-2xl border border-border/40 bg-background/95 p-1 text-foreground text-xs shadow-elevation-4 backdrop-blur-xl', dark && 'dark', className), onPointerDown: (event) => event.stopPropagation(), ref: chip, style: { bottom: SKY_DOCK_BOTTOM, height, ...style }, children: [_jsxs("button", { "aria-expanded": open, "aria-label": i18n._({ id: 'viewer.daylight.sky.open', message: 'Time and weather' }), className: cn('flex items-center gap-2 rounded-xl pr-3 pl-1.5 transition-colors', open ? 'bg-violet-500/10' : 'hover:bg-accent'), onClick: () => useSkyPanel.setState({ open: !open }), type: "button", children: [_jsx(MiniSky, { sky: sky }), _jsx("span", { className: "font-mono text-foreground text-sm tabular-nums", children: formatMinutes(sky.clock) }), compact ? null : (_jsx("span", { className: "text-muted-foreground text-sm", children: phaseName(i18n, sky.phase) }))] }), _jsx("span", { "aria-hidden": "true", className: "mx-0.5 my-auto h-6 w-px bg-border" }), _jsxs("button", { "aria-label": i18n._({
208
+ id: 'viewer.daylight.sky.cycleWeather',
209
+ message: 'Cloud cover: {current}. Change to {next}',
210
+ values: {
211
+ current: weatherName(i18n, sky.weather),
212
+ next: weatherName(i18n, nextWeather.id),
213
+ },
214
+ }), className: "flex items-center gap-1.5 rounded-xl px-2.5 text-sm transition-colors hover:bg-accent", onClick: () => useViewer.getState().setWeather(nextWeather.id), title: i18n._({
215
+ id: 'viewer.daylight.sky.cycleWeatherTip',
216
+ message: 'Cycle cloud cover',
217
+ }), type: "button", children: [_jsx(WeatherIcon, { "aria-hidden": "true", className: "h-[18px] w-[18px] text-muted-foreground" }), compact ? null : _jsx("span", { children: weatherName(i18n, sky.weather) })] })] }) }), _jsx(PopoverContent, { align: "start", className: cn('w-[22rem] rounded-2xl p-4', dark && 'dark'),
218
+ // Opened from a menu, focus returns to that menu's trigger: only a click outside closes.
219
+ onFocusOutside: (event) => event.preventDefault(), onOpenAutoFocus: (event) => event.preventDefault(), onPointerDown: (event) => event.stopPropagation(), side: "top", sideOffset: 8, children: _jsx(SkyPanel, { sky: sky }) })] }));
220
+ }
221
+ /**
222
+ * Whether the chip must drop its phase and weather labels to stay clear of the bottom dock (centred
223
+ * on the window, so a sidebar or a narrow pane brings it close). Measures the full chip, then
224
+ * compares where it would end with where the dock starts.
225
+ */
226
+ function useClearOfDock(chip) {
227
+ const [compact, setCompact] = useState(false);
228
+ const fullWidth = useRef(0);
229
+ useLayoutEffect(() => {
230
+ const element = chip.current;
231
+ if (!element)
232
+ return;
233
+ const check = () => {
234
+ const box = element.getBoundingClientRect();
235
+ if (!compact)
236
+ fullWidth.current = box.width;
237
+ const dock = document.querySelector('[data-surface="tool-dock"], [data-surface="viewer-dock"]');
238
+ const dockBox = dock?.getBoundingClientRect();
239
+ const overlaps = !!dockBox &&
240
+ dockBox.width > 0 &&
241
+ Math.abs(dockBox.bottom - box.bottom) < 40 &&
242
+ box.left + fullWidth.current + 12 > dockBox.left;
243
+ if (overlaps !== compact)
244
+ setCompact(overlaps);
245
+ };
246
+ check();
247
+ const observer = new ResizeObserver(check);
248
+ observer.observe(element);
249
+ if (element.offsetParent)
250
+ observer.observe(element.offsetParent);
251
+ window.addEventListener('resize', check);
252
+ return () => {
253
+ observer.disconnect();
254
+ window.removeEventListener('resize', check);
255
+ };
256
+ });
257
+ return compact;
258
+ }
259
+ /** The chip's swatch: the sky's colours with the sun (or moon) where it stands on its arc. */
260
+ function MiniSky({ sky }) {
261
+ const [top, horizon] = PHASE_SKY[sky.phase];
262
+ const sun = arcPoint(sky.minutes, sky.events.sunrise, sky.events.sunset);
263
+ return (_jsx("span", { "aria-hidden": "true", className: "relative h-8 w-8 shrink-0 overflow-hidden rounded-lg border border-black/10", style: { background: `linear-gradient(${top}, ${horizon} 62%, #8a8a94 63%, #6c6c7a)` }, children: _jsx("span", { className: "absolute h-2 w-2 -translate-x-1/2 -translate-y-1/2 rounded-full", style: {
264
+ left: `${(sun.x / W) * 100}%`,
265
+ top: `${Math.min(92, (sun.y / H) * 100)}%`,
266
+ background: sun.day ? '#fcd165' : '#d6dcf0',
267
+ boxShadow: sun.day ? '0 0 4px 1px rgba(252, 209, 101, 0.6)' : 'none',
268
+ } }) }));
269
+ }
270
+ function SkyPanel({ sky }) {
271
+ const i18n = usePascalI18n();
272
+ const [locating, setLocating] = useState(false);
273
+ const viewer = useViewer.getState;
274
+ return (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsxs("div", { className: "flex items-start gap-2", children: [_jsxs("div", { className: "flex-1", children: [_jsx("div", { className: "font-medium text-foreground text-sm", children: i18n._({ id: 'viewer.daylight.sky.title', message: 'Sky' }) }), _jsx("div", { className: "text-muted-foreground text-xs", children: i18n._({
275
+ id: 'viewer.daylight.sky.subtitle',
276
+ message: 'Light and weather for this view',
277
+ }) })] }), _jsxs("button", { className: "flex items-center gap-1 rounded-md px-1.5 py-1 text-violet-600 text-xs hover:bg-accent dark:text-violet-300", onClick: () => {
278
+ easeTimeTo(sky.minutes, sky.presets.midday, () => viewer().setTimeOfDay('midday'));
279
+ viewer().setWeather('fair');
280
+ }, type: "button", children: [_jsx(RotateCcw, { className: "h-3.5 w-3.5" }), i18n._({ id: 'viewer.daylight.sky.reset', message: 'Reset' })] }), _jsx("button", { "aria-label": i18n._({ id: 'viewer.daylight.sky.close', message: 'Close' }), className: "rounded-md p-1 text-muted-foreground hover:bg-accent hover:text-foreground", onClick: () => useSkyPanel.setState({ open: false }), type: "button", children: _jsx(X, { className: "h-4 w-4" }) })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "flex-1 font-medium text-[11px] text-muted-foreground uppercase tracking-wide", children: i18n._({ id: 'viewer.daylight.time.title', message: 'Time of day' }) }), _jsx("span", { className: "font-mono text-foreground text-sm tabular-nums", children: formatMinutes(sky.clock) }), _jsx("span", { className: "rounded-full bg-muted px-2 py-0.5 text-foreground text-xs", children: phaseName(i18n, sky.phase) })] }), _jsx(SunArc, { sky: sky }), _jsxs("div", { className: "-mt-1 flex justify-between text-muted-foreground text-xs", children: [_jsxs("span", { className: "flex items-center gap-1", children: [_jsx(Sunrise, { className: "h-3.5 w-3.5" }), i18n._({ id: 'viewer.daylight.sky.sunrise', message: 'Sunrise' }), _jsx("span", { className: "font-mono text-foreground tabular-nums", children: formatMinutes(sky.events.sunrise) })] }), _jsxs("span", { className: "flex items-center gap-1", children: [i18n._({ id: 'viewer.daylight.sky.sunset', message: 'Sunset' }), _jsx("span", { className: "font-mono text-foreground tabular-nums", children: formatMinutes(sky.events.sunset) }), _jsx(Sunset, { className: "h-3.5 w-3.5" })] })] }), _jsx("div", { className: "grid grid-cols-4 gap-2", children: PRESETS.map((preset) => (_jsx(OptionCard, { active: sky.activePreset === preset.id, detail: formatMinutes(sky.presets[preset.id]), icon: preset.icon, name: presetName(i18n, preset.id), onSelect: () => easeTimeTo(sky.minutes, sky.presets[preset.id], () => viewer().setTimeOfDay(preset.id)) }, preset.id))) }), _jsxs("div", { className: "flex items-center gap-3 rounded-xl bg-muted/60 px-3 py-2.5", children: [_jsx(LocateFixed, { className: "h-4 w-4 shrink-0 text-muted-foreground" }), _jsxs("div", { className: "flex-1", children: [_jsx("div", { className: "text-foreground text-sm", children: sky.source === 'site'
281
+ ? i18n._({ id: 'viewer.daylight.sky.followClock', message: 'Follow the clock' })
282
+ : i18n._({ id: 'viewer.daylight.location.use', message: 'Use my location' }) }), _jsx("div", { className: "text-muted-foreground text-xs", children: sky.source === 'site'
283
+ ? i18n._({
284
+ id: 'viewer.daylight.sky.followClockDetail',
285
+ message: 'The sun moves with your local clock.',
286
+ })
287
+ : i18n._({
288
+ id: 'viewer.daylight.sky.liveDetail',
289
+ message: 'Follows your local clock. Sun position is approximate.',
290
+ }) })] }), _jsx(Switch, { checked: sky.skyLive, disabled: locating, onCheckedChange: (live) => {
291
+ stopTween();
292
+ viewer().setSkyLive(live);
293
+ if (!live || sky.source === 'site' || sky.source === 'device')
294
+ return;
295
+ setLocating(true);
296
+ requestDeviceLocation()
297
+ .then((location) => useViewer.getState().setDeviceLocation(location))
298
+ .catch(() => { })
299
+ .finally(() => setLocating(false));
300
+ } })] }), _jsx("div", { className: "h-px bg-border" }), _jsxs("div", { className: "flex items-center", children: [_jsx("span", { className: "flex-1 font-medium text-[11px] text-muted-foreground uppercase tracking-wide", children: i18n._({ id: 'viewer.daylight.sky.cloudCover', message: 'Cloud cover' }) }), _jsx("span", { className: "text-foreground text-sm", children: weatherName(i18n, sky.weather) })] }), _jsx("div", { className: "grid grid-cols-4 gap-2", children: WEATHERS.map((option) => (_jsx(OptionCard, { active: sky.weather === option.id, detail: `${Math.round(WEATHER_COVER[option.id] * 100)}%`, icon: option.icon, name: weatherName(i18n, option.id), onSelect: () => viewer().setWeather(option.id) }, option.id))) })] }));
301
+ }
302
+ function OptionCard({ active, detail, icon: Icon, name, onSelect, }) {
303
+ return (_jsxs("button", { "aria-pressed": active, className: cn('flex flex-col items-center gap-0.5 rounded-xl border px-1 py-2 text-center transition hover:bg-accent', active
304
+ ? 'border-violet-500 bg-violet-500/10 text-violet-700 dark:text-violet-300'
305
+ : 'border-border text-foreground'), onClick: onSelect, type: "button", children: [_jsx(Icon, { className: cn('mb-0.5 h-4 w-4', active ? '' : 'text-amber-600 dark:text-amber-400') }), _jsx("span", { className: "text-xs leading-tight", children: name }), _jsx("span", { className: "font-mono text-[11px] text-muted-foreground tabular-nums", children: detail })] }));
306
+ }
307
+ // The arc: day above the horizon from sunrise (left) to sunset (right), night dotted below.
308
+ const W = 320;
309
+ const H = 150;
310
+ const CX = W / 2;
311
+ const CY = 100;
312
+ const RX = 136;
313
+ const RY_DAY = 80;
314
+ const RY_NIGHT = 30;
315
+ const SNAP = 5;
316
+ function arcPoint(minutes, sunrise, sunset) {
317
+ const dayLength = sunset - sunrise;
318
+ if (minutes >= sunrise && minutes <= sunset) {
319
+ const angle = Math.PI * (1 - (minutes - sunrise) / dayLength);
320
+ return { x: CX + RX * Math.cos(angle), y: CY - RY_DAY * Math.sin(angle), day: true };
321
+ }
322
+ const since = (minutes < sunrise ? minutes + 1440 : minutes) - sunset;
323
+ const angle = Math.PI * (since / (1440 - dayLength));
324
+ return { x: CX + RX * Math.cos(angle), y: CY + RY_NIGHT * Math.sin(angle), day: false };
325
+ }
326
+ function arcMinutes(x, y, sunrise, sunset) {
327
+ const dayLength = sunset - sunrise;
328
+ const angle = Math.atan2((CY - y) / (y < CY ? RY_DAY : RY_NIGHT), (x - CX) / RX);
329
+ const minutes = angle >= 0
330
+ ? sunrise + (1 - angle / Math.PI) * dayLength
331
+ : sunset + (-angle / Math.PI) * (1440 - dayLength);
332
+ return (((Math.round(minutes / SNAP) * SNAP) % 1440) + 1440) % 1440;
333
+ }
334
+ /** The sun's course from `from` to `to` minutes after sunrise, as an SVG path. */
335
+ function coursePath(sunrise, sunset, from, to) {
336
+ if (to - from < 1)
337
+ return '';
338
+ const steps = Math.max(2, Math.ceil((to - from) / 6));
339
+ let d = '';
340
+ for (let i = 0; i <= steps; i++) {
341
+ const point = arcPoint((sunrise + from + ((to - from) * i) / steps) % 1440, sunrise, sunset);
342
+ d += `${i ? 'L' : 'M'} ${point.x.toFixed(1)} ${point.y.toFixed(1)} `;
343
+ }
344
+ return d;
345
+ }
346
+ function SunArc({ sky }) {
347
+ const i18n = usePascalI18n();
348
+ const slider = useRef(null);
349
+ const { sunrise, sunset } = sky.events;
350
+ const sun = arcPoint(sky.minutes, sunrise, sunset);
351
+ const [top, horizon] = PHASE_SKY[sky.phase];
352
+ // The day's course from sunrise: drawn solid as far as the sun, dotted for what is left.
353
+ const elapsed = (((sky.minutes - sunrise) % 1440) + 1440) % 1440;
354
+ const travelled = coursePath(sunrise, sunset, 0, elapsed);
355
+ const ahead = coursePath(sunrise, sunset, elapsed, 1440);
356
+ const setFromPointer = (event) => {
357
+ const rect = slider.current?.getBoundingClientRect();
358
+ if (!rect)
359
+ return;
360
+ const x = ((event.clientX - rect.left) / rect.width) * W;
361
+ const y = ((event.clientY - rect.top) / rect.height) * H;
362
+ stopTween();
363
+ useViewer.getState().setTimeOfDayMinutes(arcMinutes(x, y, sunrise, sunset));
364
+ };
365
+ return (_jsx("div", { "aria-label": i18n._({
366
+ id: 'viewer.daylight.sky.arc',
367
+ message: 'Drag the sun to set the time',
368
+ }), "aria-valuemax": 1440, "aria-valuemin": 0, "aria-valuenow": Math.round(sky.minutes), "aria-valuetext": formatMinutes(sky.clock), className: "cursor-grab touch-none select-none overflow-hidden rounded-xl outline-none focus-visible:ring-2 focus-visible:ring-ring active:cursor-grabbing", onKeyDown: (event) => {
369
+ const step = event.key === 'ArrowRight' ? 15 : event.key === 'ArrowLeft' ? -15 : 0;
370
+ if (!step)
371
+ return;
372
+ event.preventDefault();
373
+ stopTween();
374
+ useViewer.getState().setTimeOfDayMinutes((sky.minutes + step + 1440) % 1440);
375
+ }, onPointerDown: (event) => {
376
+ event.currentTarget.setPointerCapture(event.pointerId);
377
+ setFromPointer(event);
378
+ }, onPointerMove: (event) => {
379
+ if (event.currentTarget.hasPointerCapture(event.pointerId))
380
+ setFromPointer(event);
381
+ }, ref: slider, role: "slider", tabIndex: 0, children: _jsxs("svg", { "aria-hidden": "true", className: "block w-full", viewBox: `0 0 ${W} ${H}`, children: [_jsx("defs", { children: _jsxs("linearGradient", { id: "sky-arc-backdrop", x1: "0", x2: "0", y1: "0", y2: "1", children: [_jsx("stop", { offset: "0", stopColor: top }), _jsx("stop", { offset: CY / H, stopColor: horizon }), _jsx("stop", { offset: CY / H, stopColor: "#9a9aa6" }), _jsx("stop", { offset: "1", stopColor: "#6c6c7a" })] }) }), _jsx("rect", { fill: "url(#sky-arc-backdrop)", height: H, width: W }), _jsx("line", { stroke: "rgba(0,0,0,0.25)", x1: "0", x2: W, y1: CY, y2: CY }), _jsx("path", { d: travelled, fill: "none", stroke: "#252a44", strokeLinecap: "round", strokeWidth: "2" }), _jsx("path", { d: ahead, fill: "none", stroke: "#252a44", strokeDasharray: "2 5", strokeLinecap: "round", strokeOpacity: "0.7", strokeWidth: "1.5" }), sun.day ? _jsx("circle", { cx: sun.x, cy: sun.y, fill: "#fcd165", opacity: "0.3", r: "15" }) : null, _jsx("circle", { cx: sun.x, cy: sun.y, fill: sun.day ? '#fcd165' : '#d6dcf0', r: "9", stroke: "#ffffff", strokeWidth: "2.5" })] }) }));
382
+ }
@@ -0,0 +1,4 @@
1
+ import type { CameraControlsImpl } from '@react-three/drei';
2
+ import { type RefObject } from 'react';
3
+ export declare function useViewerCameraNavigationSync(controls: RefObject<CameraControlsImpl | null>): () => void;
4
+ //# sourceMappingURL=use-viewer-camera-navigation-sync.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-viewer-camera-navigation-sync.d.ts","sourceRoot":"","sources":["../../../src/ui/components/use-viewer-camera-navigation-sync.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AAE3D,OAAO,EAAE,KAAK,SAAS,EAAkC,MAAM,OAAO,CAAA;AA+CtE,wBAAgB,6BAA6B,CAAC,QAAQ,EAAE,SAAS,CAAC,kBAAkB,GAAG,IAAI,CAAC,cAuF3F"}