@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,779 @@
1
+ import {
2
+ type AnyNode,
3
+ type AnyNodeId,
4
+ type BuildingNode,
5
+ envelopeFrontEdge,
6
+ type FloorplanGeometry,
7
+ type LevelNode,
8
+ type SceneSnapshot,
9
+ type SiteNode,
10
+ sightTriangle,
11
+ streetCorners,
12
+ terrainFieldOf,
13
+ } from '@pascal-app/core'
14
+ import { sitePlanContributions } from './contributors'
15
+ import {
16
+ computeSiteCoverage,
17
+ formatCoveragePercent,
18
+ formatSqFt,
19
+ type SiteCoverage,
20
+ } from './coverage'
21
+ import {
22
+ type Bounds,
23
+ boundsInsidePolygon,
24
+ castYardDimensionsOriented,
25
+ classifyEdges,
26
+ compassLabel,
27
+ edgeHeadingDeg,
28
+ edgeLength,
29
+ formatFeetInches,
30
+ METRES_PER_FOOT,
31
+ outwardNormal,
32
+ type Pt,
33
+ polygonBounds,
34
+ resolveFrontEdge,
35
+ setbackEnvelope,
36
+ setbackForRole,
37
+ type YardDimension,
38
+ } from './geometry'
39
+ import {
40
+ contourPrimitives,
41
+ drainagePrimitives,
42
+ flatworkPrimitives,
43
+ floorTops,
44
+ formatElevation,
45
+ servicePrimitives,
46
+ spotElevationPrimitives,
47
+ streetEdgeNames,
48
+ } from './site-annotations'
49
+ import {
50
+ findBuilding,
51
+ findLowestLevel,
52
+ findSite,
53
+ footprintOutline,
54
+ levelFootprintLoops,
55
+ roofOutlineRings,
56
+ siteFrame,
57
+ yawOf,
58
+ } from './site-parts'
59
+
60
+ export { footprintOutline, levelFootprintLoops, roofOutlineRings }
61
+
62
+ /** Contract shared by every drawing producer (see docs/construction-documents.md). */
63
+ export interface SitePlanDrawing {
64
+ primitives: FloorplanGeometry[]
65
+ bounds: Bounds
66
+ /** What the drawing could not take from the model — printed on the sheet, never silent. */
67
+ warnings?: string[]
68
+ /** Live values the panel / sheets reuse without re-deriving them. */
69
+ meta: {
70
+ site: SiteNode | null
71
+ frontEdge: number
72
+ lot: Pt[]
73
+ envelope: Pt[]
74
+ /** The envelope edge behind the lot's front line (the envelope has its own vertex count now). */
75
+ envelopeFrontEdge: number
76
+ /** The corner sight triangles (corner, leg end A, leg end B), site metres. */
77
+ sightTriangles: Pt[][]
78
+ /** Per-wall footprint bands of the lowest level, in SITE metres. */
79
+ footprintLoops: Pt[][]
80
+ footprintBounds: Bounds | null
81
+ yards: YardDimension[]
82
+ buildingId: AnyNodeId | null
83
+ /** Building coverage and impervious area — the figures the lot label and the sheets print. */
84
+ coverage?: SiteCoverage
85
+ }
86
+ }
87
+
88
+ const LOT_STROKE_WIDTH = 0.12
89
+ const ENVELOPE_STROKE_WIDTH = 0.05
90
+ const LABEL_SIZE = 0.9
91
+
92
+ // Plan ink. The floor-plan surface is the light plan sheet the panel paints
93
+ // behind the scene group, where the app's `currentColor` foreground is
94
+ // near-white and therefore invisible. Kinds hard-code their plan ink for the
95
+ // same reason; these match `nodes/src/wall/floorplan.ts`.
96
+ const INK = '#111827'
97
+ const INK_SOFT = '#4b5563'
98
+ /** Contour lines: the survey's brown. */
99
+ const CONTOUR_INK = '#8b5a2b'
100
+ /** The house on the lot: a light body under a heavy edge, so its label and the roof line read. */
101
+ const FOOTPRINT_FILL = '#d1d5db'
102
+ const FOOTPRINT_STROKE = '#111827'
103
+ const DIMENSION_STROKE = '#334155'
104
+
105
+ export interface SitePlanEdge {
106
+ index: number
107
+ headingDeg: number
108
+ compass: string
109
+ lengthM: number
110
+ label: string
111
+ }
112
+
113
+ /** Lot edges with their compass heading — feeds the panel's front-edge picker. */
114
+ export function describeSiteEdges(site: SiteNode | null | undefined): SitePlanEdge[] {
115
+ const lot = (site?.polygon?.points ?? []) as Pt[]
116
+ if (lot.length < 3) return []
117
+ const north = site?.northRotation ?? 0
118
+ return lot.map((_, i) => {
119
+ const headingDeg = edgeHeadingDeg(lot, i, north)
120
+ const lengthM = edgeLength(lot, i)
121
+ const compass = compassLabel(headingDeg)
122
+ return {
123
+ index: i,
124
+ headingDeg,
125
+ compass,
126
+ lengthM,
127
+ label: `Edge ${i + 1} — faces ${compass} (${Math.round(headingDeg)}°), ${formatFeetInches(lengthM)}`,
128
+ }
129
+ })
130
+ }
131
+
132
+ function unionBounds(loops: readonly Pt[][]): Bounds | null {
133
+ const all: Pt[] = []
134
+ for (const loop of loops) all.push(...loop)
135
+ if (all.length === 0) return null
136
+ return polygonBounds(all)
137
+ }
138
+
139
+ function padBounds(b: Bounds, pad: number): Bounds {
140
+ return { minX: b.minX - pad, minY: b.minY - pad, maxX: b.maxX + pad, maxY: b.maxY + pad }
141
+ }
142
+
143
+ function northArrow(at: Pt, northRotation: number, size: number): FloorplanGeometry {
144
+ const [cx, cy] = at
145
+ // Local frame: up is −y, rotated clockwise by `northRotation`.
146
+ const cos = Math.cos(northRotation)
147
+ const sin = Math.sin(northRotation)
148
+ const rot = (x: number, y: number): Pt => [cx + x * cos - y * sin, cy + x * sin + y * cos]
149
+ const tip = rot(0, -size)
150
+ const tail = rot(0, size * 0.55)
151
+ const left = rot(-size * 0.28, -size * 0.35)
152
+ const right = rot(size * 0.28, -size * 0.35)
153
+ const labelAt = rot(0, size * 0.95)
154
+ return {
155
+ kind: 'group',
156
+ children: [
157
+ {
158
+ kind: 'line',
159
+ x1: tail[0],
160
+ y1: tail[1],
161
+ x2: tip[0],
162
+ y2: tip[1],
163
+ stroke: INK,
164
+ strokeWidth: 1.4,
165
+ vectorEffect: 'non-scaling-stroke',
166
+ },
167
+ {
168
+ kind: 'polygon',
169
+ points: [tip, left, right],
170
+ fill: INK,
171
+ stroke: INK,
172
+ strokeWidth: 1,
173
+ vectorEffect: 'non-scaling-stroke',
174
+ },
175
+ {
176
+ kind: 'text',
177
+ x: labelAt[0],
178
+ y: labelAt[1],
179
+ text: 'N',
180
+ fontSize: size * 0.55,
181
+ fill: INK,
182
+ fontWeight: 600,
183
+ textAnchor: 'middle',
184
+ dominantBaseline: 'hanging',
185
+ upright: true,
186
+ },
187
+ ],
188
+ }
189
+ }
190
+
191
+ /** A survey bearing for the run (dx, dy): quadrant, degrees and minutes — "N 81°42' E". */
192
+ function bearingOf(dx: number, dy: number, northRotation: number): string {
193
+ // plan up (−y) is north and +x is east; `northRotation` turns true north
194
+ // clockwise from plan up (the north arrow's own convention)
195
+ let az = ((Math.atan2(dx, -dy) - northRotation) * 180) / Math.PI
196
+ az = ((az % 360) + 360) % 360
197
+ const ns = az <= 90 || az >= 270 ? 'N' : 'S'
198
+ const ew = az <= 180 ? 'E' : 'W'
199
+ const off = az <= 90 ? az : az <= 180 ? 180 - az : az <= 270 ? az - 180 : 360 - az
200
+ let deg = Math.floor(off)
201
+ let min = Math.round((off - deg) * 60)
202
+ if (min === 60) {
203
+ deg += 1
204
+ min = 0
205
+ }
206
+ return `${ns} ${deg}°${String(min).padStart(2, '0')}' ${ew}`
207
+ }
208
+
209
+ /** An angle a label reads at: within a quarter turn of the paper's horizontal. */
210
+ function readableAngle(a: number): number {
211
+ let r = a
212
+ while (r > Math.PI / 2) r -= Math.PI
213
+ while (r <= -Math.PI / 2) r += Math.PI
214
+ return r
215
+ }
216
+
217
+ /** A label laid ALONG a run — a group turned to the run's readable angle, the text centred on `at`. */
218
+ function alongLabel(
219
+ at: Pt,
220
+ angle: number,
221
+ text: string,
222
+ fontSize: number,
223
+ style: { fill: string; fontWeight?: number },
224
+ metadata: Record<string, unknown>,
225
+ ): FloorplanGeometry {
226
+ return {
227
+ kind: 'group',
228
+ transform: { translate: at, rotate: readableAngle(angle) },
229
+ children: [
230
+ {
231
+ kind: 'text',
232
+ x: 0,
233
+ y: 0,
234
+ text,
235
+ fontSize,
236
+ fill: style.fill,
237
+ fontWeight: style.fontWeight ?? 500,
238
+ textAnchor: 'middle',
239
+ dominantBaseline: 'middle',
240
+ },
241
+ ],
242
+ metadata,
243
+ }
244
+ }
245
+
246
+ /** The exterior steps as tread lines (three, a tread apart), site metres. */
247
+ function stairTreads(
248
+ scene: SceneSnapshot,
249
+ level: LevelNode | null,
250
+ building: BuildingNode | null,
251
+ ): FloorplanGeometry[] {
252
+ if (!level) return []
253
+ const toSite = siteFrame(building)
254
+ const out: FloorplanGeometry[] = []
255
+ for (const childId of level.children) {
256
+ const child = scene.nodes[childId as AnyNodeId] as AnyNode | undefined
257
+ if (child?.type !== 'stair') continue
258
+ const stair = child as unknown as { position?: number[]; rotation?: unknown; width?: number }
259
+ const x = stair.position?.[0] ?? 0
260
+ const z = stair.position?.[2] ?? 0
261
+ const r = yawOf(stair.rotation)
262
+ const half = (stair.width ?? 1) / 2
263
+ // the run climbs toward the floor it serves: +local z turned by the yaw
264
+ const dir: Pt = [Math.sin(r), Math.cos(r)]
265
+ const across: Pt = [Math.cos(r), -Math.sin(r)]
266
+ for (let k = 0; k < 3; k++) {
267
+ const cx = x + dir[0] * k * 0.28
268
+ const cz = z + dir[1] * k * 0.28
269
+ const a = toSite(cx - across[0] * half, cz - across[1] * half)
270
+ const b = toSite(cx + across[0] * half, cz + across[1] * half)
271
+ out.push({
272
+ kind: 'line',
273
+ x1: a[0],
274
+ y1: a[1],
275
+ x2: b[0],
276
+ y2: b[1],
277
+ stroke: INK,
278
+ strokeWidth: 0.02,
279
+ metadata: { sitePlan: 'steps' },
280
+ })
281
+ }
282
+ }
283
+ return out
284
+ }
285
+
286
+ /**
287
+ * The setbacks line the site plan prints when the yards are not the zoning
288
+ * code's own: worded for a plans examiner from the site's numbers — never the
289
+ * internal source tag a lot drop-in stored (QA 2026-09-23 printed the
290
+ * internal defaults tag on A1.0). A cited code prints nothing.
291
+ */
292
+ export function setbacksWarning(
293
+ site: Pick<SiteNode, 'setbacks' | 'setbacksSource'> | null,
294
+ ): string | null {
295
+ const source = site?.setbacksSource ?? ''
296
+ const s = site?.setbacks
297
+ if (!s || !/draft|verify|default|by hand/i.test(source)) return null
298
+ const ft = (m: number) => `${Math.round((m / METRES_PER_FOOT) * 2) / 2} ft`
299
+ const yards = `front ${ft(s.front)}, side ${ft(s.side)}, rear ${ft(s.rear)}`
300
+ if (/by hand/i.test(source))
301
+ return `Setbacks: set by hand (${yards}) — confirm with the zoning district.`
302
+ if (/default/i.test(source))
303
+ return `Setbacks: planning defaults (${yards}) — confirm with the zoning district.`
304
+ return `Setbacks: ${source}`
305
+ }
306
+
307
+ /**
308
+ * Build the site-plan drawing from the scene.
309
+ *
310
+ * Everything is recomputed from the current snapshot, so the drawing is live:
311
+ * move the building or edit a setback and the next call reflects it.
312
+ * Returns an empty drawing (no primitives) when the scene has no site polygon.
313
+ */
314
+ export function buildSitePlanDrawing(scene: SceneSnapshot): SitePlanDrawing {
315
+ const site = findSite(scene)
316
+ const lot = (site?.polygon?.points ?? []) as Pt[]
317
+ const building = findBuilding(scene, site)
318
+ const level = findLowestLevel(scene, building)
319
+ const footprintLoops = levelFootprintLoops(scene, level, building)
320
+ const footprintBounds = unionBounds(footprintLoops)
321
+ const northRotation = site?.northRotation ?? 0
322
+ const frontEdge = resolveFrontEdge(lot, site?.frontEdge, northRotation)
323
+
324
+ const empty: SitePlanDrawing = {
325
+ primitives: [],
326
+ warnings: [],
327
+ bounds: footprintBounds ?? { minX: -15, minY: -15, maxX: 15, maxY: 15 },
328
+ meta: {
329
+ site,
330
+ frontEdge,
331
+ lot,
332
+ envelope: [],
333
+ envelopeFrontEdge: 0,
334
+ sightTriangles: [],
335
+ footprintLoops,
336
+ footprintBounds,
337
+ yards: [],
338
+ buildingId: (building?.id as AnyNodeId) ?? null,
339
+ },
340
+ }
341
+ if (lot.length < 3) return empty
342
+
343
+ const primitives: FloorplanGeometry[] = []
344
+ // the coverage figures and the outdoor parts, from the one shared reading
345
+ // (coverage.ts) — the cover's PROJECT DATA prints the same numbers
346
+ const coverage = computeSiteCoverage(scene)
347
+
348
+ // ── Lot line — the heaviest line on the sheet ────────────────────────
349
+ primitives.push({
350
+ kind: 'polygon',
351
+ points: lot,
352
+ fill: 'none',
353
+ stroke: INK,
354
+ strokeWidth: LOT_STROKE_WIDTH,
355
+ strokeLinejoin: 'miter',
356
+ metadata: { sitePlan: 'lot-line' },
357
+ })
358
+
359
+ // ── Terrain contours — the ground's lines, at the site's interval ─────
360
+ // (site.contourIntervalIn, default 12 in; 0 = none): thin brown lines at
361
+ // whole elevations above the survey datum when the terrain sample carries
362
+ // one (USGS EPQS at the lot centre, NAVD88), every fifth heavier, each
363
+ // elevation labelled once or twice (site-annotations.ts).
364
+ const field = site ? terrainFieldOf(site as never) : null
365
+ const datumFt = (() => {
366
+ const sample = (site?.metadata as { terrainSample?: { datumFt?: unknown } } | undefined)
367
+ ?.terrainSample
368
+ return typeof sample?.datumFt === 'number' ? sample.datumFt : null
369
+ })()
370
+ {
371
+ const intervalIn = typeof site?.contourIntervalIn === 'number' ? site.contourIntervalIn : 12
372
+ if (site && field && intervalIn > 0) {
373
+ const lines = contourPrimitives({
374
+ site,
375
+ lot,
376
+ field,
377
+ intervalIn,
378
+ datumFt,
379
+ fontSize: LABEL_SIZE * 0.45,
380
+ // no elevation printed on the house, a porch or the paving
381
+ avoid: [...coverage.outline, ...coverage.parts.map((p) => p.ring)],
382
+ })
383
+ if (lines.length > 0) primitives.push({ kind: 'group', children: lines })
384
+ }
385
+ }
386
+
387
+ // ── The driveway and walks, under the yards' labels and dimensions ────
388
+ primitives.push(...flatworkPrimitives(scene, building, coverage.parts, LABEL_SIZE * 0.42))
389
+
390
+ // ── Setback envelope — dashed, offset inward per edge ────────────────
391
+ const streetEdges = (site?.streetEdges ?? []).filter((i) => Number.isFinite(i))
392
+ const sightTriangleM =
393
+ typeof site?.sightTriangleFt === 'number' && site.sightTriangleFt > 0
394
+ ? site.sightTriangleFt * METRES_PER_FOOT
395
+ : 0
396
+ const envelope = site?.setbacks
397
+ ? setbackEnvelope(lot, site.setbacks, frontEdge, { streetEdges, sightTriangleM })
398
+ : []
399
+ // the corner sight triangles (clear-vision at a street intersection):
400
+ // dashed, labelled with the leg — the ordinance's figure, verify locally
401
+ const sightTriangles: Pt[][] = []
402
+ if (sightTriangleM > 0) {
403
+ for (const [a, b] of streetCorners(lot, [frontEdge, ...streetEdges])) {
404
+ const tri = sightTriangle(lot, a, b, sightTriangleM)
405
+ if (!tri) continue
406
+ const ring: Pt[] = [tri.corner, tri.a, tri.b]
407
+ sightTriangles.push(ring)
408
+ primitives.push({
409
+ kind: 'polygon',
410
+ points: ring,
411
+ fill: 'none',
412
+ stroke: INK_SOFT,
413
+ strokeWidth: ENVELOPE_STROKE_WIDTH,
414
+ strokeDasharray: '0.3 0.2',
415
+ opacity: 0.75,
416
+ metadata: { sitePlan: 'sight-triangle' },
417
+ })
418
+ const cx = (tri.corner[0] + tri.a[0] + tri.b[0]) / 3
419
+ const cz = (tri.corner[1] + tri.a[1] + tri.b[1]) / 3
420
+ primitives.push({
421
+ kind: 'text',
422
+ x: cx,
423
+ y: cz,
424
+ text: `SIGHT TRIANGLE ${site?.sightTriangleFt}' (VERIFY)`,
425
+ fontSize: 0.6,
426
+ fill: INK_SOFT,
427
+ textAnchor: 'middle',
428
+ metadata: { sitePlan: 'sight-triangle-label' },
429
+ })
430
+ }
431
+ }
432
+ if (envelope.length >= 3) {
433
+ primitives.push({
434
+ kind: 'polygon',
435
+ points: envelope,
436
+ fill: 'none',
437
+ stroke: INK_SOFT,
438
+ strokeWidth: ENVELOPE_STROKE_WIDTH,
439
+ strokeDasharray: '0.9 0.45',
440
+ opacity: 0.75,
441
+ metadata: { sitePlan: 'setback-envelope' },
442
+ })
443
+ }
444
+
445
+ // ── Building footprint — the OUTLINE of the level-0 walls ─────────────
446
+ // A site plan shows the building's edge on the lot; the partitions inside
447
+ // it are the floor plan's business, so the wall bands are unioned and only
448
+ // the outer rings are kept.
449
+ const outerRings = footprintOutline(footprintLoops)
450
+ for (const loop of outerRings) {
451
+ primitives.push({
452
+ kind: 'polygon',
453
+ points: loop,
454
+ fill: FOOTPRINT_FILL,
455
+ fillOpacity: 0.7,
456
+ stroke: FOOTPRINT_STROKE,
457
+ strokeWidth: 0.06,
458
+ metadata: { sitePlan: 'building-footprint', buildingId: building?.id ?? null },
459
+ })
460
+ }
461
+
462
+ // ── Yard dimensions — the house's own faces, square out to the lot line ──
463
+ const yards = footprintLoops.length
464
+ ? castYardDimensionsOriented(lot, footprintLoops, building?.rotation?.[1] ?? 0)
465
+ : []
466
+ for (const yard of yards) {
467
+ const dx = yard.to[0] - yard.from[0]
468
+ const dy = yard.to[1] - yard.from[1]
469
+ const len = Math.hypot(dx, dy) || 1
470
+ // Offset normal is perpendicular to the measured run; zero offset keeps
471
+ // the dimension line on the yard itself, which is how site plans read.
472
+ primitives.push({
473
+ kind: 'dimension',
474
+ start: yard.from,
475
+ end: yard.to,
476
+ offsetNormal: [-dy / len, dx / len],
477
+ offsetDistance: 0,
478
+ extensionOvershoot: 0.35,
479
+ stroke: DIMENSION_STROKE,
480
+ terminator: 'architectural-tick',
481
+ text: formatFeetInches(yard.distance),
482
+ metadata: { sitePlan: 'yard-dimension', side: yard.side },
483
+ } as FloorplanGeometry)
484
+ }
485
+
486
+ const lotBounds = polygonBounds(lot)
487
+ const warnings: string[] = []
488
+
489
+ // ── Property lines: bearing and length along each edge, outside the lot ──
490
+ for (let i = 0; i < lot.length; i++) {
491
+ const p = lot[i] as Pt
492
+ const q = lot[(i + 1) % lot.length] as Pt
493
+ const dx = q[0] - p[0]
494
+ const dy = q[1] - p[1]
495
+ const len = Math.hypot(dx, dy)
496
+ if (len < 0.5) continue
497
+ const out = outwardNormal(lot, i)
498
+ primitives.push(
499
+ alongLabel(
500
+ [(p[0] + q[0]) / 2 + out[0] * 0.55, (p[1] + q[1]) / 2 + out[1] * 0.55],
501
+ Math.atan2(dy, dx),
502
+ `${bearingOf(dx, dy, northRotation)} ${(len / METRES_PER_FOOT).toFixed(2)}'`,
503
+ LABEL_SIZE * 0.48,
504
+ { fill: INK, fontWeight: 600 },
505
+ { sitePlan: 'lot-edge-label', edge: i },
506
+ ),
507
+ )
508
+ }
509
+
510
+ // ── Setback labels, in each yard along its lot edge ───────────────────
511
+ if (site?.setbacks && envelope.length >= 3) {
512
+ const roles = classifyEdges(lot, frontEdge, streetEdges)
513
+ const NAME = {
514
+ front: 'FRONT',
515
+ rear: 'REAR',
516
+ street: 'STREET SIDE',
517
+ left: 'SIDE',
518
+ right: 'SIDE',
519
+ } as const
520
+ for (let i = 0; i < lot.length; i++) {
521
+ const role = roles[i]
522
+ if (!role) continue
523
+ const s = setbackForRole(site.setbacks, role)
524
+ if (!(s > 0)) continue
525
+ const p = lot[i] as Pt
526
+ const q = lot[(i + 1) % lot.length] as Pt
527
+ const dx = q[0] - p[0]
528
+ const dy = q[1] - p[1]
529
+ if (Math.hypot(dx, dy) < 1) continue
530
+ const out = outwardNormal(lot, i)
531
+ // a third of the way along the edge, halfway into the yard — clear of
532
+ // the yard dimension that stands square off the house
533
+ primitives.push(
534
+ alongLabel(
535
+ [p[0] + dx * 0.32 - out[0] * (s / 2), p[1] + dy * 0.32 - out[1] * (s / 2)],
536
+ Math.atan2(dy, dx),
537
+ `${formatFeetInches(s)} ${NAME[role]} SETBACK`,
538
+ Math.min(LABEL_SIZE * 0.4, s * 0.45),
539
+ { fill: INK_SOFT },
540
+ { sitePlan: 'setback-label', edge: i, role },
541
+ ),
542
+ )
543
+ }
544
+ }
545
+
546
+ // ── The streets, each named beyond the edge it runs along ─────────────
547
+ // (only the edges that front a street; the name the lot drop-in stored
548
+ // for that edge — site-annotations.ts `streetEdgeNames`)
549
+ if (site) {
550
+ for (const [i, name] of streetEdgeNames(site, frontEdge)) {
551
+ const p = lot[i]
552
+ const q = lot[(i + 1) % lot.length]
553
+ if (!p || !q) continue
554
+ const out = outwardNormal(lot, i)
555
+ primitives.push(
556
+ alongLabel(
557
+ [(p[0] + q[0]) / 2 + out[0] * 2.2, (p[1] + q[1]) / 2 + out[1] * 2.2],
558
+ Math.atan2(q[1] - p[1], q[0] - p[0]),
559
+ name,
560
+ LABEL_SIZE * (name.length > 24 ? 0.6 : 0.85),
561
+ { fill: INK, fontWeight: 700 },
562
+ { sitePlan: 'street-name', edge: i },
563
+ ),
564
+ )
565
+ }
566
+ }
567
+
568
+ // ── The roof line, dashed over the footprint ─────────────────────────
569
+ for (const ring of roofOutlineRings(scene, level, building)) {
570
+ primitives.push({
571
+ kind: 'polygon',
572
+ points: ring,
573
+ fill: 'none',
574
+ stroke: INK_SOFT,
575
+ strokeWidth: 0.035,
576
+ strokeDasharray: '0.5 0.25',
577
+ opacity: 0.9,
578
+ metadata: { sitePlan: 'roof-outline' },
579
+ })
580
+ }
581
+
582
+ // ── Porches, patios, decks, landings and their steps ─────────────────
583
+ for (const porch of coverage.parts) {
584
+ if (porch.kind === 'driveway' || porch.kind === 'walk') continue
585
+ primitives.push({
586
+ kind: 'polygon',
587
+ points: porch.ring,
588
+ fill: '#f3f4f6',
589
+ fillOpacity: 0.95,
590
+ stroke: FOOTPRINT_STROKE,
591
+ strokeWidth: 0.03,
592
+ metadata: { sitePlan: 'porch' },
593
+ })
594
+ const b = polygonBounds(porch.ring)
595
+ primitives.push({
596
+ kind: 'text',
597
+ x: (b.minX + b.maxX) / 2,
598
+ y: (b.minY + b.maxY) / 2,
599
+ text: porch.covered ? porch.label : `${porch.label} (OPEN)`,
600
+ fontSize: Math.min(
601
+ LABEL_SIZE * 0.4,
602
+ Math.max(0.2, Math.min(b.maxY - b.minY, b.maxX - b.minX) * 0.3),
603
+ ),
604
+ fill: INK,
605
+ fontWeight: 600,
606
+ textAnchor: 'middle',
607
+ dominantBaseline: 'middle',
608
+ upright: true,
609
+ metadata: { sitePlan: 'porch-label' },
610
+ })
611
+ }
612
+ primitives.push(...stairTreads(scene, level, building))
613
+
614
+ // ── The utility services a plugin located, run schematically to the street ─
615
+ const streetSide = [...new Set([frontEdge, ...streetEdges])]
616
+ const services = servicePrimitives({
617
+ scene,
618
+ level,
619
+ building,
620
+ lot,
621
+ streetEdges: streetSide,
622
+ outline: outerRings,
623
+ fontSize: LABEL_SIZE * 0.4,
624
+ })
625
+ primitives.push(...services.primitives)
626
+
627
+ // ── Drainage: the grade falls away from every face of the house ──────
628
+ primitives.push(
629
+ ...drainagePrimitives(outerRings, LABEL_SIZE * 0.42, [
630
+ ...coverage.parts.map((p) => p.ring),
631
+ ...(services.pad ? [services.pad] : []),
632
+ ]),
633
+ )
634
+
635
+ // ── Spot grades at the lot and house corners (the terrain's) ─────────
636
+ if (field) {
637
+ primitives.push(
638
+ ...spotElevationPrimitives({
639
+ field,
640
+ datumFt,
641
+ lot,
642
+ outline: outerRings,
643
+ fontSize: LABEL_SIZE * 0.34,
644
+ }),
645
+ )
646
+ }
647
+
648
+ // ── The house, named on its footprint, with its finish floor ─────────
649
+ if (footprintBounds && coverage.buildingSqFt > 0) {
650
+ let storeys = 0
651
+ for (const childId of building?.children ?? []) {
652
+ const child = scene.nodes[childId as AnyNodeId] as AnyNode | undefined
653
+ if (child?.type === 'level' && (child as LevelNode).level >= 0) storeys++
654
+ }
655
+ const cx = (footprintBounds.minX + footprintBounds.maxX) / 2
656
+ const cy = (footprintBounds.minY + footprintBounds.maxY) / 2
657
+ const lines = [
658
+ `${Math.max(1, storeys)}-STORY RESIDENCE`,
659
+ `${formatSqFt(coverage.buildingSqFt)} FOOTPRINT`,
660
+ ]
661
+ // the finish floor (and the garage pad) above the survey datum: the
662
+ // building's datum plus the storey slab's top
663
+ const tops = floorTops(scene, level)
664
+ const base = (building?.position?.[1] ?? 0) + (level?.baseElevation ?? 0)
665
+ if (tops.floor !== null) lines.push(`FF EL. ${formatElevation(base + tops.floor, datumFt)}`)
666
+ if (tops.garage !== null)
667
+ lines.push(`GARAGE SLAB EL. ${formatElevation(base + tops.garage, datumFt)}`)
668
+ lines.forEach((text, i) => {
669
+ primitives.push({
670
+ kind: 'text',
671
+ x: cx,
672
+ y: cy + (i - (lines.length - 1) / 2) * LABEL_SIZE * 0.9,
673
+ text,
674
+ fontSize: LABEL_SIZE * (i < 2 ? 0.6 : 0.5),
675
+ fill: INK,
676
+ fontWeight: i < 2 ? 700 : 600,
677
+ textAnchor: 'middle',
678
+ dominantBaseline: 'middle',
679
+ upright: true,
680
+ metadata: { sitePlan: i < 2 ? 'building-label' : 'floor-elevation' },
681
+ })
682
+ })
683
+ }
684
+
685
+ const setbacksNote = setbacksWarning(site)
686
+ if (setbacksNote) warnings.push(setbacksNote)
687
+ if (!coverage.parts.some((p) => p.kind === 'driveway' || p.kind === 'walk'))
688
+ warnings.push('No driveway or walk is drawn — none is shown on the site plan.')
689
+
690
+ // ── North arrow, top-right of the lot ────────────────────────────────
691
+ const arrowSize = Math.max(1.2, Math.min(2.2, (lotBounds.maxY - lotBounds.minY) * 0.07))
692
+ primitives.push(
693
+ northArrow(
694
+ [lotBounds.maxX + arrowSize * 1.4, lotBounds.minY + arrowSize * 1.2],
695
+ northRotation,
696
+ arrowSize,
697
+ ),
698
+ )
699
+
700
+ // ── Lot area / APN, and the coverage figures, under the lot ──────────
701
+ const lotLine = [
702
+ coverage.lotSqFt !== null ? `LOT AREA ${formatSqFt(coverage.lotSqFt)}` : '',
703
+ site?.parcel?.apn ? `APN ${site.parcel.apn}` : '',
704
+ site?.zone ? `ZONE ${site.zone}` : '',
705
+ ].filter(Boolean)
706
+ const coverageLine =
707
+ coverage.buildingSqFt > 0
708
+ ? [
709
+ `BUILDING COVERAGE ${formatSqFt(coverage.buildingCoverageSqFt)} (${formatCoveragePercent(coverage.buildingCoverageRatio)})`,
710
+ `IMPERVIOUS ${formatSqFt(coverage.imperviousSqFt)} (${formatCoveragePercent(coverage.imperviousRatio)})`,
711
+ ]
712
+ : []
713
+ ;[lotLine, coverageLine].forEach((bits, i) => {
714
+ if (bits.length === 0) return
715
+ primitives.push({
716
+ kind: 'text',
717
+ x: (lotBounds.minX + lotBounds.maxX) / 2,
718
+ y: lotBounds.maxY + LABEL_SIZE * (1.6 + i * 1.25),
719
+ text: bits.join(' · '),
720
+ fontSize: LABEL_SIZE * (i === 0 ? 1 : 0.85),
721
+ fill: INK,
722
+ fontWeight: 600,
723
+ textAnchor: 'middle',
724
+ dominantBaseline: 'hanging',
725
+ upright: true,
726
+ metadata: { sitePlan: i === 0 ? 'lot-label' : 'coverage-label' },
727
+ })
728
+ })
729
+
730
+ const combined = footprintBounds
731
+ ? {
732
+ minX: Math.min(lotBounds.minX, footprintBounds.minX),
733
+ minY: Math.min(lotBounds.minY, footprintBounds.minY),
734
+ maxX: Math.max(lotBounds.maxX, footprintBounds.maxX),
735
+ maxY: Math.max(lotBounds.maxY, footprintBounds.maxY),
736
+ }
737
+ : lotBounds
738
+
739
+ // Plugin kinds that live in site metres (utilities …) — see contributors.ts.
740
+ primitives.push(...sitePlanContributions(scene))
741
+
742
+ return {
743
+ primitives,
744
+ warnings,
745
+ bounds: padBounds(combined, Math.max(2, arrowSize * 1.8)),
746
+ meta: {
747
+ site,
748
+ frontEdge,
749
+ lot,
750
+ envelope,
751
+ envelopeFrontEdge: envelope.length >= 3 ? envelopeFrontEdge(lot, frontEdge, envelope) : 0,
752
+ sightTriangles,
753
+ footprintLoops,
754
+ footprintBounds,
755
+ yards,
756
+ buildingId: (building?.id as AnyNodeId) ?? null,
757
+ coverage,
758
+ },
759
+ }
760
+ }
761
+
762
+ /**
763
+ * Translation (site metres, `[dx, dz]`) that centres the building's footprint
764
+ * on the lot centroid. `null` when there is nothing to move or the footprint
765
+ * already sits inside the ring.
766
+ */
767
+ export function buildingRecentreOffset(
768
+ lot: readonly Pt[],
769
+ footprintBounds: Bounds | null,
770
+ ): [number, number] | null {
771
+ if (lot.length < 3 || !footprintBounds) return null
772
+ if (boundsInsidePolygon(lot, footprintBounds)) return null
773
+ const lotB = polygonBounds(lot)
774
+ const lotCx = (lotB.minX + lotB.maxX) / 2
775
+ const lotCy = (lotB.minY + lotB.maxY) / 2
776
+ const fpCx = (footprintBounds.minX + footprintBounds.maxX) / 2
777
+ const fpCy = (footprintBounds.minY + footprintBounds.maxY) / 2
778
+ return [lotCx - fpCx, lotCy - fpCy]
779
+ }