@pascal-app/editor 1.0.1 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (498) hide show
  1. package/package.json +16 -9
  2. package/src/components/editor/action-menu-button.tsx +83 -0
  3. package/src/components/editor/bake-thumbnail.tsx +315 -4
  4. package/src/components/editor/capture-level-picker.tsx +72 -0
  5. package/src/components/editor/custom-camera-controls.tsx +221 -128
  6. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  7. package/src/components/editor/duplicate-node.ts +244 -0
  8. package/src/components/editor/editor-layout-mobile.tsx +37 -25
  9. package/src/components/editor/editor-layout-v2.tsx +131 -58
  10. package/src/components/editor/export-manager.tsx +34 -8
  11. package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
  12. package/src/components/editor/first-person/build-collider-world.ts +6 -2
  13. package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
  14. package/src/components/editor/first-person/mechanism-targets.ts +36 -0
  15. package/src/components/editor/first-person-controls.tsx +205 -2
  16. package/src/components/editor/floating-action-menu.tsx +63 -207
  17. package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
  18. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  19. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  20. package/src/components/editor/floor-region-controls.tsx +846 -0
  21. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  22. package/src/components/editor/floor-surface.test.ts +165 -0
  23. package/src/components/editor/floor-surface.ts +182 -0
  24. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  25. package/src/components/editor/floorplan-background-selection.ts +1 -3
  26. package/src/components/editor/floorplan-panel.tsx +536 -487
  27. package/src/components/editor/footprint-height-handle.tsx +204 -0
  28. package/src/components/editor/grid.tsx +31 -7
  29. package/src/components/editor/group-actions.ts +59 -11
  30. package/src/components/editor/group-floating-action-menu.tsx +19 -9
  31. package/src/components/editor/group-move-3d.ts +4 -6
  32. package/src/components/editor/group-rotate-handle.tsx +81 -35
  33. package/src/components/editor/group-selection-box-3d.tsx +25 -17
  34. package/src/components/editor/group-transform-shared.test.ts +126 -1
  35. package/src/components/editor/group-transform-shared.ts +79 -73
  36. package/src/components/editor/handles/handle-arrow.tsx +72 -2
  37. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  38. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  39. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  40. package/src/components/editor/handles/use-handle-drag.ts +272 -131
  41. package/src/components/editor/index.tsx +316 -176
  42. package/src/components/editor/join-walls-pill.tsx +44 -0
  43. package/src/components/editor/node-action-menu.tsx +70 -70
  44. package/src/components/editor/node-arrow-handles.tsx +167 -14
  45. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  46. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  47. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  48. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  49. package/src/components/editor/registry-action-contributions.tsx +132 -0
  50. package/src/components/editor/room-controls.tsx +847 -0
  51. package/src/components/editor/room-floor-highlight.tsx +128 -0
  52. package/src/components/editor/room-handles-2d.tsx +354 -0
  53. package/src/components/editor/room-handles.tsx +675 -0
  54. package/src/components/editor/room-highlight.tsx +225 -0
  55. package/src/components/editor/room-labels.test.ts +357 -0
  56. package/src/components/editor/room-labels.tsx +990 -0
  57. package/src/components/editor/room-transform-ghost.tsx +256 -0
  58. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  59. package/src/components/editor/selection-manager.tsx +451 -317
  60. package/src/components/editor/slab-hole-highlights.tsx +3 -0
  61. package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
  62. package/src/components/editor/snapshot-capture.test.ts +52 -4
  63. package/src/components/editor/snapshot-capture.ts +30 -5
  64. package/src/components/editor/thumbnail-generator.test.ts +308 -0
  65. package/src/components/editor/thumbnail-generator.tsx +1035 -229
  66. package/src/components/editor/use-floorplan-background-placement.ts +31 -11
  67. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  68. package/src/components/editor/views/host-view.tsx +61 -0
  69. package/src/components/editor/views/plugins-view.tsx +670 -0
  70. package/src/components/editor/views/scene-region.tsx +59 -0
  71. package/src/components/editor/views/use-editor-views.ts +57 -0
  72. package/src/components/editor/views/view-bar.tsx +220 -0
  73. package/src/components/editor/wall-justification.test.ts +140 -0
  74. package/src/components/editor/wall-move-side-handles.tsx +114 -163
  75. package/src/components/editor/wall-opening-highlights.tsx +2 -3
  76. package/src/components/editor/wall-push-arrow.tsx +92 -0
  77. package/src/components/editor/wall-region-handles.tsx +509 -0
  78. package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
  79. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  80. package/src/components/editor-2d/floorplan-group-move.tsx +17 -24
  81. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  82. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  83. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +33 -12
  84. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  85. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +242 -126
  86. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  87. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  88. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  89. package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +166 -0
  90. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  91. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +87 -8
  92. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +355 -245
  93. package/src/components/editor-2d/svg-paths.ts +12 -15
  94. package/src/components/procedural-items/inspector.tsx +40 -15
  95. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  96. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  97. package/src/components/systems/zone/zone-system.test.ts +47 -0
  98. package/src/components/systems/zone/zone-system.tsx +31 -66
  99. package/src/components/tools/__fixtures__/hosting.ts +10 -0
  100. package/src/components/tools/item/item-surface-placement.test.ts +3 -9
  101. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +3 -9
  102. package/src/components/tools/item/placement-history.test.tsx +474 -0
  103. package/src/components/tools/item/placement-strategies.ts +63 -47
  104. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +3 -3
  105. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  106. package/src/components/tools/item/use-draft-node.ts +207 -97
  107. package/src/components/tools/item/use-placement-coordinator.tsx +251 -111
  108. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  109. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  110. package/src/components/tools/registry/item-surface-move.test.ts +10 -2
  111. package/src/components/tools/registry/item-surface-move.ts +46 -24
  112. package/src/components/tools/registry/item-surface-resolver.test.ts +2 -2
  113. package/src/components/tools/registry/move-registry-node-tool.tsx +26 -10
  114. package/src/components/tools/select/box-select-tool.tsx +2 -12
  115. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  116. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  117. package/src/components/tools/select/select-candidates.test.ts +37 -4
  118. package/src/components/tools/select/select-candidates.ts +15 -11
  119. package/src/components/tools/shared/affordance-dispatch.test.ts +24 -0
  120. package/src/components/tools/shared/affordance-dispatch.ts +13 -0
  121. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  122. package/src/components/tools/shared/draft-measurement-label.tsx +38 -0
  123. package/src/components/tools/shared/pointer-support-cap.test.ts +27 -0
  124. package/src/components/tools/shared/pointer-support-cap.ts +17 -5
  125. package/src/components/tools/shared/surface-rejection.test.tsx +5 -4
  126. package/src/components/tools/shared/surface-rejection.tsx +14 -0
  127. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  128. package/src/components/tools/stair/stair-defaults.ts +0 -3
  129. package/src/components/tools/stair/stair-tool.tsx +90 -42
  130. package/src/components/tools/tool-manager.tsx +82 -23
  131. package/src/components/tools/wall/wall-drafting.test.ts +19 -4
  132. package/src/components/tools/zone/zone-tool.tsx +4 -12
  133. package/src/components/ui/action-menu/action-button.tsx +3 -0
  134. package/src/components/ui/action-menu/camera-actions.tsx +2 -1
  135. package/src/components/ui/action-menu/control-modes.tsx +2 -35
  136. package/src/components/ui/action-menu/measurement-control.tsx +3 -10
  137. package/src/components/ui/build-panel/build-panel.tsx +235 -0
  138. package/src/components/ui/command-palette/editor-commands.tsx +4 -30
  139. package/src/components/ui/command-palette/index.tsx +5 -24
  140. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  141. package/src/components/ui/controls/material-paint-panel.tsx +101 -91
  142. package/src/components/ui/controls/material-picker.tsx +8 -67
  143. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  144. package/src/components/ui/controls/select-control.tsx +65 -0
  145. package/src/components/ui/controls/slider-control.tsx +76 -31
  146. package/src/components/ui/floating-level-selector.tsx +9 -14
  147. package/src/components/ui/helpers/building-helper.tsx +4 -1
  148. package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
  149. package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
  150. package/src/components/ui/helpers/helper-manager.tsx +348 -19
  151. package/src/components/ui/helpers/item-helper.tsx +8 -0
  152. package/src/components/ui/helpers/registered-tool-helper.tsx +10 -33
  153. package/src/components/ui/icon-ref.tsx +45 -1
  154. package/src/components/ui/item-catalog/item-catalog.tsx +5 -30
  155. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  156. package/src/components/ui/node-icon-image.tsx +16 -0
  157. package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
  158. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  159. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  160. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  161. package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
  162. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  163. package/src/components/ui/panels/multi-field-value.ts +6 -3
  164. package/src/components/ui/panels/multi-height-mode.tsx +27 -21
  165. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  166. package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
  167. package/src/components/ui/panels/node-display.ts +8 -3
  168. package/src/components/ui/panels/panel-manager.tsx +103 -68
  169. package/src/components/ui/panels/panel-wrapper.tsx +78 -36
  170. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  171. package/src/components/ui/panels/parametric-inspector.tsx +7 -19
  172. package/src/components/ui/panels/reference-panel.tsx +2 -3
  173. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  174. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  175. package/src/components/ui/panels/room-name-options.ts +47 -0
  176. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  177. package/src/components/ui/panels/room-panel.tsx +725 -0
  178. package/src/components/ui/panels/shape-choice.tsx +58 -0
  179. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  180. package/src/components/ui/primitives/color-dot.tsx +36 -24
  181. package/src/components/ui/primitives/shortcut-token.tsx +4 -0
  182. package/src/components/ui/primitives/sidebar.tsx +2 -0
  183. package/src/components/ui/right-stack.test.ts +29 -0
  184. package/src/components/ui/right-stack.tsx +145 -0
  185. package/src/components/ui/sidebar/icon-rail.tsx +5 -0
  186. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
  187. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +615 -372
  188. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +7 -7
  189. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
  190. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  191. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  192. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  193. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  194. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  195. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  196. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  197. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  198. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  199. package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
  200. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  201. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  202. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  203. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  204. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  205. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  206. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  207. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  208. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  209. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  210. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
  211. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  212. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  213. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  214. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  215. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
  216. package/src/components/ui/sidebar/tab-bar.tsx +10 -0
  217. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  218. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  219. package/src/components/viewer/floorplan-preview-visibility.test.ts +35 -0
  220. package/src/components/viewer/floorplan-preview-visibility.ts +19 -0
  221. package/src/components/viewer/floorplan-preview.tsx +6 -18
  222. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  223. package/src/components/viewer/viewer-rooms.tsx +175 -0
  224. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  225. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  226. package/src/components/viewer/viewer-stage.tsx +2 -2
  227. package/src/components/viewer-zone-system.tsx +9 -9
  228. package/src/hooks/use-auto-save.integration.test.tsx +311 -0
  229. package/src/hooks/use-auto-save.ts +55 -14
  230. package/src/hooks/use-ceiling-events.test.ts +1 -0
  231. package/src/hooks/use-ceiling-events.ts +2 -0
  232. package/src/hooks/use-grid-events.test.ts +2 -0
  233. package/src/hooks/use-grid-events.ts +16 -11
  234. package/src/hooks/use-keyboard.test.ts +182 -0
  235. package/src/hooks/use-keyboard.ts +122 -91
  236. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  237. package/src/hooks/use-open-wall-ends.ts +238 -0
  238. package/src/hooks/use-placement-notice.test.ts +48 -0
  239. package/src/hooks/use-placement-notice.ts +42 -0
  240. package/src/hooks/use-selected-room.ts +143 -0
  241. package/src/index.tsx +306 -39
  242. package/src/lib/catalog-panel-model.ts +39 -0
  243. package/src/lib/catalog-selection.test.ts +27 -0
  244. package/src/lib/catalog-selection.ts +8 -0
  245. package/src/lib/ceiling-edit-session.test.ts +321 -0
  246. package/src/lib/ceiling-edit-session.ts +206 -0
  247. package/src/lib/ceiling-region-session.test.ts +179 -0
  248. package/src/lib/contextual-help.test.ts +27 -1
  249. package/src/lib/contextual-help.ts +16 -9
  250. package/src/lib/continuation.test.ts +19 -1
  251. package/src/lib/continuation.ts +25 -4
  252. package/src/lib/door-interaction.ts +14 -0
  253. package/src/lib/editor-level-display.test.ts +96 -0
  254. package/src/lib/editor-level-display.ts +48 -0
  255. package/src/lib/editor-views.ts +144 -0
  256. package/src/lib/export-geometry-wait.test.ts +90 -0
  257. package/src/lib/floor-edit-session.ts +153 -0
  258. package/src/lib/floor-footprints.test.ts +563 -0
  259. package/src/lib/floor-footprints.ts +395 -0
  260. package/src/lib/floor-foundation-support.test.ts +283 -0
  261. package/src/lib/floor-opening-draft.ts +218 -0
  262. package/src/lib/floor-openings.test.ts +216 -0
  263. package/src/lib/floor-region-geometry.ts +191 -0
  264. package/src/lib/floor-region-session.test.ts +485 -0
  265. package/src/lib/floor-region-session.ts +589 -0
  266. package/src/lib/floor-region-snap.ts +101 -0
  267. package/src/lib/floor-step-hover.test.ts +32 -0
  268. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  269. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  270. package/src/lib/floorplan/dimension-drive.ts +479 -0
  271. package/src/lib/floorplan/floorplan-export.test.ts +15 -9
  272. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  273. package/src/lib/floorplan/floorplan-extension.ts +48 -0
  274. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  275. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  276. package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
  277. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
  278. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
  279. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  280. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  281. package/src/lib/floorplan/geometry.ts +13 -6
  282. package/src/lib/floorplan/index.ts +0 -9
  283. package/src/lib/floorplan/items.ts +5 -2
  284. package/src/lib/floorplan/marks.test.ts +213 -0
  285. package/src/lib/floorplan/marks.ts +238 -0
  286. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  287. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  288. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  289. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  290. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  291. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  292. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  293. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  294. package/src/lib/floorplan/schedules.test.ts +189 -0
  295. package/src/lib/floorplan/schedules.ts +339 -0
  296. package/src/lib/floorplan/sheet-export.test.ts +100 -0
  297. package/src/lib/floorplan/sheet-export.ts +318 -0
  298. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
  299. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  300. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  301. package/src/lib/floorplan/site-plan/coverage.ts +232 -0
  302. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  303. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  304. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  305. package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
  306. package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
  307. package/src/lib/floorplan/site-plan/geometry.ts +557 -0
  308. package/src/lib/floorplan/site-plan/index.ts +71 -0
  309. package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
  310. package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
  311. package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
  312. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  313. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  314. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  315. package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
  316. package/src/lib/floorplan/types.ts +1 -27
  317. package/src/lib/fresh-planar-placement.test.ts +36 -0
  318. package/src/lib/fresh-planar-placement.ts +191 -36
  319. package/src/lib/geometry-script/author.ts +152 -0
  320. package/src/lib/geometry-script/client.ts +152 -0
  321. package/src/lib/geometry-script/protocol.ts +12 -0
  322. package/src/lib/gesture-lifecycle.test.ts +595 -0
  323. package/src/lib/gesture-lifecycle.ts +290 -0
  324. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  325. package/src/lib/glb-export.test.ts +544 -16
  326. package/src/lib/glb-export.ts +256 -125
  327. package/src/lib/grid-frame.test.ts +19 -0
  328. package/src/lib/grid-frame.ts +18 -0
  329. package/src/lib/history.test.ts +6 -0
  330. package/src/lib/history.ts +7 -0
  331. package/src/lib/hud-title.ts +109 -0
  332. package/src/lib/hydration-gated-storage.ts +49 -0
  333. package/src/lib/ifc-export.test.ts +174 -0
  334. package/src/lib/ifc-export.ts +201 -0
  335. package/src/lib/ifc-parts.ts +15 -0
  336. package/src/lib/inspector-expanded.test.ts +44 -0
  337. package/src/lib/inspector-expanded.ts +36 -0
  338. package/src/lib/interaction/scope.ts +27 -1
  339. package/src/lib/keyboard-pan.test.ts +184 -0
  340. package/src/lib/keyboard-pan.ts +238 -0
  341. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  342. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  343. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  344. package/src/lib/lot/clean-ring.test.ts +186 -0
  345. package/src/lib/lot/clean-ring.ts +217 -0
  346. package/src/lib/lot/dossier.test.ts +202 -0
  347. package/src/lib/lot/dossier.ts +498 -0
  348. package/src/lib/lot/drop-in.test.ts +326 -0
  349. package/src/lib/lot/drop-in.ts +407 -0
  350. package/src/lib/lot/index.ts +43 -0
  351. package/src/lib/lot/lot-patch.test.ts +340 -0
  352. package/src/lib/lot/lot-patch.ts +394 -0
  353. package/src/lib/lot/parcel-provider.ts +43 -0
  354. package/src/lib/lot/terrain.test.ts +161 -0
  355. package/src/lib/lot/terrain.ts +266 -0
  356. package/src/lib/material-catalog-model.ts +74 -0
  357. package/src/lib/material-paint-panel-model.ts +80 -0
  358. package/src/lib/material-paint.ts +4 -0
  359. package/src/lib/mezzanine-actions.test.ts +342 -0
  360. package/src/lib/mezzanine-draft.test.ts +501 -0
  361. package/src/lib/mezzanine-draft.ts +278 -0
  362. package/src/lib/mezzanine-messages.ts +26 -0
  363. package/src/lib/mezzanine-selection.test.ts +207 -0
  364. package/src/lib/mezzanine-stairs.ts +46 -0
  365. package/src/lib/model-export.ts +3 -1
  366. package/src/lib/node-action-movement.ts +17 -6
  367. package/src/lib/node-display-label.ts +37 -0
  368. package/src/lib/orbit-floor-clearance.test.ts +165 -0
  369. package/src/lib/orbit-floor-clearance.ts +106 -0
  370. package/src/lib/overlay-transform.test.ts +28 -0
  371. package/src/lib/overlay-transform.ts +12 -0
  372. package/src/lib/paint-commit-route.test.ts +37 -0
  373. package/src/lib/paint-commit-route.ts +17 -0
  374. package/src/lib/paint-eyedropper.test.tsx +165 -0
  375. package/src/lib/paint-eyedropper.ts +99 -0
  376. package/src/lib/paint-pass-through.test.ts +111 -0
  377. package/src/lib/paint-pass-through.ts +76 -0
  378. package/src/lib/paint-region-hover.ts +25 -0
  379. package/src/lib/paint-region-mode.ts +154 -0
  380. package/src/lib/paint-regions.test.ts +166 -0
  381. package/src/lib/paint-regions.ts +192 -0
  382. package/src/lib/paint-scope.test.ts +317 -35
  383. package/src/lib/paint-scope.ts +266 -25
  384. package/src/lib/panel-rows.ts +96 -0
  385. package/src/lib/panel-tool-options.test.ts +34 -0
  386. package/src/lib/panel-tool-options.ts +46 -0
  387. package/src/lib/parametric-node-update.ts +37 -0
  388. package/src/lib/plate-paint-affected.test.ts +300 -0
  389. package/src/lib/plate-paint-affected.ts +177 -0
  390. package/src/lib/plugin-chat.ts +50 -0
  391. package/src/lib/plugin-panels.test.ts +25 -0
  392. package/src/lib/plugin-panels.ts +94 -0
  393. package/src/lib/print-shell-compiler.ts +1 -0
  394. package/src/lib/registered-draft-snap.ts +12 -0
  395. package/src/lib/roof-duplication.ts +10 -2
  396. package/src/lib/room-assembly-overlay.test.ts +275 -0
  397. package/src/lib/room-assembly-overlay.ts +266 -0
  398. package/src/lib/room-built-on.test.ts +379 -0
  399. package/src/lib/room-built-on.ts +138 -0
  400. package/src/lib/room-construction-commands.ts +712 -0
  401. package/src/lib/room-construction.test.ts +746 -0
  402. package/src/lib/room-construction.ts +385 -0
  403. package/src/lib/room-controls-phase6.test.ts +630 -0
  404. package/src/lib/room-divide-session.ts +523 -0
  405. package/src/lib/room-handle-drag.ts +1068 -0
  406. package/src/lib/room-name-catalog.test.ts +90 -0
  407. package/src/lib/room-name-catalog.ts +168 -0
  408. package/src/lib/room-push-dimensions.test.ts +311 -0
  409. package/src/lib/room-push-dimensions.ts +251 -0
  410. package/src/lib/room-selection-commands.ts +62 -0
  411. package/src/lib/room-selection.test.ts +973 -0
  412. package/src/lib/room-selection.ts +417 -0
  413. package/src/lib/room-structure-commands.ts +94 -0
  414. package/src/lib/room-structure.test.ts +921 -0
  415. package/src/lib/room-transform-session.ts +570 -0
  416. package/src/lib/room-zone-routing.test.ts +294 -0
  417. package/src/lib/room-zone-routing.ts +197 -0
  418. package/src/lib/scene-clipboard.test.ts +364 -0
  419. package/src/lib/scene-clipboard.ts +229 -48
  420. package/src/lib/scene-material-refs.test.ts +79 -0
  421. package/src/lib/scene-material-refs.ts +79 -0
  422. package/src/lib/scene.test.ts +87 -0
  423. package/src/lib/scene.ts +39 -45
  424. package/src/lib/selection-routing.test.ts +129 -45
  425. package/src/lib/selection-routing.ts +53 -74
  426. package/src/lib/session-writes.test.ts +120 -0
  427. package/src/lib/session-writes.ts +205 -0
  428. package/src/lib/sidebar-panel.ts +62 -0
  429. package/src/lib/site-boundary.test.ts +4 -6
  430. package/src/lib/site-boundary.ts +1 -1
  431. package/src/lib/snapping-mode.test.ts +17 -2
  432. package/src/lib/snapping-mode.ts +19 -7
  433. package/src/lib/spatial-drag-plane.test.ts +53 -0
  434. package/src/lib/spatial-drag-plane.ts +19 -0
  435. package/src/lib/spatial-pointer-input.test.ts +72 -0
  436. package/src/lib/spatial-pointer-input.ts +62 -0
  437. package/src/lib/spatial-snap.test.ts +47 -0
  438. package/src/lib/terrace-draft.test.ts +145 -0
  439. package/src/lib/terrace-draft.ts +213 -0
  440. package/src/lib/terrain-panel-model.ts +117 -0
  441. package/src/lib/tool-cursor-icon.test.ts +16 -0
  442. package/src/lib/tool-cursor-icon.ts +11 -0
  443. package/src/lib/typing-target.ts +9 -0
  444. package/src/lib/units.ts +77 -39
  445. package/src/lib/vector-edges.test.ts +58 -0
  446. package/src/lib/vector-edges.ts +400 -0
  447. package/src/lib/viewer-selection.test.ts +577 -0
  448. package/src/lib/viewer-selection.ts +569 -0
  449. package/src/lib/wall-draw-variant.test.ts +71 -0
  450. package/src/lib/wall-draw-variant.ts +91 -0
  451. package/src/lib/wall-paint-region-session.ts +246 -0
  452. package/src/lib/wall-paint-region.test.ts +373 -0
  453. package/src/lib/wall-polygon-draft.test.ts +262 -0
  454. package/src/lib/wall-polygon-draft.ts +286 -0
  455. package/src/lib/wall-push.test.ts +453 -0
  456. package/src/lib/wall-region-face.ts +352 -0
  457. package/src/lib/wall-region-handles.test.ts +304 -0
  458. package/src/lib/wall-region-handles.ts +368 -0
  459. package/src/lib/wall-region-list.test.ts +119 -0
  460. package/src/lib/wall-region-list.ts +136 -0
  461. package/src/lib/wall-region-snap.ts +110 -0
  462. package/src/lib/window-interaction.ts +14 -0
  463. package/src/lib/world-grid-snap.test.ts +48 -0
  464. package/src/lib/world-grid-snap.ts +9 -5
  465. package/src/lib/zone-content.ts +4 -1
  466. package/src/lib/zone-name.test.ts +17 -0
  467. package/src/lib/zone-name.ts +17 -0
  468. package/src/store/opens-in-select-mode.test.ts +223 -0
  469. package/src/store/paint-mode.test.ts +188 -0
  470. package/src/store/rotation-snapping.test.ts +53 -0
  471. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  472. package/src/store/tool-mode.test.ts +137 -6
  473. package/src/store/use-delete-confirmation.ts +28 -1
  474. package/src/store/use-drawing-view.test.ts +24 -3
  475. package/src/store/use-drawing-view.ts +29 -1
  476. package/src/store/use-editor.tsx +442 -323
  477. package/src/store/use-floorplan-draft-preview.ts +23 -1
  478. package/src/store/use-floorplan-marquee.ts +12 -2
  479. package/src/store/use-interaction-scope.ts +155 -6
  480. package/src/store/use-quick-measurement-hud.ts +3 -3
  481. package/src/store/use-stair-build-preview.ts +13 -5
  482. package/src/store/use-wall-move-ghosts.ts +4 -2
  483. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  484. package/src/store/use-wall-region-handles-selection.ts +139 -0
  485. package/src/store/view-layout.test.ts +175 -0
  486. package/src/store/view-layout.ts +196 -0
  487. package/src/test-utils/immediate-animation-frames.ts +16 -0
  488. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  489. package/src/test-utils/selection-harness.ts +82 -0
  490. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  491. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  492. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  493. package/src/components/tools/fence/fence-drafting.ts +0 -266
  494. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  495. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  496. package/src/lib/floorplan/stairs.ts +0 -489
  497. package/src/lib/level-duplication.test.ts +0 -144
  498. package/src/lib/level-duplication.ts +0 -195
@@ -0,0 +1,557 @@
1
+ import { arcRuns, insetPolygon, type KeepOut, sightTriangle, streetCorners } 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
+ /** Yard sides in the order the site-plan dimensions are cast. */
20
+ export type YardSide = 'north' | 'south' | 'east' | 'west'
21
+
22
+ export interface SetbackInputs {
23
+ front: number
24
+ side: number
25
+ rear: number
26
+ left?: number
27
+ right?: number
28
+ /** A corner lot's second street side; absent = the front setback. */
29
+ streetSide?: number
30
+ }
31
+
32
+ /** Per-edge classification of a lot polygon relative to its front edge. */
33
+ export type EdgeRole = 'front' | 'rear' | 'left' | 'right' | 'street' | 'street'
34
+
35
+ const EPS = 1e-9
36
+
37
+ export function polygonBounds(points: readonly Pt[]): Bounds {
38
+ let minX = Number.POSITIVE_INFINITY
39
+ let minY = Number.POSITIVE_INFINITY
40
+ let maxX = Number.NEGATIVE_INFINITY
41
+ let maxY = Number.NEGATIVE_INFINITY
42
+ for (const [x, y] of points) {
43
+ if (x < minX) minX = x
44
+ if (x > maxX) maxX = x
45
+ if (y < minY) minY = y
46
+ if (y > maxY) maxY = y
47
+ }
48
+ if (!Number.isFinite(minX)) return { minX: 0, minY: 0, maxX: 0, maxY: 0 }
49
+ return { minX, minY, maxX, maxY }
50
+ }
51
+
52
+ export function polygonArea(points: readonly Pt[]): number {
53
+ let a = 0
54
+ for (let i = 0, n = points.length; i < n; i++) {
55
+ const p = points[i] as Pt
56
+ const q = points[(i + 1) % n] as Pt
57
+ a += p[0] * q[1] - q[0] * p[1]
58
+ }
59
+ return Math.abs(a / 2)
60
+ }
61
+
62
+ export function polygonCentroid(points: readonly Pt[]): Pt {
63
+ let cx = 0
64
+ let cy = 0
65
+ let a = 0
66
+ for (let i = 0, n = points.length; i < n; i++) {
67
+ const p = points[i] as Pt
68
+ const q = points[(i + 1) % n] as Pt
69
+ const cross = p[0] * q[1] - q[0] * p[1]
70
+ a += cross
71
+ cx += (p[0] + q[0]) * cross
72
+ cy += (p[1] + q[1]) * cross
73
+ }
74
+ if (Math.abs(a) < EPS) {
75
+ // Degenerate ring — fall back to the vertex average.
76
+ const n = Math.max(1, points.length)
77
+ let sx = 0
78
+ let sy = 0
79
+ for (const [x, y] of points) {
80
+ sx += x
81
+ sy += y
82
+ }
83
+ return [sx / n, sy / n]
84
+ }
85
+ a /= 2
86
+ return [cx / (6 * a), cy / (6 * a)]
87
+ }
88
+
89
+ /** True when the ring is wound counter-clockwise in a y-down (south-positive) frame. */
90
+ export function isCounterClockwise(points: readonly Pt[]): boolean {
91
+ let a = 0
92
+ for (let i = 0, n = points.length; i < n; i++) {
93
+ const p = points[i] as Pt
94
+ const q = points[(i + 1) % n] as Pt
95
+ a += p[0] * q[1] - q[0] * p[1]
96
+ }
97
+ return a > 0
98
+ }
99
+
100
+ /**
101
+ * Unit normal of edge `i` pointing OUT of the polygon, winding-agnostic.
102
+ */
103
+ export function outwardNormal(points: readonly Pt[], i: number): Pt {
104
+ const n = points.length
105
+ const p = points[i % n] as Pt
106
+ const q = points[(i + 1) % n] as Pt
107
+ const dx = q[0] - p[0]
108
+ const dy = q[1] - p[1]
109
+ const len = Math.hypot(dx, dy) || 1
110
+ // Left normal of the edge direction. For a CCW ring (positive shoelace in
111
+ // this frame) the left normal points inward, so flip.
112
+ const sign = isCounterClockwise(points) ? -1 : 1
113
+ return [(sign * -dy) / len, (sign * dx) / len]
114
+ }
115
+
116
+ /**
117
+ * Compass heading of edge `i`'s outward normal, in DEGREES clockwise from
118
+ * north, where `northRotation` (radians, clockwise) is true north's offset
119
+ * from plan up (−y). This is the direction the edge faces.
120
+ */
121
+ export function edgeHeadingDeg(points: readonly Pt[], i: number, northRotation = 0): number {
122
+ const [nx, ny] = outwardNormal(points, i)
123
+ // Plan up (−y) is 0°, +x (east) is 90°.
124
+ const raw = Math.atan2(nx, -ny) - northRotation
125
+ const deg = (raw * 180) / Math.PI
126
+ return ((deg % 360) + 360) % 360
127
+ }
128
+
129
+ const COMPASS = [
130
+ 'N',
131
+ 'NNE',
132
+ 'NE',
133
+ 'ENE',
134
+ 'E',
135
+ 'ESE',
136
+ 'SE',
137
+ 'SSE',
138
+ 'S',
139
+ 'SSW',
140
+ 'SW',
141
+ 'WSW',
142
+ 'W',
143
+ 'WNW',
144
+ 'NW',
145
+ 'NNW',
146
+ ]
147
+
148
+ export function compassLabel(headingDeg: number): string {
149
+ const idx = Math.round((((headingDeg % 360) + 360) % 360) / 22.5) % 16
150
+ return COMPASS[idx] as string
151
+ }
152
+
153
+ /** Length of edge `i`, metres. */
154
+ export function edgeLength(points: readonly Pt[], i: number): number {
155
+ const n = points.length
156
+ const p = points[i % n] as Pt
157
+ const q = points[(i + 1) % n] as Pt
158
+ return Math.hypot(q[0] - p[0], q[1] - p[1])
159
+ }
160
+
161
+ /**
162
+ * Fallback front edge: the edge whose outward normal points closest to true
163
+ * north. Ties break toward the longer edge — the street frontage of a typical
164
+ * lot is one of its long sides.
165
+ */
166
+ export function mostNorthFacingEdge(points: readonly Pt[], northRotation = 0): number {
167
+ let best = 0
168
+ let bestScore = Number.POSITIVE_INFINITY
169
+ for (let i = 0; i < points.length; i++) {
170
+ const heading = edgeHeadingDeg(points, i, northRotation)
171
+ const off = Math.min(heading, 360 - heading)
172
+ // Sub-degree ties resolve on length so a chamfer corner never wins.
173
+ const score = off - Math.min(0.5, edgeLength(points, i) / 1000)
174
+ if (score < bestScore) {
175
+ bestScore = score
176
+ best = i
177
+ }
178
+ }
179
+ return best
180
+ }
181
+
182
+ /** `frontEdge` if it indexes a real edge, else the most north-facing edge. */
183
+ export function resolveFrontEdge(
184
+ points: readonly Pt[],
185
+ frontEdge: number | undefined,
186
+ northRotation = 0,
187
+ ): number {
188
+ if (
189
+ typeof frontEdge === 'number' &&
190
+ Number.isInteger(frontEdge) &&
191
+ frontEdge >= 0 &&
192
+ frontEdge < points.length
193
+ ) {
194
+ return frontEdge
195
+ }
196
+ return mostNorthFacingEdge(points, northRotation)
197
+ }
198
+
199
+ /**
200
+ * Classify every edge as front / rear / left / right.
201
+ *
202
+ * - front = `frontIndex`.
203
+ * - rear = the edge whose outward normal is most anti-parallel to the front's.
204
+ * - the rest are sides. LEFT / RIGHT are defined along the FRONT EDGE
205
+ * DIRECTION (`points[front] → points[front + 1]`): an edge whose midpoint
206
+ * projects behind the front-edge midpoint on that axis is `left`, ahead of
207
+ * it is `right`. Deterministic and independent of winding.
208
+ */
209
+ export function classifyEdges(
210
+ points: readonly Pt[],
211
+ frontIndex: number,
212
+ streetEdges: readonly number[] = [],
213
+ ): EdgeRole[] {
214
+ const n = points.length
215
+ const roles: EdgeRole[] = new Array(n).fill('left')
216
+ if (n === 0) return roles
217
+
218
+ const front = ((frontIndex % n) + n) % n
219
+ const fn = outwardNormal(points, front)
220
+ // a corner lot's other street edges (the parcel fabric's frontage) are
221
+ // 'street' — they take the street-side setback, never a side yard, and
222
+ // the rear is never one of them
223
+ const street = new Set(streetEdges.map((i) => ((i % n) + n) % n))
224
+ street.delete(front)
225
+
226
+ let rear = -1
227
+ let rearDot = Number.POSITIVE_INFINITY
228
+ for (let i = 0; i < n; i++) {
229
+ if (i === front || street.has(i)) continue
230
+ const ni = outwardNormal(points, i)
231
+ const dot = fn[0] * ni[0] + fn[1] * ni[1]
232
+ if (dot < rearDot) {
233
+ rearDot = dot
234
+ rear = i
235
+ }
236
+ }
237
+
238
+ const fp = points[front] as Pt
239
+ const fq = points[(front + 1) % n] as Pt
240
+ const dx = fq[0] - fp[0]
241
+ const dy = fq[1] - fp[1]
242
+ const len = Math.hypot(dx, dy) || 1
243
+ const ux = dx / len
244
+ const uy = dy / len
245
+ const fmx = (fp[0] + fq[0]) / 2
246
+ const fmy = (fp[1] + fq[1]) / 2
247
+
248
+ for (let i = 0; i < n; i++) {
249
+ if (i === front) {
250
+ roles[i] = 'front'
251
+ continue
252
+ }
253
+ if (street.has(i)) {
254
+ roles[i] = 'street'
255
+ continue
256
+ }
257
+ if (i === rear) {
258
+ roles[i] = 'rear'
259
+ continue
260
+ }
261
+ const p = points[i] as Pt
262
+ const q = points[(i + 1) % n] as Pt
263
+ const mx = (p[0] + q[0]) / 2 - fmx
264
+ const my = (p[1] + q[1]) / 2 - fmy
265
+ roles[i] = mx * ux + my * uy < 0 ? 'left' : 'right'
266
+ }
267
+ return roles
268
+ }
269
+
270
+ /** Required setback distance (metres) for each edge role. */
271
+ export function setbackForRole(setbacks: SetbackInputs, role: EdgeRole): number {
272
+ switch (role) {
273
+ case 'front':
274
+ return setbacks.front
275
+ case 'street':
276
+ return setbacks.streetSide ?? setbacks.front
277
+ case 'rear':
278
+ return setbacks.rear
279
+ case 'left':
280
+ return setbacks.left ?? setbacks.side
281
+ case 'right':
282
+ return setbacks.right ?? setbacks.side
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Buildable envelope — every lot edge pushed INWARD by its own setback, with
288
+ * the new vertices taken as the intersections of adjacent offset lines
289
+ * (variable-distance straight-skeleton-lite). Exact for convex lots; on a
290
+ * concave lot a reflex corner can self-intersect, in which case the caller
291
+ * still gets a ring but it is advisory, not a legal envelope.
292
+ *
293
+ * Adjacent offset lines that are parallel (a straight run split by a
294
+ * surplus vertex) have no intersection: the shared vertex is then simply
295
+ * pushed inward along the current edge's own offset, instead of the whole
296
+ * envelope being refused.
297
+ *
298
+ * Returns `[]` when the polygon has fewer than 3 points or when the result
299
+ * inverts (setbacks larger than the lot).
300
+ */
301
+ export function setbackEnvelope(
302
+ points: readonly Pt[],
303
+ setbacks: SetbackInputs,
304
+ frontIndex: number,
305
+ options: { streetEdges?: readonly number[]; sightTriangleM?: number } = {},
306
+ ): Pt[] {
307
+ const n = points.length
308
+ if (n < 3) return []
309
+ const streetEdges = options.streetEdges ?? []
310
+ const roles = classifyEdges(points, frontIndex, streetEdges)
311
+ // A radius drawn as a run of short edges takes ONE role. A curb return
312
+ // is street frontage: a run carrying, or touching, a front / street edge
313
+ // is front / street all along (otherwise the last sliver of the return
314
+ // became a side yard, and the envelope jutted out toward the corner); a
315
+ // run carrying the rear is rear; else
316
+ // each sliver keeps its own side.
317
+ const runs = arcRuns(points)
318
+ const runRole = new Map<number, EdgeRole>()
319
+ const streetLike = (r: EdgeRole) => r === 'front' || r === 'street'
320
+ for (let i = 0; i < n; i++) {
321
+ const r = roles[i] as EdgeRole
322
+ const g = runs[i] as number
323
+ const have = runRole.get(g)
324
+ if (streetLike(r) || (r === 'rear' && !(have && streetLike(have)))) runRole.set(g, r)
325
+ else if (!have) runRole.set(g, r)
326
+ }
327
+ for (let i = 0; i < n; i++) {
328
+ const g = runs[i] as number
329
+ const have = runRole.get(g) as EdgeRole
330
+ if (streetLike(have)) continue
331
+ const before = roles[(i - 1 + n) % n] as EdgeRole
332
+ const after = roles[(i + 1) % n] as EdgeRole
333
+ const members = runs.filter((x) => x === g).length
334
+ if (members > 1) {
335
+ if (runs[(i - 1 + n) % n] !== g && streetLike(before)) runRole.set(g, before)
336
+ else if (runs[(i + 1) % n] !== g && streetLike(after)) runRole.set(g, after)
337
+ }
338
+ }
339
+ // One lot line drawn as two: the county fabric splits a frontage at a kink of
340
+ // a degree or two, and only one half may come back from the street lookup
341
+ // (or the north-facing fallback). The half left out took a side yard, and
342
+ // the front yard swung round their shared corner in a circle as deep as
343
+ // itself. An edge running on nearly straight from a front or street edge
344
+ // is that frontage too.
345
+ const roleOf = (i: number) =>
346
+ runRole.get(runs[(i + n) % n] as number) ?? (roles[(i + n) % n] as EdgeRole)
347
+ const direction = (i: number) => {
348
+ const p = points[(i + n) % n] as Pt
349
+ const q = points[(i + 1 + n) % n] as Pt
350
+ const l = Math.hypot(q[0] - p[0], q[1] - p[1]) || 1
351
+ return [(q[0] - p[0]) / l, (q[1] - p[1]) / l] as const
352
+ }
353
+ const straightOn = Math.cos((8 * Math.PI) / 180)
354
+ const continued = new Map<number, EdgeRole>()
355
+ for (let pass = 0; pass < n; pass++) {
356
+ let grew = false
357
+ for (let i = 0; i < n; i++) {
358
+ const own = continued.get(i) ?? roleOf(i)
359
+ if (streetLike(own)) continue
360
+ const d = direction(i)
361
+ for (const j of [i - 1, i + 1]) {
362
+ const k = (j + n) % n
363
+ const theirs = continued.get(k) ?? roleOf(k)
364
+ const e = direction(k)
365
+ if (streetLike(theirs) && d[0] * e[0] + d[1] * e[1] > straightOn) {
366
+ continued.set(i, theirs)
367
+ grew = true
368
+ break
369
+ }
370
+ }
371
+ }
372
+ if (!grew) break
373
+ }
374
+ const distances: number[] = []
375
+ for (let i = 0; i < n; i++) {
376
+ const role = continued.get(i) ?? runRole.get(runs[i] as number) ?? (roles[i] as EdgeRole)
377
+ const d = setbackForRole(
378
+ setbacks,
379
+ role === 'left' || role === 'right' ? (roles[i] as EdgeRole) : role,
380
+ )
381
+ if (!Number.isFinite(d) || d < 0) return []
382
+ distances.push(d)
383
+ }
384
+ // the corner sight triangles keep the envelope out of the clear-vision
385
+ // zone at each street intersection (the hypotenuse is a half-plane)
386
+ const keepOut: KeepOut[] = []
387
+ if (options.sightTriangleM && options.sightTriangleM > 0) {
388
+ for (const [a, b] of streetCorners(points, [frontIndex, ...streetEdges])) {
389
+ const tri = sightTriangle(points, a, b, options.sightTriangleM)
390
+ if (tri) keepOut.push({ a: tri.a, b: tri.b, inside: tri.corner })
391
+ }
392
+ }
393
+ // the true inward offset (core setback-envelope.ts): straight where the
394
+ // lot is straight, concentric round a radius, clipped where the offsets
395
+ // cross — the industry-standard setback for pie shapes, radius corners
396
+ // and odd lots
397
+ return insetPolygon(points, distances, { keepOut })
398
+ }
399
+
400
+ /**
401
+ * Distance from `origin` along unit direction `(dx, dy)` to the first crossing
402
+ * of the polygon boundary. `null` when the ray never hits (origin outside, or
403
+ * a degenerate ring).
404
+ */
405
+ export function rayToPolygon(
406
+ points: readonly Pt[],
407
+ origin: Pt,
408
+ dx: number,
409
+ dy: number,
410
+ ): { distance: number; point: Pt } | null {
411
+ const n = points.length
412
+ let best: { distance: number; point: Pt } | null = null
413
+ for (let i = 0; i < n; i++) {
414
+ const p = points[i] as Pt
415
+ const q = points[(i + 1) % n] as Pt
416
+ const ex = q[0] - p[0]
417
+ const ey = q[1] - p[1]
418
+ const denom = dx * ey - dy * ex
419
+ if (Math.abs(denom) < 1e-12) continue
420
+ // origin + t*d = p + u*e
421
+ const t = ((p[0] - origin[0]) * ey - (p[1] - origin[1]) * ex) / denom
422
+ const u = ((p[0] - origin[0]) * dy - (p[1] - origin[1]) * dx) / denom
423
+ if (t <= 1e-9 || u < -1e-9 || u > 1 + 1e-9) continue
424
+ if (!best || t < best.distance) {
425
+ best = { distance: t, point: [origin[0] + dx * t, origin[1] + dy * t] }
426
+ }
427
+ }
428
+ return best
429
+ }
430
+
431
+ export interface YardDimension {
432
+ side: YardSide
433
+ /** Bounding-box edge midpoint the dimension starts at. */
434
+ from: Pt
435
+ /** Where the cast ray meets the lot line. */
436
+ to: Pt
437
+ /** Metres. */
438
+ distance: number
439
+ }
440
+
441
+ /**
442
+ * The four yard dimensions: from each footprint bbox edge midpoint, straight
443
+ * out to the lot line. Sides whose ray misses the lot line (footprint outside
444
+ * the lot) are omitted rather than faked.
445
+ */
446
+ export function castYardDimensions(lot: readonly Pt[], footprint: Bounds): YardDimension[] {
447
+ if (lot.length < 3) return []
448
+ const midX = (footprint.minX + footprint.maxX) / 2
449
+ const midY = (footprint.minY + footprint.maxY) / 2
450
+ const casts: { side: YardSide; from: Pt; dx: number; dy: number }[] = [
451
+ { side: 'north', from: [midX, footprint.minY], dx: 0, dy: -1 },
452
+ { side: 'south', from: [midX, footprint.maxY], dx: 0, dy: 1 },
453
+ { side: 'west', from: [footprint.minX, midY], dx: -1, dy: 0 },
454
+ { side: 'east', from: [footprint.maxX, midY], dx: 1, dy: 0 },
455
+ ]
456
+ const out: YardDimension[] = []
457
+ for (const c of casts) {
458
+ const hit = rayToPolygon(lot, c.from, c.dx, c.dy)
459
+ if (!hit) continue
460
+ out.push({ side: c.side, from: c.from, to: hit.point, distance: hit.distance })
461
+ }
462
+ return out
463
+ }
464
+
465
+ /**
466
+ * Yard dimensions for a building TURNED on its lot. The footprint's bounds
467
+ * are taken in the building's own frame (`yaw`, the building node's Y
468
+ * rotation), and the four edge midpoints cast square to the house's faces
469
+ * out to the lot line — the yards a plan checker measures. With `yaw` 0 this
470
+ * is the axis-aligned cast above; with a house square to a diagonal lot the
471
+ * axis-aligned bbox sticks out past the real corners and reads a front yard
472
+ * inches short of the setback it actually meets (Land Park, 2026-09-06).
473
+ * `side` is the compass direction nearest the cast, so labels keep reading
474
+ * N / S / E / W.
475
+ */
476
+ export function castYardDimensionsOriented(
477
+ lot: readonly Pt[],
478
+ footprintLoops: readonly (readonly Pt[])[],
479
+ yaw: number,
480
+ ): YardDimension[] {
481
+ if (lot.length < 3) return []
482
+ const c = Math.cos(yaw)
483
+ const s = Math.sin(yaw)
484
+ // world = (c·lx + s·lz, −s·lx + c·lz) ⇒ local = (c·wx − s·wz, s·wx + c·wz)
485
+ const toLocal = (p: Pt): Pt => [c * p[0] - s * p[1], s * p[0] + c * p[1]]
486
+ const toWorld = (p: Pt): Pt => [c * p[0] + s * p[1], -s * p[0] + c * p[1]]
487
+ let minX = Number.POSITIVE_INFINITY
488
+ let minZ = Number.POSITIVE_INFINITY
489
+ let maxX = Number.NEGATIVE_INFINITY
490
+ let maxZ = Number.NEGATIVE_INFINITY
491
+ for (const loop of footprintLoops) {
492
+ for (const p of loop) {
493
+ const [x, z] = toLocal(p)
494
+ if (x < minX) minX = x
495
+ if (x > maxX) maxX = x
496
+ if (z < minZ) minZ = z
497
+ if (z > maxZ) maxZ = z
498
+ }
499
+ }
500
+ if (!Number.isFinite(minX)) return []
501
+ const midX = (minX + maxX) / 2
502
+ const midZ = (minZ + maxZ) / 2
503
+ const casts: { from: Pt; dir: Pt }[] = [
504
+ { from: [midX, minZ], dir: [0, -1] },
505
+ { from: [midX, maxZ], dir: [0, 1] },
506
+ { from: [minX, midZ], dir: [-1, 0] },
507
+ { from: [maxX, midZ], dir: [1, 0] },
508
+ ]
509
+ const sides: YardSide[] = ['north', 'east', 'south', 'west']
510
+ const out: YardDimension[] = []
511
+ for (const cast of casts) {
512
+ const from = toWorld(cast.from)
513
+ const [dx, dy] = toWorld(cast.dir)
514
+ const hit = rayToPolygon(lot, from, dx, dy)
515
+ if (!hit) continue
516
+ // heading of the cast: plan up (−y) is north, +x east
517
+ const heading = ((Math.atan2(dx, -dy) * 180) / Math.PI + 360) % 360
518
+ const side = sides[Math.round(heading / 90) % 4] as YardSide
519
+ out.push({ side, from, to: hit.point, distance: hit.distance })
520
+ }
521
+ return out
522
+ }
523
+
524
+ /** Even-odd point-in-polygon. */
525
+ export function pointInPolygon(points: readonly Pt[], x: number, y: number): boolean {
526
+ let inside = false
527
+ for (let i = 0, j = points.length - 1; i < points.length; j = i++) {
528
+ const a = points[i] as Pt
529
+ const b = points[j] as Pt
530
+ if (a[1] > y !== b[1] > y && x < ((b[0] - a[0]) * (y - a[1])) / (b[1] - a[1]) + a[0]) {
531
+ inside = !inside
532
+ }
533
+ }
534
+ return inside
535
+ }
536
+
537
+ /** True when every corner of `bounds` is inside `lot`. */
538
+ export function boundsInsidePolygon(lot: readonly Pt[], bounds: Bounds): boolean {
539
+ if (lot.length < 3) return false
540
+ const corners: Pt[] = [
541
+ [bounds.minX, bounds.minY],
542
+ [bounds.maxX, bounds.minY],
543
+ [bounds.maxX, bounds.maxY],
544
+ [bounds.minX, bounds.maxY],
545
+ ]
546
+ return corners.every((c) => pointInPolygon(lot, c[0], c[1]))
547
+ }
548
+
549
+ export const METRES_PER_FOOT = 0.3048
550
+
551
+ /** `24'-6"` — the notation a US site plan uses for yard dimensions. */
552
+ export function formatFeetInches(metres: number): string {
553
+ const totalInches = Math.round((metres / METRES_PER_FOOT) * 12)
554
+ const feet = Math.floor(totalInches / 12)
555
+ const inches = totalInches % 12
556
+ return inches === 0 ? `${feet}'-0"` : `${feet}'-${inches}"`
557
+ }
@@ -0,0 +1,71 @@
1
+ export {
2
+ buildingRecentreOffset,
3
+ buildSitePlanDrawing,
4
+ describeSiteEdges,
5
+ levelFootprintLoops,
6
+ type SitePlanDrawing,
7
+ type SitePlanEdge,
8
+ } from './build-site-plan-drawing'
9
+ export {
10
+ hasSitePlanContributors,
11
+ registerSitePlanContributor,
12
+ type SitePlanContributor,
13
+ type SitePlanServicePoint,
14
+ type SitePlanServiceRole,
15
+ type SitePlanServices,
16
+ sitePlanContributions,
17
+ subscribeSitePlanContributors,
18
+ } from './contributors'
19
+ export {
20
+ computeSiteCoverage,
21
+ formatCoveragePercent,
22
+ formatSqFt,
23
+ type ImperviousRow,
24
+ type SiteCoverage,
25
+ } from './coverage'
26
+ export {
27
+ FloorplanDrawingTypeSwitch,
28
+ isSitePlanAvailable,
29
+ useSitePlanAvailable,
30
+ } from './drawing-type-switch'
31
+ export {
32
+ detectFrontEdgeFromRoads,
33
+ FRONT_EDGE_PARALLEL_DEG,
34
+ type FrontEdgeMatch,
35
+ type RoadCenterline,
36
+ streetCore,
37
+ } from './front-edge'
38
+ export {
39
+ type Bounds,
40
+ boundsInsidePolygon,
41
+ castYardDimensions,
42
+ castYardDimensionsOriented,
43
+ classifyEdges,
44
+ compassLabel,
45
+ edgeHeadingDeg,
46
+ edgeLength,
47
+ formatFeetInches,
48
+ METRES_PER_FOOT,
49
+ mostNorthFacingEdge,
50
+ outwardNormal,
51
+ type Pt,
52
+ pointInPolygon,
53
+ polygonArea,
54
+ polygonBounds,
55
+ polygonCentroid,
56
+ rayToPolygon,
57
+ resolveFrontEdge,
58
+ type SetbackInputs,
59
+ setbackEnvelope,
60
+ setbackForRole,
61
+ type YardDimension,
62
+ type YardSide,
63
+ } from './geometry'
64
+ export {
65
+ formatStreetName,
66
+ serviceEntranceOf,
67
+ streetEdgeNames,
68
+ UNNAMED_STREET,
69
+ } from './site-annotations'
70
+ export { flatworkKindOf, type OutdoorPart } from './site-parts'
71
+ export { FloorplanSitePlanLayer } from './site-plan-layer'