@pascal-app/editor 1.0.3 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (440) hide show
  1. package/package.json +11 -9
  2. package/src/components/editor/action-menu-button.tsx +83 -0
  3. package/src/components/editor/bake-thumbnail.tsx +315 -4
  4. package/src/components/editor/capture-level-picker.tsx +72 -0
  5. package/src/components/editor/custom-camera-controls.tsx +202 -57
  6. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  7. package/src/components/editor/duplicate-node.ts +244 -0
  8. package/src/components/editor/editor-layout-mobile.tsx +36 -25
  9. package/src/components/editor/editor-layout-v2.tsx +131 -58
  10. package/src/components/editor/export-manager.tsx +34 -8
  11. package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
  12. package/src/components/editor/first-person/build-collider-world.ts +6 -2
  13. package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
  14. package/src/components/editor/first-person/mechanism-targets.ts +36 -0
  15. package/src/components/editor/first-person-controls.tsx +205 -2
  16. package/src/components/editor/floating-action-menu.tsx +63 -207
  17. package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
  18. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  19. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  20. package/src/components/editor/floor-region-controls.tsx +846 -0
  21. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  22. package/src/components/editor/floor-surface.test.ts +165 -0
  23. package/src/components/editor/floor-surface.ts +182 -0
  24. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  25. package/src/components/editor/floorplan-background-selection.ts +1 -3
  26. package/src/components/editor/floorplan-panel.tsx +344 -342
  27. package/src/components/editor/footprint-height-handle.tsx +204 -0
  28. package/src/components/editor/group-actions.ts +54 -2
  29. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  30. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  31. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  32. package/src/components/editor/handles/use-handle-drag.ts +269 -197
  33. package/src/components/editor/index.tsx +294 -169
  34. package/src/components/editor/join-walls-pill.tsx +44 -0
  35. package/src/components/editor/node-action-menu.tsx +68 -70
  36. package/src/components/editor/node-arrow-handles.tsx +25 -6
  37. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  38. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  39. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  40. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  41. package/src/components/editor/registry-action-contributions.tsx +80 -1
  42. package/src/components/editor/room-controls.tsx +847 -0
  43. package/src/components/editor/room-floor-highlight.tsx +128 -0
  44. package/src/components/editor/room-handles-2d.tsx +354 -0
  45. package/src/components/editor/room-handles.tsx +675 -0
  46. package/src/components/editor/room-highlight.tsx +225 -0
  47. package/src/components/editor/room-labels.test.ts +357 -0
  48. package/src/components/editor/room-labels.tsx +990 -0
  49. package/src/components/editor/room-transform-ghost.tsx +256 -0
  50. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  51. package/src/components/editor/selection-manager.tsx +451 -317
  52. package/src/components/editor/slab-hole-highlights.tsx +3 -0
  53. package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
  54. package/src/components/editor/snapshot-capture.test.ts +52 -4
  55. package/src/components/editor/snapshot-capture.ts +30 -5
  56. package/src/components/editor/thumbnail-generator.test.ts +308 -0
  57. package/src/components/editor/thumbnail-generator.tsx +1035 -229
  58. package/src/components/editor/use-floorplan-background-placement.ts +31 -11
  59. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  60. package/src/components/editor/views/host-view.tsx +61 -0
  61. package/src/components/editor/views/plugins-view.tsx +670 -0
  62. package/src/components/editor/views/scene-region.tsx +59 -0
  63. package/src/components/editor/views/use-editor-views.ts +57 -0
  64. package/src/components/editor/views/view-bar.tsx +220 -0
  65. package/src/components/editor/wall-justification.test.ts +140 -0
  66. package/src/components/editor/wall-move-side-handles.tsx +43 -142
  67. package/src/components/editor/wall-push-arrow.tsx +92 -0
  68. package/src/components/editor/wall-region-handles.tsx +509 -0
  69. package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
  70. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  71. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  72. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  73. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  74. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +113 -102
  75. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  76. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  77. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  78. package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
  79. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  80. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
  81. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +291 -227
  82. package/src/components/editor-2d/svg-paths.ts +12 -15
  83. package/src/components/procedural-items/inspector.tsx +40 -15
  84. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  85. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  86. package/src/components/systems/zone/zone-system.test.ts +47 -0
  87. package/src/components/systems/zone/zone-system.tsx +29 -68
  88. package/src/components/tools/item/placement-history.test.tsx +474 -0
  89. package/src/components/tools/item/placement-strategies.ts +8 -4
  90. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  91. package/src/components/tools/item/use-draft-node.ts +166 -95
  92. package/src/components/tools/item/use-placement-coordinator.tsx +93 -46
  93. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  94. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  95. package/src/components/tools/registry/move-registry-node-tool.tsx +3 -1
  96. package/src/components/tools/select/box-select-tool.tsx +2 -12
  97. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  98. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  99. package/src/components/tools/select/select-candidates.test.ts +37 -4
  100. package/src/components/tools/select/select-candidates.ts +15 -11
  101. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  102. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  103. package/src/components/tools/stair/stair-defaults.ts +0 -3
  104. package/src/components/tools/stair/stair-tool.tsx +90 -42
  105. package/src/components/tools/tool-manager.tsx +46 -20
  106. package/src/components/tools/wall/wall-drafting.test.ts +19 -4
  107. package/src/components/tools/zone/zone-tool.tsx +4 -12
  108. package/src/components/ui/action-menu/action-button.tsx +3 -0
  109. package/src/components/ui/action-menu/camera-actions.tsx +2 -1
  110. package/src/components/ui/action-menu/control-modes.tsx +2 -35
  111. package/src/components/ui/action-menu/measurement-control.tsx +3 -10
  112. package/src/components/ui/build-panel/build-panel.tsx +235 -0
  113. package/src/components/ui/command-palette/editor-commands.tsx +4 -30
  114. package/src/components/ui/command-palette/index.tsx +5 -24
  115. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  116. package/src/components/ui/controls/material-paint-panel.tsx +97 -26
  117. package/src/components/ui/controls/material-picker.tsx +4 -1
  118. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  119. package/src/components/ui/controls/select-control.tsx +65 -0
  120. package/src/components/ui/floating-level-selector.tsx +9 -14
  121. package/src/components/ui/helpers/building-helper.tsx +4 -1
  122. package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
  123. package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
  124. package/src/components/ui/helpers/helper-manager.tsx +325 -14
  125. package/src/components/ui/helpers/item-helper.tsx +8 -0
  126. package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
  127. package/src/components/ui/icon-ref.tsx +45 -1
  128. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  129. package/src/components/ui/node-icon-image.tsx +16 -0
  130. package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
  131. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  132. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  133. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  134. package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
  135. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  136. package/src/components/ui/panels/multi-field-value.ts +6 -3
  137. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  138. package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
  139. package/src/components/ui/panels/node-display.ts +8 -3
  140. package/src/components/ui/panels/panel-manager.tsx +103 -68
  141. package/src/components/ui/panels/panel-wrapper.tsx +78 -36
  142. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  143. package/src/components/ui/panels/parametric-inspector.tsx +4 -4
  144. package/src/components/ui/panels/reference-panel.tsx +2 -3
  145. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  146. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  147. package/src/components/ui/panels/room-name-options.ts +47 -0
  148. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  149. package/src/components/ui/panels/room-panel.tsx +725 -0
  150. package/src/components/ui/panels/shape-choice.tsx +58 -0
  151. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  152. package/src/components/ui/primitives/color-dot.tsx +36 -24
  153. package/src/components/ui/primitives/sidebar.tsx +2 -0
  154. package/src/components/ui/right-stack.test.ts +29 -0
  155. package/src/components/ui/right-stack.tsx +145 -0
  156. package/src/components/ui/sidebar/icon-rail.tsx +5 -0
  157. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +31 -4
  158. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
  159. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  160. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  161. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  162. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  163. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  164. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  165. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  166. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  167. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  168. package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
  169. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  170. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  171. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  172. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  173. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  174. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  175. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  176. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  177. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  178. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  179. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
  180. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  181. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  182. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  183. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  184. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
  185. package/src/components/ui/sidebar/tab-bar.tsx +10 -0
  186. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  187. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  188. package/src/components/viewer/floorplan-preview.tsx +5 -6
  189. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  190. package/src/components/viewer/viewer-rooms.tsx +175 -0
  191. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  192. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  193. package/src/components/viewer/viewer-stage.tsx +2 -2
  194. package/src/components/viewer-zone-system.tsx +3 -8
  195. package/src/hooks/use-auto-save.integration.test.tsx +123 -9
  196. package/src/hooks/use-auto-save.ts +34 -8
  197. package/src/hooks/use-keyboard.test.ts +178 -3
  198. package/src/hooks/use-keyboard.ts +89 -71
  199. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  200. package/src/hooks/use-open-wall-ends.ts +238 -0
  201. package/src/hooks/use-placement-notice.test.ts +48 -0
  202. package/src/hooks/use-placement-notice.ts +42 -0
  203. package/src/hooks/use-selected-room.ts +143 -0
  204. package/src/index.tsx +246 -39
  205. package/src/lib/ceiling-edit-session.test.ts +321 -0
  206. package/src/lib/ceiling-edit-session.ts +206 -0
  207. package/src/lib/ceiling-region-session.test.ts +179 -0
  208. package/src/lib/continuation.test.ts +19 -1
  209. package/src/lib/continuation.ts +25 -4
  210. package/src/lib/door-interaction.ts +14 -0
  211. package/src/lib/editor-level-display.test.ts +96 -0
  212. package/src/lib/editor-level-display.ts +48 -0
  213. package/src/lib/editor-views.ts +144 -0
  214. package/src/lib/export-geometry-wait.test.ts +90 -0
  215. package/src/lib/floor-edit-session.ts +153 -0
  216. package/src/lib/floor-footprints.test.ts +563 -0
  217. package/src/lib/floor-footprints.ts +395 -0
  218. package/src/lib/floor-foundation-support.test.ts +283 -0
  219. package/src/lib/floor-opening-draft.ts +218 -0
  220. package/src/lib/floor-openings.test.ts +216 -0
  221. package/src/lib/floor-region-geometry.ts +191 -0
  222. package/src/lib/floor-region-session.test.ts +485 -0
  223. package/src/lib/floor-region-session.ts +589 -0
  224. package/src/lib/floor-region-snap.ts +101 -0
  225. package/src/lib/floor-step-hover.test.ts +32 -0
  226. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  227. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  228. package/src/lib/floorplan/dimension-drive.ts +479 -0
  229. package/src/lib/floorplan/floorplan-export.test.ts +12 -8
  230. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  231. package/src/lib/floorplan/floorplan-extension.ts +32 -0
  232. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  233. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  234. package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
  235. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
  236. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
  237. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  238. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  239. package/src/lib/floorplan/geometry.ts +13 -6
  240. package/src/lib/floorplan/index.ts +0 -9
  241. package/src/lib/floorplan/items.ts +5 -2
  242. package/src/lib/floorplan/marks.test.ts +213 -0
  243. package/src/lib/floorplan/marks.ts +238 -0
  244. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  245. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  246. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  247. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  248. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  249. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  250. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  251. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  252. package/src/lib/floorplan/schedules.test.ts +189 -0
  253. package/src/lib/floorplan/schedules.ts +339 -0
  254. package/src/lib/floorplan/sheet-export.test.ts +100 -0
  255. package/src/lib/floorplan/sheet-export.ts +318 -0
  256. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
  257. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  258. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  259. package/src/lib/floorplan/site-plan/coverage.ts +232 -0
  260. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  261. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  262. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  263. package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
  264. package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
  265. package/src/lib/floorplan/site-plan/geometry.ts +557 -0
  266. package/src/lib/floorplan/site-plan/index.ts +71 -0
  267. package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
  268. package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
  269. package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
  270. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  271. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  272. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  273. package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
  274. package/src/lib/floorplan/types.ts +1 -27
  275. package/src/lib/fresh-planar-placement.test.ts +36 -0
  276. package/src/lib/fresh-planar-placement.ts +59 -36
  277. package/src/lib/geometry-script/author.ts +152 -0
  278. package/src/lib/geometry-script/client.ts +152 -0
  279. package/src/lib/geometry-script/protocol.ts +12 -0
  280. package/src/lib/gesture-lifecycle.test.ts +595 -0
  281. package/src/lib/gesture-lifecycle.ts +290 -0
  282. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  283. package/src/lib/glb-export.test.ts +485 -0
  284. package/src/lib/glb-export.ts +237 -119
  285. package/src/lib/history.test.ts +6 -0
  286. package/src/lib/history.ts +7 -0
  287. package/src/lib/hud-title.ts +109 -0
  288. package/src/lib/hydration-gated-storage.ts +49 -0
  289. package/src/lib/ifc-export.test.ts +174 -0
  290. package/src/lib/ifc-export.ts +201 -0
  291. package/src/lib/ifc-parts.ts +15 -0
  292. package/src/lib/inspector-expanded.test.ts +44 -0
  293. package/src/lib/inspector-expanded.ts +36 -0
  294. package/src/lib/interaction/scope.ts +18 -0
  295. package/src/lib/keyboard-pan.test.ts +129 -7
  296. package/src/lib/keyboard-pan.ts +161 -12
  297. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  298. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  299. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  300. package/src/lib/lot/clean-ring.test.ts +186 -0
  301. package/src/lib/lot/clean-ring.ts +217 -0
  302. package/src/lib/lot/dossier.test.ts +202 -0
  303. package/src/lib/lot/dossier.ts +498 -0
  304. package/src/lib/lot/drop-in.test.ts +326 -0
  305. package/src/lib/lot/drop-in.ts +407 -0
  306. package/src/lib/lot/index.ts +43 -0
  307. package/src/lib/lot/lot-patch.test.ts +340 -0
  308. package/src/lib/lot/lot-patch.ts +394 -0
  309. package/src/lib/lot/parcel-provider.ts +43 -0
  310. package/src/lib/lot/terrain.test.ts +161 -0
  311. package/src/lib/lot/terrain.ts +266 -0
  312. package/src/lib/material-catalog-model.ts +3 -1
  313. package/src/lib/material-paint-panel-model.ts +9 -5
  314. package/src/lib/material-paint.ts +4 -0
  315. package/src/lib/mezzanine-actions.test.ts +342 -0
  316. package/src/lib/mezzanine-draft.test.ts +501 -0
  317. package/src/lib/mezzanine-draft.ts +278 -0
  318. package/src/lib/mezzanine-messages.ts +26 -0
  319. package/src/lib/mezzanine-selection.test.ts +207 -0
  320. package/src/lib/mezzanine-stairs.ts +46 -0
  321. package/src/lib/model-export.ts +3 -1
  322. package/src/lib/node-action-movement.ts +17 -6
  323. package/src/lib/node-display-label.ts +37 -0
  324. package/src/lib/orbit-floor-clearance.test.ts +165 -0
  325. package/src/lib/orbit-floor-clearance.ts +106 -0
  326. package/src/lib/paint-commit-route.test.ts +37 -0
  327. package/src/lib/paint-commit-route.ts +17 -0
  328. package/src/lib/paint-eyedropper.test.tsx +165 -0
  329. package/src/lib/paint-eyedropper.ts +99 -0
  330. package/src/lib/paint-pass-through.test.ts +111 -0
  331. package/src/lib/paint-pass-through.ts +76 -0
  332. package/src/lib/paint-region-hover.ts +25 -0
  333. package/src/lib/paint-region-mode.ts +154 -0
  334. package/src/lib/paint-regions.test.ts +166 -0
  335. package/src/lib/paint-regions.ts +192 -0
  336. package/src/lib/paint-scope.test.ts +317 -35
  337. package/src/lib/paint-scope.ts +266 -25
  338. package/src/lib/panel-rows.ts +21 -0
  339. package/src/lib/parametric-node-update.ts +1 -1
  340. package/src/lib/plate-paint-affected.test.ts +300 -0
  341. package/src/lib/plate-paint-affected.ts +177 -0
  342. package/src/lib/plugin-chat.ts +50 -0
  343. package/src/lib/plugin-panels.test.ts +25 -0
  344. package/src/lib/plugin-panels.ts +94 -0
  345. package/src/lib/print-shell-compiler.ts +1 -0
  346. package/src/lib/registered-draft-snap.ts +12 -0
  347. package/src/lib/roof-duplication.ts +10 -2
  348. package/src/lib/room-assembly-overlay.test.ts +275 -0
  349. package/src/lib/room-assembly-overlay.ts +266 -0
  350. package/src/lib/room-built-on.test.ts +379 -0
  351. package/src/lib/room-built-on.ts +138 -0
  352. package/src/lib/room-construction-commands.ts +712 -0
  353. package/src/lib/room-construction.test.ts +746 -0
  354. package/src/lib/room-construction.ts +385 -0
  355. package/src/lib/room-controls-phase6.test.ts +630 -0
  356. package/src/lib/room-divide-session.ts +523 -0
  357. package/src/lib/room-handle-drag.ts +1068 -0
  358. package/src/lib/room-name-catalog.test.ts +90 -0
  359. package/src/lib/room-name-catalog.ts +168 -0
  360. package/src/lib/room-push-dimensions.test.ts +311 -0
  361. package/src/lib/room-push-dimensions.ts +251 -0
  362. package/src/lib/room-selection-commands.ts +62 -0
  363. package/src/lib/room-selection.test.ts +973 -0
  364. package/src/lib/room-selection.ts +417 -0
  365. package/src/lib/room-structure-commands.ts +94 -0
  366. package/src/lib/room-structure.test.ts +921 -0
  367. package/src/lib/room-transform-session.ts +570 -0
  368. package/src/lib/room-zone-routing.test.ts +294 -0
  369. package/src/lib/room-zone-routing.ts +197 -0
  370. package/src/lib/scene-clipboard.test.ts +364 -0
  371. package/src/lib/scene-clipboard.ts +229 -48
  372. package/src/lib/scene-material-refs.test.ts +79 -0
  373. package/src/lib/scene-material-refs.ts +79 -0
  374. package/src/lib/scene.test.ts +24 -36
  375. package/src/lib/scene.ts +33 -57
  376. package/src/lib/selection-routing.test.ts +129 -45
  377. package/src/lib/selection-routing.ts +53 -74
  378. package/src/lib/session-writes.test.ts +120 -0
  379. package/src/lib/session-writes.ts +205 -0
  380. package/src/lib/sidebar-panel.ts +62 -0
  381. package/src/lib/site-boundary.test.ts +4 -6
  382. package/src/lib/site-boundary.ts +1 -1
  383. package/src/lib/snapping-mode.test.ts +4 -0
  384. package/src/lib/snapping-mode.ts +8 -1
  385. package/src/lib/terrace-draft.test.ts +145 -0
  386. package/src/lib/terrace-draft.ts +213 -0
  387. package/src/lib/tool-cursor-icon.test.ts +16 -0
  388. package/src/lib/tool-cursor-icon.ts +11 -0
  389. package/src/lib/typing-target.ts +9 -0
  390. package/src/lib/units.ts +77 -39
  391. package/src/lib/vector-edges.test.ts +58 -0
  392. package/src/lib/vector-edges.ts +400 -0
  393. package/src/lib/viewer-selection.test.ts +577 -0
  394. package/src/lib/viewer-selection.ts +569 -0
  395. package/src/lib/wall-draw-variant.test.ts +71 -0
  396. package/src/lib/wall-draw-variant.ts +91 -0
  397. package/src/lib/wall-paint-region-session.ts +246 -0
  398. package/src/lib/wall-paint-region.test.ts +373 -0
  399. package/src/lib/wall-polygon-draft.test.ts +262 -0
  400. package/src/lib/wall-polygon-draft.ts +286 -0
  401. package/src/lib/wall-push.test.ts +453 -0
  402. package/src/lib/wall-region-face.ts +352 -0
  403. package/src/lib/wall-region-handles.test.ts +304 -0
  404. package/src/lib/wall-region-handles.ts +368 -0
  405. package/src/lib/wall-region-list.test.ts +119 -0
  406. package/src/lib/wall-region-list.ts +136 -0
  407. package/src/lib/wall-region-snap.ts +110 -0
  408. package/src/lib/window-interaction.ts +14 -0
  409. package/src/lib/zone-content.ts +4 -1
  410. package/src/lib/zone-name.test.ts +17 -0
  411. package/src/lib/zone-name.ts +17 -0
  412. package/src/store/opens-in-select-mode.test.ts +223 -0
  413. package/src/store/paint-mode.test.ts +188 -0
  414. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  415. package/src/store/tool-mode.test.ts +137 -6
  416. package/src/store/use-delete-confirmation.ts +28 -1
  417. package/src/store/use-drawing-view.test.ts +24 -3
  418. package/src/store/use-drawing-view.ts +29 -1
  419. package/src/store/use-editor.tsx +432 -321
  420. package/src/store/use-floorplan-draft-preview.ts +13 -0
  421. package/src/store/use-floorplan-marquee.ts +12 -2
  422. package/src/store/use-quick-measurement-hud.ts +3 -3
  423. package/src/store/use-stair-build-preview.ts +13 -5
  424. package/src/store/use-wall-move-ghosts.ts +4 -2
  425. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  426. package/src/store/use-wall-region-handles-selection.ts +139 -0
  427. package/src/store/view-layout.test.ts +175 -0
  428. package/src/store/view-layout.ts +196 -0
  429. package/src/test-utils/immediate-animation-frames.ts +16 -0
  430. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  431. package/src/test-utils/selection-harness.ts +82 -0
  432. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  433. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  434. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  435. package/src/components/tools/fence/fence-drafting.ts +0 -266
  436. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  437. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  438. package/src/lib/floorplan/stairs.ts +0 -489
  439. package/src/lib/level-duplication.test.ts +0 -144
  440. package/src/lib/level-duplication.ts +0 -195
@@ -0,0 +1,302 @@
1
+ /**
2
+ * Street names compared the way the Postal Service reads an address (USPS
3
+ * Publication 28): a name is its directional, its core and its street type,
4
+ * each spelling brought to the standard abbreviation. "4121 NW 34th St" and
5
+ * OSM's "Northwest 34th Street" are one street; "Northwest 34th Terrace" is
6
+ * another — the type is part of the name, and so is the quadrant.
7
+ */
8
+
9
+ /**
10
+ * USPS Publication 28, Appendix C1 — street suffixes: the standard
11
+ * abbreviation, then every spelling the Postal Service lists for it (lower
12
+ * case, periods dropped).
13
+ */
14
+ const PUB28_SUFFIXES: Record<string, readonly string[]> = {
15
+ aly: ['allee', 'alley', 'ally', 'aly'],
16
+ anx: ['anex', 'annex', 'annx', 'anx'],
17
+ arc: ['arc', 'arcade'],
18
+ ave: ['av', 'ave', 'aven', 'avenu', 'avenue', 'avn', 'avnue'],
19
+ byu: ['bayoo', 'bayou'],
20
+ bch: ['bch', 'beach'],
21
+ bnd: ['bend', 'bnd'],
22
+ blf: ['blf', 'bluf', 'bluff'],
23
+ blfs: ['bluffs'],
24
+ btm: ['bot', 'btm', 'bottm', 'bottom'],
25
+ blvd: ['blvd', 'boul', 'boulevard', 'boulv'],
26
+ br: ['br', 'brnch', 'branch'],
27
+ brg: ['brdge', 'brg', 'bridge'],
28
+ brk: ['brk', 'brook'],
29
+ brks: ['brooks'],
30
+ bg: ['burg'],
31
+ bgs: ['burgs'],
32
+ byp: ['byp', 'bypa', 'bypas', 'bypass', 'byps'],
33
+ cp: ['camp', 'cp', 'cmp'],
34
+ cyn: ['canyn', 'canyon', 'cnyn'],
35
+ cpe: ['cape', 'cpe'],
36
+ cswy: ['causeway', 'causwa', 'cswy'],
37
+ ctr: ['cen', 'cent', 'center', 'centr', 'centre', 'cnter', 'cntr', 'ctr'],
38
+ ctrs: ['centers'],
39
+ cir: ['cir', 'circ', 'circl', 'circle', 'crcl', 'crcle'],
40
+ cirs: ['circles'],
41
+ clf: ['clf', 'cliff'],
42
+ clfs: ['clfs', 'cliffs'],
43
+ clb: ['clb', 'club'],
44
+ cmn: ['common'],
45
+ cmns: ['commons'],
46
+ cor: ['cor', 'corner'],
47
+ cors: ['corners', 'cors'],
48
+ crse: ['course', 'crse'],
49
+ ct: ['court', 'ct'],
50
+ cts: ['courts', 'cts'],
51
+ cv: ['cove', 'cv'],
52
+ cvs: ['coves'],
53
+ crk: ['creek', 'crk'],
54
+ cres: ['crescent', 'cres', 'crsent', 'crsnt'],
55
+ crst: ['crest'],
56
+ xing: ['crossing', 'crssng', 'xing'],
57
+ xrd: ['crossroad'],
58
+ xrds: ['crossroads'],
59
+ curv: ['curve'],
60
+ dl: ['dale', 'dl'],
61
+ dm: ['dam', 'dm'],
62
+ dv: ['div', 'divide', 'dv', 'dvd'],
63
+ dr: ['dr', 'driv', 'drive', 'drv'],
64
+ drs: ['drives'],
65
+ est: ['est', 'estate'],
66
+ ests: ['estates', 'ests'],
67
+ expy: ['exp', 'expr', 'express', 'expressway', 'expw', 'expy'],
68
+ ext: ['ext', 'extension', 'extn', 'extnsn'],
69
+ exts: ['exts'],
70
+ fall: ['fall'],
71
+ fls: ['falls', 'fls'],
72
+ fry: ['ferry', 'frry', 'fry'],
73
+ fld: ['field', 'fld'],
74
+ flds: ['fields', 'flds'],
75
+ flt: ['flat', 'flt'],
76
+ flts: ['flats', 'flts'],
77
+ frd: ['ford', 'frd'],
78
+ frds: ['fords'],
79
+ frst: ['forest', 'forests', 'frst'],
80
+ frg: ['forg', 'forge', 'frg'],
81
+ frgs: ['forges'],
82
+ frk: ['fork', 'frk'],
83
+ frks: ['forks', 'frks'],
84
+ ft: ['fort', 'frt', 'ft'],
85
+ fwy: ['freeway', 'freewy', 'frway', 'frwy', 'fwy'],
86
+ gdn: ['garden', 'gardn', 'grden', 'grdn'],
87
+ gdns: ['gardens', 'gdns', 'grdns'],
88
+ gtwy: ['gateway', 'gatewy', 'gatway', 'gtway', 'gtwy'],
89
+ gln: ['glen', 'gln'],
90
+ glns: ['glens'],
91
+ grn: ['green', 'grn'],
92
+ grns: ['greens'],
93
+ grv: ['grov', 'grove', 'grv'],
94
+ grvs: ['groves'],
95
+ hbr: ['harb', 'harbor', 'harbr', 'hbr', 'hrbor'],
96
+ hbrs: ['harbors'],
97
+ hvn: ['haven', 'hvn'],
98
+ hts: ['ht', 'hts'],
99
+ hwy: ['highway', 'highwy', 'hiway', 'hiwy', 'hway', 'hwy'],
100
+ hl: ['hill', 'hl'],
101
+ hls: ['hills', 'hls'],
102
+ holw: ['hllw', 'hollow', 'hollows', 'holw', 'holws'],
103
+ inlt: ['inlt'],
104
+ is: ['is', 'island', 'islnd'],
105
+ iss: ['islands', 'islnds', 'iss'],
106
+ isle: ['isle', 'isles'],
107
+ jct: ['jct', 'jction', 'jctn', 'junction', 'junctn', 'juncton'],
108
+ jcts: ['jctns', 'jcts', 'junctions'],
109
+ ky: ['key', 'ky'],
110
+ kys: ['keys', 'kys'],
111
+ knl: ['knl', 'knol', 'knoll'],
112
+ knls: ['knls', 'knolls'],
113
+ lk: ['lk', 'lake'],
114
+ lks: ['lks', 'lakes'],
115
+ land: ['land'],
116
+ lndg: ['landing', 'lndg', 'lndng'],
117
+ ln: ['lane', 'ln'],
118
+ lgt: ['lgt', 'light'],
119
+ lgts: ['lights'],
120
+ lf: ['lf', 'loaf'],
121
+ lck: ['lck', 'lock'],
122
+ lcks: ['lcks', 'locks'],
123
+ ldg: ['ldg', 'ldge', 'lodg', 'lodge'],
124
+ loop: ['loop', 'loops'],
125
+ mall: ['mall'],
126
+ mnr: ['mnr', 'manor'],
127
+ mnrs: ['manors', 'mnrs'],
128
+ mdw: ['meadow'],
129
+ mdws: ['mdw', 'mdws', 'meadows', 'medows'],
130
+ mews: ['mews'],
131
+ ml: ['mill'],
132
+ mls: ['mills'],
133
+ msn: ['missn', 'mssn'],
134
+ mtwy: ['motorway'],
135
+ mt: ['mnt', 'mt', 'mount'],
136
+ mtn: ['mntain', 'mntn', 'mountain', 'mountin', 'mtin', 'mtn'],
137
+ mtns: ['mntns', 'mountains'],
138
+ nck: ['nck', 'neck'],
139
+ orch: ['orch', 'orchard', 'orchrd'],
140
+ oval: ['oval', 'ovl'],
141
+ opas: ['overpass'],
142
+ park: ['park', 'prk', 'parks'],
143
+ pkwy: ['parkway', 'parkwy', 'pkway', 'pkwy', 'pky', 'parkways', 'pkwys'],
144
+ pass: ['pass'],
145
+ psge: ['passage'],
146
+ path: ['path', 'paths'],
147
+ pike: ['pike', 'pikes'],
148
+ pne: ['pine'],
149
+ pnes: ['pines', 'pnes'],
150
+ pl: ['pl', 'place'],
151
+ pln: ['plain', 'pln'],
152
+ plns: ['plains', 'plns'],
153
+ plz: ['plaza', 'plz', 'plza'],
154
+ pt: ['point', 'pt'],
155
+ pts: ['points', 'pts'],
156
+ prt: ['port', 'prt'],
157
+ prts: ['ports', 'prts'],
158
+ pr: ['pr', 'prairie', 'prr'],
159
+ radl: ['rad', 'radial', 'radiel', 'radl'],
160
+ ramp: ['ramp'],
161
+ rnch: ['ranch', 'ranches', 'rnch', 'rnchs'],
162
+ rpd: ['rapid', 'rpd'],
163
+ rpds: ['rapids', 'rpds'],
164
+ rst: ['rest', 'rst'],
165
+ rdg: ['rdg', 'rdge', 'ridge'],
166
+ rdgs: ['rdgs', 'ridges'],
167
+ riv: ['riv', 'river', 'rvr', 'rivr'],
168
+ rd: ['rd', 'road'],
169
+ rds: ['roads', 'rds'],
170
+ rte: ['route'],
171
+ row: ['row'],
172
+ rue: ['rue'],
173
+ run: ['run'],
174
+ shl: ['shl', 'shoal'],
175
+ shls: ['shls', 'shoals'],
176
+ shr: ['shoar', 'shore', 'shr'],
177
+ shrs: ['shoars', 'shores', 'shrs'],
178
+ skwy: ['skyway'],
179
+ spg: ['spg', 'spng', 'spring', 'sprng'],
180
+ spgs: ['spgs', 'spngs', 'springs', 'sprngs'],
181
+ spur: ['spur', 'spurs'],
182
+ sq: ['sq', 'sqr', 'sqre', 'squ', 'square'],
183
+ sqs: ['sqrs', 'squares'],
184
+ sta: ['sta', 'station', 'statn', 'stn'],
185
+ stra: ['stra', 'strav', 'straven', 'stravenue', 'stravn', 'strvn', 'strvnue'],
186
+ strm: ['stream', 'streme', 'strm'],
187
+ st: ['street', 'strt', 'st', 'str'],
188
+ sts: ['streets'],
189
+ smt: ['smt', 'sumit', 'sumitt', 'summit'],
190
+ ter: ['ter', 'terr', 'terrace'],
191
+ trwy: ['throughway'],
192
+ trce: ['trace', 'traces', 'trce'],
193
+ trak: ['track', 'tracks', 'trak', 'trk', 'trks'],
194
+ trfy: ['trafficway'],
195
+ trl: ['trail', 'trails', 'trl', 'trls'],
196
+ trlr: ['trailer', 'trlr', 'trlrs'],
197
+ tunl: ['tunel', 'tunl', 'tunls', 'tunnel', 'tunnels', 'tunnl'],
198
+ tpke: ['trnpk', 'turnpike', 'turnpk'],
199
+ upas: ['underpass'],
200
+ un: ['un', 'union'],
201
+ uns: ['unions'],
202
+ vly: ['valley', 'vally', 'vlly', 'vly'],
203
+ vlys: ['valleys', 'vlys'],
204
+ via: ['vdct', 'via', 'viadct', 'viaduct'],
205
+ vw: ['view', 'vw'],
206
+ vws: ['views', 'vws'],
207
+ vlg: ['vill', 'villag', 'village', 'villg', 'villiage', 'vlg'],
208
+ vlgs: ['villages', 'vlgs'],
209
+ vl: ['ville', 'vl'],
210
+ vis: ['vis', 'vist', 'vista', 'vst', 'vsta'],
211
+ walk: ['walk', 'walks'],
212
+ wall: ['wall'],
213
+ way: ['wy', 'way'],
214
+ ways: ['ways'],
215
+ wl: ['well'],
216
+ wls: ['wells', 'wls'],
217
+ }
218
+
219
+ /** Any listed spelling of a street type → its Pub. 28 abbreviation. */
220
+ const SUFFIX: ReadonlyMap<string, string> = new Map(
221
+ Object.entries(PUB28_SUFFIXES).flatMap(([abbr, spellings]) =>
222
+ spellings.map((s) => [s, abbr] as const),
223
+ ),
224
+ )
225
+
226
+ /** Pub. 28 directionals, spelled out or abbreviated → the abbreviation. */
227
+ const DIRECTIONAL: ReadonlyMap<string, string> = new Map([
228
+ ['n', 'n'],
229
+ ['s', 's'],
230
+ ['e', 'e'],
231
+ ['w', 'w'],
232
+ ['ne', 'ne'],
233
+ ['nw', 'nw'],
234
+ ['se', 'se'],
235
+ ['sw', 'sw'],
236
+ ['north', 'n'],
237
+ ['south', 's'],
238
+ ['east', 'e'],
239
+ ['west', 'w'],
240
+ ['northeast', 'ne'],
241
+ ['northwest', 'nw'],
242
+ ['southeast', 'se'],
243
+ ['southwest', 'sw'],
244
+ ])
245
+
246
+ export interface StreetName {
247
+ /** The directional before the name ("nw"), '' when none. */
248
+ pre: string
249
+ /** The name itself, lower case ("34th", "castro"). */
250
+ name: string
251
+ /** The street type as its Pub. 28 abbreviation ("st", "ter"), '' when the name carries none. */
252
+ type: string
253
+ /** The directional after the type ("n" in "1st St N"), '' when none. */
254
+ post: string
255
+ }
256
+
257
+ /**
258
+ * A street line read into its parts: the house number dropped, the type and
259
+ * the directionals standardised. A word is only read as a type or a
260
+ * directional when a name is left beside it ("North St" is the street named
261
+ * North).
262
+ */
263
+ export function parseStreetName(s: string | null | undefined): StreetName {
264
+ const words = String(s ?? '')
265
+ .toLowerCase()
266
+ .replace(/\./g, '')
267
+ .replace(/^\s*\d+[a-z]?(?:-\d+[a-z]?)?\s+/, '')
268
+ .replace(/[^a-z0-9]+/g, ' ')
269
+ .trim()
270
+ .split(' ')
271
+ .filter(Boolean)
272
+ let post = ''
273
+ let type = ''
274
+ let pre = ''
275
+ if (words.length >= 2 && DIRECTIONAL.has(words[words.length - 1] as string))
276
+ post = DIRECTIONAL.get(words.pop() as string) as string
277
+ if (words.length >= 2 && SUFFIX.has(words[words.length - 1] as string))
278
+ type = SUFFIX.get(words.pop() as string) as string
279
+ if (words.length >= 2 && DIRECTIONAL.has(words[0] as string))
280
+ pre = DIRECTIONAL.get(words.shift() as string) as string
281
+ return { pre, name: words.join(' '), type, post }
282
+ }
283
+
284
+ /**
285
+ * Do two street lines name the same street? The names must agree, and so
286
+ * must the street types and the directionals wherever both lines give one:
287
+ * "NW 34th St" is "Northwest 34th Street" (and "34th Street"), never
288
+ * "Northwest 34th Terrace" or "SW 34th St". A line with no name matches
289
+ * nothing.
290
+ */
291
+ export function sameStreet(
292
+ a: string | StreetName | null | undefined,
293
+ b: string | StreetName | null | undefined,
294
+ ): boolean {
295
+ const x = typeof a === 'object' && a !== null ? a : parseStreetName(a)
296
+ const y = typeof b === 'object' && b !== null ? b : parseStreetName(b)
297
+ if (!x.name || x.name !== y.name) return false
298
+ if (x.type && y.type && x.type !== y.type) return false
299
+ const dx = [x.pre, x.post].filter(Boolean).join(' ')
300
+ const dy = [y.pre, y.post].filter(Boolean).join(' ')
301
+ return !dx || !dy || dx === dy
302
+ }
@@ -0,0 +1,69 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { castYardDimensions, castYardDimensionsOriented, type Pt } from './geometry'
3
+
4
+ // 20 m × 30 m lot centred on the origin, y south
5
+ const LOT: Pt[] = [
6
+ [-10, -15],
7
+ [10, -15],
8
+ [10, 15],
9
+ [-10, 15],
10
+ ]
11
+ const box = (w: number, d: number, cx: number, cz: number, yaw: number): Pt[] => {
12
+ const c = Math.cos(yaw)
13
+ const s = Math.sin(yaw)
14
+ const corners: Pt[] = [
15
+ [-w / 2, -d / 2],
16
+ [w / 2, -d / 2],
17
+ [w / 2, d / 2],
18
+ [-w / 2, d / 2],
19
+ ]
20
+ return corners.map(([lx, lz]) => [cx + c * lx + s * lz, cz - s * lx + c * lz])
21
+ }
22
+ const by = (yards: ReturnType<typeof castYardDimensions>) =>
23
+ Object.fromEntries(yards.map((y) => [y.side, Math.round(y.distance * 1000) / 1000]))
24
+
25
+ describe('castYardDimensionsOriented', () => {
26
+ test('with no rotation it is the axis-aligned cast', () => {
27
+ const house = box(8, 10, 0, -2, 0)
28
+ const oriented = castYardDimensionsOriented(LOT, [house], 0)
29
+ const bbox = castYardDimensions(LOT, { minX: -4, minY: -7, maxX: 4, maxY: 3 })
30
+ expect(by(oriented)).toEqual(by(bbox))
31
+ expect(by(oriented)).toEqual({ north: 8, south: 12, west: 6, east: 6 })
32
+ })
33
+
34
+ test('a house turned 90° measures square to its own faces', () => {
35
+ // 8 wide × 10 deep, turned a quarter: its 10 m side now runs east-west
36
+ const house = box(8, 10, 0, -2, Math.PI / 2)
37
+ const yards = by(castYardDimensionsOriented(LOT, [house], Math.PI / 2))
38
+ expect(yards).toEqual({ north: 9, south: 13, west: 5, east: 5 })
39
+ })
40
+
41
+ test('a house on a diagonal reads its true yards, not the bbox ones', () => {
42
+ // a 10 × 10 house turned 45° at the origin: its corners reach 7.07 m out,
43
+ // its faces 5 m — the axis-aligned bbox would put the north yard at 7.93
44
+ const house = box(10, 10, 0, 0, Math.PI / 4)
45
+ const oriented = castYardDimensionsOriented(LOT, [house], Math.PI / 4)
46
+ for (const y of oriented) {
47
+ // each face midpoint sits 5 m from the centre; the ray runs diagonally to the lot line
48
+ const fromDist = Math.hypot(y.from[0], y.from[1])
49
+ expect(fromDist).toBeCloseTo(5, 9)
50
+ }
51
+ const bbox = castYardDimensions(LOT, { minX: -7.071, minY: -7.071, maxX: 7.071, maxY: 7.071 })
52
+ expect(by(bbox).north).toBeCloseTo(7.929, 3)
53
+ expect(oriented.length).toBe(4)
54
+ })
55
+
56
+ test('no footprint, no yards', () => {
57
+ expect(castYardDimensionsOriented(LOT, [], 0.3)).toEqual([])
58
+ expect(
59
+ castYardDimensionsOriented(
60
+ [
61
+ [0, 0],
62
+ [1, 0],
63
+ ],
64
+ [box(1, 1, 0, 0, 0)],
65
+ 0,
66
+ ),
67
+ ).toEqual([])
68
+ })
69
+ })
@@ -1,4 +1,4 @@
1
- import type { AnyNode, ItemNode, Point2D, StairNode, StairSegmentNode } from '@pascal-app/core'
1
+ import type { AnyNode, ItemNode, Point2D } from '@pascal-app/core'
2
2
 
3
3
  export type FloorplanNodeTransform = {
4
4
  position: Point2D
@@ -21,27 +21,6 @@ export type FloorplanItemEntry = {
21
21
  depth: number
22
22
  }
23
23
 
24
- export type FloorplanStairSegmentEntry = {
25
- centerLine: FloorplanLineSegment | null
26
- innerPolygon: Point2D[]
27
- segment: StairSegmentNode
28
- polygon: Point2D[]
29
- treadBars: Point2D[][]
30
- treadThickness: number
31
- }
32
-
33
- export type FloorplanStairArrowEntry = {
34
- head: Point2D[]
35
- polyline: Point2D[]
36
- }
37
-
38
- export type FloorplanStairEntry = {
39
- arrow: FloorplanStairArrowEntry | null
40
- hitPolygons: Point2D[][]
41
- stair: StairNode
42
- segments: FloorplanStairSegmentEntry[]
43
- }
44
-
45
24
  export type FloorplanSelectionBounds = {
46
25
  minX: number
47
26
  maxX: number
@@ -49,9 +28,4 @@ export type FloorplanSelectionBounds = {
49
28
  maxY: number
50
29
  }
51
30
 
52
- export type StairSegmentTransform = {
53
- position: [number, number, number]
54
- rotation: number
55
- }
56
-
57
31
  export type LevelDescendantMap = ReadonlyMap<string, AnyNode>
@@ -3,11 +3,15 @@ import {
3
3
  type AnyNode,
4
4
  type AnyNodeDefinition,
5
5
  type AnyNodeId,
6
+ beginSceneHistoryPauseSession,
6
7
  CabinetModuleNode,
7
8
  CabinetNode,
8
9
  findLevelAncestorId,
10
+ getSceneHistoryPauseDepth,
9
11
  nodeRegistry,
12
+ pauseSceneHistory,
10
13
  registerNode,
14
+ resumeSceneHistory,
11
15
  type SceneCommit,
12
16
  subscribeSceneCommits,
13
17
  useScene,
@@ -187,6 +191,8 @@ describe('commitFreshPlacementSubtree', () => {
187
191
  } as Partial<AnyNode>)
188
192
  unsubscribe()
189
193
 
194
+ // A legacy caller's raw pause is kept after the one recorded step.
195
+ expect(useScene.temporal.getState().isTracking).toBe(false)
190
196
  expect(committedId).toBeTruthy()
191
197
  expect(committedId).not.toBe(SHELF_ID)
192
198
  const finalId = committedId as AnyNodeId
@@ -217,6 +223,36 @@ describe('commitFreshPlacementSubtree', () => {
217
223
  expect((useScene.getState().nodes[LEVEL_ID] as { children: AnyNodeId[] }).children).toEqual([])
218
224
  })
219
225
 
226
+ test("never records inside another owner's pause", () => {
227
+ pauseSceneHistory(useScene)
228
+ try {
229
+ const committedId = commitFreshPlacementSubtree(SHELF_ID, {
230
+ position: [2, 0, 3],
231
+ visible: true,
232
+ } as Partial<AnyNode>)
233
+ expect(committedId).toBeTruthy()
234
+ expect(useScene.temporal.getState().pastStates).toHaveLength(0)
235
+ expect(useScene.temporal.getState().isTracking).toBe(false)
236
+ expect(getSceneHistoryPauseDepth()).toBe(1)
237
+ } finally {
238
+ resumeSceneHistory(useScene)
239
+ }
240
+ })
241
+
242
+ test("lifts the moving node's gesture session for its one step and keeps it after", () => {
243
+ const overlay = beginSceneHistoryPauseSession(useScene, { gesture: SHELF_ID })
244
+ const committedId = commitFreshPlacementSubtree(SHELF_ID, {
245
+ position: [2, 0, 3],
246
+ visible: true,
247
+ } as Partial<AnyNode>)
248
+ expect(committedId).toBeTruthy()
249
+ expect(useScene.temporal.getState().pastStates).toHaveLength(1)
250
+ expect(useScene.temporal.getState().isTracking).toBe(false)
251
+ overlay.end()
252
+ expect(getSceneHistoryPauseDepth()).toBe(0)
253
+ expect(useScene.temporal.getState().isTracking).toBe(true)
254
+ })
255
+
220
256
  test('uses the subtree contract for childless variants and never aliases root-only children', () => {
221
257
  registerCabinetClonePrepTestKind()
222
258
  const childlessCabinet = {
@@ -1,13 +1,18 @@
1
1
  import {
2
2
  type AnyNode,
3
3
  type AnyNodeId,
4
+ beginSceneHistoryPauseSession,
5
+ type CloneNodesIntoResult,
4
6
  cloneNodesInto,
7
+ collectionIdsOf,
5
8
  collectSubtree,
6
9
  createSceneApi,
7
10
  type DuplicableConfig,
11
+ getSceneHistoryPauseDepth,
8
12
  getSurfaceProvider,
9
13
  nodeRegistry,
10
14
  resolveSurfacePlacement,
15
+ runSceneHistoryDraftWrite,
11
16
  type SurfaceRejectReason,
12
17
  useScene,
13
18
  } from '@pascal-app/core'
@@ -34,6 +39,20 @@ function parentIdOf(node: AnyNode): AnyNodeId | undefined {
34
39
  return parentId ?? undefined
35
40
  }
36
41
 
42
+ /**
43
+ * Create ops for a clone of scene nodes: each copy joins the collections its
44
+ * source is in, whatever its kind.
45
+ */
46
+ export function copyCreateOps(cloned: CloneNodesIntoResult, parentId: AnyNodeId | undefined) {
47
+ const collections = useScene.getState().collections
48
+ const sourceIds = new Map([...cloned.idMap].map(([source, copy]) => [copy, source]))
49
+ return cloned.nodes.map((node, index) => ({
50
+ node,
51
+ ...(index === 0 && parentId ? { parentId } : {}),
52
+ collectionIds: collectionIdsOf(collections, sourceIds.get(node.id as AnyNodeId)!),
53
+ }))
54
+ }
55
+
37
56
  function duplicableConfigFor(node: AnyNode): DuplicableConfig | null {
38
57
  const duplicable = nodeRegistry.get(node.type)?.capabilities?.duplicable
39
58
  return duplicable && typeof duplicable === 'object' ? duplicable : null
@@ -114,9 +133,7 @@ export function createFreshPlacementSubtree(
114
133
  })
115
134
 
116
135
  scene.applyNodeChanges({
117
- create: cloned.nodes.map((node, index) =>
118
- index === 0 && parentId ? { node, parentId } : { node },
119
- ),
136
+ create: copyCreateOps(cloned, parentId),
120
137
  update: surfaceAttachmentUpdates(cloned.rootId, parentId, surfaceAttachmentId(subtree.root)),
121
138
  })
122
139
 
@@ -202,6 +219,21 @@ function namedSurfaceRejection(
202
219
  return null
203
220
  }
204
221
 
222
+ /**
223
+ * Runs a placement's one committing write as a history step. The moving node's gesture
224
+ * sessions (the 2D move overlay's) are lifted for it; any other owner's pause still holds, so
225
+ * the write then records nothing. A legacy caller's raw `temporal.pause()` is kept afterwards.
226
+ */
227
+ function recordPlacementStep(rootId: AnyNodeId, wasTracking: boolean, write: () => void): void {
228
+ const step = beginSceneHistoryPauseSession(useScene, { gesture: rootId })
229
+ try {
230
+ step.commitStep(write)
231
+ } finally {
232
+ step.end()
233
+ if (!wasTracking && getSceneHistoryPauseDepth() === 0) useScene.temporal.getState().pause()
234
+ }
235
+ }
236
+
205
237
  /**
206
238
  * Replace the draft in one validated write. History already excludes fresh
207
239
  * subtrees, so this records one creation without first deleting the preview.
@@ -226,6 +258,8 @@ export function commitFreshPlacementSubtree(
226
258
  const descendants = subtree.descendants.map((node) => cleanPlacementMetadata(node))
227
259
  const parentId = parentIdOf(root)
228
260
  const cloned = cloneNodesInto([root, ...descendants], { rootId, parentId })
261
+ // The drafts' memberships, read before they are deleted.
262
+ const create = copyCreateOps(cloned, parentId)
229
263
  const updates = surfaceAttachmentUpdates(rootId, null, null)
230
264
  for (const update of surfaceAttachmentUpdates(cloned.rootId, parentId, surfaceId)) {
231
265
  const attachments = { ...(update.data as { attachments: Record<string, string> }).attachments }
@@ -243,43 +277,32 @@ export function commitFreshPlacementSubtree(
243
277
  const wasTracking = temporal.isTracking
244
278
  if (!factoryCreated && descendants.length === 0 && surfaceId === null) {
245
279
  // Preserve the established root-only lifecycle for placements outside the subtree factory.
246
- if (wasTracking) temporal.pause()
247
- updateSurfaceNode(rootId, {}, null)
248
- useScene.getState().deleteNode(rootId)
249
- temporal.resume()
250
- useScene.getState().applyNodeChanges({
251
- create: cloned.nodes.map((node, index) =>
252
- index === 0 && parentId ? { node, parentId } : { node },
253
- ),
254
- update: updates,
280
+ runSceneHistoryDraftWrite(() => {
281
+ updateSurfaceNode(rootId, {}, null)
282
+ useScene.getState().deleteNode(rootId)
255
283
  })
256
- if (!wasTracking) temporal.pause()
284
+ recordPlacementStep(rootId, wasTracking, () =>
285
+ useScene.getState().applyNodeChanges({ create, update: updates }),
286
+ )
257
287
  } else {
258
- temporal.resume()
259
- try {
260
- // applyNodeChanges validates the complete proposed graph before publishing any part of it.
261
- scene.applyNodeChanges({
262
- delete: [rootId],
263
- create: cloned.nodes.map((node, index) =>
264
- index === 0 && parentId ? { node, parentId } : { node },
265
- ),
266
- update: updates,
267
- })
268
- for (const node of [subtree.root, ...subtree.descendants]) scene.clearDirty(node.id)
269
- } catch (error) {
270
- // Zustand publishes before notifying subscribers. A subscriber error must restore
271
- // the draft and its ownership/history, never masquerade as a placement refusal.
272
- temporal.pause()
288
+ recordPlacementStep(rootId, wasTracking, () => {
273
289
  try {
274
- if (useScene.getState() !== scene) useScene.setState(scene, true)
275
- } finally {
276
- useInteractionScope.setState(scope)
277
- useScene.temporal.setState(temporal)
290
+ // applyNodeChanges validates the complete proposed graph before publishing any part of it.
291
+ scene.applyNodeChanges({ delete: [rootId], create, update: updates })
292
+ for (const node of [subtree.root, ...subtree.descendants]) scene.clearDirty(node.id)
293
+ } catch (error) {
294
+ // Zustand publishes before notifying subscribers. A subscriber error must restore
295
+ // the draft and its ownership/history, never masquerade as a placement refusal.
296
+ useScene.temporal.getState().pause()
297
+ try {
298
+ if (useScene.getState() !== scene) useScene.setState(scene, true)
299
+ } finally {
300
+ useInteractionScope.setState(scope)
301
+ useScene.temporal.setState(temporal)
302
+ }
303
+ throw error
278
304
  }
279
- throw error
280
- } finally {
281
- if (!wasTracking) temporal.pause()
282
- }
305
+ })
283
306
  }
284
307
  useInteractionScope.getState().finishSubtree(rootId)
285
308
  if (usePlacementPreview.getState().node?.id === rootId) usePlacementPreview.getState().clear()