@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,157 @@
1
+ /**
2
+ * The street-facing lot edge from the mapped roads. Pure: no store, no
3
+ * network.
4
+ *
5
+ * For every lot edge, score how well a road fronts it:
6
+ * 1. PARALLELISM — the edge direction must be within `FRONT_EDGE_PARALLEL_DEG`
7
+ * of the nearest road segment (streets run ALONG a frontage; a road
8
+ * perpendicular to an edge is rejected).
9
+ * 2. OUTSIDE — the nearest road point must sit on the OUTWARD side of the
10
+ * edge: the street is off the lot, not a driveway cutting through it.
11
+ * 3. DISTANCE — among edges that pass, the smallest edge-midpoint-to-road
12
+ * distance wins (the closest fronting street); edges within
13
+ * `FRONT_EDGE_DISTANCE_TIE_M` of each other are a tie, and the LONGER
14
+ * edge takes it — a sliver left by a curb return or a split frontage
15
+ * never beats the frontage it sits beside.
16
+ * CORNER AND THROUGH LOTS: when the address's street matches a road, edges
17
+ * fronting THAT road are preferred over a merely-closer other street — the
18
+ * addressed street is the true front. The match keeps the street type and
19
+ * the directional (street-name.ts, USPS Pub. 28): "NW 34th St" is not
20
+ * "NW 34th Terrace".
21
+ *
22
+ * Coordinates are the site plan frame (metres, x east, y south), the frame
23
+ * the lot polygon and the OSM roads share.
24
+ */
25
+ import { edgeLength, outwardNormal } from '@pascal-app/core'
26
+ import type { Pt } from './geometry'
27
+ import { parseStreetName, sameStreet } from './street-name'
28
+
29
+ export interface RoadCenterline {
30
+ name?: string
31
+ centerline: readonly Pt[]
32
+ }
33
+
34
+ export interface FrontEdgeMatch {
35
+ /** Lot edge index: the edge runs from `points[index]` to `points[index + 1]`. */
36
+ index: number
37
+ /** Edge midpoint to the road centerline, metres. */
38
+ distance: number
39
+ /** The fronting road's name ('' when OSM has none). */
40
+ name: string
41
+ /** True when the road is the address's street — name, type and directional (corner / through-lot rule). */
42
+ named: boolean
43
+ /** Every lot edge a street runs along (parallel, outside, within 25 m): a corner lot lists two or more. */
44
+ streetEdges?: number[]
45
+ /** The street each of those edges runs along (the nearest road's name), by edge index; unnamed roads are left out. */
46
+ streetNames?: Record<string, string>
47
+ }
48
+
49
+ export const FRONT_EDGE_PARALLEL_DEG = 30
50
+ export const FRONT_EDGE_DISTANCE_TIE_M = 1.5
51
+
52
+ /** `next` beats `best` when it is clearly nearer, or as near and longer. */
53
+ function better(
54
+ next: FrontEdgeMatch & { length: number },
55
+ best: (FrontEdgeMatch & { length: number }) | null,
56
+ ): boolean {
57
+ if (!best) return true
58
+ if (next.distance < best.distance - FRONT_EDGE_DISTANCE_TIE_M) return true
59
+ if (next.distance > best.distance + FRONT_EDGE_DISTANCE_TIE_M) return false
60
+ return next.length > best.length
61
+ }
62
+
63
+ /**
64
+ * Core street name: the house number, the directionals and the street type
65
+ * dropped — "2600 Castro Way" ↔ OSM "Castro Way" ↔ "S Castro"; OSM spells
66
+ * the quadrant out ("Northeast 109th Street" ↔ "NE 109th St"). The core
67
+ * alone does not say two lines are one street: `sameStreet` also compares
68
+ * the type and the directional.
69
+ */
70
+ export function streetCore(s: string | null | undefined): string {
71
+ return parseStreetName(s).name
72
+ }
73
+
74
+ /** The winning lot-edge index with its road, or null (keep the current / fallback edge). */
75
+ export function detectFrontEdgeFromRoads(
76
+ lot: readonly Pt[],
77
+ roads: readonly RoadCenterline[],
78
+ addressStreet?: string | null,
79
+ ): FrontEdgeMatch | null {
80
+ const n = lot.length
81
+ if (n < 3 || roads.length === 0) return null
82
+ const sinLimit = Math.sin((FRONT_EDGE_PARALLEL_DEG * Math.PI) / 180)
83
+ const want = parseStreetName(addressStreet)
84
+ let best: (FrontEdgeMatch & { length: number }) | null = null
85
+ let bestNamed: (FrontEdgeMatch & { length: number }) | null = null
86
+ const streetEdges: number[] = []
87
+ const streetNames: Record<string, string> = {}
88
+
89
+ for (let i = 0; i < n; i++) {
90
+ const a = lot[i] as Pt
91
+ const b = lot[(i + 1) % n] as Pt
92
+ const len = edgeLength(lot, i)
93
+ if (len < 0.05) continue
94
+ const mid: Pt = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2]
95
+ const ex = (b[0] - a[0]) / len
96
+ const ey = (b[1] - a[1]) / len
97
+ const [nx, ny] = outwardNormal(lot, i)
98
+
99
+ // nearest road segment to this edge's midpoint, across every centerline
100
+ let near = Number.POSITIVE_INFINITY
101
+ let nearPt: Pt | null = null
102
+ let nearDir: Pt | null = null
103
+ let nearName = ''
104
+ for (const road of roads) {
105
+ const cl = road.centerline
106
+ for (let k = 0; k + 1 < cl.length; k++) {
107
+ const s0 = cl[k] as Pt
108
+ const s1 = cl[k + 1] as Pt
109
+ const dx = s1[0] - s0[0]
110
+ const dy = s1[1] - s0[1]
111
+ const dd = dx * dx + dy * dy
112
+ if (dd < 1e-12) continue
113
+ const t = Math.max(0, Math.min(1, ((mid[0] - s0[0]) * dx + (mid[1] - s0[1]) * dy) / dd))
114
+ const foot: Pt = [s0[0] + dx * t, s0[1] + dy * t]
115
+ const d = Math.hypot(mid[0] - foot[0], mid[1] - foot[1])
116
+ if (d < near) {
117
+ near = d
118
+ nearPt = foot
119
+ const sl = Math.sqrt(dd)
120
+ nearDir = [dx / sl, dy / sl]
121
+ nearName = road.name ?? ''
122
+ }
123
+ }
124
+ }
125
+ if (!nearPt || !nearDir) continue
126
+ // (1) parallel: |sin(angle between the edge and the road)| small
127
+ const sinAng = Math.abs(ex * nearDir[1] - ey * nearDir[0])
128
+ if (sinAng > sinLimit) continue
129
+ // (2) outside: the road lies on the outward side of the edge
130
+ if ((nearPt[0] - mid[0]) * nx + (nearPt[1] - mid[1]) * ny <= 0) continue
131
+
132
+ // a street runs along this edge (a corner lot collects two or more)
133
+ if (near <= 25 && len >= 3) {
134
+ streetEdges.push(i)
135
+ if (nearName) streetNames[String(i)] = nearName
136
+ }
137
+ const match = { index: i, distance: near, name: nearName, named: false, length: len }
138
+ if (better(match, best)) best = match
139
+ if (sameStreet(want, nearName) && better(match, bestNamed)) {
140
+ bestNamed = { ...match, named: true }
141
+ }
142
+ }
143
+ const pick = bestNamed ?? best
144
+ return pick
145
+ ? {
146
+ index: pick.index,
147
+ distance: pick.distance,
148
+ name: pick.name,
149
+ named: pick.named,
150
+ streetEdges: streetEdges.includes(pick.index) ? streetEdges : [pick.index, ...streetEdges],
151
+ streetNames:
152
+ pick.name && !streetNames[String(pick.index)]
153
+ ? { ...streetNames, [String(pick.index)]: pick.name }
154
+ : streetNames,
155
+ }
156
+ : null
157
+ }
@@ -0,0 +1,269 @@
1
+ import { describe, expect, it } from 'bun:test'
2
+ import {
3
+ classifyEdges,
4
+ edgeHeadingDeg,
5
+ mostNorthFacingEdge,
6
+ outwardNormal,
7
+ rayToPolygon,
8
+ resolveFrontEdge,
9
+ setbackEnvelope,
10
+ setbackForRole,
11
+ sightTriangle,
12
+ streetCorners,
13
+ } from '@pascal-app/core'
14
+ import {
15
+ castYardDimensions,
16
+ formatFeetInches,
17
+ METRES_PER_FOOT,
18
+ type Pt,
19
+ polygonArea,
20
+ } from './geometry'
21
+
22
+ /**
23
+ * 30 m (east-west) × 20 m (north-south) lot, origin at the north-west corner,
24
+ * wound clockwise in the plan frame (x east, y south).
25
+ *
26
+ * edge 0: north side (top, faces north)
27
+ * edge 1: east side
28
+ * edge 2: south side
29
+ * edge 3: west side
30
+ */
31
+ const LOT: Pt[] = [
32
+ [0, 0],
33
+ [30, 0],
34
+ [30, 20],
35
+ [0, 20],
36
+ ]
37
+
38
+ describe('outwardNormal', () => {
39
+ it('points out of the ring for every edge regardless of winding', () => {
40
+ const near = (i: number, x: number, y: number) => {
41
+ const n = outwardNormal(LOT, i)
42
+ expect(n[0]).toBeCloseTo(x, 9)
43
+ expect(n[1]).toBeCloseTo(y, 9)
44
+ }
45
+ near(0, 0, -1) // north edge faces north (−y)
46
+ near(1, 1, 0) // east
47
+ near(2, 0, 1) // south
48
+ near(3, -1, 0) // west
49
+
50
+ // Reversing the winding relabels the edges but normals still point out:
51
+ // reversed edge 0 runs [0,20] → [30,20], the south side.
52
+ const reversed = [...LOT].reverse()
53
+ const n0 = outwardNormal(reversed, 0)
54
+ expect(n0[0]).toBeCloseTo(0, 9)
55
+ expect(n0[1]).toBeCloseTo(1, 9)
56
+ })
57
+ })
58
+
59
+ describe('edge headings + front edge', () => {
60
+ it('reports compass headings clockwise from north', () => {
61
+ expect(edgeHeadingDeg(LOT, 0)).toBeCloseTo(0, 6)
62
+ expect(edgeHeadingDeg(LOT, 1)).toBeCloseTo(90, 6)
63
+ expect(edgeHeadingDeg(LOT, 2)).toBeCloseTo(180, 6)
64
+ expect(edgeHeadingDeg(LOT, 3)).toBeCloseTo(270, 6)
65
+ })
66
+
67
+ it('honours northRotation', () => {
68
+ // Rotate true north 90° clockwise: the plan-up edge now faces west.
69
+ expect(edgeHeadingDeg(LOT, 0, Math.PI / 2)).toBeCloseTo(270, 6)
70
+ })
71
+
72
+ it('picks the most north-facing edge as the fallback front', () => {
73
+ expect(mostNorthFacingEdge(LOT)).toBe(0)
74
+ expect(resolveFrontEdge(LOT, undefined)).toBe(0)
75
+ expect(resolveFrontEdge(LOT, 2)).toBe(2)
76
+ // Out-of-range indexes fall back rather than throw.
77
+ expect(resolveFrontEdge(LOT, 9)).toBe(0)
78
+ })
79
+ })
80
+
81
+ describe('classifyEdges', () => {
82
+ it('assigns front / rear and splits the remaining edges left and right', () => {
83
+ const roles = classifyEdges(LOT, 0)
84
+ expect(roles[0]).toBe('front')
85
+ expect(roles[2]).toBe('rear')
86
+ expect(new Set([roles[1], roles[3]])).toEqual(new Set(['left', 'right']))
87
+ })
88
+
89
+ it('resolves per-role setbacks with left/right overriding side', () => {
90
+ const s = { front: 7.6, side: 1.5, rear: 4.6, right: 3 }
91
+ expect(setbackForRole(s, 'front')).toBe(7.6)
92
+ expect(setbackForRole(s, 'rear')).toBe(4.6)
93
+ expect(setbackForRole(s, 'left')).toBe(1.5)
94
+ expect(setbackForRole(s, 'right')).toBe(3)
95
+ })
96
+ })
97
+
98
+ describe('setbackEnvelope', () => {
99
+ it('a corner lot: the second street side takes the street setback and the sight triangle clips the corner', () => {
100
+ // a 30 × 20 lot; edge 0 (north, y = 0) is the front, edge 1 (east, x = 30) the other street
101
+ const roles = classifyEdges(LOT, 0, [0, 1])
102
+ expect(roles[1]).toBe('street')
103
+ expect(roles[2]).toBe('rear')
104
+ expect(setbackForRole({ front: 7.5, side: 2, rear: 5 }, 'street')).toBe(7.5)
105
+ expect(setbackForRole({ front: 7.5, side: 2, rear: 5, streetSide: 4 }, 'street')).toBe(4)
106
+ const plain = setbackEnvelope(LOT, { front: 7.5, side: 2, rear: 5, streetSide: 4 }, 0, {
107
+ streetEdges: [0, 1],
108
+ })
109
+ const xs = plain.map((p) => p[0])
110
+ expect(Math.max(...xs)).toBeCloseTo(26, 6) // the east street side in 4, not the 2 ft side yard
111
+ // the 25 ft (7.62 m) sight triangle at the NE corner: its hypotenuse from (22.38, 0) to (30, 7.62) keeps the envelope out
112
+ const clipped = setbackEnvelope(LOT, { front: 7.5, side: 2, rear: 5, streetSide: 4 }, 0, {
113
+ streetEdges: [0, 1],
114
+ sightTriangleM: 7.62,
115
+ })
116
+ expect(clipped.length).toBeGreaterThanOrEqual(4)
117
+ for (const v of clipped) {
118
+ // outside the triangle: x + y ≥ 30 − 7.62 ... the hypotenuse is x − y = 22.38 ... points with x − y > 22.38 are inside it
119
+ expect(v[0] - v[1]).toBeLessThanOrEqual(22.38 + 0.05)
120
+ }
121
+ const tri = sightTriangle(LOT, 0, 1, 7.62)
122
+ expect(tri).not.toBeNull()
123
+ expect(tri!.corner[0]).toBeCloseTo(30, 6)
124
+ expect(tri!.corner[1]).toBeCloseTo(0, 6)
125
+ expect(streetCorners(LOT, [0, 1])).toEqual([[0, 1]])
126
+ })
127
+
128
+ it('a cul-de-sac corner (a run of short arc edges) is offset as one chord — no vertex runs away', () => {
129
+ const cape: [number, number][] = [
130
+ [-2, 0],
131
+ [-2, 30.1],
132
+ [-40, 32.7],
133
+ [-40, -5.6],
134
+ [-10.1, -7.6],
135
+ [-8.9, -7.6],
136
+ [-7.7, -7.3],
137
+ [-6.5, -6.9],
138
+ [-5.4, -6.3],
139
+ [-4.4, -5.5],
140
+ [-3.6, -4.6],
141
+ [-2.9, -3.6],
142
+ [-2.4, -2.4],
143
+ [-2.1, -1.2],
144
+ ]
145
+ const env = setbackEnvelope(cape, { front: 6.096, side: 1.524, rear: 4.572 }, 0)
146
+ expect(env.length).toBeGreaterThanOrEqual(6)
147
+ // every envelope vertex lies inside the lot, at least its setback from every lot line
148
+ const segDist = (p: readonly number[], a: readonly number[], b: readonly number[]) => {
149
+ const abx = b[0]! - a[0]!
150
+ const aby = b[1]! - a[1]!
151
+ const t = Math.max(
152
+ 0,
153
+ Math.min(1, ((p[0]! - a[0]!) * abx + (p[1]! - a[1]!) * aby) / (abx * abx + aby * aby)),
154
+ )
155
+ return Math.hypot(p[0]! - (a[0]! + abx * t), p[1]! - (a[1]! + aby * t))
156
+ }
157
+ // the curb return (edges 4–13) is street frontage: it takes the FRONT setback, never a side yard
158
+ const frontLike = new Set([0, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13])
159
+ for (const v of env) {
160
+ for (let i = 0; i < cape.length; i++) {
161
+ const want = frontLike.has(i) ? 6.096 : 1.524
162
+ expect(segDist(v, cape[i]!, cape[(i + 1) % cape.length]!)).toBeGreaterThan(want - 0.06)
163
+ }
164
+ }
165
+ // the front line (x = −2, the east side) offset 6.1 m in: two vertices on x ≈ −8.1
166
+ const onFront = env.filter((v) => Math.abs(v[0] - (-2 - 6.096)) < 0.05)
167
+ expect(onFront.length).toBeGreaterThanOrEqual(2)
168
+ // the return's offset is a curve of its own (a 6.1 m buffer round the return's points), not a chord: the
169
+ // envelope turns through more than one vertex between the front offset and the south offset
170
+ const turning = env.filter((v) => v[0] < -8.2 + 0.05 && v[1] < 2 && v[1] > -6.2)
171
+ expect(turning.length).toBeGreaterThanOrEqual(2)
172
+ })
173
+
174
+ it('offsets each edge inward by its own setback', () => {
175
+ const env = setbackEnvelope(LOT, { front: 7.5, side: 2, rear: 5 }, 0)
176
+ expect(env).toHaveLength(4)
177
+ const xs = env.map((p) => p[0]).sort((a, b) => a - b)
178
+ const ys = env.map((p) => p[1]).sort((a, b) => a - b)
179
+ // North (front) edge in 7.5, south (rear) in 5, both sides in 2.
180
+ expect(xs[0]).toBeCloseTo(2, 9)
181
+ expect(xs[3]).toBeCloseTo(28, 9)
182
+ expect(ys[0]).toBeCloseTo(7.5, 9)
183
+ expect(ys[3]).toBeCloseTo(15, 9)
184
+ expect(polygonArea(env)).toBeCloseTo(26 * 7.5, 6)
185
+ })
186
+
187
+ it('applies left/right overrides to the correct sides', () => {
188
+ const env = setbackEnvelope(LOT, { front: 0, side: 0, rear: 0, left: 4, right: 1 }, 0)
189
+ const xs = env.map((p) => p[0]).sort((a, b) => a - b)
190
+ // Front edge runs [0,0] → [30,0]; "left" is behind that direction (west).
191
+ expect(xs[0]).toBeCloseTo(4, 9)
192
+ expect(xs[3]).toBeCloseTo(29, 9)
193
+ })
194
+
195
+ it('rejects setbacks that consume the lot instead of drawing a bow tie', () => {
196
+ expect(setbackEnvelope(LOT, { front: 12, side: 2, rear: 12 }, 0)).toEqual([])
197
+ expect(setbackEnvelope(LOT, { front: 0, side: 0, rear: 0 }, 0)).toEqual([])
198
+ expect(setbackEnvelope([[0, 0]], { front: 1, side: 1, rear: 1 }, 0)).toEqual([])
199
+ })
200
+ it('a frontage the county drew as two nearly straight lines takes one front yard, not an arc', () => {
201
+ // 1054 Rivera Dr, Sacramento as the parcel fabric draws it: the front (north, y ≈ −15.4) split
202
+ // at a kink of under 2° into edges 3 and 4; the street lookup failed, so only edge 4 came back
203
+ // as frontage
204
+ const rivera: Pt[] = [
205
+ [12.39, -15.35],
206
+ [12.77, 14.38],
207
+ [-14.32, 15.47],
208
+ [-14.34, -15.26],
209
+ [-1.53, -15.56],
210
+ ]
211
+ const env = setbackEnvelope(rivera, { front: 6.096, side: 1.524, rear: 4.572 }, 4, {
212
+ streetEdges: [4],
213
+ })
214
+ // straight across the whole frontage (the kink's own corner, no curve round it): five corners at most
215
+ expect(env.length).toBeLessThanOrEqual(5)
216
+ const segDist = (p: readonly number[], a: readonly number[], b: readonly number[]) => {
217
+ const abx = b[0]! - a[0]!
218
+ const aby = b[1]! - a[1]!
219
+ const t = Math.max(
220
+ 0,
221
+ Math.min(1, ((p[0]! - a[0]!) * abx + (p[1]! - a[1]!) * aby) / (abx * abx + aby * aby)),
222
+ )
223
+ return Math.hypot(p[0]! - (a[0]! + abx * t), p[1]! - (a[1]! + aby * t))
224
+ }
225
+ // both halves of the frontage keep the front yard
226
+ for (const v of env)
227
+ for (const i of [3, 4])
228
+ expect(segDist(v, rivera[i]!, rivera[(i + 1) % rivera.length]!)).toBeGreaterThan(
229
+ 6.096 - 0.06,
230
+ )
231
+ })
232
+ })
233
+
234
+ describe('rayToPolygon + castYardDimensions', () => {
235
+ it('finds the nearest boundary crossing along a ray', () => {
236
+ const hit = rayToPolygon(LOT, [15, 10], 0, -1)
237
+ expect(hit?.distance).toBeCloseTo(10, 9)
238
+ expect(hit?.point[1]).toBeCloseTo(0, 9)
239
+ // A ray from outside pointing away never hits.
240
+ expect(rayToPolygon(LOT, [15, -5], 0, -1)).toBeNull()
241
+ })
242
+
243
+ it('casts four yard dimensions from the footprint bbox edge midpoints', () => {
244
+ const yards = castYardDimensions(LOT, { minX: 10, minY: 6, maxX: 20, maxY: 14 })
245
+ expect(yards.map((y) => y.side).sort()).toEqual(['east', 'north', 'south', 'west'])
246
+ const by = Object.fromEntries(yards.map((y) => [y.side, y]))
247
+ expect(by.north?.distance).toBeCloseTo(6, 9)
248
+ expect(by.south?.distance).toBeCloseTo(6, 9)
249
+ expect(by.west?.distance).toBeCloseTo(10, 9)
250
+ expect(by.east?.distance).toBeCloseTo(10, 9)
251
+ // Dimensions start at the bbox midpoints, not the corners.
252
+ expect(by.north?.from).toEqual([15, 6])
253
+ expect(by.west?.from).toEqual([10, 10])
254
+ })
255
+
256
+ it('omits a side whose ray misses the lot rather than faking a number', () => {
257
+ // Footprint entirely north of (above) the lot: nothing casts north.
258
+ const yards = castYardDimensions(LOT, { minX: 10, minY: -30, maxX: 20, maxY: -25 })
259
+ expect(yards.map((y) => y.side)).not.toContain('north')
260
+ })
261
+ })
262
+
263
+ describe('formatFeetInches', () => {
264
+ it('renders metres as feet-and-inches', () => {
265
+ expect(formatFeetInches(7.62)).toBe(`25'-0"`)
266
+ expect(formatFeetInches(METRES_PER_FOOT * 24.5)).toBe(`24'-6"`)
267
+ expect(formatFeetInches(0)).toBe(`0'-0"`)
268
+ })
269
+ })
@@ -0,0 +1,142 @@
1
+ import { rayToPolygon, type YardDimension, type YardSide } from '@pascal-app/core'
2
+ /**
3
+ * Site-plan geometry — pure functions, no store, no React.
4
+ *
5
+ * All coordinates are SITE metres: origin = the geocoded point, x → east,
6
+ * y → south (the plan frame the site polygon is stored in). Consumers
7
+ * (2D editor, sheets) turn these into SVG through the existing renderer.
8
+ */
9
+
10
+ export type Pt = readonly [number, number]
11
+
12
+ export interface Bounds {
13
+ minX: number
14
+ minY: number
15
+ maxX: number
16
+ maxY: number
17
+ }
18
+
19
+ const EPS = 1e-9
20
+
21
+ export function polygonBounds(points: readonly Pt[]): Bounds {
22
+ let minX = Number.POSITIVE_INFINITY
23
+ let minY = Number.POSITIVE_INFINITY
24
+ let maxX = Number.NEGATIVE_INFINITY
25
+ let maxY = Number.NEGATIVE_INFINITY
26
+ for (const [x, y] of points) {
27
+ if (x < minX) minX = x
28
+ if (x > maxX) maxX = x
29
+ if (y < minY) minY = y
30
+ if (y > maxY) maxY = y
31
+ }
32
+ if (!Number.isFinite(minX)) return { minX: 0, minY: 0, maxX: 0, maxY: 0 }
33
+ return { minX, minY, maxX, maxY }
34
+ }
35
+
36
+ export function polygonArea(points: readonly Pt[]): number {
37
+ let a = 0
38
+ for (let i = 0, n = points.length; i < n; i++) {
39
+ const p = points[i] as Pt
40
+ const q = points[(i + 1) % n] as Pt
41
+ a += p[0] * q[1] - q[0] * p[1]
42
+ }
43
+ return Math.abs(a / 2)
44
+ }
45
+
46
+ export function polygonCentroid(points: readonly Pt[]): Pt {
47
+ let cx = 0
48
+ let cy = 0
49
+ let a = 0
50
+ for (let i = 0, n = points.length; i < n; i++) {
51
+ const p = points[i] as Pt
52
+ const q = points[(i + 1) % n] as Pt
53
+ const cross = p[0] * q[1] - q[0] * p[1]
54
+ a += cross
55
+ cx += (p[0] + q[0]) * cross
56
+ cy += (p[1] + q[1]) * cross
57
+ }
58
+ if (Math.abs(a) < EPS) {
59
+ // Degenerate ring — fall back to the vertex average.
60
+ const n = Math.max(1, points.length)
61
+ let sx = 0
62
+ let sy = 0
63
+ for (const [x, y] of points) {
64
+ sx += x
65
+ sy += y
66
+ }
67
+ return [sx / n, sy / n]
68
+ }
69
+ a /= 2
70
+ return [cx / (6 * a), cy / (6 * a)]
71
+ }
72
+
73
+ const COMPASS = [
74
+ 'N',
75
+ 'NNE',
76
+ 'NE',
77
+ 'ENE',
78
+ 'E',
79
+ 'ESE',
80
+ 'SE',
81
+ 'SSE',
82
+ 'S',
83
+ 'SSW',
84
+ 'SW',
85
+ 'WSW',
86
+ 'W',
87
+ 'WNW',
88
+ 'NW',
89
+ 'NNW',
90
+ ]
91
+
92
+ export function compassLabel(headingDeg: number): string {
93
+ const idx = Math.round((((headingDeg % 360) + 360) % 360) / 22.5) % 16
94
+ return COMPASS[idx] as string
95
+ }
96
+
97
+ /**
98
+ * The four yard dimensions: from each footprint bbox edge midpoint, straight
99
+ * out to the lot line. Sides whose ray misses the lot line (footprint outside
100
+ * the lot) are omitted rather than faked.
101
+ */
102
+ export function castYardDimensions(lot: readonly Pt[], footprint: Bounds): YardDimension[] {
103
+ if (lot.length < 3) return []
104
+ const midX = (footprint.minX + footprint.maxX) / 2
105
+ const midY = (footprint.minY + footprint.maxY) / 2
106
+ const casts: { side: YardSide; from: Pt; dx: number; dy: number }[] = [
107
+ { side: 'north', from: [midX, footprint.minY], dx: 0, dy: -1 },
108
+ { side: 'south', from: [midX, footprint.maxY], dx: 0, dy: 1 },
109
+ { side: 'west', from: [footprint.minX, midY], dx: -1, dy: 0 },
110
+ { side: 'east', from: [footprint.maxX, midY], dx: 1, dy: 0 },
111
+ ]
112
+ const out: YardDimension[] = []
113
+ for (const c of casts) {
114
+ const hit = rayToPolygon(lot, c.from, c.dx, c.dy)
115
+ if (!hit) continue
116
+ out.push({ side: c.side, from: c.from, to: hit.point, distance: hit.distance })
117
+ }
118
+ return out
119
+ }
120
+
121
+ /** Even-odd point-in-polygon. */
122
+ export function pointInPolygon(points: readonly Pt[], x: number, y: number): boolean {
123
+ let inside = false
124
+ for (let i = 0, j = points.length - 1; i < points.length; j = i++) {
125
+ const a = points[i] as Pt
126
+ const b = points[j] as Pt
127
+ if (a[1] > y !== b[1] > y && x < ((b[0] - a[0]) * (y - a[1])) / (b[1] - a[1]) + a[0]) {
128
+ inside = !inside
129
+ }
130
+ }
131
+ return inside
132
+ }
133
+
134
+ export const METRES_PER_FOOT = 0.3048
135
+
136
+ /** `24'-6"` — the notation a US site plan uses for yard dimensions. */
137
+ export function formatFeetInches(metres: number): string {
138
+ const totalInches = Math.round((metres / METRES_PER_FOOT) * 12)
139
+ const feet = Math.floor(totalInches / 12)
140
+ const inches = totalInches % 12
141
+ return inches === 0 ? `${feet}'-0"` : `${feet}'-${inches}"`
142
+ }
@@ -0,0 +1,55 @@
1
+ export {
2
+ buildSitePlanDrawing,
3
+ describeSiteEdges,
4
+ type SitePlanDrawing,
5
+ type SitePlanEdge,
6
+ } from './build-site-plan-drawing'
7
+ export {
8
+ hasSitePlanContributors,
9
+ registerSitePlanContributor,
10
+ type SitePlanContributor,
11
+ type SitePlanServicePoint,
12
+ type SitePlanServiceRole,
13
+ type SitePlanServices,
14
+ sitePlanContributions,
15
+ subscribeSitePlanContributors,
16
+ } from './contributors'
17
+ export {
18
+ computeSiteCoverage,
19
+ formatCoveragePercent,
20
+ formatSqFt,
21
+ type ImperviousRow,
22
+ type SiteCoverage,
23
+ } from './coverage'
24
+ export {
25
+ FloorplanDrawingTypeSwitch,
26
+ isSitePlanAvailable,
27
+ useSitePlanAvailable,
28
+ } from './drawing-type-switch'
29
+ export {
30
+ detectFrontEdgeFromRoads,
31
+ FRONT_EDGE_PARALLEL_DEG,
32
+ type FrontEdgeMatch,
33
+ type RoadCenterline,
34
+ streetCore,
35
+ } from './front-edge'
36
+ export {
37
+ type Bounds,
38
+ castYardDimensions,
39
+ compassLabel,
40
+ formatFeetInches,
41
+ METRES_PER_FOOT,
42
+ type Pt,
43
+ pointInPolygon,
44
+ polygonArea,
45
+ polygonBounds,
46
+ polygonCentroid,
47
+ } from './geometry'
48
+ export {
49
+ formatStreetName,
50
+ serviceEntranceOf,
51
+ streetEdgeNames,
52
+ UNNAMED_STREET,
53
+ } from './site-annotations'
54
+ export { flatworkKindOf, type OutdoorPart } from './site-parts'
55
+ export { FloorplanSitePlanLayer } from './site-plan-layer'