@pascal-app/editor 1.0.3 → 1.0.4

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 (440) hide show
  1. package/package.json +11 -9
  2. package/src/components/editor/action-menu-button.tsx +83 -0
  3. package/src/components/editor/bake-thumbnail.tsx +315 -4
  4. package/src/components/editor/capture-level-picker.tsx +72 -0
  5. package/src/components/editor/custom-camera-controls.tsx +202 -57
  6. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  7. package/src/components/editor/duplicate-node.ts +244 -0
  8. package/src/components/editor/editor-layout-mobile.tsx +36 -25
  9. package/src/components/editor/editor-layout-v2.tsx +131 -58
  10. package/src/components/editor/export-manager.tsx +34 -8
  11. package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
  12. package/src/components/editor/first-person/build-collider-world.ts +6 -2
  13. package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
  14. package/src/components/editor/first-person/mechanism-targets.ts +36 -0
  15. package/src/components/editor/first-person-controls.tsx +205 -2
  16. package/src/components/editor/floating-action-menu.tsx +63 -207
  17. package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
  18. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  19. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  20. package/src/components/editor/floor-region-controls.tsx +846 -0
  21. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  22. package/src/components/editor/floor-surface.test.ts +165 -0
  23. package/src/components/editor/floor-surface.ts +182 -0
  24. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  25. package/src/components/editor/floorplan-background-selection.ts +1 -3
  26. package/src/components/editor/floorplan-panel.tsx +344 -342
  27. package/src/components/editor/footprint-height-handle.tsx +204 -0
  28. package/src/components/editor/group-actions.ts +54 -2
  29. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  30. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  31. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  32. package/src/components/editor/handles/use-handle-drag.ts +269 -197
  33. package/src/components/editor/index.tsx +294 -169
  34. package/src/components/editor/join-walls-pill.tsx +44 -0
  35. package/src/components/editor/node-action-menu.tsx +68 -70
  36. package/src/components/editor/node-arrow-handles.tsx +25 -6
  37. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  38. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  39. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  40. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  41. package/src/components/editor/registry-action-contributions.tsx +80 -1
  42. package/src/components/editor/room-controls.tsx +847 -0
  43. package/src/components/editor/room-floor-highlight.tsx +128 -0
  44. package/src/components/editor/room-handles-2d.tsx +354 -0
  45. package/src/components/editor/room-handles.tsx +675 -0
  46. package/src/components/editor/room-highlight.tsx +225 -0
  47. package/src/components/editor/room-labels.test.ts +357 -0
  48. package/src/components/editor/room-labels.tsx +990 -0
  49. package/src/components/editor/room-transform-ghost.tsx +256 -0
  50. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  51. package/src/components/editor/selection-manager.tsx +451 -317
  52. package/src/components/editor/slab-hole-highlights.tsx +3 -0
  53. package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
  54. package/src/components/editor/snapshot-capture.test.ts +52 -4
  55. package/src/components/editor/snapshot-capture.ts +30 -5
  56. package/src/components/editor/thumbnail-generator.test.ts +308 -0
  57. package/src/components/editor/thumbnail-generator.tsx +1035 -229
  58. package/src/components/editor/use-floorplan-background-placement.ts +31 -11
  59. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  60. package/src/components/editor/views/host-view.tsx +61 -0
  61. package/src/components/editor/views/plugins-view.tsx +670 -0
  62. package/src/components/editor/views/scene-region.tsx +59 -0
  63. package/src/components/editor/views/use-editor-views.ts +57 -0
  64. package/src/components/editor/views/view-bar.tsx +220 -0
  65. package/src/components/editor/wall-justification.test.ts +140 -0
  66. package/src/components/editor/wall-move-side-handles.tsx +43 -142
  67. package/src/components/editor/wall-push-arrow.tsx +92 -0
  68. package/src/components/editor/wall-region-handles.tsx +509 -0
  69. package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
  70. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  71. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  72. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  73. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  74. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +113 -102
  75. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  76. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  77. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  78. package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
  79. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  80. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
  81. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +291 -227
  82. package/src/components/editor-2d/svg-paths.ts +12 -15
  83. package/src/components/procedural-items/inspector.tsx +40 -15
  84. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  85. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  86. package/src/components/systems/zone/zone-system.test.ts +47 -0
  87. package/src/components/systems/zone/zone-system.tsx +29 -68
  88. package/src/components/tools/item/placement-history.test.tsx +474 -0
  89. package/src/components/tools/item/placement-strategies.ts +8 -4
  90. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  91. package/src/components/tools/item/use-draft-node.ts +166 -95
  92. package/src/components/tools/item/use-placement-coordinator.tsx +93 -46
  93. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  94. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  95. package/src/components/tools/registry/move-registry-node-tool.tsx +3 -1
  96. package/src/components/tools/select/box-select-tool.tsx +2 -12
  97. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  98. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  99. package/src/components/tools/select/select-candidates.test.ts +37 -4
  100. package/src/components/tools/select/select-candidates.ts +15 -11
  101. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  102. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  103. package/src/components/tools/stair/stair-defaults.ts +0 -3
  104. package/src/components/tools/stair/stair-tool.tsx +90 -42
  105. package/src/components/tools/tool-manager.tsx +46 -20
  106. package/src/components/tools/wall/wall-drafting.test.ts +19 -4
  107. package/src/components/tools/zone/zone-tool.tsx +4 -12
  108. package/src/components/ui/action-menu/action-button.tsx +3 -0
  109. package/src/components/ui/action-menu/camera-actions.tsx +2 -1
  110. package/src/components/ui/action-menu/control-modes.tsx +2 -35
  111. package/src/components/ui/action-menu/measurement-control.tsx +3 -10
  112. package/src/components/ui/build-panel/build-panel.tsx +235 -0
  113. package/src/components/ui/command-palette/editor-commands.tsx +4 -30
  114. package/src/components/ui/command-palette/index.tsx +5 -24
  115. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  116. package/src/components/ui/controls/material-paint-panel.tsx +97 -26
  117. package/src/components/ui/controls/material-picker.tsx +4 -1
  118. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  119. package/src/components/ui/controls/select-control.tsx +65 -0
  120. package/src/components/ui/floating-level-selector.tsx +9 -14
  121. package/src/components/ui/helpers/building-helper.tsx +4 -1
  122. package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
  123. package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
  124. package/src/components/ui/helpers/helper-manager.tsx +325 -14
  125. package/src/components/ui/helpers/item-helper.tsx +8 -0
  126. package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
  127. package/src/components/ui/icon-ref.tsx +45 -1
  128. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  129. package/src/components/ui/node-icon-image.tsx +16 -0
  130. package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
  131. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  132. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  133. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  134. package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
  135. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  136. package/src/components/ui/panels/multi-field-value.ts +6 -3
  137. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  138. package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
  139. package/src/components/ui/panels/node-display.ts +8 -3
  140. package/src/components/ui/panels/panel-manager.tsx +103 -68
  141. package/src/components/ui/panels/panel-wrapper.tsx +78 -36
  142. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  143. package/src/components/ui/panels/parametric-inspector.tsx +4 -4
  144. package/src/components/ui/panels/reference-panel.tsx +2 -3
  145. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  146. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  147. package/src/components/ui/panels/room-name-options.ts +47 -0
  148. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  149. package/src/components/ui/panels/room-panel.tsx +725 -0
  150. package/src/components/ui/panels/shape-choice.tsx +58 -0
  151. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  152. package/src/components/ui/primitives/color-dot.tsx +36 -24
  153. package/src/components/ui/primitives/sidebar.tsx +2 -0
  154. package/src/components/ui/right-stack.test.ts +29 -0
  155. package/src/components/ui/right-stack.tsx +145 -0
  156. package/src/components/ui/sidebar/icon-rail.tsx +5 -0
  157. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +31 -4
  158. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
  159. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  160. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  161. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  162. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  163. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  164. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  165. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  166. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  167. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  168. package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
  169. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  170. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  171. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  172. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  173. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  174. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  175. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  176. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  177. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  178. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  179. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
  180. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  181. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  182. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  183. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  184. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
  185. package/src/components/ui/sidebar/tab-bar.tsx +10 -0
  186. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  187. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  188. package/src/components/viewer/floorplan-preview.tsx +5 -6
  189. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  190. package/src/components/viewer/viewer-rooms.tsx +175 -0
  191. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  192. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  193. package/src/components/viewer/viewer-stage.tsx +2 -2
  194. package/src/components/viewer-zone-system.tsx +3 -8
  195. package/src/hooks/use-auto-save.integration.test.tsx +123 -9
  196. package/src/hooks/use-auto-save.ts +34 -8
  197. package/src/hooks/use-keyboard.test.ts +178 -3
  198. package/src/hooks/use-keyboard.ts +89 -71
  199. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  200. package/src/hooks/use-open-wall-ends.ts +238 -0
  201. package/src/hooks/use-placement-notice.test.ts +48 -0
  202. package/src/hooks/use-placement-notice.ts +42 -0
  203. package/src/hooks/use-selected-room.ts +143 -0
  204. package/src/index.tsx +246 -39
  205. package/src/lib/ceiling-edit-session.test.ts +321 -0
  206. package/src/lib/ceiling-edit-session.ts +206 -0
  207. package/src/lib/ceiling-region-session.test.ts +179 -0
  208. package/src/lib/continuation.test.ts +19 -1
  209. package/src/lib/continuation.ts +25 -4
  210. package/src/lib/door-interaction.ts +14 -0
  211. package/src/lib/editor-level-display.test.ts +96 -0
  212. package/src/lib/editor-level-display.ts +48 -0
  213. package/src/lib/editor-views.ts +144 -0
  214. package/src/lib/export-geometry-wait.test.ts +90 -0
  215. package/src/lib/floor-edit-session.ts +153 -0
  216. package/src/lib/floor-footprints.test.ts +563 -0
  217. package/src/lib/floor-footprints.ts +395 -0
  218. package/src/lib/floor-foundation-support.test.ts +283 -0
  219. package/src/lib/floor-opening-draft.ts +218 -0
  220. package/src/lib/floor-openings.test.ts +216 -0
  221. package/src/lib/floor-region-geometry.ts +191 -0
  222. package/src/lib/floor-region-session.test.ts +485 -0
  223. package/src/lib/floor-region-session.ts +589 -0
  224. package/src/lib/floor-region-snap.ts +101 -0
  225. package/src/lib/floor-step-hover.test.ts +32 -0
  226. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  227. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  228. package/src/lib/floorplan/dimension-drive.ts +479 -0
  229. package/src/lib/floorplan/floorplan-export.test.ts +12 -8
  230. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  231. package/src/lib/floorplan/floorplan-extension.ts +32 -0
  232. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  233. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  234. package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
  235. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
  236. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
  237. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  238. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  239. package/src/lib/floorplan/geometry.ts +13 -6
  240. package/src/lib/floorplan/index.ts +0 -9
  241. package/src/lib/floorplan/items.ts +5 -2
  242. package/src/lib/floorplan/marks.test.ts +213 -0
  243. package/src/lib/floorplan/marks.ts +238 -0
  244. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  245. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  246. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  247. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  248. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  249. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  250. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  251. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  252. package/src/lib/floorplan/schedules.test.ts +189 -0
  253. package/src/lib/floorplan/schedules.ts +339 -0
  254. package/src/lib/floorplan/sheet-export.test.ts +100 -0
  255. package/src/lib/floorplan/sheet-export.ts +318 -0
  256. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
  257. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  258. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  259. package/src/lib/floorplan/site-plan/coverage.ts +232 -0
  260. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  261. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  262. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  263. package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
  264. package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
  265. package/src/lib/floorplan/site-plan/geometry.ts +557 -0
  266. package/src/lib/floorplan/site-plan/index.ts +71 -0
  267. package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
  268. package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
  269. package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
  270. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  271. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  272. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  273. package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
  274. package/src/lib/floorplan/types.ts +1 -27
  275. package/src/lib/fresh-planar-placement.test.ts +36 -0
  276. package/src/lib/fresh-planar-placement.ts +59 -36
  277. package/src/lib/geometry-script/author.ts +152 -0
  278. package/src/lib/geometry-script/client.ts +152 -0
  279. package/src/lib/geometry-script/protocol.ts +12 -0
  280. package/src/lib/gesture-lifecycle.test.ts +595 -0
  281. package/src/lib/gesture-lifecycle.ts +290 -0
  282. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  283. package/src/lib/glb-export.test.ts +485 -0
  284. package/src/lib/glb-export.ts +237 -119
  285. package/src/lib/history.test.ts +6 -0
  286. package/src/lib/history.ts +7 -0
  287. package/src/lib/hud-title.ts +109 -0
  288. package/src/lib/hydration-gated-storage.ts +49 -0
  289. package/src/lib/ifc-export.test.ts +174 -0
  290. package/src/lib/ifc-export.ts +201 -0
  291. package/src/lib/ifc-parts.ts +15 -0
  292. package/src/lib/inspector-expanded.test.ts +44 -0
  293. package/src/lib/inspector-expanded.ts +36 -0
  294. package/src/lib/interaction/scope.ts +18 -0
  295. package/src/lib/keyboard-pan.test.ts +129 -7
  296. package/src/lib/keyboard-pan.ts +161 -12
  297. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  298. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  299. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  300. package/src/lib/lot/clean-ring.test.ts +186 -0
  301. package/src/lib/lot/clean-ring.ts +217 -0
  302. package/src/lib/lot/dossier.test.ts +202 -0
  303. package/src/lib/lot/dossier.ts +498 -0
  304. package/src/lib/lot/drop-in.test.ts +326 -0
  305. package/src/lib/lot/drop-in.ts +407 -0
  306. package/src/lib/lot/index.ts +43 -0
  307. package/src/lib/lot/lot-patch.test.ts +340 -0
  308. package/src/lib/lot/lot-patch.ts +394 -0
  309. package/src/lib/lot/parcel-provider.ts +43 -0
  310. package/src/lib/lot/terrain.test.ts +161 -0
  311. package/src/lib/lot/terrain.ts +266 -0
  312. package/src/lib/material-catalog-model.ts +3 -1
  313. package/src/lib/material-paint-panel-model.ts +9 -5
  314. package/src/lib/material-paint.ts +4 -0
  315. package/src/lib/mezzanine-actions.test.ts +342 -0
  316. package/src/lib/mezzanine-draft.test.ts +501 -0
  317. package/src/lib/mezzanine-draft.ts +278 -0
  318. package/src/lib/mezzanine-messages.ts +26 -0
  319. package/src/lib/mezzanine-selection.test.ts +207 -0
  320. package/src/lib/mezzanine-stairs.ts +46 -0
  321. package/src/lib/model-export.ts +3 -1
  322. package/src/lib/node-action-movement.ts +17 -6
  323. package/src/lib/node-display-label.ts +37 -0
  324. package/src/lib/orbit-floor-clearance.test.ts +165 -0
  325. package/src/lib/orbit-floor-clearance.ts +106 -0
  326. package/src/lib/paint-commit-route.test.ts +37 -0
  327. package/src/lib/paint-commit-route.ts +17 -0
  328. package/src/lib/paint-eyedropper.test.tsx +165 -0
  329. package/src/lib/paint-eyedropper.ts +99 -0
  330. package/src/lib/paint-pass-through.test.ts +111 -0
  331. package/src/lib/paint-pass-through.ts +76 -0
  332. package/src/lib/paint-region-hover.ts +25 -0
  333. package/src/lib/paint-region-mode.ts +154 -0
  334. package/src/lib/paint-regions.test.ts +166 -0
  335. package/src/lib/paint-regions.ts +192 -0
  336. package/src/lib/paint-scope.test.ts +317 -35
  337. package/src/lib/paint-scope.ts +266 -25
  338. package/src/lib/panel-rows.ts +21 -0
  339. package/src/lib/parametric-node-update.ts +1 -1
  340. package/src/lib/plate-paint-affected.test.ts +300 -0
  341. package/src/lib/plate-paint-affected.ts +177 -0
  342. package/src/lib/plugin-chat.ts +50 -0
  343. package/src/lib/plugin-panels.test.ts +25 -0
  344. package/src/lib/plugin-panels.ts +94 -0
  345. package/src/lib/print-shell-compiler.ts +1 -0
  346. package/src/lib/registered-draft-snap.ts +12 -0
  347. package/src/lib/roof-duplication.ts +10 -2
  348. package/src/lib/room-assembly-overlay.test.ts +275 -0
  349. package/src/lib/room-assembly-overlay.ts +266 -0
  350. package/src/lib/room-built-on.test.ts +379 -0
  351. package/src/lib/room-built-on.ts +138 -0
  352. package/src/lib/room-construction-commands.ts +712 -0
  353. package/src/lib/room-construction.test.ts +746 -0
  354. package/src/lib/room-construction.ts +385 -0
  355. package/src/lib/room-controls-phase6.test.ts +630 -0
  356. package/src/lib/room-divide-session.ts +523 -0
  357. package/src/lib/room-handle-drag.ts +1068 -0
  358. package/src/lib/room-name-catalog.test.ts +90 -0
  359. package/src/lib/room-name-catalog.ts +168 -0
  360. package/src/lib/room-push-dimensions.test.ts +311 -0
  361. package/src/lib/room-push-dimensions.ts +251 -0
  362. package/src/lib/room-selection-commands.ts +62 -0
  363. package/src/lib/room-selection.test.ts +973 -0
  364. package/src/lib/room-selection.ts +417 -0
  365. package/src/lib/room-structure-commands.ts +94 -0
  366. package/src/lib/room-structure.test.ts +921 -0
  367. package/src/lib/room-transform-session.ts +570 -0
  368. package/src/lib/room-zone-routing.test.ts +294 -0
  369. package/src/lib/room-zone-routing.ts +197 -0
  370. package/src/lib/scene-clipboard.test.ts +364 -0
  371. package/src/lib/scene-clipboard.ts +229 -48
  372. package/src/lib/scene-material-refs.test.ts +79 -0
  373. package/src/lib/scene-material-refs.ts +79 -0
  374. package/src/lib/scene.test.ts +24 -36
  375. package/src/lib/scene.ts +33 -57
  376. package/src/lib/selection-routing.test.ts +129 -45
  377. package/src/lib/selection-routing.ts +53 -74
  378. package/src/lib/session-writes.test.ts +120 -0
  379. package/src/lib/session-writes.ts +205 -0
  380. package/src/lib/sidebar-panel.ts +62 -0
  381. package/src/lib/site-boundary.test.ts +4 -6
  382. package/src/lib/site-boundary.ts +1 -1
  383. package/src/lib/snapping-mode.test.ts +4 -0
  384. package/src/lib/snapping-mode.ts +8 -1
  385. package/src/lib/terrace-draft.test.ts +145 -0
  386. package/src/lib/terrace-draft.ts +213 -0
  387. package/src/lib/tool-cursor-icon.test.ts +16 -0
  388. package/src/lib/tool-cursor-icon.ts +11 -0
  389. package/src/lib/typing-target.ts +9 -0
  390. package/src/lib/units.ts +77 -39
  391. package/src/lib/vector-edges.test.ts +58 -0
  392. package/src/lib/vector-edges.ts +400 -0
  393. package/src/lib/viewer-selection.test.ts +577 -0
  394. package/src/lib/viewer-selection.ts +569 -0
  395. package/src/lib/wall-draw-variant.test.ts +71 -0
  396. package/src/lib/wall-draw-variant.ts +91 -0
  397. package/src/lib/wall-paint-region-session.ts +246 -0
  398. package/src/lib/wall-paint-region.test.ts +373 -0
  399. package/src/lib/wall-polygon-draft.test.ts +262 -0
  400. package/src/lib/wall-polygon-draft.ts +286 -0
  401. package/src/lib/wall-push.test.ts +453 -0
  402. package/src/lib/wall-region-face.ts +352 -0
  403. package/src/lib/wall-region-handles.test.ts +304 -0
  404. package/src/lib/wall-region-handles.ts +368 -0
  405. package/src/lib/wall-region-list.test.ts +119 -0
  406. package/src/lib/wall-region-list.ts +136 -0
  407. package/src/lib/wall-region-snap.ts +110 -0
  408. package/src/lib/window-interaction.ts +14 -0
  409. package/src/lib/zone-content.ts +4 -1
  410. package/src/lib/zone-name.test.ts +17 -0
  411. package/src/lib/zone-name.ts +17 -0
  412. package/src/store/opens-in-select-mode.test.ts +223 -0
  413. package/src/store/paint-mode.test.ts +188 -0
  414. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  415. package/src/store/tool-mode.test.ts +137 -6
  416. package/src/store/use-delete-confirmation.ts +28 -1
  417. package/src/store/use-drawing-view.test.ts +24 -3
  418. package/src/store/use-drawing-view.ts +29 -1
  419. package/src/store/use-editor.tsx +432 -321
  420. package/src/store/use-floorplan-draft-preview.ts +13 -0
  421. package/src/store/use-floorplan-marquee.ts +12 -2
  422. package/src/store/use-quick-measurement-hud.ts +3 -3
  423. package/src/store/use-stair-build-preview.ts +13 -5
  424. package/src/store/use-wall-move-ghosts.ts +4 -2
  425. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  426. package/src/store/use-wall-region-handles-selection.ts +139 -0
  427. package/src/store/view-layout.test.ts +175 -0
  428. package/src/store/view-layout.ts +196 -0
  429. package/src/test-utils/immediate-animation-frames.ts +16 -0
  430. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  431. package/src/test-utils/selection-harness.ts +82 -0
  432. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  433. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  434. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  435. package/src/components/tools/fence/fence-drafting.ts +0 -266
  436. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  437. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  438. package/src/lib/floorplan/stairs.ts +0 -489
  439. package/src/lib/level-duplication.test.ts +0 -144
  440. 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' })
@@ -1,6 +1,33 @@
1
+ import type { DeleteZonePayload } from '@pascal-app/core'
1
2
  import { create } from 'zustand'
2
3
 
3
- type DeleteConfirmationRequest = {
4
+ /** Changing one of a room's construction parts, with what it takes along. */
5
+ export type RoomConstructionChange = {
6
+ part: 'floor' | 'walls' | 'ceiling'
7
+ /** Walls can be confirmed on the way in too; everything else asks on removal. */
8
+ action?: 'add' | 'remove'
9
+ roomName: string
10
+ /**
11
+ * Walls: hosted openings and objects removed with them. Ceiling: items
12
+ * hanging from it. Floor: items standing on it.
13
+ */
14
+ hostedIds: string[]
15
+ /** Walls turned into separators, or built on them. */
16
+ wallCount?: number
17
+ sharedWalls?: boolean
18
+ /** Walls added: clear floor area the room gives up to them, m². */
19
+ areaLoss?: number
20
+ /** Ceiling: names of the hand-drawn ceilings removed ('' when unnamed). */
21
+ manualCeilings?: string[]
22
+ /** Ceiling: other rooms those hand-drawn ceilings also cover. */
23
+ alsoCovers?: string[]
24
+ }
25
+
26
+ export type DeleteConfirmationRequest = {
27
+ room?: DeleteZonePayload
28
+ construction?: RoomConstructionChange
29
+ conflict?: string
30
+ onKeepContents?: () => void
4
31
  count: number
5
32
  onConfirm: () => void
6
33
  }
@@ -1,10 +1,31 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
- import useDrawingView, { normalizeAnnotationLayoutOverrides } from './use-drawing-view'
2
+ import useDrawingView, {
3
+ EDITOR_DRAWING_TYPE_OPTIONS,
4
+ normalizeAnnotationLayoutOverrides,
5
+ normalizeEditorDrawingType,
6
+ } from './use-drawing-view'
3
7
 
4
8
  describe('drawing type', () => {
5
- test('keeps the workspace on the floor plan', () => {
9
+ test('starts on the floor plan', () => {
6
10
  expect(useDrawingView.getState().drawingType).toBe('floor-plan')
7
- expect('setDrawingType' in useDrawingView.getState()).toBe(false)
11
+ })
12
+
13
+ test('switches between the drawing types the editor can render', () => {
14
+ useDrawingView.getState().setDrawingType('site-plan')
15
+ expect(useDrawingView.getState().drawingType).toBe('site-plan')
16
+ useDrawingView.getState().setDrawingType('floor-plan')
17
+ expect(useDrawingView.getState().drawingType).toBe('floor-plan')
18
+ })
19
+
20
+ test('only offers the two types that have a renderer', () => {
21
+ expect(EDITOR_DRAWING_TYPE_OPTIONS.map((o) => o.id)).toEqual(['floor-plan', 'site-plan'])
22
+ })
23
+
24
+ test('a persisted value the editor cannot render falls back to the floor plan', () => {
25
+ expect(normalizeEditorDrawingType('site-plan')).toBe('site-plan')
26
+ expect(normalizeEditorDrawingType('roof-plan')).toBe('floor-plan')
27
+ expect(normalizeEditorDrawingType(undefined)).toBe('floor-plan')
28
+ expect(normalizeEditorDrawingType(42)).toBe('floor-plan')
8
29
  })
9
30
  })
10
31
 
@@ -20,8 +20,24 @@ export type DrawingAnnotationLayoutOverride = {
20
20
 
21
21
  export type DrawingAnnotationLayoutOverrides = Record<string, DrawingAnnotationLayoutOverride>
22
22
 
23
+ /**
24
+ * Drawing types the 2D editor can actually render today. `floor-plan` is the
25
+ * default; `site-plan` renders through
26
+ * `lib/floorplan/site-plan/buildSitePlanDrawing`. The remaining
27
+ * `ConstructionDrawingType` members exist on annotations (they gate dimension
28
+ * visibility) but have no editor renderer yet, so the switch does not offer
29
+ * them — widen this union as each one lands.
30
+ */
31
+ export type EditorDrawingType = Extract<ConstructionDrawingType, 'floor-plan' | 'site-plan'>
32
+
33
+ export const EDITOR_DRAWING_TYPE_OPTIONS = [
34
+ { id: 'floor-plan', label: 'Floor plan' },
35
+ { id: 'site-plan', label: 'Site plan' },
36
+ ] as const satisfies readonly { id: EditorDrawingType; label: string }[]
37
+
23
38
  type DrawingViewState = {
24
- drawingType: Extract<ConstructionDrawingType, 'floor-plan'>
39
+ drawingType: EditorDrawingType
40
+ setDrawingType: (drawingType: EditorDrawingType) => void
25
41
  annotationLayoutOverrides: DrawingAnnotationLayoutOverrides
26
42
  setAnnotationLayoutOverride: (
27
43
  id: string,
@@ -52,10 +68,18 @@ export function normalizeAnnotationLayoutOverrides(
52
68
  return out
53
69
  }
54
70
 
71
+ /** Persisted value → a drawing type the editor can render. Unknown → floor-plan. */
72
+ export function normalizeEditorDrawingType(value: unknown): EditorDrawingType {
73
+ return EDITOR_DRAWING_TYPE_OPTIONS.some((o) => o.id === value)
74
+ ? (value as EditorDrawingType)
75
+ : 'floor-plan'
76
+ }
77
+
55
78
  const useDrawingView = create<DrawingViewState>()(
56
79
  persist(
57
80
  (set) => ({
58
81
  drawingType: 'floor-plan',
82
+ setDrawingType: (drawingType) => set({ drawingType }),
59
83
  annotationLayoutOverrides: {},
60
84
  setAnnotationLayoutOverride: (id, override) =>
61
85
  set((state) => {
@@ -69,12 +93,16 @@ const useDrawingView = create<DrawingViewState>()(
69
93
  name: 'pascal-floorplan-drawing-view',
70
94
  merge: (persistedState, currentState) => ({
71
95
  ...currentState,
96
+ drawingType: normalizeEditorDrawingType(
97
+ (persistedState as { drawingType?: unknown } | undefined)?.drawingType,
98
+ ),
72
99
  annotationLayoutOverrides: normalizeAnnotationLayoutOverrides(
73
100
  (persistedState as { annotationLayoutOverrides?: unknown } | undefined)
74
101
  ?.annotationLayoutOverrides,
75
102
  ),
76
103
  }),
77
104
  partialize: (state) => ({
105
+ drawingType: state.drawingType,
78
106
  annotationLayoutOverrides: state.annotationLayoutOverrides,
79
107
  }),
80
108
  },