@pascal-app/editor 1.0.3 → 1.0.5

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 (624) hide show
  1. package/README.md +106 -0
  2. package/package.json +42 -38
  3. package/src/components/editor/__bench__/room-label-placement.bench.ts +108 -0
  4. package/src/components/editor/action-menu-button.tsx +83 -0
  5. package/src/components/editor/bake-thumbnail.tsx +307 -5
  6. package/src/components/editor/capture-level-picker.tsx +72 -0
  7. package/src/components/editor/construction-reveal-audio.test.tsx +150 -0
  8. package/src/components/editor/construction-reveal-audio.tsx +40 -0
  9. package/src/components/editor/custom-camera-controls.tsx +275 -57
  10. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  11. package/src/components/editor/duplicate-node.ts +244 -0
  12. package/src/components/editor/editor-layout-mobile.tsx +37 -26
  13. package/src/components/editor/editor-layout-v2.tsx +132 -59
  14. package/src/components/editor/export-manager.tsx +47 -26
  15. package/src/components/editor/first-person/build-collider-world.test.ts +320 -0
  16. package/src/components/editor/first-person/build-collider-world.ts +28 -7
  17. package/src/components/editor/first-person/ground-holes.test.ts +128 -0
  18. package/src/components/editor/first-person/ground-holes.ts +130 -0
  19. package/src/components/editor/first-person/mechanism-targets.test.ts +162 -0
  20. package/src/components/editor/first-person/mechanism-targets.ts +88 -0
  21. package/src/components/editor/first-person-controls.tsx +220 -9
  22. package/src/components/editor/floating-action-menu.tsx +63 -207
  23. package/src/components/editor/floor-aware-camera-controls.test.ts +466 -0
  24. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  25. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  26. package/src/components/editor/floor-region-controls.tsx +846 -0
  27. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  28. package/src/components/editor/floor-surface.test.ts +165 -0
  29. package/src/components/editor/floor-surface.ts +182 -0
  30. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  31. package/src/components/editor/floorplan-background-selection.ts +1 -3
  32. package/src/components/editor/floorplan-mode-coordinator.tsx +2 -2
  33. package/src/components/editor/floorplan-panel.tsx +414 -363
  34. package/src/components/editor/follow-pascal.test.tsx +912 -0
  35. package/src/components/editor/follow-pascal.tsx +354 -0
  36. package/src/components/editor/footprint-height-handle.tsx +204 -0
  37. package/src/components/editor/group-actions.ts +56 -2
  38. package/src/components/editor/group-move-3d.ts +2 -0
  39. package/src/components/editor/group-rotate-handle.tsx +2 -0
  40. package/src/components/editor/group-transform-shared.test.ts +460 -0
  41. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  42. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  43. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  44. package/src/components/editor/handles/use-handle-drag.ts +269 -197
  45. package/src/components/editor/index.tsx +409 -197
  46. package/src/components/editor/join-walls-pill.tsx +44 -0
  47. package/src/components/editor/node-action-menu.tsx +68 -70
  48. package/src/components/editor/node-arrow-handles.tsx +25 -6
  49. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  50. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  51. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  52. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  53. package/src/components/editor/registry-action-contributions.tsx +80 -1
  54. package/src/components/editor/riser-diagram-panel.tsx +14 -7
  55. package/src/components/editor/room-controls.tsx +847 -0
  56. package/src/components/editor/room-floor-highlight.tsx +128 -0
  57. package/src/components/editor/room-handles-2d.tsx +354 -0
  58. package/src/components/editor/room-handles.tsx +675 -0
  59. package/src/components/editor/room-highlight.tsx +233 -0
  60. package/src/components/editor/room-labels.test.ts +850 -0
  61. package/src/components/editor/room-labels.tsx +1890 -0
  62. package/src/components/editor/room-transform-ghost.tsx +256 -0
  63. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  64. package/src/components/editor/selection-manager.tsx +474 -419
  65. package/src/components/editor/slab-hole-highlights.tsx +5 -2
  66. package/src/components/editor/snapshot-capture-overlay.tsx +7 -2
  67. package/src/components/editor/snapshot-capture.test.ts +119 -60
  68. package/src/components/editor/snapshot-capture.ts +36 -5
  69. package/src/components/editor/thumbnail-generator.test.ts +654 -0
  70. package/src/components/editor/thumbnail-generator.tsx +1103 -223
  71. package/src/components/editor/use-floorplan-background-placement.ts +34 -12
  72. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  73. package/src/components/editor/views/host-view.tsx +61 -0
  74. package/src/components/editor/views/plugins-view.tsx +663 -0
  75. package/src/components/editor/views/scene-region.tsx +59 -0
  76. package/src/components/editor/views/use-editor-views.ts +57 -0
  77. package/src/components/editor/views/view-bar.test.tsx +54 -0
  78. package/src/components/editor/views/view-bar.tsx +343 -0
  79. package/src/components/editor/wall-justification.test.ts +140 -0
  80. package/src/components/editor/wall-move-side-handles.tsx +43 -142
  81. package/src/components/editor/wall-push-arrow.tsx +92 -0
  82. package/src/components/editor/wall-region-handles.tsx +509 -0
  83. package/src/components/editor/wall-snap-beacon-layer.tsx +8 -4
  84. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  85. package/src/components/editor-2d/floorplan-group-move.tsx +3 -0
  86. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  87. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  88. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +2 -2
  89. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  90. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +116 -106
  91. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  92. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  93. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  94. package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
  95. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  96. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
  97. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +319 -233
  98. package/src/components/editor-2d/renderers/floorplan-reveal.test.tsx +254 -0
  99. package/src/components/editor-2d/renderers/floorplan-reveal.ts +141 -0
  100. package/src/components/editor-2d/svg-paths.ts +12 -15
  101. package/src/components/point-ask/bubble.tsx +538 -0
  102. package/src/components/point-ask/hover-chip.tsx +91 -0
  103. package/src/components/point-ask/mode-pill.tsx +92 -0
  104. package/src/components/point-ask/optional-motion.ts +22 -0
  105. package/src/components/point-ask/outline-corners.tsx +93 -0
  106. package/src/components/point-ask/pins.tsx +711 -0
  107. package/src/components/point-ask/point-ask-layer.tsx +83 -0
  108. package/src/components/point-ask/point-ask-styles.ts +105 -0
  109. package/src/components/point-ask/region-marquee-styles.ts +15 -0
  110. package/src/components/point-ask/region-marquee.tsx +64 -0
  111. package/src/components/procedural-items/inspector.tsx +40 -15
  112. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +12 -2
  113. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  114. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  115. package/src/components/systems/roof/roof-edit-system.tsx +1 -0
  116. package/src/components/systems/zone/zone-system.test.ts +47 -0
  117. package/src/components/systems/zone/zone-system.tsx +29 -68
  118. package/src/components/tools/distribution/connection-compatibility.test.ts +34 -0
  119. package/src/components/tools/distribution/connection-compatibility.ts +43 -0
  120. package/src/components/tools/distribution/distribution-public.ts +9 -0
  121. package/src/components/tools/distribution/distribution-run-contract.test.ts +26 -0
  122. package/src/components/tools/distribution/distribution-run-contract.ts +94 -0
  123. package/src/components/tools/distribution/distribution-run-tool.test.ts +241 -0
  124. package/src/components/tools/distribution/distribution-run-tool.test.tsx +125 -0
  125. package/src/components/tools/distribution/distribution-run-tool.tsx +1323 -0
  126. package/src/components/tools/distribution/draw-alignment.ts +35 -0
  127. package/src/components/tools/distribution/ports.test.ts +56 -0
  128. package/src/components/tools/distribution/ports.ts +378 -0
  129. package/src/components/tools/distribution/run-cursor.test.ts +135 -0
  130. package/src/components/tools/distribution/run-cursor.ts +36 -0
  131. package/src/components/tools/distribution/run-direction-feedback.test.ts +60 -0
  132. package/src/components/tools/distribution/run-direction-feedback.tsx +251 -0
  133. package/src/components/tools/distribution/run-port-snap.test.ts +79 -0
  134. package/src/components/tools/distribution/run-port-snap.ts +25 -0
  135. package/src/components/tools/distribution/run-screen-direction.test.ts +23 -0
  136. package/src/components/tools/distribution/run-screen-direction.ts +30 -0
  137. package/src/components/tools/elevator/elevator-tool.tsx +3 -6
  138. package/src/components/tools/elevator/move-elevator-tool.tsx +2 -3
  139. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +67 -68
  140. package/src/components/tools/item/placement-history.test.tsx +512 -0
  141. package/src/components/tools/item/placement-strategies.test.ts +46 -1
  142. package/src/components/tools/item/placement-strategies.ts +11 -10
  143. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +56 -56
  144. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  145. package/src/components/tools/item/use-draft-node.ts +166 -95
  146. package/src/components/tools/item/use-placement-coordinator.tsx +148 -95
  147. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  148. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  149. package/src/components/tools/registry/item-surface-move.test.ts +62 -63
  150. package/src/components/tools/registry/item-surface-move.ts +2 -0
  151. package/src/components/tools/registry/item-surface-resolver.test.ts +26 -26
  152. package/src/components/tools/registry/move-registry-node-tool.test.ts +268 -1
  153. package/src/components/tools/registry/move-registry-node-tool.tsx +66 -30
  154. package/src/components/tools/select/box-select-tool.tsx +2 -12
  155. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  156. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  157. package/src/components/tools/select/select-candidates.test.ts +37 -4
  158. package/src/components/tools/select/select-candidates.ts +15 -11
  159. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  160. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  161. package/src/components/tools/tool-manager.tsx +59 -24
  162. package/src/components/tools/wall/wall-drafting.test.ts +175 -47
  163. package/src/components/tools/zone/zone-tool.tsx +4 -12
  164. package/src/components/ui/action-menu/action-button.tsx +9 -1
  165. package/src/components/ui/action-menu/camera-actions.tsx +9 -4
  166. package/src/components/ui/action-menu/control-modes.tsx +15 -42
  167. package/src/components/ui/action-menu/follow-pascal-pill.tsx +32 -0
  168. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +36 -0
  169. package/src/components/ui/action-menu/index.tsx +56 -33
  170. package/src/components/ui/action-menu/measurement-control.tsx +14 -19
  171. package/src/components/ui/action-menu/placement.ts +20 -0
  172. package/src/components/ui/action-menu/point-ask-button.tsx +52 -0
  173. package/src/components/ui/action-menu/view-toggles.tsx +47 -42
  174. package/src/components/ui/build-panel/build-panel.tsx +242 -0
  175. package/src/components/ui/command-palette/editor-commands.tsx +7 -42
  176. package/src/components/ui/command-palette/index.tsx +50 -39
  177. package/src/components/ui/command-palette/ranked-groups.test.ts +31 -0
  178. package/src/components/ui/controls/action-button.tsx +5 -2
  179. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  180. package/src/components/ui/controls/material-paint-panel.tsx +97 -26
  181. package/src/components/ui/controls/material-picker.tsx +4 -1
  182. package/src/components/ui/controls/metric-control.tsx +1 -1
  183. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  184. package/src/components/ui/controls/segmented-control.tsx +21 -8
  185. package/src/components/ui/controls/select-control.tsx +65 -0
  186. package/src/components/ui/controls/slider-control.tsx +1 -1
  187. package/src/components/ui/controls/toggle-control.tsx +3 -3
  188. package/src/components/ui/controls/tool-options-panel.tsx +1 -1
  189. package/src/components/ui/floating-level-selector.tsx +159 -59
  190. package/src/components/ui/helpers/building-helper.tsx +4 -1
  191. package/src/components/ui/helpers/contextual-helper-panel.tsx +181 -72
  192. package/src/components/ui/helpers/helper-manager.test.tsx +121 -0
  193. package/src/components/ui/helpers/helper-manager.tsx +331 -14
  194. package/src/components/ui/helpers/item-helper.tsx +17 -1
  195. package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
  196. package/src/components/ui/icon-ref.tsx +45 -1
  197. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  198. package/src/components/ui/node-icon-image.tsx +16 -0
  199. package/src/components/ui/panels/collections/collections-popover.tsx +28 -22
  200. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  201. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  202. package/src/components/ui/panels/mobile-panel-sheet.tsx +4 -4
  203. package/src/components/ui/panels/mobile-selection-bar.tsx +8 -5
  204. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  205. package/src/components/ui/panels/multi-field-value.ts +6 -3
  206. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  207. package/src/components/ui/panels/multi-selection-panel.tsx +161 -5
  208. package/src/components/ui/panels/node-display.ts +8 -3
  209. package/src/components/ui/panels/panel-manager.tsx +103 -68
  210. package/src/components/ui/panels/panel-wrapper.tsx +130 -62
  211. package/src/components/ui/panels/parametric-field-control.tsx +3 -3
  212. package/src/components/ui/panels/parametric-inspector.tsx +7 -7
  213. package/src/components/ui/panels/reference-panel.tsx +2 -3
  214. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  215. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  216. package/src/components/ui/panels/room-name-options.ts +47 -0
  217. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  218. package/src/components/ui/panels/room-panel.tsx +790 -0
  219. package/src/components/ui/panels/shape-choice.tsx +58 -0
  220. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  221. package/src/components/ui/primitives/badge.tsx +8 -0
  222. package/src/components/ui/primitives/color-dot.tsx +36 -24
  223. package/src/components/ui/primitives/sidebar.tsx +3 -1
  224. package/src/components/ui/right-stack.test.ts +29 -0
  225. package/src/components/ui/right-stack.tsx +164 -0
  226. package/src/components/ui/sidebar/app-sidebar.tsx +1 -1
  227. package/src/components/ui/sidebar/icon-rail.tsx +8 -3
  228. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
  229. package/src/components/ui/sidebar/panels/items-panel/index.tsx +6 -6
  230. package/src/components/ui/sidebar/panels/settings-panel/animation-speed-setting.tsx +40 -0
  231. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +7 -8
  232. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.test.tsx +170 -0
  233. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +92 -0
  234. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +138 -27
  235. package/src/components/ui/sidebar/panels/settings-panel/interface-theme.test.tsx +20 -0
  236. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
  237. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +4 -10
  238. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -11
  239. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  240. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  241. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  242. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  243. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  244. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  245. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  246. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  247. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  248. package/src/components/ui/sidebar/panels/site-panel/index.tsx +181 -223
  249. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  250. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  251. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  252. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  253. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  254. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  255. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  256. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  257. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  258. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  259. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +13 -37
  260. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  261. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  262. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  263. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  264. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +2 -4
  265. package/src/components/ui/sidebar/tab-bar.tsx +14 -4
  266. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  267. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  268. package/src/components/viewer/floorplan-preview.tsx +5 -6
  269. package/src/components/viewer/viewer-controls-bar.tsx +18 -6
  270. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  271. package/src/components/viewer/viewer-rooms.tsx +191 -0
  272. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  273. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  274. package/src/components/viewer/viewer-stage.tsx +2 -2
  275. package/src/components/viewer-zone-system.tsx +3 -8
  276. package/src/hooks/use-auto-save.integration.test.tsx +215 -43
  277. package/src/hooks/use-auto-save.ts +60 -9
  278. package/src/hooks/use-ceiling-events.test.ts +123 -118
  279. package/src/hooks/use-grid-events.test.ts +105 -105
  280. package/src/hooks/use-keyboard.test.ts +243 -19
  281. package/src/hooks/use-keyboard.ts +118 -83
  282. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  283. package/src/hooks/use-open-wall-ends.ts +238 -0
  284. package/src/hooks/use-placement-notice.test.ts +71 -0
  285. package/src/hooks/use-placement-notice.ts +73 -0
  286. package/src/hooks/use-selected-room.ts +143 -0
  287. package/src/index.tsx +312 -58
  288. package/src/lib/__fixtures__/owner-victor-seattle-parking.json +1 -0
  289. package/src/lib/__fixtures__/owner-villa-aurelia-basement.json +1 -0
  290. package/src/lib/add-storey.test.ts +177 -0
  291. package/src/lib/add-storey.ts +59 -0
  292. package/src/lib/below-grade.test.tsx +171 -0
  293. package/src/lib/below-grade.ts +133 -0
  294. package/src/lib/camera-facing.test.ts +24 -0
  295. package/src/lib/camera-facing.ts +24 -0
  296. package/src/lib/ceiling-edit-session.test.ts +321 -0
  297. package/src/lib/ceiling-edit-session.ts +206 -0
  298. package/src/lib/ceiling-region-session.test.ts +179 -0
  299. package/src/lib/continuation.test.ts +19 -1
  300. package/src/lib/continuation.ts +25 -4
  301. package/src/lib/direct-manipulation.ts +2 -0
  302. package/src/lib/door-interaction.ts +14 -0
  303. package/src/lib/download-project-file.ts +17 -0
  304. package/src/lib/editor-level-display.test.ts +96 -0
  305. package/src/lib/editor-level-display.ts +55 -0
  306. package/src/lib/editor-views.ts +178 -0
  307. package/src/lib/export-geometry-wait.test.ts +90 -0
  308. package/src/lib/export-texture-utils.ts +2 -4
  309. package/src/lib/floor-edit-session.ts +153 -0
  310. package/src/lib/floor-footprints.test.ts +563 -0
  311. package/src/lib/floor-footprints.ts +395 -0
  312. package/src/lib/floor-foundation-support.test.ts +284 -0
  313. package/src/lib/floor-opening-draft.ts +218 -0
  314. package/src/lib/floor-openings.test.ts +216 -0
  315. package/src/lib/floor-region-geometry.ts +191 -0
  316. package/src/lib/floor-region-session.test.ts +485 -0
  317. package/src/lib/floor-region-session.ts +589 -0
  318. package/src/lib/floor-region-snap.ts +101 -0
  319. package/src/lib/floor-step-hover.test.ts +32 -0
  320. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  321. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  322. package/src/lib/floorplan/dimension-drive.ts +479 -0
  323. package/src/lib/floorplan/floorplan-export.test.ts +12 -8
  324. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  325. package/src/lib/floorplan/floorplan-extension.ts +32 -0
  326. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  327. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  328. package/src/lib/floorplan/floorplan-pdfkit-document.ts +363 -27
  329. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +177 -8
  330. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +60 -25
  331. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  332. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  333. package/src/lib/floorplan/geometry.ts +13 -6
  334. package/src/lib/floorplan/index.ts +0 -9
  335. package/src/lib/floorplan/items.ts +5 -2
  336. package/src/lib/floorplan/marks.test.ts +213 -0
  337. package/src/lib/floorplan/marks.ts +238 -0
  338. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  339. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  340. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  341. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  342. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  343. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  344. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  345. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  346. package/src/lib/floorplan/pdfkit-standard-fonts.d.ts +5 -0
  347. package/src/lib/floorplan/schedules.test.ts +189 -0
  348. package/src/lib/floorplan/schedules.ts +339 -0
  349. package/src/lib/floorplan/sheet-export.test.ts +124 -0
  350. package/src/lib/floorplan/sheet-export.ts +318 -0
  351. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +768 -0
  352. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  353. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  354. package/src/lib/floorplan/site-plan/coverage.ts +230 -0
  355. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  356. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  357. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  358. package/src/lib/floorplan/site-plan/front-edge.ts +157 -0
  359. package/src/lib/floorplan/site-plan/geometry.test.ts +269 -0
  360. package/src/lib/floorplan/site-plan/geometry.ts +142 -0
  361. package/src/lib/floorplan/site-plan/index.ts +55 -0
  362. package/src/lib/floorplan/site-plan/site-annotations.test.ts +483 -0
  363. package/src/lib/floorplan/site-plan/site-annotations.ts +1084 -0
  364. package/src/lib/floorplan/site-plan/site-parts.ts +287 -0
  365. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  366. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  367. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  368. package/src/lib/floorplan/site-plan/yards.test.ts +70 -0
  369. package/src/lib/floorplan/types.ts +1 -27
  370. package/src/lib/follow-pascal-exterior.test-fixture.ts +17 -0
  371. package/src/lib/follow-pascal-exterior.test.ts +171 -0
  372. package/src/lib/follow-pascal-exterior.ts +39 -0
  373. package/src/lib/follow-pascal-presentation.test.ts +203 -0
  374. package/src/lib/follow-pascal-presentation.ts +129 -0
  375. package/src/lib/follow-pascal-sight.ts +84 -0
  376. package/src/lib/follow-pascal-targets.test.ts +406 -0
  377. package/src/lib/follow-pascal-targets.ts +198 -0
  378. package/src/lib/follow-pascal.test.ts +350 -0
  379. package/src/lib/follow-pascal.ts +284 -0
  380. package/src/lib/fresh-planar-placement.test.ts +148 -0
  381. package/src/lib/fresh-planar-placement.ts +114 -37
  382. package/src/lib/geometry-script/author.ts +157 -0
  383. package/src/lib/geometry-script/client.ts +159 -0
  384. package/src/lib/geometry-script/finish-scripts.ts +127 -0
  385. package/src/lib/geometry-script/protocol.ts +13 -0
  386. package/src/lib/gesture-lifecycle.test.ts +595 -0
  387. package/src/lib/gesture-lifecycle.ts +290 -0
  388. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  389. package/src/lib/glb-export.test.ts +636 -126
  390. package/src/lib/glb-export.ts +241 -120
  391. package/src/lib/history.test.ts +18 -7
  392. package/src/lib/history.ts +7 -0
  393. package/src/lib/hud-title.ts +109 -0
  394. package/src/lib/hydration-gated-storage.ts +49 -0
  395. package/src/lib/ifc-export.test.ts +174 -0
  396. package/src/lib/ifc-export.ts +201 -0
  397. package/src/lib/ifc-parts.ts +15 -0
  398. package/src/lib/inspector-expanded.test.ts +44 -0
  399. package/src/lib/inspector-expanded.ts +36 -0
  400. package/src/lib/interaction/scope.ts +22 -0
  401. package/src/lib/keyboard-pan.test.ts +129 -7
  402. package/src/lib/keyboard-pan.ts +161 -12
  403. package/src/lib/level-selection.test.ts +57 -0
  404. package/src/lib/level-selection.ts +16 -1
  405. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  406. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  407. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  408. package/src/lib/lot/clean-ring.test.ts +187 -0
  409. package/src/lib/lot/clean-ring.ts +217 -0
  410. package/src/lib/lot/dossier.test.ts +202 -0
  411. package/src/lib/lot/dossier.ts +498 -0
  412. package/src/lib/lot/drop-in.test.ts +326 -0
  413. package/src/lib/lot/drop-in.ts +392 -0
  414. package/src/lib/lot/index.ts +43 -0
  415. package/src/lib/lot/lot-patch.test.ts +340 -0
  416. package/src/lib/lot/lot-patch.ts +394 -0
  417. package/src/lib/lot/parcel-provider.ts +43 -0
  418. package/src/lib/lot/terrain.test.ts +161 -0
  419. package/src/lib/lot/terrain.ts +266 -0
  420. package/src/lib/material-catalog-model.ts +3 -1
  421. package/src/lib/material-paint-panel-model.ts +9 -5
  422. package/src/lib/material-paint.ts +4 -0
  423. package/src/lib/measurement-kind.test.ts +6 -8
  424. package/src/lib/measurements.test.ts +23 -0
  425. package/src/lib/mezzanine-actions.test.ts +342 -0
  426. package/src/lib/mezzanine-draft.test.ts +501 -0
  427. package/src/lib/mezzanine-draft.ts +278 -0
  428. package/src/lib/mezzanine-messages.ts +26 -0
  429. package/src/lib/mezzanine-selection.test.ts +207 -0
  430. package/src/lib/mezzanine-stairs.ts +46 -0
  431. package/src/lib/model-export.ts +3 -1
  432. package/src/lib/node-action-movement.ts +17 -6
  433. package/src/lib/node-display-label.ts +37 -0
  434. package/src/lib/orbit-floor-clearance.test.ts +201 -0
  435. package/src/lib/orbit-floor-clearance.ts +154 -0
  436. package/src/lib/paint-commit-route.test.ts +37 -0
  437. package/src/lib/paint-commit-route.ts +17 -0
  438. package/src/lib/paint-eyedropper.test.tsx +165 -0
  439. package/src/lib/paint-eyedropper.ts +99 -0
  440. package/src/lib/paint-pass-through.test.ts +122 -0
  441. package/src/lib/paint-pass-through.ts +78 -0
  442. package/src/lib/paint-region-hover.ts +25 -0
  443. package/src/lib/paint-region-mode.ts +154 -0
  444. package/src/lib/paint-regions.test.ts +166 -0
  445. package/src/lib/paint-regions.ts +192 -0
  446. package/src/lib/paint-scope.test.ts +317 -35
  447. package/src/lib/paint-scope.ts +267 -25
  448. package/src/lib/panel-rows.ts +21 -0
  449. package/src/lib/parametric-node-update.ts +16 -7
  450. package/src/lib/plate-paint-affected.test.ts +300 -0
  451. package/src/lib/plate-paint-affected.ts +177 -0
  452. package/src/lib/plugin-chat.ts +65 -0
  453. package/src/lib/plugin-panels.test.ts +25 -0
  454. package/src/lib/plugin-panels.ts +94 -0
  455. package/src/lib/point-ask/__fixtures__/consumer-browser.tsx +76 -0
  456. package/src/lib/point-ask/__fixtures__/house.ts +81 -0
  457. package/src/lib/point-ask/ask-created.test.ts +195 -0
  458. package/src/lib/point-ask/ask-flow.test.ts +380 -0
  459. package/src/lib/point-ask/build-context.test.ts +173 -0
  460. package/src/lib/point-ask/build-context.ts +94 -0
  461. package/src/lib/point-ask/capture-crop.ts +129 -0
  462. package/src/lib/point-ask/choreography.ts +69 -0
  463. package/src/lib/point-ask/compare.test.ts +145 -0
  464. package/src/lib/point-ask/compare.ts +73 -0
  465. package/src/lib/point-ask/describe-target.test.ts +165 -0
  466. package/src/lib/point-ask/describe-target.ts +234 -0
  467. package/src/lib/point-ask/dictation.ts +79 -0
  468. package/src/lib/point-ask/edge-marker.test.ts +61 -0
  469. package/src/lib/point-ask/edge-marker.ts +27 -0
  470. package/src/lib/point-ask/frame-loop.ts +25 -0
  471. package/src/lib/point-ask/history-marks.test.ts +94 -0
  472. package/src/lib/point-ask/history-marks.ts +39 -0
  473. package/src/lib/point-ask/host-api.ts +32 -0
  474. package/src/lib/point-ask/outline-box.test.ts +30 -0
  475. package/src/lib/point-ask/outline-box.ts +11 -0
  476. package/src/lib/point-ask/overlay-metrics.ts +6 -0
  477. package/src/lib/point-ask/pick.test.ts +220 -0
  478. package/src/lib/point-ask/pick.ts +121 -0
  479. package/src/lib/point-ask/pin-machine.test.ts +494 -0
  480. package/src/lib/point-ask/pin-machine.ts +217 -0
  481. package/src/lib/point-ask/placement.test.ts +203 -0
  482. package/src/lib/point-ask/placement.ts +141 -0
  483. package/src/lib/point-ask/pointer-tracker.ts +34 -0
  484. package/src/lib/point-ask/projector.ts +83 -0
  485. package/src/lib/point-ask/region.test.ts +157 -0
  486. package/src/lib/point-ask/region.ts +136 -0
  487. package/src/lib/point-ask/retry.test.ts +217 -0
  488. package/src/lib/point-ask/retry.ts +37 -0
  489. package/src/lib/point-ask/scene-commit.test.ts +312 -0
  490. package/src/lib/point-ask/scene-commit.ts +32 -0
  491. package/src/lib/point-ask/session.ts +1391 -0
  492. package/src/lib/point-ask/spring-keyframes.test.ts +45 -0
  493. package/src/lib/point-ask/spring-keyframes.ts +38 -0
  494. package/src/lib/point-ask/spring.test.ts +63 -0
  495. package/src/lib/point-ask/spring.ts +107 -0
  496. package/src/lib/point-ask/suggestions.test.ts +80 -0
  497. package/src/lib/point-ask/suggestions.ts +125 -0
  498. package/src/lib/point-ask/target-scope.test.ts +622 -0
  499. package/src/lib/point-ask/target-scope.ts +338 -0
  500. package/src/lib/point-ask/types.ts +48 -0
  501. package/src/lib/portable-export.test.ts +34 -1
  502. package/src/lib/portable-export.ts +119 -53
  503. package/src/lib/print-feature-thickness.ts +10 -1
  504. package/src/lib/print-roof-solids.test.ts +39 -1
  505. package/src/lib/print-roof-solids.ts +43 -10
  506. package/src/lib/print-shell-compiler-manifold-core.ts +1 -1
  507. package/src/lib/print-shell-compiler.ts +1 -0
  508. package/src/lib/registered-draft-snap.ts +12 -0
  509. package/src/lib/roof-duplication.ts +10 -2
  510. package/src/lib/room-assembly-overlay.test.ts +275 -0
  511. package/src/lib/room-assembly-overlay.ts +265 -0
  512. package/src/lib/room-built-on.test.ts +379 -0
  513. package/src/lib/room-built-on.ts +138 -0
  514. package/src/lib/room-construction-commands.ts +712 -0
  515. package/src/lib/room-construction.test.ts +746 -0
  516. package/src/lib/room-construction.ts +385 -0
  517. package/src/lib/room-controls-phase6.test.ts +630 -0
  518. package/src/lib/room-divide-session.ts +523 -0
  519. package/src/lib/room-handle-drag.ts +1068 -0
  520. package/src/lib/room-name-catalog.test.ts +90 -0
  521. package/src/lib/room-name-catalog.ts +168 -0
  522. package/src/lib/room-push-dimensions.test.ts +311 -0
  523. package/src/lib/room-push-dimensions.ts +251 -0
  524. package/src/lib/room-selection-commands.ts +62 -0
  525. package/src/lib/room-selection.test.ts +1017 -0
  526. package/src/lib/room-selection.ts +417 -0
  527. package/src/lib/room-structure-commands.ts +94 -0
  528. package/src/lib/room-structure.test.ts +921 -0
  529. package/src/lib/room-transform-session.ts +570 -0
  530. package/src/lib/room-zone-routing.test.ts +294 -0
  531. package/src/lib/room-zone-routing.ts +197 -0
  532. package/src/lib/scene-clipboard.test.ts +610 -1
  533. package/src/lib/scene-clipboard.ts +269 -58
  534. package/src/lib/scene-material-refs.test.ts +79 -0
  535. package/src/lib/scene-material-refs.ts +79 -0
  536. package/src/lib/scene.test.ts +24 -36
  537. package/src/lib/scene.ts +33 -57
  538. package/src/lib/selection-routing.test.ts +129 -45
  539. package/src/lib/selection-routing.ts +54 -75
  540. package/src/lib/session-writes.test.ts +120 -0
  541. package/src/lib/session-writes.ts +205 -0
  542. package/src/lib/sfx-bus.ts +13 -0
  543. package/src/lib/sfx-player.test.ts +70 -9
  544. package/src/lib/sfx-player.ts +6 -0
  545. package/src/lib/sidebar-panel.ts +62 -0
  546. package/src/lib/site-boundary.test.ts +4 -6
  547. package/src/lib/site-boundary.ts +1 -1
  548. package/src/lib/snapping-mode.test.ts +4 -0
  549. package/src/lib/snapping-mode.ts +8 -1
  550. package/src/lib/stair-levels.test.ts +25 -0
  551. package/src/lib/stair-levels.ts +22 -3
  552. package/src/lib/terrace-draft.test.ts +145 -0
  553. package/src/lib/terrace-draft.ts +214 -0
  554. package/src/lib/tool-cursor-icon.test.ts +16 -0
  555. package/src/lib/tool-cursor-icon.ts +11 -0
  556. package/src/lib/typing-target.ts +9 -0
  557. package/src/lib/units.ts +77 -39
  558. package/src/lib/vector-edges.test.ts +58 -0
  559. package/src/lib/vector-edges.ts +400 -0
  560. package/src/lib/viewer-selection.test.ts +577 -0
  561. package/src/lib/viewer-selection.ts +580 -0
  562. package/src/lib/wall-draw-variant.test.ts +71 -0
  563. package/src/lib/wall-draw-variant.ts +91 -0
  564. package/src/lib/wall-paint-region-session.ts +246 -0
  565. package/src/lib/wall-paint-region.test.ts +373 -0
  566. package/src/lib/wall-placement-pointer.test.ts +42 -0
  567. package/src/lib/wall-placement-pointer.ts +57 -0
  568. package/src/lib/wall-polygon-draft.test.ts +262 -0
  569. package/src/lib/wall-polygon-draft.ts +286 -0
  570. package/src/lib/wall-push.test.ts +453 -0
  571. package/src/lib/wall-region-face.ts +352 -0
  572. package/src/lib/wall-region-handles.test.ts +304 -0
  573. package/src/lib/wall-region-handles.ts +368 -0
  574. package/src/lib/wall-region-list.test.ts +119 -0
  575. package/src/lib/wall-region-list.ts +136 -0
  576. package/src/lib/wall-region-snap.ts +110 -0
  577. package/src/lib/window-changes.ts +35 -0
  578. package/src/lib/window-interaction.ts +14 -0
  579. package/src/lib/zone-content.ts +4 -1
  580. package/src/lib/zone-name.test.ts +17 -0
  581. package/src/lib/zone-name.ts +17 -0
  582. package/src/store/opens-in-select-mode.test.ts +223 -0
  583. package/src/store/paint-mode.test.ts +188 -0
  584. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  585. package/src/store/tool-mode.test.ts +137 -6
  586. package/src/store/use-audio.test.ts +146 -0
  587. package/src/store/use-audio.tsx +127 -3
  588. package/src/store/use-construction-reveal.test.ts +173 -0
  589. package/src/store/use-construction-reveal.ts +145 -0
  590. package/src/store/use-corner-snap-hint.test.ts +24 -0
  591. package/src/store/use-corner-snap-hint.ts +47 -0
  592. package/src/store/use-delete-confirmation.ts +28 -1
  593. package/src/store/use-drawing-view.test.ts +24 -3
  594. package/src/store/use-drawing-view.ts +29 -1
  595. package/src/store/use-editor.tsx +432 -321
  596. package/src/store/use-floorplan-draft-preview.ts +13 -0
  597. package/src/store/use-floorplan-marquee.ts +12 -2
  598. package/src/store/use-follow-pascal.test.ts +62 -0
  599. package/src/store/use-follow-pascal.ts +53 -0
  600. package/src/store/use-point-ask.ts +247 -0
  601. package/src/store/use-quick-measurement-hud.ts +3 -3
  602. package/src/store/use-stair-build-preview.ts +33 -5
  603. package/src/store/use-wall-move-ghosts.ts +4 -2
  604. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  605. package/src/store/use-wall-region-handles-selection.ts +139 -0
  606. package/src/store/view-layout.test.ts +201 -0
  607. package/src/store/view-layout.ts +199 -0
  608. package/src/test-utils/immediate-animation-frames.ts +16 -0
  609. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  610. package/src/test-utils/render-current-markup.tsx +16 -0
  611. package/src/test-utils/selection-harness.ts +82 -0
  612. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  613. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  614. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  615. package/src/components/tools/fence/fence-drafting.ts +0 -266
  616. package/src/components/tools/stair/stair-click-guard.test.ts +0 -58
  617. package/src/components/tools/stair/stair-click-guard.ts +0 -72
  618. package/src/components/tools/stair/stair-defaults.ts +0 -18
  619. package/src/components/tools/stair/stair-tool.tsx +0 -634
  620. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  621. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  622. package/src/lib/floorplan/stairs.ts +0 -489
  623. package/src/lib/level-duplication.test.ts +0 -144
  624. package/src/lib/level-duplication.ts +0 -195
@@ -1,6 +1,7 @@
1
1
  import { afterEach, describe, expect, test } from 'bun:test'
2
2
  import useEditor, { isBrushMode, type Mode, normalizePersistedEditorUiState } from './use-editor'
3
3
  import useInteractionScope from './use-interaction-scope'
4
+ import { isViewVisible, sceneLayout, VIEW_2D, visibleScene } from './view-layout'
4
5
 
5
6
  /**
6
7
  * The mode-lifecycle contract for terrain sculpting.
@@ -17,9 +18,9 @@ function reset() {
17
18
  useEditor.getState().setPreviewMode(false)
18
19
  useEditor.getState().setFirstPersonMode(false)
19
20
  useEditor.getState().setWorkspaceMode('edit')
20
- // Before the mode: `setViewMode('2d')` disarms sculpt, and leaving a stale view
21
+ // Before the mode: hiding 3D disarms sculpt, and leaving a stale layout
21
22
  // behind would silently change what the next test's `setMode` does.
22
- useEditor.getState().setViewMode('3d')
23
+ useEditor.getState().setViewLayout(sceneLayout('3d'))
23
24
  useEditor.getState().setMode('select')
24
25
  useEditor.getState().setTerrainVerb('flatten')
25
26
  useEditor.getState().setTerrainSampling(false)
@@ -41,8 +42,8 @@ describe('terrain-sculpt mode lifecycle', () => {
41
42
  expect(useInteractionScope.getState().scope.kind).toBe('idle')
42
43
  })
43
44
 
44
- test('arming sculpt from another phase moves to the site phase', () => {
45
- useEditor.getState().setPhase('structure')
45
+ test('arming sculpt from the building moves to site', () => {
46
+ useEditor.getState().setPhase('building')
46
47
  useEditor.getState().setMode('terrain-sculpt')
47
48
  // Otherwise `normalizeModeForPhase` rejects the mode on the next rehydrate
48
49
  // and the UI shows a mode the store does not hold.
@@ -53,7 +54,7 @@ describe('terrain-sculpt mode lifecycle', () => {
53
54
  test('a phase switch out of site drops the scope', () => {
54
55
  useEditor.getState().setMode('terrain-sculpt')
55
56
  // The phase setter delegates its mode rewrite to the ToolMode transition.
56
- useEditor.getState().setPhase('structure')
57
+ useEditor.getState().setPhase('building')
57
58
  expect(useInteractionScope.getState().scope.kind).toBe('idle')
58
59
  })
59
60
  })
@@ -71,7 +72,7 @@ describe('the eyedropper arm is not allowed to outlive sculpt mode', () => {
71
72
  test('leaving via a phase switch disarms it too', () => {
72
73
  useEditor.getState().setMode('terrain-sculpt')
73
74
  useEditor.getState().setTerrainSampling(true)
74
- useEditor.getState().setPhase('furnish')
75
+ useEditor.getState().setPhase('building')
75
76
  expect(useEditor.getState().terrainSampling).toBe(false)
76
77
  })
77
78
 
@@ -106,17 +107,17 @@ describe('the brush and the 3D canvas travel together', () => {
106
107
  // moves), while choosing the floorplan asks for the floorplan (so the mode
107
108
  // yields).
108
109
  test('arming from the 2D floorplan opens a 3D pane beside it', () => {
109
- useEditor.getState().setViewMode('2d')
110
+ useEditor.getState().setViewLayout(sceneLayout('2d'))
110
111
  useEditor.getState().setMode('terrain-sculpt')
111
112
  // `split`, not `3d`: the floorplan was a deliberate choice, so keep it.
112
- expect(useEditor.getState().viewMode).toBe('split')
113
- expect(useEditor.getState().isFloorplanOpen).toBe(true)
113
+ expect(visibleScene(useEditor.getState())).toBe('split')
114
+ expect(isViewVisible(useEditor.getState(), VIEW_2D)).toBe(true)
114
115
  expect(useEditor.getState().mode).toBe('terrain-sculpt')
115
116
  })
116
117
 
117
118
  test('switching to the 2D floorplan disarms the brush', () => {
118
119
  useEditor.getState().setMode('terrain-sculpt')
119
- useEditor.getState().setViewMode('2d')
120
+ useEditor.getState().setViewLayout(sceneLayout('2d'))
120
121
  expect(useEditor.getState().mode).toBe('select')
121
122
  // And the scope goes with it — otherwise selection stays suppressed in the
122
123
  // floorplan the user just asked for, with no visible way out.
@@ -125,10 +126,16 @@ describe('the brush and the 3D canvas travel together', () => {
125
126
 
126
127
  test('split keeps it armed — the canvas is right there', () => {
127
128
  useEditor.getState().setMode('terrain-sculpt')
128
- useEditor.getState().setViewMode('split')
129
+ useEditor.getState().setViewLayout(sceneLayout('split'))
129
130
  expect(useEditor.getState().mode).toBe('terrain-sculpt')
130
131
  })
131
132
 
133
+ test('another view in the 3D pane disarms it, as the plan does', () => {
134
+ useEditor.getState().setMode('terrain-sculpt')
135
+ useEditor.getState().showView('gallery')
136
+ expect(useEditor.getState().mode).toBe('select')
137
+ })
138
+
132
139
  test('rehydrating the pair resolves it to select', () => {
133
140
  // Mode and view persist independently and rehydrate runs neither setter, so
134
141
  // this is the one reconciliation the setters cannot cover.
@@ -172,20 +179,11 @@ describe('the modes that hide the editing canvas release the brush', () => {
172
179
  useEditor.getState().setWorkspaceMode('studio')
173
180
  expect(useInteractionScope.getState().scope.kind).toBe('idle')
174
181
  })
175
-
176
- test('a structure-layer switch releases it', () => {
177
- useEditor.getState().setMode('terrain-sculpt')
178
- // Reachable from the layer toggle while sculpt is armed, and it takes the
179
- // same raw-`set` path — the mode resets, so the scope has to follow.
180
- useEditor.getState().setStructureLayer('zones')
181
- expect(useEditor.getState().mode).toBe('select')
182
- expect(useInteractionScope.getState().scope.kind).toBe('idle')
183
- })
184
182
  })
185
183
 
186
184
  describe('entering sculpt mode stands other affordances down', () => {
187
185
  test('the armed build tool is cleared', () => {
188
- useEditor.getState().setPhase('structure')
186
+ useEditor.getState().setPhase('building')
189
187
  useEditor.getState().setMode('build')
190
188
  useEditor.getState().setTool('wall')
191
189
  useEditor.getState().setMode('terrain-sculpt')
@@ -1,9 +1,14 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
2
+ import { type AnyNode, useScene } from '@pascal-app/core'
3
+ import { useViewer } from '@pascal-app/viewer'
4
+ import { beginGesture } from '../lib/gesture-lifecycle'
2
5
  import useEditor, { normalizePersistedEditorUiState } from './use-editor'
6
+ import useInteractionScope from './use-interaction-scope'
3
7
 
4
8
  function resetToolMode() {
5
- useEditor.getState().setPhase('structure')
6
- useEditor.getState().setStructureLayer('elements')
9
+ useEditor.getState().clearRoom()
10
+ useViewer.getState().setSelection({ selectedIds: [], zoneId: null })
11
+ useEditor.getState().setPhase('building')
7
12
  useEditor.getState().armToolMode({ mode: 'select' })
8
13
  useEditor.getState().setActivePaintMaterial(null)
9
14
  }
@@ -12,6 +17,18 @@ beforeEach(resetToolMode)
12
17
  afterEach(resetToolMode)
13
18
 
14
19
  describe('ToolMode transition', () => {
20
+ test('arming building tools preserves the current phase', () => {
21
+ for (const phase of ['site', 'building'] as const) {
22
+ useEditor.getState().setPhase(phase)
23
+ useEditor.getState().armToolMode({ mode: 'select' })
24
+ for (const tool of ['item', 'wall', 'measurement', 'zone'] as const) {
25
+ useEditor.getState().armToolMode({ mode: 'build', tool })
26
+ expect(useEditor.getState().phase).toBe(phase)
27
+ expect(useEditor.getState().tool).toBe(tool)
28
+ }
29
+ }
30
+ })
31
+
15
32
  test('choosing a paint swatch while selected arms material paint', () => {
16
33
  useEditor.getState().armToolMode({ mode: 'select' })
17
34
  useEditor.getState().armMaterialPaint({
@@ -56,14 +73,129 @@ describe('ToolMode transition', () => {
56
73
  })
57
74
  })
58
75
 
76
+ describe('leaving Select ends the selection', () => {
77
+ const ROOM = { levelId: 'level_1', zoneId: 'zone_1' }
78
+
79
+ function selectRoomAndNode() {
80
+ useEditor.getState().armToolMode({ mode: 'select' })
81
+ useViewer
82
+ .getState()
83
+ .setSelection({ selectedIds: ['wall_1' as never], zoneId: 'zone_1' as never })
84
+ useEditor.getState().selectRoom(ROOM)
85
+ useEditor.getState().setSelectedReferenceId(null)
86
+ }
87
+
88
+ function expectNothingSelected() {
89
+ expect(useViewer.getState().selection.selectedIds).toEqual([])
90
+ expect(useViewer.getState().selection.zoneId).toBeNull()
91
+ expect(useEditor.getState().room).toBeNull()
92
+ }
93
+
94
+ test('picking a catalog item (the item tool) clears the room and node selection', () => {
95
+ selectRoomAndNode()
96
+ useEditor.getState().setSelectedItem({ id: 'chair' } as never)
97
+ useEditor.getState().setTool('item')
98
+ expectNothingSelected()
99
+ })
100
+
101
+ test('the door and window tools clear it', () => {
102
+ for (const tool of ['door', 'window'] as const) {
103
+ selectRoomAndNode()
104
+ useEditor.getState().armToolMode({ mode: 'build', tool })
105
+ expectNothingSelected()
106
+ }
107
+ })
108
+
109
+ test('draw tools clear it, and so does switching from one tool to another', () => {
110
+ selectRoomAndNode()
111
+ useEditor.getState().setMode('build')
112
+ expect(useEditor.getState().tool).toBe('wall')
113
+ expectNothingSelected()
114
+
115
+ // A tool that selects what it just drew (the roof, to draw into it next)
116
+ // loses that selection when another tool is picked.
117
+ useViewer.getState().setSelection({ selectedIds: ['roof_1' as never] })
118
+ useEditor.getState().armToolMode({ mode: 'build', tool: 'slab' })
119
+ expectNothingSelected()
120
+ })
121
+
122
+ test('re-arming the tool already in hand is not a transition', () => {
123
+ useEditor.getState().armToolMode({ mode: 'build', tool: 'roof' })
124
+ useViewer.getState().setSelection({ selectedIds: ['roof_1' as never] })
125
+ useEditor.getState().setTool('roof')
126
+ expect(useViewer.getState().selection.selectedIds).toEqual(['roof_1'])
127
+ })
128
+
129
+ test('paint clears it, after taking its target from the selection', () => {
130
+ const previousNodes = useScene.getState().nodes
131
+ useScene.setState({
132
+ nodes: { roof_1: { id: 'roof_1', type: 'roof' } as unknown as AnyNode } as never,
133
+ })
134
+ try {
135
+ useEditor.getState().setActivePaintTarget('wall')
136
+ selectRoomAndNode()
137
+ useViewer.getState().setSelection({ selectedIds: ['roof_1' as never] })
138
+ useEditor.getState().armMaterialPaint()
139
+ expect(useEditor.getState().mode).toBe('material-paint')
140
+ expect(useEditor.getState().activePaintTarget).toBe('roof')
141
+ expectNothingSelected()
142
+ } finally {
143
+ useScene.setState({ nodes: previousNodes })
144
+ }
145
+ })
146
+
147
+ test('terrain sculpt and delete clear it', () => {
148
+ for (const mode of ['terrain-sculpt', 'delete'] as const) {
149
+ selectRoomAndNode()
150
+ useEditor.getState().armToolMode({ mode })
151
+ expectNothingSelected()
152
+ }
153
+ })
154
+
155
+ test('the level and building context survive', () => {
156
+ useViewer
157
+ .getState()
158
+ .setSelection({ buildingId: 'building_1' as never, levelId: 'level_1' as never })
159
+ selectRoomAndNode()
160
+ useEditor.getState().armToolMode({ mode: 'build', tool: 'door' })
161
+ expect(useViewer.getState().selection.levelId).toBe('level_1')
162
+ expect(useViewer.getState().selection.buildingId).toBe('building_1')
163
+ })
164
+
165
+ test('Select, and a move or handle drag inside it, keep the selection', () => {
166
+ selectRoomAndNode()
167
+ useEditor.getState().armToolMode({ mode: 'select' })
168
+ expect(useViewer.getState().selection.selectedIds).toEqual(['wall_1'])
169
+ expect(useEditor.getState().room).toEqual(ROOM)
170
+
171
+ let cancelled = false
172
+ const handle = beginGesture({
173
+ kind: 'test-handle',
174
+ onCancel: () => {
175
+ cancelled = true
176
+ },
177
+ })
178
+ useEditor.getState().armToolMode({ mode: 'select' })
179
+ expect(handle.active).toBe(true)
180
+ handle.end()
181
+ expect(cancelled).toBe(false)
182
+
183
+ useEditor.getState().setMovingNode({ id: 'wall_1', type: 'wall' } as unknown as AnyNode)
184
+ expect(useInteractionScope.getState().scope.kind).toBe('moving')
185
+ expect(useEditor.getState().mode).toBe('select')
186
+ expect(useViewer.getState().selection.selectedIds).toEqual(['wall_1'])
187
+ useEditor.getState().setMovingNode(null)
188
+ expect(useViewer.getState().selection.selectedIds).toEqual(['wall_1'])
189
+ })
190
+ })
191
+
59
192
  describe('persisted ToolMode normalization', () => {
60
193
  test('elects a default for build with a null tool', () => {
61
194
  const state = normalizePersistedEditorUiState({
62
- phase: 'structure',
195
+ phase: 'building',
63
196
  toolMode: { mode: 'build', tool: null as never },
64
197
  mode: 'select',
65
198
  tool: 'slab',
66
- structureLayer: 'elements',
67
199
  })
68
200
 
69
201
  expect(state.toolMode).toEqual({ mode: 'build', tool: 'wall' })
@@ -73,11 +205,10 @@ describe('persisted ToolMode normalization', () => {
73
205
 
74
206
  test('clears a persisted tool from a non-build mode', () => {
75
207
  const state = normalizePersistedEditorUiState({
76
- phase: 'structure',
208
+ phase: 'building',
77
209
  toolMode: { mode: 'select' },
78
210
  mode: 'build',
79
211
  tool: 'wall',
80
- structureLayer: 'elements',
81
212
  })
82
213
 
83
214
  expect(state.toolMode).toEqual({ mode: 'select' })
@@ -0,0 +1,146 @@
1
+ import { afterAll, beforeEach, describe, expect, test } from 'bun:test'
2
+ import useAudio, { AUDIO_MUTED_STORAGE_KEY, AUDIO_SETTINGS_STORAGE_KEY } from './use-audio'
3
+
4
+ function memoryStorage() {
5
+ const items = new Map<string, string>()
6
+ return {
7
+ items,
8
+ getItem: (key: string) => items.get(key) ?? null,
9
+ setItem: (key: string, value: string) => void items.set(key, value),
10
+ removeItem: (key: string) => void items.delete(key),
11
+ }
12
+ }
13
+
14
+ const originals = {
15
+ localStorage: Object.getOwnPropertyDescriptor(globalThis, 'localStorage'),
16
+ sessionStorage: Object.getOwnPropertyDescriptor(globalThis, 'sessionStorage'),
17
+ }
18
+
19
+ function use(name: 'localStorage' | 'sessionStorage', storage: ReturnType<typeof memoryStorage>) {
20
+ Object.defineProperty(globalThis, name, { configurable: true, value: storage })
21
+ }
22
+
23
+ /** One browser profile: its tabs share `localStorage`, each has its own `sessionStorage`. */
24
+ let browser = memoryStorage()
25
+
26
+ /** Another tab of the profile saves the settings: the browser stores them and tells this tab. */
27
+ function savedInAnotherTab(state: Record<string, unknown>) {
28
+ const value = JSON.stringify({ state, version: 0 })
29
+ browser.setItem(AUDIO_SETTINGS_STORAGE_KEY, value)
30
+ globalThis.dispatchEvent(
31
+ Object.assign(new Event('storage'), { key: AUDIO_SETTINGS_STORAGE_KEY, newValue: value }),
32
+ )
33
+ }
34
+
35
+ /** The editor loading in a tab: a new one, or the same one reloaded (its session kept). */
36
+ async function loadIn(tab: ReturnType<typeof memoryStorage>) {
37
+ use('sessionStorage', tab)
38
+ await useAudio.persist.rehydrate()
39
+ return useAudio.getState()
40
+ }
41
+
42
+ beforeEach(() => {
43
+ browser = memoryStorage()
44
+ use('localStorage', browser)
45
+ })
46
+
47
+ afterAll(() => {
48
+ for (const [name, descriptor] of Object.entries(originals)) {
49
+ if (descriptor) Object.defineProperty(globalThis, name, descriptor)
50
+ else Reflect.deleteProperty(globalThis, name)
51
+ }
52
+ })
53
+
54
+ describe('muting a tab', () => {
55
+ test('a tab keeps its own mute across reloads, and a new tab starts from the last choice', async () => {
56
+ const first = memoryStorage()
57
+ const second = memoryStorage()
58
+
59
+ expect((await loadIn(first)).muted).toBe(false)
60
+ useAudio.getState().toggleMute()
61
+
62
+ expect((await loadIn(second)).muted).toBe(true)
63
+ useAudio.getState().setMuted(false)
64
+
65
+ expect((await loadIn(first)).muted).toBe(true)
66
+ expect((await loadIn(memoryStorage())).muted).toBe(false)
67
+ })
68
+
69
+ test("another tab's choice does not change a tab that never chose", async () => {
70
+ const quiet = memoryStorage()
71
+ expect((await loadIn(quiet)).muted).toBe(false)
72
+
73
+ await loadIn(memoryStorage())
74
+ useAudio.getState().setMuted(true)
75
+
76
+ expect((await loadIn(quiet)).muted).toBe(false)
77
+ })
78
+
79
+ test("volumes are every tab's and never carry a mute", async () => {
80
+ const muting = memoryStorage()
81
+ await loadIn(muting)
82
+ useAudio.getState().setMuted(true)
83
+
84
+ await loadIn(memoryStorage())
85
+ useAudio.getState().setMuted(false)
86
+ await loadIn(muting)
87
+ useAudio.getState().setSfxVolume(20)
88
+
89
+ expect(JSON.parse(browser.getItem(AUDIO_SETTINGS_STORAGE_KEY)!).state).not.toHaveProperty(
90
+ 'muted',
91
+ )
92
+ expect(browser.getItem(AUDIO_MUTED_STORAGE_KEY)).toBe('false')
93
+ const next = await loadIn(memoryStorage())
94
+ expect(next.sfxVolume).toBe(20)
95
+ expect(next.muted).toBe(false)
96
+ })
97
+
98
+ test("a volume another tab sets reaches this one at once, and this tab's saves carry it", async () => {
99
+ await loadIn(memoryStorage())
100
+ useAudio.getState().setSfxVolume(30)
101
+ useAudio.getState().setRadioPlaying(true)
102
+ const stored = JSON.parse(browser.getItem(AUDIO_SETTINGS_STORAGE_KEY)!)
103
+
104
+ savedInAnotherTab({ ...stored.state, isRadioPlaying: false, muted: true, sfxVolume: 80 })
105
+
106
+ expect(useAudio.getState()).toMatchObject({ isRadioPlaying: true, muted: false, sfxVolume: 80 })
107
+ const setElsewhere = browser.getItem(AUDIO_SETTINGS_STORAGE_KEY)
108
+ useAudio.getState().setMuted(true)
109
+ expect(browser.getItem(AUDIO_SETTINGS_STORAGE_KEY)).toBe(setElsewhere)
110
+ // The radio pausing as the tab is hidden saves every setting: the other tab's volume stays.
111
+ useAudio.getState().setRadioPlaying(false)
112
+ useAudio.getState().setMasterVolume(60)
113
+ expect(JSON.parse(browser.getItem(AUDIO_SETTINGS_STORAGE_KEY)!).state).toMatchObject({
114
+ masterVolume: 60,
115
+ sfxVolume: 80,
116
+ })
117
+ })
118
+
119
+ test("a mute saved when it was every tab's still holds, in settings the old build still reads", async () => {
120
+ browser.setItem(
121
+ AUDIO_SETTINGS_STORAGE_KEY,
122
+ JSON.stringify({ state: { masterVolume: 40, muted: true, sfxVolume: 50 }, version: 0 }),
123
+ )
124
+
125
+ const state = await loadIn(memoryStorage())
126
+
127
+ expect(state.muted).toBe(true)
128
+ expect(state.masterVolume).toBe(40)
129
+ expect((await loadIn(memoryStorage())).muted).toBe(true)
130
+ useAudio.getState().setSfxVolume(45)
131
+ const saved = JSON.parse(browser.getItem(AUDIO_SETTINGS_STORAGE_KEY)!)
132
+ expect(saved).toMatchObject({ state: { masterVolume: 40, sfxVolume: 45 }, version: 0 })
133
+ expect(saved.state).not.toHaveProperty('muted')
134
+ })
135
+
136
+ test('a tab still on the old settings does not undo a choice made since', async () => {
137
+ await loadIn(memoryStorage())
138
+ useAudio.getState().setMuted(false)
139
+ browser.setItem(
140
+ AUDIO_SETTINGS_STORAGE_KEY,
141
+ JSON.stringify({ state: { muted: true, sfxVolume: 50 }, version: 0 }),
142
+ )
143
+
144
+ expect((await loadIn(memoryStorage())).muted).toBe(false)
145
+ })
146
+ })
@@ -1,13 +1,22 @@
1
1
  'use client'
2
2
 
3
3
  import { create } from 'zustand'
4
- import { persist } from 'zustand/middleware'
4
+ import { createJSONStorage, persist } from 'zustand/middleware'
5
+
6
+ export const AUDIO_SETTINGS_STORAGE_KEY = 'pascal-audio-settings'
7
+ /**
8
+ * Mute is each tab's own, so someone working in two projects can silence one and hear the other.
9
+ * Under this key `sessionStorage` holds this tab's mute (kept across its reloads) and
10
+ * `localStorage` the last choice made in any tab, which a new tab starts from.
11
+ */
12
+ export const AUDIO_MUTED_STORAGE_KEY = 'pascal-audio-muted'
5
13
 
6
14
  interface AudioState {
7
15
  masterVolume: number
8
16
  sfxVolume: number
9
17
  radioVolume: number
10
18
  isRadioPlaying: boolean
19
+ /** This tab's mute; the volumes are every tab's. */
11
20
  muted: boolean
12
21
  autoplay: boolean
13
22
  setMasterVolume: (v: number) => void
@@ -15,10 +24,73 @@ interface AudioState {
15
24
  setRadioVolume: (v: number) => void
16
25
  setRadioPlaying: (v: boolean) => void
17
26
  toggleRadioPlaying: () => void
27
+ setMuted: (muted: boolean) => void
18
28
  toggleMute: () => void
19
29
  setAutoplay: (v: boolean) => void
20
30
  }
21
31
 
32
+ type StoredSettings = Pick<
33
+ AudioState,
34
+ 'masterVolume' | 'sfxVolume' | 'radioVolume' | 'isRadioPlaying' | 'autoplay'
35
+ >
36
+
37
+ type BrowserStorage = 'localStorage' | 'sessionStorage'
38
+
39
+ // Storage can be missing (a test, a server render) or refuse access (site data blocked): the page
40
+ // then keeps its choices in memory only.
41
+ function readStored(storage: BrowserStorage, key: string) {
42
+ try {
43
+ return globalThis[storage]?.getItem(key) ?? null
44
+ } catch {
45
+ return null
46
+ }
47
+ }
48
+
49
+ function writeStored(storage: BrowserStorage, key: string, value: string) {
50
+ try {
51
+ globalThis[storage]?.setItem(key, value)
52
+ } catch {}
53
+ }
54
+
55
+ /** This tab's mute. A tab without one takes the last choice made in any tab, and keeps it. */
56
+ function tabMuted(): boolean {
57
+ const own = readStored('sessionStorage', AUDIO_MUTED_STORAGE_KEY)
58
+ if (own !== null) return own === 'true'
59
+ const muted = readStored('localStorage', AUDIO_MUTED_STORAGE_KEY) === 'true'
60
+ writeStored('sessionStorage', AUDIO_MUTED_STORAGE_KEY, String(muted))
61
+ return muted
62
+ }
63
+
64
+ /** A mute chosen in this tab: this tab's, and where the next new tab starts. Open tabs keep theirs. */
65
+ function chooseMuted(muted: boolean) {
66
+ writeStored('sessionStorage', AUDIO_MUTED_STORAGE_KEY, String(muted))
67
+ writeStored('localStorage', AUDIO_MUTED_STORAGE_KEY, String(muted))
68
+ return { muted }
69
+ }
70
+
71
+ // The settings as this tab last read, wrote or took from another tab. A save carries every
72
+ // setting, so a save that changes none (a mute, which is this tab's own) is skipped.
73
+ let heldSettings: string | null = null
74
+ // Set while this tab takes another tab's volumes: that is no choice of its own to save back.
75
+ let adopting = false
76
+
77
+ const settingsStorage = createJSONStorage<StoredSettings>(() => ({
78
+ getItem: (key) => {
79
+ heldSettings = readStored('localStorage', key)
80
+ return heldSettings
81
+ },
82
+ setItem: (key, value) => {
83
+ if (value === heldSettings) return
84
+ heldSettings = value
85
+ if (!adopting) writeStored('localStorage', key, value)
86
+ },
87
+ removeItem: (key) => {
88
+ try {
89
+ globalThis.localStorage?.removeItem(key)
90
+ } catch {}
91
+ },
92
+ }))
93
+
22
94
  const useAudio = create<AudioState>()(
23
95
  persist(
24
96
  (set) => ({
@@ -33,13 +105,65 @@ const useAudio = create<AudioState>()(
33
105
  setRadioVolume: (v) => set({ radioVolume: v }),
34
106
  setRadioPlaying: (v) => set({ isRadioPlaying: v }),
35
107
  toggleRadioPlaying: () => set((state) => ({ isRadioPlaying: !state.isRadioPlaying })),
36
- toggleMute: () => set((state) => ({ muted: !state.muted })),
108
+ setMuted: (muted) => set(chooseMuted(muted)),
109
+ toggleMute: () => set((state) => chooseMuted(!state.muted)),
37
110
  setAutoplay: (v) => set({ autoplay: v }),
38
111
  }),
39
112
  {
40
- name: 'pascal-audio-settings',
113
+ name: AUDIO_SETTINGS_STORAGE_KEY,
114
+ storage: settingsStorage,
115
+ partialize: ({ masterVolume, sfxVolume, radioVolume, isRadioPlaying, autoplay }) => ({
116
+ masterVolume,
117
+ sfxVolume,
118
+ radioVolume,
119
+ isRadioPlaying,
120
+ autoplay,
121
+ }),
122
+ // The settings once held one mute for every tab: whoever had muted stays muted, unless a tab
123
+ // has chosen since (a tab still on that build can write it again). The format is unchanged,
124
+ // so that build reads these settings back with only the mute gone.
125
+ merge: (persisted, current) => {
126
+ const { muted, ...settings } = (persisted ?? {}) as Partial<StoredSettings> & {
127
+ muted?: unknown
128
+ }
129
+ if (
130
+ typeof muted === 'boolean' &&
131
+ readStored('localStorage', AUDIO_MUTED_STORAGE_KEY) === null
132
+ ) {
133
+ writeStored('localStorage', AUDIO_MUTED_STORAGE_KEY, String(muted))
134
+ }
135
+ return { ...current, ...settings, muted: tabMuted() }
136
+ },
41
137
  },
42
138
  ),
43
139
  )
44
140
 
141
+ /**
142
+ * Another tab saved the settings: the volumes are every tab's, so this tab takes them at once, and
143
+ * a later save of its own carries them. Its mute and whether its radio plays stay its own.
144
+ */
145
+ function adoptOtherTabSettings(event: StorageEvent) {
146
+ if (event.key !== AUDIO_SETTINGS_STORAGE_KEY || event.newValue === null) return
147
+ let stored: Partial<Record<keyof StoredSettings, unknown>> | undefined
148
+ try {
149
+ stored = JSON.parse(event.newValue)?.state
150
+ } catch {
151
+ return
152
+ }
153
+ const shared: Partial<StoredSettings> = {}
154
+ for (const key of ['masterVolume', 'sfxVolume', 'radioVolume'] as const) {
155
+ const value = stored?.[key]
156
+ if (typeof value === 'number') shared[key] = value
157
+ }
158
+ if (typeof stored?.autoplay === 'boolean') shared.autoplay = stored.autoplay
159
+ adopting = true
160
+ try {
161
+ useAudio.setState(shared)
162
+ } finally {
163
+ adopting = false
164
+ }
165
+ }
166
+
167
+ globalThis.addEventListener?.('storage', adoptOtherTabSettings)
168
+
45
169
  export default useAudio