@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,557 @@
|
|
|
1
|
+
import { arcRuns, insetPolygon, type KeepOut, sightTriangle, streetCorners } from '@pascal-app/core'
|
|
2
|
+
/**
|
|
3
|
+
* Site-plan geometry — pure functions, no store, no React.
|
|
4
|
+
*
|
|
5
|
+
* All coordinates are SITE metres: origin = the geocoded point, x → east,
|
|
6
|
+
* y → south (the plan frame the site polygon is stored in). Consumers
|
|
7
|
+
* (2D editor, sheets) turn these into SVG through the existing renderer.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type Pt = readonly [number, number]
|
|
11
|
+
|
|
12
|
+
export interface Bounds {
|
|
13
|
+
minX: number
|
|
14
|
+
minY: number
|
|
15
|
+
maxX: number
|
|
16
|
+
maxY: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Yard sides in the order the site-plan dimensions are cast. */
|
|
20
|
+
export type YardSide = 'north' | 'south' | 'east' | 'west'
|
|
21
|
+
|
|
22
|
+
export interface SetbackInputs {
|
|
23
|
+
front: number
|
|
24
|
+
side: number
|
|
25
|
+
rear: number
|
|
26
|
+
left?: number
|
|
27
|
+
right?: number
|
|
28
|
+
/** A corner lot's second street side; absent = the front setback. */
|
|
29
|
+
streetSide?: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Per-edge classification of a lot polygon relative to its front edge. */
|
|
33
|
+
export type EdgeRole = 'front' | 'rear' | 'left' | 'right' | 'street' | 'street'
|
|
34
|
+
|
|
35
|
+
const EPS = 1e-9
|
|
36
|
+
|
|
37
|
+
export function polygonBounds(points: readonly Pt[]): Bounds {
|
|
38
|
+
let minX = Number.POSITIVE_INFINITY
|
|
39
|
+
let minY = Number.POSITIVE_INFINITY
|
|
40
|
+
let maxX = Number.NEGATIVE_INFINITY
|
|
41
|
+
let maxY = Number.NEGATIVE_INFINITY
|
|
42
|
+
for (const [x, y] of points) {
|
|
43
|
+
if (x < minX) minX = x
|
|
44
|
+
if (x > maxX) maxX = x
|
|
45
|
+
if (y < minY) minY = y
|
|
46
|
+
if (y > maxY) maxY = y
|
|
47
|
+
}
|
|
48
|
+
if (!Number.isFinite(minX)) return { minX: 0, minY: 0, maxX: 0, maxY: 0 }
|
|
49
|
+
return { minX, minY, maxX, maxY }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function polygonArea(points: readonly Pt[]): number {
|
|
53
|
+
let a = 0
|
|
54
|
+
for (let i = 0, n = points.length; i < n; i++) {
|
|
55
|
+
const p = points[i] as Pt
|
|
56
|
+
const q = points[(i + 1) % n] as Pt
|
|
57
|
+
a += p[0] * q[1] - q[0] * p[1]
|
|
58
|
+
}
|
|
59
|
+
return Math.abs(a / 2)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function polygonCentroid(points: readonly Pt[]): Pt {
|
|
63
|
+
let cx = 0
|
|
64
|
+
let cy = 0
|
|
65
|
+
let a = 0
|
|
66
|
+
for (let i = 0, n = points.length; i < n; i++) {
|
|
67
|
+
const p = points[i] as Pt
|
|
68
|
+
const q = points[(i + 1) % n] as Pt
|
|
69
|
+
const cross = p[0] * q[1] - q[0] * p[1]
|
|
70
|
+
a += cross
|
|
71
|
+
cx += (p[0] + q[0]) * cross
|
|
72
|
+
cy += (p[1] + q[1]) * cross
|
|
73
|
+
}
|
|
74
|
+
if (Math.abs(a) < EPS) {
|
|
75
|
+
// Degenerate ring — fall back to the vertex average.
|
|
76
|
+
const n = Math.max(1, points.length)
|
|
77
|
+
let sx = 0
|
|
78
|
+
let sy = 0
|
|
79
|
+
for (const [x, y] of points) {
|
|
80
|
+
sx += x
|
|
81
|
+
sy += y
|
|
82
|
+
}
|
|
83
|
+
return [sx / n, sy / n]
|
|
84
|
+
}
|
|
85
|
+
a /= 2
|
|
86
|
+
return [cx / (6 * a), cy / (6 * a)]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** True when the ring is wound counter-clockwise in a y-down (south-positive) frame. */
|
|
90
|
+
export function isCounterClockwise(points: readonly Pt[]): boolean {
|
|
91
|
+
let a = 0
|
|
92
|
+
for (let i = 0, n = points.length; i < n; i++) {
|
|
93
|
+
const p = points[i] as Pt
|
|
94
|
+
const q = points[(i + 1) % n] as Pt
|
|
95
|
+
a += p[0] * q[1] - q[0] * p[1]
|
|
96
|
+
}
|
|
97
|
+
return a > 0
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Unit normal of edge `i` pointing OUT of the polygon, winding-agnostic.
|
|
102
|
+
*/
|
|
103
|
+
export function outwardNormal(points: readonly Pt[], i: number): Pt {
|
|
104
|
+
const n = points.length
|
|
105
|
+
const p = points[i % n] as Pt
|
|
106
|
+
const q = points[(i + 1) % n] as Pt
|
|
107
|
+
const dx = q[0] - p[0]
|
|
108
|
+
const dy = q[1] - p[1]
|
|
109
|
+
const len = Math.hypot(dx, dy) || 1
|
|
110
|
+
// Left normal of the edge direction. For a CCW ring (positive shoelace in
|
|
111
|
+
// this frame) the left normal points inward, so flip.
|
|
112
|
+
const sign = isCounterClockwise(points) ? -1 : 1
|
|
113
|
+
return [(sign * -dy) / len, (sign * dx) / len]
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Compass heading of edge `i`'s outward normal, in DEGREES clockwise from
|
|
118
|
+
* north, where `northRotation` (radians, clockwise) is true north's offset
|
|
119
|
+
* from plan up (−y). This is the direction the edge faces.
|
|
120
|
+
*/
|
|
121
|
+
export function edgeHeadingDeg(points: readonly Pt[], i: number, northRotation = 0): number {
|
|
122
|
+
const [nx, ny] = outwardNormal(points, i)
|
|
123
|
+
// Plan up (−y) is 0°, +x (east) is 90°.
|
|
124
|
+
const raw = Math.atan2(nx, -ny) - northRotation
|
|
125
|
+
const deg = (raw * 180) / Math.PI
|
|
126
|
+
return ((deg % 360) + 360) % 360
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const COMPASS = [
|
|
130
|
+
'N',
|
|
131
|
+
'NNE',
|
|
132
|
+
'NE',
|
|
133
|
+
'ENE',
|
|
134
|
+
'E',
|
|
135
|
+
'ESE',
|
|
136
|
+
'SE',
|
|
137
|
+
'SSE',
|
|
138
|
+
'S',
|
|
139
|
+
'SSW',
|
|
140
|
+
'SW',
|
|
141
|
+
'WSW',
|
|
142
|
+
'W',
|
|
143
|
+
'WNW',
|
|
144
|
+
'NW',
|
|
145
|
+
'NNW',
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
export function compassLabel(headingDeg: number): string {
|
|
149
|
+
const idx = Math.round((((headingDeg % 360) + 360) % 360) / 22.5) % 16
|
|
150
|
+
return COMPASS[idx] as string
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Length of edge `i`, metres. */
|
|
154
|
+
export function edgeLength(points: readonly Pt[], i: number): number {
|
|
155
|
+
const n = points.length
|
|
156
|
+
const p = points[i % n] as Pt
|
|
157
|
+
const q = points[(i + 1) % n] as Pt
|
|
158
|
+
return Math.hypot(q[0] - p[0], q[1] - p[1])
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Fallback front edge: the edge whose outward normal points closest to true
|
|
163
|
+
* north. Ties break toward the longer edge — the street frontage of a typical
|
|
164
|
+
* lot is one of its long sides.
|
|
165
|
+
*/
|
|
166
|
+
export function mostNorthFacingEdge(points: readonly Pt[], northRotation = 0): number {
|
|
167
|
+
let best = 0
|
|
168
|
+
let bestScore = Number.POSITIVE_INFINITY
|
|
169
|
+
for (let i = 0; i < points.length; i++) {
|
|
170
|
+
const heading = edgeHeadingDeg(points, i, northRotation)
|
|
171
|
+
const off = Math.min(heading, 360 - heading)
|
|
172
|
+
// Sub-degree ties resolve on length so a chamfer corner never wins.
|
|
173
|
+
const score = off - Math.min(0.5, edgeLength(points, i) / 1000)
|
|
174
|
+
if (score < bestScore) {
|
|
175
|
+
bestScore = score
|
|
176
|
+
best = i
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return best
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** `frontEdge` if it indexes a real edge, else the most north-facing edge. */
|
|
183
|
+
export function resolveFrontEdge(
|
|
184
|
+
points: readonly Pt[],
|
|
185
|
+
frontEdge: number | undefined,
|
|
186
|
+
northRotation = 0,
|
|
187
|
+
): number {
|
|
188
|
+
if (
|
|
189
|
+
typeof frontEdge === 'number' &&
|
|
190
|
+
Number.isInteger(frontEdge) &&
|
|
191
|
+
frontEdge >= 0 &&
|
|
192
|
+
frontEdge < points.length
|
|
193
|
+
) {
|
|
194
|
+
return frontEdge
|
|
195
|
+
}
|
|
196
|
+
return mostNorthFacingEdge(points, northRotation)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Classify every edge as front / rear / left / right.
|
|
201
|
+
*
|
|
202
|
+
* - front = `frontIndex`.
|
|
203
|
+
* - rear = the edge whose outward normal is most anti-parallel to the front's.
|
|
204
|
+
* - the rest are sides. LEFT / RIGHT are defined along the FRONT EDGE
|
|
205
|
+
* DIRECTION (`points[front] → points[front + 1]`): an edge whose midpoint
|
|
206
|
+
* projects behind the front-edge midpoint on that axis is `left`, ahead of
|
|
207
|
+
* it is `right`. Deterministic and independent of winding.
|
|
208
|
+
*/
|
|
209
|
+
export function classifyEdges(
|
|
210
|
+
points: readonly Pt[],
|
|
211
|
+
frontIndex: number,
|
|
212
|
+
streetEdges: readonly number[] = [],
|
|
213
|
+
): EdgeRole[] {
|
|
214
|
+
const n = points.length
|
|
215
|
+
const roles: EdgeRole[] = new Array(n).fill('left')
|
|
216
|
+
if (n === 0) return roles
|
|
217
|
+
|
|
218
|
+
const front = ((frontIndex % n) + n) % n
|
|
219
|
+
const fn = outwardNormal(points, front)
|
|
220
|
+
// a corner lot's other street edges (the parcel fabric's frontage) are
|
|
221
|
+
// 'street' — they take the street-side setback, never a side yard, and
|
|
222
|
+
// the rear is never one of them
|
|
223
|
+
const street = new Set(streetEdges.map((i) => ((i % n) + n) % n))
|
|
224
|
+
street.delete(front)
|
|
225
|
+
|
|
226
|
+
let rear = -1
|
|
227
|
+
let rearDot = Number.POSITIVE_INFINITY
|
|
228
|
+
for (let i = 0; i < n; i++) {
|
|
229
|
+
if (i === front || street.has(i)) continue
|
|
230
|
+
const ni = outwardNormal(points, i)
|
|
231
|
+
const dot = fn[0] * ni[0] + fn[1] * ni[1]
|
|
232
|
+
if (dot < rearDot) {
|
|
233
|
+
rearDot = dot
|
|
234
|
+
rear = i
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const fp = points[front] as Pt
|
|
239
|
+
const fq = points[(front + 1) % n] as Pt
|
|
240
|
+
const dx = fq[0] - fp[0]
|
|
241
|
+
const dy = fq[1] - fp[1]
|
|
242
|
+
const len = Math.hypot(dx, dy) || 1
|
|
243
|
+
const ux = dx / len
|
|
244
|
+
const uy = dy / len
|
|
245
|
+
const fmx = (fp[0] + fq[0]) / 2
|
|
246
|
+
const fmy = (fp[1] + fq[1]) / 2
|
|
247
|
+
|
|
248
|
+
for (let i = 0; i < n; i++) {
|
|
249
|
+
if (i === front) {
|
|
250
|
+
roles[i] = 'front'
|
|
251
|
+
continue
|
|
252
|
+
}
|
|
253
|
+
if (street.has(i)) {
|
|
254
|
+
roles[i] = 'street'
|
|
255
|
+
continue
|
|
256
|
+
}
|
|
257
|
+
if (i === rear) {
|
|
258
|
+
roles[i] = 'rear'
|
|
259
|
+
continue
|
|
260
|
+
}
|
|
261
|
+
const p = points[i] as Pt
|
|
262
|
+
const q = points[(i + 1) % n] as Pt
|
|
263
|
+
const mx = (p[0] + q[0]) / 2 - fmx
|
|
264
|
+
const my = (p[1] + q[1]) / 2 - fmy
|
|
265
|
+
roles[i] = mx * ux + my * uy < 0 ? 'left' : 'right'
|
|
266
|
+
}
|
|
267
|
+
return roles
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Required setback distance (metres) for each edge role. */
|
|
271
|
+
export function setbackForRole(setbacks: SetbackInputs, role: EdgeRole): number {
|
|
272
|
+
switch (role) {
|
|
273
|
+
case 'front':
|
|
274
|
+
return setbacks.front
|
|
275
|
+
case 'street':
|
|
276
|
+
return setbacks.streetSide ?? setbacks.front
|
|
277
|
+
case 'rear':
|
|
278
|
+
return setbacks.rear
|
|
279
|
+
case 'left':
|
|
280
|
+
return setbacks.left ?? setbacks.side
|
|
281
|
+
case 'right':
|
|
282
|
+
return setbacks.right ?? setbacks.side
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Buildable envelope — every lot edge pushed INWARD by its own setback, with
|
|
288
|
+
* the new vertices taken as the intersections of adjacent offset lines
|
|
289
|
+
* (variable-distance straight-skeleton-lite). Exact for convex lots; on a
|
|
290
|
+
* concave lot a reflex corner can self-intersect, in which case the caller
|
|
291
|
+
* still gets a ring but it is advisory, not a legal envelope.
|
|
292
|
+
*
|
|
293
|
+
* Adjacent offset lines that are parallel (a straight run split by a
|
|
294
|
+
* surplus vertex) have no intersection: the shared vertex is then simply
|
|
295
|
+
* pushed inward along the current edge's own offset, instead of the whole
|
|
296
|
+
* envelope being refused.
|
|
297
|
+
*
|
|
298
|
+
* Returns `[]` when the polygon has fewer than 3 points or when the result
|
|
299
|
+
* inverts (setbacks larger than the lot).
|
|
300
|
+
*/
|
|
301
|
+
export function setbackEnvelope(
|
|
302
|
+
points: readonly Pt[],
|
|
303
|
+
setbacks: SetbackInputs,
|
|
304
|
+
frontIndex: number,
|
|
305
|
+
options: { streetEdges?: readonly number[]; sightTriangleM?: number } = {},
|
|
306
|
+
): Pt[] {
|
|
307
|
+
const n = points.length
|
|
308
|
+
if (n < 3) return []
|
|
309
|
+
const streetEdges = options.streetEdges ?? []
|
|
310
|
+
const roles = classifyEdges(points, frontIndex, streetEdges)
|
|
311
|
+
// A radius drawn as a run of short edges takes ONE role. A curb return
|
|
312
|
+
// is street frontage: a run carrying, or touching, a front / street edge
|
|
313
|
+
// is front / street all along (otherwise the last sliver of the return
|
|
314
|
+
// became a side yard, and the envelope jutted out toward the corner); a
|
|
315
|
+
// run carrying the rear is rear; else
|
|
316
|
+
// each sliver keeps its own side.
|
|
317
|
+
const runs = arcRuns(points)
|
|
318
|
+
const runRole = new Map<number, EdgeRole>()
|
|
319
|
+
const streetLike = (r: EdgeRole) => r === 'front' || r === 'street'
|
|
320
|
+
for (let i = 0; i < n; i++) {
|
|
321
|
+
const r = roles[i] as EdgeRole
|
|
322
|
+
const g = runs[i] as number
|
|
323
|
+
const have = runRole.get(g)
|
|
324
|
+
if (streetLike(r) || (r === 'rear' && !(have && streetLike(have)))) runRole.set(g, r)
|
|
325
|
+
else if (!have) runRole.set(g, r)
|
|
326
|
+
}
|
|
327
|
+
for (let i = 0; i < n; i++) {
|
|
328
|
+
const g = runs[i] as number
|
|
329
|
+
const have = runRole.get(g) as EdgeRole
|
|
330
|
+
if (streetLike(have)) continue
|
|
331
|
+
const before = roles[(i - 1 + n) % n] as EdgeRole
|
|
332
|
+
const after = roles[(i + 1) % n] as EdgeRole
|
|
333
|
+
const members = runs.filter((x) => x === g).length
|
|
334
|
+
if (members > 1) {
|
|
335
|
+
if (runs[(i - 1 + n) % n] !== g && streetLike(before)) runRole.set(g, before)
|
|
336
|
+
else if (runs[(i + 1) % n] !== g && streetLike(after)) runRole.set(g, after)
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
// One lot line drawn as two: the county fabric splits a frontage at a kink of
|
|
340
|
+
// a degree or two, and only one half may come back from the street lookup
|
|
341
|
+
// (or the north-facing fallback). The half left out took a side yard, and
|
|
342
|
+
// the front yard swung round their shared corner in a circle as deep as
|
|
343
|
+
// itself. An edge running on nearly straight from a front or street edge
|
|
344
|
+
// is that frontage too.
|
|
345
|
+
const roleOf = (i: number) =>
|
|
346
|
+
runRole.get(runs[(i + n) % n] as number) ?? (roles[(i + n) % n] as EdgeRole)
|
|
347
|
+
const direction = (i: number) => {
|
|
348
|
+
const p = points[(i + n) % n] as Pt
|
|
349
|
+
const q = points[(i + 1 + n) % n] as Pt
|
|
350
|
+
const l = Math.hypot(q[0] - p[0], q[1] - p[1]) || 1
|
|
351
|
+
return [(q[0] - p[0]) / l, (q[1] - p[1]) / l] as const
|
|
352
|
+
}
|
|
353
|
+
const straightOn = Math.cos((8 * Math.PI) / 180)
|
|
354
|
+
const continued = new Map<number, EdgeRole>()
|
|
355
|
+
for (let pass = 0; pass < n; pass++) {
|
|
356
|
+
let grew = false
|
|
357
|
+
for (let i = 0; i < n; i++) {
|
|
358
|
+
const own = continued.get(i) ?? roleOf(i)
|
|
359
|
+
if (streetLike(own)) continue
|
|
360
|
+
const d = direction(i)
|
|
361
|
+
for (const j of [i - 1, i + 1]) {
|
|
362
|
+
const k = (j + n) % n
|
|
363
|
+
const theirs = continued.get(k) ?? roleOf(k)
|
|
364
|
+
const e = direction(k)
|
|
365
|
+
if (streetLike(theirs) && d[0] * e[0] + d[1] * e[1] > straightOn) {
|
|
366
|
+
continued.set(i, theirs)
|
|
367
|
+
grew = true
|
|
368
|
+
break
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (!grew) break
|
|
373
|
+
}
|
|
374
|
+
const distances: number[] = []
|
|
375
|
+
for (let i = 0; i < n; i++) {
|
|
376
|
+
const role = continued.get(i) ?? runRole.get(runs[i] as number) ?? (roles[i] as EdgeRole)
|
|
377
|
+
const d = setbackForRole(
|
|
378
|
+
setbacks,
|
|
379
|
+
role === 'left' || role === 'right' ? (roles[i] as EdgeRole) : role,
|
|
380
|
+
)
|
|
381
|
+
if (!Number.isFinite(d) || d < 0) return []
|
|
382
|
+
distances.push(d)
|
|
383
|
+
}
|
|
384
|
+
// the corner sight triangles keep the envelope out of the clear-vision
|
|
385
|
+
// zone at each street intersection (the hypotenuse is a half-plane)
|
|
386
|
+
const keepOut: KeepOut[] = []
|
|
387
|
+
if (options.sightTriangleM && options.sightTriangleM > 0) {
|
|
388
|
+
for (const [a, b] of streetCorners(points, [frontIndex, ...streetEdges])) {
|
|
389
|
+
const tri = sightTriangle(points, a, b, options.sightTriangleM)
|
|
390
|
+
if (tri) keepOut.push({ a: tri.a, b: tri.b, inside: tri.corner })
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
// the true inward offset (core setback-envelope.ts): straight where the
|
|
394
|
+
// lot is straight, concentric round a radius, clipped where the offsets
|
|
395
|
+
// cross — the industry-standard setback for pie shapes, radius corners
|
|
396
|
+
// and odd lots
|
|
397
|
+
return insetPolygon(points, distances, { keepOut })
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Distance from `origin` along unit direction `(dx, dy)` to the first crossing
|
|
402
|
+
* of the polygon boundary. `null` when the ray never hits (origin outside, or
|
|
403
|
+
* a degenerate ring).
|
|
404
|
+
*/
|
|
405
|
+
export function rayToPolygon(
|
|
406
|
+
points: readonly Pt[],
|
|
407
|
+
origin: Pt,
|
|
408
|
+
dx: number,
|
|
409
|
+
dy: number,
|
|
410
|
+
): { distance: number; point: Pt } | null {
|
|
411
|
+
const n = points.length
|
|
412
|
+
let best: { distance: number; point: Pt } | null = null
|
|
413
|
+
for (let i = 0; i < n; i++) {
|
|
414
|
+
const p = points[i] as Pt
|
|
415
|
+
const q = points[(i + 1) % n] as Pt
|
|
416
|
+
const ex = q[0] - p[0]
|
|
417
|
+
const ey = q[1] - p[1]
|
|
418
|
+
const denom = dx * ey - dy * ex
|
|
419
|
+
if (Math.abs(denom) < 1e-12) continue
|
|
420
|
+
// origin + t*d = p + u*e
|
|
421
|
+
const t = ((p[0] - origin[0]) * ey - (p[1] - origin[1]) * ex) / denom
|
|
422
|
+
const u = ((p[0] - origin[0]) * dy - (p[1] - origin[1]) * dx) / denom
|
|
423
|
+
if (t <= 1e-9 || u < -1e-9 || u > 1 + 1e-9) continue
|
|
424
|
+
if (!best || t < best.distance) {
|
|
425
|
+
best = { distance: t, point: [origin[0] + dx * t, origin[1] + dy * t] }
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return best
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export interface YardDimension {
|
|
432
|
+
side: YardSide
|
|
433
|
+
/** Bounding-box edge midpoint the dimension starts at. */
|
|
434
|
+
from: Pt
|
|
435
|
+
/** Where the cast ray meets the lot line. */
|
|
436
|
+
to: Pt
|
|
437
|
+
/** Metres. */
|
|
438
|
+
distance: number
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* The four yard dimensions: from each footprint bbox edge midpoint, straight
|
|
443
|
+
* out to the lot line. Sides whose ray misses the lot line (footprint outside
|
|
444
|
+
* the lot) are omitted rather than faked.
|
|
445
|
+
*/
|
|
446
|
+
export function castYardDimensions(lot: readonly Pt[], footprint: Bounds): YardDimension[] {
|
|
447
|
+
if (lot.length < 3) return []
|
|
448
|
+
const midX = (footprint.minX + footprint.maxX) / 2
|
|
449
|
+
const midY = (footprint.minY + footprint.maxY) / 2
|
|
450
|
+
const casts: { side: YardSide; from: Pt; dx: number; dy: number }[] = [
|
|
451
|
+
{ side: 'north', from: [midX, footprint.minY], dx: 0, dy: -1 },
|
|
452
|
+
{ side: 'south', from: [midX, footprint.maxY], dx: 0, dy: 1 },
|
|
453
|
+
{ side: 'west', from: [footprint.minX, midY], dx: -1, dy: 0 },
|
|
454
|
+
{ side: 'east', from: [footprint.maxX, midY], dx: 1, dy: 0 },
|
|
455
|
+
]
|
|
456
|
+
const out: YardDimension[] = []
|
|
457
|
+
for (const c of casts) {
|
|
458
|
+
const hit = rayToPolygon(lot, c.from, c.dx, c.dy)
|
|
459
|
+
if (!hit) continue
|
|
460
|
+
out.push({ side: c.side, from: c.from, to: hit.point, distance: hit.distance })
|
|
461
|
+
}
|
|
462
|
+
return out
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Yard dimensions for a building TURNED on its lot. The footprint's bounds
|
|
467
|
+
* are taken in the building's own frame (`yaw`, the building node's Y
|
|
468
|
+
* rotation), and the four edge midpoints cast square to the house's faces
|
|
469
|
+
* out to the lot line — the yards a plan checker measures. With `yaw` 0 this
|
|
470
|
+
* is the axis-aligned cast above; with a house square to a diagonal lot the
|
|
471
|
+
* axis-aligned bbox sticks out past the real corners and reads a front yard
|
|
472
|
+
* inches short of the setback it actually meets (Land Park, 2026-09-06).
|
|
473
|
+
* `side` is the compass direction nearest the cast, so labels keep reading
|
|
474
|
+
* N / S / E / W.
|
|
475
|
+
*/
|
|
476
|
+
export function castYardDimensionsOriented(
|
|
477
|
+
lot: readonly Pt[],
|
|
478
|
+
footprintLoops: readonly (readonly Pt[])[],
|
|
479
|
+
yaw: number,
|
|
480
|
+
): YardDimension[] {
|
|
481
|
+
if (lot.length < 3) return []
|
|
482
|
+
const c = Math.cos(yaw)
|
|
483
|
+
const s = Math.sin(yaw)
|
|
484
|
+
// world = (c·lx + s·lz, −s·lx + c·lz) ⇒ local = (c·wx − s·wz, s·wx + c·wz)
|
|
485
|
+
const toLocal = (p: Pt): Pt => [c * p[0] - s * p[1], s * p[0] + c * p[1]]
|
|
486
|
+
const toWorld = (p: Pt): Pt => [c * p[0] + s * p[1], -s * p[0] + c * p[1]]
|
|
487
|
+
let minX = Number.POSITIVE_INFINITY
|
|
488
|
+
let minZ = Number.POSITIVE_INFINITY
|
|
489
|
+
let maxX = Number.NEGATIVE_INFINITY
|
|
490
|
+
let maxZ = Number.NEGATIVE_INFINITY
|
|
491
|
+
for (const loop of footprintLoops) {
|
|
492
|
+
for (const p of loop) {
|
|
493
|
+
const [x, z] = toLocal(p)
|
|
494
|
+
if (x < minX) minX = x
|
|
495
|
+
if (x > maxX) maxX = x
|
|
496
|
+
if (z < minZ) minZ = z
|
|
497
|
+
if (z > maxZ) maxZ = z
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
if (!Number.isFinite(minX)) return []
|
|
501
|
+
const midX = (minX + maxX) / 2
|
|
502
|
+
const midZ = (minZ + maxZ) / 2
|
|
503
|
+
const casts: { from: Pt; dir: Pt }[] = [
|
|
504
|
+
{ from: [midX, minZ], dir: [0, -1] },
|
|
505
|
+
{ from: [midX, maxZ], dir: [0, 1] },
|
|
506
|
+
{ from: [minX, midZ], dir: [-1, 0] },
|
|
507
|
+
{ from: [maxX, midZ], dir: [1, 0] },
|
|
508
|
+
]
|
|
509
|
+
const sides: YardSide[] = ['north', 'east', 'south', 'west']
|
|
510
|
+
const out: YardDimension[] = []
|
|
511
|
+
for (const cast of casts) {
|
|
512
|
+
const from = toWorld(cast.from)
|
|
513
|
+
const [dx, dy] = toWorld(cast.dir)
|
|
514
|
+
const hit = rayToPolygon(lot, from, dx, dy)
|
|
515
|
+
if (!hit) continue
|
|
516
|
+
// heading of the cast: plan up (−y) is north, +x east
|
|
517
|
+
const heading = ((Math.atan2(dx, -dy) * 180) / Math.PI + 360) % 360
|
|
518
|
+
const side = sides[Math.round(heading / 90) % 4] as YardSide
|
|
519
|
+
out.push({ side, from, to: hit.point, distance: hit.distance })
|
|
520
|
+
}
|
|
521
|
+
return out
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/** Even-odd point-in-polygon. */
|
|
525
|
+
export function pointInPolygon(points: readonly Pt[], x: number, y: number): boolean {
|
|
526
|
+
let inside = false
|
|
527
|
+
for (let i = 0, j = points.length - 1; i < points.length; j = i++) {
|
|
528
|
+
const a = points[i] as Pt
|
|
529
|
+
const b = points[j] as Pt
|
|
530
|
+
if (a[1] > y !== b[1] > y && x < ((b[0] - a[0]) * (y - a[1])) / (b[1] - a[1]) + a[0]) {
|
|
531
|
+
inside = !inside
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return inside
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/** True when every corner of `bounds` is inside `lot`. */
|
|
538
|
+
export function boundsInsidePolygon(lot: readonly Pt[], bounds: Bounds): boolean {
|
|
539
|
+
if (lot.length < 3) return false
|
|
540
|
+
const corners: Pt[] = [
|
|
541
|
+
[bounds.minX, bounds.minY],
|
|
542
|
+
[bounds.maxX, bounds.minY],
|
|
543
|
+
[bounds.maxX, bounds.maxY],
|
|
544
|
+
[bounds.minX, bounds.maxY],
|
|
545
|
+
]
|
|
546
|
+
return corners.every((c) => pointInPolygon(lot, c[0], c[1]))
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
export const METRES_PER_FOOT = 0.3048
|
|
550
|
+
|
|
551
|
+
/** `24'-6"` — the notation a US site plan uses for yard dimensions. */
|
|
552
|
+
export function formatFeetInches(metres: number): string {
|
|
553
|
+
const totalInches = Math.round((metres / METRES_PER_FOOT) * 12)
|
|
554
|
+
const feet = Math.floor(totalInches / 12)
|
|
555
|
+
const inches = totalInches % 12
|
|
556
|
+
return inches === 0 ? `${feet}'-0"` : `${feet}'-${inches}"`
|
|
557
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export {
|
|
2
|
+
buildingRecentreOffset,
|
|
3
|
+
buildSitePlanDrawing,
|
|
4
|
+
describeSiteEdges,
|
|
5
|
+
levelFootprintLoops,
|
|
6
|
+
type SitePlanDrawing,
|
|
7
|
+
type SitePlanEdge,
|
|
8
|
+
} from './build-site-plan-drawing'
|
|
9
|
+
export {
|
|
10
|
+
hasSitePlanContributors,
|
|
11
|
+
registerSitePlanContributor,
|
|
12
|
+
type SitePlanContributor,
|
|
13
|
+
type SitePlanServicePoint,
|
|
14
|
+
type SitePlanServiceRole,
|
|
15
|
+
type SitePlanServices,
|
|
16
|
+
sitePlanContributions,
|
|
17
|
+
subscribeSitePlanContributors,
|
|
18
|
+
} from './contributors'
|
|
19
|
+
export {
|
|
20
|
+
computeSiteCoverage,
|
|
21
|
+
formatCoveragePercent,
|
|
22
|
+
formatSqFt,
|
|
23
|
+
type ImperviousRow,
|
|
24
|
+
type SiteCoverage,
|
|
25
|
+
} from './coverage'
|
|
26
|
+
export {
|
|
27
|
+
FloorplanDrawingTypeSwitch,
|
|
28
|
+
isSitePlanAvailable,
|
|
29
|
+
useSitePlanAvailable,
|
|
30
|
+
} from './drawing-type-switch'
|
|
31
|
+
export {
|
|
32
|
+
detectFrontEdgeFromRoads,
|
|
33
|
+
FRONT_EDGE_PARALLEL_DEG,
|
|
34
|
+
type FrontEdgeMatch,
|
|
35
|
+
type RoadCenterline,
|
|
36
|
+
streetCore,
|
|
37
|
+
} from './front-edge'
|
|
38
|
+
export {
|
|
39
|
+
type Bounds,
|
|
40
|
+
boundsInsidePolygon,
|
|
41
|
+
castYardDimensions,
|
|
42
|
+
castYardDimensionsOriented,
|
|
43
|
+
classifyEdges,
|
|
44
|
+
compassLabel,
|
|
45
|
+
edgeHeadingDeg,
|
|
46
|
+
edgeLength,
|
|
47
|
+
formatFeetInches,
|
|
48
|
+
METRES_PER_FOOT,
|
|
49
|
+
mostNorthFacingEdge,
|
|
50
|
+
outwardNormal,
|
|
51
|
+
type Pt,
|
|
52
|
+
pointInPolygon,
|
|
53
|
+
polygonArea,
|
|
54
|
+
polygonBounds,
|
|
55
|
+
polygonCentroid,
|
|
56
|
+
rayToPolygon,
|
|
57
|
+
resolveFrontEdge,
|
|
58
|
+
type SetbackInputs,
|
|
59
|
+
setbackEnvelope,
|
|
60
|
+
setbackForRole,
|
|
61
|
+
type YardDimension,
|
|
62
|
+
type YardSide,
|
|
63
|
+
} from './geometry'
|
|
64
|
+
export {
|
|
65
|
+
formatStreetName,
|
|
66
|
+
serviceEntranceOf,
|
|
67
|
+
streetEdgeNames,
|
|
68
|
+
UNNAMED_STREET,
|
|
69
|
+
} from './site-annotations'
|
|
70
|
+
export { flatworkKindOf, type OutdoorPart } from './site-parts'
|
|
71
|
+
export { FloorplanSitePlanLayer } from './site-plan-layer'
|