@pascal-app/editor 1.0.1 → 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 (498) hide show
  1. package/package.json +16 -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 +221 -128
  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 +37 -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 +536 -487
  27. package/src/components/editor/footprint-height-handle.tsx +204 -0
  28. package/src/components/editor/grid.tsx +31 -7
  29. package/src/components/editor/group-actions.ts +59 -11
  30. package/src/components/editor/group-floating-action-menu.tsx +19 -9
  31. package/src/components/editor/group-move-3d.ts +4 -6
  32. package/src/components/editor/group-rotate-handle.tsx +81 -35
  33. package/src/components/editor/group-selection-box-3d.tsx +25 -17
  34. package/src/components/editor/group-transform-shared.test.ts +126 -1
  35. package/src/components/editor/group-transform-shared.ts +79 -73
  36. package/src/components/editor/handles/handle-arrow.tsx +72 -2
  37. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  38. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  39. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  40. package/src/components/editor/handles/use-handle-drag.ts +272 -131
  41. package/src/components/editor/index.tsx +316 -176
  42. package/src/components/editor/join-walls-pill.tsx +44 -0
  43. package/src/components/editor/node-action-menu.tsx +70 -70
  44. package/src/components/editor/node-arrow-handles.tsx +167 -14
  45. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  46. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  47. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  48. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  49. package/src/components/editor/registry-action-contributions.tsx +132 -0
  50. package/src/components/editor/room-controls.tsx +847 -0
  51. package/src/components/editor/room-floor-highlight.tsx +128 -0
  52. package/src/components/editor/room-handles-2d.tsx +354 -0
  53. package/src/components/editor/room-handles.tsx +675 -0
  54. package/src/components/editor/room-highlight.tsx +225 -0
  55. package/src/components/editor/room-labels.test.ts +357 -0
  56. package/src/components/editor/room-labels.tsx +990 -0
  57. package/src/components/editor/room-transform-ghost.tsx +256 -0
  58. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  59. package/src/components/editor/selection-manager.tsx +451 -317
  60. package/src/components/editor/slab-hole-highlights.tsx +3 -0
  61. package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
  62. package/src/components/editor/snapshot-capture.test.ts +52 -4
  63. package/src/components/editor/snapshot-capture.ts +30 -5
  64. package/src/components/editor/thumbnail-generator.test.ts +308 -0
  65. package/src/components/editor/thumbnail-generator.tsx +1035 -229
  66. package/src/components/editor/use-floorplan-background-placement.ts +31 -11
  67. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  68. package/src/components/editor/views/host-view.tsx +61 -0
  69. package/src/components/editor/views/plugins-view.tsx +670 -0
  70. package/src/components/editor/views/scene-region.tsx +59 -0
  71. package/src/components/editor/views/use-editor-views.ts +57 -0
  72. package/src/components/editor/views/view-bar.tsx +220 -0
  73. package/src/components/editor/wall-justification.test.ts +140 -0
  74. package/src/components/editor/wall-move-side-handles.tsx +114 -163
  75. package/src/components/editor/wall-opening-highlights.tsx +2 -3
  76. package/src/components/editor/wall-push-arrow.tsx +92 -0
  77. package/src/components/editor/wall-region-handles.tsx +509 -0
  78. package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
  79. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  80. package/src/components/editor-2d/floorplan-group-move.tsx +17 -24
  81. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  82. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  83. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +33 -12
  84. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  85. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +242 -126
  86. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  87. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  88. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  89. package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +166 -0
  90. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  91. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +87 -8
  92. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +355 -245
  93. package/src/components/editor-2d/svg-paths.ts +12 -15
  94. package/src/components/procedural-items/inspector.tsx +40 -15
  95. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  96. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  97. package/src/components/systems/zone/zone-system.test.ts +47 -0
  98. package/src/components/systems/zone/zone-system.tsx +31 -66
  99. package/src/components/tools/__fixtures__/hosting.ts +10 -0
  100. package/src/components/tools/item/item-surface-placement.test.ts +3 -9
  101. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +3 -9
  102. package/src/components/tools/item/placement-history.test.tsx +474 -0
  103. package/src/components/tools/item/placement-strategies.ts +63 -47
  104. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +3 -3
  105. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  106. package/src/components/tools/item/use-draft-node.ts +207 -97
  107. package/src/components/tools/item/use-placement-coordinator.tsx +251 -111
  108. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  109. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  110. package/src/components/tools/registry/item-surface-move.test.ts +10 -2
  111. package/src/components/tools/registry/item-surface-move.ts +46 -24
  112. package/src/components/tools/registry/item-surface-resolver.test.ts +2 -2
  113. package/src/components/tools/registry/move-registry-node-tool.tsx +26 -10
  114. package/src/components/tools/select/box-select-tool.tsx +2 -12
  115. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  116. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  117. package/src/components/tools/select/select-candidates.test.ts +37 -4
  118. package/src/components/tools/select/select-candidates.ts +15 -11
  119. package/src/components/tools/shared/affordance-dispatch.test.ts +24 -0
  120. package/src/components/tools/shared/affordance-dispatch.ts +13 -0
  121. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  122. package/src/components/tools/shared/draft-measurement-label.tsx +38 -0
  123. package/src/components/tools/shared/pointer-support-cap.test.ts +27 -0
  124. package/src/components/tools/shared/pointer-support-cap.ts +17 -5
  125. package/src/components/tools/shared/surface-rejection.test.tsx +5 -4
  126. package/src/components/tools/shared/surface-rejection.tsx +14 -0
  127. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  128. package/src/components/tools/stair/stair-defaults.ts +0 -3
  129. package/src/components/tools/stair/stair-tool.tsx +90 -42
  130. package/src/components/tools/tool-manager.tsx +82 -23
  131. package/src/components/tools/wall/wall-drafting.test.ts +19 -4
  132. package/src/components/tools/zone/zone-tool.tsx +4 -12
  133. package/src/components/ui/action-menu/action-button.tsx +3 -0
  134. package/src/components/ui/action-menu/camera-actions.tsx +2 -1
  135. package/src/components/ui/action-menu/control-modes.tsx +2 -35
  136. package/src/components/ui/action-menu/measurement-control.tsx +3 -10
  137. package/src/components/ui/build-panel/build-panel.tsx +235 -0
  138. package/src/components/ui/command-palette/editor-commands.tsx +4 -30
  139. package/src/components/ui/command-palette/index.tsx +5 -24
  140. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  141. package/src/components/ui/controls/material-paint-panel.tsx +101 -91
  142. package/src/components/ui/controls/material-picker.tsx +8 -67
  143. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  144. package/src/components/ui/controls/select-control.tsx +65 -0
  145. package/src/components/ui/controls/slider-control.tsx +76 -31
  146. package/src/components/ui/floating-level-selector.tsx +9 -14
  147. package/src/components/ui/helpers/building-helper.tsx +4 -1
  148. package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
  149. package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
  150. package/src/components/ui/helpers/helper-manager.tsx +348 -19
  151. package/src/components/ui/helpers/item-helper.tsx +8 -0
  152. package/src/components/ui/helpers/registered-tool-helper.tsx +10 -33
  153. package/src/components/ui/icon-ref.tsx +45 -1
  154. package/src/components/ui/item-catalog/item-catalog.tsx +5 -30
  155. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  156. package/src/components/ui/node-icon-image.tsx +16 -0
  157. package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
  158. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  159. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  160. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  161. package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
  162. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  163. package/src/components/ui/panels/multi-field-value.ts +6 -3
  164. package/src/components/ui/panels/multi-height-mode.tsx +27 -21
  165. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  166. package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
  167. package/src/components/ui/panels/node-display.ts +8 -3
  168. package/src/components/ui/panels/panel-manager.tsx +103 -68
  169. package/src/components/ui/panels/panel-wrapper.tsx +78 -36
  170. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  171. package/src/components/ui/panels/parametric-inspector.tsx +7 -19
  172. package/src/components/ui/panels/reference-panel.tsx +2 -3
  173. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  174. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  175. package/src/components/ui/panels/room-name-options.ts +47 -0
  176. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  177. package/src/components/ui/panels/room-panel.tsx +725 -0
  178. package/src/components/ui/panels/shape-choice.tsx +58 -0
  179. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  180. package/src/components/ui/primitives/color-dot.tsx +36 -24
  181. package/src/components/ui/primitives/shortcut-token.tsx +4 -0
  182. package/src/components/ui/primitives/sidebar.tsx +2 -0
  183. package/src/components/ui/right-stack.test.ts +29 -0
  184. package/src/components/ui/right-stack.tsx +145 -0
  185. package/src/components/ui/sidebar/icon-rail.tsx +5 -0
  186. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
  187. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +615 -372
  188. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +7 -7
  189. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
  190. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  191. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  192. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  193. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  194. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  195. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  196. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  197. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  198. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  199. package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
  200. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  201. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  202. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  203. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  204. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  205. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  206. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  207. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  208. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  209. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  210. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
  211. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  212. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  213. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  214. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  215. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
  216. package/src/components/ui/sidebar/tab-bar.tsx +10 -0
  217. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  218. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  219. package/src/components/viewer/floorplan-preview-visibility.test.ts +35 -0
  220. package/src/components/viewer/floorplan-preview-visibility.ts +19 -0
  221. package/src/components/viewer/floorplan-preview.tsx +6 -18
  222. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  223. package/src/components/viewer/viewer-rooms.tsx +175 -0
  224. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  225. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  226. package/src/components/viewer/viewer-stage.tsx +2 -2
  227. package/src/components/viewer-zone-system.tsx +9 -9
  228. package/src/hooks/use-auto-save.integration.test.tsx +311 -0
  229. package/src/hooks/use-auto-save.ts +55 -14
  230. package/src/hooks/use-ceiling-events.test.ts +1 -0
  231. package/src/hooks/use-ceiling-events.ts +2 -0
  232. package/src/hooks/use-grid-events.test.ts +2 -0
  233. package/src/hooks/use-grid-events.ts +16 -11
  234. package/src/hooks/use-keyboard.test.ts +182 -0
  235. package/src/hooks/use-keyboard.ts +122 -91
  236. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  237. package/src/hooks/use-open-wall-ends.ts +238 -0
  238. package/src/hooks/use-placement-notice.test.ts +48 -0
  239. package/src/hooks/use-placement-notice.ts +42 -0
  240. package/src/hooks/use-selected-room.ts +143 -0
  241. package/src/index.tsx +306 -39
  242. package/src/lib/catalog-panel-model.ts +39 -0
  243. package/src/lib/catalog-selection.test.ts +27 -0
  244. package/src/lib/catalog-selection.ts +8 -0
  245. package/src/lib/ceiling-edit-session.test.ts +321 -0
  246. package/src/lib/ceiling-edit-session.ts +206 -0
  247. package/src/lib/ceiling-region-session.test.ts +179 -0
  248. package/src/lib/contextual-help.test.ts +27 -1
  249. package/src/lib/contextual-help.ts +16 -9
  250. package/src/lib/continuation.test.ts +19 -1
  251. package/src/lib/continuation.ts +25 -4
  252. package/src/lib/door-interaction.ts +14 -0
  253. package/src/lib/editor-level-display.test.ts +96 -0
  254. package/src/lib/editor-level-display.ts +48 -0
  255. package/src/lib/editor-views.ts +144 -0
  256. package/src/lib/export-geometry-wait.test.ts +90 -0
  257. package/src/lib/floor-edit-session.ts +153 -0
  258. package/src/lib/floor-footprints.test.ts +563 -0
  259. package/src/lib/floor-footprints.ts +395 -0
  260. package/src/lib/floor-foundation-support.test.ts +283 -0
  261. package/src/lib/floor-opening-draft.ts +218 -0
  262. package/src/lib/floor-openings.test.ts +216 -0
  263. package/src/lib/floor-region-geometry.ts +191 -0
  264. package/src/lib/floor-region-session.test.ts +485 -0
  265. package/src/lib/floor-region-session.ts +589 -0
  266. package/src/lib/floor-region-snap.ts +101 -0
  267. package/src/lib/floor-step-hover.test.ts +32 -0
  268. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  269. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  270. package/src/lib/floorplan/dimension-drive.ts +479 -0
  271. package/src/lib/floorplan/floorplan-export.test.ts +15 -9
  272. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  273. package/src/lib/floorplan/floorplan-extension.ts +48 -0
  274. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  275. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  276. package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
  277. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
  278. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
  279. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  280. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  281. package/src/lib/floorplan/geometry.ts +13 -6
  282. package/src/lib/floorplan/index.ts +0 -9
  283. package/src/lib/floorplan/items.ts +5 -2
  284. package/src/lib/floorplan/marks.test.ts +213 -0
  285. package/src/lib/floorplan/marks.ts +238 -0
  286. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  287. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  288. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  289. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  290. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  291. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  292. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  293. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  294. package/src/lib/floorplan/schedules.test.ts +189 -0
  295. package/src/lib/floorplan/schedules.ts +339 -0
  296. package/src/lib/floorplan/sheet-export.test.ts +100 -0
  297. package/src/lib/floorplan/sheet-export.ts +318 -0
  298. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
  299. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  300. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  301. package/src/lib/floorplan/site-plan/coverage.ts +232 -0
  302. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  303. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  304. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  305. package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
  306. package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
  307. package/src/lib/floorplan/site-plan/geometry.ts +557 -0
  308. package/src/lib/floorplan/site-plan/index.ts +71 -0
  309. package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
  310. package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
  311. package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
  312. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  313. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  314. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  315. package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
  316. package/src/lib/floorplan/types.ts +1 -27
  317. package/src/lib/fresh-planar-placement.test.ts +36 -0
  318. package/src/lib/fresh-planar-placement.ts +191 -36
  319. package/src/lib/geometry-script/author.ts +152 -0
  320. package/src/lib/geometry-script/client.ts +152 -0
  321. package/src/lib/geometry-script/protocol.ts +12 -0
  322. package/src/lib/gesture-lifecycle.test.ts +595 -0
  323. package/src/lib/gesture-lifecycle.ts +290 -0
  324. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  325. package/src/lib/glb-export.test.ts +544 -16
  326. package/src/lib/glb-export.ts +256 -125
  327. package/src/lib/grid-frame.test.ts +19 -0
  328. package/src/lib/grid-frame.ts +18 -0
  329. package/src/lib/history.test.ts +6 -0
  330. package/src/lib/history.ts +7 -0
  331. package/src/lib/hud-title.ts +109 -0
  332. package/src/lib/hydration-gated-storage.ts +49 -0
  333. package/src/lib/ifc-export.test.ts +174 -0
  334. package/src/lib/ifc-export.ts +201 -0
  335. package/src/lib/ifc-parts.ts +15 -0
  336. package/src/lib/inspector-expanded.test.ts +44 -0
  337. package/src/lib/inspector-expanded.ts +36 -0
  338. package/src/lib/interaction/scope.ts +27 -1
  339. package/src/lib/keyboard-pan.test.ts +184 -0
  340. package/src/lib/keyboard-pan.ts +238 -0
  341. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  342. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  343. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  344. package/src/lib/lot/clean-ring.test.ts +186 -0
  345. package/src/lib/lot/clean-ring.ts +217 -0
  346. package/src/lib/lot/dossier.test.ts +202 -0
  347. package/src/lib/lot/dossier.ts +498 -0
  348. package/src/lib/lot/drop-in.test.ts +326 -0
  349. package/src/lib/lot/drop-in.ts +407 -0
  350. package/src/lib/lot/index.ts +43 -0
  351. package/src/lib/lot/lot-patch.test.ts +340 -0
  352. package/src/lib/lot/lot-patch.ts +394 -0
  353. package/src/lib/lot/parcel-provider.ts +43 -0
  354. package/src/lib/lot/terrain.test.ts +161 -0
  355. package/src/lib/lot/terrain.ts +266 -0
  356. package/src/lib/material-catalog-model.ts +74 -0
  357. package/src/lib/material-paint-panel-model.ts +80 -0
  358. package/src/lib/material-paint.ts +4 -0
  359. package/src/lib/mezzanine-actions.test.ts +342 -0
  360. package/src/lib/mezzanine-draft.test.ts +501 -0
  361. package/src/lib/mezzanine-draft.ts +278 -0
  362. package/src/lib/mezzanine-messages.ts +26 -0
  363. package/src/lib/mezzanine-selection.test.ts +207 -0
  364. package/src/lib/mezzanine-stairs.ts +46 -0
  365. package/src/lib/model-export.ts +3 -1
  366. package/src/lib/node-action-movement.ts +17 -6
  367. package/src/lib/node-display-label.ts +37 -0
  368. package/src/lib/orbit-floor-clearance.test.ts +165 -0
  369. package/src/lib/orbit-floor-clearance.ts +106 -0
  370. package/src/lib/overlay-transform.test.ts +28 -0
  371. package/src/lib/overlay-transform.ts +12 -0
  372. package/src/lib/paint-commit-route.test.ts +37 -0
  373. package/src/lib/paint-commit-route.ts +17 -0
  374. package/src/lib/paint-eyedropper.test.tsx +165 -0
  375. package/src/lib/paint-eyedropper.ts +99 -0
  376. package/src/lib/paint-pass-through.test.ts +111 -0
  377. package/src/lib/paint-pass-through.ts +76 -0
  378. package/src/lib/paint-region-hover.ts +25 -0
  379. package/src/lib/paint-region-mode.ts +154 -0
  380. package/src/lib/paint-regions.test.ts +166 -0
  381. package/src/lib/paint-regions.ts +192 -0
  382. package/src/lib/paint-scope.test.ts +317 -35
  383. package/src/lib/paint-scope.ts +266 -25
  384. package/src/lib/panel-rows.ts +96 -0
  385. package/src/lib/panel-tool-options.test.ts +34 -0
  386. package/src/lib/panel-tool-options.ts +46 -0
  387. package/src/lib/parametric-node-update.ts +37 -0
  388. package/src/lib/plate-paint-affected.test.ts +300 -0
  389. package/src/lib/plate-paint-affected.ts +177 -0
  390. package/src/lib/plugin-chat.ts +50 -0
  391. package/src/lib/plugin-panels.test.ts +25 -0
  392. package/src/lib/plugin-panels.ts +94 -0
  393. package/src/lib/print-shell-compiler.ts +1 -0
  394. package/src/lib/registered-draft-snap.ts +12 -0
  395. package/src/lib/roof-duplication.ts +10 -2
  396. package/src/lib/room-assembly-overlay.test.ts +275 -0
  397. package/src/lib/room-assembly-overlay.ts +266 -0
  398. package/src/lib/room-built-on.test.ts +379 -0
  399. package/src/lib/room-built-on.ts +138 -0
  400. package/src/lib/room-construction-commands.ts +712 -0
  401. package/src/lib/room-construction.test.ts +746 -0
  402. package/src/lib/room-construction.ts +385 -0
  403. package/src/lib/room-controls-phase6.test.ts +630 -0
  404. package/src/lib/room-divide-session.ts +523 -0
  405. package/src/lib/room-handle-drag.ts +1068 -0
  406. package/src/lib/room-name-catalog.test.ts +90 -0
  407. package/src/lib/room-name-catalog.ts +168 -0
  408. package/src/lib/room-push-dimensions.test.ts +311 -0
  409. package/src/lib/room-push-dimensions.ts +251 -0
  410. package/src/lib/room-selection-commands.ts +62 -0
  411. package/src/lib/room-selection.test.ts +973 -0
  412. package/src/lib/room-selection.ts +417 -0
  413. package/src/lib/room-structure-commands.ts +94 -0
  414. package/src/lib/room-structure.test.ts +921 -0
  415. package/src/lib/room-transform-session.ts +570 -0
  416. package/src/lib/room-zone-routing.test.ts +294 -0
  417. package/src/lib/room-zone-routing.ts +197 -0
  418. package/src/lib/scene-clipboard.test.ts +364 -0
  419. package/src/lib/scene-clipboard.ts +229 -48
  420. package/src/lib/scene-material-refs.test.ts +79 -0
  421. package/src/lib/scene-material-refs.ts +79 -0
  422. package/src/lib/scene.test.ts +87 -0
  423. package/src/lib/scene.ts +39 -45
  424. package/src/lib/selection-routing.test.ts +129 -45
  425. package/src/lib/selection-routing.ts +53 -74
  426. package/src/lib/session-writes.test.ts +120 -0
  427. package/src/lib/session-writes.ts +205 -0
  428. package/src/lib/sidebar-panel.ts +62 -0
  429. package/src/lib/site-boundary.test.ts +4 -6
  430. package/src/lib/site-boundary.ts +1 -1
  431. package/src/lib/snapping-mode.test.ts +17 -2
  432. package/src/lib/snapping-mode.ts +19 -7
  433. package/src/lib/spatial-drag-plane.test.ts +53 -0
  434. package/src/lib/spatial-drag-plane.ts +19 -0
  435. package/src/lib/spatial-pointer-input.test.ts +72 -0
  436. package/src/lib/spatial-pointer-input.ts +62 -0
  437. package/src/lib/spatial-snap.test.ts +47 -0
  438. package/src/lib/terrace-draft.test.ts +145 -0
  439. package/src/lib/terrace-draft.ts +213 -0
  440. package/src/lib/terrain-panel-model.ts +117 -0
  441. package/src/lib/tool-cursor-icon.test.ts +16 -0
  442. package/src/lib/tool-cursor-icon.ts +11 -0
  443. package/src/lib/typing-target.ts +9 -0
  444. package/src/lib/units.ts +77 -39
  445. package/src/lib/vector-edges.test.ts +58 -0
  446. package/src/lib/vector-edges.ts +400 -0
  447. package/src/lib/viewer-selection.test.ts +577 -0
  448. package/src/lib/viewer-selection.ts +569 -0
  449. package/src/lib/wall-draw-variant.test.ts +71 -0
  450. package/src/lib/wall-draw-variant.ts +91 -0
  451. package/src/lib/wall-paint-region-session.ts +246 -0
  452. package/src/lib/wall-paint-region.test.ts +373 -0
  453. package/src/lib/wall-polygon-draft.test.ts +262 -0
  454. package/src/lib/wall-polygon-draft.ts +286 -0
  455. package/src/lib/wall-push.test.ts +453 -0
  456. package/src/lib/wall-region-face.ts +352 -0
  457. package/src/lib/wall-region-handles.test.ts +304 -0
  458. package/src/lib/wall-region-handles.ts +368 -0
  459. package/src/lib/wall-region-list.test.ts +119 -0
  460. package/src/lib/wall-region-list.ts +136 -0
  461. package/src/lib/wall-region-snap.ts +110 -0
  462. package/src/lib/window-interaction.ts +14 -0
  463. package/src/lib/world-grid-snap.test.ts +48 -0
  464. package/src/lib/world-grid-snap.ts +9 -5
  465. package/src/lib/zone-content.ts +4 -1
  466. package/src/lib/zone-name.test.ts +17 -0
  467. package/src/lib/zone-name.ts +17 -0
  468. package/src/store/opens-in-select-mode.test.ts +223 -0
  469. package/src/store/paint-mode.test.ts +188 -0
  470. package/src/store/rotation-snapping.test.ts +53 -0
  471. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  472. package/src/store/tool-mode.test.ts +137 -6
  473. package/src/store/use-delete-confirmation.ts +28 -1
  474. package/src/store/use-drawing-view.test.ts +24 -3
  475. package/src/store/use-drawing-view.ts +29 -1
  476. package/src/store/use-editor.tsx +442 -323
  477. package/src/store/use-floorplan-draft-preview.ts +23 -1
  478. package/src/store/use-floorplan-marquee.ts +12 -2
  479. package/src/store/use-interaction-scope.ts +155 -6
  480. package/src/store/use-quick-measurement-hud.ts +3 -3
  481. package/src/store/use-stair-build-preview.ts +13 -5
  482. package/src/store/use-wall-move-ghosts.ts +4 -2
  483. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  484. package/src/store/use-wall-region-handles-selection.ts +139 -0
  485. package/src/store/view-layout.test.ts +175 -0
  486. package/src/store/view-layout.ts +196 -0
  487. package/src/test-utils/immediate-animation-frames.ts +16 -0
  488. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  489. package/src/test-utils/selection-harness.ts +82 -0
  490. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  491. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  492. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  493. package/src/components/tools/fence/fence-drafting.ts +0 -266
  494. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  495. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  496. package/src/lib/floorplan/stairs.ts +0 -489
  497. package/src/lib/level-duplication.test.ts +0 -144
  498. package/src/lib/level-duplication.ts +0 -195
@@ -0,0 +1,53 @@
1
+ import { afterEach, expect, test } from 'bun:test'
2
+ import { GROUP_ROTATE_DRAG_LABEL, ROTATE_HANDLE_DRAG_LABEL } from '../lib/contextual-help'
3
+ import useEditor, {
4
+ getActiveSnapContext,
5
+ isAngleSnapActive,
6
+ normalizePersistedEditorLayoutState,
7
+ } from './use-editor'
8
+ import useInteractionScope from './use-interaction-scope'
9
+
10
+ const initialEditor = useEditor.getState()
11
+ const initialScope = useInteractionScope.getState()
12
+ afterEach(() => {
13
+ useEditor.setState(initialEditor)
14
+ useInteractionScope.setState(initialScope)
15
+ })
16
+
17
+ for (const handle of [ROTATE_HANDLE_DRAG_LABEL, GROUP_ROTATE_DRAG_LABEL]) {
18
+ test(`${handle} reads and cycles the shared mode without changing placement modes`, () => {
19
+ useInteractionScope.getState().begin({ kind: 'handle-drag', nodeId: 'item_test', handle })
20
+ const placementModes = useEditor.getState().snappingModeByContext
21
+ expect(getActiveSnapContext()).toBe('rotation')
22
+ expect(isAngleSnapActive()).toBe(true)
23
+ expect(useEditor.getState().cycleSnappingMode()).toBe('off')
24
+ expect(isAngleSnapActive()).toBe(false)
25
+ expect(useEditor.getState().snappingModeByContext.wall).toBe(placementModes.wall)
26
+ expect(useEditor.getState().snappingModeByContext.item).toBe(placementModes.item)
27
+ expect(useEditor.getState().cycleSnappingMode()).toBe('angles')
28
+ expect(isAngleSnapActive()).toBe(true)
29
+ })
30
+ }
31
+
32
+ test('older persisted preferences receive the rotation default and retain other modes', () => {
33
+ const merged = normalizePersistedEditorLayoutState({
34
+ snappingModeByContext: { wall: 'off', item: 'lines', polygon: 'grid' },
35
+ })
36
+ expect(merged?.snappingModeByContext).toEqual({
37
+ wall: 'off',
38
+ item: 'lines',
39
+ polygon: 'grid',
40
+ rotation: 'angles',
41
+ })
42
+ })
43
+
44
+ test('persisted rotation mode is retained when valid and reset when invalid', () => {
45
+ expect(
46
+ normalizePersistedEditorLayoutState({ snappingModeByContext: { rotation: 'off' } })
47
+ .snappingModeByContext.rotation,
48
+ ).toBe('off')
49
+ expect(
50
+ normalizePersistedEditorLayoutState({ snappingModeByContext: { rotation: 'grid' } })
51
+ .snappingModeByContext.rotation,
52
+ ).toBe('angles')
53
+ })
@@ -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
  },