@pascal-app/editor 1.0.3 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (440) hide show
  1. package/package.json +11 -9
  2. package/src/components/editor/action-menu-button.tsx +83 -0
  3. package/src/components/editor/bake-thumbnail.tsx +315 -4
  4. package/src/components/editor/capture-level-picker.tsx +72 -0
  5. package/src/components/editor/custom-camera-controls.tsx +202 -57
  6. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  7. package/src/components/editor/duplicate-node.ts +244 -0
  8. package/src/components/editor/editor-layout-mobile.tsx +36 -25
  9. package/src/components/editor/editor-layout-v2.tsx +131 -58
  10. package/src/components/editor/export-manager.tsx +34 -8
  11. package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
  12. package/src/components/editor/first-person/build-collider-world.ts +6 -2
  13. package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
  14. package/src/components/editor/first-person/mechanism-targets.ts +36 -0
  15. package/src/components/editor/first-person-controls.tsx +205 -2
  16. package/src/components/editor/floating-action-menu.tsx +63 -207
  17. package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
  18. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  19. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  20. package/src/components/editor/floor-region-controls.tsx +846 -0
  21. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  22. package/src/components/editor/floor-surface.test.ts +165 -0
  23. package/src/components/editor/floor-surface.ts +182 -0
  24. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  25. package/src/components/editor/floorplan-background-selection.ts +1 -3
  26. package/src/components/editor/floorplan-panel.tsx +344 -342
  27. package/src/components/editor/footprint-height-handle.tsx +204 -0
  28. package/src/components/editor/group-actions.ts +54 -2
  29. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  30. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  31. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  32. package/src/components/editor/handles/use-handle-drag.ts +269 -197
  33. package/src/components/editor/index.tsx +294 -169
  34. package/src/components/editor/join-walls-pill.tsx +44 -0
  35. package/src/components/editor/node-action-menu.tsx +68 -70
  36. package/src/components/editor/node-arrow-handles.tsx +25 -6
  37. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  38. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  39. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  40. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  41. package/src/components/editor/registry-action-contributions.tsx +80 -1
  42. package/src/components/editor/room-controls.tsx +847 -0
  43. package/src/components/editor/room-floor-highlight.tsx +128 -0
  44. package/src/components/editor/room-handles-2d.tsx +354 -0
  45. package/src/components/editor/room-handles.tsx +675 -0
  46. package/src/components/editor/room-highlight.tsx +225 -0
  47. package/src/components/editor/room-labels.test.ts +357 -0
  48. package/src/components/editor/room-labels.tsx +990 -0
  49. package/src/components/editor/room-transform-ghost.tsx +256 -0
  50. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  51. package/src/components/editor/selection-manager.tsx +451 -317
  52. package/src/components/editor/slab-hole-highlights.tsx +3 -0
  53. package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
  54. package/src/components/editor/snapshot-capture.test.ts +52 -4
  55. package/src/components/editor/snapshot-capture.ts +30 -5
  56. package/src/components/editor/thumbnail-generator.test.ts +308 -0
  57. package/src/components/editor/thumbnail-generator.tsx +1035 -229
  58. package/src/components/editor/use-floorplan-background-placement.ts +31 -11
  59. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  60. package/src/components/editor/views/host-view.tsx +61 -0
  61. package/src/components/editor/views/plugins-view.tsx +670 -0
  62. package/src/components/editor/views/scene-region.tsx +59 -0
  63. package/src/components/editor/views/use-editor-views.ts +57 -0
  64. package/src/components/editor/views/view-bar.tsx +220 -0
  65. package/src/components/editor/wall-justification.test.ts +140 -0
  66. package/src/components/editor/wall-move-side-handles.tsx +43 -142
  67. package/src/components/editor/wall-push-arrow.tsx +92 -0
  68. package/src/components/editor/wall-region-handles.tsx +509 -0
  69. package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
  70. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  71. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  72. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  73. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  74. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +113 -102
  75. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  76. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  77. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  78. package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
  79. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  80. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
  81. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +291 -227
  82. package/src/components/editor-2d/svg-paths.ts +12 -15
  83. package/src/components/procedural-items/inspector.tsx +40 -15
  84. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  85. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  86. package/src/components/systems/zone/zone-system.test.ts +47 -0
  87. package/src/components/systems/zone/zone-system.tsx +29 -68
  88. package/src/components/tools/item/placement-history.test.tsx +474 -0
  89. package/src/components/tools/item/placement-strategies.ts +8 -4
  90. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  91. package/src/components/tools/item/use-draft-node.ts +166 -95
  92. package/src/components/tools/item/use-placement-coordinator.tsx +93 -46
  93. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  94. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  95. package/src/components/tools/registry/move-registry-node-tool.tsx +3 -1
  96. package/src/components/tools/select/box-select-tool.tsx +2 -12
  97. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  98. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  99. package/src/components/tools/select/select-candidates.test.ts +37 -4
  100. package/src/components/tools/select/select-candidates.ts +15 -11
  101. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  102. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  103. package/src/components/tools/stair/stair-defaults.ts +0 -3
  104. package/src/components/tools/stair/stair-tool.tsx +90 -42
  105. package/src/components/tools/tool-manager.tsx +46 -20
  106. package/src/components/tools/wall/wall-drafting.test.ts +19 -4
  107. package/src/components/tools/zone/zone-tool.tsx +4 -12
  108. package/src/components/ui/action-menu/action-button.tsx +3 -0
  109. package/src/components/ui/action-menu/camera-actions.tsx +2 -1
  110. package/src/components/ui/action-menu/control-modes.tsx +2 -35
  111. package/src/components/ui/action-menu/measurement-control.tsx +3 -10
  112. package/src/components/ui/build-panel/build-panel.tsx +235 -0
  113. package/src/components/ui/command-palette/editor-commands.tsx +4 -30
  114. package/src/components/ui/command-palette/index.tsx +5 -24
  115. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  116. package/src/components/ui/controls/material-paint-panel.tsx +97 -26
  117. package/src/components/ui/controls/material-picker.tsx +4 -1
  118. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  119. package/src/components/ui/controls/select-control.tsx +65 -0
  120. package/src/components/ui/floating-level-selector.tsx +9 -14
  121. package/src/components/ui/helpers/building-helper.tsx +4 -1
  122. package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
  123. package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
  124. package/src/components/ui/helpers/helper-manager.tsx +325 -14
  125. package/src/components/ui/helpers/item-helper.tsx +8 -0
  126. package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
  127. package/src/components/ui/icon-ref.tsx +45 -1
  128. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  129. package/src/components/ui/node-icon-image.tsx +16 -0
  130. package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
  131. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  132. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  133. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  134. package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
  135. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  136. package/src/components/ui/panels/multi-field-value.ts +6 -3
  137. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  138. package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
  139. package/src/components/ui/panels/node-display.ts +8 -3
  140. package/src/components/ui/panels/panel-manager.tsx +103 -68
  141. package/src/components/ui/panels/panel-wrapper.tsx +78 -36
  142. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  143. package/src/components/ui/panels/parametric-inspector.tsx +4 -4
  144. package/src/components/ui/panels/reference-panel.tsx +2 -3
  145. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  146. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  147. package/src/components/ui/panels/room-name-options.ts +47 -0
  148. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  149. package/src/components/ui/panels/room-panel.tsx +725 -0
  150. package/src/components/ui/panels/shape-choice.tsx +58 -0
  151. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  152. package/src/components/ui/primitives/color-dot.tsx +36 -24
  153. package/src/components/ui/primitives/sidebar.tsx +2 -0
  154. package/src/components/ui/right-stack.test.ts +29 -0
  155. package/src/components/ui/right-stack.tsx +145 -0
  156. package/src/components/ui/sidebar/icon-rail.tsx +5 -0
  157. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +31 -4
  158. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
  159. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  160. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  161. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  162. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  163. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  164. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  165. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  166. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  167. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  168. package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
  169. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  170. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  171. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  172. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  173. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  174. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  175. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  176. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  177. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  178. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  179. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
  180. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  181. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  182. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  183. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  184. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
  185. package/src/components/ui/sidebar/tab-bar.tsx +10 -0
  186. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  187. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  188. package/src/components/viewer/floorplan-preview.tsx +5 -6
  189. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  190. package/src/components/viewer/viewer-rooms.tsx +175 -0
  191. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  192. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  193. package/src/components/viewer/viewer-stage.tsx +2 -2
  194. package/src/components/viewer-zone-system.tsx +3 -8
  195. package/src/hooks/use-auto-save.integration.test.tsx +123 -9
  196. package/src/hooks/use-auto-save.ts +34 -8
  197. package/src/hooks/use-keyboard.test.ts +178 -3
  198. package/src/hooks/use-keyboard.ts +89 -71
  199. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  200. package/src/hooks/use-open-wall-ends.ts +238 -0
  201. package/src/hooks/use-placement-notice.test.ts +48 -0
  202. package/src/hooks/use-placement-notice.ts +42 -0
  203. package/src/hooks/use-selected-room.ts +143 -0
  204. package/src/index.tsx +246 -39
  205. package/src/lib/ceiling-edit-session.test.ts +321 -0
  206. package/src/lib/ceiling-edit-session.ts +206 -0
  207. package/src/lib/ceiling-region-session.test.ts +179 -0
  208. package/src/lib/continuation.test.ts +19 -1
  209. package/src/lib/continuation.ts +25 -4
  210. package/src/lib/door-interaction.ts +14 -0
  211. package/src/lib/editor-level-display.test.ts +96 -0
  212. package/src/lib/editor-level-display.ts +48 -0
  213. package/src/lib/editor-views.ts +144 -0
  214. package/src/lib/export-geometry-wait.test.ts +90 -0
  215. package/src/lib/floor-edit-session.ts +153 -0
  216. package/src/lib/floor-footprints.test.ts +563 -0
  217. package/src/lib/floor-footprints.ts +395 -0
  218. package/src/lib/floor-foundation-support.test.ts +283 -0
  219. package/src/lib/floor-opening-draft.ts +218 -0
  220. package/src/lib/floor-openings.test.ts +216 -0
  221. package/src/lib/floor-region-geometry.ts +191 -0
  222. package/src/lib/floor-region-session.test.ts +485 -0
  223. package/src/lib/floor-region-session.ts +589 -0
  224. package/src/lib/floor-region-snap.ts +101 -0
  225. package/src/lib/floor-step-hover.test.ts +32 -0
  226. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  227. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  228. package/src/lib/floorplan/dimension-drive.ts +479 -0
  229. package/src/lib/floorplan/floorplan-export.test.ts +12 -8
  230. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  231. package/src/lib/floorplan/floorplan-extension.ts +32 -0
  232. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  233. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  234. package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
  235. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
  236. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
  237. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  238. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  239. package/src/lib/floorplan/geometry.ts +13 -6
  240. package/src/lib/floorplan/index.ts +0 -9
  241. package/src/lib/floorplan/items.ts +5 -2
  242. package/src/lib/floorplan/marks.test.ts +213 -0
  243. package/src/lib/floorplan/marks.ts +238 -0
  244. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  245. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  246. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  247. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  248. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  249. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  250. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  251. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  252. package/src/lib/floorplan/schedules.test.ts +189 -0
  253. package/src/lib/floorplan/schedules.ts +339 -0
  254. package/src/lib/floorplan/sheet-export.test.ts +100 -0
  255. package/src/lib/floorplan/sheet-export.ts +318 -0
  256. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
  257. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  258. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  259. package/src/lib/floorplan/site-plan/coverage.ts +232 -0
  260. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  261. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  262. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  263. package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
  264. package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
  265. package/src/lib/floorplan/site-plan/geometry.ts +557 -0
  266. package/src/lib/floorplan/site-plan/index.ts +71 -0
  267. package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
  268. package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
  269. package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
  270. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  271. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  272. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  273. package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
  274. package/src/lib/floorplan/types.ts +1 -27
  275. package/src/lib/fresh-planar-placement.test.ts +36 -0
  276. package/src/lib/fresh-planar-placement.ts +59 -36
  277. package/src/lib/geometry-script/author.ts +152 -0
  278. package/src/lib/geometry-script/client.ts +152 -0
  279. package/src/lib/geometry-script/protocol.ts +12 -0
  280. package/src/lib/gesture-lifecycle.test.ts +595 -0
  281. package/src/lib/gesture-lifecycle.ts +290 -0
  282. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  283. package/src/lib/glb-export.test.ts +485 -0
  284. package/src/lib/glb-export.ts +237 -119
  285. package/src/lib/history.test.ts +6 -0
  286. package/src/lib/history.ts +7 -0
  287. package/src/lib/hud-title.ts +109 -0
  288. package/src/lib/hydration-gated-storage.ts +49 -0
  289. package/src/lib/ifc-export.test.ts +174 -0
  290. package/src/lib/ifc-export.ts +201 -0
  291. package/src/lib/ifc-parts.ts +15 -0
  292. package/src/lib/inspector-expanded.test.ts +44 -0
  293. package/src/lib/inspector-expanded.ts +36 -0
  294. package/src/lib/interaction/scope.ts +18 -0
  295. package/src/lib/keyboard-pan.test.ts +129 -7
  296. package/src/lib/keyboard-pan.ts +161 -12
  297. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  298. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  299. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  300. package/src/lib/lot/clean-ring.test.ts +186 -0
  301. package/src/lib/lot/clean-ring.ts +217 -0
  302. package/src/lib/lot/dossier.test.ts +202 -0
  303. package/src/lib/lot/dossier.ts +498 -0
  304. package/src/lib/lot/drop-in.test.ts +326 -0
  305. package/src/lib/lot/drop-in.ts +407 -0
  306. package/src/lib/lot/index.ts +43 -0
  307. package/src/lib/lot/lot-patch.test.ts +340 -0
  308. package/src/lib/lot/lot-patch.ts +394 -0
  309. package/src/lib/lot/parcel-provider.ts +43 -0
  310. package/src/lib/lot/terrain.test.ts +161 -0
  311. package/src/lib/lot/terrain.ts +266 -0
  312. package/src/lib/material-catalog-model.ts +3 -1
  313. package/src/lib/material-paint-panel-model.ts +9 -5
  314. package/src/lib/material-paint.ts +4 -0
  315. package/src/lib/mezzanine-actions.test.ts +342 -0
  316. package/src/lib/mezzanine-draft.test.ts +501 -0
  317. package/src/lib/mezzanine-draft.ts +278 -0
  318. package/src/lib/mezzanine-messages.ts +26 -0
  319. package/src/lib/mezzanine-selection.test.ts +207 -0
  320. package/src/lib/mezzanine-stairs.ts +46 -0
  321. package/src/lib/model-export.ts +3 -1
  322. package/src/lib/node-action-movement.ts +17 -6
  323. package/src/lib/node-display-label.ts +37 -0
  324. package/src/lib/orbit-floor-clearance.test.ts +165 -0
  325. package/src/lib/orbit-floor-clearance.ts +106 -0
  326. package/src/lib/paint-commit-route.test.ts +37 -0
  327. package/src/lib/paint-commit-route.ts +17 -0
  328. package/src/lib/paint-eyedropper.test.tsx +165 -0
  329. package/src/lib/paint-eyedropper.ts +99 -0
  330. package/src/lib/paint-pass-through.test.ts +111 -0
  331. package/src/lib/paint-pass-through.ts +76 -0
  332. package/src/lib/paint-region-hover.ts +25 -0
  333. package/src/lib/paint-region-mode.ts +154 -0
  334. package/src/lib/paint-regions.test.ts +166 -0
  335. package/src/lib/paint-regions.ts +192 -0
  336. package/src/lib/paint-scope.test.ts +317 -35
  337. package/src/lib/paint-scope.ts +266 -25
  338. package/src/lib/panel-rows.ts +21 -0
  339. package/src/lib/parametric-node-update.ts +1 -1
  340. package/src/lib/plate-paint-affected.test.ts +300 -0
  341. package/src/lib/plate-paint-affected.ts +177 -0
  342. package/src/lib/plugin-chat.ts +50 -0
  343. package/src/lib/plugin-panels.test.ts +25 -0
  344. package/src/lib/plugin-panels.ts +94 -0
  345. package/src/lib/print-shell-compiler.ts +1 -0
  346. package/src/lib/registered-draft-snap.ts +12 -0
  347. package/src/lib/roof-duplication.ts +10 -2
  348. package/src/lib/room-assembly-overlay.test.ts +275 -0
  349. package/src/lib/room-assembly-overlay.ts +266 -0
  350. package/src/lib/room-built-on.test.ts +379 -0
  351. package/src/lib/room-built-on.ts +138 -0
  352. package/src/lib/room-construction-commands.ts +712 -0
  353. package/src/lib/room-construction.test.ts +746 -0
  354. package/src/lib/room-construction.ts +385 -0
  355. package/src/lib/room-controls-phase6.test.ts +630 -0
  356. package/src/lib/room-divide-session.ts +523 -0
  357. package/src/lib/room-handle-drag.ts +1068 -0
  358. package/src/lib/room-name-catalog.test.ts +90 -0
  359. package/src/lib/room-name-catalog.ts +168 -0
  360. package/src/lib/room-push-dimensions.test.ts +311 -0
  361. package/src/lib/room-push-dimensions.ts +251 -0
  362. package/src/lib/room-selection-commands.ts +62 -0
  363. package/src/lib/room-selection.test.ts +973 -0
  364. package/src/lib/room-selection.ts +417 -0
  365. package/src/lib/room-structure-commands.ts +94 -0
  366. package/src/lib/room-structure.test.ts +921 -0
  367. package/src/lib/room-transform-session.ts +570 -0
  368. package/src/lib/room-zone-routing.test.ts +294 -0
  369. package/src/lib/room-zone-routing.ts +197 -0
  370. package/src/lib/scene-clipboard.test.ts +364 -0
  371. package/src/lib/scene-clipboard.ts +229 -48
  372. package/src/lib/scene-material-refs.test.ts +79 -0
  373. package/src/lib/scene-material-refs.ts +79 -0
  374. package/src/lib/scene.test.ts +24 -36
  375. package/src/lib/scene.ts +33 -57
  376. package/src/lib/selection-routing.test.ts +129 -45
  377. package/src/lib/selection-routing.ts +53 -74
  378. package/src/lib/session-writes.test.ts +120 -0
  379. package/src/lib/session-writes.ts +205 -0
  380. package/src/lib/sidebar-panel.ts +62 -0
  381. package/src/lib/site-boundary.test.ts +4 -6
  382. package/src/lib/site-boundary.ts +1 -1
  383. package/src/lib/snapping-mode.test.ts +4 -0
  384. package/src/lib/snapping-mode.ts +8 -1
  385. package/src/lib/terrace-draft.test.ts +145 -0
  386. package/src/lib/terrace-draft.ts +213 -0
  387. package/src/lib/tool-cursor-icon.test.ts +16 -0
  388. package/src/lib/tool-cursor-icon.ts +11 -0
  389. package/src/lib/typing-target.ts +9 -0
  390. package/src/lib/units.ts +77 -39
  391. package/src/lib/vector-edges.test.ts +58 -0
  392. package/src/lib/vector-edges.ts +400 -0
  393. package/src/lib/viewer-selection.test.ts +577 -0
  394. package/src/lib/viewer-selection.ts +569 -0
  395. package/src/lib/wall-draw-variant.test.ts +71 -0
  396. package/src/lib/wall-draw-variant.ts +91 -0
  397. package/src/lib/wall-paint-region-session.ts +246 -0
  398. package/src/lib/wall-paint-region.test.ts +373 -0
  399. package/src/lib/wall-polygon-draft.test.ts +262 -0
  400. package/src/lib/wall-polygon-draft.ts +286 -0
  401. package/src/lib/wall-push.test.ts +453 -0
  402. package/src/lib/wall-region-face.ts +352 -0
  403. package/src/lib/wall-region-handles.test.ts +304 -0
  404. package/src/lib/wall-region-handles.ts +368 -0
  405. package/src/lib/wall-region-list.test.ts +119 -0
  406. package/src/lib/wall-region-list.ts +136 -0
  407. package/src/lib/wall-region-snap.ts +110 -0
  408. package/src/lib/window-interaction.ts +14 -0
  409. package/src/lib/zone-content.ts +4 -1
  410. package/src/lib/zone-name.test.ts +17 -0
  411. package/src/lib/zone-name.ts +17 -0
  412. package/src/store/opens-in-select-mode.test.ts +223 -0
  413. package/src/store/paint-mode.test.ts +188 -0
  414. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  415. package/src/store/tool-mode.test.ts +137 -6
  416. package/src/store/use-delete-confirmation.ts +28 -1
  417. package/src/store/use-drawing-view.test.ts +24 -3
  418. package/src/store/use-drawing-view.ts +29 -1
  419. package/src/store/use-editor.tsx +432 -321
  420. package/src/store/use-floorplan-draft-preview.ts +13 -0
  421. package/src/store/use-floorplan-marquee.ts +12 -2
  422. package/src/store/use-quick-measurement-hud.ts +3 -3
  423. package/src/store/use-stair-build-preview.ts +13 -5
  424. package/src/store/use-wall-move-ghosts.ts +4 -2
  425. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  426. package/src/store/use-wall-region-handles-selection.ts +139 -0
  427. package/src/store/view-layout.test.ts +175 -0
  428. package/src/store/view-layout.ts +196 -0
  429. package/src/test-utils/immediate-animation-frames.ts +16 -0
  430. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  431. package/src/test-utils/selection-harness.ts +82 -0
  432. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  433. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  434. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  435. package/src/components/tools/fence/fence-drafting.ts +0 -266
  436. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  437. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  438. package/src/lib/floorplan/stairs.ts +0 -489
  439. package/src/lib/level-duplication.test.ts +0 -144
  440. package/src/lib/level-duplication.ts +0 -195
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Lot ring cleanup — a GIS parcel ring as the registry draws it, made fit
3
+ * for planning geometry (setbacks, the street edge, yard dimensions).
4
+ *
5
+ * What comes back from a parcel layer (seen live on the Land Park preset,
6
+ * 2026-09-06): the closing vertex repeated, points dropped along straight
7
+ * lines, and a curb-return CORNER drawn as nine ~1 m segments turning
8
+ * 8–12° each. Left alone, that ring breaks everything downstream: the
9
+ * street-facing "edge" the road detector picks is a 1 m sliver of the arc,
10
+ * the sliver takes the 20 ft front setback while the real 30 ft frontage
11
+ * beside it takes 5 ft, the offset lines cross and the setback envelope
12
+ * collapses to nothing — and the house lands at the origin, unplaced.
13
+ *
14
+ * Three passes, each pure and tested:
15
+ * 1. duplicates — consecutive vertices closer than `DUPLICATE_M` merge,
16
+ * the closing repeat goes;
17
+ * 2. collinear — a vertex whose turn is under `COLLINEAR_DEG` goes;
18
+ * 3. arcs — a run of short edges (each ≤ `ARC_EDGE_MAX_M`, the run ≤
19
+ * `ARC_RUN_MAX_M` long) between two longer edges that meet at
20
+ * ≥ `ARC_MIN_TURN_DEG` is a rounded or chamfered corner: its vertices
21
+ * are replaced by the corner the two long edges make when extended. A
22
+ * surveyed plat calls that point the lot corner; the envelope drawn
23
+ * from the squared edges sits inside the curved line by far more than
24
+ * the rounding took away, so it is the conservative reading for
25
+ * setbacks. The recorded lot area is never recomputed from it.
26
+ *
27
+ * Returns the cleaned ring with a count of what each pass removed, so the
28
+ * caller can say so in the parcel notes.
29
+ */
30
+ import type { Pt } from '../floorplan/site-plan/geometry'
31
+
32
+ export const DUPLICATE_M = 0.05
33
+ export const COLLINEAR_DEG = 1.5
34
+ /** A 10 ft corner cut (3.05 m) is a chamfer; a 15 ft one is a real side. */
35
+ export const ARC_EDGE_MAX_M = 3.5
36
+ export const ARC_RUN_MAX_M = 12
37
+ export const ARC_MIN_TURN_DEG = 20
38
+
39
+ export interface CleanRingResult {
40
+ points: Pt[]
41
+ removed: { duplicates: number; collinear: number; arcVertices: number; arcs: number }
42
+ }
43
+
44
+ const dist = (a: Pt, b: Pt): number => Math.hypot(b[0] - a[0], b[1] - a[1])
45
+
46
+ const turnDeg = (a: Pt, b: Pt, c: Pt): number => {
47
+ const ux = b[0] - a[0]
48
+ const uy = b[1] - a[1]
49
+ const vx = c[0] - b[0]
50
+ const vy = c[1] - b[1]
51
+ const lu = Math.hypot(ux, uy)
52
+ const lv = Math.hypot(vx, vy)
53
+ if (lu < 1e-12 || lv < 1e-12) return 0
54
+ return (Math.atan2(ux * vy - uy * vx, ux * vx + uy * vy) * 180) / Math.PI
55
+ }
56
+
57
+ /** Angle between two edge directions, degrees, unsigned. */
58
+ const angleBetweenDeg = (a0: Pt, a1: Pt, b0: Pt, b1: Pt): number => {
59
+ const ux = a1[0] - a0[0]
60
+ const uy = a1[1] - a0[1]
61
+ const vx = b1[0] - b0[0]
62
+ const vy = b1[1] - b0[1]
63
+ return Math.abs((Math.atan2(ux * vy - uy * vx, ux * vx + uy * vy) * 180) / Math.PI)
64
+ }
65
+
66
+ /** Pass 1: consecutive near-duplicates (and the closing repeat) merge. */
67
+ export function dropDuplicateVertices(points: readonly Pt[], tol = DUPLICATE_M): Pt[] {
68
+ const out: Pt[] = []
69
+ for (const p of points) {
70
+ const last = out[out.length - 1]
71
+ if (last && dist(last, p) < tol) continue
72
+ out.push([p[0], p[1]])
73
+ }
74
+ while (out.length > 1 && dist(out[0] as Pt, out[out.length - 1] as Pt) < tol) out.pop()
75
+ return out
76
+ }
77
+
78
+ /** Pass 2: vertices that do not turn go. */
79
+ export function mergeCollinearVertices(points: readonly Pt[], tolDeg = COLLINEAR_DEG): Pt[] {
80
+ let ring = points.map((p) => [p[0], p[1]] as Pt)
81
+ let changed = true
82
+ while (changed && ring.length > 3) {
83
+ changed = false
84
+ for (let i = 0; i < ring.length && ring.length > 3; i++) {
85
+ const n = ring.length
86
+ const a = ring[(i - 1 + n) % n] as Pt
87
+ const b = ring[i] as Pt
88
+ const c = ring[(i + 1) % n] as Pt
89
+ if (Math.abs(turnDeg(a, b, c)) < tolDeg) {
90
+ ring = ring.filter((_, k) => k !== i)
91
+ changed = true
92
+ i--
93
+ }
94
+ }
95
+ }
96
+ return ring
97
+ }
98
+
99
+ /** Intersection of the lines a0→a1 and b0→b1, or null when parallel. */
100
+ function lineIntersection(a0: Pt, a1: Pt, b0: Pt, b1: Pt): Pt | null {
101
+ const dx1 = a1[0] - a0[0]
102
+ const dy1 = a1[1] - a0[1]
103
+ const dx2 = b1[0] - b0[0]
104
+ const dy2 = b1[1] - b0[1]
105
+ const denom = dx1 * dy2 - dy1 * dx2
106
+ if (Math.abs(denom) < 1e-9) return null
107
+ const t = ((b0[0] - a0[0]) * dy2 - (b0[1] - a0[1]) * dx2) / denom
108
+ return [a0[0] + dx1 * t, a0[1] + dy1 * t]
109
+ }
110
+
111
+ /**
112
+ * Pass 3: rounded / chamfered corners squared. A run is a maximal set of
113
+ * consecutive SHORT edges between two LONG edges; every vertex of the run
114
+ * (the point where the long edge before it ends, through the point where
115
+ * the long edge after it starts) is replaced by the long edges'
116
+ * intersection when the run is short, the long edges meet at a real angle,
117
+ * and the corner point lands near the run.
118
+ */
119
+ export function squareCornerArcs(
120
+ points: readonly Pt[],
121
+ opts: { edgeMax?: number; runMax?: number; minTurnDeg?: number } = {},
122
+ ): { points: Pt[]; arcs: number; arcVertices: number } {
123
+ const edgeMax = opts.edgeMax ?? ARC_EDGE_MAX_M
124
+ const runMax = opts.runMax ?? ARC_RUN_MAX_M
125
+ const minTurn = opts.minTurnDeg ?? ARC_MIN_TURN_DEG
126
+ const ring = points.map((p) => [p[0], p[1]] as Pt)
127
+ const n = ring.length
128
+ const none = { points: ring, arcs: 0, arcVertices: 0 }
129
+ if (n < 4) return none
130
+
131
+ const at = (i: number) => ring[((i % n) + n) % n] as Pt
132
+ const short: boolean[] = []
133
+ for (let i = 0; i < n; i++) short.push(dist(at(i), at(i + 1)) <= edgeMax)
134
+ const start = short.indexOf(false)
135
+ if (start < 0) return none // every edge short: not a lot we can square
136
+
137
+ // Runs of short edges, as [first edge, last edge] in ring order.
138
+ const runs: [number, number][] = []
139
+ let k = 1
140
+ while (k <= n) {
141
+ const idx = (start + k) % n
142
+ if (!short[idx]) {
143
+ k++
144
+ continue
145
+ }
146
+ const first = idx
147
+ let last = idx
148
+ while (short[(last + 1) % n] && (last + 1) % n !== start) last = (last + 1) % n
149
+ runs.push([first, last])
150
+ k += ((last - first + n) % n) + 1
151
+ }
152
+
153
+ // Which vertices each qualifying run replaces, and with what.
154
+ const replaced = new Map<number, Pt | null>() // vertex → corner (first vertex) or null (dropped)
155
+ let arcs = 0
156
+ let arcVertices = 0
157
+ for (const [first, last] of runs) {
158
+ const before = (first - 1 + n) % n // long edge before the run
159
+ const after = (last + 1) % n // long edge after the run
160
+ if (short[before] || short[after]) continue
161
+ let runLen = 0
162
+ for (let e = first; ; e = (e + 1) % n) {
163
+ runLen += dist(at(e), at(e + 1))
164
+ if (e === last) break
165
+ }
166
+ if (runLen > runMax) continue
167
+ if (angleBetweenDeg(at(before), at(before + 1), at(after), at(after + 1)) < minTurn) continue
168
+ const corner = lineIntersection(at(before), at(before + 1), at(after), at(after + 1))
169
+ if (!corner) continue
170
+ if (dist(corner, at(first)) > runMax || dist(corner, at(after)) > runMax) continue
171
+ // vertices first .. after (inclusive) → the corner
172
+ const count = ((after - first + n) % n) + 1
173
+ for (let j = 0; j < count; j++) replaced.set((first + j) % n, j === 0 ? corner : null)
174
+ arcs++
175
+ arcVertices += count - 1
176
+ }
177
+ if (arcs === 0) return none
178
+
179
+ const out: Pt[] = []
180
+ for (let i = 0; i < n; i++) {
181
+ if (!replaced.has(i)) {
182
+ out.push(at(i))
183
+ continue
184
+ }
185
+ const corner = replaced.get(i)
186
+ if (corner) out.push(corner)
187
+ }
188
+ return { points: out, arcs, arcVertices }
189
+ }
190
+
191
+ /** All three passes. */
192
+ export function cleanLotRing(points: readonly Pt[]): CleanRingResult {
193
+ const deduped = dropDuplicateVertices(points)
194
+ const duplicates = points.length - deduped.length
195
+ const merged = mergeCollinearVertices(deduped)
196
+ const squared = squareCornerArcs(merged)
197
+ const final = mergeCollinearVertices(squared.points)
198
+ const collinear = deduped.length - merged.length + (squared.points.length - final.length)
199
+ return {
200
+ points: final,
201
+ removed: { duplicates, collinear, arcVertices: squared.arcVertices, arcs: squared.arcs },
202
+ }
203
+ }
204
+
205
+ /** One sentence for the parcel notes, or '' when nothing was removed. */
206
+ export function describeRingCleanup(before: number, result: CleanRingResult): string {
207
+ const r = result.removed
208
+ if (before === result.points.length) return ''
209
+ const parts = [
210
+ r.arcs > 0
211
+ ? `${r.arcs} rounded corner${r.arcs === 1 ? '' : 's'} squared (${r.arcVertices} arc vertices)`
212
+ : '',
213
+ r.collinear > 0 ? `${r.collinear} collinear point${r.collinear === 1 ? '' : 's'} merged` : '',
214
+ r.duplicates > 0 ? `${r.duplicates} duplicate${r.duplicates === 1 ? '' : 's'} dropped` : '',
215
+ ].filter(Boolean)
216
+ return `Lot ring simplified for planning: ${before} → ${result.points.length} vertices (${parts.join(', ')}); setbacks and the street edge use the simplified ring, the recorded lot area stands.`
217
+ }
@@ -0,0 +1,202 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import miami from './__fixtures__/map-dossier-miami-shores.json'
3
+ import stpete from './__fixtures__/map-dossier-stpete.json'
4
+ import { cleanLotRing } from './clean-ring'
5
+ import {
6
+ answered,
7
+ contourLinesFromDossier,
8
+ type Dossier,
9
+ describeDossier,
10
+ detectFrontEdgeFromFrontage,
11
+ frontageSegmentsMetres,
12
+ type ParcelData,
13
+ parcelRingMetres,
14
+ planPointFromLngLat,
15
+ setbacksFromZoning,
16
+ siteFactsFromDossier,
17
+ type ZoningData,
18
+ } from './dossier'
19
+ import { DEFAULT_SETBACKS_M, type ParcelResolveData, sitePatchFromParcel } from './lot-patch'
20
+
21
+ /**
22
+ * Two RECORDED Pascal Map dossiers (2026-09-07, `__fixtures__/map-dossier-*.json`):
23
+ * St Petersburg — every section answers, a 13-acre block with three
24
+ * frontage segments, zone AE, wind 150 mph, DC-3 form-based zoning;
25
+ * Miami Shores — no parcel plane, but flood / utilities / soils answer.
26
+ */
27
+ const STPETE = stpete as unknown as Dossier
28
+ const MIAMI = miami as unknown as Dossier
29
+
30
+ describe('projection', () => {
31
+ test('a point east and south of the origin reads +x, +z in metres', () => {
32
+ const p = planPointFromLngLat([-82.6, 27.7], [-82.599, 27.699])
33
+ expect(p[0]).toBeGreaterThan(0)
34
+ expect(p[1]).toBeGreaterThan(0)
35
+ // 0.001° of latitude ≈ 364 ft ≈ 111 m
36
+ expect(p[1]).toBeCloseTo(0.001 * 364000 * 0.3048, 3)
37
+ })
38
+ })
39
+
40
+ describe('the St Petersburg dossier', () => {
41
+ const parcel = answered<ParcelData>(STPETE, 'parcel')!
42
+ const origin: [number, number] = [STPETE.point.lng, STPETE.point.lat]
43
+
44
+ test('the parcel ring: the largest polygon, in plan metres, about 13 acres', () => {
45
+ const ring = parcelRingMetres(parcel, origin)
46
+ expect(ring.length).toBeGreaterThanOrEqual(4)
47
+ let a = 0
48
+ for (let i = 0; i < ring.length; i++) {
49
+ const p = ring[i]!
50
+ const q = ring[(i + 1) % ring.length]!
51
+ a += p[0] * q[1] - q[0] * p[1]
52
+ }
53
+ expect(Math.abs(a) / 2).toBeCloseTo(53022, -3) // area_m2 on the record, within a few hundred m²
54
+ })
55
+
56
+ test('the frontage: three street edges of the fabric become segments; the longest fronting edge of the cleaned ring is the front', () => {
57
+ const segments = frontageSegmentsMetres(parcel, origin)
58
+ expect(segments.length).toBeGreaterThanOrEqual(3)
59
+ const ring = cleanLotRing(parcelRingMetres(parcel, origin)).points
60
+ const match = detectFrontEdgeFromFrontage(ring, segments)
61
+ expect(match).not.toBeNull()
62
+ expect(match!.frontingEdges).toBeGreaterThanOrEqual(2) // a block: streets on more than one side
63
+ // the front is the longest fronting edge
64
+ for (let i = 0; i < ring.length; i++) {
65
+ const a = ring[i]!
66
+ const b = ring[(i + 1) % ring.length]!
67
+ const len = Math.hypot(b[0] - a[0], b[1] - a[1])
68
+ if (i !== match!.index && len > match!.lengthM + 1e-6) {
69
+ const other = detectFrontEdgeFromFrontage([a, b, [b[0] + 1, b[1] + 1]], segments)
70
+ // a longer edge exists only if it does not front
71
+ expect(other === null || other.index !== 0).toBe(true)
72
+ }
73
+ }
74
+ })
75
+
76
+ test('DC-3 form-based zoning: null setbacks stay null, the note is the code text', () => {
77
+ const zoning = answered<ZoningData>(STPETE, 'zoning')!
78
+ expect(zoning.district).toBe('DC-3')
79
+ expect(setbacksFromZoning(zoning)).toBeNull()
80
+ expect(zoning.dimensional_note).toContain('FAR')
81
+ expect(setbacksFromZoning({ setbacks: { front_ft: 25, side_ft: 7.5, rear_ft: 20 } })).toEqual({
82
+ front: 25 * 0.3048,
83
+ side: 7.5 * 0.3048,
84
+ rear: 20 * 0.3048,
85
+ })
86
+ })
87
+
88
+ test('the facts the site keeps: every section status, the flood / code basis / zoning / utilities data, no geometry, no neighbours', () => {
89
+ const facts = siteFactsFromDossier(STPETE)
90
+ expect(facts.provider).toBe('Pascal Map')
91
+ expect(facts.sections.parcel?.status).toBe('available')
92
+ expect(facts.sections.permits?.status).toBe('not_covered')
93
+ expect((facts.flood as { zone_at_point?: { zone?: string } }).zone_at_point?.zone).toBe('AE')
94
+ expect((facts.codeBasis as { wind_speed_mph?: number }).wind_speed_mph).toBe(150)
95
+ expect((facts.zoning as { district?: string }).district).toBe('DC-3')
96
+ expect((facts.utilities as { wastewater?: string }).wastewater).toBe('sewer')
97
+ expect(JSON.stringify(facts)).not.toContain('"geometry"')
98
+ expect((facts.parcel as Record<string, unknown>).adjacent_parcels).toBeUndefined()
99
+ expect(describeDossier(STPETE)).toContain('sections answered')
100
+ })
101
+
102
+ test('the lot patch: the frontage edge beats the road match; the facts ride the site', () => {
103
+ const ring = parcelRingMetres(parcel, origin)
104
+ const data: ParcelResolveData = {
105
+ ok: true,
106
+ polygonM: ring.map((p) => [p[0], p[1]]),
107
+ originLngLat: origin,
108
+ state: 'FL',
109
+ geocodedBy: 'pascal-map',
110
+ }
111
+ const segments = frontageSegmentsMetres(parcel, origin)
112
+ const facts = siteFactsFromDossier(STPETE)
113
+ const out = sitePatchFromParcel(
114
+ null,
115
+ { address: '501 5th Ave N' },
116
+ data,
117
+ null,
118
+ '2026-09-07T00:00:00Z',
119
+ {
120
+ frontageSegmentsM: segments,
121
+ setbacks: null,
122
+ dimensionalNote: 'No minimum lot area',
123
+ zone: 'DC-3',
124
+ facts,
125
+ line: 'Pascal Map: 12 sections answered',
126
+ },
127
+ )!
128
+ expect(out.summary.frontEdgeSource).toBe('frontage')
129
+ expect(out.summary.frontEdge).not.toBeNull()
130
+ expect(out.patch.frontEdge).toBe(out.summary.frontEdge!)
131
+ expect(out.patch.zone).toBe('DC-3')
132
+ // a conditional rule: the default stands and the code text is noted
133
+ expect(out.patch.setbacks).toEqual(DEFAULT_SETBACKS_M)
134
+ expect(out.summary.notes.some((n) => n.includes('Zoning condition'))).toBe(true)
135
+ expect(out.patch.dossier?.provider).toBe('Pascal Map')
136
+ // numbers from the code win over the default
137
+ const zoned = sitePatchFromParcel(null, { address: 'x' }, data, null, '2026-09-07T00:00:00Z', {
138
+ setbacks: { front: 7.62, side: 2.286, rear: 6.096 },
139
+ setbacksSource: 'Zoning R-1 — Section 17.212 — via Pascal Map',
140
+ facts,
141
+ line: '',
142
+ })!
143
+ expect(zoned.patch.setbacks).toEqual({ front: 7.62, side: 2.286, rear: 6.096 })
144
+ expect(zoned.patch.setbacksSource).toContain('Section 17.212')
145
+ expect(zoned.summary.setbacksDefaulted).toBe(false)
146
+ })
147
+ })
148
+
149
+ describe('the Miami Shores dossier (no parcel plane)', () => {
150
+ test('parcel not covered, flood and utilities answer; the ring is empty', () => {
151
+ expect(answered(MIAMI, 'parcel')).toBeNull()
152
+ expect(MIAMI.layers.parcel?.status).toBe('not_covered')
153
+ expect(
154
+ parcelRingMetres(answered<ParcelData>(MIAMI, 'parcel'), [MIAMI.point.lng, MIAMI.point.lat]),
155
+ ).toEqual([])
156
+ const facts = siteFactsFromDossier(MIAMI)
157
+ expect(facts.parcel).toBeUndefined()
158
+ expect((facts.flood as { zone_at_point?: { zone?: string } }).zone_at_point?.zone).toBe('X')
159
+ expect(
160
+ ((facts.utilities as { electric_providers?: { name: string }[] }).electric_providers ?? [])[0]
161
+ ?.name,
162
+ ).toContain('FLORIDA POWER')
163
+ expect(describeDossier(MIAMI)).toContain('parcel')
164
+ })
165
+ })
166
+
167
+ describe('the 3DEP contour lines', () => {
168
+ test('St Petersburg: 129 one-foot lines projected into the site frame, thinned, NAVD88', () => {
169
+ const origin: [number, number] = [STPETE.point.lng, STPETE.point.lat]
170
+ const lines = contourLinesFromDossier(STPETE, origin)
171
+ expect(lines).not.toBeNull()
172
+ expect(lines!.datum).toBe('NAVD88')
173
+ expect(lines!.intervalFt).toBe(1)
174
+ expect(lines!.source).toContain('3DEP')
175
+ expect(lines!.lines.length).toBe(129)
176
+ const elevations = new Set(lines!.lines.map((l) => l.elevationFt))
177
+ expect(Math.min(...elevations)).toBe(0)
178
+ expect(Math.max(...elevations)).toBeCloseTo(20, 0)
179
+ // every point within the parcel bbox's reach of the origin (a 13-acre block: a few hundred metres)
180
+ let n = 0
181
+ for (const l of lines!.lines) {
182
+ expect(l.points.length).toBeGreaterThanOrEqual(2)
183
+ for (const p of l.points) {
184
+ expect(Math.hypot(p[0], p[1])).toBeLessThan(600)
185
+ n++
186
+ }
187
+ for (let i = 1; i < l.points.length; i++) {
188
+ const a = l.points[i - 1]!
189
+ const b = l.points[i]!
190
+ expect(Math.hypot(a[0] - b[0], a[1] - b[1])).toBeGreaterThanOrEqual(0.3 - 1e-9)
191
+ }
192
+ }
193
+ // thinned below the 3501 recorded vertices
194
+ expect(n).toBeLessThan(3501)
195
+ expect(n).toBeGreaterThan(1000)
196
+ })
197
+
198
+ test('Miami Shores: the section answers without lines → null', () => {
199
+ const origin: [number, number] = [MIAMI.point.lng, MIAMI.point.lat]
200
+ expect(contourLinesFromDossier(MIAMI, origin)).toBeNull()
201
+ })
202
+ })