@pascal-app/editor 1.0.3 → 1.0.5

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 (624) hide show
  1. package/README.md +106 -0
  2. package/package.json +42 -38
  3. package/src/components/editor/__bench__/room-label-placement.bench.ts +108 -0
  4. package/src/components/editor/action-menu-button.tsx +83 -0
  5. package/src/components/editor/bake-thumbnail.tsx +307 -5
  6. package/src/components/editor/capture-level-picker.tsx +72 -0
  7. package/src/components/editor/construction-reveal-audio.test.tsx +150 -0
  8. package/src/components/editor/construction-reveal-audio.tsx +40 -0
  9. package/src/components/editor/custom-camera-controls.tsx +275 -57
  10. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  11. package/src/components/editor/duplicate-node.ts +244 -0
  12. package/src/components/editor/editor-layout-mobile.tsx +37 -26
  13. package/src/components/editor/editor-layout-v2.tsx +132 -59
  14. package/src/components/editor/export-manager.tsx +47 -26
  15. package/src/components/editor/first-person/build-collider-world.test.ts +320 -0
  16. package/src/components/editor/first-person/build-collider-world.ts +28 -7
  17. package/src/components/editor/first-person/ground-holes.test.ts +128 -0
  18. package/src/components/editor/first-person/ground-holes.ts +130 -0
  19. package/src/components/editor/first-person/mechanism-targets.test.ts +162 -0
  20. package/src/components/editor/first-person/mechanism-targets.ts +88 -0
  21. package/src/components/editor/first-person-controls.tsx +220 -9
  22. package/src/components/editor/floating-action-menu.tsx +63 -207
  23. package/src/components/editor/floor-aware-camera-controls.test.ts +466 -0
  24. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  25. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  26. package/src/components/editor/floor-region-controls.tsx +846 -0
  27. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  28. package/src/components/editor/floor-surface.test.ts +165 -0
  29. package/src/components/editor/floor-surface.ts +182 -0
  30. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  31. package/src/components/editor/floorplan-background-selection.ts +1 -3
  32. package/src/components/editor/floorplan-mode-coordinator.tsx +2 -2
  33. package/src/components/editor/floorplan-panel.tsx +414 -363
  34. package/src/components/editor/follow-pascal.test.tsx +912 -0
  35. package/src/components/editor/follow-pascal.tsx +354 -0
  36. package/src/components/editor/footprint-height-handle.tsx +204 -0
  37. package/src/components/editor/group-actions.ts +56 -2
  38. package/src/components/editor/group-move-3d.ts +2 -0
  39. package/src/components/editor/group-rotate-handle.tsx +2 -0
  40. package/src/components/editor/group-transform-shared.test.ts +460 -0
  41. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  42. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  43. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  44. package/src/components/editor/handles/use-handle-drag.ts +269 -197
  45. package/src/components/editor/index.tsx +409 -197
  46. package/src/components/editor/join-walls-pill.tsx +44 -0
  47. package/src/components/editor/node-action-menu.tsx +68 -70
  48. package/src/components/editor/node-arrow-handles.tsx +25 -6
  49. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  50. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  51. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  52. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  53. package/src/components/editor/registry-action-contributions.tsx +80 -1
  54. package/src/components/editor/riser-diagram-panel.tsx +14 -7
  55. package/src/components/editor/room-controls.tsx +847 -0
  56. package/src/components/editor/room-floor-highlight.tsx +128 -0
  57. package/src/components/editor/room-handles-2d.tsx +354 -0
  58. package/src/components/editor/room-handles.tsx +675 -0
  59. package/src/components/editor/room-highlight.tsx +233 -0
  60. package/src/components/editor/room-labels.test.ts +850 -0
  61. package/src/components/editor/room-labels.tsx +1890 -0
  62. package/src/components/editor/room-transform-ghost.tsx +256 -0
  63. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  64. package/src/components/editor/selection-manager.tsx +474 -419
  65. package/src/components/editor/slab-hole-highlights.tsx +5 -2
  66. package/src/components/editor/snapshot-capture-overlay.tsx +7 -2
  67. package/src/components/editor/snapshot-capture.test.ts +119 -60
  68. package/src/components/editor/snapshot-capture.ts +36 -5
  69. package/src/components/editor/thumbnail-generator.test.ts +654 -0
  70. package/src/components/editor/thumbnail-generator.tsx +1103 -223
  71. package/src/components/editor/use-floorplan-background-placement.ts +34 -12
  72. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  73. package/src/components/editor/views/host-view.tsx +61 -0
  74. package/src/components/editor/views/plugins-view.tsx +663 -0
  75. package/src/components/editor/views/scene-region.tsx +59 -0
  76. package/src/components/editor/views/use-editor-views.ts +57 -0
  77. package/src/components/editor/views/view-bar.test.tsx +54 -0
  78. package/src/components/editor/views/view-bar.tsx +343 -0
  79. package/src/components/editor/wall-justification.test.ts +140 -0
  80. package/src/components/editor/wall-move-side-handles.tsx +43 -142
  81. package/src/components/editor/wall-push-arrow.tsx +92 -0
  82. package/src/components/editor/wall-region-handles.tsx +509 -0
  83. package/src/components/editor/wall-snap-beacon-layer.tsx +8 -4
  84. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  85. package/src/components/editor-2d/floorplan-group-move.tsx +3 -0
  86. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  87. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  88. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +2 -2
  89. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  90. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +116 -106
  91. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  92. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  93. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  94. package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
  95. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  96. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
  97. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +319 -233
  98. package/src/components/editor-2d/renderers/floorplan-reveal.test.tsx +254 -0
  99. package/src/components/editor-2d/renderers/floorplan-reveal.ts +141 -0
  100. package/src/components/editor-2d/svg-paths.ts +12 -15
  101. package/src/components/point-ask/bubble.tsx +538 -0
  102. package/src/components/point-ask/hover-chip.tsx +91 -0
  103. package/src/components/point-ask/mode-pill.tsx +92 -0
  104. package/src/components/point-ask/optional-motion.ts +22 -0
  105. package/src/components/point-ask/outline-corners.tsx +93 -0
  106. package/src/components/point-ask/pins.tsx +711 -0
  107. package/src/components/point-ask/point-ask-layer.tsx +83 -0
  108. package/src/components/point-ask/point-ask-styles.ts +105 -0
  109. package/src/components/point-ask/region-marquee-styles.ts +15 -0
  110. package/src/components/point-ask/region-marquee.tsx +64 -0
  111. package/src/components/procedural-items/inspector.tsx +40 -15
  112. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +12 -2
  113. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  114. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  115. package/src/components/systems/roof/roof-edit-system.tsx +1 -0
  116. package/src/components/systems/zone/zone-system.test.ts +47 -0
  117. package/src/components/systems/zone/zone-system.tsx +29 -68
  118. package/src/components/tools/distribution/connection-compatibility.test.ts +34 -0
  119. package/src/components/tools/distribution/connection-compatibility.ts +43 -0
  120. package/src/components/tools/distribution/distribution-public.ts +9 -0
  121. package/src/components/tools/distribution/distribution-run-contract.test.ts +26 -0
  122. package/src/components/tools/distribution/distribution-run-contract.ts +94 -0
  123. package/src/components/tools/distribution/distribution-run-tool.test.ts +241 -0
  124. package/src/components/tools/distribution/distribution-run-tool.test.tsx +125 -0
  125. package/src/components/tools/distribution/distribution-run-tool.tsx +1323 -0
  126. package/src/components/tools/distribution/draw-alignment.ts +35 -0
  127. package/src/components/tools/distribution/ports.test.ts +56 -0
  128. package/src/components/tools/distribution/ports.ts +378 -0
  129. package/src/components/tools/distribution/run-cursor.test.ts +135 -0
  130. package/src/components/tools/distribution/run-cursor.ts +36 -0
  131. package/src/components/tools/distribution/run-direction-feedback.test.ts +60 -0
  132. package/src/components/tools/distribution/run-direction-feedback.tsx +251 -0
  133. package/src/components/tools/distribution/run-port-snap.test.ts +79 -0
  134. package/src/components/tools/distribution/run-port-snap.ts +25 -0
  135. package/src/components/tools/distribution/run-screen-direction.test.ts +23 -0
  136. package/src/components/tools/distribution/run-screen-direction.ts +30 -0
  137. package/src/components/tools/elevator/elevator-tool.tsx +3 -6
  138. package/src/components/tools/elevator/move-elevator-tool.tsx +2 -3
  139. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +67 -68
  140. package/src/components/tools/item/placement-history.test.tsx +512 -0
  141. package/src/components/tools/item/placement-strategies.test.ts +46 -1
  142. package/src/components/tools/item/placement-strategies.ts +11 -10
  143. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +56 -56
  144. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  145. package/src/components/tools/item/use-draft-node.ts +166 -95
  146. package/src/components/tools/item/use-placement-coordinator.tsx +148 -95
  147. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  148. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  149. package/src/components/tools/registry/item-surface-move.test.ts +62 -63
  150. package/src/components/tools/registry/item-surface-move.ts +2 -0
  151. package/src/components/tools/registry/item-surface-resolver.test.ts +26 -26
  152. package/src/components/tools/registry/move-registry-node-tool.test.ts +268 -1
  153. package/src/components/tools/registry/move-registry-node-tool.tsx +66 -30
  154. package/src/components/tools/select/box-select-tool.tsx +2 -12
  155. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  156. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  157. package/src/components/tools/select/select-candidates.test.ts +37 -4
  158. package/src/components/tools/select/select-candidates.ts +15 -11
  159. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  160. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  161. package/src/components/tools/tool-manager.tsx +59 -24
  162. package/src/components/tools/wall/wall-drafting.test.ts +175 -47
  163. package/src/components/tools/zone/zone-tool.tsx +4 -12
  164. package/src/components/ui/action-menu/action-button.tsx +9 -1
  165. package/src/components/ui/action-menu/camera-actions.tsx +9 -4
  166. package/src/components/ui/action-menu/control-modes.tsx +15 -42
  167. package/src/components/ui/action-menu/follow-pascal-pill.tsx +32 -0
  168. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +36 -0
  169. package/src/components/ui/action-menu/index.tsx +56 -33
  170. package/src/components/ui/action-menu/measurement-control.tsx +14 -19
  171. package/src/components/ui/action-menu/placement.ts +20 -0
  172. package/src/components/ui/action-menu/point-ask-button.tsx +52 -0
  173. package/src/components/ui/action-menu/view-toggles.tsx +47 -42
  174. package/src/components/ui/build-panel/build-panel.tsx +242 -0
  175. package/src/components/ui/command-palette/editor-commands.tsx +7 -42
  176. package/src/components/ui/command-palette/index.tsx +50 -39
  177. package/src/components/ui/command-palette/ranked-groups.test.ts +31 -0
  178. package/src/components/ui/controls/action-button.tsx +5 -2
  179. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  180. package/src/components/ui/controls/material-paint-panel.tsx +97 -26
  181. package/src/components/ui/controls/material-picker.tsx +4 -1
  182. package/src/components/ui/controls/metric-control.tsx +1 -1
  183. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  184. package/src/components/ui/controls/segmented-control.tsx +21 -8
  185. package/src/components/ui/controls/select-control.tsx +65 -0
  186. package/src/components/ui/controls/slider-control.tsx +1 -1
  187. package/src/components/ui/controls/toggle-control.tsx +3 -3
  188. package/src/components/ui/controls/tool-options-panel.tsx +1 -1
  189. package/src/components/ui/floating-level-selector.tsx +159 -59
  190. package/src/components/ui/helpers/building-helper.tsx +4 -1
  191. package/src/components/ui/helpers/contextual-helper-panel.tsx +181 -72
  192. package/src/components/ui/helpers/helper-manager.test.tsx +121 -0
  193. package/src/components/ui/helpers/helper-manager.tsx +331 -14
  194. package/src/components/ui/helpers/item-helper.tsx +17 -1
  195. package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
  196. package/src/components/ui/icon-ref.tsx +45 -1
  197. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  198. package/src/components/ui/node-icon-image.tsx +16 -0
  199. package/src/components/ui/panels/collections/collections-popover.tsx +28 -22
  200. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  201. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  202. package/src/components/ui/panels/mobile-panel-sheet.tsx +4 -4
  203. package/src/components/ui/panels/mobile-selection-bar.tsx +8 -5
  204. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  205. package/src/components/ui/panels/multi-field-value.ts +6 -3
  206. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  207. package/src/components/ui/panels/multi-selection-panel.tsx +161 -5
  208. package/src/components/ui/panels/node-display.ts +8 -3
  209. package/src/components/ui/panels/panel-manager.tsx +103 -68
  210. package/src/components/ui/panels/panel-wrapper.tsx +130 -62
  211. package/src/components/ui/panels/parametric-field-control.tsx +3 -3
  212. package/src/components/ui/panels/parametric-inspector.tsx +7 -7
  213. package/src/components/ui/panels/reference-panel.tsx +2 -3
  214. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  215. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  216. package/src/components/ui/panels/room-name-options.ts +47 -0
  217. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  218. package/src/components/ui/panels/room-panel.tsx +790 -0
  219. package/src/components/ui/panels/shape-choice.tsx +58 -0
  220. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  221. package/src/components/ui/primitives/badge.tsx +8 -0
  222. package/src/components/ui/primitives/color-dot.tsx +36 -24
  223. package/src/components/ui/primitives/sidebar.tsx +3 -1
  224. package/src/components/ui/right-stack.test.ts +29 -0
  225. package/src/components/ui/right-stack.tsx +164 -0
  226. package/src/components/ui/sidebar/app-sidebar.tsx +1 -1
  227. package/src/components/ui/sidebar/icon-rail.tsx +8 -3
  228. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
  229. package/src/components/ui/sidebar/panels/items-panel/index.tsx +6 -6
  230. package/src/components/ui/sidebar/panels/settings-panel/animation-speed-setting.tsx +40 -0
  231. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +7 -8
  232. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.test.tsx +170 -0
  233. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +92 -0
  234. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +138 -27
  235. package/src/components/ui/sidebar/panels/settings-panel/interface-theme.test.tsx +20 -0
  236. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
  237. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +4 -10
  238. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -11
  239. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  240. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  241. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  242. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  243. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  244. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  245. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  246. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  247. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  248. package/src/components/ui/sidebar/panels/site-panel/index.tsx +181 -223
  249. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  250. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  251. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  252. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  253. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  254. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  255. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  256. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  257. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  258. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  259. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +13 -37
  260. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  261. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  262. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  263. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  264. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +2 -4
  265. package/src/components/ui/sidebar/tab-bar.tsx +14 -4
  266. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  267. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  268. package/src/components/viewer/floorplan-preview.tsx +5 -6
  269. package/src/components/viewer/viewer-controls-bar.tsx +18 -6
  270. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  271. package/src/components/viewer/viewer-rooms.tsx +191 -0
  272. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  273. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  274. package/src/components/viewer/viewer-stage.tsx +2 -2
  275. package/src/components/viewer-zone-system.tsx +3 -8
  276. package/src/hooks/use-auto-save.integration.test.tsx +215 -43
  277. package/src/hooks/use-auto-save.ts +60 -9
  278. package/src/hooks/use-ceiling-events.test.ts +123 -118
  279. package/src/hooks/use-grid-events.test.ts +105 -105
  280. package/src/hooks/use-keyboard.test.ts +243 -19
  281. package/src/hooks/use-keyboard.ts +118 -83
  282. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  283. package/src/hooks/use-open-wall-ends.ts +238 -0
  284. package/src/hooks/use-placement-notice.test.ts +71 -0
  285. package/src/hooks/use-placement-notice.ts +73 -0
  286. package/src/hooks/use-selected-room.ts +143 -0
  287. package/src/index.tsx +312 -58
  288. package/src/lib/__fixtures__/owner-victor-seattle-parking.json +1 -0
  289. package/src/lib/__fixtures__/owner-villa-aurelia-basement.json +1 -0
  290. package/src/lib/add-storey.test.ts +177 -0
  291. package/src/lib/add-storey.ts +59 -0
  292. package/src/lib/below-grade.test.tsx +171 -0
  293. package/src/lib/below-grade.ts +133 -0
  294. package/src/lib/camera-facing.test.ts +24 -0
  295. package/src/lib/camera-facing.ts +24 -0
  296. package/src/lib/ceiling-edit-session.test.ts +321 -0
  297. package/src/lib/ceiling-edit-session.ts +206 -0
  298. package/src/lib/ceiling-region-session.test.ts +179 -0
  299. package/src/lib/continuation.test.ts +19 -1
  300. package/src/lib/continuation.ts +25 -4
  301. package/src/lib/direct-manipulation.ts +2 -0
  302. package/src/lib/door-interaction.ts +14 -0
  303. package/src/lib/download-project-file.ts +17 -0
  304. package/src/lib/editor-level-display.test.ts +96 -0
  305. package/src/lib/editor-level-display.ts +55 -0
  306. package/src/lib/editor-views.ts +178 -0
  307. package/src/lib/export-geometry-wait.test.ts +90 -0
  308. package/src/lib/export-texture-utils.ts +2 -4
  309. package/src/lib/floor-edit-session.ts +153 -0
  310. package/src/lib/floor-footprints.test.ts +563 -0
  311. package/src/lib/floor-footprints.ts +395 -0
  312. package/src/lib/floor-foundation-support.test.ts +284 -0
  313. package/src/lib/floor-opening-draft.ts +218 -0
  314. package/src/lib/floor-openings.test.ts +216 -0
  315. package/src/lib/floor-region-geometry.ts +191 -0
  316. package/src/lib/floor-region-session.test.ts +485 -0
  317. package/src/lib/floor-region-session.ts +589 -0
  318. package/src/lib/floor-region-snap.ts +101 -0
  319. package/src/lib/floor-step-hover.test.ts +32 -0
  320. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  321. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  322. package/src/lib/floorplan/dimension-drive.ts +479 -0
  323. package/src/lib/floorplan/floorplan-export.test.ts +12 -8
  324. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  325. package/src/lib/floorplan/floorplan-extension.ts +32 -0
  326. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  327. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  328. package/src/lib/floorplan/floorplan-pdfkit-document.ts +363 -27
  329. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +177 -8
  330. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +60 -25
  331. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  332. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  333. package/src/lib/floorplan/geometry.ts +13 -6
  334. package/src/lib/floorplan/index.ts +0 -9
  335. package/src/lib/floorplan/items.ts +5 -2
  336. package/src/lib/floorplan/marks.test.ts +213 -0
  337. package/src/lib/floorplan/marks.ts +238 -0
  338. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  339. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  340. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  341. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  342. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  343. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  344. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  345. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  346. package/src/lib/floorplan/pdfkit-standard-fonts.d.ts +5 -0
  347. package/src/lib/floorplan/schedules.test.ts +189 -0
  348. package/src/lib/floorplan/schedules.ts +339 -0
  349. package/src/lib/floorplan/sheet-export.test.ts +124 -0
  350. package/src/lib/floorplan/sheet-export.ts +318 -0
  351. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +768 -0
  352. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  353. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  354. package/src/lib/floorplan/site-plan/coverage.ts +230 -0
  355. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  356. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  357. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  358. package/src/lib/floorplan/site-plan/front-edge.ts +157 -0
  359. package/src/lib/floorplan/site-plan/geometry.test.ts +269 -0
  360. package/src/lib/floorplan/site-plan/geometry.ts +142 -0
  361. package/src/lib/floorplan/site-plan/index.ts +55 -0
  362. package/src/lib/floorplan/site-plan/site-annotations.test.ts +483 -0
  363. package/src/lib/floorplan/site-plan/site-annotations.ts +1084 -0
  364. package/src/lib/floorplan/site-plan/site-parts.ts +287 -0
  365. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  366. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  367. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  368. package/src/lib/floorplan/site-plan/yards.test.ts +70 -0
  369. package/src/lib/floorplan/types.ts +1 -27
  370. package/src/lib/follow-pascal-exterior.test-fixture.ts +17 -0
  371. package/src/lib/follow-pascal-exterior.test.ts +171 -0
  372. package/src/lib/follow-pascal-exterior.ts +39 -0
  373. package/src/lib/follow-pascal-presentation.test.ts +203 -0
  374. package/src/lib/follow-pascal-presentation.ts +129 -0
  375. package/src/lib/follow-pascal-sight.ts +84 -0
  376. package/src/lib/follow-pascal-targets.test.ts +406 -0
  377. package/src/lib/follow-pascal-targets.ts +198 -0
  378. package/src/lib/follow-pascal.test.ts +350 -0
  379. package/src/lib/follow-pascal.ts +284 -0
  380. package/src/lib/fresh-planar-placement.test.ts +148 -0
  381. package/src/lib/fresh-planar-placement.ts +114 -37
  382. package/src/lib/geometry-script/author.ts +157 -0
  383. package/src/lib/geometry-script/client.ts +159 -0
  384. package/src/lib/geometry-script/finish-scripts.ts +127 -0
  385. package/src/lib/geometry-script/protocol.ts +13 -0
  386. package/src/lib/gesture-lifecycle.test.ts +595 -0
  387. package/src/lib/gesture-lifecycle.ts +290 -0
  388. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  389. package/src/lib/glb-export.test.ts +636 -126
  390. package/src/lib/glb-export.ts +241 -120
  391. package/src/lib/history.test.ts +18 -7
  392. package/src/lib/history.ts +7 -0
  393. package/src/lib/hud-title.ts +109 -0
  394. package/src/lib/hydration-gated-storage.ts +49 -0
  395. package/src/lib/ifc-export.test.ts +174 -0
  396. package/src/lib/ifc-export.ts +201 -0
  397. package/src/lib/ifc-parts.ts +15 -0
  398. package/src/lib/inspector-expanded.test.ts +44 -0
  399. package/src/lib/inspector-expanded.ts +36 -0
  400. package/src/lib/interaction/scope.ts +22 -0
  401. package/src/lib/keyboard-pan.test.ts +129 -7
  402. package/src/lib/keyboard-pan.ts +161 -12
  403. package/src/lib/level-selection.test.ts +57 -0
  404. package/src/lib/level-selection.ts +16 -1
  405. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  406. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  407. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  408. package/src/lib/lot/clean-ring.test.ts +187 -0
  409. package/src/lib/lot/clean-ring.ts +217 -0
  410. package/src/lib/lot/dossier.test.ts +202 -0
  411. package/src/lib/lot/dossier.ts +498 -0
  412. package/src/lib/lot/drop-in.test.ts +326 -0
  413. package/src/lib/lot/drop-in.ts +392 -0
  414. package/src/lib/lot/index.ts +43 -0
  415. package/src/lib/lot/lot-patch.test.ts +340 -0
  416. package/src/lib/lot/lot-patch.ts +394 -0
  417. package/src/lib/lot/parcel-provider.ts +43 -0
  418. package/src/lib/lot/terrain.test.ts +161 -0
  419. package/src/lib/lot/terrain.ts +266 -0
  420. package/src/lib/material-catalog-model.ts +3 -1
  421. package/src/lib/material-paint-panel-model.ts +9 -5
  422. package/src/lib/material-paint.ts +4 -0
  423. package/src/lib/measurement-kind.test.ts +6 -8
  424. package/src/lib/measurements.test.ts +23 -0
  425. package/src/lib/mezzanine-actions.test.ts +342 -0
  426. package/src/lib/mezzanine-draft.test.ts +501 -0
  427. package/src/lib/mezzanine-draft.ts +278 -0
  428. package/src/lib/mezzanine-messages.ts +26 -0
  429. package/src/lib/mezzanine-selection.test.ts +207 -0
  430. package/src/lib/mezzanine-stairs.ts +46 -0
  431. package/src/lib/model-export.ts +3 -1
  432. package/src/lib/node-action-movement.ts +17 -6
  433. package/src/lib/node-display-label.ts +37 -0
  434. package/src/lib/orbit-floor-clearance.test.ts +201 -0
  435. package/src/lib/orbit-floor-clearance.ts +154 -0
  436. package/src/lib/paint-commit-route.test.ts +37 -0
  437. package/src/lib/paint-commit-route.ts +17 -0
  438. package/src/lib/paint-eyedropper.test.tsx +165 -0
  439. package/src/lib/paint-eyedropper.ts +99 -0
  440. package/src/lib/paint-pass-through.test.ts +122 -0
  441. package/src/lib/paint-pass-through.ts +78 -0
  442. package/src/lib/paint-region-hover.ts +25 -0
  443. package/src/lib/paint-region-mode.ts +154 -0
  444. package/src/lib/paint-regions.test.ts +166 -0
  445. package/src/lib/paint-regions.ts +192 -0
  446. package/src/lib/paint-scope.test.ts +317 -35
  447. package/src/lib/paint-scope.ts +267 -25
  448. package/src/lib/panel-rows.ts +21 -0
  449. package/src/lib/parametric-node-update.ts +16 -7
  450. package/src/lib/plate-paint-affected.test.ts +300 -0
  451. package/src/lib/plate-paint-affected.ts +177 -0
  452. package/src/lib/plugin-chat.ts +65 -0
  453. package/src/lib/plugin-panels.test.ts +25 -0
  454. package/src/lib/plugin-panels.ts +94 -0
  455. package/src/lib/point-ask/__fixtures__/consumer-browser.tsx +76 -0
  456. package/src/lib/point-ask/__fixtures__/house.ts +81 -0
  457. package/src/lib/point-ask/ask-created.test.ts +195 -0
  458. package/src/lib/point-ask/ask-flow.test.ts +380 -0
  459. package/src/lib/point-ask/build-context.test.ts +173 -0
  460. package/src/lib/point-ask/build-context.ts +94 -0
  461. package/src/lib/point-ask/capture-crop.ts +129 -0
  462. package/src/lib/point-ask/choreography.ts +69 -0
  463. package/src/lib/point-ask/compare.test.ts +145 -0
  464. package/src/lib/point-ask/compare.ts +73 -0
  465. package/src/lib/point-ask/describe-target.test.ts +165 -0
  466. package/src/lib/point-ask/describe-target.ts +234 -0
  467. package/src/lib/point-ask/dictation.ts +79 -0
  468. package/src/lib/point-ask/edge-marker.test.ts +61 -0
  469. package/src/lib/point-ask/edge-marker.ts +27 -0
  470. package/src/lib/point-ask/frame-loop.ts +25 -0
  471. package/src/lib/point-ask/history-marks.test.ts +94 -0
  472. package/src/lib/point-ask/history-marks.ts +39 -0
  473. package/src/lib/point-ask/host-api.ts +32 -0
  474. package/src/lib/point-ask/outline-box.test.ts +30 -0
  475. package/src/lib/point-ask/outline-box.ts +11 -0
  476. package/src/lib/point-ask/overlay-metrics.ts +6 -0
  477. package/src/lib/point-ask/pick.test.ts +220 -0
  478. package/src/lib/point-ask/pick.ts +121 -0
  479. package/src/lib/point-ask/pin-machine.test.ts +494 -0
  480. package/src/lib/point-ask/pin-machine.ts +217 -0
  481. package/src/lib/point-ask/placement.test.ts +203 -0
  482. package/src/lib/point-ask/placement.ts +141 -0
  483. package/src/lib/point-ask/pointer-tracker.ts +34 -0
  484. package/src/lib/point-ask/projector.ts +83 -0
  485. package/src/lib/point-ask/region.test.ts +157 -0
  486. package/src/lib/point-ask/region.ts +136 -0
  487. package/src/lib/point-ask/retry.test.ts +217 -0
  488. package/src/lib/point-ask/retry.ts +37 -0
  489. package/src/lib/point-ask/scene-commit.test.ts +312 -0
  490. package/src/lib/point-ask/scene-commit.ts +32 -0
  491. package/src/lib/point-ask/session.ts +1391 -0
  492. package/src/lib/point-ask/spring-keyframes.test.ts +45 -0
  493. package/src/lib/point-ask/spring-keyframes.ts +38 -0
  494. package/src/lib/point-ask/spring.test.ts +63 -0
  495. package/src/lib/point-ask/spring.ts +107 -0
  496. package/src/lib/point-ask/suggestions.test.ts +80 -0
  497. package/src/lib/point-ask/suggestions.ts +125 -0
  498. package/src/lib/point-ask/target-scope.test.ts +622 -0
  499. package/src/lib/point-ask/target-scope.ts +338 -0
  500. package/src/lib/point-ask/types.ts +48 -0
  501. package/src/lib/portable-export.test.ts +34 -1
  502. package/src/lib/portable-export.ts +119 -53
  503. package/src/lib/print-feature-thickness.ts +10 -1
  504. package/src/lib/print-roof-solids.test.ts +39 -1
  505. package/src/lib/print-roof-solids.ts +43 -10
  506. package/src/lib/print-shell-compiler-manifold-core.ts +1 -1
  507. package/src/lib/print-shell-compiler.ts +1 -0
  508. package/src/lib/registered-draft-snap.ts +12 -0
  509. package/src/lib/roof-duplication.ts +10 -2
  510. package/src/lib/room-assembly-overlay.test.ts +275 -0
  511. package/src/lib/room-assembly-overlay.ts +265 -0
  512. package/src/lib/room-built-on.test.ts +379 -0
  513. package/src/lib/room-built-on.ts +138 -0
  514. package/src/lib/room-construction-commands.ts +712 -0
  515. package/src/lib/room-construction.test.ts +746 -0
  516. package/src/lib/room-construction.ts +385 -0
  517. package/src/lib/room-controls-phase6.test.ts +630 -0
  518. package/src/lib/room-divide-session.ts +523 -0
  519. package/src/lib/room-handle-drag.ts +1068 -0
  520. package/src/lib/room-name-catalog.test.ts +90 -0
  521. package/src/lib/room-name-catalog.ts +168 -0
  522. package/src/lib/room-push-dimensions.test.ts +311 -0
  523. package/src/lib/room-push-dimensions.ts +251 -0
  524. package/src/lib/room-selection-commands.ts +62 -0
  525. package/src/lib/room-selection.test.ts +1017 -0
  526. package/src/lib/room-selection.ts +417 -0
  527. package/src/lib/room-structure-commands.ts +94 -0
  528. package/src/lib/room-structure.test.ts +921 -0
  529. package/src/lib/room-transform-session.ts +570 -0
  530. package/src/lib/room-zone-routing.test.ts +294 -0
  531. package/src/lib/room-zone-routing.ts +197 -0
  532. package/src/lib/scene-clipboard.test.ts +610 -1
  533. package/src/lib/scene-clipboard.ts +269 -58
  534. package/src/lib/scene-material-refs.test.ts +79 -0
  535. package/src/lib/scene-material-refs.ts +79 -0
  536. package/src/lib/scene.test.ts +24 -36
  537. package/src/lib/scene.ts +33 -57
  538. package/src/lib/selection-routing.test.ts +129 -45
  539. package/src/lib/selection-routing.ts +54 -75
  540. package/src/lib/session-writes.test.ts +120 -0
  541. package/src/lib/session-writes.ts +205 -0
  542. package/src/lib/sfx-bus.ts +13 -0
  543. package/src/lib/sfx-player.test.ts +70 -9
  544. package/src/lib/sfx-player.ts +6 -0
  545. package/src/lib/sidebar-panel.ts +62 -0
  546. package/src/lib/site-boundary.test.ts +4 -6
  547. package/src/lib/site-boundary.ts +1 -1
  548. package/src/lib/snapping-mode.test.ts +4 -0
  549. package/src/lib/snapping-mode.ts +8 -1
  550. package/src/lib/stair-levels.test.ts +25 -0
  551. package/src/lib/stair-levels.ts +22 -3
  552. package/src/lib/terrace-draft.test.ts +145 -0
  553. package/src/lib/terrace-draft.ts +214 -0
  554. package/src/lib/tool-cursor-icon.test.ts +16 -0
  555. package/src/lib/tool-cursor-icon.ts +11 -0
  556. package/src/lib/typing-target.ts +9 -0
  557. package/src/lib/units.ts +77 -39
  558. package/src/lib/vector-edges.test.ts +58 -0
  559. package/src/lib/vector-edges.ts +400 -0
  560. package/src/lib/viewer-selection.test.ts +577 -0
  561. package/src/lib/viewer-selection.ts +580 -0
  562. package/src/lib/wall-draw-variant.test.ts +71 -0
  563. package/src/lib/wall-draw-variant.ts +91 -0
  564. package/src/lib/wall-paint-region-session.ts +246 -0
  565. package/src/lib/wall-paint-region.test.ts +373 -0
  566. package/src/lib/wall-placement-pointer.test.ts +42 -0
  567. package/src/lib/wall-placement-pointer.ts +57 -0
  568. package/src/lib/wall-polygon-draft.test.ts +262 -0
  569. package/src/lib/wall-polygon-draft.ts +286 -0
  570. package/src/lib/wall-push.test.ts +453 -0
  571. package/src/lib/wall-region-face.ts +352 -0
  572. package/src/lib/wall-region-handles.test.ts +304 -0
  573. package/src/lib/wall-region-handles.ts +368 -0
  574. package/src/lib/wall-region-list.test.ts +119 -0
  575. package/src/lib/wall-region-list.ts +136 -0
  576. package/src/lib/wall-region-snap.ts +110 -0
  577. package/src/lib/window-changes.ts +35 -0
  578. package/src/lib/window-interaction.ts +14 -0
  579. package/src/lib/zone-content.ts +4 -1
  580. package/src/lib/zone-name.test.ts +17 -0
  581. package/src/lib/zone-name.ts +17 -0
  582. package/src/store/opens-in-select-mode.test.ts +223 -0
  583. package/src/store/paint-mode.test.ts +188 -0
  584. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  585. package/src/store/tool-mode.test.ts +137 -6
  586. package/src/store/use-audio.test.ts +146 -0
  587. package/src/store/use-audio.tsx +127 -3
  588. package/src/store/use-construction-reveal.test.ts +173 -0
  589. package/src/store/use-construction-reveal.ts +145 -0
  590. package/src/store/use-corner-snap-hint.test.ts +24 -0
  591. package/src/store/use-corner-snap-hint.ts +47 -0
  592. package/src/store/use-delete-confirmation.ts +28 -1
  593. package/src/store/use-drawing-view.test.ts +24 -3
  594. package/src/store/use-drawing-view.ts +29 -1
  595. package/src/store/use-editor.tsx +432 -321
  596. package/src/store/use-floorplan-draft-preview.ts +13 -0
  597. package/src/store/use-floorplan-marquee.ts +12 -2
  598. package/src/store/use-follow-pascal.test.ts +62 -0
  599. package/src/store/use-follow-pascal.ts +53 -0
  600. package/src/store/use-point-ask.ts +247 -0
  601. package/src/store/use-quick-measurement-hud.ts +3 -3
  602. package/src/store/use-stair-build-preview.ts +33 -5
  603. package/src/store/use-wall-move-ghosts.ts +4 -2
  604. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  605. package/src/store/use-wall-region-handles-selection.ts +139 -0
  606. package/src/store/view-layout.test.ts +201 -0
  607. package/src/store/view-layout.ts +199 -0
  608. package/src/test-utils/immediate-animation-frames.ts +16 -0
  609. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  610. package/src/test-utils/render-current-markup.tsx +16 -0
  611. package/src/test-utils/selection-harness.ts +82 -0
  612. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  613. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  614. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  615. package/src/components/tools/fence/fence-drafting.ts +0 -266
  616. package/src/components/tools/stair/stair-click-guard.test.ts +0 -58
  617. package/src/components/tools/stair/stair-click-guard.ts +0 -72
  618. package/src/components/tools/stair/stair-defaults.ts +0 -18
  619. package/src/components/tools/stair/stair-tool.tsx +0 -634
  620. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  621. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  622. package/src/lib/floorplan/stairs.ts +0 -489
  623. package/src/lib/level-duplication.test.ts +0 -144
  624. package/src/lib/level-duplication.ts +0 -195
@@ -0,0 +1,768 @@
1
+ import {
2
+ type AnyNode,
3
+ type AnyNodeId,
4
+ type BuildingNode,
5
+ castYardDimensionsOriented,
6
+ classifyEdges,
7
+ edgeHeadingDeg,
8
+ edgeLength,
9
+ envelopeFrontEdge,
10
+ type FloorplanGeometry,
11
+ footprintOutline,
12
+ isRoofLevel,
13
+ type LevelNode,
14
+ levelFootprintLoops,
15
+ outwardNormal,
16
+ type SceneSnapshot,
17
+ type SiteNode,
18
+ setbackForRole,
19
+ sightTriangle,
20
+ siteFrame,
21
+ siteFrontEdge,
22
+ siteSetbackEnvelope,
23
+ streetCorners,
24
+ terrainFieldOf,
25
+ type YardDimension,
26
+ yawOf,
27
+ } from '@pascal-app/core'
28
+ import { sitePlanContributions } from './contributors'
29
+ import {
30
+ computeSiteCoverage,
31
+ formatCoveragePercent,
32
+ formatSqFt,
33
+ type SiteCoverage,
34
+ } from './coverage'
35
+ import {
36
+ type Bounds,
37
+ compassLabel,
38
+ formatFeetInches,
39
+ METRES_PER_FOOT,
40
+ type Pt,
41
+ polygonBounds,
42
+ } from './geometry'
43
+ import {
44
+ contourPrimitives,
45
+ drainagePrimitives,
46
+ flatworkPrimitives,
47
+ floorTops,
48
+ formatElevation,
49
+ servicePrimitives,
50
+ spotElevationPrimitives,
51
+ streetEdgeNames,
52
+ } from './site-annotations'
53
+ import {
54
+ aboveGradeLevels,
55
+ findBuilding,
56
+ findLowestLevel,
57
+ findSite,
58
+ roofOutlineRings,
59
+ } from './site-parts'
60
+
61
+ export { roofOutlineRings }
62
+
63
+ /** Contract shared by every drawing producer (see docs/construction-documents.md). */
64
+ export interface SitePlanDrawing {
65
+ primitives: FloorplanGeometry[]
66
+ bounds: Bounds
67
+ /** What the drawing could not take from the model — printed on the sheet, never silent. */
68
+ warnings?: string[]
69
+ /** Live values the panel / sheets reuse without re-deriving them. */
70
+ meta: {
71
+ site: SiteNode | null
72
+ frontEdge: number
73
+ lot: Pt[]
74
+ envelope: Pt[]
75
+ /**
76
+ * The envelope edge behind the lot's front line (the envelope has its own vertex count now);
77
+ * null with no envelope, or when no envelope edge faces the street.
78
+ */
79
+ envelopeFrontEdge: number | null
80
+ /** The corner sight triangles (corner, leg end A, leg end B), site metres. */
81
+ sightTriangles: Pt[][]
82
+ /** Per-wall footprint bands of the lowest level, in SITE metres. */
83
+ footprintLoops: Pt[][]
84
+ footprintBounds: Bounds | null
85
+ yards: YardDimension[]
86
+ buildingId: AnyNodeId | null
87
+ /** Building coverage and impervious area — the figures the lot label and the sheets print. */
88
+ coverage?: SiteCoverage
89
+ }
90
+ }
91
+
92
+ const LOT_STROKE_WIDTH = 0.12
93
+ const ENVELOPE_STROKE_WIDTH = 0.05
94
+ const LABEL_SIZE = 0.9
95
+
96
+ // Plan ink. The floor-plan surface is the light plan sheet the panel paints
97
+ // behind the scene group, where the app's `currentColor` foreground is
98
+ // near-white and therefore invisible. Kinds hard-code their plan ink for the
99
+ // same reason; these match `nodes/src/wall/floorplan.ts`.
100
+ const INK = '#111827'
101
+ const INK_SOFT = '#4b5563'
102
+ /** Contour lines: the survey's brown. */
103
+ const CONTOUR_INK = '#8b5a2b'
104
+ /** The house on the lot: a light body under a heavy edge, so its label and the roof line read. */
105
+ const FOOTPRINT_FILL = '#d1d5db'
106
+ const FOOTPRINT_STROKE = '#111827'
107
+ const DIMENSION_STROKE = '#334155'
108
+
109
+ export interface SitePlanEdge {
110
+ index: number
111
+ headingDeg: number
112
+ compass: string
113
+ lengthM: number
114
+ label: string
115
+ }
116
+
117
+ /** Lot edges with their compass heading — feeds the panel's front-edge picker. */
118
+ export function describeSiteEdges(site: SiteNode | null | undefined): SitePlanEdge[] {
119
+ const lot = (site?.polygon?.points ?? []) as Pt[]
120
+ if (lot.length < 3) return []
121
+ const north = site?.northRotation ?? 0
122
+ return lot.map((_, i) => {
123
+ const headingDeg = edgeHeadingDeg(lot, i, north)
124
+ const lengthM = edgeLength(lot, i)
125
+ const compass = compassLabel(headingDeg)
126
+ return {
127
+ index: i,
128
+ headingDeg,
129
+ compass,
130
+ lengthM,
131
+ label: `Edge ${i + 1} — faces ${compass} (${Math.round(headingDeg)}°), ${formatFeetInches(lengthM)}`,
132
+ }
133
+ })
134
+ }
135
+
136
+ function unionBounds(loops: readonly Pt[][]): Bounds | null {
137
+ const all: Pt[] = []
138
+ for (const loop of loops) all.push(...loop)
139
+ if (all.length === 0) return null
140
+ return polygonBounds(all)
141
+ }
142
+
143
+ function padBounds(b: Bounds, pad: number): Bounds {
144
+ return { minX: b.minX - pad, minY: b.minY - pad, maxX: b.maxX + pad, maxY: b.maxY + pad }
145
+ }
146
+
147
+ function northArrow(at: Pt, northRotation: number, size: number): FloorplanGeometry {
148
+ const [cx, cy] = at
149
+ // Local frame: up is −y, rotated clockwise by `northRotation`.
150
+ const cos = Math.cos(northRotation)
151
+ const sin = Math.sin(northRotation)
152
+ const rot = (x: number, y: number): Pt => [cx + x * cos - y * sin, cy + x * sin + y * cos]
153
+ const tip = rot(0, -size)
154
+ const tail = rot(0, size * 0.55)
155
+ const left = rot(-size * 0.28, -size * 0.35)
156
+ const right = rot(size * 0.28, -size * 0.35)
157
+ const labelAt = rot(0, size * 0.95)
158
+ return {
159
+ kind: 'group',
160
+ children: [
161
+ {
162
+ kind: 'line',
163
+ x1: tail[0],
164
+ y1: tail[1],
165
+ x2: tip[0],
166
+ y2: tip[1],
167
+ stroke: INK,
168
+ strokeWidth: 1.4,
169
+ vectorEffect: 'non-scaling-stroke',
170
+ },
171
+ {
172
+ kind: 'polygon',
173
+ points: [tip, left, right],
174
+ fill: INK,
175
+ stroke: INK,
176
+ strokeWidth: 1,
177
+ vectorEffect: 'non-scaling-stroke',
178
+ },
179
+ {
180
+ kind: 'text',
181
+ x: labelAt[0],
182
+ y: labelAt[1],
183
+ text: 'N',
184
+ fontSize: size * 0.55,
185
+ fill: INK,
186
+ fontWeight: 600,
187
+ textAnchor: 'middle',
188
+ dominantBaseline: 'hanging',
189
+ upright: true,
190
+ },
191
+ ],
192
+ }
193
+ }
194
+
195
+ /** A survey bearing for the run (dx, dy): quadrant, degrees and minutes — "N 81°42' E". */
196
+ function bearingOf(dx: number, dy: number, northRotation: number): string {
197
+ // plan up (−y) is north and +x is east; `northRotation` turns true north
198
+ // clockwise from plan up (the north arrow's own convention)
199
+ let az = ((Math.atan2(dx, -dy) - northRotation) * 180) / Math.PI
200
+ az = ((az % 360) + 360) % 360
201
+ const ns = az <= 90 || az >= 270 ? 'N' : 'S'
202
+ const ew = az <= 180 ? 'E' : 'W'
203
+ const off = az <= 90 ? az : az <= 180 ? 180 - az : az <= 270 ? az - 180 : 360 - az
204
+ let deg = Math.floor(off)
205
+ let min = Math.round((off - deg) * 60)
206
+ if (min === 60) {
207
+ deg += 1
208
+ min = 0
209
+ }
210
+ return `${ns} ${deg}°${String(min).padStart(2, '0')}' ${ew}`
211
+ }
212
+
213
+ /** An angle a label reads at: within a quarter turn of the paper's horizontal. */
214
+ function readableAngle(a: number): number {
215
+ let r = a
216
+ while (r > Math.PI / 2) r -= Math.PI
217
+ while (r <= -Math.PI / 2) r += Math.PI
218
+ return r
219
+ }
220
+
221
+ /** A label laid ALONG a run — a group turned to the run's readable angle, the text centred on `at`. */
222
+ function alongLabel(
223
+ at: Pt,
224
+ angle: number,
225
+ text: string,
226
+ fontSize: number,
227
+ style: { fill: string; fontWeight?: number },
228
+ metadata: Record<string, unknown>,
229
+ ): FloorplanGeometry {
230
+ return {
231
+ kind: 'group',
232
+ transform: { translate: at, rotate: readableAngle(angle) },
233
+ children: [
234
+ {
235
+ kind: 'text',
236
+ x: 0,
237
+ y: 0,
238
+ text,
239
+ fontSize,
240
+ fill: style.fill,
241
+ fontWeight: style.fontWeight ?? 500,
242
+ textAnchor: 'middle',
243
+ dominantBaseline: 'middle',
244
+ },
245
+ ],
246
+ metadata,
247
+ }
248
+ }
249
+
250
+ /** The exterior steps as tread lines (three, a tread apart), site metres. */
251
+ function stairTreads(
252
+ scene: SceneSnapshot,
253
+ level: LevelNode | null,
254
+ building: BuildingNode | null,
255
+ ): FloorplanGeometry[] {
256
+ if (!level) return []
257
+ const toSite = siteFrame(building)
258
+ const out: FloorplanGeometry[] = []
259
+ for (const childId of level.children) {
260
+ const child = scene.nodes[childId as AnyNodeId] as AnyNode | undefined
261
+ if (child?.type !== 'stair') continue
262
+ const stair = child as unknown as { position?: number[]; rotation?: unknown; width?: number }
263
+ const x = stair.position?.[0] ?? 0
264
+ const z = stair.position?.[2] ?? 0
265
+ const r = yawOf(stair.rotation)
266
+ const half = (stair.width ?? 1) / 2
267
+ // the run climbs toward the floor it serves: +local z turned by the yaw
268
+ const dir: Pt = [Math.sin(r), Math.cos(r)]
269
+ const across: Pt = [Math.cos(r), -Math.sin(r)]
270
+ for (let k = 0; k < 3; k++) {
271
+ const cx = x + dir[0] * k * 0.28
272
+ const cz = z + dir[1] * k * 0.28
273
+ const a = toSite(cx - across[0] * half, cz - across[1] * half)
274
+ const b = toSite(cx + across[0] * half, cz + across[1] * half)
275
+ out.push({
276
+ kind: 'line',
277
+ x1: a[0],
278
+ y1: a[1],
279
+ x2: b[0],
280
+ y2: b[1],
281
+ stroke: INK,
282
+ strokeWidth: 0.02,
283
+ metadata: { sitePlan: 'steps' },
284
+ })
285
+ }
286
+ }
287
+ return out
288
+ }
289
+
290
+ /**
291
+ * The setbacks line the site plan prints when the yards are not the zoning
292
+ * code's own: worded for a plans examiner from the site's numbers — never the
293
+ * internal source tag a lot drop-in stored (QA 2026-09-23 printed the
294
+ * internal defaults tag on A1.0). A cited code prints nothing.
295
+ */
296
+ export function setbacksWarning(
297
+ site: Pick<SiteNode, 'setbacks' | 'setbacksSource'> | null,
298
+ ): string | null {
299
+ const source = site?.setbacksSource ?? ''
300
+ const s = site?.setbacks
301
+ if (!s || !/draft|verify|default|by hand/i.test(source)) return null
302
+ const ft = (m: number) => `${Math.round((m / METRES_PER_FOOT) * 2) / 2} ft`
303
+ const yards = `front ${ft(s.front)}, side ${ft(s.side)}, rear ${ft(s.rear)}`
304
+ if (/by hand/i.test(source))
305
+ return `Setbacks: set by hand (${yards}) — confirm with the zoning district.`
306
+ if (/default/i.test(source))
307
+ return `Setbacks: planning defaults (${yards}) — confirm with the zoning district.`
308
+ return `Setbacks: ${source}`
309
+ }
310
+
311
+ /**
312
+ * Build the site-plan drawing from the scene.
313
+ *
314
+ * Everything is recomputed from the current snapshot, so the drawing is live:
315
+ * move the building or edit a setback and the next call reflects it.
316
+ * Returns an empty drawing (no primitives) when the scene has no site polygon.
317
+ */
318
+ export function buildSitePlanDrawing(scene: SceneSnapshot): SitePlanDrawing {
319
+ const site = findSite(scene)
320
+ const lot = (site?.polygon?.points ?? []) as Pt[]
321
+ const building = findBuilding(scene, site)
322
+ const level = findLowestLevel(scene, building)
323
+ const footprintLoops = levelFootprintLoops(scene.nodes, level, building)
324
+ const footprintBounds = unionBounds(footprintLoops)
325
+ const northRotation = site?.northRotation ?? 0
326
+ const frontEdge = site ? siteFrontEdge(site) : 0
327
+
328
+ const empty: SitePlanDrawing = {
329
+ primitives: [],
330
+ warnings: [],
331
+ bounds: footprintBounds ?? { minX: -15, minY: -15, maxX: 15, maxY: 15 },
332
+ meta: {
333
+ site,
334
+ frontEdge,
335
+ lot,
336
+ envelope: [],
337
+ envelopeFrontEdge: null,
338
+ sightTriangles: [],
339
+ footprintLoops,
340
+ footprintBounds,
341
+ yards: [],
342
+ buildingId: (building?.id as AnyNodeId) ?? null,
343
+ },
344
+ }
345
+ if (lot.length < 3) return empty
346
+
347
+ const primitives: FloorplanGeometry[] = []
348
+ // the coverage figures and the outdoor parts, from the one shared reading
349
+ // (coverage.ts) — the cover's PROJECT DATA prints the same numbers
350
+ const coverage = computeSiteCoverage(scene)
351
+
352
+ // ── Lot line — the heaviest line on the sheet ────────────────────────
353
+ primitives.push({
354
+ kind: 'polygon',
355
+ points: lot,
356
+ fill: 'none',
357
+ stroke: INK,
358
+ strokeWidth: LOT_STROKE_WIDTH,
359
+ strokeLinejoin: 'miter',
360
+ metadata: { sitePlan: 'lot-line' },
361
+ })
362
+
363
+ // ── Terrain contours — the ground's lines, at the site's interval ─────
364
+ // (site.contourIntervalIn, default 12 in; 0 = none): thin brown lines at
365
+ // whole elevations above the survey datum when the terrain sample carries
366
+ // one (USGS EPQS at the lot centre, NAVD88), every fifth heavier, each
367
+ // elevation labelled once or twice (site-annotations.ts).
368
+ const field = site ? terrainFieldOf(site as never) : null
369
+ const datumFt = (() => {
370
+ const sample = (site?.metadata as { terrainSample?: { datumFt?: unknown } } | undefined)
371
+ ?.terrainSample
372
+ return typeof sample?.datumFt === 'number' ? sample.datumFt : null
373
+ })()
374
+ {
375
+ const intervalIn = typeof site?.contourIntervalIn === 'number' ? site.contourIntervalIn : 12
376
+ if (site && field && intervalIn > 0) {
377
+ const lines = contourPrimitives({
378
+ site,
379
+ lot,
380
+ field,
381
+ intervalIn,
382
+ datumFt,
383
+ fontSize: LABEL_SIZE * 0.45,
384
+ // no elevation printed on the house, a porch or the paving
385
+ avoid: [...coverage.outline, ...coverage.parts.map((p) => p.ring)],
386
+ })
387
+ if (lines.length > 0) primitives.push({ kind: 'group', children: lines })
388
+ }
389
+ }
390
+
391
+ // ── The driveway and walks, under the yards' labels and dimensions ────
392
+ primitives.push(...flatworkPrimitives(scene, building, coverage.parts, LABEL_SIZE * 0.42))
393
+
394
+ // ── Setback envelope — dashed, offset inward per edge ────────────────
395
+ const streetEdges = (site?.streetEdges ?? []).filter((i) => Number.isFinite(i))
396
+ const sightTriangleM =
397
+ typeof site?.sightTriangleFt === 'number' && site.sightTriangleFt > 0
398
+ ? site.sightTriangleFt * METRES_PER_FOOT
399
+ : 0
400
+ const envelope = site ? siteSetbackEnvelope(site) : []
401
+ // the corner sight triangles (clear-vision at a street intersection):
402
+ // dashed, labelled with the leg — the ordinance's figure, verify locally
403
+ const sightTriangles: Pt[][] = []
404
+ if (sightTriangleM > 0) {
405
+ for (const [a, b] of streetCorners(lot, [frontEdge, ...streetEdges])) {
406
+ const tri = sightTriangle(lot, a, b, sightTriangleM)
407
+ if (!tri) continue
408
+ const ring: Pt[] = [tri.corner, tri.a, tri.b]
409
+ sightTriangles.push(ring)
410
+ primitives.push({
411
+ kind: 'polygon',
412
+ points: ring,
413
+ fill: 'none',
414
+ stroke: INK_SOFT,
415
+ strokeWidth: ENVELOPE_STROKE_WIDTH,
416
+ strokeDasharray: '0.3 0.2',
417
+ opacity: 0.75,
418
+ metadata: { sitePlan: 'sight-triangle' },
419
+ })
420
+ const cx = (tri.corner[0] + tri.a[0] + tri.b[0]) / 3
421
+ const cz = (tri.corner[1] + tri.a[1] + tri.b[1]) / 3
422
+ primitives.push({
423
+ kind: 'text',
424
+ x: cx,
425
+ y: cz,
426
+ text: `SIGHT TRIANGLE ${site?.sightTriangleFt}' (VERIFY)`,
427
+ fontSize: 0.6,
428
+ fill: INK_SOFT,
429
+ textAnchor: 'middle',
430
+ metadata: { sitePlan: 'sight-triangle-label' },
431
+ })
432
+ }
433
+ }
434
+ if (envelope.length >= 3) {
435
+ primitives.push({
436
+ kind: 'polygon',
437
+ points: envelope,
438
+ fill: 'none',
439
+ stroke: INK_SOFT,
440
+ strokeWidth: ENVELOPE_STROKE_WIDTH,
441
+ strokeDasharray: '0.9 0.45',
442
+ opacity: 0.75,
443
+ metadata: { sitePlan: 'setback-envelope' },
444
+ })
445
+ }
446
+
447
+ // ── Building footprint — the OUTLINE of the level-0 walls ─────────────
448
+ // A site plan shows the building's edge on the lot; the partitions inside
449
+ // it are the floor plan's business, so the wall bands are unioned and only
450
+ // the outer rings are kept.
451
+ const outerRings = footprintOutline(footprintLoops)
452
+ for (const loop of outerRings) {
453
+ primitives.push({
454
+ kind: 'polygon',
455
+ points: loop,
456
+ fill: FOOTPRINT_FILL,
457
+ fillOpacity: 0.7,
458
+ stroke: FOOTPRINT_STROKE,
459
+ strokeWidth: 0.06,
460
+ metadata: { sitePlan: 'building-footprint', buildingId: building?.id ?? null },
461
+ })
462
+ }
463
+
464
+ // ── Yard dimensions — the house's own faces, square out to the lot line ──
465
+ const yards = footprintLoops.length
466
+ ? castYardDimensionsOriented(lot, footprintLoops, building?.rotation?.[1] ?? 0)
467
+ : []
468
+ for (const yard of yards) {
469
+ const dx = yard.to[0] - yard.from[0]
470
+ const dy = yard.to[1] - yard.from[1]
471
+ const len = Math.hypot(dx, dy) || 1
472
+ // Offset normal is perpendicular to the measured run; zero offset keeps
473
+ // the dimension line on the yard itself, which is how site plans read.
474
+ primitives.push({
475
+ kind: 'dimension',
476
+ start: yard.from,
477
+ end: yard.to,
478
+ offsetNormal: [-dy / len, dx / len],
479
+ offsetDistance: 0,
480
+ extensionOvershoot: 0.35,
481
+ stroke: DIMENSION_STROKE,
482
+ terminator: 'architectural-tick',
483
+ text: formatFeetInches(yard.distance),
484
+ metadata: { sitePlan: 'yard-dimension', side: yard.side },
485
+ } as FloorplanGeometry)
486
+ }
487
+
488
+ const lotBounds = polygonBounds(lot)
489
+ const warnings: string[] = []
490
+
491
+ // ── Property lines: bearing and length along each edge, outside the lot ──
492
+ for (let i = 0; i < lot.length; i++) {
493
+ const p = lot[i] as Pt
494
+ const q = lot[(i + 1) % lot.length] as Pt
495
+ const dx = q[0] - p[0]
496
+ const dy = q[1] - p[1]
497
+ const len = Math.hypot(dx, dy)
498
+ if (len < 0.5) continue
499
+ const out = outwardNormal(lot, i)
500
+ primitives.push(
501
+ alongLabel(
502
+ [(p[0] + q[0]) / 2 + out[0] * 0.55, (p[1] + q[1]) / 2 + out[1] * 0.55],
503
+ Math.atan2(dy, dx),
504
+ `${bearingOf(dx, dy, northRotation)} ${(len / METRES_PER_FOOT).toFixed(2)}'`,
505
+ LABEL_SIZE * 0.48,
506
+ { fill: INK, fontWeight: 600 },
507
+ { sitePlan: 'lot-edge-label', edge: i },
508
+ ),
509
+ )
510
+ }
511
+
512
+ // ── Setback labels, in each yard along its lot edge ───────────────────
513
+ if (site?.setbacks && envelope.length >= 3) {
514
+ const roles = classifyEdges(lot, frontEdge, streetEdges)
515
+ const NAME = {
516
+ front: 'FRONT',
517
+ rear: 'REAR',
518
+ street: 'STREET SIDE',
519
+ left: 'SIDE',
520
+ right: 'SIDE',
521
+ } as const
522
+ for (let i = 0; i < lot.length; i++) {
523
+ const role = roles[i]
524
+ if (!role) continue
525
+ const s = setbackForRole(site.setbacks, role)
526
+ if (!(s > 0)) continue
527
+ const p = lot[i] as Pt
528
+ const q = lot[(i + 1) % lot.length] as Pt
529
+ const dx = q[0] - p[0]
530
+ const dy = q[1] - p[1]
531
+ if (Math.hypot(dx, dy) < 1) continue
532
+ const out = outwardNormal(lot, i)
533
+ // a third of the way along the edge, halfway into the yard — clear of
534
+ // the yard dimension that stands square off the house
535
+ primitives.push(
536
+ alongLabel(
537
+ [p[0] + dx * 0.32 - out[0] * (s / 2), p[1] + dy * 0.32 - out[1] * (s / 2)],
538
+ Math.atan2(dy, dx),
539
+ `${formatFeetInches(s)} ${NAME[role]} SETBACK`,
540
+ Math.min(LABEL_SIZE * 0.4, s * 0.45),
541
+ { fill: INK_SOFT },
542
+ { sitePlan: 'setback-label', edge: i, role },
543
+ ),
544
+ )
545
+ }
546
+ }
547
+
548
+ // ── The streets, each named beyond the edge it runs along ─────────────
549
+ // (only the edges that front a street; the name the lot drop-in stored
550
+ // for that edge — site-annotations.ts `streetEdgeNames`)
551
+ if (site) {
552
+ for (const [i, name] of streetEdgeNames(site, frontEdge)) {
553
+ const p = lot[i]
554
+ const q = lot[(i + 1) % lot.length]
555
+ if (!p || !q) continue
556
+ const out = outwardNormal(lot, i)
557
+ primitives.push(
558
+ alongLabel(
559
+ [(p[0] + q[0]) / 2 + out[0] * 2.2, (p[1] + q[1]) / 2 + out[1] * 2.2],
560
+ Math.atan2(q[1] - p[1], q[0] - p[0]),
561
+ name,
562
+ LABEL_SIZE * (name.length > 24 ? 0.6 : 0.85),
563
+ { fill: INK, fontWeight: 700 },
564
+ { sitePlan: 'street-name', edge: i },
565
+ ),
566
+ )
567
+ }
568
+ }
569
+
570
+ // ── The roof line, dashed over the footprint ─────────────────────────
571
+ // Every storey's roofs, and the roof level's above them all.
572
+ const roofRings = aboveGradeLevels(scene, building).flatMap((roofed) =>
573
+ roofOutlineRings(scene, roofed, building),
574
+ )
575
+ for (const ring of roofRings) {
576
+ primitives.push({
577
+ kind: 'polygon',
578
+ points: ring,
579
+ fill: 'none',
580
+ stroke: INK_SOFT,
581
+ strokeWidth: 0.035,
582
+ strokeDasharray: '0.5 0.25',
583
+ opacity: 0.9,
584
+ metadata: { sitePlan: 'roof-outline' },
585
+ })
586
+ }
587
+
588
+ // ── Porches, patios, decks, landings and their steps ─────────────────
589
+ for (const porch of coverage.parts) {
590
+ if (porch.kind === 'driveway' || porch.kind === 'walk') continue
591
+ primitives.push({
592
+ kind: 'polygon',
593
+ points: porch.ring,
594
+ fill: '#f3f4f6',
595
+ fillOpacity: 0.95,
596
+ stroke: FOOTPRINT_STROKE,
597
+ strokeWidth: 0.03,
598
+ metadata: { sitePlan: 'porch' },
599
+ })
600
+ const b = polygonBounds(porch.ring)
601
+ primitives.push({
602
+ kind: 'text',
603
+ x: (b.minX + b.maxX) / 2,
604
+ y: (b.minY + b.maxY) / 2,
605
+ text: porch.covered ? porch.label : `${porch.label} (OPEN)`,
606
+ fontSize: Math.min(
607
+ LABEL_SIZE * 0.4,
608
+ Math.max(0.2, Math.min(b.maxY - b.minY, b.maxX - b.minX) * 0.3),
609
+ ),
610
+ fill: INK,
611
+ fontWeight: 600,
612
+ textAnchor: 'middle',
613
+ dominantBaseline: 'middle',
614
+ upright: true,
615
+ metadata: { sitePlan: 'porch-label' },
616
+ })
617
+ }
618
+ primitives.push(...stairTreads(scene, level, building))
619
+
620
+ // ── The utility services a plugin located, run schematically to the street ─
621
+ const streetSide = [...new Set([frontEdge, ...streetEdges])]
622
+ const services = servicePrimitives({
623
+ scene,
624
+ level,
625
+ building,
626
+ lot,
627
+ streetEdges: streetSide,
628
+ outline: outerRings,
629
+ fontSize: LABEL_SIZE * 0.4,
630
+ })
631
+ primitives.push(...services.primitives)
632
+
633
+ // ── Drainage: the grade falls away from every face of the house ──────
634
+ primitives.push(
635
+ ...drainagePrimitives(outerRings, LABEL_SIZE * 0.42, [
636
+ ...coverage.parts.map((p) => p.ring),
637
+ ...(services.pad ? [services.pad] : []),
638
+ ]),
639
+ )
640
+
641
+ // ── Spot grades at the lot and house corners (the terrain's) ─────────
642
+ if (field) {
643
+ primitives.push(
644
+ ...spotElevationPrimitives({
645
+ field,
646
+ datumFt,
647
+ lot,
648
+ outline: outerRings,
649
+ fontSize: LABEL_SIZE * 0.34,
650
+ }),
651
+ )
652
+ }
653
+
654
+ // ── The house, named on its footprint, with its finish floor ─────────
655
+ if (footprintBounds && coverage.buildingSqFt > 0) {
656
+ let storeys = 0
657
+ for (const childId of building?.children ?? []) {
658
+ const child = scene.nodes[childId as AnyNodeId] as AnyNode | undefined
659
+ // The roof level is not a storey: a bungalow under it is one storey.
660
+ if (child?.type === 'level' && (child as LevelNode).level >= 0 && !isRoofLevel(child))
661
+ storeys++
662
+ }
663
+ const cx = (footprintBounds.minX + footprintBounds.maxX) / 2
664
+ const cy = (footprintBounds.minY + footprintBounds.maxY) / 2
665
+ const lines = [
666
+ `${Math.max(1, storeys)}-STORY RESIDENCE`,
667
+ `${formatSqFt(coverage.buildingSqFt)} FOOTPRINT`,
668
+ ]
669
+ // the finish floor (and the garage pad) above the survey datum: the
670
+ // building's datum plus the storey slab's top
671
+ const tops = floorTops(scene, level)
672
+ const base = (building?.position?.[1] ?? 0) + (level?.baseElevation ?? 0)
673
+ if (tops.floor !== null) lines.push(`FF EL. ${formatElevation(base + tops.floor, datumFt)}`)
674
+ if (tops.garage !== null)
675
+ lines.push(`GARAGE SLAB EL. ${formatElevation(base + tops.garage, datumFt)}`)
676
+ lines.forEach((text, i) => {
677
+ primitives.push({
678
+ kind: 'text',
679
+ x: cx,
680
+ y: cy + (i - (lines.length - 1) / 2) * LABEL_SIZE * 0.9,
681
+ text,
682
+ fontSize: LABEL_SIZE * (i < 2 ? 0.6 : 0.5),
683
+ fill: INK,
684
+ fontWeight: i < 2 ? 700 : 600,
685
+ textAnchor: 'middle',
686
+ dominantBaseline: 'middle',
687
+ upright: true,
688
+ metadata: { sitePlan: i < 2 ? 'building-label' : 'floor-elevation' },
689
+ })
690
+ })
691
+ }
692
+
693
+ const setbacksNote = setbacksWarning(site)
694
+ if (setbacksNote) warnings.push(setbacksNote)
695
+ if (!coverage.parts.some((p) => p.kind === 'driveway' || p.kind === 'walk'))
696
+ warnings.push('No driveway or walk is drawn — none is shown on the site plan.')
697
+
698
+ // ── North arrow, top-right of the lot ────────────────────────────────
699
+ const arrowSize = Math.max(1.2, Math.min(2.2, (lotBounds.maxY - lotBounds.minY) * 0.07))
700
+ primitives.push(
701
+ northArrow(
702
+ [lotBounds.maxX + arrowSize * 1.4, lotBounds.minY + arrowSize * 1.2],
703
+ northRotation,
704
+ arrowSize,
705
+ ),
706
+ )
707
+
708
+ // ── Lot area / APN, and the coverage figures, under the lot ──────────
709
+ const lotLine = [
710
+ coverage.lotSqFt !== null ? `LOT AREA ${formatSqFt(coverage.lotSqFt)}` : '',
711
+ site?.parcel?.apn ? `APN ${site.parcel.apn}` : '',
712
+ site?.zone ? `ZONE ${site.zone}` : '',
713
+ ].filter(Boolean)
714
+ const coverageLine =
715
+ coverage.buildingSqFt > 0
716
+ ? [
717
+ `BUILDING COVERAGE ${formatSqFt(coverage.buildingCoverageSqFt)} (${formatCoveragePercent(coverage.buildingCoverageRatio)})`,
718
+ `IMPERVIOUS ${formatSqFt(coverage.imperviousSqFt)} (${formatCoveragePercent(coverage.imperviousRatio)})`,
719
+ ]
720
+ : []
721
+ ;[lotLine, coverageLine].forEach((bits, i) => {
722
+ if (bits.length === 0) return
723
+ primitives.push({
724
+ kind: 'text',
725
+ x: (lotBounds.minX + lotBounds.maxX) / 2,
726
+ y: lotBounds.maxY + LABEL_SIZE * (1.6 + i * 1.25),
727
+ text: bits.join(' · '),
728
+ fontSize: LABEL_SIZE * (i === 0 ? 1 : 0.85),
729
+ fill: INK,
730
+ fontWeight: 600,
731
+ textAnchor: 'middle',
732
+ dominantBaseline: 'hanging',
733
+ upright: true,
734
+ metadata: { sitePlan: i === 0 ? 'lot-label' : 'coverage-label' },
735
+ })
736
+ })
737
+
738
+ const combined = footprintBounds
739
+ ? {
740
+ minX: Math.min(lotBounds.minX, footprintBounds.minX),
741
+ minY: Math.min(lotBounds.minY, footprintBounds.minY),
742
+ maxX: Math.max(lotBounds.maxX, footprintBounds.maxX),
743
+ maxY: Math.max(lotBounds.maxY, footprintBounds.maxY),
744
+ }
745
+ : lotBounds
746
+
747
+ // Plugin kinds that live in site metres (utilities …) — see contributors.ts.
748
+ primitives.push(...sitePlanContributions(scene))
749
+
750
+ return {
751
+ primitives,
752
+ warnings,
753
+ bounds: padBounds(combined, Math.max(2, arrowSize * 1.8)),
754
+ meta: {
755
+ site,
756
+ frontEdge,
757
+ lot,
758
+ envelope,
759
+ envelopeFrontEdge: envelope.length >= 3 ? envelopeFrontEdge(lot, frontEdge, envelope) : null,
760
+ sightTriangles,
761
+ footprintLoops,
762
+ footprintBounds,
763
+ yards,
764
+ buildingId: (building?.id as AnyNodeId) ?? null,
765
+ coverage,
766
+ },
767
+ }
768
+ }