@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
@@ -40,6 +40,12 @@ type FloorplanDraftPreviewState = {
40
40
  wallDraftStart: WallPlanPoint | null
41
41
  fenceDraftStart: WallPlanPoint | null
42
42
  roofDraftStart: WallPlanPoint | null
43
+ /** First corner of an open rectangle-wall draft; the live cursor is the
44
+ * opposite corner, so the rectangle tool publishes only per click. */
45
+ wallRectangleDraftStart: WallPlanPoint | null
46
+ /** Corners placed so far by an open Polygon room. Draft only — no wall is
47
+ * written until the polygon completes (`lib/wall-polygon-draft.ts`). */
48
+ wallPolygonDraftPoints: WallPlanPoint[]
43
49
  roofDraftQuarterTurn: boolean
44
50
  polygonDraftType: FloorplanPolygonDraftType | null
45
51
  polygonDraftPoints: WallPlanPoint[]
@@ -55,6 +61,8 @@ type FloorplanDraftPreviewState = {
55
61
  setWallDraftStart(point: WallPlanPoint | null): void
56
62
  setFenceDraftStart(point: WallPlanPoint | null): void
57
63
  setRoofDraftStart(point: WallPlanPoint | null): void
64
+ setWallRectangleDraftStart(point: WallPlanPoint | null): void
65
+ setWallPolygonDraftPoints(points: readonly WallPlanPoint[]): void
58
66
  setRoofDraftQuarterTurn(quarterTurn: boolean): void
59
67
  setPolygonDraft(type: FloorplanPolygonDraftType | null, points: readonly WallPlanPoint[]): void
60
68
  reset(): void
@@ -74,7 +82,8 @@ function setPlanPointField(
74
82
  | 'roofDraftEnd'
75
83
  | 'roofDraftStart'
76
84
  | 'wallDraftEnd'
77
- | 'wallDraftStart',
85
+ | 'wallDraftStart'
86
+ | 'wallRectangleDraftStart',
78
87
  point: WallPlanPoint | null,
79
88
  ) {
80
89
  return (
@@ -96,6 +105,8 @@ export const useFloorplanDraftPreview = create<FloorplanDraftPreviewState>((set)
96
105
  wallDraftStart: null,
97
106
  fenceDraftStart: null,
98
107
  roofDraftStart: null,
108
+ wallRectangleDraftStart: null,
109
+ wallPolygonDraftPoints: [],
99
110
  roofDraftQuarterTurn: false,
100
111
  polygonDraftType: null,
101
112
  polygonDraftPoints: [],
@@ -119,6 +130,13 @@ export const useFloorplanDraftPreview = create<FloorplanDraftPreviewState>((set)
119
130
  setWallDraftStart: (point) => set(setPlanPointField('wallDraftStart', point)),
120
131
  setFenceDraftStart: (point) => set(setPlanPointField('fenceDraftStart', point)),
121
132
  setRoofDraftStart: (point) => set(setPlanPointField('roofDraftStart', point)),
133
+ setWallRectangleDraftStart: (point) => set(setPlanPointField('wallRectangleDraftStart', point)),
134
+ setWallPolygonDraftPoints: (points) =>
135
+ set((state) =>
136
+ planPointsEqual(state.wallPolygonDraftPoints, points)
137
+ ? state
138
+ : { wallPolygonDraftPoints: points.map(([x, z]) => [x, z]) },
139
+ ),
122
140
  setRoofDraftQuarterTurn: (quarterTurn) =>
123
141
  set((state) =>
124
142
  state.roofDraftQuarterTurn === quarterTurn ? state : { roofDraftQuarterTurn: quarterTurn },
@@ -139,6 +157,8 @@ export const useFloorplanDraftPreview = create<FloorplanDraftPreviewState>((set)
139
157
  state.wallDraftStart === null &&
140
158
  state.fenceDraftStart === null &&
141
159
  state.roofDraftStart === null &&
160
+ state.wallRectangleDraftStart === null &&
161
+ state.wallPolygonDraftPoints.length === 0 &&
142
162
  state.roofDraftQuarterTurn === false &&
143
163
  state.polygonDraftType === null &&
144
164
  state.polygonDraftPoints.length === 0
@@ -152,6 +172,8 @@ export const useFloorplanDraftPreview = create<FloorplanDraftPreviewState>((set)
152
172
  wallDraftStart: null,
153
173
  fenceDraftStart: null,
154
174
  roofDraftStart: null,
175
+ wallRectangleDraftStart: null,
176
+ wallPolygonDraftPoints: [],
155
177
  roofDraftQuarterTurn: false,
156
178
  polygonDraftType: null,
157
179
  polygonDraftPoints: [],
@@ -13,6 +13,7 @@
13
13
 
14
14
  import type { WallPlanPoint } from '@pascal-app/core'
15
15
  import { create } from 'zustand'
16
+ import useInteractionScope from './use-interaction-scope'
16
17
 
17
18
  export type FloorplanMarqueeDrag = {
18
19
  pointerId: number
@@ -34,7 +35,10 @@ type FloorplanMarqueeState = {
34
35
 
35
36
  export const useFloorplanMarquee = create<FloorplanMarqueeState>((set) => ({
36
37
  drag: null,
37
- begin: (drag) => set({ drag }),
38
+ begin: (drag) => {
39
+ useInteractionScope.getState().begin({ kind: 'box-select' })
40
+ set({ drag })
41
+ },
38
42
  setCurrent: (point) =>
39
43
  set((state) => {
40
44
  const prev = state.drag
@@ -44,7 +48,13 @@ export const useFloorplanMarquee = create<FloorplanMarqueeState>((set) => ({
44
48
  }
45
49
  return { drag: { ...prev, currentPlanPoint: point } }
46
50
  }),
47
- reset: () => set((state) => (state.drag === null ? state : { drag: null })),
51
+ reset: () => {
52
+ set((state) => {
53
+ if (!state.drag) return state
54
+ useInteractionScope.getState().endIf((scope) => scope.kind === 'box-select')
55
+ return { drag: null }
56
+ })
57
+ },
48
58
  }))
49
59
 
50
60
  export default useFloorplanMarquee
@@ -1,6 +1,13 @@
1
1
  'use client'
2
2
 
3
- import { type AnyNode, type AnyNodeId, useScene } from '@pascal-app/core'
3
+ import {
4
+ type AnyNode,
5
+ type AnyNodeId,
6
+ collectSubtree,
7
+ useLiveNodeOverrides,
8
+ useLiveTransforms,
9
+ useScene,
10
+ } from '@pascal-app/core'
4
11
  import {
5
12
  beginPerfAction,
6
13
  commitPerfAction,
@@ -10,6 +17,7 @@ import {
10
17
  import { useRef } from 'react'
11
18
  import { create } from 'zustand'
12
19
  import { useShallow } from 'zustand/react/shallow'
20
+ import { discardFreshPlacementSubtree } from '../lib/fresh-planar-placement'
13
21
  import {
14
22
  type ActiveInteractionScope,
15
23
  controlPointReshapeInfo,
@@ -25,6 +33,7 @@ import {
25
33
  reshapingNodeId,
26
34
  tangentReshapeInfo,
27
35
  } from '../lib/interaction/scope'
36
+ import usePlacementPreview from './use-placement-preview'
28
37
 
29
38
  // The authoritative interaction state machine. A single owner holds exactly one
30
39
  // scope at a time. `begin` enters an interaction (atomically replacing any prior
@@ -33,8 +42,21 @@ import {
33
42
  // the end of its interaction. There is no setter that can leave the store in an
34
43
  // illegal half-state: the only writable shape is `InteractionScope`.
35
44
 
45
+ type SubtreeCreation = {
46
+ rootId: AnyNodeId
47
+ node: AnyNode
48
+ hydrationId: object | null
49
+ }
50
+ type OwnedSubtree = { creation: SubtreeCreation; gesture: object }
51
+
36
52
  export type InteractionScopeState = {
37
53
  scope: InteractionScope
54
+ gesture: object | null
55
+ pendingSubtree: SubtreeCreation | null
56
+ ownedSubtree: OwnedSubtree | null
57
+ noteSubtreeCreation: (node: AnyNode) => void
58
+ adoptSubtree: (rootId: AnyNodeId) => boolean
59
+ finishSubtree: (rootId: AnyNodeId) => void
38
60
  // Enter an interaction. If one is already active it is ended first, so the
39
61
  // store is always single-owner.
40
62
  begin: (scope: ActiveInteractionScope) => void
@@ -101,9 +123,62 @@ function commitScopePerfAction(): void {
101
123
 
102
124
  const useInteractionScope = create<InteractionScopeState>((set, get) => ({
103
125
  scope: IDLE_SCOPE,
126
+ gesture: null,
127
+ pendingSubtree: null,
128
+ ownedSubtree: null,
129
+ noteSubtreeCreation: (node) =>
130
+ set({
131
+ pendingSubtree: { rootId: node.id, node, hydrationId: useScene.getState().hydrationId },
132
+ }),
133
+ adoptSubtree: (rootId) => {
134
+ const state = get()
135
+ if (state.ownedSubtree?.creation.rootId === rootId)
136
+ return (
137
+ state.ownedSubtree.gesture === state.gesture &&
138
+ isCurrentCreation(state.ownedSubtree.creation)
139
+ )
140
+ const creation = state.pendingSubtree
141
+ if (
142
+ !creation ||
143
+ creation.rootId !== rootId ||
144
+ !state.gesture ||
145
+ movingNodeOf(state.scope)?.id !== rootId ||
146
+ !isCurrentCreation(creation)
147
+ )
148
+ return false
149
+ set({ ownedSubtree: { creation, gesture: state.gesture }, pendingSubtree: null })
150
+ return true
151
+ },
152
+ finishSubtree: (rootId) =>
153
+ set((state) => {
154
+ if (state.ownedSubtree?.creation.rootId !== rootId && state.pendingSubtree?.rootId !== rootId)
155
+ return state
156
+ return {
157
+ ownedSubtree: state.ownedSubtree?.creation.rootId === rootId ? null : state.ownedSubtree,
158
+ pendingSubtree: state.pendingSubtree?.rootId === rootId ? null : state.pendingSubtree,
159
+ }
160
+ }),
104
161
  begin: (scope) => {
162
+ const state = get()
163
+ const nodeId = movingNodeOf(scope)?.id
164
+ const creation = state.ownedSubtree?.creation ?? state.pendingSubtree
165
+ if (
166
+ nodeId &&
167
+ nodeId === movingNodeOf(state.scope)?.id &&
168
+ creation?.rootId === nodeId &&
169
+ isCurrentCreation(creation)
170
+ ) {
171
+ set({ scope })
172
+ return
173
+ }
105
174
  beginScopePerfAction(scope)
106
- set({ scope })
175
+ set({
176
+ scope,
177
+ gesture: {},
178
+ ownedSubtree: null,
179
+ pendingSubtree:
180
+ get().pendingSubtree?.rootId === movingNodeOf(scope)?.id ? get().pendingSubtree : null,
181
+ })
107
182
  },
108
183
  update: (patch) =>
109
184
  set((state) => {
@@ -114,18 +189,92 @@ const useInteractionScope = create<InteractionScopeState>((set, get) => ({
114
189
  end: () => {
115
190
  if (get().scope.kind === 'idle') return
116
191
  commitScopePerfAction()
117
- set({ scope: IDLE_SCOPE })
192
+ set({ scope: IDLE_SCOPE, gesture: null, ownedSubtree: null, pendingSubtree: null })
118
193
  },
119
194
  endIf: (match) => {
120
195
  const scope = get().scope
121
196
  if (scope.kind === 'idle') return
122
197
  if (match(scope)) {
123
198
  commitScopePerfAction()
124
- set({ scope: IDLE_SCOPE })
199
+ set({ scope: IDLE_SCOPE, gesture: null, ownedSubtree: null, pendingSubtree: null })
125
200
  }
126
201
  },
127
202
  }))
128
203
 
204
+ function isCurrentCreation(creation: SubtreeCreation): boolean {
205
+ const scene = useScene.getState()
206
+ return (
207
+ scene.hydrationId === creation.hydrationId && scene.nodes[creation.rootId] === creation.node
208
+ )
209
+ }
210
+
211
+ export function isInteractionSubtreeDraft(
212
+ rootId = movingNodeOf(useInteractionScope.getState().scope)?.id,
213
+ ): boolean {
214
+ const state = useInteractionScope.getState()
215
+ const creation = state.ownedSubtree?.creation ?? state.pendingSubtree
216
+ return Boolean(
217
+ creation &&
218
+ state.gesture &&
219
+ rootId === movingNodeOf(state.scope)?.id &&
220
+ creation.rootId === rootId &&
221
+ (!state.ownedSubtree || state.ownedSubtree.gesture === state.gesture) &&
222
+ isCurrentCreation(creation),
223
+ )
224
+ }
225
+
226
+ // Track the identity of the factory-created node through scene patches, never through metadata.
227
+ // Hydration or deletion invalidates the creation even if a later scene reuses the same id.
228
+ useScene.subscribe((scene, previous) => {
229
+ const state = useInteractionScope.getState()
230
+ for (const creation of [state.pendingSubtree, state.ownedSubtree?.creation]) {
231
+ if (!creation) continue
232
+ if (
233
+ scene.hydrationId !== creation.hydrationId ||
234
+ previous.nodes[creation.rootId] !== creation.node ||
235
+ !scene.nodes[creation.rootId]
236
+ ) {
237
+ state.finishSubtree(creation.rootId)
238
+ } else creation.node = scene.nodes[creation.rootId]!
239
+ }
240
+ })
241
+
242
+ // Factory drafts belong to their gesture even before a mover mounts to adopt them.
243
+ useInteractionScope.subscribe((state, previous) => {
244
+ if (
245
+ state.gesture === previous.gesture &&
246
+ movingNodeOf(state.scope)?.id === movingNodeOf(previous.scope)?.id
247
+ )
248
+ return
249
+ const owned =
250
+ previous.ownedSubtree ??
251
+ (previous.pendingSubtree && previous.gesture
252
+ ? { creation: previous.pendingSubtree, gesture: previous.gesture }
253
+ : null)
254
+ if (
255
+ !owned ||
256
+ owned.gesture !== previous.gesture ||
257
+ movingNodeOf(previous.scope)?.id !== owned.creation.rootId ||
258
+ !isCurrentCreation(owned.creation)
259
+ )
260
+ return
261
+ const node = owned.creation.node
262
+ const subtree = collectSubtree(useScene.getState().nodes, node.id)
263
+ const temporal = useScene.temporal.getState()
264
+ const wasTracking = temporal.isTracking
265
+ temporal.pause()
266
+ try {
267
+ discardFreshPlacementSubtree(node.id)
268
+ for (const entry of subtree ? [subtree.root, ...subtree.descendants] : []) {
269
+ useLiveTransforms.getState().clear(entry.id)
270
+ useLiveNodeOverrides.getState().clear(entry.id)
271
+ }
272
+ if (usePlacementPreview.getState().node?.id === node.id) usePlacementPreview.getState().clear()
273
+ } finally {
274
+ if (wasTracking) temporal.resume()
275
+ }
276
+ })
277
+
129
278
  // Derived, reference-stable views of the active scope, replacing the legacy
130
279
  // `useEditor.activeHandleDrag` / `useEditor.editingHole` flags. `useShallow`
131
280
  // keeps the result reference-stable across unrelated scope changes, so hot-path
@@ -190,8 +339,8 @@ export const useReshapingNode = (): AnyNode | null => {
190
339
  // The node currently being placed or moved. Replaces the legacy
191
340
  // `useEditor.movingNode` flag. Unlike `useReshapingNode`, no `useRef` snapshot is
192
341
  // needed: the node is carried inline in the scope and set once at `begin`, so it
193
- // is already a stable reference for the whole gesture (nothing calls `begin` mid
194
- // drag). Returns null whenever no placing/moving interaction is active.
342
+ // remains stable until a caller explicitly re-arms the same interaction.
343
+ // Returns null whenever no placing/moving interaction is active.
195
344
  export const useMovingNode = (): AnyNode | null => useInteractionScope((s) => movingNodeOf(s.scope))
196
345
 
197
346
  // Imperative (non-React) read for event handlers / effects.
@@ -1,6 +1,6 @@
1
1
  import type { QuickMeasurementReport } from '@pascal-app/core'
2
2
  import { create } from 'zustand'
3
- import type { ViewMode } from './use-editor'
3
+ import type { VisibleScene } from './view-layout'
4
4
 
5
5
  export type QuickMeasurementHudSource = '2d' | '3d'
6
6
 
@@ -55,9 +55,9 @@ export const useQuickMeasurementHud = create<QuickMeasurementHudState>((set) =>
55
55
 
56
56
  export function selectQuickMeasurementHudEntry(
57
57
  state: QuickMeasurementHudState,
58
- viewMode: ViewMode,
58
+ scene: VisibleScene | null,
59
59
  ): QuickMeasurementHudEntry | null {
60
- const source = viewMode === 'split' ? state.activeSource : viewMode
60
+ const source = scene === 'split' ? state.activeSource : scene
61
61
  return source ? state.sources[source] : null
62
62
  }
63
63
 
@@ -17,11 +17,12 @@ type StairBuildPreviewState = {
17
17
  point: StairPreviewPoint | null
18
18
  /** Yaw (radians), cycled by R / T. */
19
19
  rotation: number
20
+ rise: number | null
20
21
  /** Set the snapped point. No-ops (skips the store update, so subscribers
21
22
  * don't re-render) when the point is unchanged — `grid:move` fires far more
22
23
  * often than the snapped cell actually changes. */
23
24
  setPoint(point: StairPreviewPoint | null): void
24
- setPreview(point: StairPreviewPoint | null, rotation: number): void
25
+ setPreview(point: StairPreviewPoint | null, rotation: number, rise?: number | null): void
25
26
  rotateBy(deltaRadians: number): void
26
27
  reset(): void
27
28
  }
@@ -29,6 +30,7 @@ type StairBuildPreviewState = {
29
30
  export const useStairBuildPreview = create<StairBuildPreviewState>((set) => ({
30
31
  point: null,
31
32
  rotation: 0,
33
+ rise: null,
32
34
  setPoint: (point) =>
33
35
  set((state) => {
34
36
  const prev = state.point
@@ -36,18 +38,24 @@ export const useStairBuildPreview = create<StairBuildPreviewState>((set) => ({
36
38
  if (point && prev && prev[0] === point[0] && prev[1] === point[1]) return state
37
39
  return { point: point ? [point[0], point[1]] : null }
38
40
  }),
39
- setPreview: (point, rotation) =>
41
+ setPreview: (point, rotation, rise) =>
40
42
  set((state) => {
41
43
  const samePoint =
42
44
  (!point && !state.point) ||
43
45
  Boolean(point && state.point && state.point[0] === point[0] && state.point[1] === point[1])
44
- return samePoint && state.rotation === rotation
46
+ return samePoint && state.rotation === rotation && (rise === undefined || rise === state.rise)
45
47
  ? state
46
- : { point: point ? [point[0], point[1]] : null, rotation }
48
+ : {
49
+ point: point ? [point[0], point[1]] : null,
50
+ rotation,
51
+ rise: rise === undefined ? state.rise : rise,
52
+ }
47
53
  }),
48
54
  rotateBy: (deltaRadians) => set((state) => ({ rotation: state.rotation + deltaRadians })),
49
55
  reset: () =>
50
56
  set((state) =>
51
- state.point === null && state.rotation === 0 ? state : { point: null, rotation: 0 },
57
+ state.point === null && state.rotation === 0 && state.rise === null
58
+ ? state
59
+ : { point: null, rotation: 0, rise: null },
52
60
  ),
53
61
  }))
@@ -10,8 +10,10 @@ import { create } from 'zustand'
10
10
  * as translucent ghost meshes mid-drag; the 2D `wallFloorplanMoveTarget`
11
11
  * writes here so the floor-plan SVG layer can render the same hint.
12
12
  *
13
- * Writer: `packages/nodes/src/wall/floorplan-move.ts` (on each `apply`,
14
- * cleared on `commit` and by the move overlay's cleanup).
13
+ * Writers: `packages/nodes/src/wall/floorplan-move.ts` (on each `apply`,
14
+ * cleared on `commit` and by the move overlay's cleanup), and the wall push
15
+ * preview in `lib/room-handle-drag.ts` (the pieces a push would create,
16
+ * cleared when the push previews again, commits or cancels).
15
17
  * Reader: `packages/editor/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx`.
16
18
  */
17
19
  export type WallMoveGhostBridge = {
@@ -0,0 +1,222 @@
1
+ import { afterAll, beforeEach, describe, expect, test } from 'bun:test'
2
+ import { type AnyNodeId, useScene, WallNode } from '@pascal-app/core'
3
+ import { useViewer } from '@pascal-app/viewer'
4
+ import useInteractionScope from './use-interaction-scope'
5
+ import {
6
+ handleWallRegionDeleteKey,
7
+ installWallRegionSceneSync,
8
+ resolveActiveWallRegion,
9
+ syncWallRegionSelection,
10
+ useWallRegionSelection,
11
+ } from './use-wall-region-handles-selection'
12
+
13
+ type RafFn = (callback: (time: number) => void) => number
14
+ ;(globalThis as { requestAnimationFrame?: RafFn }).requestAnimationFrame ??= (callback) => {
15
+ callback(0)
16
+ return 0
17
+ }
18
+ ;(globalThis as { cancelAnimationFrame?: (id: number) => void }).cancelAnimationFrame ??= () => {}
19
+
20
+ const WALL_ID = 'wall_region-selection' as AnyNodeId
21
+ const OTHER_WALL_ID = 'wall_region-selection-other' as AnyNodeId
22
+
23
+ function keyEvent(key: string, target: unknown = null) {
24
+ const calls = { prevented: 0, stopped: 0 }
25
+ return {
26
+ calls,
27
+ event: {
28
+ key,
29
+ target: target as EventTarget | null,
30
+ preventDefault: () => {
31
+ calls.prevented += 1
32
+ },
33
+ stopPropagation: () => {
34
+ calls.stopped += 1
35
+ },
36
+ },
37
+ }
38
+ }
39
+
40
+ // The bun env has no DOM: just enough of `document` and elements for the focus check.
41
+ const originalDocument = (globalThis as { document?: unknown }).document
42
+ const body = { tagName: 'BODY', closest: () => null }
43
+ ;(globalThis as { document?: unknown }).document = { body, documentElement: { tagName: 'HTML' } }
44
+
45
+ function element(tagName: string, ancestors: string[] = []) {
46
+ return {
47
+ tagName,
48
+ closest: (selector: string) => (ancestors.includes(selector) ? {} : null),
49
+ }
50
+ }
51
+
52
+ const uninstallSceneSync = installWallRegionSceneSync()
53
+
54
+ const wall = () => useScene.getState().nodes[WALL_ID] as WallNode | undefined
55
+
56
+ describe('active wall region', () => {
57
+ beforeEach(() => {
58
+ useScene.setState({ nodes: {}, rootNodeIds: [], dirtyNodes: new Set() } as never)
59
+ for (const id of [WALL_ID, OTHER_WALL_ID]) {
60
+ useScene.getState().createNode(
61
+ WallNode.parse({
62
+ id,
63
+ start: [0, 0],
64
+ end: [4, 0],
65
+ faceRegions: [
66
+ { id: 'r1', face: 'a', v1: 0.9, finish: 'library:paint-white' },
67
+ { id: 'r2', face: 'b', u0: 1, finish: 'library:paint-white' },
68
+ ],
69
+ }),
70
+ )
71
+ }
72
+ useScene.temporal.getState().clear()
73
+ useScene.temporal.getState().resume()
74
+ useViewer.getState().setSelection({ selectedIds: [WALL_ID] })
75
+ useWallRegionSelection.setState({ active: null, stale: null })
76
+ useInteractionScope.getState().end()
77
+ })
78
+
79
+ afterAll(() => {
80
+ uninstallSceneSync()
81
+ useWallRegionSelection.setState({ active: null, stale: null })
82
+ useInteractionScope.getState().end()
83
+ useViewer.getState().setSelection({ selectedIds: [] })
84
+ ;(globalThis as { document?: unknown }).document = originalDocument
85
+ })
86
+
87
+ test('Delete removes only the active region, as one undo step', () => {
88
+ useWallRegionSelection.getState().setActive({ wallId: WALL_ID, regionId: 'r2' })
89
+ const { event, calls } = keyEvent('Delete')
90
+ expect(handleWallRegionDeleteKey(event)).toBe(true)
91
+ expect(calls).toEqual({ prevented: 1, stopped: 1 })
92
+ expect(wall()).toBeDefined()
93
+ expect(wall()!.faceRegions!.map((region) => region.id)).toEqual(['r1'])
94
+ expect(useWallRegionSelection.getState().active).toBeNull()
95
+ expect(useScene.temporal.getState().pastStates).toHaveLength(1)
96
+ useScene.temporal.getState().undo()
97
+ expect(wall()!.faceRegions!.map((region) => region.id)).toEqual(['r1', 'r2'])
98
+ })
99
+
100
+ test('Backspace works too; the last region clears the field', () => {
101
+ useWallRegionSelection.getState().setActive({ wallId: WALL_ID, regionId: 'r1' })
102
+ expect(handleWallRegionDeleteKey(keyEvent('Backspace').event)).toBe(true)
103
+ useWallRegionSelection.getState().setActive({ wallId: WALL_ID, regionId: 'r2' })
104
+ expect(handleWallRegionDeleteKey(keyEvent('Backspace').event)).toBe(true)
105
+ expect(wall()!.faceRegions).toBeUndefined()
106
+ })
107
+
108
+ test('without an active region the key passes through to the wall delete', () => {
109
+ const { event, calls } = keyEvent('Delete')
110
+ expect(handleWallRegionDeleteKey(event)).toBe(false)
111
+ expect(calls).toEqual({ prevented: 0, stopped: 0 })
112
+ expect(wall()!.faceRegions).toHaveLength(2)
113
+ })
114
+
115
+ test('typing in a field and other keys are left alone', () => {
116
+ useWallRegionSelection.getState().setActive({ wallId: WALL_ID, regionId: 'r1' })
117
+ const input = { closest: (selector: string) => (selector.includes('input') ? {} : null) }
118
+ expect(handleWallRegionDeleteKey(keyEvent('Backspace', input).event)).toBe(false)
119
+ expect(handleWallRegionDeleteKey(keyEvent('Escape').event)).toBe(false)
120
+ expect(wall()!.faceRegions).toHaveLength(2)
121
+ })
122
+
123
+ test('an active region on a wall that is no longer selected is ignored and cleared', () => {
124
+ useWallRegionSelection.getState().setActive({ wallId: WALL_ID, regionId: 'r1' })
125
+ useViewer.getState().setSelection({ selectedIds: [OTHER_WALL_ID] })
126
+ expect(resolveActiveWallRegion()).toBeNull()
127
+ expect(handleWallRegionDeleteKey(keyEvent('Delete').event)).toBe(false)
128
+ syncWallRegionSelection([OTHER_WALL_ID])
129
+ expect(useWallRegionSelection.getState().active).toBeNull()
130
+ const other = useScene.getState().nodes[OTHER_WALL_ID] as WallNode
131
+ expect(other.faceRegions).toHaveLength(2)
132
+ })
133
+
134
+ test('a selection that still holds just the wall keeps the active region', () => {
135
+ useWallRegionSelection.getState().setActive({ wallId: WALL_ID, regionId: 'r1' })
136
+ syncWallRegionSelection([WALL_ID])
137
+ expect(useWallRegionSelection.getState().active).toEqual({ wallId: WALL_ID, regionId: 'r1' })
138
+ syncWallRegionSelection([WALL_ID, OTHER_WALL_ID])
139
+ expect(useWallRegionSelection.getState().active).toBeNull()
140
+ })
141
+
142
+ test('focus on the body or the canvas acts on the region', () => {
143
+ useWallRegionSelection.getState().setActive({ wallId: WALL_ID, regionId: 'r1' })
144
+ expect(handleWallRegionDeleteKey(keyEvent('Delete', body).event)).toBe(true)
145
+ useWallRegionSelection.getState().setActive({ wallId: WALL_ID, regionId: 'r2' })
146
+ expect(handleWallRegionDeleteKey(keyEvent('Delete', element('CANVAS')).event)).toBe(true)
147
+ expect(wall()!.faceRegions).toBeUndefined()
148
+ })
149
+
150
+ test('focus on a toolbar button leaves the key alone', () => {
151
+ useWallRegionSelection.getState().setActive({ wallId: WALL_ID, regionId: 'r1' })
152
+ const { event, calls } = keyEvent('Delete', element('BUTTON'))
153
+ expect(handleWallRegionDeleteKey(event)).toBe(false)
154
+ expect(calls).toEqual({ prevented: 0, stopped: 0 })
155
+ expect(wall()!.faceRegions).toHaveLength(2)
156
+ })
157
+
158
+ test('focus inside the wall region list acts on the region', () => {
159
+ useWallRegionSelection.getState().setActive({ wallId: WALL_ID, regionId: 'r1' })
160
+ const row = element('BUTTON', ['[data-wall-region-list]'])
161
+ expect(handleWallRegionDeleteKey(keyEvent('Delete', row).event)).toBe(true)
162
+ expect(wall()!.faceRegions!.map((region) => region.id)).toEqual(['r2'])
163
+ })
164
+
165
+ test('mid-gesture the key is swallowed: nothing deleted, the wall stays', () => {
166
+ useWallRegionSelection.getState().setActive({ wallId: WALL_ID, regionId: 'r1' })
167
+ useInteractionScope
168
+ .getState()
169
+ .begin({ kind: 'handle-drag', nodeId: WALL_ID, handle: 'paint-region-bound' })
170
+ const { event, calls } = keyEvent('Delete')
171
+ expect(handleWallRegionDeleteKey(event)).toBe(true)
172
+ expect(calls).toEqual({ prevented: 1, stopped: 1 })
173
+ expect(wall()!.faceRegions).toHaveLength(2)
174
+ expect(useWallRegionSelection.getState().active).toEqual({ wallId: WALL_ID, regionId: 'r1' })
175
+ })
176
+
177
+ test('mid-gesture without an active region the key passes through', () => {
178
+ useInteractionScope
179
+ .getState()
180
+ .begin({ kind: 'handle-drag', nodeId: WALL_ID, handle: 'paint-region-bound' })
181
+ expect(handleWallRegionDeleteKey(keyEvent('Delete').event)).toBe(false)
182
+ })
183
+
184
+ test('a region that vanished under the user swallows the next Delete once', () => {
185
+ useWallRegionSelection.getState().setActive({ wallId: WALL_ID, regionId: 'r1' })
186
+ const regions = wall()!.faceRegions!.filter((region) => region.id !== 'r1')
187
+ useScene.getState().updateNode(WALL_ID, { faceRegions: regions })
188
+ expect(useWallRegionSelection.getState().active).toBeNull()
189
+ expect(useWallRegionSelection.getState().stale).toEqual({ wallId: WALL_ID, regionId: 'r1' })
190
+
191
+ const { event, calls } = keyEvent('Delete')
192
+ expect(handleWallRegionDeleteKey(event)).toBe(true)
193
+ expect(calls).toEqual({ prevented: 1, stopped: 1 })
194
+ expect(wall()).toBeDefined()
195
+ expect(wall()!.faceRegions!.map((region) => region.id)).toEqual(['r2'])
196
+ expect(useWallRegionSelection.getState().stale).toBeNull()
197
+ expect(handleWallRegionDeleteKey(keyEvent('Delete').event)).toBe(false)
198
+ })
199
+
200
+ test('removing the active region with Delete does not leave it stale', () => {
201
+ useWallRegionSelection.getState().setActive({ wallId: WALL_ID, regionId: 'r1' })
202
+ expect(handleWallRegionDeleteKey(keyEvent('Delete').event)).toBe(true)
203
+ expect(useWallRegionSelection.getState().stale).toBeNull()
204
+ expect(handleWallRegionDeleteKey(keyEvent('Delete').event)).toBe(false)
205
+ })
206
+
207
+ test('a selection change clears the stale region, so Delete falls through', () => {
208
+ useWallRegionSelection.getState().setActive({ wallId: WALL_ID, regionId: 'r1' })
209
+ const regions = wall()!.faceRegions!.filter((region) => region.id !== 'r1')
210
+ useScene.getState().updateNode(WALL_ID, { faceRegions: regions })
211
+ expect(useWallRegionSelection.getState().stale).not.toBeNull()
212
+
213
+ useViewer.getState().setSelection({ selectedIds: [OTHER_WALL_ID] })
214
+ syncWallRegionSelection([OTHER_WALL_ID])
215
+ useViewer.getState().setSelection({ selectedIds: [WALL_ID] })
216
+ syncWallRegionSelection([WALL_ID])
217
+ expect(useWallRegionSelection.getState().stale).toBeNull()
218
+ const { event, calls } = keyEvent('Delete')
219
+ expect(handleWallRegionDeleteKey(event)).toBe(false)
220
+ expect(calls).toEqual({ prevented: 0, stopped: 0 })
221
+ })
222
+ })