@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,379 @@
1
+ /**
2
+ * What the site plan reads off the scene — the site, the building on it, its
3
+ * storeys, the walls' outline, the roof's outline and the outdoor slabs
4
+ * (porches, decks, patios, and the flatwork: driveway and walks) — in SITE
5
+ * metres. Shared by the drawing (`build-site-plan-drawing.ts`) and the
6
+ * coverage / impervious-area figures (`coverage.ts`) so both read the same
7
+ * parts. Pure: no store.
8
+ */
9
+ import {
10
+ type AnyNode,
11
+ type AnyNodeId,
12
+ type BuildingNode,
13
+ calculateLevelMiters,
14
+ getWallPlanFootprint,
15
+ type LevelNode,
16
+ migrateSiteMetadata,
17
+ type SceneSnapshot,
18
+ type SiteNode,
19
+ unionPolygons,
20
+ type WallNode,
21
+ } from '@pascal-app/core'
22
+ import { type Pt, pointInPolygon, polygonArea, polygonBounds } from './geometry'
23
+
24
+ /**
25
+ * The scene's site node, with legacy `metadata.setbacks / zone / apn` lifted
26
+ * onto the real fields. This is a READ-side lift only — nothing is written
27
+ * back to the store, so a scene that was never re-saved keeps its metadata.
28
+ * A persistence-side migration would belong in
29
+ * `packages/core/src/utils/scene-migrations.ts` (not owned by this
30
+ * workstream); see the note in `migrateSiteMetadata`.
31
+ */
32
+ export function findSite(scene: SceneSnapshot): SiteNode | null {
33
+ for (const node of Object.values(scene.nodes)) {
34
+ if (node.type !== 'site') continue
35
+ const site = node as SiteNode
36
+ const patch = migrateSiteMetadata(site)
37
+ return Object.keys(patch).length > 0 ? { ...site, ...patch } : site
38
+ }
39
+ return null
40
+ }
41
+
42
+ export function findBuilding(scene: SceneSnapshot, site: SiteNode | null): BuildingNode | null {
43
+ if (site) {
44
+ for (const childId of site.children ?? []) {
45
+ const child = scene.nodes[childId as AnyNodeId]
46
+ if (child?.type === 'building') return child as BuildingNode
47
+ }
48
+ }
49
+ for (const node of Object.values(scene.nodes)) {
50
+ if (node.type === 'building') return node as BuildingNode
51
+ }
52
+ return null
53
+ }
54
+
55
+ /** Lowest `level` number among the building's level children. */
56
+ export function findLowestLevel(
57
+ scene: SceneSnapshot,
58
+ building: BuildingNode | null,
59
+ ): LevelNode | null {
60
+ if (!building) return null
61
+ let best: LevelNode | null = null
62
+ for (const childId of building.children ?? []) {
63
+ const child = scene.nodes[childId as AnyNodeId] as AnyNode | undefined
64
+ if (child?.type !== 'level') continue
65
+ const level = child as LevelNode
66
+ if (!best || level.level < best.level) best = level
67
+ }
68
+ return best
69
+ }
70
+
71
+ /**
72
+ * Per-wall plan footprints for a level, mitred, then transformed by the
73
+ * building's site placement. `position` is `[x, y, z]` in site metres (y is
74
+ * height, ignored in plan); `rotation[1]` is the yaw in radians.
75
+ */
76
+ export function levelFootprintLoops(
77
+ scene: SceneSnapshot,
78
+ level: LevelNode | null,
79
+ building: BuildingNode | null,
80
+ ): Pt[][] {
81
+ if (!level) return []
82
+ const walls: WallNode[] = []
83
+ for (const childId of level.children ?? []) {
84
+ const child = scene.nodes[childId as AnyNodeId] as AnyNode | undefined
85
+ if (child?.type === 'wall') walls.push(child as WallNode)
86
+ }
87
+ if (walls.length === 0) return []
88
+
89
+ const miters = calculateLevelMiters(walls)
90
+ const yaw = building?.rotation?.[1] ?? 0
91
+ const cos = Math.cos(yaw)
92
+ const sin = Math.sin(yaw)
93
+ const ox = building?.position?.[0] ?? 0
94
+ const oz = building?.position?.[2] ?? 0
95
+
96
+ const loops: Pt[][] = []
97
+ for (const wall of walls) {
98
+ const poly = getWallPlanFootprint(wall, miters)
99
+ if (poly.length < 3) continue
100
+ // three.js Y rotation, the convention BuildingRenderer applies:
101
+ // world = (cos·lx + sin·lz, −sin·lx + cos·lz) + position
102
+ loops.push(poly.map((p) => [ox + p.x * cos + p.y * sin, oz - p.x * sin + p.y * cos] as Pt))
103
+ }
104
+ return loops
105
+ }
106
+
107
+ /**
108
+ * The outer ring(s) of a set of wall bands: their union, minus any ring that
109
+ * lies inside another (a room enclosed by partitions is a hole in the union,
110
+ * not a second building). Falls back to the bands themselves when the union
111
+ * yields nothing.
112
+ */
113
+ export function footprintOutline(loops: readonly Pt[][]): Pt[][] {
114
+ const rings = unionPolygons(loops.map((loop) => loop.map((p) => [p[0], p[1]]))) as Pt[][]
115
+ if (rings.length === 0) return [...loops]
116
+ const inside = (p: Pt, ring: Pt[]): boolean => {
117
+ let hit = false
118
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
119
+ const a = ring[i] as Pt
120
+ const b = ring[j] as Pt
121
+ if (
122
+ a[1] > p[1] !== b[1] > p[1] &&
123
+ p[0] < ((b[0] - a[0]) * (p[1] - a[1])) / (b[1] - a[1]) + a[0]
124
+ ) {
125
+ hit = !hit
126
+ }
127
+ }
128
+ return hit
129
+ }
130
+ return rings.filter(
131
+ (ring, i) =>
132
+ !rings.some((other, j) => j !== i && ring[0] !== undefined && inside(ring[0], other)),
133
+ )
134
+ }
135
+
136
+ /** A three.js Y rotation of a plan point: local (x, z) turned by `yaw`. */
137
+ export function turn(x: number, z: number, yaw: number): Pt {
138
+ const c = Math.cos(yaw)
139
+ const s = Math.sin(yaw)
140
+ return [c * x + s * z, -s * x + c * z]
141
+ }
142
+
143
+ export function yawOf(rotation: unknown): number {
144
+ if (typeof rotation === 'number') return rotation
145
+ if (Array.isArray(rotation)) return Number(rotation[1] ?? 0) || 0
146
+ return 0
147
+ }
148
+
149
+ /** Level-local plan (x, z) → site metres, through the building's placement (see `levelFootprintLoops`). */
150
+ export function siteFrame(building: BuildingNode | null): (x: number, z: number) => Pt {
151
+ const yaw = building?.rotation?.[1] ?? 0
152
+ const ox = building?.position?.[0] ?? 0
153
+ const oz = building?.position?.[2] ?? 0
154
+ return (x, z) => {
155
+ const [tx, tz] = turn(x, z, yaw)
156
+ return [ox + tx, oz + tz]
157
+ }
158
+ }
159
+
160
+ /**
161
+ * The roof's outline on the lot — every roof segment's plan rectangle plus
162
+ * its overhang, through the segment's, the roof's and the building's turns,
163
+ * unioned into the outer ring(s). Dashed on the site plan: what the eye sees
164
+ * from above is the roof, and the setback is measured to the wall under it.
165
+ */
166
+ export function roofOutlineRings(
167
+ scene: SceneSnapshot,
168
+ level: LevelNode | null,
169
+ building: BuildingNode | null,
170
+ ): Pt[][] {
171
+ if (!level) return []
172
+ const toSite = siteFrame(building)
173
+ const roofs = new Map<string, { position?: number[]; rotation?: unknown }>()
174
+ for (const childId of level.children ?? []) {
175
+ const child = scene.nodes[childId as AnyNodeId] as AnyNode | undefined
176
+ if (child?.type === 'roof')
177
+ roofs.set(String(child.id), child as { position?: number[]; rotation?: unknown })
178
+ }
179
+ if (roofs.size === 0) return []
180
+ const rects: Pt[][] = []
181
+ for (const node of Object.values(scene.nodes)) {
182
+ if (node.type !== 'roof-segment') continue
183
+ const roof = roofs.get(String((node as { parentId?: unknown }).parentId))
184
+ if (!roof) continue
185
+ const seg = node as unknown as {
186
+ position?: number[]
187
+ rotation?: unknown
188
+ width?: number
189
+ depth?: number
190
+ overhang?: number
191
+ }
192
+ const w = (seg.width ?? 0) / 2 + (seg.overhang ?? 0)
193
+ const d = (seg.depth ?? 0) / 2 + (seg.overhang ?? 0)
194
+ if (w <= 0 || d <= 0) continue
195
+ const segYaw = yawOf(seg.rotation)
196
+ const roofYaw = yawOf(roof.rotation)
197
+ const sx = seg.position?.[0] ?? 0
198
+ const sz = seg.position?.[2] ?? 0
199
+ const rx = roof.position?.[0] ?? 0
200
+ const rz = roof.position?.[2] ?? 0
201
+ const local: Pt[] = [
202
+ [-w, -d],
203
+ [w, -d],
204
+ [w, d],
205
+ [-w, d],
206
+ ]
207
+ rects.push(
208
+ local.map(([lx, lz]) => {
209
+ const [ax, az] = turn(lx, lz, segYaw)
210
+ const [bx, bz] = turn(ax + sx, az + sz, roofYaw)
211
+ return toSite(bx + rx, bz + rz)
212
+ }),
213
+ )
214
+ }
215
+ if (rects.length === 0) return []
216
+ const rings = unionPolygons(rects.map((r) => r.map((p) => [p[0], p[1]]))) as Pt[][]
217
+ return rings.length > 0 ? rings : rects
218
+ }
219
+
220
+ /** Every storey of the building at or above grade (level ≥ 0), lowest first. */
221
+ export function aboveGradeLevels(scene: SceneSnapshot, building: BuildingNode | null): LevelNode[] {
222
+ if (!building) return []
223
+ const out: LevelNode[] = []
224
+ for (const childId of building.children ?? []) {
225
+ const child = scene.nodes[childId as AnyNodeId] as AnyNode | undefined
226
+ if (child?.type === 'level' && (child as LevelNode).level >= 0) out.push(child as LevelNode)
227
+ }
228
+ return out.sort((a, b) => a.level - b.level)
229
+ }
230
+
231
+ /* ------------------------------------------------------ outdoor slabs */
232
+
233
+ /** What an outdoor slab is on the site plan and in the impervious table. */
234
+ export type OutdoorKind = 'porch' | 'deck' | 'patio' | 'landing' | 'driveway' | 'walk'
235
+
236
+ export interface OutdoorPart {
237
+ id: string
238
+ kind: OutdoorKind
239
+ /** Upper-case label for the plan ("PORCH", "REAR DECK", "DRIVEWAY"). */
240
+ label: string
241
+ ring: Pt[]
242
+ /** Plan area, m². */
243
+ area: number
244
+ /** Under a roof (≥ half its area inside a roof outline): building coverage, not open paving. */
245
+ covered: boolean
246
+ /** Wood decking (a deck): open boards over grade. */
247
+ wood: boolean
248
+ }
249
+
250
+ /**
251
+ * Flatwork: a slab that is site paving, not part of the building — the
252
+ * generator's driveway and walks (`metadata.flatwork`), and a hand-drawn slab
253
+ * named like one (the floor plans leave the same slabs out).
254
+ */
255
+ const HARDSCAPE =
256
+ /driveway|drive\s*way|\bdrive\b|approach|walkway|sidewalk|\bwalk\b|\bpath\b|apron|parking|\bpavers?\b/i
257
+ export function flatworkKindOf(node: {
258
+ type?: string
259
+ name?: unknown
260
+ metadata?: unknown
261
+ }): 'driveway' | 'walk' | null {
262
+ if (node.type !== 'slab') return null
263
+ const meta = (node.metadata ?? {}) as { flatwork?: unknown; floor?: unknown }
264
+ if (meta.flatwork === 'driveway' || meta.flatwork === 'walk') return meta.flatwork
265
+ const name = typeof node.name === 'string' ? node.name : ''
266
+ const floor = typeof meta.floor === 'string' ? meta.floor : ''
267
+ const text = `${name} ${floor}`
268
+ if (!HARDSCAPE.test(text)) return null
269
+ return /drive|approach|apron|parking/i.test(text) ? 'driveway' : 'walk'
270
+ }
271
+
272
+ /** The house's own floors: the storey slab, a raised platform, the garage pad, an upper floor. */
273
+ const HOUSE_FLOORS = new Set(['slab-on-grade', 'platform', 'garage-slab-at-grade', 'floor'])
274
+
275
+ /** Share of `ring`'s area inside any of `covers` (a 6 × 6 sample grid). */
276
+ function coveredShare(ring: readonly Pt[], covers: readonly Pt[][]): number {
277
+ if (covers.length === 0) return 0
278
+ const b = polygonBounds(ring)
279
+ let inside = 0
280
+ let under = 0
281
+ const N = 6
282
+ for (let i = 0; i < N; i++) {
283
+ for (let j = 0; j < N; j++) {
284
+ const x = b.minX + ((i + 0.5) / N) * (b.maxX - b.minX)
285
+ const y = b.minY + ((j + 0.5) / N) * (b.maxY - b.minY)
286
+ if (!pointInPolygon(ring, x, y)) continue
287
+ inside++
288
+ if (covers.some((c) => pointInPolygon(c, x, y))) under++
289
+ }
290
+ }
291
+ return inside > 0 ? under / inside : 0
292
+ }
293
+
294
+ /**
295
+ * The level's outdoor slabs in site metres: porches, decks, patios and
296
+ * landings (a generator `metadata.porch`, or a deck / porch-slab floor, or a
297
+ * slab lying outside the house's walls) and the flatwork. The house's own
298
+ * floors (the storey slab, a raised platform, the garage pad) and a porch
299
+ * cover's beam are not outdoor parts. `roofs` decides which are covered.
300
+ */
301
+ export function outdoorParts(
302
+ scene: SceneSnapshot,
303
+ level: LevelNode | null,
304
+ building: BuildingNode | null,
305
+ walls: readonly Pt[][],
306
+ roofs: readonly Pt[][],
307
+ ): OutdoorPart[] {
308
+ if (!level) return []
309
+ const toSite = siteFrame(building)
310
+ const out: OutdoorPart[] = []
311
+ for (const childId of level.children ?? []) {
312
+ const child = scene.nodes[childId as AnyNodeId] as AnyNode | undefined
313
+ if (child?.type !== 'slab' || child.visible === false) continue
314
+ const slab = child as unknown as {
315
+ id: string
316
+ polygon?: number[][]
317
+ metadata?: Record<string, unknown>
318
+ name?: string
319
+ }
320
+ const poly = slab.polygon ?? []
321
+ if (poly.length < 3) continue
322
+ const ring = poly.map((p) => toSite(p[0] ?? 0, p[1] ?? 0))
323
+ const area = polygonArea(ring)
324
+ const meta = slab.metadata ?? {}
325
+ const floor = typeof meta.floor === 'string' ? meta.floor : ''
326
+ const flat = flatworkKindOf(child as never)
327
+ if (flat) {
328
+ out.push({
329
+ id: slab.id,
330
+ kind: flat,
331
+ label: flat === 'driveway' ? 'DRIVEWAY' : 'WALK',
332
+ ring,
333
+ area,
334
+ covered: false,
335
+ wood: false,
336
+ })
337
+ continue
338
+ }
339
+ // a porch cover's beam box is not a floor
340
+ if (/beam/i.test(floor) || /beam/i.test(slab.name ?? '')) continue
341
+ const porch = meta.porch as { policy?: string; entrance?: string } | undefined
342
+ const outdoorFloor = floor === 'deck' || floor === 'porch-slab'
343
+ if (!porch && !outdoorFloor) {
344
+ // a hand-drawn slab: the house's floor when it lies under the walls
345
+ if (HOUSE_FLOORS.has(floor)) continue
346
+ const c = ring.reduce<[number, number]>(
347
+ (s, p) => [s[0] + p[0] / ring.length, s[1] + p[1] / ring.length],
348
+ [0, 0],
349
+ )
350
+ if (walls.length === 0 || walls.some((w) => pointInPolygon(w, c[0], c[1]))) continue
351
+ }
352
+ const wood = floor === 'deck'
353
+ const policy = porch?.policy ?? ''
354
+ const kind: OutdoorKind =
355
+ policy === 'deck' || (wood && policy !== 'full' && policy !== 'entry')
356
+ ? 'deck'
357
+ : policy === 'landing'
358
+ ? 'landing'
359
+ : policy === 'patio'
360
+ ? 'patio'
361
+ : 'porch'
362
+ const label =
363
+ porch?.policy === 'entry'
364
+ ? 'PORCH'
365
+ : porch?.policy === 'landing'
366
+ ? 'LANDING'
367
+ : (slab.name ?? kind).toUpperCase()
368
+ out.push({
369
+ id: slab.id,
370
+ kind,
371
+ label,
372
+ ring,
373
+ area,
374
+ covered: coveredShare(ring, roofs) >= 0.5,
375
+ wood,
376
+ })
377
+ }
378
+ return out
379
+ }
@@ -0,0 +1,138 @@
1
+ 'use client'
2
+
3
+ import {
4
+ type AnyNodeId,
5
+ type BuildingNode,
6
+ pauseSceneHistory,
7
+ resumeSceneHistory,
8
+ useScene,
9
+ } from '@pascal-app/core'
10
+ import { type PointerEvent as ReactPointerEvent, useCallback, useMemo, useRef } from 'react'
11
+ import { FloorplanGeometryRenderer } from '../../../components/editor-2d/renderers/floorplan-geometry-renderer'
12
+ import { clientToPlan } from '../plan-coords'
13
+ import { buildSitePlanDrawing } from './build-site-plan-drawing'
14
+
15
+ /**
16
+ * Site-plan view of the 2D editor.
17
+ *
18
+ * Mounted INSTEAD of `<FloorplanRegistryLayer>` when
19
+ * `useDrawingView().drawingType === 'site-plan'`, inside the same scene `<g>`,
20
+ * so the coordinates it draws in (site metres, origin = the geocoded point,
21
+ * x east, y south) are the ones `clientToPlan` reports back.
22
+ *
23
+ * Everything is recomputed from the scene snapshot on each store change, so
24
+ * the lot line, envelope, footprint and yard dimensions are live.
25
+ */
26
+ export function FloorplanSitePlanLayer() {
27
+ // Subscribing to the whole node map is what the site plan actually depends
28
+ // on (walls, building placement, site fields); the drawing is cheap enough
29
+ // (tens of primitives) that memoising on the map identity is sufficient.
30
+ const nodes = useScene((s) => s.nodes)
31
+
32
+ const drawing = useMemo(() => {
33
+ const state = useScene.getState()
34
+ return buildSitePlanDrawing({
35
+ nodes,
36
+ rootNodeIds: state.rootNodeIds,
37
+ collections: state.collections,
38
+ materials: state.materials,
39
+ installedPlugins: state.installedPlugins,
40
+ })
41
+ }, [nodes])
42
+
43
+ const dragRef = useRef<{
44
+ id: AnyNodeId
45
+ pointerId: number
46
+ startPlan: [number, number]
47
+ startPosition: [number, number, number]
48
+ } | null>(null)
49
+
50
+ const buildingId = drawing.meta.buildingId
51
+
52
+ const handlePointerDown = useCallback(
53
+ (event: ReactPointerEvent<SVGGElement>) => {
54
+ if (!buildingId || event.button !== 0) return
55
+ const building = useScene.getState().nodes[buildingId] as BuildingNode | undefined
56
+ if (!building) return
57
+ const startPlan = clientToPlan(event.clientX, event.clientY)
58
+ if (!startPlan) return
59
+ event.preventDefault()
60
+ event.stopPropagation()
61
+ event.currentTarget.setPointerCapture(event.pointerId)
62
+ dragRef.current = {
63
+ id: buildingId,
64
+ pointerId: event.pointerId,
65
+ startPlan: [startPlan[0], startPlan[1]],
66
+ startPosition: [...building.position] as [number, number, number],
67
+ }
68
+ // One history entry for the whole drag, committed on pointer-up.
69
+ pauseSceneHistory(useScene)
70
+ },
71
+ [buildingId],
72
+ )
73
+
74
+ const handlePointerMove = useCallback((event: ReactPointerEvent<SVGGElement>) => {
75
+ const drag = dragRef.current
76
+ if (!drag || drag.pointerId !== event.pointerId) return
77
+ const plan = clientToPlan(event.clientX, event.clientY)
78
+ if (!plan) return
79
+ const dx = plan[0] - drag.startPlan[0]
80
+ const dz = plan[1] - drag.startPlan[1]
81
+ useScene.getState().updateNode(drag.id, {
82
+ position: [drag.startPosition[0] + dx, drag.startPosition[1], drag.startPosition[2] + dz],
83
+ })
84
+ }, [])
85
+
86
+ const endDrag = useCallback((event: ReactPointerEvent<SVGGElement>) => {
87
+ const drag = dragRef.current
88
+ if (!drag || drag.pointerId !== event.pointerId) return
89
+ dragRef.current = null
90
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) {
91
+ event.currentTarget.releasePointerCapture(event.pointerId)
92
+ }
93
+ resumeSceneHistory(useScene)
94
+ }, [])
95
+
96
+ const footprintIndexes = new Set<number>()
97
+ drawing.primitives.forEach((p, i) => {
98
+ const meta = (p as { metadata?: Record<string, unknown> }).metadata
99
+ if (meta?.sitePlan === 'building-footprint') footprintIndexes.add(i)
100
+ })
101
+
102
+ return (
103
+ <g data-site-plan="">
104
+ {drawing.primitives.map((geometry, index) =>
105
+ footprintIndexes.has(index) ? null : (
106
+ <FloorplanGeometryRenderer
107
+ geometry={geometry}
108
+ // eslint-disable-next-line react/no-array-index-key -- primitives are positional, rebuilt wholesale each render
109
+ key={`site-plan-${index}`}
110
+ />
111
+ ),
112
+ )}
113
+ {/* Footprint bands live in their own group so the whole building drags
114
+ as one target, writing `building.position` — the same field the 3D
115
+ move tool writes. */}
116
+ <g
117
+ data-site-plan-building=""
118
+ onPointerCancel={endDrag}
119
+ onPointerDown={handlePointerDown}
120
+ onPointerMove={handlePointerMove}
121
+ onPointerUp={endDrag}
122
+ style={{ cursor: buildingId ? 'move' : undefined }}
123
+ >
124
+ {drawing.primitives.map((geometry, index) =>
125
+ footprintIndexes.has(index) ? (
126
+ <FloorplanGeometryRenderer
127
+ geometry={geometry}
128
+ // eslint-disable-next-line react/no-array-index-key -- see above
129
+ key={`site-plan-fp-${index}`}
130
+ />
131
+ ) : null,
132
+ )}
133
+ </g>
134
+ </g>
135
+ )
136
+ }
137
+
138
+ export default FloorplanSitePlanLayer
@@ -0,0 +1,69 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { parseStreetName, sameStreet } from './street-name'
3
+
4
+ describe('parseStreetName (USPS Pub. 28)', () => {
5
+ test('the number dropped, the directional and the type standardised', () => {
6
+ expect(parseStreetName('4121 NW 34th St')).toEqual({
7
+ pre: 'nw',
8
+ name: '34th',
9
+ type: 'st',
10
+ post: '',
11
+ })
12
+ expect(parseStreetName('Northwest 34th Terrace')).toEqual({
13
+ pre: 'nw',
14
+ name: '34th',
15
+ type: 'ter',
16
+ post: '',
17
+ })
18
+ expect(parseStreetName('N.W. 34th Terr.')).toEqual({
19
+ pre: 'nw',
20
+ name: '34th',
21
+ type: 'ter',
22
+ post: '',
23
+ })
24
+ expect(parseStreetName('2715 LAKE HUNTER DR')).toEqual({
25
+ pre: '',
26
+ name: 'lake hunter',
27
+ type: 'dr',
28
+ post: '',
29
+ })
30
+ expect(parseStreetName('100 1st Ave N')).toEqual({
31
+ pre: '',
32
+ name: '1st',
33
+ type: 'ave',
34
+ post: 'n',
35
+ })
36
+ expect(parseStreetName('Del Prado Boulevard South').type).toBe('blvd')
37
+ expect(parseStreetName('Pine Island Pkwy').type).toBe('pkwy')
38
+ expect(parseStreetName('Old Dixie Highway').type).toBe('hwy')
39
+ })
40
+
41
+ test('a word is only a type or a directional when a name is left beside it', () => {
42
+ expect(parseStreetName('North St')).toEqual({ pre: '', name: 'north', type: 'st', post: '' })
43
+ expect(parseStreetName('Broadway')).toEqual({ pre: '', name: 'broadway', type: '', post: '' })
44
+ expect(parseStreetName('')).toEqual({ pre: '', name: '', type: '', post: '' })
45
+ })
46
+ })
47
+
48
+ describe('sameStreet', () => {
49
+ test('abbreviated and spelled-out lines of one street match', () => {
50
+ expect(sameStreet('4121 NW 34th St', 'Northwest 34th Street')).toBe(true)
51
+ expect(sameStreet('1247 NE 109th St', 'Northeast 109th Street')).toBe(true)
52
+ expect(sameStreet('2600 Castro Way', 'Castro Way')).toBe(true)
53
+ expect(sameStreet('44 Oak Cir', 'Oak Circle')).toBe(true)
54
+ expect(sameStreet('9 Elm Ct', 'Elm Court')).toBe(true)
55
+ })
56
+
57
+ test('a different street type or quadrant is a different street', () => {
58
+ expect(sameStreet('4121 NW 34th St', 'Northwest 34th Terrace')).toBe(false)
59
+ expect(sameStreet('4121 NW 34th St', 'Southwest 34th Street')).toBe(false)
60
+ expect(sameStreet('12 Oak Ln', 'Oak Place')).toBe(false)
61
+ expect(sameStreet('12 Oak Rd', 'Oak Road')).toBe(true)
62
+ })
63
+
64
+ test('a line that leaves the type or the directional out does not rule a street out', () => {
65
+ expect(sameStreet('S Castro', 'Castro Way')).toBe(true)
66
+ expect(sameStreet('4121 NW 34th St', '34th Street')).toBe(true)
67
+ expect(sameStreet('', 'Castro Way')).toBe(false)
68
+ })
69
+ })