@pascal-app/editor 0.9.1 → 1.0.0-beta.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 (366) hide show
  1. package/package.json +14 -9
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
  3. package/src/components/editor/bake-exporter.tsx +49 -0
  4. package/src/components/editor/bake-thumbnail.tsx +95 -0
  5. package/src/components/editor/camera-dragging-lifecycle.test.ts +49 -0
  6. package/src/components/editor/camera-dragging-lifecycle.ts +41 -0
  7. package/src/components/editor/custom-camera-controls.tsx +332 -199
  8. package/src/components/editor/delete-confirmation-dialog.tsx +54 -0
  9. package/src/components/editor/editor-layout-mobile.tsx +5 -0
  10. package/src/components/editor/editor-layout-v2.tsx +18 -0
  11. package/src/components/editor/elevation-3d-guide-layer.tsx +120 -0
  12. package/src/components/editor/export-manager.tsx +71 -63
  13. package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
  14. package/src/components/editor/first-person/build-collider-world.test.ts +102 -5
  15. package/src/components/editor/first-person/build-collider-world.ts +52 -5
  16. package/src/components/editor/first-person/terrain-collider.test.ts +132 -0
  17. package/src/components/editor/first-person/terrain-collider.ts +212 -0
  18. package/src/components/editor/first-person-controls.tsx +410 -136
  19. package/src/components/editor/floating-action-menu.tsx +435 -64
  20. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  21. package/src/components/editor/floorplan-background-selection.test.ts +72 -0
  22. package/src/components/editor/floorplan-background-selection.ts +7 -20
  23. package/src/components/editor/floorplan-camera-sync.test.ts +249 -0
  24. package/src/components/editor/floorplan-camera-sync.ts +213 -0
  25. package/src/components/editor/floorplan-mode-coordinator.tsx +82 -0
  26. package/src/components/editor/floorplan-navigation-presentation.test.ts +142 -0
  27. package/src/components/editor/floorplan-navigation-presentation.ts +156 -0
  28. package/src/components/editor/floorplan-panel.tsx +2517 -1492
  29. package/src/components/editor/grid.tsx +166 -39
  30. package/src/components/editor/group-actions.ts +597 -0
  31. package/src/components/editor/group-floating-action-menu.tsx +130 -0
  32. package/src/components/editor/group-move-3d.ts +397 -0
  33. package/src/components/editor/group-rotate-handle.tsx +102 -64
  34. package/src/components/editor/group-selection-box-3d.tsx +173 -0
  35. package/src/components/editor/group-transform-shared.test.ts +192 -1
  36. package/src/components/editor/group-transform-shared.ts +240 -7
  37. package/src/components/editor/handles/handle-arrow.tsx +81 -23
  38. package/src/components/editor/handles/handle-drag-history.test.ts +96 -0
  39. package/src/components/editor/handles/handle-drag-history.ts +14 -0
  40. package/src/components/editor/handles/preview-overrides.test.ts +26 -0
  41. package/src/components/editor/handles/preview-overrides.ts +13 -0
  42. package/src/components/editor/handles/resize-snap.test.ts +52 -0
  43. package/src/components/editor/handles/resize-snap.ts +23 -0
  44. package/src/components/editor/handles/use-handle-drag.ts +49 -6
  45. package/src/components/editor/index.tsx +289 -35
  46. package/src/components/editor/measurement-pill.tsx +68 -23
  47. package/src/components/editor/node-action-menu.tsx +14 -1
  48. package/src/components/editor/node-arrow-handles.tsx +409 -180
  49. package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
  50. package/src/components/editor/quick-measurement-card.tsx +76 -0
  51. package/src/components/editor/quick-measurement-hud.tsx +34 -0
  52. package/src/components/editor/riser-diagram-panel.tsx +137 -0
  53. package/src/components/editor/selection-manager.tsx +814 -466
  54. package/src/components/editor/site-edge-labels.tsx +28 -6
  55. package/src/components/editor/slab-hole-highlights.tsx +13 -5
  56. package/src/components/editor/snapshot-capture-overlay.tsx +258 -115
  57. package/src/components/editor/three-context-bridge.ts +22 -0
  58. package/src/components/editor/thumbnail-generator.tsx +118 -253
  59. package/src/components/editor/use-floorplan-background-placement.ts +113 -45
  60. package/src/components/editor/use-floorplan-hit-testing.test.ts +82 -0
  61. package/src/components/editor/use-floorplan-hit-testing.ts +144 -161
  62. package/src/components/editor/use-floorplan-scene-data.ts +5 -5
  63. package/src/components/editor/use-mesh-settle-epoch.ts +27 -0
  64. package/src/components/editor/wall-measurement-label.tsx +10 -28
  65. package/src/components/editor/wall-move-side-handles.tsx +229 -39
  66. package/src/components/editor/wall-opening-highlights.tsx +63 -32
  67. package/src/components/editor/wall-snap-beacon-layer.tsx +162 -6
  68. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
  69. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +2 -1
  70. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +40 -9
  71. package/src/components/editor-2d/floorplan-cursor-indicator-position.test.ts +31 -0
  72. package/src/components/editor-2d/floorplan-cursor-indicator-position.ts +39 -0
  73. package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
  74. package/src/components/editor-2d/floorplan-group-move.tsx +744 -0
  75. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
  76. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1672 -0
  77. package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
  78. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +72 -0
  79. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +219 -20
  80. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +327 -59
  81. package/src/components/editor-2d/floorplan-render-context.test.ts +27 -0
  82. package/src/components/editor-2d/floorplan-render-context.tsx +113 -8
  83. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
  84. package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +285 -0
  85. package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +516 -0
  86. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +236 -0
  87. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +618 -0
  88. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +369 -0
  89. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +431 -32
  90. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +89 -0
  91. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +106 -0
  92. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +94 -26
  93. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +587 -0
  94. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +2280 -578
  95. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +20 -10
  96. package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
  97. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +425 -61
  98. package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
  99. package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
  100. package/src/components/systems/selection-affordance-manager.tsx +38 -0
  101. package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
  102. package/src/components/systems/zone/zone-system.tsx +42 -13
  103. package/src/components/tools/elevator/elevator-tool.tsx +34 -8
  104. package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
  105. package/src/components/tools/fence/fence-drafting.ts +107 -8
  106. package/src/components/tools/item/move-tool.tsx +17 -13
  107. package/src/components/tools/item/placement-math.test.ts +13 -1
  108. package/src/components/tools/item/placement-math.ts +28 -2
  109. package/src/components/tools/item/placement-strategies.ts +246 -5
  110. package/src/components/tools/item/placement-types.ts +13 -1
  111. package/src/components/tools/item/use-draft-node.ts +146 -57
  112. package/src/components/tools/item/use-placement-coordinator.tsx +813 -125
  113. package/src/components/tools/registry/move-registry-node-tool.tsx +636 -103
  114. package/src/components/tools/roof/roof-tool.tsx +336 -94
  115. package/src/components/tools/select/box-select-state.ts +18 -7
  116. package/src/components/tools/select/box-select-tool.tsx +176 -40
  117. package/src/components/tools/select/marquee-geometry.test.ts +161 -0
  118. package/src/components/tools/select/marquee-geometry.ts +155 -0
  119. package/src/components/tools/select/plane-box-select-tool.tsx +1 -8
  120. package/src/components/tools/select/select-candidates.ts +1 -1
  121. package/src/components/tools/shared/cursor-sphere.tsx +62 -26
  122. package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
  123. package/src/components/tools/shared/facing-indicator.tsx +87 -0
  124. package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
  125. package/src/components/tools/shared/floor-stack-preview.ts +4 -0
  126. package/src/components/tools/shared/horizontal-construction-plane.ts +123 -0
  127. package/src/components/tools/shared/placement-box.tsx +183 -1
  128. package/src/components/tools/shared/pointer-support-cap.test.ts +98 -0
  129. package/src/components/tools/shared/pointer-support-cap.ts +273 -0
  130. package/src/components/tools/shared/polygon-editor.tsx +276 -35
  131. package/src/components/tools/site/site-boundary-editor.tsx +101 -42
  132. package/src/components/tools/site/stroke-pointer-ownership.test.ts +91 -0
  133. package/src/components/tools/site/stroke-pointer-ownership.ts +57 -0
  134. package/src/components/tools/site/terrain-brush-cursor.tsx +247 -0
  135. package/src/components/tools/site/terrain-sculpt-grid.tsx +192 -0
  136. package/src/components/tools/site/terrain-sculpt-tool.tsx +356 -0
  137. package/src/components/tools/stair/stair-click-guard.test.ts +58 -0
  138. package/src/components/tools/stair/stair-click-guard.ts +72 -0
  139. package/src/components/tools/stair/stair-defaults.ts +1 -0
  140. package/src/components/tools/stair/stair-tool.tsx +164 -42
  141. package/src/components/tools/tool-manager.tsx +149 -30
  142. package/src/components/tools/wall/wall-drafting.test.ts +447 -0
  143. package/src/components/tools/wall/wall-drafting.ts +304 -108
  144. package/src/components/tools/wall/wall-snap-geometry.test.ts +52 -0
  145. package/src/components/tools/wall/wall-snap-geometry.ts +137 -9
  146. package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
  147. package/src/components/tools/zone/zone-tool.tsx +98 -88
  148. package/src/components/ui/action-menu/camera-actions.tsx +24 -17
  149. package/src/components/ui/action-menu/control-modes.tsx +44 -94
  150. package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
  151. package/src/components/ui/action-menu/index.tsx +2 -4
  152. package/src/components/ui/action-menu/measurement-control.tsx +267 -0
  153. package/src/components/ui/action-menu/structure-tools.tsx +22 -13
  154. package/src/components/ui/action-menu/view-toggles.tsx +42 -90
  155. package/src/components/ui/command-palette/editor-commands.tsx +16 -3
  156. package/src/components/ui/command-palette/index.tsx +4 -3
  157. package/src/components/ui/controls/material-paint-panel.tsx +93 -18
  158. package/src/components/ui/controls/material-picker.tsx +162 -152
  159. package/src/components/ui/controls/material-properties-editor.tsx +108 -0
  160. package/src/components/ui/controls/metric-control.tsx +118 -44
  161. package/src/components/ui/controls/scene-material-list.tsx +247 -0
  162. package/src/components/ui/controls/slider-control.tsx +88 -38
  163. package/src/components/ui/controls/terrain-sculpt-panel.tsx +193 -0
  164. package/src/components/ui/floating-level-selector.tsx +74 -20
  165. package/src/components/ui/helpers/building-helper.tsx +10 -23
  166. package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
  167. package/src/components/ui/helpers/helper-manager.tsx +273 -14
  168. package/src/components/ui/helpers/item-helper.tsx +28 -32
  169. package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
  170. package/src/components/ui/helpers/roof-helper.tsx +10 -12
  171. package/src/components/ui/icon-ref.tsx +47 -0
  172. package/src/components/ui/item-catalog/catalog-items.tsx +39 -1
  173. package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
  174. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  175. package/src/components/ui/panels/multi-selection-panel.tsx +57 -0
  176. package/src/components/ui/panels/node-display.ts +17 -17
  177. package/src/components/ui/panels/panel-manager.tsx +36 -14
  178. package/src/components/ui/panels/panel-wrapper.tsx +24 -3
  179. package/src/components/ui/panels/parametric-inspector.tsx +19 -4
  180. package/src/components/ui/panels/reference-panel.tsx +54 -19
  181. package/src/components/ui/panels/selection-breakdown.test.ts +24 -0
  182. package/src/components/ui/panels/selection-breakdown.ts +20 -0
  183. package/src/components/ui/primitives/shortcut-token.tsx +39 -3
  184. package/src/components/ui/primitives/sidebar.tsx +1 -0
  185. package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
  186. package/src/components/ui/sidebar/icon-rail.tsx +50 -31
  187. package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
  188. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +82 -41
  189. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +76 -6
  190. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
  191. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -1
  192. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
  193. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
  194. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
  195. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
  196. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +10 -7
  197. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
  198. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
  199. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
  200. package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -17
  201. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
  202. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
  203. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
  204. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
  205. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
  206. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
  207. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
  208. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
  209. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
  210. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +44 -3
  211. package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
  212. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
  213. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
  214. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
  215. package/src/components/ui/sidebar/tab-bar.tsx +42 -28
  216. package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
  217. package/src/components/ui/snap-target-badge.test.tsx +40 -0
  218. package/src/components/ui/snap-target-badge.tsx +88 -0
  219. package/src/components/viewer/viewer-controls-bar.tsx +466 -0
  220. package/src/components/viewer/viewer-scene-header.tsx +235 -0
  221. package/src/components/viewer-overlay.tsx +36 -596
  222. package/src/components/viewer-zone-system.tsx +6 -1
  223. package/src/components/walkthrough-hud.tsx +111 -0
  224. package/src/hooks/use-auto-save.test.ts +14 -0
  225. package/src/hooks/use-auto-save.ts +54 -7
  226. package/src/hooks/use-ceiling-events.ts +3 -2
  227. package/src/hooks/use-drag-action.ts +4 -1
  228. package/src/hooks/use-grid-events.ts +24 -0
  229. package/src/hooks/use-keyboard.ts +438 -70
  230. package/src/index.tsx +355 -9
  231. package/src/lib/active-placement-surface.test.ts +24 -0
  232. package/src/lib/active-placement-surface.ts +42 -0
  233. package/src/lib/camera-pose.test.ts +237 -0
  234. package/src/lib/camera-pose.ts +172 -0
  235. package/src/lib/ceiling-plan-snap.ts +24 -0
  236. package/src/lib/contextual-help.test.ts +112 -0
  237. package/src/lib/contextual-help.ts +144 -0
  238. package/src/lib/continuation.ts +64 -0
  239. package/src/lib/direct-manipulation.test.ts +275 -0
  240. package/src/lib/direct-manipulation.ts +118 -0
  241. package/src/lib/door-interaction.ts +1 -1
  242. package/src/lib/editor-api.ts +23 -35
  243. package/src/lib/elevation-guides.test.ts +107 -0
  244. package/src/lib/elevation-guides.ts +236 -0
  245. package/src/lib/floorplan/annotation-visibility.test.ts +327 -0
  246. package/src/lib/floorplan/annotation-visibility.ts +133 -0
  247. package/src/lib/floorplan/apply-alignment.test.ts +25 -0
  248. package/src/lib/floorplan/apply-alignment.ts +18 -12
  249. package/src/lib/floorplan/drawing-coordination.test.ts +46 -0
  250. package/src/lib/floorplan/drawing-coordination.ts +13 -0
  251. package/src/lib/floorplan/floorplan-export.test.ts +319 -0
  252. package/src/lib/floorplan/floorplan-export.tsx +978 -0
  253. package/src/lib/floorplan/floorplan-extension.test.ts +43 -0
  254. package/src/lib/floorplan/floorplan-extension.ts +181 -0
  255. package/src/lib/floorplan/floorplan-mode.test.ts +120 -0
  256. package/src/lib/floorplan/floorplan-mode.ts +73 -0
  257. package/src/lib/floorplan/floorplan-pdfkit-document.ts +194 -0
  258. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +107 -0
  259. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +680 -0
  260. package/src/lib/floorplan/geometry.ts +52 -0
  261. package/src/lib/floorplan/index.ts +3 -0
  262. package/src/lib/floorplan/items.ts +5 -2
  263. package/src/lib/floorplan/plan-coords.ts +21 -0
  264. package/src/lib/fresh-planar-placement.test.ts +283 -3
  265. package/src/lib/fresh-planar-placement.ts +85 -1
  266. package/src/lib/glb-export.test.ts +556 -0
  267. package/src/lib/glb-export.ts +1071 -0
  268. package/src/lib/ground-surface.ts +95 -0
  269. package/src/lib/history.test.ts +96 -0
  270. package/src/lib/history.ts +31 -0
  271. package/src/lib/interaction/hot-set.test.ts +133 -0
  272. package/src/lib/interaction/hot-set.ts +67 -0
  273. package/src/lib/interaction/overlay-policy.test.ts +52 -0
  274. package/src/lib/interaction/overlay-policy.ts +59 -0
  275. package/src/lib/interaction/scope.ts +243 -0
  276. package/src/lib/level-duplication.test.ts +2 -1
  277. package/src/lib/level-selection.ts +2 -2
  278. package/src/lib/material-paint.ts +36 -49
  279. package/src/lib/measurement-kind.test.ts +30 -0
  280. package/src/lib/measurement-kind.ts +19 -0
  281. package/src/lib/measurement-label.test.ts +47 -0
  282. package/src/lib/measurement-label.ts +68 -0
  283. package/src/lib/measurement-parser.ts +116 -0
  284. package/src/lib/measurements.test.ts +188 -0
  285. package/src/lib/measurements.ts +210 -0
  286. package/src/lib/paint-scope.test.ts +454 -0
  287. package/src/lib/paint-scope.ts +461 -0
  288. package/src/lib/placement-drag-release.ts +23 -0
  289. package/src/lib/planar-cursor-placement.test.ts +57 -0
  290. package/src/lib/plugin-panels.test.ts +19 -0
  291. package/src/lib/plugin-panels.ts +91 -0
  292. package/src/lib/quick-action-feedback.ts +33 -0
  293. package/src/lib/quick-action-nodes.test.ts +94 -0
  294. package/src/lib/quick-action-nodes.ts +47 -0
  295. package/src/lib/quick-measurement.test.ts +77 -0
  296. package/src/lib/quick-measurement.ts +109 -0
  297. package/src/lib/roof-duplication.ts +6 -6
  298. package/src/lib/roof-hover-outline-proxy.ts +22 -0
  299. package/src/lib/roof-wall-hit.ts +164 -0
  300. package/src/lib/scene-clipboard.test.ts +325 -0
  301. package/src/lib/scene-clipboard.ts +310 -21
  302. package/src/lib/scene.ts +28 -5
  303. package/src/lib/selection-routing.test.ts +313 -0
  304. package/src/lib/selection-routing.ts +180 -0
  305. package/src/lib/sfx/index.ts +1 -0
  306. package/src/lib/sfx/movement-tick.test.ts +65 -0
  307. package/src/lib/sfx/movement-tick.ts +18 -0
  308. package/src/lib/sfx-bus.ts +82 -14
  309. package/src/lib/sfx-player.test.ts +266 -0
  310. package/src/lib/sfx-player.ts +262 -69
  311. package/src/lib/site-boundary.test.ts +53 -0
  312. package/src/lib/site-boundary.ts +27 -0
  313. package/src/lib/slab-plan-snap.test.ts +93 -0
  314. package/src/lib/slab-plan-snap.ts +108 -0
  315. package/src/lib/snapping-mode.test.ts +165 -0
  316. package/src/lib/snapping-mode.ts +191 -0
  317. package/src/lib/stair-duplication.test.ts +103 -0
  318. package/src/lib/stair-duplication.ts +58 -86
  319. package/src/lib/stair-levels.ts +2 -0
  320. package/src/lib/surface-plan-snap.test.ts +71 -0
  321. package/src/lib/surface-plan-snap.ts +256 -0
  322. package/src/lib/terrain-sculpt.test.ts +429 -0
  323. package/src/lib/terrain-sculpt.ts +301 -0
  324. package/src/lib/terrain-verb-color.test.ts +51 -0
  325. package/src/lib/terrain-verb-color.ts +58 -0
  326. package/src/lib/touch-gesture-priority.test.ts +45 -0
  327. package/src/lib/touch-gesture-priority.ts +30 -0
  328. package/src/lib/use-linear-display.ts +40 -0
  329. package/src/lib/window-interaction.ts +1 -1
  330. package/src/lib/world-grid-snap.ts +37 -5
  331. package/src/store/camera-pose-store.test.ts +29 -0
  332. package/src/store/camera-pose-store.ts +27 -0
  333. package/src/store/live-draft-preview-stores.test.ts +136 -0
  334. package/src/store/navigation-sync-pose-store.test.ts +34 -0
  335. package/src/store/navigation-sync-pose-store.ts +27 -0
  336. package/src/store/terrain-sculpt-mode.test.ts +223 -0
  337. package/src/store/use-delete-confirmation.ts +27 -0
  338. package/src/store/use-direct-manipulation-feedback.ts +20 -0
  339. package/src/store/use-drawing-view.test.ts +23 -0
  340. package/src/store/use-drawing-view.ts +84 -0
  341. package/src/store/use-editor.tsx +665 -126
  342. package/src/store/use-elevation-guides.ts +40 -0
  343. package/src/store/use-facing-pose.ts +44 -0
  344. package/src/store/use-fence-curve-draft.ts +51 -0
  345. package/src/store/use-first-person-hud.ts +40 -0
  346. package/src/store/use-floorplan-annotation-visibility.ts +60 -0
  347. package/src/store/use-floorplan-draft-preview.ts +151 -0
  348. package/src/store/use-floorplan-marquee.ts +50 -0
  349. package/src/store/use-floorplan-mode.ts +110 -0
  350. package/src/store/use-interaction-scope.test.ts +206 -0
  351. package/src/store/use-interaction-scope.ts +140 -0
  352. package/src/store/use-measurement-draft.test.ts +559 -0
  353. package/src/store/use-measurement-draft.ts +570 -0
  354. package/src/store/use-opening-guides.ts +41 -0
  355. package/src/store/use-path-draft-preview.ts +90 -0
  356. package/src/store/use-placement-preview.ts +11 -3
  357. package/src/store/use-quick-measurement-hud.test.ts +53 -0
  358. package/src/store/use-quick-measurement-hud.ts +77 -0
  359. package/src/store/use-segment-draft-chain.ts +28 -0
  360. package/src/store/use-stair-build-preview.ts +53 -0
  361. package/src/store/use-wall-snap-indicator.ts +2 -0
  362. package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
  363. package/src/components/editor/group-move-handle.tsx +0 -316
  364. package/src/components/ui/panels/paint-panel.tsx +0 -163
  365. package/src/lib/floorplan/selection-tool.ts +0 -271
  366. package/src/lib/level-name.ts +0 -11
@@ -0,0 +1,40 @@
1
+ import { create } from 'zustand'
2
+
3
+ export type ElevationGuide3D = {
4
+ ownerId: string
5
+ levelId: string
6
+ center: readonly [number, number]
7
+ direction: readonly [number, number]
8
+ elevation: number
9
+ label: string
10
+ }
11
+
12
+ type ElevationGuidesState = {
13
+ guide: ElevationGuide3D | null
14
+ publish(guide: ElevationGuide3D): void
15
+ clear(ownerId: string): void
16
+ }
17
+
18
+ const useElevationGuides = create<ElevationGuidesState>((set) => ({
19
+ guide: null,
20
+ publish: (guide) =>
21
+ set((state) => {
22
+ const current = state.guide
23
+ if (
24
+ current?.ownerId === guide.ownerId &&
25
+ current.levelId === guide.levelId &&
26
+ current.elevation === guide.elevation &&
27
+ current.label === guide.label &&
28
+ current.center[0] === guide.center[0] &&
29
+ current.center[1] === guide.center[1] &&
30
+ current.direction[0] === guide.direction[0] &&
31
+ current.direction[1] === guide.direction[1]
32
+ ) {
33
+ return state
34
+ }
35
+ return { guide }
36
+ }),
37
+ clear: (ownerId) => set((state) => (state.guide?.ownerId === ownerId ? { guide: null } : state)),
38
+ }))
39
+
40
+ export default useElevationGuides
@@ -0,0 +1,44 @@
1
+ // Ephemeral store for the forward-facing floor triangle shown while placing or
2
+ // moving a node. A single editor-side overlay (`<FacingPoseIndicator>`)
3
+ // subscribes and renders the triangle; every placement/move path publishes its
4
+ // ghost pose here instead of drawing its own triangle. This is deliberately the
5
+ // one renderer for the facing indicator: rendering it from inside a tool's own
6
+ // cursor ghost (especially tools living in `@pascal-app/nodes`) left it
7
+ // invisible, while the editor-side overlay renders reliably. Producers clear on
8
+ // commit, cancel, and unmount.
9
+ //
10
+ // Poses are in the same building-local frame the tools already work in (the
11
+ // overlay is mounted inside ToolManager's building-local group).
12
+
13
+ import { create } from 'zustand'
14
+
15
+ export type FacingPose = {
16
+ /** Ghost origin in building-local space. */
17
+ position: [number, number, number]
18
+ /** Ghost yaw (radians). The triangle inherits this so it points where the
19
+ * node faces. */
20
+ rotationY: number
21
+ /** Footprint depth along the ghost's local +Z; the triangle sits just past
22
+ * `center[1] + depth / 2`. */
23
+ depth: number
24
+ /** Footprint centre offset `[x, z]` in the ghost's local frame. Defaults to
25
+ * the origin. Kinds whose forward edge isn't centred on the origin (e.g. a
26
+ * stair, whose run starts at the entry) shift the triangle via this. */
27
+ center?: [number, number]
28
+ /** Point along local -Z (the front is the -Z side) instead of +Z. */
29
+ reversed?: boolean
30
+ }
31
+
32
+ type FacingPoseState = {
33
+ pose: FacingPose | null
34
+ set(pose: FacingPose): void
35
+ clear(): void
36
+ }
37
+
38
+ const useFacingPose = create<FacingPoseState>((set) => ({
39
+ pose: null,
40
+ set: (pose) => set({ pose }),
41
+ clear: () => set({ pose: null }),
42
+ }))
43
+
44
+ export default useFacingPose
@@ -0,0 +1,51 @@
1
+ // Ephemeral store for the in-progress curved-fence draft. Written by the 3D
2
+ // spline tool and read by the contextual helper plus out-of-tree consumers.
3
+ // Reset on commit, cancel, and unmount — never persisted or recorded in undo
4
+ // history.
5
+
6
+ import { create } from 'zustand'
7
+
8
+ export type FenceCurveDraftPoint = [number, number]
9
+
10
+ type FenceCurveDraftState = {
11
+ pointCount: number
12
+ points: FenceCurveDraftPoint[]
13
+ cursor: FenceCurveDraftPoint | null
14
+ setDraft(points: readonly FenceCurveDraftPoint[], cursor: FenceCurveDraftPoint | null): void
15
+ reset(): void
16
+ }
17
+
18
+ function pointsEqual(a: readonly FenceCurveDraftPoint[], b: readonly FenceCurveDraftPoint[]) {
19
+ return (
20
+ a.length === b.length &&
21
+ a.every((point, index) => point[0] === b[index]?.[0] && point[1] === b[index]?.[1])
22
+ )
23
+ }
24
+
25
+ const useFenceCurveDraft = create<FenceCurveDraftState>((set) => ({
26
+ pointCount: 0,
27
+ points: [],
28
+ cursor: null,
29
+ setDraft: (points, cursor) =>
30
+ set((state) => {
31
+ const sameCursor =
32
+ (!cursor && !state.cursor) ||
33
+ Boolean(
34
+ cursor && state.cursor && state.cursor[0] === cursor[0] && state.cursor[1] === cursor[1],
35
+ )
36
+ if (sameCursor && pointsEqual(state.points, points)) return state
37
+ return {
38
+ pointCount: points.length,
39
+ points: points.map(([x, z]) => [x, z] as FenceCurveDraftPoint),
40
+ cursor: cursor ? [cursor[0], cursor[1]] : null,
41
+ }
42
+ }),
43
+ reset: () =>
44
+ set((state) =>
45
+ state.pointCount === 0 && state.points.length === 0 && state.cursor === null
46
+ ? state
47
+ : { pointCount: 0, points: [], cursor: null },
48
+ ),
49
+ }))
50
+
51
+ export default useFenceCurveDraft
@@ -0,0 +1,40 @@
1
+ import { create } from 'zustand'
2
+
3
+ export type WalkthroughInteract = { label: string; verb: string } | null
4
+
5
+ export type FirstPersonHudState = {
6
+ floorLabel: string | null
7
+ zoneLabel: string | null
8
+ interact: WalkthroughInteract
9
+ setHud: (hud: Partial<Pick<FirstPersonHudState, 'floorLabel' | 'zoneLabel' | 'interact'>>) => void
10
+ reset: () => void
11
+ }
12
+
13
+ export const useFirstPersonHud = create<FirstPersonHudState>((set) => ({
14
+ floorLabel: null,
15
+ zoneLabel: null,
16
+ interact: null,
17
+ setHud: (hud) =>
18
+ set((state) => {
19
+ const floorLabel = hud.floorLabel === undefined ? state.floorLabel : hud.floorLabel
20
+ const zoneLabel = hud.zoneLabel === undefined ? state.zoneLabel : hud.zoneLabel
21
+ const interact = hud.interact === undefined ? state.interact : hud.interact
22
+
23
+ if (
24
+ floorLabel === state.floorLabel &&
25
+ zoneLabel === state.zoneLabel &&
26
+ interact?.label === state.interact?.label &&
27
+ interact?.verb === state.interact?.verb
28
+ ) {
29
+ return state
30
+ }
31
+
32
+ return { floorLabel, zoneLabel, interact }
33
+ }),
34
+ reset: () =>
35
+ set((state) =>
36
+ state.floorLabel === null && state.zoneLabel === null && state.interact === null
37
+ ? state
38
+ : { floorLabel: null, zoneLabel: null, interact: null },
39
+ ),
40
+ }))
@@ -0,0 +1,60 @@
1
+ 'use client'
2
+
3
+ import { create } from 'zustand'
4
+ import { persist } from 'zustand/middleware'
5
+ import {
6
+ DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY,
7
+ type FloorplanAnnotationCategory,
8
+ type FloorplanAnnotationVisibility,
9
+ normalizeFloorplanAnnotationVisibility,
10
+ } from '../lib/floorplan/annotation-visibility'
11
+ import {
12
+ DEFAULT_FLOORPLAN_WALL_DIMENSION_REFERENCE,
13
+ type FloorplanWallDimensionReference,
14
+ normalizeFloorplanWallDimensionReference,
15
+ } from '../lib/floorplan/floorplan-extension'
16
+
17
+ type FloorplanAnnotationVisibilityState = {
18
+ visibility: FloorplanAnnotationVisibility
19
+ wallDimensionReference: FloorplanWallDimensionReference
20
+ setCategory: (category: FloorplanAnnotationCategory, visible: boolean) => void
21
+ setWallDimensionReference: (reference: FloorplanWallDimensionReference) => void
22
+ reset: () => void
23
+ }
24
+
25
+ const useFloorplanAnnotationVisibility = create<FloorplanAnnotationVisibilityState>()(
26
+ persist(
27
+ (set) => ({
28
+ visibility: { ...DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY },
29
+ wallDimensionReference: DEFAULT_FLOORPLAN_WALL_DIMENSION_REFERENCE,
30
+ setCategory: (category, visible) =>
31
+ set((state) => ({ visibility: { ...state.visibility, [category]: visible } })),
32
+ setWallDimensionReference: (wallDimensionReference) => set({ wallDimensionReference }),
33
+ reset: () =>
34
+ set({
35
+ visibility: { ...DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY },
36
+ wallDimensionReference: DEFAULT_FLOORPLAN_WALL_DIMENSION_REFERENCE,
37
+ }),
38
+ }),
39
+ {
40
+ name: 'pascal-floorplan-annotation-visibility',
41
+ merge: (persistedState, currentState) => ({
42
+ ...currentState,
43
+ visibility: normalizeFloorplanAnnotationVisibility(
44
+ (persistedState as { visibility?: unknown } | undefined)?.visibility,
45
+ ),
46
+ wallDimensionReference: normalizeFloorplanWallDimensionReference(
47
+ (persistedState as { wallDimensionReference?: unknown } | undefined)
48
+ ?.wallDimensionReference,
49
+ ),
50
+ }),
51
+ partialize: (state) =>
52
+ ({
53
+ visibility: state.visibility,
54
+ wallDimensionReference: state.wallDimensionReference,
55
+ }) as FloorplanAnnotationVisibilityState,
56
+ },
57
+ ),
58
+ )
59
+
60
+ export default useFloorplanAnnotationVisibility
@@ -0,0 +1,151 @@
1
+ // Ephemeral store for the 2D floor-plan's in-flight DRAFT preview state — the
2
+ // hot, per-pointer-move values every build/edit tool republishes on `grid:move`
3
+ // (the snapped cursor point today; wall/fence/roof draft endpoints as later
4
+ // slices land here). It exists so those per-move updates DON'T live in
5
+ // `FloorplanPanel`'s own `useState`: the panel is a ~10k-line component whose
6
+ // render costs ~120-220ms, so a `setState` per move made every 2D draft tool
7
+ // feel laggy. Producers write via `getState().setX(...)` (no panel re-render);
8
+ // the small overlay leaves subscribe and re-render alone. Same pattern that
9
+ // keeps stair / column / elevator placement smooth (`useStairBuildPreview`,
10
+ // `usePlacementPreview`).
11
+ //
12
+ // Editor-only. Producers clear on tool-inactive, commit, and unmount.
13
+
14
+ import type { WallPlanPoint } from '@pascal-app/core'
15
+ import { create } from 'zustand'
16
+
17
+ /** Screen-space (SVG-local px) cursor point — drives the coordinate badge. */
18
+ type SvgPoint = { x: number; y: number }
19
+
20
+ export type FloorplanPolygonDraftType = 'ceiling' | 'slab' | 'zone'
21
+
22
+ type FloorplanDraftPreviewState = {
23
+ /** Snapped plan-XZ point under the cursor; drives the crosshair + the
24
+ * cursor-following polygon-draft preview. `null` when idle. */
25
+ cursorPoint: WallPlanPoint | null
26
+ /** Screen-space cursor point driving the coordinate-indicator badge. Set on
27
+ * every SVG `pointermove` while a build/select tool is active, so it's the
28
+ * single hottest 2D update — keeping it out of panel state is what stops the
29
+ * panel re-rendering per move. `null` when idle. */
30
+ cursorPosition: SvgPoint | null
31
+ /** Live END point of the open wall / fence / roof draft segment — the per-move
32
+ * endpoint that drives the 2D draft polygon + measurement. Each is `null`
33
+ * unless that tool's draft is open. The START points are mirrored here (set
34
+ * per click / per 3D draft move) so out-of-tree consumers — e.g. the hosted
35
+ * host-owned preview publishers — can observe the whole open
36
+ * segment; panel state remains the 2D interaction source of truth. */
37
+ wallDraftEnd: WallPlanPoint | null
38
+ fenceDraftEnd: WallPlanPoint | null
39
+ roofDraftEnd: WallPlanPoint | null
40
+ wallDraftStart: WallPlanPoint | null
41
+ fenceDraftStart: WallPlanPoint | null
42
+ roofDraftStart: WallPlanPoint | null
43
+ polygonDraftType: FloorplanPolygonDraftType | null
44
+ polygonDraftPoints: WallPlanPoint[]
45
+ /** Set the snapped cursor point. No-ops (skips the store update, so
46
+ * subscribers don't re-render) when unchanged — `grid:move` fires far more
47
+ * often than the snapped cell actually changes. */
48
+ setCursorPoint(point: WallPlanPoint | null): void
49
+ /** Set the screen-space cursor point (deduped on x/y). */
50
+ setCursorPosition(point: SvgPoint | null): void
51
+ setWallDraftEnd(point: WallPlanPoint | null): void
52
+ setFenceDraftEnd(point: WallPlanPoint | null): void
53
+ setRoofDraftEnd(point: WallPlanPoint | null): void
54
+ setWallDraftStart(point: WallPlanPoint | null): void
55
+ setFenceDraftStart(point: WallPlanPoint | null): void
56
+ setRoofDraftStart(point: WallPlanPoint | null): void
57
+ setPolygonDraft(type: FloorplanPolygonDraftType | null, points: readonly WallPlanPoint[]): void
58
+ reset(): void
59
+ }
60
+
61
+ function planPointsEqual(a: readonly WallPlanPoint[], b: readonly WallPlanPoint[]) {
62
+ return (
63
+ a.length === b.length &&
64
+ a.every((point, index) => point[0] === b[index]?.[0] && point[1] === b[index]?.[1])
65
+ )
66
+ }
67
+
68
+ function setPlanPointField(
69
+ field:
70
+ | 'fenceDraftEnd'
71
+ | 'fenceDraftStart'
72
+ | 'roofDraftEnd'
73
+ | 'roofDraftStart'
74
+ | 'wallDraftEnd'
75
+ | 'wallDraftStart',
76
+ point: WallPlanPoint | null,
77
+ ) {
78
+ return (
79
+ state: FloorplanDraftPreviewState,
80
+ ): Partial<FloorplanDraftPreviewState> | typeof state => {
81
+ const prev = state[field]
82
+ if (!point && !prev) return state
83
+ if (point && prev && prev[0] === point[0] && prev[1] === point[1]) return state
84
+ return { [field]: point }
85
+ }
86
+ }
87
+
88
+ export const useFloorplanDraftPreview = create<FloorplanDraftPreviewState>((set) => ({
89
+ cursorPoint: null,
90
+ cursorPosition: null,
91
+ wallDraftEnd: null,
92
+ fenceDraftEnd: null,
93
+ roofDraftEnd: null,
94
+ wallDraftStart: null,
95
+ fenceDraftStart: null,
96
+ roofDraftStart: null,
97
+ polygonDraftType: null,
98
+ polygonDraftPoints: [],
99
+ setCursorPoint: (point) =>
100
+ set((state) => {
101
+ const prev = state.cursorPoint
102
+ if (!point && !prev) return state
103
+ if (point && prev && prev[0] === point[0] && prev[1] === point[1]) return state
104
+ return { cursorPoint: point }
105
+ }),
106
+ setCursorPosition: (point) =>
107
+ set((state) => {
108
+ const prev = state.cursorPosition
109
+ if (!point && !prev) return state
110
+ if (point && prev && prev.x === point.x && prev.y === point.y) return state
111
+ return { cursorPosition: point }
112
+ }),
113
+ setWallDraftEnd: (point) => set(setPlanPointField('wallDraftEnd', point)),
114
+ setFenceDraftEnd: (point) => set(setPlanPointField('fenceDraftEnd', point)),
115
+ setRoofDraftEnd: (point) => set(setPlanPointField('roofDraftEnd', point)),
116
+ setWallDraftStart: (point) => set(setPlanPointField('wallDraftStart', point)),
117
+ setFenceDraftStart: (point) => set(setPlanPointField('fenceDraftStart', point)),
118
+ setRoofDraftStart: (point) => set(setPlanPointField('roofDraftStart', point)),
119
+ setPolygonDraft: (type, points) =>
120
+ set((state) =>
121
+ state.polygonDraftType === type && planPointsEqual(state.polygonDraftPoints, points)
122
+ ? state
123
+ : { polygonDraftType: type, polygonDraftPoints: points.map(([x, z]) => [x, z]) },
124
+ ),
125
+ reset: () =>
126
+ set((state) =>
127
+ state.cursorPoint === null &&
128
+ state.cursorPosition === null &&
129
+ state.wallDraftEnd === null &&
130
+ state.fenceDraftEnd === null &&
131
+ state.roofDraftEnd === null &&
132
+ state.wallDraftStart === null &&
133
+ state.fenceDraftStart === null &&
134
+ state.roofDraftStart === null &&
135
+ state.polygonDraftType === null &&
136
+ state.polygonDraftPoints.length === 0
137
+ ? state
138
+ : {
139
+ cursorPoint: null,
140
+ cursorPosition: null,
141
+ wallDraftEnd: null,
142
+ fenceDraftEnd: null,
143
+ roofDraftEnd: null,
144
+ wallDraftStart: null,
145
+ fenceDraftStart: null,
146
+ roofDraftStart: null,
147
+ polygonDraftType: null,
148
+ polygonDraftPoints: [],
149
+ },
150
+ ),
151
+ }))
@@ -0,0 +1,50 @@
1
+ // Ephemeral store for the 2D floor-plan marquee (box-select) drag — the hot,
2
+ // per-pointer-move rectangle the select tool republishes on every move. It
3
+ // lives here, not in `FloorplanPanel`'s `useState`, for the same reason as
4
+ // `useFloorplanDraftPreview`: the panel is a ~10k-line component whose render
5
+ // costs ~120-220ms, so a `setState` per move made dragging a selection box
6
+ // re-render the whole panel. Producers write via `getState()` (no panel
7
+ // re-render); the small marquee overlay leaf subscribes and re-renders alone.
8
+ //
9
+ // The whole drag struct lives here (not just the moving corner) so the
10
+ // pointer-move / -up handlers read it non-reactively via `getState()` and the
11
+ // panel never subscribes. Editor-only; reset on pointer-up / cancel /
12
+ // tool-inactive.
13
+
14
+ import type { WallPlanPoint } from '@pascal-app/core'
15
+ import { create } from 'zustand'
16
+
17
+ export type FloorplanMarqueeDrag = {
18
+ pointerId: number
19
+ startClientX: number
20
+ startClientY: number
21
+ startPlanPoint: WallPlanPoint
22
+ /** Moving corner under the cursor — the only field that changes per move. */
23
+ currentPlanPoint: WallPlanPoint
24
+ }
25
+
26
+ type FloorplanMarqueeState = {
27
+ drag: FloorplanMarqueeDrag | null
28
+ begin(drag: FloorplanMarqueeDrag): void
29
+ /** Advance the moving corner. No-ops (skips the store update, so the overlay
30
+ * doesn't re-render) when the snapped point is unchanged or no drag is open. */
31
+ setCurrent(point: WallPlanPoint): void
32
+ reset(): void
33
+ }
34
+
35
+ export const useFloorplanMarquee = create<FloorplanMarqueeState>((set) => ({
36
+ drag: null,
37
+ begin: (drag) => set({ drag }),
38
+ setCurrent: (point) =>
39
+ set((state) => {
40
+ const prev = state.drag
41
+ if (!prev) return state
42
+ if (prev.currentPlanPoint[0] === point[0] && prev.currentPlanPoint[1] === point[1]) {
43
+ return state
44
+ }
45
+ return { drag: { ...prev, currentPlanPoint: point } }
46
+ }),
47
+ reset: () => set((state) => (state.drag === null ? state : { drag: null })),
48
+ }))
49
+
50
+ export default useFloorplanMarquee
@@ -0,0 +1,110 @@
1
+ 'use client'
2
+
3
+ import { create } from 'zustand'
4
+ import { persist } from 'zustand/middleware'
5
+ import {
6
+ DEFAULT_FLOORPLAN_MODE,
7
+ type FloorplanMode,
8
+ normalizeFloorplanModesByProject,
9
+ } from '../lib/floorplan/floorplan-mode'
10
+
11
+ export type FloorplanModeNotice = {
12
+ id: number
13
+ kind: 'info' | 'switch-to-expert'
14
+ message: string
15
+ }
16
+
17
+ type FloorplanModeState = {
18
+ mode: FloorplanMode
19
+ projectId: string | null
20
+ modesByProject: Record<string, FloorplanMode>
21
+ hasShownDefaultReassurance: boolean
22
+ notice: FloorplanModeNotice | null
23
+ dismissNotice: () => void
24
+ setMode: (mode: FloorplanMode) => void
25
+ setProjectId: (projectId: string | null) => void
26
+ showExpertModeNotice: (toolLabel: string) => void
27
+ showNotice: (message: string) => void
28
+ }
29
+
30
+ let nextNoticeId = 0
31
+
32
+ const useFloorplanMode = create<FloorplanModeState>()(
33
+ persist(
34
+ (set) => ({
35
+ mode: DEFAULT_FLOORPLAN_MODE,
36
+ projectId: null,
37
+ modesByProject: {},
38
+ hasShownDefaultReassurance: false,
39
+ notice: null,
40
+ dismissNotice: () => set({ notice: null }),
41
+ setMode: (mode) =>
42
+ set((state) => {
43
+ const shouldReassure =
44
+ state.mode === 'expert' && mode === 'default' && !state.hasShownDefaultReassurance
45
+ return {
46
+ mode,
47
+ modesByProject: state.projectId
48
+ ? { ...state.modesByProject, [state.projectId]: mode }
49
+ : state.modesByProject,
50
+ hasShownDefaultReassurance: state.hasShownDefaultReassurance || shouldReassure,
51
+ notice: shouldReassure
52
+ ? {
53
+ id: ++nextNoticeId,
54
+ kind: 'info',
55
+ message:
56
+ 'Default mode hides Expert annotations. Your saved work was not deleted.',
57
+ }
58
+ : state.notice,
59
+ }
60
+ }),
61
+ setProjectId: (projectId) =>
62
+ set((state) => ({
63
+ projectId,
64
+ mode: projectId
65
+ ? (state.modesByProject[projectId] ?? DEFAULT_FLOORPLAN_MODE)
66
+ : DEFAULT_FLOORPLAN_MODE,
67
+ notice: null,
68
+ })),
69
+ showExpertModeNotice: (toolLabel) =>
70
+ set({
71
+ notice: {
72
+ id: ++nextNoticeId,
73
+ kind: 'switch-to-expert',
74
+ message: `${toolLabel} is available in Expert mode.`,
75
+ },
76
+ }),
77
+ showNotice: (message) =>
78
+ set({
79
+ notice: {
80
+ id: ++nextNoticeId,
81
+ kind: 'info',
82
+ message,
83
+ },
84
+ }),
85
+ }),
86
+ {
87
+ name: 'pascal-floorplan-mode-by-project',
88
+ merge: (persistedState, currentState) => {
89
+ const persisted = persistedState as
90
+ | { hasShownDefaultReassurance?: unknown; modesByProject?: unknown }
91
+ | undefined
92
+ const modesByProject = normalizeFloorplanModesByProject(persisted?.modesByProject)
93
+ return {
94
+ ...currentState,
95
+ modesByProject,
96
+ hasShownDefaultReassurance: persisted?.hasShownDefaultReassurance === true,
97
+ mode: currentState.projectId
98
+ ? (modesByProject[currentState.projectId] ?? DEFAULT_FLOORPLAN_MODE)
99
+ : DEFAULT_FLOORPLAN_MODE,
100
+ }
101
+ },
102
+ partialize: (state) => ({
103
+ hasShownDefaultReassurance: state.hasShownDefaultReassurance,
104
+ modesByProject: state.modesByProject,
105
+ }),
106
+ },
107
+ ),
108
+ )
109
+
110
+ export default useFloorplanMode