@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,43 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { createTerrainField, quantize, terrainContours } from '@pascal-app/core'
3
+
4
+ /** A 20 × 20 m field at 1 m spacing rising 2 m from x = −10 to x = +10 (a plane). */
5
+ function ramp() {
6
+ const field = createTerrainField({ origin: [-10, -10], spacing: 1, cols: 21, rows: 21 })
7
+ const heights = new Int16Array(field.heights)
8
+ for (let row = 0; row < 21; row++)
9
+ for (let col = 0; col < 21; col++) heights[row * 21 + col] = quantize(field, (col / 20) * 2)
10
+ return { ...field, heights }
11
+ }
12
+
13
+ const LOT: [number, number][] = [
14
+ [-8, -8],
15
+ [8, -8],
16
+ [8, 8],
17
+ [-8, 8],
18
+ ]
19
+
20
+ describe('terrainContours', () => {
21
+ test('a plane rising along x: straight north–south lines at every interval, clipped to the lot, every fifth an index', () => {
22
+ const contours = terrainContours(ramp(), 0.3048, LOT) // 1 ft interval on a 2 m rise → levels 0.30 … 1.83
23
+ expect(contours.length).toBeGreaterThanOrEqual(5)
24
+ for (const c of contours) {
25
+ // a level line of a plane rising in x is a vertical line x = const
26
+ const xs = c.points.map((p) => p[0])
27
+ expect(Math.max(...xs) - Math.min(...xs)).toBeLessThan(1e-6)
28
+ expect(Math.abs(xs[0]! - (-10 + (c.levelM / 2) * 20))).toBeLessThan(1e-6)
29
+ // clipped to the lot: no point outside z ∈ [−8, 8] by more than a cell
30
+ for (const p of c.points) expect(Math.abs(p[1])).toBeLessThanOrEqual(9)
31
+ }
32
+ expect(contours.some((c) => c.index)).toBe(true)
33
+ // one polyline per level: the chaining joined the cell segments
34
+ const levels = new Set(contours.map((c) => c.levelM.toFixed(4)))
35
+ expect(levels.size).toBe(contours.length)
36
+ })
37
+
38
+ test('no interval, or flat ground, gives no lines', () => {
39
+ expect(terrainContours(ramp(), 0, LOT)).toEqual([])
40
+ const flat = createTerrainField({ origin: [-10, -10], spacing: 1, cols: 21, rows: 21 })
41
+ expect(terrainContours(flat, 0.3048, LOT)).toEqual([])
42
+ })
43
+ })
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Site-plan contributors — how plugin packages put their own kinds on the
3
+ * site plan without the editor depending on them.
4
+ *
5
+ * The site-plan layer draws only what `buildSitePlanDrawing` returns (it
6
+ * replaces the registry layer, so `def.floorplan` is not consulted in site
7
+ * view). A plugin whose kinds live in SITE metres (utilities: poles, runs,
8
+ * service points) registers a contributor at bootstrap; its primitives are
9
+ * appended to every site-plan drawing, on screen and on sheets alike. A
10
+ * plugin that locates the house's utility services also passes `services`,
11
+ * and the site plan runs them schematically to the street.
12
+ */
13
+ import type { AnyNodeId, FloorplanGeometry, SceneSnapshot } from '@pascal-app/core'
14
+
15
+ export type SitePlanContributor = (
16
+ scene: SceneSnapshot,
17
+ ) => { primitives: FloorplanGeometry[] } | null
18
+
19
+ export type SitePlanServiceRole = 'electric' | 'water' | 'sewer'
20
+
21
+ /**
22
+ * Where one service leaves the house, in the storey's plan frame. A point
23
+ * hosted by a wall with no position of its own sits `wallT` along `wallId`.
24
+ */
25
+ export type SitePlanServicePoint = {
26
+ role: SitePlanServiceRole | 'ac' | 'pole'
27
+ position: readonly [number, number, number]
28
+ wallId?: AnyNodeId
29
+ wallT?: number
30
+ }
31
+
32
+ export type SitePlanServices = {
33
+ /** The storey's service points; the first point of each role is drawn. */
34
+ points?: (scene: SceneSnapshot, levelId: AnyNodeId) => readonly SitePlanServicePoint[]
35
+ /** The storey's electric service entrance, when the plugin decides it. */
36
+ entrance?: (scene: SceneSnapshot, levelId: AnyNodeId) => 'overhead' | 'underground' | null
37
+ /** Services the plugin already draws as its own run, so the site plan does not route them. */
38
+ drawnRuns?: (scene: SceneSnapshot) => readonly SitePlanServiceRole[]
39
+ }
40
+
41
+ type Registration = { draw: SitePlanContributor | null; services?: SitePlanServices }
42
+
43
+ const contributors = new Map<string, Registration>()
44
+ const listeners = new Set<() => void>()
45
+
46
+ export function registerSitePlanContributor(
47
+ key: string,
48
+ contributor: SitePlanContributor | null,
49
+ services?: SitePlanServices,
50
+ ): void {
51
+ contributors.set(key, { draw: contributor, services })
52
+ for (const listener of listeners) listener()
53
+ }
54
+
55
+ /** Whether any plugin has put its kinds on the site plan. */
56
+ export function hasSitePlanContributors(): boolean {
57
+ return contributors.size > 0
58
+ }
59
+
60
+ export function subscribeSitePlanContributors(listener: () => void): () => void {
61
+ listeners.add(listener)
62
+ return () => {
63
+ listeners.delete(listener)
64
+ }
65
+ }
66
+
67
+ export function sitePlanContributions(scene: SceneSnapshot): FloorplanGeometry[] {
68
+ const out: FloorplanGeometry[] = []
69
+ for (const [key, { draw }] of contributors) {
70
+ if (!draw) continue
71
+ try {
72
+ const result = draw(scene)
73
+ if (result) out.push(...result.primitives)
74
+ } catch (error) {
75
+ if (typeof console !== 'undefined')
76
+ console.warn(`[site-plan] contributor ${key} failed`, error)
77
+ }
78
+ }
79
+ return out
80
+ }
81
+
82
+ /** Every registered `services` hook's answer, in registration order; a throwing hook is skipped. */
83
+ export function sitePlanServiceAnswers<T>(
84
+ ask: (services: SitePlanServices) => T | null | undefined,
85
+ ): T[] {
86
+ const out: T[] = []
87
+ for (const [key, { services }] of contributors) {
88
+ if (!services) continue
89
+ try {
90
+ const answer = ask(services)
91
+ if (answer !== null && answer !== undefined) out.push(answer)
92
+ } catch (error) {
93
+ if (typeof console !== 'undefined') console.warn(`[site-plan] services ${key} failed`, error)
94
+ }
95
+ }
96
+ return out
97
+ }
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Lot coverage and impervious surface — the ONE computation the site plan
3
+ * (A1.0) and the cover's PROJECT DATA both print, so the two sheets can never
4
+ * disagree again (QA 2026-09-23: the cover said 24.6 %, the site plan 45.6 %
5
+ * — the site plan had counted the house's own floor platform and garage pad
6
+ * as "porches" on top of the footprint).
7
+ *
8
+ * The two figures a Florida plans examiner asks for (Alachua County / the
9
+ * City of Gainesville ask for both on a new single-family site plan; the
10
+ * zoning district caps the first, the stormwater review reads the second):
11
+ *
12
+ * BUILDING COVERAGE the lot area under roofed structure: the building's
13
+ * footprint to the OUTSIDE FACE of its exterior walls —
14
+ * every storey at or above grade projected, the attached
15
+ * garage included — plus the roofed porches, patios and
16
+ * decks (a slab at least half under a roof outline).
17
+ * Eaves and the roof overhang are not counted.
18
+ * IMPERVIOUS SURFACE building coverage plus the paving ON THE LOT (an apron
19
+ * drawn out into the right-of-way is not counted): the driveway, the
20
+ * walks, and the uncovered patios, landings and decks
21
+ * (a slatted deck over pervious ground is counted —
22
+ * the conservative reading; some jurisdictions exclude
23
+ * it, verify).
24
+ *
25
+ * Both are divided by the LOT AREA: the county parcel record when the lot
26
+ * drop-in resolved one (`site.parcel.lotAreaSqFt`), else the drawn lot
27
+ * polygon. Pure: reads a scene snapshot, touches no store.
28
+ */
29
+ import type { SceneSnapshot } from '@pascal-app/core'
30
+ import { METRES_PER_FOOT, type Pt, pointInPolygon, polygonArea, polygonBounds } from './geometry'
31
+ import {
32
+ aboveGradeLevels,
33
+ findBuilding,
34
+ findLowestLevel,
35
+ findSite,
36
+ footprintOutline,
37
+ levelFootprintLoops,
38
+ type OutdoorPart,
39
+ outdoorParts,
40
+ roofOutlineRings,
41
+ } from './site-parts'
42
+
43
+ const SQFT_PER_SQM = 1 / (METRES_PER_FOOT * METRES_PER_FOOT)
44
+
45
+ export type ImperviousRowKey = 'building' | 'porches' | 'driveway' | 'walks' | 'patios'
46
+
47
+ export interface ImperviousRow {
48
+ key: ImperviousRowKey
49
+ label: string
50
+ sqFt: number
51
+ /** What the row holds, e.g. "2 covered: porch, rear deck". */
52
+ note: string
53
+ }
54
+
55
+ export interface SiteCoverage {
56
+ /** Lot area, SF, and where it came from; null without a lot. */
57
+ lotSqFt: number | null
58
+ lotSource: 'parcel' | 'polygon' | null
59
+ /** The building's footprint to the outside face of its walls (garage included), SF. */
60
+ buildingSqFt: number
61
+ /** Roofed porches, patios and decks, SF. */
62
+ coveredOutdoorSqFt: number
63
+ /** Building coverage = footprint + roofed porches, SF. */
64
+ buildingCoverageSqFt: number
65
+ /** Building coverage ÷ lot, 0–1; null without a lot or a building. */
66
+ buildingCoverageRatio: number | null
67
+ drivewaySqFt: number
68
+ walksSqFt: number
69
+ /** Uncovered patios, landings and decks, SF. */
70
+ openOutdoorSqFt: number
71
+ /** Everything impervious, SF. */
72
+ imperviousSqFt: number
73
+ imperviousRatio: number | null
74
+ /** The IMPERVIOUS AREA table's rows, in print order (total and % are the caller's). */
75
+ rows: ImperviousRow[]
76
+ /** One line each: what the two figures are measured to — printed with them. */
77
+ basis: { building: string; impervious: string; lot: string }
78
+ /** The parts, for the drawing: the walls' outline and every outdoor slab (site metres). */
79
+ outline: Pt[][]
80
+ parts: OutdoorPart[]
81
+ }
82
+
83
+ const sf = (m2: number) => m2 * SQFT_PER_SQM
84
+
85
+ /**
86
+ * The part of a slab's area on the lot: a driveway apron drawn out into the
87
+ * right-of-way is the road authority's paving, not the lot's. Whole when
88
+ * every corner is on the lot, else sampled on a fine grid.
89
+ */
90
+ function onLotArea(ring: readonly Pt[], lot: readonly Pt[]): number {
91
+ const area = polygonArea(ring)
92
+ if (lot.length < 3) return area
93
+ const c = ring.reduce<[number, number]>(
94
+ (s, p) => [s[0] + p[0] / ring.length, s[1] + p[1] / ring.length],
95
+ [0, 0],
96
+ )
97
+ // a corner ON the lot line (the generator's driveway mouth) counts as on the lot
98
+ const onLot = (p: Pt) =>
99
+ pointInPolygon(lot, p[0] + (c[0] - p[0]) * 1e-3, p[1] + (c[1] - p[1]) * 1e-3)
100
+ if (ring.every(onLot)) return area
101
+ const b = polygonBounds(ring)
102
+ const N = 24
103
+ let inside = 0
104
+ let kept = 0
105
+ for (let i = 0; i < N; i++) {
106
+ for (let j = 0; j < N; j++) {
107
+ const x = b.minX + ((i + 0.5) / N) * (b.maxX - b.minX)
108
+ const y = b.minY + ((j + 0.5) / N) * (b.maxY - b.minY)
109
+ if (!pointInPolygon(ring, x, y)) continue
110
+ inside++
111
+ if (pointInPolygon(lot, x, y)) kept++
112
+ }
113
+ }
114
+ return inside > 0 ? (area * kept) / inside : area
115
+ }
116
+
117
+ /** "12.3 %" — the one way both sheets print a ratio. */
118
+ export function formatCoveragePercent(ratio: number | null): string {
119
+ return ratio === null ? '—' : `${(ratio * 100).toFixed(1)} %`
120
+ }
121
+
122
+ /** "2,739 SF" — the one way both sheets print an area. */
123
+ export function formatSqFt(sqFt: number): string {
124
+ return `${Math.round(sqFt).toLocaleString('en-US')} SF`
125
+ }
126
+
127
+ export function computeSiteCoverage(scene: Pick<SceneSnapshot, 'nodes'>): SiteCoverage {
128
+ const snapshot = scene as SceneSnapshot
129
+ const site = findSite(snapshot)
130
+ const building = findBuilding(snapshot, site)
131
+ const ground = findLowestLevel(snapshot, building)
132
+ const storeys = aboveGradeLevels(snapshot, building)
133
+ const levels = storeys.length > 0 ? storeys : ground ? [ground] : []
134
+
135
+ // the footprint: every storey's wall bands, unioned, outer rings only
136
+ const loops = levels.flatMap((level) => levelFootprintLoops(snapshot, level, building))
137
+ const outline = loops.length > 0 ? footprintOutline(loops) : []
138
+ const buildingM2 = outline.reduce((s, ring) => s + polygonArea(ring), 0)
139
+
140
+ const roofs = levels.flatMap((level) => roofOutlineRings(snapshot, level, building))
141
+ const parts = outdoorParts(snapshot, ground, building, outline, roofs)
142
+ const lotRing = (site?.polygon?.points ?? []) as Pt[]
143
+ const sum = (list: readonly OutdoorPart[]) =>
144
+ list.reduce((s, p) => s + onLotArea(p.ring, lotRing), 0)
145
+ const porches = parts.filter((p) => p.kind !== 'driveway' && p.kind !== 'walk')
146
+ const covered = porches.filter((p) => p.covered)
147
+ const open = porches.filter((p) => !p.covered)
148
+ const driveway = parts.filter((p) => p.kind === 'driveway')
149
+ const walks = parts.filter((p) => p.kind === 'walk')
150
+
151
+ const lotPoly = lotRing
152
+ const parcelSqFt =
153
+ typeof site?.parcel?.lotAreaSqFt === 'number' && site.parcel.lotAreaSqFt > 0
154
+ ? site.parcel.lotAreaSqFt
155
+ : null
156
+ const polygonSqFt = lotPoly.length >= 3 ? sf(polygonArea(lotPoly)) : 0
157
+ const lotSqFt = parcelSqFt ?? (polygonSqFt > 0 ? polygonSqFt : null)
158
+ const lotSource = parcelSqFt !== null ? 'parcel' : polygonSqFt > 0 ? 'polygon' : null
159
+
160
+ const buildingSqFt = sf(buildingM2)
161
+ const coveredOutdoorSqFt = sf(sum(covered))
162
+ const buildingCoverageSqFt = buildingSqFt + coveredOutdoorSqFt
163
+ const drivewaySqFt = sf(sum(driveway))
164
+ const walksSqFt = sf(sum(walks))
165
+ const openOutdoorSqFt = sf(sum(open))
166
+ const imperviousSqFt = buildingCoverageSqFt + drivewaySqFt + walksSqFt + openOutdoorSqFt
167
+ const ratio = (x: number) =>
168
+ lotSqFt !== null && lotSqFt > 0 && buildingSqFt > 0 ? x / lotSqFt : null
169
+
170
+ const names = (list: readonly OutdoorPart[]) => list.map((p) => p.label.toLowerCase()).join(', ')
171
+ const rows: ImperviousRow[] = [
172
+ {
173
+ key: 'building',
174
+ label: 'BUILDING (INCL. GARAGE)',
175
+ sqFt: buildingSqFt,
176
+ note: 'outside face of the exterior walls',
177
+ },
178
+ {
179
+ key: 'porches',
180
+ label: 'COVERED PORCHES / PATIOS',
181
+ sqFt: coveredOutdoorSqFt,
182
+ note: covered.length > 0 ? names(covered) : 'none',
183
+ },
184
+ {
185
+ key: 'driveway',
186
+ label: 'DRIVEWAY',
187
+ sqFt: drivewaySqFt,
188
+ note: driveway.length > 0 ? 'concrete' : 'none drawn',
189
+ },
190
+ {
191
+ key: 'walks',
192
+ label: 'WALKS',
193
+ sqFt: walksSqFt,
194
+ note: walks.length > 0 ? 'concrete' : 'none drawn',
195
+ },
196
+ {
197
+ key: 'patios',
198
+ label: 'OPEN PATIOS / DECKS / LANDINGS',
199
+ sqFt: openOutdoorSqFt,
200
+ note: open.length > 0 ? names(open) : 'none',
201
+ },
202
+ ]
203
+
204
+ return {
205
+ lotSqFt,
206
+ lotSource,
207
+ buildingSqFt,
208
+ coveredOutdoorSqFt,
209
+ buildingCoverageSqFt,
210
+ buildingCoverageRatio: ratio(buildingCoverageSqFt),
211
+ drivewaySqFt,
212
+ walksSqFt,
213
+ openOutdoorSqFt,
214
+ imperviousSqFt,
215
+ imperviousRatio: ratio(imperviousSqFt),
216
+ rows,
217
+ basis: {
218
+ building:
219
+ 'Building coverage is the roofed area: the footprint to the outside face of the exterior walls (garage included, every storey projected) plus the roofed porches and patios; eaves excluded.',
220
+ impervious:
221
+ 'Impervious surface is the building coverage plus the driveway, walks and the open patios, landings and decks (decks counted — verify the local rule).',
222
+ lot:
223
+ lotSource === 'parcel'
224
+ ? 'Lot area is the county parcel record.'
225
+ : lotSource === 'polygon'
226
+ ? 'Lot area is the drawn lot polygon (not a survey).'
227
+ : 'No lot area.',
228
+ },
229
+ outline,
230
+ parts,
231
+ }
232
+ }
@@ -0,0 +1,15 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { isSitePlanAvailable } from './drawing-type-switch'
3
+
4
+ describe('isSitePlanAvailable', () => {
5
+ test('a site with neither a parcel nor setbacks has no site plan', () => {
6
+ expect(isSitePlanAvailable(null, false)).toBe(false)
7
+ expect(isSitePlanAvailable({}, false)).toBe(false)
8
+ })
9
+
10
+ test('a resolved lot or a contributing plugin offers it', () => {
11
+ expect(isSitePlanAvailable({ parcel: { apn: '06075' } }, false)).toBe(true)
12
+ expect(isSitePlanAvailable({ setbacks: { front: 6, side: 1.5, rear: 3 } }, false)).toBe(true)
13
+ expect(isSitePlanAvailable({}, true)).toBe(true)
14
+ })
15
+ })
@@ -0,0 +1,86 @@
1
+ 'use client'
2
+
3
+ import { type SiteNode, useScene } from '@pascal-app/core'
4
+ import { useEffect, useSyncExternalStore } from 'react'
5
+ import useDrawingView, { EDITOR_DRAWING_TYPE_OPTIONS } from '../../../store/use-drawing-view'
6
+ import { cn } from '../../utils'
7
+ import { hasSitePlanContributors, subscribeSitePlanContributors } from './contributors'
8
+
9
+ /**
10
+ * The site plan only has something to draw for a resolved lot (a parcel or
11
+ * setbacks on the site) or when a plugin contributes site-plan content; every
12
+ * other scene would get the default 30 m square.
13
+ */
14
+ export function isSitePlanAvailable(
15
+ site: Pick<SiteNode, 'parcel' | 'setbacks'> | null,
16
+ pluginContributes: boolean,
17
+ ): boolean {
18
+ return pluginContributes || Boolean(site?.parcel || site?.setbacks)
19
+ }
20
+
21
+ export function useSitePlanAvailable(): boolean {
22
+ const pluginContributes = useSyncExternalStore(
23
+ subscribeSitePlanContributors,
24
+ hasSitePlanContributors,
25
+ hasSitePlanContributors,
26
+ )
27
+ const siteHasLot = useScene((state) => {
28
+ for (const id of state.rootNodeIds) {
29
+ const node = state.nodes[id]
30
+ if (node?.type === 'site') return isSitePlanAvailable(node as SiteNode, false)
31
+ }
32
+ return false
33
+ })
34
+ return pluginContributes || siteHasLot
35
+ }
36
+
37
+ /**
38
+ * Floor plan ⇄ Site plan switch for the 2D editor.
39
+ *
40
+ * Sits with the other floating plan controls in the floor-plan viewport
41
+ * (bottom-left, beside the compass). Theme tokens only. Hidden, and a
42
+ * persisted site-plan choice reset to the floor plan, when the scene has no
43
+ * site plan to show.
44
+ */
45
+ export function FloorplanDrawingTypeSwitch({ className }: { className?: string }) {
46
+ const drawingType = useDrawingView((s) => s.drawingType)
47
+ const setDrawingType = useDrawingView((s) => s.setDrawingType)
48
+ const available = useSitePlanAvailable()
49
+
50
+ useEffect(() => {
51
+ if (!available && drawingType === 'site-plan') setDrawingType('floor-plan')
52
+ }, [available, drawingType, setDrawingType])
53
+
54
+ if (!available) return null
55
+
56
+ return (
57
+ <div
58
+ className={cn(
59
+ 'pointer-events-auto absolute bottom-3 left-14 z-30 flex items-center gap-0.5 rounded-full border border-border bg-card/90 p-0.5 shadow-sm backdrop-blur-md',
60
+ className,
61
+ )}
62
+ >
63
+ {EDITOR_DRAWING_TYPE_OPTIONS.map((option) => (
64
+ <button
65
+ aria-pressed={drawingType === option.id}
66
+ className={cn(
67
+ 'rounded-full px-2.5 py-1 font-medium text-xs transition',
68
+ drawingType === option.id
69
+ ? 'bg-primary text-primary-foreground'
70
+ : 'text-muted-foreground hover:text-foreground',
71
+ )}
72
+ key={option.id}
73
+ onClick={(event) => {
74
+ event.preventDefault()
75
+ event.stopPropagation()
76
+ setDrawingType(option.id)
77
+ }}
78
+ onPointerDown={(event) => event.stopPropagation()}
79
+ type="button"
80
+ >
81
+ {option.label}
82
+ </button>
83
+ ))}
84
+ </div>
85
+ )
86
+ }
@@ -0,0 +1,103 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { detectFrontEdgeFromRoads, streetCore } from './front-edge'
3
+ import type { Pt } from './geometry'
4
+
5
+ // 20 m × 30 m lot centred on the origin, y south. Edge 0 = north side,
6
+ // 1 = east, 2 = south, 3 = west.
7
+ const LOT: Pt[] = [
8
+ [-10, -15],
9
+ [10, -15],
10
+ [10, 15],
11
+ [-10, 15],
12
+ ]
13
+ const road = (name: string, ...pts: Pt[]) => ({ name, centerline: pts })
14
+
15
+ describe('streetCore', () => {
16
+ test('drops the number, the direction and the suffix', () => {
17
+ expect(streetCore('2600 Castro Way')).toBe('castro')
18
+ expect(streetCore('S Castro')).toBe('castro')
19
+ expect(streetCore('W Cass St.')).toBe('cass')
20
+ expect(streetCore('1200 W Cass St')).toBe('cass')
21
+ expect(streetCore('North Troy Street')).toBe('troy')
22
+ // OSM spells the quadrant out
23
+ expect(streetCore('Northeast 109th Street')).toBe('109th')
24
+ expect(streetCore('1247 NE 109th St')).toBe('109th')
25
+ expect(streetCore('')).toBe('')
26
+ expect(streetCore(null)).toBe('')
27
+ })
28
+ })
29
+
30
+ describe('detectFrontEdgeFromRoads', () => {
31
+ test('the nearest parallel street on the outside picks its edge', () => {
32
+ const roads = [road('Castro Way', [-60, -25], [60, -25])]
33
+ const m = detectFrontEdgeFromRoads(LOT, roads)
34
+ expect(m?.index).toBe(0)
35
+ expect(m?.name).toBe('Castro Way')
36
+ expect(m?.distance).toBeCloseTo(10, 9)
37
+ expect(m?.named).toBe(false)
38
+ })
39
+
40
+ test('a road perpendicular to an edge never fronts it', () => {
41
+ // runs north-south past the middle of the north edge, 25 m out
42
+ const roads = [road('Stab St', [0, -60], [0, -25])]
43
+ expect(detectFrontEdgeFromRoads(LOT, roads)).toBeNull()
44
+ })
45
+
46
+ test('a road through the lot (inside the edge) is rejected', () => {
47
+ const roads = [road('Driveway', [-60, -5], [60, -5])]
48
+ expect(detectFrontEdgeFromRoads(LOT, roads)).toBeNull()
49
+ })
50
+
51
+ test('corner lot: the addressed street wins over a closer cross street', () => {
52
+ const roads = [
53
+ road('Castro Way', [-60, -30], [60, -30]), // north, 15 m off
54
+ road('Elm St', [18, -60], [18, 60]), // east, 8 m off — closer
55
+ ]
56
+ expect(detectFrontEdgeFromRoads(LOT, roads)?.index).toBe(1) // closest wins with no address
57
+ const named = detectFrontEdgeFromRoads(LOT, roads, '2600 Castro Way')
58
+ expect(named?.index).toBe(0)
59
+ expect(named?.named).toBe(true)
60
+ // an address on a street that is not mapped falls back to the closest
61
+ expect(detectFrontEdgeFromRoads(LOT, roads, '9 Nowhere Ln')?.index).toBe(1)
62
+ })
63
+
64
+ test('through lot: a street of the same number but another type is not the addressed street', () => {
65
+ const roads = [
66
+ road('Northwest 34th Terrace', [-18, -60], [-18, 60]), // west, 8 m off — closer
67
+ road('Northwest 34th Street', [25, -60], [25, 60]), // east, 15 m off
68
+ ]
69
+ const m = detectFrontEdgeFromRoads(LOT, roads, '4121 NW 34th St')
70
+ expect(m?.index).toBe(1)
71
+ expect(m?.name).toBe('Northwest 34th Street')
72
+ expect(m?.named).toBe(true)
73
+ expect(m?.streetNames).toEqual({ '1': 'Northwest 34th Street', '3': 'Northwest 34th Terrace' })
74
+ // addressed on the Terrace, the Terrace fronts it
75
+ expect(detectFrontEdgeFromRoads(LOT, roads, '4120 NW 34th Ter')?.index).toBe(3)
76
+ })
77
+
78
+ test('a bent road counts by its nearest segment; slight skew within 30° passes', () => {
79
+ const roads = [road('Bend Rd', [-80, -40], [-30, -26], [30, -24], [80, -40])]
80
+ expect(detectFrontEdgeFromRoads(LOT, roads)?.index).toBe(0)
81
+ })
82
+
83
+ test('winding does not matter', () => {
84
+ const cw = [...LOT].reverse() as Pt[]
85
+ const roads = [road('Castro Way', [-60, -25], [60, -25])]
86
+ const m = detectFrontEdgeFromRoads(cw, roads)
87
+ // reversed ring: the north edge is now points[2] → points[3]
88
+ expect(m?.index).toBe(2)
89
+ })
90
+
91
+ test('no roads, no answer', () => {
92
+ expect(detectFrontEdgeFromRoads(LOT, [])).toBeNull()
93
+ expect(
94
+ detectFrontEdgeFromRoads(
95
+ [
96
+ [0, 0],
97
+ [1, 0],
98
+ ],
99
+ [road('x', [0, -5], [1, -5])],
100
+ ),
101
+ ).toBeNull()
102
+ })
103
+ })