@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.
- package/package.json +11 -9
- package/src/components/editor/action-menu-button.tsx +83 -0
- package/src/components/editor/bake-thumbnail.tsx +315 -4
- package/src/components/editor/capture-level-picker.tsx +72 -0
- package/src/components/editor/custom-camera-controls.tsx +202 -57
- package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
- package/src/components/editor/duplicate-node.ts +244 -0
- package/src/components/editor/editor-layout-mobile.tsx +36 -25
- package/src/components/editor/editor-layout-v2.tsx +131 -58
- package/src/components/editor/export-manager.tsx +34 -8
- package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
- package/src/components/editor/first-person/build-collider-world.ts +6 -2
- package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
- package/src/components/editor/first-person/mechanism-targets.ts +36 -0
- package/src/components/editor/first-person-controls.tsx +205 -2
- package/src/components/editor/floating-action-menu.tsx +63 -207
- package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
- package/src/components/editor/floor-aware-camera-controls.ts +324 -0
- package/src/components/editor/floor-openings-overlay.tsx +243 -0
- package/src/components/editor/floor-region-controls.tsx +846 -0
- package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
- package/src/components/editor/floor-surface.test.ts +165 -0
- package/src/components/editor/floor-surface.ts +182 -0
- package/src/components/editor/floorplan-background-selection.test.ts +11 -2
- package/src/components/editor/floorplan-background-selection.ts +1 -3
- package/src/components/editor/floorplan-panel.tsx +344 -342
- package/src/components/editor/footprint-height-handle.tsx +204 -0
- package/src/components/editor/group-actions.ts +54 -2
- package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
- package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
- package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
- package/src/components/editor/handles/use-handle-drag.ts +269 -197
- package/src/components/editor/index.tsx +294 -169
- package/src/components/editor/join-walls-pill.tsx +44 -0
- package/src/components/editor/node-action-menu.tsx +68 -70
- package/src/components/editor/node-arrow-handles.tsx +25 -6
- package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
- package/src/components/editor/open-wall-ends-hint.tsx +89 -0
- package/src/components/editor/quick-measurement-hud.tsx +3 -2
- package/src/components/editor/registry-action-contributions.test.tsx +63 -0
- package/src/components/editor/registry-action-contributions.tsx +80 -1
- package/src/components/editor/room-controls.tsx +847 -0
- package/src/components/editor/room-floor-highlight.tsx +128 -0
- package/src/components/editor/room-handles-2d.tsx +354 -0
- package/src/components/editor/room-handles.tsx +675 -0
- package/src/components/editor/room-highlight.tsx +225 -0
- package/src/components/editor/room-labels.test.ts +357 -0
- package/src/components/editor/room-labels.tsx +990 -0
- package/src/components/editor/room-transform-ghost.tsx +256 -0
- package/src/components/editor/room-webgpu-materials.test.ts +27 -0
- package/src/components/editor/selection-manager.tsx +451 -317
- package/src/components/editor/slab-hole-highlights.tsx +3 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
- package/src/components/editor/snapshot-capture.test.ts +52 -4
- package/src/components/editor/snapshot-capture.ts +30 -5
- package/src/components/editor/thumbnail-generator.test.ts +308 -0
- package/src/components/editor/thumbnail-generator.tsx +1035 -229
- package/src/components/editor/use-floorplan-background-placement.ts +31 -11
- package/src/components/editor/vector-edge-extractor.tsx +121 -0
- package/src/components/editor/views/host-view.tsx +61 -0
- package/src/components/editor/views/plugins-view.tsx +670 -0
- package/src/components/editor/views/scene-region.tsx +59 -0
- package/src/components/editor/views/use-editor-views.ts +57 -0
- package/src/components/editor/views/view-bar.tsx +220 -0
- package/src/components/editor/wall-justification.test.ts +140 -0
- package/src/components/editor/wall-move-side-handles.tsx +43 -142
- package/src/components/editor/wall-push-arrow.tsx +92 -0
- package/src/components/editor/wall-region-handles.tsx +509 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +113 -102
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
- package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +291 -227
- package/src/components/editor-2d/svg-paths.ts +12 -15
- package/src/components/procedural-items/inspector.tsx +40 -15
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
- package/src/components/systems/zone/zone-system.test.ts +47 -0
- package/src/components/systems/zone/zone-system.tsx +29 -68
- package/src/components/tools/item/placement-history.test.tsx +474 -0
- package/src/components/tools/item/placement-strategies.ts +8 -4
- package/src/components/tools/item/use-draft-node.test.tsx +432 -1
- package/src/components/tools/item/use-draft-node.ts +166 -95
- package/src/components/tools/item/use-placement-coordinator.tsx +93 -46
- package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
- package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +3 -1
- package/src/components/tools/select/box-select-tool.tsx +2 -12
- package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
- package/src/components/tools/select/select-candidates.test.ts +37 -4
- package/src/components/tools/select/select-candidates.ts +15 -11
- package/src/components/tools/shared/cursor-sphere.tsx +36 -12
- package/src/components/tools/site/site-boundary-editor.tsx +1 -2
- package/src/components/tools/stair/stair-defaults.ts +0 -3
- package/src/components/tools/stair/stair-tool.tsx +90 -42
- package/src/components/tools/tool-manager.tsx +46 -20
- package/src/components/tools/wall/wall-drafting.test.ts +19 -4
- package/src/components/tools/zone/zone-tool.tsx +4 -12
- package/src/components/ui/action-menu/action-button.tsx +3 -0
- package/src/components/ui/action-menu/camera-actions.tsx +2 -1
- package/src/components/ui/action-menu/control-modes.tsx +2 -35
- package/src/components/ui/action-menu/measurement-control.tsx +3 -10
- package/src/components/ui/build-panel/build-panel.tsx +235 -0
- package/src/components/ui/command-palette/editor-commands.tsx +4 -30
- package/src/components/ui/command-palette/index.tsx +5 -24
- package/src/components/ui/controls/finish-swatch.tsx +110 -0
- package/src/components/ui/controls/material-paint-panel.tsx +97 -26
- package/src/components/ui/controls/material-picker.tsx +4 -1
- package/src/components/ui/controls/scene-material-list.tsx +4 -32
- package/src/components/ui/controls/select-control.tsx +65 -0
- package/src/components/ui/floating-level-selector.tsx +9 -14
- package/src/components/ui/helpers/building-helper.tsx +4 -1
- package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
- package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
- package/src/components/ui/helpers/helper-manager.tsx +325 -14
- package/src/components/ui/helpers/item-helper.tsx +8 -0
- package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
- package/src/components/ui/icon-ref.tsx +45 -1
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/node-icon-image.tsx +16 -0
- package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
- package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
- package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
- package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
- package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
- package/src/components/ui/panels/multi-field-value.test.ts +26 -3
- package/src/components/ui/panels/multi-field-value.ts +6 -3
- package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
- package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
- package/src/components/ui/panels/node-display.ts +8 -3
- package/src/components/ui/panels/panel-manager.tsx +103 -68
- package/src/components/ui/panels/panel-wrapper.tsx +78 -36
- package/src/components/ui/panels/parametric-field-control.tsx +1 -1
- package/src/components/ui/panels/parametric-inspector.tsx +4 -4
- package/src/components/ui/panels/reference-panel.tsx +2 -3
- package/src/components/ui/panels/room-construction-rows.tsx +774 -0
- package/src/components/ui/panels/room-name-options.test.ts +55 -0
- package/src/components/ui/panels/room-name-options.ts +47 -0
- package/src/components/ui/panels/room-openings-field.tsx +110 -0
- package/src/components/ui/panels/room-panel.tsx +725 -0
- package/src/components/ui/panels/shape-choice.tsx +58 -0
- package/src/components/ui/panels/wall-region-list.tsx +109 -0
- package/src/components/ui/primitives/color-dot.tsx +36 -24
- package/src/components/ui/primitives/sidebar.tsx +2 -0
- package/src/components/ui/right-stack.test.ts +29 -0
- package/src/components/ui/right-stack.tsx +145 -0
- package/src/components/ui/sidebar/icon-rail.tsx +5 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +31 -4
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
- package/src/components/ui/sidebar/tab-bar.tsx +10 -0
- package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
- package/src/components/viewer/floorplan-compass-button.tsx +9 -1
- package/src/components/viewer/floorplan-preview.tsx +5 -6
- package/src/components/viewer/viewer-hover-label.tsx +34 -0
- package/src/components/viewer/viewer-rooms.tsx +175 -0
- package/src/components/viewer/viewer-scene-header.tsx +181 -145
- package/src/components/viewer/viewer-selection-manager.tsx +124 -0
- package/src/components/viewer/viewer-stage.tsx +2 -2
- package/src/components/viewer-zone-system.tsx +3 -8
- package/src/hooks/use-auto-save.integration.test.tsx +123 -9
- package/src/hooks/use-auto-save.ts +34 -8
- package/src/hooks/use-keyboard.test.ts +178 -3
- package/src/hooks/use-keyboard.ts +89 -71
- package/src/hooks/use-open-wall-ends.test.ts +99 -0
- package/src/hooks/use-open-wall-ends.ts +238 -0
- package/src/hooks/use-placement-notice.test.ts +48 -0
- package/src/hooks/use-placement-notice.ts +42 -0
- package/src/hooks/use-selected-room.ts +143 -0
- package/src/index.tsx +246 -39
- package/src/lib/ceiling-edit-session.test.ts +321 -0
- package/src/lib/ceiling-edit-session.ts +206 -0
- package/src/lib/ceiling-region-session.test.ts +179 -0
- package/src/lib/continuation.test.ts +19 -1
- package/src/lib/continuation.ts +25 -4
- package/src/lib/door-interaction.ts +14 -0
- package/src/lib/editor-level-display.test.ts +96 -0
- package/src/lib/editor-level-display.ts +48 -0
- package/src/lib/editor-views.ts +144 -0
- package/src/lib/export-geometry-wait.test.ts +90 -0
- package/src/lib/floor-edit-session.ts +153 -0
- package/src/lib/floor-footprints.test.ts +563 -0
- package/src/lib/floor-footprints.ts +395 -0
- package/src/lib/floor-foundation-support.test.ts +283 -0
- package/src/lib/floor-opening-draft.ts +218 -0
- package/src/lib/floor-openings.test.ts +216 -0
- package/src/lib/floor-region-geometry.ts +191 -0
- package/src/lib/floor-region-session.test.ts +485 -0
- package/src/lib/floor-region-session.ts +589 -0
- package/src/lib/floor-region-snap.ts +101 -0
- package/src/lib/floor-step-hover.test.ts +32 -0
- package/src/lib/floorplan/annotation-visibility.ts +17 -0
- package/src/lib/floorplan/dimension-drive.test.ts +360 -0
- package/src/lib/floorplan/dimension-drive.ts +479 -0
- package/src/lib/floorplan/floorplan-export.test.ts +12 -8
- package/src/lib/floorplan/floorplan-export.tsx +32 -8
- package/src/lib/floorplan/floorplan-extension.ts +32 -0
- package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
- package/src/lib/floorplan/floorplan-mode.ts +2 -0
- package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
- package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
- package/src/lib/floorplan/floorplan-readonly.ts +6 -0
- package/src/lib/floorplan/geometry.ts +13 -6
- package/src/lib/floorplan/index.ts +0 -9
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/marks.test.ts +213 -0
- package/src/lib/floorplan/marks.ts +238 -0
- package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
- package/src/lib/floorplan/open-wall-ends.ts +162 -0
- package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
- package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
- package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
- package/src/lib/floorplan/schedules.test.ts +189 -0
- package/src/lib/floorplan/schedules.ts +339 -0
- package/src/lib/floorplan/sheet-export.test.ts +100 -0
- package/src/lib/floorplan/sheet-export.ts +318 -0
- package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
- package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
- package/src/lib/floorplan/site-plan/contributors.ts +97 -0
- package/src/lib/floorplan/site-plan/coverage.ts +232 -0
- package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
- package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
- package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
- package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
- package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
- package/src/lib/floorplan/site-plan/geometry.ts +557 -0
- package/src/lib/floorplan/site-plan/index.ts +71 -0
- package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
- package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
- package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
- package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
- package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
- package/src/lib/floorplan/site-plan/street-name.ts +302 -0
- package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
- package/src/lib/floorplan/types.ts +1 -27
- package/src/lib/fresh-planar-placement.test.ts +36 -0
- package/src/lib/fresh-planar-placement.ts +59 -36
- package/src/lib/geometry-script/author.ts +152 -0
- package/src/lib/geometry-script/client.ts +152 -0
- package/src/lib/geometry-script/protocol.ts +12 -0
- package/src/lib/gesture-lifecycle.test.ts +595 -0
- package/src/lib/gesture-lifecycle.ts +290 -0
- package/src/lib/glb-export-zone-holes.test.ts +41 -0
- package/src/lib/glb-export.test.ts +485 -0
- package/src/lib/glb-export.ts +237 -119
- package/src/lib/history.test.ts +6 -0
- package/src/lib/history.ts +7 -0
- package/src/lib/hud-title.ts +109 -0
- package/src/lib/hydration-gated-storage.ts +49 -0
- package/src/lib/ifc-export.test.ts +174 -0
- package/src/lib/ifc-export.ts +201 -0
- package/src/lib/ifc-parts.ts +15 -0
- package/src/lib/inspector-expanded.test.ts +44 -0
- package/src/lib/inspector-expanded.ts +36 -0
- package/src/lib/interaction/scope.ts +18 -0
- package/src/lib/keyboard-pan.test.ts +129 -7
- package/src/lib/keyboard-pan.ts +161 -12
- package/src/lib/live-gesture-round-trip.test.ts +360 -0
- package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
- package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
- package/src/lib/lot/clean-ring.test.ts +186 -0
- package/src/lib/lot/clean-ring.ts +217 -0
- package/src/lib/lot/dossier.test.ts +202 -0
- package/src/lib/lot/dossier.ts +498 -0
- package/src/lib/lot/drop-in.test.ts +326 -0
- package/src/lib/lot/drop-in.ts +407 -0
- package/src/lib/lot/index.ts +43 -0
- package/src/lib/lot/lot-patch.test.ts +340 -0
- package/src/lib/lot/lot-patch.ts +394 -0
- package/src/lib/lot/parcel-provider.ts +43 -0
- package/src/lib/lot/terrain.test.ts +161 -0
- package/src/lib/lot/terrain.ts +266 -0
- package/src/lib/material-catalog-model.ts +3 -1
- package/src/lib/material-paint-panel-model.ts +9 -5
- package/src/lib/material-paint.ts +4 -0
- package/src/lib/mezzanine-actions.test.ts +342 -0
- package/src/lib/mezzanine-draft.test.ts +501 -0
- package/src/lib/mezzanine-draft.ts +278 -0
- package/src/lib/mezzanine-messages.ts +26 -0
- package/src/lib/mezzanine-selection.test.ts +207 -0
- package/src/lib/mezzanine-stairs.ts +46 -0
- package/src/lib/model-export.ts +3 -1
- package/src/lib/node-action-movement.ts +17 -6
- package/src/lib/node-display-label.ts +37 -0
- package/src/lib/orbit-floor-clearance.test.ts +165 -0
- package/src/lib/orbit-floor-clearance.ts +106 -0
- package/src/lib/paint-commit-route.test.ts +37 -0
- package/src/lib/paint-commit-route.ts +17 -0
- package/src/lib/paint-eyedropper.test.tsx +165 -0
- package/src/lib/paint-eyedropper.ts +99 -0
- package/src/lib/paint-pass-through.test.ts +111 -0
- package/src/lib/paint-pass-through.ts +76 -0
- package/src/lib/paint-region-hover.ts +25 -0
- package/src/lib/paint-region-mode.ts +154 -0
- package/src/lib/paint-regions.test.ts +166 -0
- package/src/lib/paint-regions.ts +192 -0
- package/src/lib/paint-scope.test.ts +317 -35
- package/src/lib/paint-scope.ts +266 -25
- package/src/lib/panel-rows.ts +21 -0
- package/src/lib/parametric-node-update.ts +1 -1
- package/src/lib/plate-paint-affected.test.ts +300 -0
- package/src/lib/plate-paint-affected.ts +177 -0
- package/src/lib/plugin-chat.ts +50 -0
- package/src/lib/plugin-panels.test.ts +25 -0
- package/src/lib/plugin-panels.ts +94 -0
- package/src/lib/print-shell-compiler.ts +1 -0
- package/src/lib/registered-draft-snap.ts +12 -0
- package/src/lib/roof-duplication.ts +10 -2
- package/src/lib/room-assembly-overlay.test.ts +275 -0
- package/src/lib/room-assembly-overlay.ts +266 -0
- package/src/lib/room-built-on.test.ts +379 -0
- package/src/lib/room-built-on.ts +138 -0
- package/src/lib/room-construction-commands.ts +712 -0
- package/src/lib/room-construction.test.ts +746 -0
- package/src/lib/room-construction.ts +385 -0
- package/src/lib/room-controls-phase6.test.ts +630 -0
- package/src/lib/room-divide-session.ts +523 -0
- package/src/lib/room-handle-drag.ts +1068 -0
- package/src/lib/room-name-catalog.test.ts +90 -0
- package/src/lib/room-name-catalog.ts +168 -0
- package/src/lib/room-push-dimensions.test.ts +311 -0
- package/src/lib/room-push-dimensions.ts +251 -0
- package/src/lib/room-selection-commands.ts +62 -0
- package/src/lib/room-selection.test.ts +973 -0
- package/src/lib/room-selection.ts +417 -0
- package/src/lib/room-structure-commands.ts +94 -0
- package/src/lib/room-structure.test.ts +921 -0
- package/src/lib/room-transform-session.ts +570 -0
- package/src/lib/room-zone-routing.test.ts +294 -0
- package/src/lib/room-zone-routing.ts +197 -0
- package/src/lib/scene-clipboard.test.ts +364 -0
- package/src/lib/scene-clipboard.ts +229 -48
- package/src/lib/scene-material-refs.test.ts +79 -0
- package/src/lib/scene-material-refs.ts +79 -0
- package/src/lib/scene.test.ts +24 -36
- package/src/lib/scene.ts +33 -57
- package/src/lib/selection-routing.test.ts +129 -45
- package/src/lib/selection-routing.ts +53 -74
- package/src/lib/session-writes.test.ts +120 -0
- package/src/lib/session-writes.ts +205 -0
- package/src/lib/sidebar-panel.ts +62 -0
- package/src/lib/site-boundary.test.ts +4 -6
- package/src/lib/site-boundary.ts +1 -1
- package/src/lib/snapping-mode.test.ts +4 -0
- package/src/lib/snapping-mode.ts +8 -1
- package/src/lib/terrace-draft.test.ts +145 -0
- package/src/lib/terrace-draft.ts +213 -0
- package/src/lib/tool-cursor-icon.test.ts +16 -0
- package/src/lib/tool-cursor-icon.ts +11 -0
- package/src/lib/typing-target.ts +9 -0
- package/src/lib/units.ts +77 -39
- package/src/lib/vector-edges.test.ts +58 -0
- package/src/lib/vector-edges.ts +400 -0
- package/src/lib/viewer-selection.test.ts +577 -0
- package/src/lib/viewer-selection.ts +569 -0
- package/src/lib/wall-draw-variant.test.ts +71 -0
- package/src/lib/wall-draw-variant.ts +91 -0
- package/src/lib/wall-paint-region-session.ts +246 -0
- package/src/lib/wall-paint-region.test.ts +373 -0
- package/src/lib/wall-polygon-draft.test.ts +262 -0
- package/src/lib/wall-polygon-draft.ts +286 -0
- package/src/lib/wall-push.test.ts +453 -0
- package/src/lib/wall-region-face.ts +352 -0
- package/src/lib/wall-region-handles.test.ts +304 -0
- package/src/lib/wall-region-handles.ts +368 -0
- package/src/lib/wall-region-list.test.ts +119 -0
- package/src/lib/wall-region-list.ts +136 -0
- package/src/lib/wall-region-snap.ts +110 -0
- package/src/lib/window-interaction.ts +14 -0
- package/src/lib/zone-content.ts +4 -1
- package/src/lib/zone-name.test.ts +17 -0
- package/src/lib/zone-name.ts +17 -0
- package/src/store/opens-in-select-mode.test.ts +223 -0
- package/src/store/paint-mode.test.ts +188 -0
- package/src/store/terrain-sculpt-mode.test.ts +19 -21
- package/src/store/tool-mode.test.ts +137 -6
- package/src/store/use-delete-confirmation.ts +28 -1
- package/src/store/use-drawing-view.test.ts +24 -3
- package/src/store/use-drawing-view.ts +29 -1
- package/src/store/use-editor.tsx +432 -321
- package/src/store/use-floorplan-draft-preview.ts +13 -0
- package/src/store/use-floorplan-marquee.ts +12 -2
- package/src/store/use-quick-measurement-hud.ts +3 -3
- package/src/store/use-stair-build-preview.ts +13 -5
- package/src/store/use-wall-move-ghosts.ts +4 -2
- package/src/store/use-wall-region-handles-selection.test.ts +222 -0
- package/src/store/use-wall-region-handles-selection.ts +139 -0
- package/src/store/view-layout.test.ts +175 -0
- package/src/store/view-layout.ts +196 -0
- package/src/test-utils/immediate-animation-frames.ts +16 -0
- package/src/test-utils/live-gesture-round-trip.ts +224 -0
- package/src/test-utils/selection-harness.ts +82 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
- package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
- package/src/components/tools/fence/fence-drafting.test.ts +0 -100
- package/src/components/tools/fence/fence-drafting.ts +0 -266
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
- package/src/components/viewer/viewer-units-panel.tsx +0 -116
- package/src/lib/floorplan/stairs.ts +0 -489
- package/src/lib/level-duplication.test.ts +0 -144
- 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
|
+
})
|