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