@pascal-app/editor 1.0.1 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +16 -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 +221 -128
- 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 +37 -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 +536 -487
- package/src/components/editor/footprint-height-handle.tsx +204 -0
- package/src/components/editor/grid.tsx +31 -7
- package/src/components/editor/group-actions.ts +59 -11
- package/src/components/editor/group-floating-action-menu.tsx +19 -9
- package/src/components/editor/group-move-3d.ts +4 -6
- package/src/components/editor/group-rotate-handle.tsx +81 -35
- package/src/components/editor/group-selection-box-3d.tsx +25 -17
- package/src/components/editor/group-transform-shared.test.ts +126 -1
- package/src/components/editor/group-transform-shared.ts +79 -73
- package/src/components/editor/handles/handle-arrow.tsx +72 -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 +272 -131
- package/src/components/editor/index.tsx +316 -176
- package/src/components/editor/join-walls-pill.tsx +44 -0
- package/src/components/editor/node-action-menu.tsx +70 -70
- package/src/components/editor/node-arrow-handles.tsx +167 -14
- 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 +132 -0
- 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 +114 -163
- package/src/components/editor/wall-opening-highlights.tsx +2 -3
- 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-group-move.tsx +17 -24
- 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-registered-tool-layer.tsx +33 -12
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +242 -126
- 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-measurement.tsx +166 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +87 -8
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +355 -245
- 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 +31 -66
- package/src/components/tools/__fixtures__/hosting.ts +10 -0
- package/src/components/tools/item/item-surface-placement.test.ts +3 -9
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +3 -9
- package/src/components/tools/item/placement-history.test.tsx +474 -0
- package/src/components/tools/item/placement-strategies.ts +63 -47
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +3 -3
- package/src/components/tools/item/use-draft-node.test.tsx +432 -1
- package/src/components/tools/item/use-draft-node.ts +207 -97
- package/src/components/tools/item/use-placement-coordinator.tsx +251 -111
- 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/item-surface-move.test.ts +10 -2
- package/src/components/tools/registry/item-surface-move.ts +46 -24
- package/src/components/tools/registry/item-surface-resolver.test.ts +2 -2
- package/src/components/tools/registry/move-registry-node-tool.tsx +26 -10
- 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/affordance-dispatch.test.ts +24 -0
- package/src/components/tools/shared/affordance-dispatch.ts +13 -0
- package/src/components/tools/shared/cursor-sphere.tsx +36 -12
- package/src/components/tools/shared/draft-measurement-label.tsx +38 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +27 -0
- package/src/components/tools/shared/pointer-support-cap.ts +17 -5
- package/src/components/tools/shared/surface-rejection.test.tsx +5 -4
- package/src/components/tools/shared/surface-rejection.tsx +14 -0
- 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 +82 -23
- 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 +101 -91
- package/src/components/ui/controls/material-picker.tsx +8 -67
- 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/controls/slider-control.tsx +76 -31
- 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 +348 -19
- package/src/components/ui/helpers/item-helper.tsx +8 -0
- package/src/components/ui/helpers/registered-tool-helper.tsx +10 -33
- package/src/components/ui/icon-ref.tsx +45 -1
- package/src/components/ui/item-catalog/item-catalog.tsx +5 -30
- 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-height-mode.tsx +27 -21
- 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 +7 -19
- 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/shortcut-token.tsx +4 -0
- 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/audio-settings-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +615 -372
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +7 -7
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
- 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-visibility.test.ts +35 -0
- package/src/components/viewer/floorplan-preview-visibility.ts +19 -0
- package/src/components/viewer/floorplan-preview.tsx +6 -18
- 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 +9 -9
- package/src/hooks/use-auto-save.integration.test.tsx +311 -0
- package/src/hooks/use-auto-save.ts +55 -14
- package/src/hooks/use-ceiling-events.test.ts +1 -0
- package/src/hooks/use-ceiling-events.ts +2 -0
- package/src/hooks/use-grid-events.test.ts +2 -0
- package/src/hooks/use-grid-events.ts +16 -11
- package/src/hooks/use-keyboard.test.ts +182 -0
- package/src/hooks/use-keyboard.ts +122 -91
- 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 +306 -39
- package/src/lib/catalog-panel-model.ts +39 -0
- package/src/lib/catalog-selection.test.ts +27 -0
- package/src/lib/catalog-selection.ts +8 -0
- 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/contextual-help.test.ts +27 -1
- package/src/lib/contextual-help.ts +16 -9
- 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 +15 -9
- package/src/lib/floorplan/floorplan-export.tsx +32 -8
- package/src/lib/floorplan/floorplan-extension.ts +48 -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 +191 -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 +544 -16
- package/src/lib/glb-export.ts +256 -125
- package/src/lib/grid-frame.test.ts +19 -0
- package/src/lib/grid-frame.ts +18 -0
- 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 +27 -1
- package/src/lib/keyboard-pan.test.ts +184 -0
- package/src/lib/keyboard-pan.ts +238 -0
- 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 +74 -0
- package/src/lib/material-paint-panel-model.ts +80 -0
- 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/overlay-transform.test.ts +28 -0
- package/src/lib/overlay-transform.ts +12 -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 +96 -0
- package/src/lib/panel-tool-options.test.ts +34 -0
- package/src/lib/panel-tool-options.ts +46 -0
- package/src/lib/parametric-node-update.ts +37 -0
- 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 +87 -0
- package/src/lib/scene.ts +39 -45
- 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 +17 -2
- package/src/lib/snapping-mode.ts +19 -7
- package/src/lib/spatial-drag-plane.test.ts +53 -0
- package/src/lib/spatial-drag-plane.ts +19 -0
- package/src/lib/spatial-pointer-input.test.ts +72 -0
- package/src/lib/spatial-pointer-input.ts +62 -0
- package/src/lib/spatial-snap.test.ts +47 -0
- package/src/lib/terrace-draft.test.ts +145 -0
- package/src/lib/terrace-draft.ts +213 -0
- package/src/lib/terrain-panel-model.ts +117 -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/world-grid-snap.test.ts +48 -0
- package/src/lib/world-grid-snap.ts +9 -5
- 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/rotation-snapping.test.ts +53 -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 +442 -323
- package/src/store/use-floorplan-draft-preview.ts +23 -1
- package/src/store/use-floorplan-marquee.ts +12 -2
- package/src/store/use-interaction-scope.ts +155 -6
- 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,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The street-facing lot edge from the mapped roads. Pure: no store, no
|
|
3
|
+
* network.
|
|
4
|
+
*
|
|
5
|
+
* For every lot edge, score how well a road fronts it:
|
|
6
|
+
* 1. PARALLELISM — the edge direction must be within `FRONT_EDGE_PARALLEL_DEG`
|
|
7
|
+
* of the nearest road segment (streets run ALONG a frontage; a road
|
|
8
|
+
* perpendicular to an edge is rejected).
|
|
9
|
+
* 2. OUTSIDE — the nearest road point must sit on the OUTWARD side of the
|
|
10
|
+
* edge: the street is off the lot, not a driveway cutting through it.
|
|
11
|
+
* 3. DISTANCE — among edges that pass, the smallest edge-midpoint-to-road
|
|
12
|
+
* distance wins (the closest fronting street); edges within
|
|
13
|
+
* `FRONT_EDGE_DISTANCE_TIE_M` of each other are a tie, and the LONGER
|
|
14
|
+
* edge takes it — a sliver left by a curb return or a split frontage
|
|
15
|
+
* never beats the frontage it sits beside.
|
|
16
|
+
* CORNER AND THROUGH LOTS: when the address's street matches a road, edges
|
|
17
|
+
* fronting THAT road are preferred over a merely-closer other street — the
|
|
18
|
+
* addressed street is the true front. The match keeps the street type and
|
|
19
|
+
* the directional (street-name.ts, USPS Pub. 28): "NW 34th St" is not
|
|
20
|
+
* "NW 34th Terrace".
|
|
21
|
+
*
|
|
22
|
+
* Coordinates are the site plan frame (metres, x east, y south), the frame
|
|
23
|
+
* the lot polygon and the OSM roads share.
|
|
24
|
+
*/
|
|
25
|
+
import { edgeLength, outwardNormal, type Pt } from './geometry'
|
|
26
|
+
import { parseStreetName, sameStreet } from './street-name'
|
|
27
|
+
|
|
28
|
+
export interface RoadCenterline {
|
|
29
|
+
name?: string
|
|
30
|
+
centerline: readonly Pt[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface FrontEdgeMatch {
|
|
34
|
+
/** Lot edge index: the edge runs from `points[index]` to `points[index + 1]`. */
|
|
35
|
+
index: number
|
|
36
|
+
/** Edge midpoint to the road centerline, metres. */
|
|
37
|
+
distance: number
|
|
38
|
+
/** The fronting road's name ('' when OSM has none). */
|
|
39
|
+
name: string
|
|
40
|
+
/** True when the road is the address's street — name, type and directional (corner / through-lot rule). */
|
|
41
|
+
named: boolean
|
|
42
|
+
/** Every lot edge a street runs along (parallel, outside, within 25 m): a corner lot lists two or more. */
|
|
43
|
+
streetEdges?: number[]
|
|
44
|
+
/** The street each of those edges runs along (the nearest road's name), by edge index; unnamed roads are left out. */
|
|
45
|
+
streetNames?: Record<string, string>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const FRONT_EDGE_PARALLEL_DEG = 30
|
|
49
|
+
export const FRONT_EDGE_DISTANCE_TIE_M = 1.5
|
|
50
|
+
|
|
51
|
+
/** `next` beats `best` when it is clearly nearer, or as near and longer. */
|
|
52
|
+
function better(
|
|
53
|
+
next: FrontEdgeMatch & { length: number },
|
|
54
|
+
best: (FrontEdgeMatch & { length: number }) | null,
|
|
55
|
+
): boolean {
|
|
56
|
+
if (!best) return true
|
|
57
|
+
if (next.distance < best.distance - FRONT_EDGE_DISTANCE_TIE_M) return true
|
|
58
|
+
if (next.distance > best.distance + FRONT_EDGE_DISTANCE_TIE_M) return false
|
|
59
|
+
return next.length > best.length
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Core street name: the house number, the directionals and the street type
|
|
64
|
+
* dropped — "2600 Castro Way" ↔ OSM "Castro Way" ↔ "S Castro"; OSM spells
|
|
65
|
+
* the quadrant out ("Northeast 109th Street" ↔ "NE 109th St"). The core
|
|
66
|
+
* alone does not say two lines are one street: `sameStreet` also compares
|
|
67
|
+
* the type and the directional.
|
|
68
|
+
*/
|
|
69
|
+
export function streetCore(s: string | null | undefined): string {
|
|
70
|
+
return parseStreetName(s).name
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The winning lot-edge index with its road, or null (keep the current / fallback edge). */
|
|
74
|
+
export function detectFrontEdgeFromRoads(
|
|
75
|
+
lot: readonly Pt[],
|
|
76
|
+
roads: readonly RoadCenterline[],
|
|
77
|
+
addressStreet?: string | null,
|
|
78
|
+
): FrontEdgeMatch | null {
|
|
79
|
+
const n = lot.length
|
|
80
|
+
if (n < 3 || roads.length === 0) return null
|
|
81
|
+
const sinLimit = Math.sin((FRONT_EDGE_PARALLEL_DEG * Math.PI) / 180)
|
|
82
|
+
const want = parseStreetName(addressStreet)
|
|
83
|
+
let best: (FrontEdgeMatch & { length: number }) | null = null
|
|
84
|
+
let bestNamed: (FrontEdgeMatch & { length: number }) | null = null
|
|
85
|
+
const streetEdges: number[] = []
|
|
86
|
+
const streetNames: Record<string, string> = {}
|
|
87
|
+
|
|
88
|
+
for (let i = 0; i < n; i++) {
|
|
89
|
+
const a = lot[i] as Pt
|
|
90
|
+
const b = lot[(i + 1) % n] as Pt
|
|
91
|
+
const len = edgeLength(lot, i)
|
|
92
|
+
if (len < 0.05) continue
|
|
93
|
+
const mid: Pt = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2]
|
|
94
|
+
const ex = (b[0] - a[0]) / len
|
|
95
|
+
const ey = (b[1] - a[1]) / len
|
|
96
|
+
const [nx, ny] = outwardNormal(lot, i)
|
|
97
|
+
|
|
98
|
+
// nearest road segment to this edge's midpoint, across every centerline
|
|
99
|
+
let near = Number.POSITIVE_INFINITY
|
|
100
|
+
let nearPt: Pt | null = null
|
|
101
|
+
let nearDir: Pt | null = null
|
|
102
|
+
let nearName = ''
|
|
103
|
+
for (const road of roads) {
|
|
104
|
+
const cl = road.centerline
|
|
105
|
+
for (let k = 0; k + 1 < cl.length; k++) {
|
|
106
|
+
const s0 = cl[k] as Pt
|
|
107
|
+
const s1 = cl[k + 1] as Pt
|
|
108
|
+
const dx = s1[0] - s0[0]
|
|
109
|
+
const dy = s1[1] - s0[1]
|
|
110
|
+
const dd = dx * dx + dy * dy
|
|
111
|
+
if (dd < 1e-12) continue
|
|
112
|
+
const t = Math.max(0, Math.min(1, ((mid[0] - s0[0]) * dx + (mid[1] - s0[1]) * dy) / dd))
|
|
113
|
+
const foot: Pt = [s0[0] + dx * t, s0[1] + dy * t]
|
|
114
|
+
const d = Math.hypot(mid[0] - foot[0], mid[1] - foot[1])
|
|
115
|
+
if (d < near) {
|
|
116
|
+
near = d
|
|
117
|
+
nearPt = foot
|
|
118
|
+
const sl = Math.sqrt(dd)
|
|
119
|
+
nearDir = [dx / sl, dy / sl]
|
|
120
|
+
nearName = road.name ?? ''
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (!nearPt || !nearDir) continue
|
|
125
|
+
// (1) parallel: |sin(angle between the edge and the road)| small
|
|
126
|
+
const sinAng = Math.abs(ex * nearDir[1] - ey * nearDir[0])
|
|
127
|
+
if (sinAng > sinLimit) continue
|
|
128
|
+
// (2) outside: the road lies on the outward side of the edge
|
|
129
|
+
if ((nearPt[0] - mid[0]) * nx + (nearPt[1] - mid[1]) * ny <= 0) continue
|
|
130
|
+
|
|
131
|
+
// a street runs along this edge (a corner lot collects two or more)
|
|
132
|
+
if (near <= 25 && len >= 3) {
|
|
133
|
+
streetEdges.push(i)
|
|
134
|
+
if (nearName) streetNames[String(i)] = nearName
|
|
135
|
+
}
|
|
136
|
+
const match = { index: i, distance: near, name: nearName, named: false, length: len }
|
|
137
|
+
if (better(match, best)) best = match
|
|
138
|
+
if (sameStreet(want, nearName) && better(match, bestNamed)) {
|
|
139
|
+
bestNamed = { ...match, named: true }
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const pick = bestNamed ?? best
|
|
143
|
+
return pick
|
|
144
|
+
? {
|
|
145
|
+
index: pick.index,
|
|
146
|
+
distance: pick.distance,
|
|
147
|
+
name: pick.name,
|
|
148
|
+
named: pick.named,
|
|
149
|
+
streetEdges: streetEdges.includes(pick.index) ? streetEdges : [pick.index, ...streetEdges],
|
|
150
|
+
streetNames:
|
|
151
|
+
pick.name && !streetNames[String(pick.index)]
|
|
152
|
+
? { ...streetNames, [String(pick.index)]: pick.name }
|
|
153
|
+
: streetNames,
|
|
154
|
+
}
|
|
155
|
+
: null
|
|
156
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
import { sightTriangle, streetCorners } from '@pascal-app/core'
|
|
3
|
+
import {
|
|
4
|
+
boundsInsidePolygon,
|
|
5
|
+
castYardDimensions,
|
|
6
|
+
classifyEdges,
|
|
7
|
+
edgeHeadingDeg,
|
|
8
|
+
formatFeetInches,
|
|
9
|
+
METRES_PER_FOOT,
|
|
10
|
+
mostNorthFacingEdge,
|
|
11
|
+
outwardNormal,
|
|
12
|
+
type Pt,
|
|
13
|
+
polygonArea,
|
|
14
|
+
rayToPolygon,
|
|
15
|
+
resolveFrontEdge,
|
|
16
|
+
setbackEnvelope,
|
|
17
|
+
setbackForRole,
|
|
18
|
+
} from './geometry'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 30 m (east-west) × 20 m (north-south) lot, origin at the north-west corner,
|
|
22
|
+
* wound clockwise in the plan frame (x east, y south).
|
|
23
|
+
*
|
|
24
|
+
* edge 0: north side (top, faces north)
|
|
25
|
+
* edge 1: east side
|
|
26
|
+
* edge 2: south side
|
|
27
|
+
* edge 3: west side
|
|
28
|
+
*/
|
|
29
|
+
const LOT: Pt[] = [
|
|
30
|
+
[0, 0],
|
|
31
|
+
[30, 0],
|
|
32
|
+
[30, 20],
|
|
33
|
+
[0, 20],
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
describe('outwardNormal', () => {
|
|
37
|
+
it('points out of the ring for every edge regardless of winding', () => {
|
|
38
|
+
const near = (i: number, x: number, y: number) => {
|
|
39
|
+
const n = outwardNormal(LOT, i)
|
|
40
|
+
expect(n[0]).toBeCloseTo(x, 9)
|
|
41
|
+
expect(n[1]).toBeCloseTo(y, 9)
|
|
42
|
+
}
|
|
43
|
+
near(0, 0, -1) // north edge faces north (−y)
|
|
44
|
+
near(1, 1, 0) // east
|
|
45
|
+
near(2, 0, 1) // south
|
|
46
|
+
near(3, -1, 0) // west
|
|
47
|
+
|
|
48
|
+
// Reversing the winding relabels the edges but normals still point out:
|
|
49
|
+
// reversed edge 0 runs [0,20] → [30,20], the south side.
|
|
50
|
+
const reversed = [...LOT].reverse()
|
|
51
|
+
const n0 = outwardNormal(reversed, 0)
|
|
52
|
+
expect(n0[0]).toBeCloseTo(0, 9)
|
|
53
|
+
expect(n0[1]).toBeCloseTo(1, 9)
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
describe('edge headings + front edge', () => {
|
|
58
|
+
it('reports compass headings clockwise from north', () => {
|
|
59
|
+
expect(edgeHeadingDeg(LOT, 0)).toBeCloseTo(0, 6)
|
|
60
|
+
expect(edgeHeadingDeg(LOT, 1)).toBeCloseTo(90, 6)
|
|
61
|
+
expect(edgeHeadingDeg(LOT, 2)).toBeCloseTo(180, 6)
|
|
62
|
+
expect(edgeHeadingDeg(LOT, 3)).toBeCloseTo(270, 6)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('honours northRotation', () => {
|
|
66
|
+
// Rotate true north 90° clockwise: the plan-up edge now faces west.
|
|
67
|
+
expect(edgeHeadingDeg(LOT, 0, Math.PI / 2)).toBeCloseTo(270, 6)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('picks the most north-facing edge as the fallback front', () => {
|
|
71
|
+
expect(mostNorthFacingEdge(LOT)).toBe(0)
|
|
72
|
+
expect(resolveFrontEdge(LOT, undefined)).toBe(0)
|
|
73
|
+
expect(resolveFrontEdge(LOT, 2)).toBe(2)
|
|
74
|
+
// Out-of-range indexes fall back rather than throw.
|
|
75
|
+
expect(resolveFrontEdge(LOT, 9)).toBe(0)
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
describe('classifyEdges', () => {
|
|
80
|
+
it('assigns front / rear and splits the remaining edges left and right', () => {
|
|
81
|
+
const roles = classifyEdges(LOT, 0)
|
|
82
|
+
expect(roles[0]).toBe('front')
|
|
83
|
+
expect(roles[2]).toBe('rear')
|
|
84
|
+
expect(new Set([roles[1], roles[3]])).toEqual(new Set(['left', 'right']))
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('resolves per-role setbacks with left/right overriding side', () => {
|
|
88
|
+
const s = { front: 7.6, side: 1.5, rear: 4.6, right: 3 }
|
|
89
|
+
expect(setbackForRole(s, 'front')).toBe(7.6)
|
|
90
|
+
expect(setbackForRole(s, 'rear')).toBe(4.6)
|
|
91
|
+
expect(setbackForRole(s, 'left')).toBe(1.5)
|
|
92
|
+
expect(setbackForRole(s, 'right')).toBe(3)
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
describe('setbackEnvelope', () => {
|
|
97
|
+
it('a corner lot: the second street side takes the street setback and the sight triangle clips the corner', () => {
|
|
98
|
+
// a 30 × 20 lot; edge 0 (north, y = 0) is the front, edge 1 (east, x = 30) the other street
|
|
99
|
+
const roles = classifyEdges(LOT, 0, [0, 1])
|
|
100
|
+
expect(roles[1]).toBe('street')
|
|
101
|
+
expect(roles[2]).toBe('rear')
|
|
102
|
+
expect(setbackForRole({ front: 7.5, side: 2, rear: 5 }, 'street')).toBe(7.5)
|
|
103
|
+
expect(setbackForRole({ front: 7.5, side: 2, rear: 5, streetSide: 4 }, 'street')).toBe(4)
|
|
104
|
+
const plain = setbackEnvelope(LOT, { front: 7.5, side: 2, rear: 5, streetSide: 4 }, 0, {
|
|
105
|
+
streetEdges: [0, 1],
|
|
106
|
+
})
|
|
107
|
+
const xs = plain.map((p) => p[0])
|
|
108
|
+
expect(Math.max(...xs)).toBeCloseTo(26, 6) // the east street side in 4, not the 2 ft side yard
|
|
109
|
+
// the 25 ft (7.62 m) sight triangle at the NE corner: its hypotenuse from (22.38, 0) to (30, 7.62) keeps the envelope out
|
|
110
|
+
const clipped = setbackEnvelope(LOT, { front: 7.5, side: 2, rear: 5, streetSide: 4 }, 0, {
|
|
111
|
+
streetEdges: [0, 1],
|
|
112
|
+
sightTriangleM: 7.62,
|
|
113
|
+
})
|
|
114
|
+
expect(clipped.length).toBeGreaterThanOrEqual(4)
|
|
115
|
+
for (const v of clipped) {
|
|
116
|
+
// outside the triangle: x + y ≥ 30 − 7.62 ... the hypotenuse is x − y = 22.38 ... points with x − y > 22.38 are inside it
|
|
117
|
+
expect(v[0] - v[1]).toBeLessThanOrEqual(22.38 + 0.05)
|
|
118
|
+
}
|
|
119
|
+
const tri = sightTriangle(LOT, 0, 1, 7.62)
|
|
120
|
+
expect(tri).not.toBeNull()
|
|
121
|
+
expect(tri!.corner[0]).toBeCloseTo(30, 6)
|
|
122
|
+
expect(tri!.corner[1]).toBeCloseTo(0, 6)
|
|
123
|
+
expect(streetCorners(LOT, [0, 1])).toEqual([[0, 1]])
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('a cul-de-sac corner (a run of short arc edges) is offset as one chord — no vertex runs away', () => {
|
|
127
|
+
const cape: [number, number][] = [
|
|
128
|
+
[-2, 0],
|
|
129
|
+
[-2, 30.1],
|
|
130
|
+
[-40, 32.7],
|
|
131
|
+
[-40, -5.6],
|
|
132
|
+
[-10.1, -7.6],
|
|
133
|
+
[-8.9, -7.6],
|
|
134
|
+
[-7.7, -7.3],
|
|
135
|
+
[-6.5, -6.9],
|
|
136
|
+
[-5.4, -6.3],
|
|
137
|
+
[-4.4, -5.5],
|
|
138
|
+
[-3.6, -4.6],
|
|
139
|
+
[-2.9, -3.6],
|
|
140
|
+
[-2.4, -2.4],
|
|
141
|
+
[-2.1, -1.2],
|
|
142
|
+
]
|
|
143
|
+
const env = setbackEnvelope(cape, { front: 6.096, side: 1.524, rear: 4.572 }, 0)
|
|
144
|
+
expect(env.length).toBeGreaterThanOrEqual(6)
|
|
145
|
+
// every envelope vertex lies inside the lot, at least its setback from every lot line
|
|
146
|
+
const segDist = (p: readonly number[], a: readonly number[], b: readonly number[]) => {
|
|
147
|
+
const abx = b[0]! - a[0]!
|
|
148
|
+
const aby = b[1]! - a[1]!
|
|
149
|
+
const t = Math.max(
|
|
150
|
+
0,
|
|
151
|
+
Math.min(1, ((p[0]! - a[0]!) * abx + (p[1]! - a[1]!) * aby) / (abx * abx + aby * aby)),
|
|
152
|
+
)
|
|
153
|
+
return Math.hypot(p[0]! - (a[0]! + abx * t), p[1]! - (a[1]! + aby * t))
|
|
154
|
+
}
|
|
155
|
+
// the curb return (edges 4–13) is street frontage: it takes the FRONT setback, never a side yard
|
|
156
|
+
const frontLike = new Set([0, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13])
|
|
157
|
+
for (const v of env) {
|
|
158
|
+
for (let i = 0; i < cape.length; i++) {
|
|
159
|
+
const want = frontLike.has(i) ? 6.096 : 1.524
|
|
160
|
+
expect(segDist(v, cape[i]!, cape[(i + 1) % cape.length]!)).toBeGreaterThan(want - 0.06)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// the front line (x = −2, the east side) offset 6.1 m in: two vertices on x ≈ −8.1
|
|
164
|
+
const onFront = env.filter((v) => Math.abs(v[0] - (-2 - 6.096)) < 0.05)
|
|
165
|
+
expect(onFront.length).toBeGreaterThanOrEqual(2)
|
|
166
|
+
// the return's offset is a curve of its own (a 6.1 m buffer round the return's points), not a chord: the
|
|
167
|
+
// envelope turns through more than one vertex between the front offset and the south offset
|
|
168
|
+
const turning = env.filter((v) => v[0] < -8.2 + 0.05 && v[1] < 2 && v[1] > -6.2)
|
|
169
|
+
expect(turning.length).toBeGreaterThanOrEqual(2)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('offsets each edge inward by its own setback', () => {
|
|
173
|
+
const env = setbackEnvelope(LOT, { front: 7.5, side: 2, rear: 5 }, 0)
|
|
174
|
+
expect(env).toHaveLength(4)
|
|
175
|
+
const xs = env.map((p) => p[0]).sort((a, b) => a - b)
|
|
176
|
+
const ys = env.map((p) => p[1]).sort((a, b) => a - b)
|
|
177
|
+
// North (front) edge in 7.5, south (rear) in 5, both sides in 2.
|
|
178
|
+
expect(xs[0]).toBeCloseTo(2, 9)
|
|
179
|
+
expect(xs[3]).toBeCloseTo(28, 9)
|
|
180
|
+
expect(ys[0]).toBeCloseTo(7.5, 9)
|
|
181
|
+
expect(ys[3]).toBeCloseTo(15, 9)
|
|
182
|
+
expect(polygonArea(env)).toBeCloseTo(26 * 7.5, 6)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('applies left/right overrides to the correct sides', () => {
|
|
186
|
+
const env = setbackEnvelope(LOT, { front: 0, side: 0, rear: 0, left: 4, right: 1 }, 0)
|
|
187
|
+
const xs = env.map((p) => p[0]).sort((a, b) => a - b)
|
|
188
|
+
// Front edge runs [0,0] → [30,0]; "left" is behind that direction (west).
|
|
189
|
+
expect(xs[0]).toBeCloseTo(4, 9)
|
|
190
|
+
expect(xs[3]).toBeCloseTo(29, 9)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('rejects setbacks that consume the lot instead of drawing a bow tie', () => {
|
|
194
|
+
expect(setbackEnvelope(LOT, { front: 12, side: 2, rear: 12 }, 0)).toEqual([])
|
|
195
|
+
expect(setbackEnvelope(LOT, { front: 0, side: 0, rear: 0 }, 0)).toEqual([])
|
|
196
|
+
expect(setbackEnvelope([[0, 0]], { front: 1, side: 1, rear: 1 }, 0)).toEqual([])
|
|
197
|
+
})
|
|
198
|
+
it('a frontage the county drew as two nearly straight lines takes one front yard, not an arc', () => {
|
|
199
|
+
// 1054 Rivera Dr, Sacramento as the parcel fabric draws it: the front (north, y ≈ −15.4) split
|
|
200
|
+
// at a kink of under 2° into edges 3 and 4; the street lookup failed, so only edge 4 came back
|
|
201
|
+
// as frontage
|
|
202
|
+
const rivera: Pt[] = [
|
|
203
|
+
[12.39, -15.35],
|
|
204
|
+
[12.77, 14.38],
|
|
205
|
+
[-14.32, 15.47],
|
|
206
|
+
[-14.34, -15.26],
|
|
207
|
+
[-1.53, -15.56],
|
|
208
|
+
]
|
|
209
|
+
const env = setbackEnvelope(rivera, { front: 6.096, side: 1.524, rear: 4.572 }, 4, {
|
|
210
|
+
streetEdges: [4],
|
|
211
|
+
})
|
|
212
|
+
// straight across the whole frontage (the kink's own corner, no curve round it): five corners at most
|
|
213
|
+
expect(env.length).toBeLessThanOrEqual(5)
|
|
214
|
+
const segDist = (p: readonly number[], a: readonly number[], b: readonly number[]) => {
|
|
215
|
+
const abx = b[0]! - a[0]!
|
|
216
|
+
const aby = b[1]! - a[1]!
|
|
217
|
+
const t = Math.max(
|
|
218
|
+
0,
|
|
219
|
+
Math.min(1, ((p[0]! - a[0]!) * abx + (p[1]! - a[1]!) * aby) / (abx * abx + aby * aby)),
|
|
220
|
+
)
|
|
221
|
+
return Math.hypot(p[0]! - (a[0]! + abx * t), p[1]! - (a[1]! + aby * t))
|
|
222
|
+
}
|
|
223
|
+
// both halves of the frontage keep the front yard
|
|
224
|
+
for (const v of env)
|
|
225
|
+
for (const i of [3, 4])
|
|
226
|
+
expect(segDist(v, rivera[i]!, rivera[(i + 1) % rivera.length]!)).toBeGreaterThan(
|
|
227
|
+
6.096 - 0.06,
|
|
228
|
+
)
|
|
229
|
+
})
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
describe('rayToPolygon + castYardDimensions', () => {
|
|
233
|
+
it('finds the nearest boundary crossing along a ray', () => {
|
|
234
|
+
const hit = rayToPolygon(LOT, [15, 10], 0, -1)
|
|
235
|
+
expect(hit?.distance).toBeCloseTo(10, 9)
|
|
236
|
+
expect(hit?.point[1]).toBeCloseTo(0, 9)
|
|
237
|
+
// A ray from outside pointing away never hits.
|
|
238
|
+
expect(rayToPolygon(LOT, [15, -5], 0, -1)).toBeNull()
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it('casts four yard dimensions from the footprint bbox edge midpoints', () => {
|
|
242
|
+
const yards = castYardDimensions(LOT, { minX: 10, minY: 6, maxX: 20, maxY: 14 })
|
|
243
|
+
expect(yards.map((y) => y.side).sort()).toEqual(['east', 'north', 'south', 'west'])
|
|
244
|
+
const by = Object.fromEntries(yards.map((y) => [y.side, y]))
|
|
245
|
+
expect(by.north?.distance).toBeCloseTo(6, 9)
|
|
246
|
+
expect(by.south?.distance).toBeCloseTo(6, 9)
|
|
247
|
+
expect(by.west?.distance).toBeCloseTo(10, 9)
|
|
248
|
+
expect(by.east?.distance).toBeCloseTo(10, 9)
|
|
249
|
+
// Dimensions start at the bbox midpoints, not the corners.
|
|
250
|
+
expect(by.north?.from).toEqual([15, 6])
|
|
251
|
+
expect(by.west?.from).toEqual([10, 10])
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
it('omits a side whose ray misses the lot rather than faking a number', () => {
|
|
255
|
+
// Footprint entirely north of (above) the lot: nothing casts north.
|
|
256
|
+
const yards = castYardDimensions(LOT, { minX: 10, minY: -30, maxX: 20, maxY: -25 })
|
|
257
|
+
expect(yards.map((y) => y.side)).not.toContain('north')
|
|
258
|
+
})
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
describe('boundsInsidePolygon', () => {
|
|
262
|
+
it('is true only when every bbox corner is inside the lot', () => {
|
|
263
|
+
expect(boundsInsidePolygon(LOT, { minX: 5, minY: 5, maxX: 25, maxY: 15 })).toBe(true)
|
|
264
|
+
expect(boundsInsidePolygon(LOT, { minX: -5, minY: 5, maxX: 25, maxY: 15 })).toBe(false)
|
|
265
|
+
expect(boundsInsidePolygon(LOT, { minX: 100, minY: 100, maxX: 110, maxY: 110 })).toBe(false)
|
|
266
|
+
})
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
describe('formatFeetInches', () => {
|
|
270
|
+
it('renders metres as feet-and-inches', () => {
|
|
271
|
+
expect(formatFeetInches(7.62)).toBe(`25'-0"`)
|
|
272
|
+
expect(formatFeetInches(METRES_PER_FOOT * 24.5)).toBe(`24'-6"`)
|
|
273
|
+
expect(formatFeetInches(0)).toBe(`0'-0"`)
|
|
274
|
+
})
|
|
275
|
+
})
|