@pascal-app/editor 1.0.3 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +11 -9
- package/src/components/editor/action-menu-button.tsx +83 -0
- package/src/components/editor/bake-thumbnail.tsx +315 -4
- package/src/components/editor/capture-level-picker.tsx +72 -0
- package/src/components/editor/custom-camera-controls.tsx +202 -57
- package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
- package/src/components/editor/duplicate-node.ts +244 -0
- package/src/components/editor/editor-layout-mobile.tsx +36 -25
- package/src/components/editor/editor-layout-v2.tsx +131 -58
- package/src/components/editor/export-manager.tsx +34 -8
- package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
- package/src/components/editor/first-person/build-collider-world.ts +6 -2
- package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
- package/src/components/editor/first-person/mechanism-targets.ts +36 -0
- package/src/components/editor/first-person-controls.tsx +205 -2
- package/src/components/editor/floating-action-menu.tsx +63 -207
- package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
- package/src/components/editor/floor-aware-camera-controls.ts +324 -0
- package/src/components/editor/floor-openings-overlay.tsx +243 -0
- package/src/components/editor/floor-region-controls.tsx +846 -0
- package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
- package/src/components/editor/floor-surface.test.ts +165 -0
- package/src/components/editor/floor-surface.ts +182 -0
- package/src/components/editor/floorplan-background-selection.test.ts +11 -2
- package/src/components/editor/floorplan-background-selection.ts +1 -3
- package/src/components/editor/floorplan-panel.tsx +344 -342
- package/src/components/editor/footprint-height-handle.tsx +204 -0
- package/src/components/editor/group-actions.ts +54 -2
- package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
- package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
- package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
- package/src/components/editor/handles/use-handle-drag.ts +269 -197
- package/src/components/editor/index.tsx +294 -169
- package/src/components/editor/join-walls-pill.tsx +44 -0
- package/src/components/editor/node-action-menu.tsx +68 -70
- package/src/components/editor/node-arrow-handles.tsx +25 -6
- package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
- package/src/components/editor/open-wall-ends-hint.tsx +89 -0
- package/src/components/editor/quick-measurement-hud.tsx +3 -2
- package/src/components/editor/registry-action-contributions.test.tsx +63 -0
- package/src/components/editor/registry-action-contributions.tsx +80 -1
- package/src/components/editor/room-controls.tsx +847 -0
- package/src/components/editor/room-floor-highlight.tsx +128 -0
- package/src/components/editor/room-handles-2d.tsx +354 -0
- package/src/components/editor/room-handles.tsx +675 -0
- package/src/components/editor/room-highlight.tsx +225 -0
- package/src/components/editor/room-labels.test.ts +357 -0
- package/src/components/editor/room-labels.tsx +990 -0
- package/src/components/editor/room-transform-ghost.tsx +256 -0
- package/src/components/editor/room-webgpu-materials.test.ts +27 -0
- package/src/components/editor/selection-manager.tsx +451 -317
- package/src/components/editor/slab-hole-highlights.tsx +3 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
- package/src/components/editor/snapshot-capture.test.ts +52 -4
- package/src/components/editor/snapshot-capture.ts +30 -5
- package/src/components/editor/thumbnail-generator.test.ts +308 -0
- package/src/components/editor/thumbnail-generator.tsx +1035 -229
- package/src/components/editor/use-floorplan-background-placement.ts +31 -11
- package/src/components/editor/vector-edge-extractor.tsx +121 -0
- package/src/components/editor/views/host-view.tsx +61 -0
- package/src/components/editor/views/plugins-view.tsx +670 -0
- package/src/components/editor/views/scene-region.tsx +59 -0
- package/src/components/editor/views/use-editor-views.ts +57 -0
- package/src/components/editor/views/view-bar.tsx +220 -0
- package/src/components/editor/wall-justification.test.ts +140 -0
- package/src/components/editor/wall-move-side-handles.tsx +43 -142
- package/src/components/editor/wall-push-arrow.tsx +92 -0
- package/src/components/editor/wall-region-handles.tsx +509 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +113 -102
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
- package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +291 -227
- package/src/components/editor-2d/svg-paths.ts +12 -15
- package/src/components/procedural-items/inspector.tsx +40 -15
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
- package/src/components/systems/zone/zone-system.test.ts +47 -0
- package/src/components/systems/zone/zone-system.tsx +29 -68
- package/src/components/tools/item/placement-history.test.tsx +474 -0
- package/src/components/tools/item/placement-strategies.ts +8 -4
- package/src/components/tools/item/use-draft-node.test.tsx +432 -1
- package/src/components/tools/item/use-draft-node.ts +166 -95
- package/src/components/tools/item/use-placement-coordinator.tsx +93 -46
- package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
- package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +3 -1
- package/src/components/tools/select/box-select-tool.tsx +2 -12
- package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
- package/src/components/tools/select/select-candidates.test.ts +37 -4
- package/src/components/tools/select/select-candidates.ts +15 -11
- package/src/components/tools/shared/cursor-sphere.tsx +36 -12
- package/src/components/tools/site/site-boundary-editor.tsx +1 -2
- package/src/components/tools/stair/stair-defaults.ts +0 -3
- package/src/components/tools/stair/stair-tool.tsx +90 -42
- package/src/components/tools/tool-manager.tsx +46 -20
- package/src/components/tools/wall/wall-drafting.test.ts +19 -4
- package/src/components/tools/zone/zone-tool.tsx +4 -12
- package/src/components/ui/action-menu/action-button.tsx +3 -0
- package/src/components/ui/action-menu/camera-actions.tsx +2 -1
- package/src/components/ui/action-menu/control-modes.tsx +2 -35
- package/src/components/ui/action-menu/measurement-control.tsx +3 -10
- package/src/components/ui/build-panel/build-panel.tsx +235 -0
- package/src/components/ui/command-palette/editor-commands.tsx +4 -30
- package/src/components/ui/command-palette/index.tsx +5 -24
- package/src/components/ui/controls/finish-swatch.tsx +110 -0
- package/src/components/ui/controls/material-paint-panel.tsx +97 -26
- package/src/components/ui/controls/material-picker.tsx +4 -1
- package/src/components/ui/controls/scene-material-list.tsx +4 -32
- package/src/components/ui/controls/select-control.tsx +65 -0
- package/src/components/ui/floating-level-selector.tsx +9 -14
- package/src/components/ui/helpers/building-helper.tsx +4 -1
- package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
- package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
- package/src/components/ui/helpers/helper-manager.tsx +325 -14
- package/src/components/ui/helpers/item-helper.tsx +8 -0
- package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
- package/src/components/ui/icon-ref.tsx +45 -1
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/node-icon-image.tsx +16 -0
- package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
- package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
- package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
- package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
- package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
- package/src/components/ui/panels/multi-field-value.test.ts +26 -3
- package/src/components/ui/panels/multi-field-value.ts +6 -3
- package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
- package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
- package/src/components/ui/panels/node-display.ts +8 -3
- package/src/components/ui/panels/panel-manager.tsx +103 -68
- package/src/components/ui/panels/panel-wrapper.tsx +78 -36
- package/src/components/ui/panels/parametric-field-control.tsx +1 -1
- package/src/components/ui/panels/parametric-inspector.tsx +4 -4
- package/src/components/ui/panels/reference-panel.tsx +2 -3
- package/src/components/ui/panels/room-construction-rows.tsx +774 -0
- package/src/components/ui/panels/room-name-options.test.ts +55 -0
- package/src/components/ui/panels/room-name-options.ts +47 -0
- package/src/components/ui/panels/room-openings-field.tsx +110 -0
- package/src/components/ui/panels/room-panel.tsx +725 -0
- package/src/components/ui/panels/shape-choice.tsx +58 -0
- package/src/components/ui/panels/wall-region-list.tsx +109 -0
- package/src/components/ui/primitives/color-dot.tsx +36 -24
- package/src/components/ui/primitives/sidebar.tsx +2 -0
- package/src/components/ui/right-stack.test.ts +29 -0
- package/src/components/ui/right-stack.tsx +145 -0
- package/src/components/ui/sidebar/icon-rail.tsx +5 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +31 -4
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
- package/src/components/ui/sidebar/tab-bar.tsx +10 -0
- package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
- package/src/components/viewer/floorplan-compass-button.tsx +9 -1
- package/src/components/viewer/floorplan-preview.tsx +5 -6
- package/src/components/viewer/viewer-hover-label.tsx +34 -0
- package/src/components/viewer/viewer-rooms.tsx +175 -0
- package/src/components/viewer/viewer-scene-header.tsx +181 -145
- package/src/components/viewer/viewer-selection-manager.tsx +124 -0
- package/src/components/viewer/viewer-stage.tsx +2 -2
- package/src/components/viewer-zone-system.tsx +3 -8
- package/src/hooks/use-auto-save.integration.test.tsx +123 -9
- package/src/hooks/use-auto-save.ts +34 -8
- package/src/hooks/use-keyboard.test.ts +178 -3
- package/src/hooks/use-keyboard.ts +89 -71
- package/src/hooks/use-open-wall-ends.test.ts +99 -0
- package/src/hooks/use-open-wall-ends.ts +238 -0
- package/src/hooks/use-placement-notice.test.ts +48 -0
- package/src/hooks/use-placement-notice.ts +42 -0
- package/src/hooks/use-selected-room.ts +143 -0
- package/src/index.tsx +246 -39
- package/src/lib/ceiling-edit-session.test.ts +321 -0
- package/src/lib/ceiling-edit-session.ts +206 -0
- package/src/lib/ceiling-region-session.test.ts +179 -0
- package/src/lib/continuation.test.ts +19 -1
- package/src/lib/continuation.ts +25 -4
- package/src/lib/door-interaction.ts +14 -0
- package/src/lib/editor-level-display.test.ts +96 -0
- package/src/lib/editor-level-display.ts +48 -0
- package/src/lib/editor-views.ts +144 -0
- package/src/lib/export-geometry-wait.test.ts +90 -0
- package/src/lib/floor-edit-session.ts +153 -0
- package/src/lib/floor-footprints.test.ts +563 -0
- package/src/lib/floor-footprints.ts +395 -0
- package/src/lib/floor-foundation-support.test.ts +283 -0
- package/src/lib/floor-opening-draft.ts +218 -0
- package/src/lib/floor-openings.test.ts +216 -0
- package/src/lib/floor-region-geometry.ts +191 -0
- package/src/lib/floor-region-session.test.ts +485 -0
- package/src/lib/floor-region-session.ts +589 -0
- package/src/lib/floor-region-snap.ts +101 -0
- package/src/lib/floor-step-hover.test.ts +32 -0
- package/src/lib/floorplan/annotation-visibility.ts +17 -0
- package/src/lib/floorplan/dimension-drive.test.ts +360 -0
- package/src/lib/floorplan/dimension-drive.ts +479 -0
- package/src/lib/floorplan/floorplan-export.test.ts +12 -8
- package/src/lib/floorplan/floorplan-export.tsx +32 -8
- package/src/lib/floorplan/floorplan-extension.ts +32 -0
- package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
- package/src/lib/floorplan/floorplan-mode.ts +2 -0
- package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
- package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
- package/src/lib/floorplan/floorplan-readonly.ts +6 -0
- package/src/lib/floorplan/geometry.ts +13 -6
- package/src/lib/floorplan/index.ts +0 -9
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/marks.test.ts +213 -0
- package/src/lib/floorplan/marks.ts +238 -0
- package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
- package/src/lib/floorplan/open-wall-ends.ts +162 -0
- package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
- package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
- package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
- package/src/lib/floorplan/schedules.test.ts +189 -0
- package/src/lib/floorplan/schedules.ts +339 -0
- package/src/lib/floorplan/sheet-export.test.ts +100 -0
- package/src/lib/floorplan/sheet-export.ts +318 -0
- package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
- package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
- package/src/lib/floorplan/site-plan/contributors.ts +97 -0
- package/src/lib/floorplan/site-plan/coverage.ts +232 -0
- package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
- package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
- package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
- package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
- package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
- package/src/lib/floorplan/site-plan/geometry.ts +557 -0
- package/src/lib/floorplan/site-plan/index.ts +71 -0
- package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
- package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
- package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
- package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
- package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
- package/src/lib/floorplan/site-plan/street-name.ts +302 -0
- package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
- package/src/lib/floorplan/types.ts +1 -27
- package/src/lib/fresh-planar-placement.test.ts +36 -0
- package/src/lib/fresh-planar-placement.ts +59 -36
- package/src/lib/geometry-script/author.ts +152 -0
- package/src/lib/geometry-script/client.ts +152 -0
- package/src/lib/geometry-script/protocol.ts +12 -0
- package/src/lib/gesture-lifecycle.test.ts +595 -0
- package/src/lib/gesture-lifecycle.ts +290 -0
- package/src/lib/glb-export-zone-holes.test.ts +41 -0
- package/src/lib/glb-export.test.ts +485 -0
- package/src/lib/glb-export.ts +237 -119
- package/src/lib/history.test.ts +6 -0
- package/src/lib/history.ts +7 -0
- package/src/lib/hud-title.ts +109 -0
- package/src/lib/hydration-gated-storage.ts +49 -0
- package/src/lib/ifc-export.test.ts +174 -0
- package/src/lib/ifc-export.ts +201 -0
- package/src/lib/ifc-parts.ts +15 -0
- package/src/lib/inspector-expanded.test.ts +44 -0
- package/src/lib/inspector-expanded.ts +36 -0
- package/src/lib/interaction/scope.ts +18 -0
- package/src/lib/keyboard-pan.test.ts +129 -7
- package/src/lib/keyboard-pan.ts +161 -12
- package/src/lib/live-gesture-round-trip.test.ts +360 -0
- package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
- package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
- package/src/lib/lot/clean-ring.test.ts +186 -0
- package/src/lib/lot/clean-ring.ts +217 -0
- package/src/lib/lot/dossier.test.ts +202 -0
- package/src/lib/lot/dossier.ts +498 -0
- package/src/lib/lot/drop-in.test.ts +326 -0
- package/src/lib/lot/drop-in.ts +407 -0
- package/src/lib/lot/index.ts +43 -0
- package/src/lib/lot/lot-patch.test.ts +340 -0
- package/src/lib/lot/lot-patch.ts +394 -0
- package/src/lib/lot/parcel-provider.ts +43 -0
- package/src/lib/lot/terrain.test.ts +161 -0
- package/src/lib/lot/terrain.ts +266 -0
- package/src/lib/material-catalog-model.ts +3 -1
- package/src/lib/material-paint-panel-model.ts +9 -5
- package/src/lib/material-paint.ts +4 -0
- package/src/lib/mezzanine-actions.test.ts +342 -0
- package/src/lib/mezzanine-draft.test.ts +501 -0
- package/src/lib/mezzanine-draft.ts +278 -0
- package/src/lib/mezzanine-messages.ts +26 -0
- package/src/lib/mezzanine-selection.test.ts +207 -0
- package/src/lib/mezzanine-stairs.ts +46 -0
- package/src/lib/model-export.ts +3 -1
- package/src/lib/node-action-movement.ts +17 -6
- package/src/lib/node-display-label.ts +37 -0
- package/src/lib/orbit-floor-clearance.test.ts +165 -0
- package/src/lib/orbit-floor-clearance.ts +106 -0
- package/src/lib/paint-commit-route.test.ts +37 -0
- package/src/lib/paint-commit-route.ts +17 -0
- package/src/lib/paint-eyedropper.test.tsx +165 -0
- package/src/lib/paint-eyedropper.ts +99 -0
- package/src/lib/paint-pass-through.test.ts +111 -0
- package/src/lib/paint-pass-through.ts +76 -0
- package/src/lib/paint-region-hover.ts +25 -0
- package/src/lib/paint-region-mode.ts +154 -0
- package/src/lib/paint-regions.test.ts +166 -0
- package/src/lib/paint-regions.ts +192 -0
- package/src/lib/paint-scope.test.ts +317 -35
- package/src/lib/paint-scope.ts +266 -25
- package/src/lib/panel-rows.ts +21 -0
- package/src/lib/parametric-node-update.ts +1 -1
- package/src/lib/plate-paint-affected.test.ts +300 -0
- package/src/lib/plate-paint-affected.ts +177 -0
- package/src/lib/plugin-chat.ts +50 -0
- package/src/lib/plugin-panels.test.ts +25 -0
- package/src/lib/plugin-panels.ts +94 -0
- package/src/lib/print-shell-compiler.ts +1 -0
- package/src/lib/registered-draft-snap.ts +12 -0
- package/src/lib/roof-duplication.ts +10 -2
- package/src/lib/room-assembly-overlay.test.ts +275 -0
- package/src/lib/room-assembly-overlay.ts +266 -0
- package/src/lib/room-built-on.test.ts +379 -0
- package/src/lib/room-built-on.ts +138 -0
- package/src/lib/room-construction-commands.ts +712 -0
- package/src/lib/room-construction.test.ts +746 -0
- package/src/lib/room-construction.ts +385 -0
- package/src/lib/room-controls-phase6.test.ts +630 -0
- package/src/lib/room-divide-session.ts +523 -0
- package/src/lib/room-handle-drag.ts +1068 -0
- package/src/lib/room-name-catalog.test.ts +90 -0
- package/src/lib/room-name-catalog.ts +168 -0
- package/src/lib/room-push-dimensions.test.ts +311 -0
- package/src/lib/room-push-dimensions.ts +251 -0
- package/src/lib/room-selection-commands.ts +62 -0
- package/src/lib/room-selection.test.ts +973 -0
- package/src/lib/room-selection.ts +417 -0
- package/src/lib/room-structure-commands.ts +94 -0
- package/src/lib/room-structure.test.ts +921 -0
- package/src/lib/room-transform-session.ts +570 -0
- package/src/lib/room-zone-routing.test.ts +294 -0
- package/src/lib/room-zone-routing.ts +197 -0
- package/src/lib/scene-clipboard.test.ts +364 -0
- package/src/lib/scene-clipboard.ts +229 -48
- package/src/lib/scene-material-refs.test.ts +79 -0
- package/src/lib/scene-material-refs.ts +79 -0
- package/src/lib/scene.test.ts +24 -36
- package/src/lib/scene.ts +33 -57
- package/src/lib/selection-routing.test.ts +129 -45
- package/src/lib/selection-routing.ts +53 -74
- package/src/lib/session-writes.test.ts +120 -0
- package/src/lib/session-writes.ts +205 -0
- package/src/lib/sidebar-panel.ts +62 -0
- package/src/lib/site-boundary.test.ts +4 -6
- package/src/lib/site-boundary.ts +1 -1
- package/src/lib/snapping-mode.test.ts +4 -0
- package/src/lib/snapping-mode.ts +8 -1
- package/src/lib/terrace-draft.test.ts +145 -0
- package/src/lib/terrace-draft.ts +213 -0
- package/src/lib/tool-cursor-icon.test.ts +16 -0
- package/src/lib/tool-cursor-icon.ts +11 -0
- package/src/lib/typing-target.ts +9 -0
- package/src/lib/units.ts +77 -39
- package/src/lib/vector-edges.test.ts +58 -0
- package/src/lib/vector-edges.ts +400 -0
- package/src/lib/viewer-selection.test.ts +577 -0
- package/src/lib/viewer-selection.ts +569 -0
- package/src/lib/wall-draw-variant.test.ts +71 -0
- package/src/lib/wall-draw-variant.ts +91 -0
- package/src/lib/wall-paint-region-session.ts +246 -0
- package/src/lib/wall-paint-region.test.ts +373 -0
- package/src/lib/wall-polygon-draft.test.ts +262 -0
- package/src/lib/wall-polygon-draft.ts +286 -0
- package/src/lib/wall-push.test.ts +453 -0
- package/src/lib/wall-region-face.ts +352 -0
- package/src/lib/wall-region-handles.test.ts +304 -0
- package/src/lib/wall-region-handles.ts +368 -0
- package/src/lib/wall-region-list.test.ts +119 -0
- package/src/lib/wall-region-list.ts +136 -0
- package/src/lib/wall-region-snap.ts +110 -0
- package/src/lib/window-interaction.ts +14 -0
- package/src/lib/zone-content.ts +4 -1
- package/src/lib/zone-name.test.ts +17 -0
- package/src/lib/zone-name.ts +17 -0
- package/src/store/opens-in-select-mode.test.ts +223 -0
- package/src/store/paint-mode.test.ts +188 -0
- package/src/store/terrain-sculpt-mode.test.ts +19 -21
- package/src/store/tool-mode.test.ts +137 -6
- package/src/store/use-delete-confirmation.ts +28 -1
- package/src/store/use-drawing-view.test.ts +24 -3
- package/src/store/use-drawing-view.ts +29 -1
- package/src/store/use-editor.tsx +432 -321
- package/src/store/use-floorplan-draft-preview.ts +13 -0
- package/src/store/use-floorplan-marquee.ts +12 -2
- package/src/store/use-quick-measurement-hud.ts +3 -3
- package/src/store/use-stair-build-preview.ts +13 -5
- package/src/store/use-wall-move-ghosts.ts +4 -2
- package/src/store/use-wall-region-handles-selection.test.ts +222 -0
- package/src/store/use-wall-region-handles-selection.ts +139 -0
- package/src/store/view-layout.test.ts +175 -0
- package/src/store/view-layout.ts +196 -0
- package/src/test-utils/immediate-animation-frames.ts +16 -0
- package/src/test-utils/live-gesture-round-trip.ts +224 -0
- package/src/test-utils/selection-harness.ts +82 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
- package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
- package/src/components/tools/fence/fence-drafting.test.ts +0 -100
- package/src/components/tools/fence/fence-drafting.ts +0 -266
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
- package/src/components/viewer/viewer-units-panel.tsx +0 -116
- package/src/lib/floorplan/stairs.ts +0 -489
- package/src/lib/level-duplication.test.ts +0 -144
- package/src/lib/level-duplication.ts +0 -195
|
@@ -0,0 +1,779 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyNode,
|
|
3
|
+
type AnyNodeId,
|
|
4
|
+
type BuildingNode,
|
|
5
|
+
envelopeFrontEdge,
|
|
6
|
+
type FloorplanGeometry,
|
|
7
|
+
type LevelNode,
|
|
8
|
+
type SceneSnapshot,
|
|
9
|
+
type SiteNode,
|
|
10
|
+
sightTriangle,
|
|
11
|
+
streetCorners,
|
|
12
|
+
terrainFieldOf,
|
|
13
|
+
} from '@pascal-app/core'
|
|
14
|
+
import { sitePlanContributions } from './contributors'
|
|
15
|
+
import {
|
|
16
|
+
computeSiteCoverage,
|
|
17
|
+
formatCoveragePercent,
|
|
18
|
+
formatSqFt,
|
|
19
|
+
type SiteCoverage,
|
|
20
|
+
} from './coverage'
|
|
21
|
+
import {
|
|
22
|
+
type Bounds,
|
|
23
|
+
boundsInsidePolygon,
|
|
24
|
+
castYardDimensionsOriented,
|
|
25
|
+
classifyEdges,
|
|
26
|
+
compassLabel,
|
|
27
|
+
edgeHeadingDeg,
|
|
28
|
+
edgeLength,
|
|
29
|
+
formatFeetInches,
|
|
30
|
+
METRES_PER_FOOT,
|
|
31
|
+
outwardNormal,
|
|
32
|
+
type Pt,
|
|
33
|
+
polygonBounds,
|
|
34
|
+
resolveFrontEdge,
|
|
35
|
+
setbackEnvelope,
|
|
36
|
+
setbackForRole,
|
|
37
|
+
type YardDimension,
|
|
38
|
+
} from './geometry'
|
|
39
|
+
import {
|
|
40
|
+
contourPrimitives,
|
|
41
|
+
drainagePrimitives,
|
|
42
|
+
flatworkPrimitives,
|
|
43
|
+
floorTops,
|
|
44
|
+
formatElevation,
|
|
45
|
+
servicePrimitives,
|
|
46
|
+
spotElevationPrimitives,
|
|
47
|
+
streetEdgeNames,
|
|
48
|
+
} from './site-annotations'
|
|
49
|
+
import {
|
|
50
|
+
findBuilding,
|
|
51
|
+
findLowestLevel,
|
|
52
|
+
findSite,
|
|
53
|
+
footprintOutline,
|
|
54
|
+
levelFootprintLoops,
|
|
55
|
+
roofOutlineRings,
|
|
56
|
+
siteFrame,
|
|
57
|
+
yawOf,
|
|
58
|
+
} from './site-parts'
|
|
59
|
+
|
|
60
|
+
export { footprintOutline, levelFootprintLoops, roofOutlineRings }
|
|
61
|
+
|
|
62
|
+
/** Contract shared by every drawing producer (see docs/construction-documents.md). */
|
|
63
|
+
export interface SitePlanDrawing {
|
|
64
|
+
primitives: FloorplanGeometry[]
|
|
65
|
+
bounds: Bounds
|
|
66
|
+
/** What the drawing could not take from the model — printed on the sheet, never silent. */
|
|
67
|
+
warnings?: string[]
|
|
68
|
+
/** Live values the panel / sheets reuse without re-deriving them. */
|
|
69
|
+
meta: {
|
|
70
|
+
site: SiteNode | null
|
|
71
|
+
frontEdge: number
|
|
72
|
+
lot: Pt[]
|
|
73
|
+
envelope: Pt[]
|
|
74
|
+
/** The envelope edge behind the lot's front line (the envelope has its own vertex count now). */
|
|
75
|
+
envelopeFrontEdge: number
|
|
76
|
+
/** The corner sight triangles (corner, leg end A, leg end B), site metres. */
|
|
77
|
+
sightTriangles: Pt[][]
|
|
78
|
+
/** Per-wall footprint bands of the lowest level, in SITE metres. */
|
|
79
|
+
footprintLoops: Pt[][]
|
|
80
|
+
footprintBounds: Bounds | null
|
|
81
|
+
yards: YardDimension[]
|
|
82
|
+
buildingId: AnyNodeId | null
|
|
83
|
+
/** Building coverage and impervious area — the figures the lot label and the sheets print. */
|
|
84
|
+
coverage?: SiteCoverage
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const LOT_STROKE_WIDTH = 0.12
|
|
89
|
+
const ENVELOPE_STROKE_WIDTH = 0.05
|
|
90
|
+
const LABEL_SIZE = 0.9
|
|
91
|
+
|
|
92
|
+
// Plan ink. The floor-plan surface is the light plan sheet the panel paints
|
|
93
|
+
// behind the scene group, where the app's `currentColor` foreground is
|
|
94
|
+
// near-white and therefore invisible. Kinds hard-code their plan ink for the
|
|
95
|
+
// same reason; these match `nodes/src/wall/floorplan.ts`.
|
|
96
|
+
const INK = '#111827'
|
|
97
|
+
const INK_SOFT = '#4b5563'
|
|
98
|
+
/** Contour lines: the survey's brown. */
|
|
99
|
+
const CONTOUR_INK = '#8b5a2b'
|
|
100
|
+
/** The house on the lot: a light body under a heavy edge, so its label and the roof line read. */
|
|
101
|
+
const FOOTPRINT_FILL = '#d1d5db'
|
|
102
|
+
const FOOTPRINT_STROKE = '#111827'
|
|
103
|
+
const DIMENSION_STROKE = '#334155'
|
|
104
|
+
|
|
105
|
+
export interface SitePlanEdge {
|
|
106
|
+
index: number
|
|
107
|
+
headingDeg: number
|
|
108
|
+
compass: string
|
|
109
|
+
lengthM: number
|
|
110
|
+
label: string
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Lot edges with their compass heading — feeds the panel's front-edge picker. */
|
|
114
|
+
export function describeSiteEdges(site: SiteNode | null | undefined): SitePlanEdge[] {
|
|
115
|
+
const lot = (site?.polygon?.points ?? []) as Pt[]
|
|
116
|
+
if (lot.length < 3) return []
|
|
117
|
+
const north = site?.northRotation ?? 0
|
|
118
|
+
return lot.map((_, i) => {
|
|
119
|
+
const headingDeg = edgeHeadingDeg(lot, i, north)
|
|
120
|
+
const lengthM = edgeLength(lot, i)
|
|
121
|
+
const compass = compassLabel(headingDeg)
|
|
122
|
+
return {
|
|
123
|
+
index: i,
|
|
124
|
+
headingDeg,
|
|
125
|
+
compass,
|
|
126
|
+
lengthM,
|
|
127
|
+
label: `Edge ${i + 1} — faces ${compass} (${Math.round(headingDeg)}°), ${formatFeetInches(lengthM)}`,
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function unionBounds(loops: readonly Pt[][]): Bounds | null {
|
|
133
|
+
const all: Pt[] = []
|
|
134
|
+
for (const loop of loops) all.push(...loop)
|
|
135
|
+
if (all.length === 0) return null
|
|
136
|
+
return polygonBounds(all)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function padBounds(b: Bounds, pad: number): Bounds {
|
|
140
|
+
return { minX: b.minX - pad, minY: b.minY - pad, maxX: b.maxX + pad, maxY: b.maxY + pad }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function northArrow(at: Pt, northRotation: number, size: number): FloorplanGeometry {
|
|
144
|
+
const [cx, cy] = at
|
|
145
|
+
// Local frame: up is −y, rotated clockwise by `northRotation`.
|
|
146
|
+
const cos = Math.cos(northRotation)
|
|
147
|
+
const sin = Math.sin(northRotation)
|
|
148
|
+
const rot = (x: number, y: number): Pt => [cx + x * cos - y * sin, cy + x * sin + y * cos]
|
|
149
|
+
const tip = rot(0, -size)
|
|
150
|
+
const tail = rot(0, size * 0.55)
|
|
151
|
+
const left = rot(-size * 0.28, -size * 0.35)
|
|
152
|
+
const right = rot(size * 0.28, -size * 0.35)
|
|
153
|
+
const labelAt = rot(0, size * 0.95)
|
|
154
|
+
return {
|
|
155
|
+
kind: 'group',
|
|
156
|
+
children: [
|
|
157
|
+
{
|
|
158
|
+
kind: 'line',
|
|
159
|
+
x1: tail[0],
|
|
160
|
+
y1: tail[1],
|
|
161
|
+
x2: tip[0],
|
|
162
|
+
y2: tip[1],
|
|
163
|
+
stroke: INK,
|
|
164
|
+
strokeWidth: 1.4,
|
|
165
|
+
vectorEffect: 'non-scaling-stroke',
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
kind: 'polygon',
|
|
169
|
+
points: [tip, left, right],
|
|
170
|
+
fill: INK,
|
|
171
|
+
stroke: INK,
|
|
172
|
+
strokeWidth: 1,
|
|
173
|
+
vectorEffect: 'non-scaling-stroke',
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
kind: 'text',
|
|
177
|
+
x: labelAt[0],
|
|
178
|
+
y: labelAt[1],
|
|
179
|
+
text: 'N',
|
|
180
|
+
fontSize: size * 0.55,
|
|
181
|
+
fill: INK,
|
|
182
|
+
fontWeight: 600,
|
|
183
|
+
textAnchor: 'middle',
|
|
184
|
+
dominantBaseline: 'hanging',
|
|
185
|
+
upright: true,
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** A survey bearing for the run (dx, dy): quadrant, degrees and minutes — "N 81°42' E". */
|
|
192
|
+
function bearingOf(dx: number, dy: number, northRotation: number): string {
|
|
193
|
+
// plan up (−y) is north and +x is east; `northRotation` turns true north
|
|
194
|
+
// clockwise from plan up (the north arrow's own convention)
|
|
195
|
+
let az = ((Math.atan2(dx, -dy) - northRotation) * 180) / Math.PI
|
|
196
|
+
az = ((az % 360) + 360) % 360
|
|
197
|
+
const ns = az <= 90 || az >= 270 ? 'N' : 'S'
|
|
198
|
+
const ew = az <= 180 ? 'E' : 'W'
|
|
199
|
+
const off = az <= 90 ? az : az <= 180 ? 180 - az : az <= 270 ? az - 180 : 360 - az
|
|
200
|
+
let deg = Math.floor(off)
|
|
201
|
+
let min = Math.round((off - deg) * 60)
|
|
202
|
+
if (min === 60) {
|
|
203
|
+
deg += 1
|
|
204
|
+
min = 0
|
|
205
|
+
}
|
|
206
|
+
return `${ns} ${deg}°${String(min).padStart(2, '0')}' ${ew}`
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** An angle a label reads at: within a quarter turn of the paper's horizontal. */
|
|
210
|
+
function readableAngle(a: number): number {
|
|
211
|
+
let r = a
|
|
212
|
+
while (r > Math.PI / 2) r -= Math.PI
|
|
213
|
+
while (r <= -Math.PI / 2) r += Math.PI
|
|
214
|
+
return r
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** A label laid ALONG a run — a group turned to the run's readable angle, the text centred on `at`. */
|
|
218
|
+
function alongLabel(
|
|
219
|
+
at: Pt,
|
|
220
|
+
angle: number,
|
|
221
|
+
text: string,
|
|
222
|
+
fontSize: number,
|
|
223
|
+
style: { fill: string; fontWeight?: number },
|
|
224
|
+
metadata: Record<string, unknown>,
|
|
225
|
+
): FloorplanGeometry {
|
|
226
|
+
return {
|
|
227
|
+
kind: 'group',
|
|
228
|
+
transform: { translate: at, rotate: readableAngle(angle) },
|
|
229
|
+
children: [
|
|
230
|
+
{
|
|
231
|
+
kind: 'text',
|
|
232
|
+
x: 0,
|
|
233
|
+
y: 0,
|
|
234
|
+
text,
|
|
235
|
+
fontSize,
|
|
236
|
+
fill: style.fill,
|
|
237
|
+
fontWeight: style.fontWeight ?? 500,
|
|
238
|
+
textAnchor: 'middle',
|
|
239
|
+
dominantBaseline: 'middle',
|
|
240
|
+
},
|
|
241
|
+
],
|
|
242
|
+
metadata,
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** The exterior steps as tread lines (three, a tread apart), site metres. */
|
|
247
|
+
function stairTreads(
|
|
248
|
+
scene: SceneSnapshot,
|
|
249
|
+
level: LevelNode | null,
|
|
250
|
+
building: BuildingNode | null,
|
|
251
|
+
): FloorplanGeometry[] {
|
|
252
|
+
if (!level) return []
|
|
253
|
+
const toSite = siteFrame(building)
|
|
254
|
+
const out: FloorplanGeometry[] = []
|
|
255
|
+
for (const childId of level.children) {
|
|
256
|
+
const child = scene.nodes[childId as AnyNodeId] as AnyNode | undefined
|
|
257
|
+
if (child?.type !== 'stair') continue
|
|
258
|
+
const stair = child as unknown as { position?: number[]; rotation?: unknown; width?: number }
|
|
259
|
+
const x = stair.position?.[0] ?? 0
|
|
260
|
+
const z = stair.position?.[2] ?? 0
|
|
261
|
+
const r = yawOf(stair.rotation)
|
|
262
|
+
const half = (stair.width ?? 1) / 2
|
|
263
|
+
// the run climbs toward the floor it serves: +local z turned by the yaw
|
|
264
|
+
const dir: Pt = [Math.sin(r), Math.cos(r)]
|
|
265
|
+
const across: Pt = [Math.cos(r), -Math.sin(r)]
|
|
266
|
+
for (let k = 0; k < 3; k++) {
|
|
267
|
+
const cx = x + dir[0] * k * 0.28
|
|
268
|
+
const cz = z + dir[1] * k * 0.28
|
|
269
|
+
const a = toSite(cx - across[0] * half, cz - across[1] * half)
|
|
270
|
+
const b = toSite(cx + across[0] * half, cz + across[1] * half)
|
|
271
|
+
out.push({
|
|
272
|
+
kind: 'line',
|
|
273
|
+
x1: a[0],
|
|
274
|
+
y1: a[1],
|
|
275
|
+
x2: b[0],
|
|
276
|
+
y2: b[1],
|
|
277
|
+
stroke: INK,
|
|
278
|
+
strokeWidth: 0.02,
|
|
279
|
+
metadata: { sitePlan: 'steps' },
|
|
280
|
+
})
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return out
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* The setbacks line the site plan prints when the yards are not the zoning
|
|
288
|
+
* code's own: worded for a plans examiner from the site's numbers — never the
|
|
289
|
+
* internal source tag a lot drop-in stored (QA 2026-09-23 printed the
|
|
290
|
+
* internal defaults tag on A1.0). A cited code prints nothing.
|
|
291
|
+
*/
|
|
292
|
+
export function setbacksWarning(
|
|
293
|
+
site: Pick<SiteNode, 'setbacks' | 'setbacksSource'> | null,
|
|
294
|
+
): string | null {
|
|
295
|
+
const source = site?.setbacksSource ?? ''
|
|
296
|
+
const s = site?.setbacks
|
|
297
|
+
if (!s || !/draft|verify|default|by hand/i.test(source)) return null
|
|
298
|
+
const ft = (m: number) => `${Math.round((m / METRES_PER_FOOT) * 2) / 2} ft`
|
|
299
|
+
const yards = `front ${ft(s.front)}, side ${ft(s.side)}, rear ${ft(s.rear)}`
|
|
300
|
+
if (/by hand/i.test(source))
|
|
301
|
+
return `Setbacks: set by hand (${yards}) — confirm with the zoning district.`
|
|
302
|
+
if (/default/i.test(source))
|
|
303
|
+
return `Setbacks: planning defaults (${yards}) — confirm with the zoning district.`
|
|
304
|
+
return `Setbacks: ${source}`
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Build the site-plan drawing from the scene.
|
|
309
|
+
*
|
|
310
|
+
* Everything is recomputed from the current snapshot, so the drawing is live:
|
|
311
|
+
* move the building or edit a setback and the next call reflects it.
|
|
312
|
+
* Returns an empty drawing (no primitives) when the scene has no site polygon.
|
|
313
|
+
*/
|
|
314
|
+
export function buildSitePlanDrawing(scene: SceneSnapshot): SitePlanDrawing {
|
|
315
|
+
const site = findSite(scene)
|
|
316
|
+
const lot = (site?.polygon?.points ?? []) as Pt[]
|
|
317
|
+
const building = findBuilding(scene, site)
|
|
318
|
+
const level = findLowestLevel(scene, building)
|
|
319
|
+
const footprintLoops = levelFootprintLoops(scene, level, building)
|
|
320
|
+
const footprintBounds = unionBounds(footprintLoops)
|
|
321
|
+
const northRotation = site?.northRotation ?? 0
|
|
322
|
+
const frontEdge = resolveFrontEdge(lot, site?.frontEdge, northRotation)
|
|
323
|
+
|
|
324
|
+
const empty: SitePlanDrawing = {
|
|
325
|
+
primitives: [],
|
|
326
|
+
warnings: [],
|
|
327
|
+
bounds: footprintBounds ?? { minX: -15, minY: -15, maxX: 15, maxY: 15 },
|
|
328
|
+
meta: {
|
|
329
|
+
site,
|
|
330
|
+
frontEdge,
|
|
331
|
+
lot,
|
|
332
|
+
envelope: [],
|
|
333
|
+
envelopeFrontEdge: 0,
|
|
334
|
+
sightTriangles: [],
|
|
335
|
+
footprintLoops,
|
|
336
|
+
footprintBounds,
|
|
337
|
+
yards: [],
|
|
338
|
+
buildingId: (building?.id as AnyNodeId) ?? null,
|
|
339
|
+
},
|
|
340
|
+
}
|
|
341
|
+
if (lot.length < 3) return empty
|
|
342
|
+
|
|
343
|
+
const primitives: FloorplanGeometry[] = []
|
|
344
|
+
// the coverage figures and the outdoor parts, from the one shared reading
|
|
345
|
+
// (coverage.ts) — the cover's PROJECT DATA prints the same numbers
|
|
346
|
+
const coverage = computeSiteCoverage(scene)
|
|
347
|
+
|
|
348
|
+
// ── Lot line — the heaviest line on the sheet ────────────────────────
|
|
349
|
+
primitives.push({
|
|
350
|
+
kind: 'polygon',
|
|
351
|
+
points: lot,
|
|
352
|
+
fill: 'none',
|
|
353
|
+
stroke: INK,
|
|
354
|
+
strokeWidth: LOT_STROKE_WIDTH,
|
|
355
|
+
strokeLinejoin: 'miter',
|
|
356
|
+
metadata: { sitePlan: 'lot-line' },
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
// ── Terrain contours — the ground's lines, at the site's interval ─────
|
|
360
|
+
// (site.contourIntervalIn, default 12 in; 0 = none): thin brown lines at
|
|
361
|
+
// whole elevations above the survey datum when the terrain sample carries
|
|
362
|
+
// one (USGS EPQS at the lot centre, NAVD88), every fifth heavier, each
|
|
363
|
+
// elevation labelled once or twice (site-annotations.ts).
|
|
364
|
+
const field = site ? terrainFieldOf(site as never) : null
|
|
365
|
+
const datumFt = (() => {
|
|
366
|
+
const sample = (site?.metadata as { terrainSample?: { datumFt?: unknown } } | undefined)
|
|
367
|
+
?.terrainSample
|
|
368
|
+
return typeof sample?.datumFt === 'number' ? sample.datumFt : null
|
|
369
|
+
})()
|
|
370
|
+
{
|
|
371
|
+
const intervalIn = typeof site?.contourIntervalIn === 'number' ? site.contourIntervalIn : 12
|
|
372
|
+
if (site && field && intervalIn > 0) {
|
|
373
|
+
const lines = contourPrimitives({
|
|
374
|
+
site,
|
|
375
|
+
lot,
|
|
376
|
+
field,
|
|
377
|
+
intervalIn,
|
|
378
|
+
datumFt,
|
|
379
|
+
fontSize: LABEL_SIZE * 0.45,
|
|
380
|
+
// no elevation printed on the house, a porch or the paving
|
|
381
|
+
avoid: [...coverage.outline, ...coverage.parts.map((p) => p.ring)],
|
|
382
|
+
})
|
|
383
|
+
if (lines.length > 0) primitives.push({ kind: 'group', children: lines })
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ── The driveway and walks, under the yards' labels and dimensions ────
|
|
388
|
+
primitives.push(...flatworkPrimitives(scene, building, coverage.parts, LABEL_SIZE * 0.42))
|
|
389
|
+
|
|
390
|
+
// ── Setback envelope — dashed, offset inward per edge ────────────────
|
|
391
|
+
const streetEdges = (site?.streetEdges ?? []).filter((i) => Number.isFinite(i))
|
|
392
|
+
const sightTriangleM =
|
|
393
|
+
typeof site?.sightTriangleFt === 'number' && site.sightTriangleFt > 0
|
|
394
|
+
? site.sightTriangleFt * METRES_PER_FOOT
|
|
395
|
+
: 0
|
|
396
|
+
const envelope = site?.setbacks
|
|
397
|
+
? setbackEnvelope(lot, site.setbacks, frontEdge, { streetEdges, sightTriangleM })
|
|
398
|
+
: []
|
|
399
|
+
// the corner sight triangles (clear-vision at a street intersection):
|
|
400
|
+
// dashed, labelled with the leg — the ordinance's figure, verify locally
|
|
401
|
+
const sightTriangles: Pt[][] = []
|
|
402
|
+
if (sightTriangleM > 0) {
|
|
403
|
+
for (const [a, b] of streetCorners(lot, [frontEdge, ...streetEdges])) {
|
|
404
|
+
const tri = sightTriangle(lot, a, b, sightTriangleM)
|
|
405
|
+
if (!tri) continue
|
|
406
|
+
const ring: Pt[] = [tri.corner, tri.a, tri.b]
|
|
407
|
+
sightTriangles.push(ring)
|
|
408
|
+
primitives.push({
|
|
409
|
+
kind: 'polygon',
|
|
410
|
+
points: ring,
|
|
411
|
+
fill: 'none',
|
|
412
|
+
stroke: INK_SOFT,
|
|
413
|
+
strokeWidth: ENVELOPE_STROKE_WIDTH,
|
|
414
|
+
strokeDasharray: '0.3 0.2',
|
|
415
|
+
opacity: 0.75,
|
|
416
|
+
metadata: { sitePlan: 'sight-triangle' },
|
|
417
|
+
})
|
|
418
|
+
const cx = (tri.corner[0] + tri.a[0] + tri.b[0]) / 3
|
|
419
|
+
const cz = (tri.corner[1] + tri.a[1] + tri.b[1]) / 3
|
|
420
|
+
primitives.push({
|
|
421
|
+
kind: 'text',
|
|
422
|
+
x: cx,
|
|
423
|
+
y: cz,
|
|
424
|
+
text: `SIGHT TRIANGLE ${site?.sightTriangleFt}' (VERIFY)`,
|
|
425
|
+
fontSize: 0.6,
|
|
426
|
+
fill: INK_SOFT,
|
|
427
|
+
textAnchor: 'middle',
|
|
428
|
+
metadata: { sitePlan: 'sight-triangle-label' },
|
|
429
|
+
})
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
if (envelope.length >= 3) {
|
|
433
|
+
primitives.push({
|
|
434
|
+
kind: 'polygon',
|
|
435
|
+
points: envelope,
|
|
436
|
+
fill: 'none',
|
|
437
|
+
stroke: INK_SOFT,
|
|
438
|
+
strokeWidth: ENVELOPE_STROKE_WIDTH,
|
|
439
|
+
strokeDasharray: '0.9 0.45',
|
|
440
|
+
opacity: 0.75,
|
|
441
|
+
metadata: { sitePlan: 'setback-envelope' },
|
|
442
|
+
})
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ── Building footprint — the OUTLINE of the level-0 walls ─────────────
|
|
446
|
+
// A site plan shows the building's edge on the lot; the partitions inside
|
|
447
|
+
// it are the floor plan's business, so the wall bands are unioned and only
|
|
448
|
+
// the outer rings are kept.
|
|
449
|
+
const outerRings = footprintOutline(footprintLoops)
|
|
450
|
+
for (const loop of outerRings) {
|
|
451
|
+
primitives.push({
|
|
452
|
+
kind: 'polygon',
|
|
453
|
+
points: loop,
|
|
454
|
+
fill: FOOTPRINT_FILL,
|
|
455
|
+
fillOpacity: 0.7,
|
|
456
|
+
stroke: FOOTPRINT_STROKE,
|
|
457
|
+
strokeWidth: 0.06,
|
|
458
|
+
metadata: { sitePlan: 'building-footprint', buildingId: building?.id ?? null },
|
|
459
|
+
})
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// ── Yard dimensions — the house's own faces, square out to the lot line ──
|
|
463
|
+
const yards = footprintLoops.length
|
|
464
|
+
? castYardDimensionsOriented(lot, footprintLoops, building?.rotation?.[1] ?? 0)
|
|
465
|
+
: []
|
|
466
|
+
for (const yard of yards) {
|
|
467
|
+
const dx = yard.to[0] - yard.from[0]
|
|
468
|
+
const dy = yard.to[1] - yard.from[1]
|
|
469
|
+
const len = Math.hypot(dx, dy) || 1
|
|
470
|
+
// Offset normal is perpendicular to the measured run; zero offset keeps
|
|
471
|
+
// the dimension line on the yard itself, which is how site plans read.
|
|
472
|
+
primitives.push({
|
|
473
|
+
kind: 'dimension',
|
|
474
|
+
start: yard.from,
|
|
475
|
+
end: yard.to,
|
|
476
|
+
offsetNormal: [-dy / len, dx / len],
|
|
477
|
+
offsetDistance: 0,
|
|
478
|
+
extensionOvershoot: 0.35,
|
|
479
|
+
stroke: DIMENSION_STROKE,
|
|
480
|
+
terminator: 'architectural-tick',
|
|
481
|
+
text: formatFeetInches(yard.distance),
|
|
482
|
+
metadata: { sitePlan: 'yard-dimension', side: yard.side },
|
|
483
|
+
} as FloorplanGeometry)
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const lotBounds = polygonBounds(lot)
|
|
487
|
+
const warnings: string[] = []
|
|
488
|
+
|
|
489
|
+
// ── Property lines: bearing and length along each edge, outside the lot ──
|
|
490
|
+
for (let i = 0; i < lot.length; i++) {
|
|
491
|
+
const p = lot[i] as Pt
|
|
492
|
+
const q = lot[(i + 1) % lot.length] as Pt
|
|
493
|
+
const dx = q[0] - p[0]
|
|
494
|
+
const dy = q[1] - p[1]
|
|
495
|
+
const len = Math.hypot(dx, dy)
|
|
496
|
+
if (len < 0.5) continue
|
|
497
|
+
const out = outwardNormal(lot, i)
|
|
498
|
+
primitives.push(
|
|
499
|
+
alongLabel(
|
|
500
|
+
[(p[0] + q[0]) / 2 + out[0] * 0.55, (p[1] + q[1]) / 2 + out[1] * 0.55],
|
|
501
|
+
Math.atan2(dy, dx),
|
|
502
|
+
`${bearingOf(dx, dy, northRotation)} ${(len / METRES_PER_FOOT).toFixed(2)}'`,
|
|
503
|
+
LABEL_SIZE * 0.48,
|
|
504
|
+
{ fill: INK, fontWeight: 600 },
|
|
505
|
+
{ sitePlan: 'lot-edge-label', edge: i },
|
|
506
|
+
),
|
|
507
|
+
)
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// ── Setback labels, in each yard along its lot edge ───────────────────
|
|
511
|
+
if (site?.setbacks && envelope.length >= 3) {
|
|
512
|
+
const roles = classifyEdges(lot, frontEdge, streetEdges)
|
|
513
|
+
const NAME = {
|
|
514
|
+
front: 'FRONT',
|
|
515
|
+
rear: 'REAR',
|
|
516
|
+
street: 'STREET SIDE',
|
|
517
|
+
left: 'SIDE',
|
|
518
|
+
right: 'SIDE',
|
|
519
|
+
} as const
|
|
520
|
+
for (let i = 0; i < lot.length; i++) {
|
|
521
|
+
const role = roles[i]
|
|
522
|
+
if (!role) continue
|
|
523
|
+
const s = setbackForRole(site.setbacks, role)
|
|
524
|
+
if (!(s > 0)) continue
|
|
525
|
+
const p = lot[i] as Pt
|
|
526
|
+
const q = lot[(i + 1) % lot.length] as Pt
|
|
527
|
+
const dx = q[0] - p[0]
|
|
528
|
+
const dy = q[1] - p[1]
|
|
529
|
+
if (Math.hypot(dx, dy) < 1) continue
|
|
530
|
+
const out = outwardNormal(lot, i)
|
|
531
|
+
// a third of the way along the edge, halfway into the yard — clear of
|
|
532
|
+
// the yard dimension that stands square off the house
|
|
533
|
+
primitives.push(
|
|
534
|
+
alongLabel(
|
|
535
|
+
[p[0] + dx * 0.32 - out[0] * (s / 2), p[1] + dy * 0.32 - out[1] * (s / 2)],
|
|
536
|
+
Math.atan2(dy, dx),
|
|
537
|
+
`${formatFeetInches(s)} ${NAME[role]} SETBACK`,
|
|
538
|
+
Math.min(LABEL_SIZE * 0.4, s * 0.45),
|
|
539
|
+
{ fill: INK_SOFT },
|
|
540
|
+
{ sitePlan: 'setback-label', edge: i, role },
|
|
541
|
+
),
|
|
542
|
+
)
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// ── The streets, each named beyond the edge it runs along ─────────────
|
|
547
|
+
// (only the edges that front a street; the name the lot drop-in stored
|
|
548
|
+
// for that edge — site-annotations.ts `streetEdgeNames`)
|
|
549
|
+
if (site) {
|
|
550
|
+
for (const [i, name] of streetEdgeNames(site, frontEdge)) {
|
|
551
|
+
const p = lot[i]
|
|
552
|
+
const q = lot[(i + 1) % lot.length]
|
|
553
|
+
if (!p || !q) continue
|
|
554
|
+
const out = outwardNormal(lot, i)
|
|
555
|
+
primitives.push(
|
|
556
|
+
alongLabel(
|
|
557
|
+
[(p[0] + q[0]) / 2 + out[0] * 2.2, (p[1] + q[1]) / 2 + out[1] * 2.2],
|
|
558
|
+
Math.atan2(q[1] - p[1], q[0] - p[0]),
|
|
559
|
+
name,
|
|
560
|
+
LABEL_SIZE * (name.length > 24 ? 0.6 : 0.85),
|
|
561
|
+
{ fill: INK, fontWeight: 700 },
|
|
562
|
+
{ sitePlan: 'street-name', edge: i },
|
|
563
|
+
),
|
|
564
|
+
)
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// ── The roof line, dashed over the footprint ─────────────────────────
|
|
569
|
+
for (const ring of roofOutlineRings(scene, level, building)) {
|
|
570
|
+
primitives.push({
|
|
571
|
+
kind: 'polygon',
|
|
572
|
+
points: ring,
|
|
573
|
+
fill: 'none',
|
|
574
|
+
stroke: INK_SOFT,
|
|
575
|
+
strokeWidth: 0.035,
|
|
576
|
+
strokeDasharray: '0.5 0.25',
|
|
577
|
+
opacity: 0.9,
|
|
578
|
+
metadata: { sitePlan: 'roof-outline' },
|
|
579
|
+
})
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// ── Porches, patios, decks, landings and their steps ─────────────────
|
|
583
|
+
for (const porch of coverage.parts) {
|
|
584
|
+
if (porch.kind === 'driveway' || porch.kind === 'walk') continue
|
|
585
|
+
primitives.push({
|
|
586
|
+
kind: 'polygon',
|
|
587
|
+
points: porch.ring,
|
|
588
|
+
fill: '#f3f4f6',
|
|
589
|
+
fillOpacity: 0.95,
|
|
590
|
+
stroke: FOOTPRINT_STROKE,
|
|
591
|
+
strokeWidth: 0.03,
|
|
592
|
+
metadata: { sitePlan: 'porch' },
|
|
593
|
+
})
|
|
594
|
+
const b = polygonBounds(porch.ring)
|
|
595
|
+
primitives.push({
|
|
596
|
+
kind: 'text',
|
|
597
|
+
x: (b.minX + b.maxX) / 2,
|
|
598
|
+
y: (b.minY + b.maxY) / 2,
|
|
599
|
+
text: porch.covered ? porch.label : `${porch.label} (OPEN)`,
|
|
600
|
+
fontSize: Math.min(
|
|
601
|
+
LABEL_SIZE * 0.4,
|
|
602
|
+
Math.max(0.2, Math.min(b.maxY - b.minY, b.maxX - b.minX) * 0.3),
|
|
603
|
+
),
|
|
604
|
+
fill: INK,
|
|
605
|
+
fontWeight: 600,
|
|
606
|
+
textAnchor: 'middle',
|
|
607
|
+
dominantBaseline: 'middle',
|
|
608
|
+
upright: true,
|
|
609
|
+
metadata: { sitePlan: 'porch-label' },
|
|
610
|
+
})
|
|
611
|
+
}
|
|
612
|
+
primitives.push(...stairTreads(scene, level, building))
|
|
613
|
+
|
|
614
|
+
// ── The utility services a plugin located, run schematically to the street ─
|
|
615
|
+
const streetSide = [...new Set([frontEdge, ...streetEdges])]
|
|
616
|
+
const services = servicePrimitives({
|
|
617
|
+
scene,
|
|
618
|
+
level,
|
|
619
|
+
building,
|
|
620
|
+
lot,
|
|
621
|
+
streetEdges: streetSide,
|
|
622
|
+
outline: outerRings,
|
|
623
|
+
fontSize: LABEL_SIZE * 0.4,
|
|
624
|
+
})
|
|
625
|
+
primitives.push(...services.primitives)
|
|
626
|
+
|
|
627
|
+
// ── Drainage: the grade falls away from every face of the house ──────
|
|
628
|
+
primitives.push(
|
|
629
|
+
...drainagePrimitives(outerRings, LABEL_SIZE * 0.42, [
|
|
630
|
+
...coverage.parts.map((p) => p.ring),
|
|
631
|
+
...(services.pad ? [services.pad] : []),
|
|
632
|
+
]),
|
|
633
|
+
)
|
|
634
|
+
|
|
635
|
+
// ── Spot grades at the lot and house corners (the terrain's) ─────────
|
|
636
|
+
if (field) {
|
|
637
|
+
primitives.push(
|
|
638
|
+
...spotElevationPrimitives({
|
|
639
|
+
field,
|
|
640
|
+
datumFt,
|
|
641
|
+
lot,
|
|
642
|
+
outline: outerRings,
|
|
643
|
+
fontSize: LABEL_SIZE * 0.34,
|
|
644
|
+
}),
|
|
645
|
+
)
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// ── The house, named on its footprint, with its finish floor ─────────
|
|
649
|
+
if (footprintBounds && coverage.buildingSqFt > 0) {
|
|
650
|
+
let storeys = 0
|
|
651
|
+
for (const childId of building?.children ?? []) {
|
|
652
|
+
const child = scene.nodes[childId as AnyNodeId] as AnyNode | undefined
|
|
653
|
+
if (child?.type === 'level' && (child as LevelNode).level >= 0) storeys++
|
|
654
|
+
}
|
|
655
|
+
const cx = (footprintBounds.minX + footprintBounds.maxX) / 2
|
|
656
|
+
const cy = (footprintBounds.minY + footprintBounds.maxY) / 2
|
|
657
|
+
const lines = [
|
|
658
|
+
`${Math.max(1, storeys)}-STORY RESIDENCE`,
|
|
659
|
+
`${formatSqFt(coverage.buildingSqFt)} FOOTPRINT`,
|
|
660
|
+
]
|
|
661
|
+
// the finish floor (and the garage pad) above the survey datum: the
|
|
662
|
+
// building's datum plus the storey slab's top
|
|
663
|
+
const tops = floorTops(scene, level)
|
|
664
|
+
const base = (building?.position?.[1] ?? 0) + (level?.baseElevation ?? 0)
|
|
665
|
+
if (tops.floor !== null) lines.push(`FF EL. ${formatElevation(base + tops.floor, datumFt)}`)
|
|
666
|
+
if (tops.garage !== null)
|
|
667
|
+
lines.push(`GARAGE SLAB EL. ${formatElevation(base + tops.garage, datumFt)}`)
|
|
668
|
+
lines.forEach((text, i) => {
|
|
669
|
+
primitives.push({
|
|
670
|
+
kind: 'text',
|
|
671
|
+
x: cx,
|
|
672
|
+
y: cy + (i - (lines.length - 1) / 2) * LABEL_SIZE * 0.9,
|
|
673
|
+
text,
|
|
674
|
+
fontSize: LABEL_SIZE * (i < 2 ? 0.6 : 0.5),
|
|
675
|
+
fill: INK,
|
|
676
|
+
fontWeight: i < 2 ? 700 : 600,
|
|
677
|
+
textAnchor: 'middle',
|
|
678
|
+
dominantBaseline: 'middle',
|
|
679
|
+
upright: true,
|
|
680
|
+
metadata: { sitePlan: i < 2 ? 'building-label' : 'floor-elevation' },
|
|
681
|
+
})
|
|
682
|
+
})
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const setbacksNote = setbacksWarning(site)
|
|
686
|
+
if (setbacksNote) warnings.push(setbacksNote)
|
|
687
|
+
if (!coverage.parts.some((p) => p.kind === 'driveway' || p.kind === 'walk'))
|
|
688
|
+
warnings.push('No driveway or walk is drawn — none is shown on the site plan.')
|
|
689
|
+
|
|
690
|
+
// ── North arrow, top-right of the lot ────────────────────────────────
|
|
691
|
+
const arrowSize = Math.max(1.2, Math.min(2.2, (lotBounds.maxY - lotBounds.minY) * 0.07))
|
|
692
|
+
primitives.push(
|
|
693
|
+
northArrow(
|
|
694
|
+
[lotBounds.maxX + arrowSize * 1.4, lotBounds.minY + arrowSize * 1.2],
|
|
695
|
+
northRotation,
|
|
696
|
+
arrowSize,
|
|
697
|
+
),
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
// ── Lot area / APN, and the coverage figures, under the lot ──────────
|
|
701
|
+
const lotLine = [
|
|
702
|
+
coverage.lotSqFt !== null ? `LOT AREA ${formatSqFt(coverage.lotSqFt)}` : '',
|
|
703
|
+
site?.parcel?.apn ? `APN ${site.parcel.apn}` : '',
|
|
704
|
+
site?.zone ? `ZONE ${site.zone}` : '',
|
|
705
|
+
].filter(Boolean)
|
|
706
|
+
const coverageLine =
|
|
707
|
+
coverage.buildingSqFt > 0
|
|
708
|
+
? [
|
|
709
|
+
`BUILDING COVERAGE ${formatSqFt(coverage.buildingCoverageSqFt)} (${formatCoveragePercent(coverage.buildingCoverageRatio)})`,
|
|
710
|
+
`IMPERVIOUS ${formatSqFt(coverage.imperviousSqFt)} (${formatCoveragePercent(coverage.imperviousRatio)})`,
|
|
711
|
+
]
|
|
712
|
+
: []
|
|
713
|
+
;[lotLine, coverageLine].forEach((bits, i) => {
|
|
714
|
+
if (bits.length === 0) return
|
|
715
|
+
primitives.push({
|
|
716
|
+
kind: 'text',
|
|
717
|
+
x: (lotBounds.minX + lotBounds.maxX) / 2,
|
|
718
|
+
y: lotBounds.maxY + LABEL_SIZE * (1.6 + i * 1.25),
|
|
719
|
+
text: bits.join(' · '),
|
|
720
|
+
fontSize: LABEL_SIZE * (i === 0 ? 1 : 0.85),
|
|
721
|
+
fill: INK,
|
|
722
|
+
fontWeight: 600,
|
|
723
|
+
textAnchor: 'middle',
|
|
724
|
+
dominantBaseline: 'hanging',
|
|
725
|
+
upright: true,
|
|
726
|
+
metadata: { sitePlan: i === 0 ? 'lot-label' : 'coverage-label' },
|
|
727
|
+
})
|
|
728
|
+
})
|
|
729
|
+
|
|
730
|
+
const combined = footprintBounds
|
|
731
|
+
? {
|
|
732
|
+
minX: Math.min(lotBounds.minX, footprintBounds.minX),
|
|
733
|
+
minY: Math.min(lotBounds.minY, footprintBounds.minY),
|
|
734
|
+
maxX: Math.max(lotBounds.maxX, footprintBounds.maxX),
|
|
735
|
+
maxY: Math.max(lotBounds.maxY, footprintBounds.maxY),
|
|
736
|
+
}
|
|
737
|
+
: lotBounds
|
|
738
|
+
|
|
739
|
+
// Plugin kinds that live in site metres (utilities …) — see contributors.ts.
|
|
740
|
+
primitives.push(...sitePlanContributions(scene))
|
|
741
|
+
|
|
742
|
+
return {
|
|
743
|
+
primitives,
|
|
744
|
+
warnings,
|
|
745
|
+
bounds: padBounds(combined, Math.max(2, arrowSize * 1.8)),
|
|
746
|
+
meta: {
|
|
747
|
+
site,
|
|
748
|
+
frontEdge,
|
|
749
|
+
lot,
|
|
750
|
+
envelope,
|
|
751
|
+
envelopeFrontEdge: envelope.length >= 3 ? envelopeFrontEdge(lot, frontEdge, envelope) : 0,
|
|
752
|
+
sightTriangles,
|
|
753
|
+
footprintLoops,
|
|
754
|
+
footprintBounds,
|
|
755
|
+
yards,
|
|
756
|
+
buildingId: (building?.id as AnyNodeId) ?? null,
|
|
757
|
+
coverage,
|
|
758
|
+
},
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Translation (site metres, `[dx, dz]`) that centres the building's footprint
|
|
764
|
+
* on the lot centroid. `null` when there is nothing to move or the footprint
|
|
765
|
+
* already sits inside the ring.
|
|
766
|
+
*/
|
|
767
|
+
export function buildingRecentreOffset(
|
|
768
|
+
lot: readonly Pt[],
|
|
769
|
+
footprintBounds: Bounds | null,
|
|
770
|
+
): [number, number] | null {
|
|
771
|
+
if (lot.length < 3 || !footprintBounds) return null
|
|
772
|
+
if (boundsInsidePolygon(lot, footprintBounds)) return null
|
|
773
|
+
const lotB = polygonBounds(lot)
|
|
774
|
+
const lotCx = (lotB.minX + lotB.maxX) / 2
|
|
775
|
+
const lotCy = (lotB.minY + lotB.maxY) / 2
|
|
776
|
+
const fpCx = (footprintBounds.minX + footprintBounds.maxX) / 2
|
|
777
|
+
const fpCy = (footprintBounds.minY + footprintBounds.maxY) / 2
|
|
778
|
+
return [lotCx - fpCx, lotCy - fpCy]
|
|
779
|
+
}
|