@pascal-app/editor 1.0.3 → 1.0.5
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/README.md +106 -0
- package/package.json +42 -38
- package/src/components/editor/__bench__/room-label-placement.bench.ts +108 -0
- package/src/components/editor/action-menu-button.tsx +83 -0
- package/src/components/editor/bake-thumbnail.tsx +307 -5
- package/src/components/editor/capture-level-picker.tsx +72 -0
- package/src/components/editor/construction-reveal-audio.test.tsx +150 -0
- package/src/components/editor/construction-reveal-audio.tsx +40 -0
- package/src/components/editor/custom-camera-controls.tsx +275 -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 +37 -26
- package/src/components/editor/editor-layout-v2.tsx +132 -59
- package/src/components/editor/export-manager.tsx +47 -26
- package/src/components/editor/first-person/build-collider-world.test.ts +320 -0
- package/src/components/editor/first-person/build-collider-world.ts +28 -7
- package/src/components/editor/first-person/ground-holes.test.ts +128 -0
- package/src/components/editor/first-person/ground-holes.ts +130 -0
- package/src/components/editor/first-person/mechanism-targets.test.ts +162 -0
- package/src/components/editor/first-person/mechanism-targets.ts +88 -0
- package/src/components/editor/first-person-controls.tsx +220 -9
- package/src/components/editor/floating-action-menu.tsx +63 -207
- package/src/components/editor/floor-aware-camera-controls.test.ts +466 -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-mode-coordinator.tsx +2 -2
- package/src/components/editor/floorplan-panel.tsx +414 -363
- package/src/components/editor/follow-pascal.test.tsx +912 -0
- package/src/components/editor/follow-pascal.tsx +354 -0
- package/src/components/editor/footprint-height-handle.tsx +204 -0
- package/src/components/editor/group-actions.ts +56 -2
- package/src/components/editor/group-move-3d.ts +2 -0
- package/src/components/editor/group-rotate-handle.tsx +2 -0
- package/src/components/editor/group-transform-shared.test.ts +460 -0
- 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 +409 -197
- 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/riser-diagram-panel.tsx +14 -7
- 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 +233 -0
- package/src/components/editor/room-labels.test.ts +850 -0
- package/src/components/editor/room-labels.tsx +1890 -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 +474 -419
- package/src/components/editor/slab-hole-highlights.tsx +5 -2
- package/src/components/editor/snapshot-capture-overlay.tsx +7 -2
- package/src/components/editor/snapshot-capture.test.ts +119 -60
- package/src/components/editor/snapshot-capture.ts +36 -5
- package/src/components/editor/thumbnail-generator.test.ts +654 -0
- package/src/components/editor/thumbnail-generator.tsx +1103 -223
- package/src/components/editor/use-floorplan-background-placement.ts +34 -12
- 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 +663 -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.test.tsx +54 -0
- package/src/components/editor/views/view-bar.tsx +343 -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 +8 -4
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
- package/src/components/editor-2d/floorplan-group-move.tsx +3 -0
- 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 +2 -2
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +116 -106
- 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 +319 -233
- package/src/components/editor-2d/renderers/floorplan-reveal.test.tsx +254 -0
- package/src/components/editor-2d/renderers/floorplan-reveal.ts +141 -0
- package/src/components/editor-2d/svg-paths.ts +12 -15
- package/src/components/point-ask/bubble.tsx +538 -0
- package/src/components/point-ask/hover-chip.tsx +91 -0
- package/src/components/point-ask/mode-pill.tsx +92 -0
- package/src/components/point-ask/optional-motion.ts +22 -0
- package/src/components/point-ask/outline-corners.tsx +93 -0
- package/src/components/point-ask/pins.tsx +711 -0
- package/src/components/point-ask/point-ask-layer.tsx +83 -0
- package/src/components/point-ask/point-ask-styles.ts +105 -0
- package/src/components/point-ask/region-marquee-styles.ts +15 -0
- package/src/components/point-ask/region-marquee.tsx +64 -0
- package/src/components/procedural-items/inspector.tsx +40 -15
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +12 -2
- 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/roof/roof-edit-system.tsx +1 -0
- 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/distribution/connection-compatibility.test.ts +34 -0
- package/src/components/tools/distribution/connection-compatibility.ts +43 -0
- package/src/components/tools/distribution/distribution-public.ts +9 -0
- package/src/components/tools/distribution/distribution-run-contract.test.ts +26 -0
- package/src/components/tools/distribution/distribution-run-contract.ts +94 -0
- package/src/components/tools/distribution/distribution-run-tool.test.ts +241 -0
- package/src/components/tools/distribution/distribution-run-tool.test.tsx +125 -0
- package/src/components/tools/distribution/distribution-run-tool.tsx +1323 -0
- package/src/components/tools/distribution/draw-alignment.ts +35 -0
- package/src/components/tools/distribution/ports.test.ts +56 -0
- package/src/components/tools/distribution/ports.ts +378 -0
- package/src/components/tools/distribution/run-cursor.test.ts +135 -0
- package/src/components/tools/distribution/run-cursor.ts +36 -0
- package/src/components/tools/distribution/run-direction-feedback.test.ts +60 -0
- package/src/components/tools/distribution/run-direction-feedback.tsx +251 -0
- package/src/components/tools/distribution/run-port-snap.test.ts +79 -0
- package/src/components/tools/distribution/run-port-snap.ts +25 -0
- package/src/components/tools/distribution/run-screen-direction.test.ts +23 -0
- package/src/components/tools/distribution/run-screen-direction.ts +30 -0
- package/src/components/tools/elevator/elevator-tool.tsx +3 -6
- package/src/components/tools/elevator/move-elevator-tool.tsx +2 -3
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +67 -68
- package/src/components/tools/item/placement-history.test.tsx +512 -0
- package/src/components/tools/item/placement-strategies.test.ts +46 -1
- package/src/components/tools/item/placement-strategies.ts +11 -10
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +56 -56
- 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 +148 -95
- 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 +62 -63
- package/src/components/tools/registry/item-surface-move.ts +2 -0
- package/src/components/tools/registry/item-surface-resolver.test.ts +26 -26
- package/src/components/tools/registry/move-registry-node-tool.test.ts +268 -1
- package/src/components/tools/registry/move-registry-node-tool.tsx +66 -30
- 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/tool-manager.tsx +59 -24
- package/src/components/tools/wall/wall-drafting.test.ts +175 -47
- package/src/components/tools/zone/zone-tool.tsx +4 -12
- package/src/components/ui/action-menu/action-button.tsx +9 -1
- package/src/components/ui/action-menu/camera-actions.tsx +9 -4
- package/src/components/ui/action-menu/control-modes.tsx +15 -42
- package/src/components/ui/action-menu/follow-pascal-pill.tsx +32 -0
- package/src/components/ui/action-menu/follow-pascal-toggle.tsx +36 -0
- package/src/components/ui/action-menu/index.tsx +56 -33
- package/src/components/ui/action-menu/measurement-control.tsx +14 -19
- package/src/components/ui/action-menu/placement.ts +20 -0
- package/src/components/ui/action-menu/point-ask-button.tsx +52 -0
- package/src/components/ui/action-menu/view-toggles.tsx +47 -42
- package/src/components/ui/build-panel/build-panel.tsx +242 -0
- package/src/components/ui/command-palette/editor-commands.tsx +7 -42
- package/src/components/ui/command-palette/index.tsx +50 -39
- package/src/components/ui/command-palette/ranked-groups.test.ts +31 -0
- package/src/components/ui/controls/action-button.tsx +5 -2
- 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/metric-control.tsx +1 -1
- package/src/components/ui/controls/scene-material-list.tsx +4 -32
- package/src/components/ui/controls/segmented-control.tsx +21 -8
- package/src/components/ui/controls/select-control.tsx +65 -0
- package/src/components/ui/controls/slider-control.tsx +1 -1
- package/src/components/ui/controls/toggle-control.tsx +3 -3
- package/src/components/ui/controls/tool-options-panel.tsx +1 -1
- package/src/components/ui/floating-level-selector.tsx +159 -59
- package/src/components/ui/helpers/building-helper.tsx +4 -1
- package/src/components/ui/helpers/contextual-helper-panel.tsx +181 -72
- package/src/components/ui/helpers/helper-manager.test.tsx +121 -0
- package/src/components/ui/helpers/helper-manager.tsx +331 -14
- package/src/components/ui/helpers/item-helper.tsx +17 -1
- 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 +28 -22
- 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 +4 -4
- package/src/components/ui/panels/mobile-selection-bar.tsx +8 -5
- 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 +161 -5
- 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 +130 -62
- package/src/components/ui/panels/parametric-field-control.tsx +3 -3
- package/src/components/ui/panels/parametric-inspector.tsx +7 -7
- 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 +790 -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/badge.tsx +8 -0
- package/src/components/ui/primitives/color-dot.tsx +36 -24
- package/src/components/ui/primitives/sidebar.tsx +3 -1
- package/src/components/ui/right-stack.test.ts +29 -0
- package/src/components/ui/right-stack.tsx +164 -0
- package/src/components/ui/sidebar/app-sidebar.tsx +1 -1
- package/src/components/ui/sidebar/icon-rail.tsx +8 -3
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +6 -6
- package/src/components/ui/sidebar/panels/settings-panel/animation-speed-setting.tsx +40 -0
- package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +7 -8
- package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.test.tsx +170 -0
- package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +92 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +138 -27
- package/src/components/ui/sidebar/panels/settings-panel/interface-theme.test.tsx +20 -0
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +4 -10
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -11
- 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 +181 -223
- 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 +13 -37
- 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 +2 -4
- package/src/components/ui/sidebar/tab-bar.tsx +14 -4
- 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-controls-bar.tsx +18 -6
- package/src/components/viewer/viewer-hover-label.tsx +34 -0
- package/src/components/viewer/viewer-rooms.tsx +191 -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 +215 -43
- package/src/hooks/use-auto-save.ts +60 -9
- package/src/hooks/use-ceiling-events.test.ts +123 -118
- package/src/hooks/use-grid-events.test.ts +105 -105
- package/src/hooks/use-keyboard.test.ts +243 -19
- package/src/hooks/use-keyboard.ts +118 -83
- 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 +71 -0
- package/src/hooks/use-placement-notice.ts +73 -0
- package/src/hooks/use-selected-room.ts +143 -0
- package/src/index.tsx +312 -58
- package/src/lib/__fixtures__/owner-victor-seattle-parking.json +1 -0
- package/src/lib/__fixtures__/owner-villa-aurelia-basement.json +1 -0
- package/src/lib/add-storey.test.ts +177 -0
- package/src/lib/add-storey.ts +59 -0
- package/src/lib/below-grade.test.tsx +171 -0
- package/src/lib/below-grade.ts +133 -0
- package/src/lib/camera-facing.test.ts +24 -0
- package/src/lib/camera-facing.ts +24 -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/continuation.test.ts +19 -1
- package/src/lib/continuation.ts +25 -4
- package/src/lib/direct-manipulation.ts +2 -0
- package/src/lib/door-interaction.ts +14 -0
- package/src/lib/download-project-file.ts +17 -0
- package/src/lib/editor-level-display.test.ts +96 -0
- package/src/lib/editor-level-display.ts +55 -0
- package/src/lib/editor-views.ts +178 -0
- package/src/lib/export-geometry-wait.test.ts +90 -0
- package/src/lib/export-texture-utils.ts +2 -4
- 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 +284 -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 +363 -27
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +177 -8
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +60 -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/pdfkit-standard-fonts.d.ts +5 -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 +124 -0
- package/src/lib/floorplan/sheet-export.ts +318 -0
- package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +768 -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 +230 -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 +157 -0
- package/src/lib/floorplan/site-plan/geometry.test.ts +269 -0
- package/src/lib/floorplan/site-plan/geometry.ts +142 -0
- package/src/lib/floorplan/site-plan/index.ts +55 -0
- package/src/lib/floorplan/site-plan/site-annotations.test.ts +483 -0
- package/src/lib/floorplan/site-plan/site-annotations.ts +1084 -0
- package/src/lib/floorplan/site-plan/site-parts.ts +287 -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 +70 -0
- package/src/lib/floorplan/types.ts +1 -27
- package/src/lib/follow-pascal-exterior.test-fixture.ts +17 -0
- package/src/lib/follow-pascal-exterior.test.ts +171 -0
- package/src/lib/follow-pascal-exterior.ts +39 -0
- package/src/lib/follow-pascal-presentation.test.ts +203 -0
- package/src/lib/follow-pascal-presentation.ts +129 -0
- package/src/lib/follow-pascal-sight.ts +84 -0
- package/src/lib/follow-pascal-targets.test.ts +406 -0
- package/src/lib/follow-pascal-targets.ts +198 -0
- package/src/lib/follow-pascal.test.ts +350 -0
- package/src/lib/follow-pascal.ts +284 -0
- package/src/lib/fresh-planar-placement.test.ts +148 -0
- package/src/lib/fresh-planar-placement.ts +114 -37
- package/src/lib/geometry-script/author.ts +157 -0
- package/src/lib/geometry-script/client.ts +159 -0
- package/src/lib/geometry-script/finish-scripts.ts +127 -0
- package/src/lib/geometry-script/protocol.ts +13 -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 +636 -126
- package/src/lib/glb-export.ts +241 -120
- package/src/lib/history.test.ts +18 -7
- 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 +22 -0
- package/src/lib/keyboard-pan.test.ts +129 -7
- package/src/lib/keyboard-pan.ts +161 -12
- package/src/lib/level-selection.test.ts +57 -0
- package/src/lib/level-selection.ts +16 -1
- 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 +187 -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 +392 -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/measurement-kind.test.ts +6 -8
- package/src/lib/measurements.test.ts +23 -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 +201 -0
- package/src/lib/orbit-floor-clearance.ts +154 -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 +122 -0
- package/src/lib/paint-pass-through.ts +78 -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 +267 -25
- package/src/lib/panel-rows.ts +21 -0
- package/src/lib/parametric-node-update.ts +16 -7
- 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 +65 -0
- package/src/lib/plugin-panels.test.ts +25 -0
- package/src/lib/plugin-panels.ts +94 -0
- package/src/lib/point-ask/__fixtures__/consumer-browser.tsx +76 -0
- package/src/lib/point-ask/__fixtures__/house.ts +81 -0
- package/src/lib/point-ask/ask-created.test.ts +195 -0
- package/src/lib/point-ask/ask-flow.test.ts +380 -0
- package/src/lib/point-ask/build-context.test.ts +173 -0
- package/src/lib/point-ask/build-context.ts +94 -0
- package/src/lib/point-ask/capture-crop.ts +129 -0
- package/src/lib/point-ask/choreography.ts +69 -0
- package/src/lib/point-ask/compare.test.ts +145 -0
- package/src/lib/point-ask/compare.ts +73 -0
- package/src/lib/point-ask/describe-target.test.ts +165 -0
- package/src/lib/point-ask/describe-target.ts +234 -0
- package/src/lib/point-ask/dictation.ts +79 -0
- package/src/lib/point-ask/edge-marker.test.ts +61 -0
- package/src/lib/point-ask/edge-marker.ts +27 -0
- package/src/lib/point-ask/frame-loop.ts +25 -0
- package/src/lib/point-ask/history-marks.test.ts +94 -0
- package/src/lib/point-ask/history-marks.ts +39 -0
- package/src/lib/point-ask/host-api.ts +32 -0
- package/src/lib/point-ask/outline-box.test.ts +30 -0
- package/src/lib/point-ask/outline-box.ts +11 -0
- package/src/lib/point-ask/overlay-metrics.ts +6 -0
- package/src/lib/point-ask/pick.test.ts +220 -0
- package/src/lib/point-ask/pick.ts +121 -0
- package/src/lib/point-ask/pin-machine.test.ts +494 -0
- package/src/lib/point-ask/pin-machine.ts +217 -0
- package/src/lib/point-ask/placement.test.ts +203 -0
- package/src/lib/point-ask/placement.ts +141 -0
- package/src/lib/point-ask/pointer-tracker.ts +34 -0
- package/src/lib/point-ask/projector.ts +83 -0
- package/src/lib/point-ask/region.test.ts +157 -0
- package/src/lib/point-ask/region.ts +136 -0
- package/src/lib/point-ask/retry.test.ts +217 -0
- package/src/lib/point-ask/retry.ts +37 -0
- package/src/lib/point-ask/scene-commit.test.ts +312 -0
- package/src/lib/point-ask/scene-commit.ts +32 -0
- package/src/lib/point-ask/session.ts +1391 -0
- package/src/lib/point-ask/spring-keyframes.test.ts +45 -0
- package/src/lib/point-ask/spring-keyframes.ts +38 -0
- package/src/lib/point-ask/spring.test.ts +63 -0
- package/src/lib/point-ask/spring.ts +107 -0
- package/src/lib/point-ask/suggestions.test.ts +80 -0
- package/src/lib/point-ask/suggestions.ts +125 -0
- package/src/lib/point-ask/target-scope.test.ts +622 -0
- package/src/lib/point-ask/target-scope.ts +338 -0
- package/src/lib/point-ask/types.ts +48 -0
- package/src/lib/portable-export.test.ts +34 -1
- package/src/lib/portable-export.ts +119 -53
- package/src/lib/print-feature-thickness.ts +10 -1
- package/src/lib/print-roof-solids.test.ts +39 -1
- package/src/lib/print-roof-solids.ts +43 -10
- package/src/lib/print-shell-compiler-manifold-core.ts +1 -1
- 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 +265 -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 +1017 -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 +610 -1
- package/src/lib/scene-clipboard.ts +269 -58
- 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 +54 -75
- package/src/lib/session-writes.test.ts +120 -0
- package/src/lib/session-writes.ts +205 -0
- package/src/lib/sfx-bus.ts +13 -0
- package/src/lib/sfx-player.test.ts +70 -9
- package/src/lib/sfx-player.ts +6 -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/stair-levels.test.ts +25 -0
- package/src/lib/stair-levels.ts +22 -3
- package/src/lib/terrace-draft.test.ts +145 -0
- package/src/lib/terrace-draft.ts +214 -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 +580 -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-placement-pointer.test.ts +42 -0
- package/src/lib/wall-placement-pointer.ts +57 -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-changes.ts +35 -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-audio.test.ts +146 -0
- package/src/store/use-audio.tsx +127 -3
- package/src/store/use-construction-reveal.test.ts +173 -0
- package/src/store/use-construction-reveal.ts +145 -0
- package/src/store/use-corner-snap-hint.test.ts +24 -0
- package/src/store/use-corner-snap-hint.ts +47 -0
- 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-follow-pascal.test.ts +62 -0
- package/src/store/use-follow-pascal.ts +53 -0
- package/src/store/use-point-ask.ts +247 -0
- package/src/store/use-quick-measurement-hud.ts +3 -3
- package/src/store/use-stair-build-preview.ts +33 -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 +201 -0
- package/src/store/view-layout.ts +199 -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/render-current-markup.tsx +16 -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/tools/stair/stair-click-guard.test.ts +0 -58
- package/src/components/tools/stair/stair-click-guard.ts +0 -72
- package/src/components/tools/stair/stair-defaults.ts +0 -18
- package/src/components/tools/stair/stair-tool.tsx +0 -634
- 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,1084 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The permit-set layers of the site plan (A1.0) beyond the lot, the yards
|
|
3
|
+
* and the house: the street each street edge fronts, the contour labels,
|
|
4
|
+
* the driveway and walks, the utility services a plugin located, the drainage
|
|
5
|
+
* arrows, and the finish-floor and spot elevations. Every function is pure
|
|
6
|
+
* and returns site-metre primitives (x east, y south) for
|
|
7
|
+
* `buildSitePlanDrawing`, which owns the order they stack in.
|
|
8
|
+
*/
|
|
9
|
+
import {
|
|
10
|
+
type AnyNode,
|
|
11
|
+
type AnyNodeId,
|
|
12
|
+
type BuildingNode,
|
|
13
|
+
type FloorplanGeometry,
|
|
14
|
+
heightAt,
|
|
15
|
+
type LevelNode,
|
|
16
|
+
outwardNormal,
|
|
17
|
+
type SceneSnapshot,
|
|
18
|
+
type SiteNode,
|
|
19
|
+
siteFrame,
|
|
20
|
+
type TerrainField,
|
|
21
|
+
terrainContours,
|
|
22
|
+
} from '@pascal-app/core'
|
|
23
|
+
import {
|
|
24
|
+
type SitePlanServicePoint,
|
|
25
|
+
type SitePlanServiceRole,
|
|
26
|
+
sitePlanServiceAnswers,
|
|
27
|
+
} from './contributors'
|
|
28
|
+
import {
|
|
29
|
+
formatFeetInches,
|
|
30
|
+
METRES_PER_FOOT,
|
|
31
|
+
type Pt,
|
|
32
|
+
pointInPolygon,
|
|
33
|
+
polygonArea,
|
|
34
|
+
polygonBounds,
|
|
35
|
+
polygonCentroid,
|
|
36
|
+
} from './geometry'
|
|
37
|
+
import { flatworkKindOf, type OutdoorPart } from './site-parts'
|
|
38
|
+
|
|
39
|
+
const INK = '#111827'
|
|
40
|
+
const INK_SOFT = '#4b5563'
|
|
41
|
+
const CONTOUR_INK = '#8b5a2b'
|
|
42
|
+
const HALO = '#ffffff'
|
|
43
|
+
const FT = METRES_PER_FOOT
|
|
44
|
+
|
|
45
|
+
/** An angle a label reads at: within a quarter turn of the paper's horizontal. */
|
|
46
|
+
function readable(a: number): number {
|
|
47
|
+
let r = a
|
|
48
|
+
while (r > Math.PI / 2) r -= Math.PI
|
|
49
|
+
while (r <= -Math.PI / 2) r += Math.PI
|
|
50
|
+
return r
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A label laid along a run, centred on `at`, knocked out of what it crosses. */
|
|
54
|
+
function runLabel(
|
|
55
|
+
at: Pt,
|
|
56
|
+
angle: number,
|
|
57
|
+
text: string,
|
|
58
|
+
fontSize: number,
|
|
59
|
+
fill: string,
|
|
60
|
+
metadata: Record<string, unknown>,
|
|
61
|
+
weight = 600,
|
|
62
|
+
): FloorplanGeometry {
|
|
63
|
+
return {
|
|
64
|
+
kind: 'group',
|
|
65
|
+
transform: { translate: at, rotate: readable(angle) },
|
|
66
|
+
children: [
|
|
67
|
+
{
|
|
68
|
+
kind: 'text',
|
|
69
|
+
x: 0,
|
|
70
|
+
y: 0,
|
|
71
|
+
text,
|
|
72
|
+
fontSize,
|
|
73
|
+
fill,
|
|
74
|
+
fontWeight: weight,
|
|
75
|
+
textAnchor: 'middle',
|
|
76
|
+
dominantBaseline: 'middle',
|
|
77
|
+
stroke: HALO,
|
|
78
|
+
strokeWidth: fontSize * 0.28,
|
|
79
|
+
paintOrder: 'stroke',
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
metadata,
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const sub = (a: Pt, b: Pt): Pt => [a[0] - b[0], a[1] - b[1]]
|
|
87
|
+
const add = (a: Pt, b: Pt): Pt => [a[0] + b[0], a[1] + b[1]]
|
|
88
|
+
const mul = (a: Pt, k: number): Pt => [a[0] * k, a[1] * k]
|
|
89
|
+
const len = (a: Pt) => Math.hypot(a[0], a[1])
|
|
90
|
+
const unit = (a: Pt): Pt => {
|
|
91
|
+
const l = len(a) || 1
|
|
92
|
+
return [a[0] / l, a[1] / l]
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The nearest point to `p` on segment ab. */
|
|
96
|
+
function footOn(p: Pt, a: Pt, b: Pt): Pt {
|
|
97
|
+
const ab = sub(b, a)
|
|
98
|
+
const l2 = ab[0] * ab[0] + ab[1] * ab[1]
|
|
99
|
+
const t =
|
|
100
|
+
l2 < 1e-12 ? 0 : Math.max(0, Math.min(1, ((p[0] - a[0]) * ab[0] + (p[1] - a[1]) * ab[1]) / l2))
|
|
101
|
+
return [a[0] + ab[0] * t, a[1] + ab[1] * t]
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Proper crossing of segments pq and ab. */
|
|
105
|
+
function crosses(p: Pt, q: Pt, a: Pt, b: Pt): boolean {
|
|
106
|
+
const d = (u: Pt, v: Pt, w: Pt) => (v[0] - u[0]) * (w[1] - u[1]) - (v[1] - u[1]) * (w[0] - u[0])
|
|
107
|
+
const d1 = d(a, b, p)
|
|
108
|
+
const d2 = d(a, b, q)
|
|
109
|
+
const d3 = d(p, q, a)
|
|
110
|
+
const d4 = d(p, q, b)
|
|
111
|
+
return d1 * d2 < 0 && d3 * d4 < 0
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function crossesRings(p: Pt, q: Pt, rings: readonly Pt[][]): boolean {
|
|
115
|
+
for (const ring of rings) {
|
|
116
|
+
for (let i = 0; i < ring.length; i++) {
|
|
117
|
+
if (crosses(p, q, ring[i] as Pt, ring[(i + 1) % ring.length] as Pt)) return true
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return false
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* ============================================================ streets */
|
|
124
|
+
|
|
125
|
+
const DIRECTIONS: Record<string, string> = {
|
|
126
|
+
north: 'N',
|
|
127
|
+
south: 'S',
|
|
128
|
+
east: 'E',
|
|
129
|
+
west: 'W',
|
|
130
|
+
northeast: 'NE',
|
|
131
|
+
northwest: 'NW',
|
|
132
|
+
southeast: 'SE',
|
|
133
|
+
southwest: 'SW',
|
|
134
|
+
n: 'N',
|
|
135
|
+
s: 'S',
|
|
136
|
+
e: 'E',
|
|
137
|
+
w: 'W',
|
|
138
|
+
ne: 'NE',
|
|
139
|
+
nw: 'NW',
|
|
140
|
+
se: 'SE',
|
|
141
|
+
sw: 'SW',
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const SUFFIXES: Record<string, string> = {
|
|
145
|
+
st: 'STREET',
|
|
146
|
+
street: 'STREET',
|
|
147
|
+
ave: 'AVENUE',
|
|
148
|
+
av: 'AVENUE',
|
|
149
|
+
avenue: 'AVENUE',
|
|
150
|
+
rd: 'ROAD',
|
|
151
|
+
road: 'ROAD',
|
|
152
|
+
dr: 'DRIVE',
|
|
153
|
+
drive: 'DRIVE',
|
|
154
|
+
ln: 'LANE',
|
|
155
|
+
lane: 'LANE',
|
|
156
|
+
ct: 'COURT',
|
|
157
|
+
court: 'COURT',
|
|
158
|
+
pl: 'PLACE',
|
|
159
|
+
place: 'PLACE',
|
|
160
|
+
blvd: 'BOULEVARD',
|
|
161
|
+
boulevard: 'BOULEVARD',
|
|
162
|
+
cir: 'CIRCLE',
|
|
163
|
+
circle: 'CIRCLE',
|
|
164
|
+
ter: 'TERRACE',
|
|
165
|
+
terr: 'TERRACE',
|
|
166
|
+
terrace: 'TERRACE',
|
|
167
|
+
trl: 'TRAIL',
|
|
168
|
+
trail: 'TRAIL',
|
|
169
|
+
pkwy: 'PARKWAY',
|
|
170
|
+
parkway: 'PARKWAY',
|
|
171
|
+
hwy: 'HIGHWAY',
|
|
172
|
+
highway: 'HIGHWAY',
|
|
173
|
+
way: 'WAY',
|
|
174
|
+
loop: 'LOOP',
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* A street name as a site plan prints it: the house number dropped, the
|
|
179
|
+
* quadrant abbreviated, the street type spelled out — "4121 NW 34th St" and
|
|
180
|
+
* OSM's "Northwest 34th Street" both read "NW 34TH STREET", and "Northwest
|
|
181
|
+
* 34th Terrace" reads "NW 34TH TERRACE" (a different street: the type is part
|
|
182
|
+
* of the name). Empty for an empty name.
|
|
183
|
+
*/
|
|
184
|
+
export function formatStreetName(raw: string | null | undefined): string {
|
|
185
|
+
const words = String(raw ?? '')
|
|
186
|
+
.replace(/^\s*\d+[A-Za-z]?(?:[-–]\d+)?\s+/, '')
|
|
187
|
+
.replace(/\./g, '')
|
|
188
|
+
.split(/\s+/)
|
|
189
|
+
.filter(Boolean)
|
|
190
|
+
if (words.length === 0) return ''
|
|
191
|
+
const out = words.map((w) => w.toUpperCase())
|
|
192
|
+
const first = DIRECTIONS[(words[0] as string).toLowerCase()]
|
|
193
|
+
if (first && words.length > 1) out[0] = first
|
|
194
|
+
const lastIndex = words.length - 1
|
|
195
|
+
const last = (words[lastIndex] as string).toLowerCase()
|
|
196
|
+
const trailingDir = DIRECTIONS[last]
|
|
197
|
+
if (trailingDir && lastIndex > 0) {
|
|
198
|
+
out[lastIndex] = trailingDir
|
|
199
|
+
const type = SUFFIXES[(words[lastIndex - 1] as string).toLowerCase()]
|
|
200
|
+
if (type && lastIndex - 1 > 0) out[lastIndex - 1] = type
|
|
201
|
+
} else {
|
|
202
|
+
const type = SUFFIXES[last]
|
|
203
|
+
if (type && lastIndex > 0) out[lastIndex] = type
|
|
204
|
+
}
|
|
205
|
+
return out.join(' ')
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** The front edge's street as the lot drop-in's note recorded it (scenes dropped before per-edge names were kept). */
|
|
209
|
+
function frontStreetFromNotes(
|
|
210
|
+
notes: readonly string[] | undefined,
|
|
211
|
+
frontEdge: number,
|
|
212
|
+
): string | null {
|
|
213
|
+
for (const note of notes ?? []) {
|
|
214
|
+
const osm = /^Front edge (\d+): fronts "([^"]+)"/.exec(note)
|
|
215
|
+
if (osm && Number(osm[1]) - 1 === frontEdge) return osm[2] as string
|
|
216
|
+
const fabric =
|
|
217
|
+
/^Front edge: edge (\d+) of the parcel fabric's frontage \([^,)]*,\s*([^)]+)\)/.exec(note)
|
|
218
|
+
if (fabric && Number(fabric[1]) - 1 === frontEdge) return (fabric[2] as string).trim()
|
|
219
|
+
}
|
|
220
|
+
return null
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Printed on a street edge whose street the lot drop-in could not name. */
|
|
224
|
+
export const UNNAMED_STREET = 'STREET (NAME NOT ON RECORD — VERIFY)'
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* The street each street edge of the lot runs along, as printed: only the
|
|
228
|
+
* edges that front a street — `site.streetEdges` and the front edge — and
|
|
229
|
+
* each with ITS street: the name the lot drop-in stored for that edge
|
|
230
|
+
* (`site.metadata.streetNames`, from the mapped roads), else — for a lot dropped
|
|
231
|
+
* before names were kept per edge — the front edge's street from the
|
|
232
|
+
* drop-in's note, the address's street on the one other street edge when
|
|
233
|
+
* the front is a different street (a through or corner lot is addressed on
|
|
234
|
+
* one of its streets), and "name not on record" where neither says.
|
|
235
|
+
*/
|
|
236
|
+
export function streetEdgeNames(
|
|
237
|
+
site: Pick<SiteNode, 'address' | 'parcel' | 'streetEdges'> & { metadata?: unknown },
|
|
238
|
+
frontEdge: number,
|
|
239
|
+
): Map<number, string> {
|
|
240
|
+
const edges = [...new Set([frontEdge, ...(site.streetEdges ?? [])])].filter(
|
|
241
|
+
(i) => Number.isInteger(i) && i >= 0,
|
|
242
|
+
)
|
|
243
|
+
const stored = ((site.metadata as { streetNames?: unknown } | undefined)?.streetNames ??
|
|
244
|
+
{}) as Record<string, string>
|
|
245
|
+
const address = formatStreetName(site.address?.street)
|
|
246
|
+
const names = new Map<number, string>()
|
|
247
|
+
for (const i of edges) {
|
|
248
|
+
const own = formatStreetName(stored[String(i)])
|
|
249
|
+
if (own) names.set(i, own)
|
|
250
|
+
}
|
|
251
|
+
if (!names.has(frontEdge)) {
|
|
252
|
+
const noted = formatStreetName(frontStreetFromNotes(site.parcel?.notes, frontEdge))
|
|
253
|
+
const front = noted || address
|
|
254
|
+
if (front) names.set(frontEdge, front)
|
|
255
|
+
}
|
|
256
|
+
const unnamed = edges.filter((i) => !names.has(i))
|
|
257
|
+
const taken = new Set(names.values())
|
|
258
|
+
if (address && unnamed.length === 1 && !taken.has(address))
|
|
259
|
+
names.set(unnamed[0] as number, address)
|
|
260
|
+
for (const i of edges) if (!names.has(i)) names.set(i, UNNAMED_STREET)
|
|
261
|
+
return names
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/* =========================================================== contours */
|
|
265
|
+
|
|
266
|
+
type ContourLine = { levelM: number; absFt: number | null; points: Pt[]; index: boolean }
|
|
267
|
+
|
|
268
|
+
function polylineLength(points: readonly Pt[]): number {
|
|
269
|
+
let s = 0
|
|
270
|
+
for (let i = 1; i < points.length; i++) s += len(sub(points[i] as Pt, points[i - 1] as Pt))
|
|
271
|
+
return s
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** The point `t` (0–1) of the way along a polyline, with the run's direction there. */
|
|
275
|
+
function alongPolyline(points: readonly Pt[], t: number): { at: Pt; angle: number } {
|
|
276
|
+
const total = polylineLength(points)
|
|
277
|
+
let want = total * t
|
|
278
|
+
for (let i = 1; i < points.length; i++) {
|
|
279
|
+
const a = points[i - 1] as Pt
|
|
280
|
+
const b = points[i] as Pt
|
|
281
|
+
const l = len(sub(b, a))
|
|
282
|
+
if (want <= l || i === points.length - 1) {
|
|
283
|
+
const k = l > 0 ? Math.min(1, want / l) : 0
|
|
284
|
+
return {
|
|
285
|
+
at: [a[0] + (b[0] - a[0]) * k, a[1] + (b[1] - a[1]) * k],
|
|
286
|
+
angle: Math.atan2(b[1] - a[1], b[0] - a[0]),
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
want -= l
|
|
290
|
+
}
|
|
291
|
+
const p = points[0] as Pt
|
|
292
|
+
return { at: p, angle: 0 }
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* The contour lines and their labels. With a survey datum (the terrain
|
|
297
|
+
* sample's USGS elevation, or the dossier's 3DEP lines) the lines fall on
|
|
298
|
+
* whole multiples of the interval in ABSOLUTE feet — 173, 174, 175 — not on
|
|
299
|
+
* the site datum's fraction, and each is labelled with its elevation; every
|
|
300
|
+
* fifth is an index contour (heavier). Each elevation is labelled once, or
|
|
301
|
+
* twice on a long line, on its longest pieces — never on every loop.
|
|
302
|
+
*/
|
|
303
|
+
export function contourPrimitives(args: {
|
|
304
|
+
site: SiteNode
|
|
305
|
+
lot: readonly Pt[]
|
|
306
|
+
field: TerrainField
|
|
307
|
+
intervalIn: number
|
|
308
|
+
datumFt: number | null
|
|
309
|
+
fontSize: number
|
|
310
|
+
/** Rings no label is set inside (the house, porches, paving). */
|
|
311
|
+
avoid?: readonly Pt[][]
|
|
312
|
+
}): FloorplanGeometry[] {
|
|
313
|
+
const { site, lot, field, intervalIn, datumFt, fontSize } = args
|
|
314
|
+
const avoid = args.avoid ?? []
|
|
315
|
+
const clear = (p: Pt) => !avoid.some((ring) => pointInPolygon(ring, p[0], p[1]))
|
|
316
|
+
const stepFt = intervalIn / 12
|
|
317
|
+
const isIndex = (ft: number) => Math.abs(ft / (stepFt * 5) - Math.round(ft / (stepFt * 5))) < 1e-6
|
|
318
|
+
let lines: ContourLine[]
|
|
319
|
+
const surveyed = site.terrainContours
|
|
320
|
+
const useSurveyed =
|
|
321
|
+
surveyed !== undefined &&
|
|
322
|
+
surveyed.lines.length > 0 &&
|
|
323
|
+
Math.abs(stepFt / surveyed.intervalFt - Math.round(stepFt / surveyed.intervalFt)) < 1e-9
|
|
324
|
+
if (useSurveyed) {
|
|
325
|
+
lines = surveyed.lines
|
|
326
|
+
.filter((l) => Math.abs(l.elevationFt / stepFt - Math.round(l.elevationFt / stepFt)) < 1e-9)
|
|
327
|
+
.map((l) => ({
|
|
328
|
+
levelM: (l.elevationFt - (datumFt ?? 0)) * FT,
|
|
329
|
+
absFt: datumFt !== null ? l.elevationFt : null,
|
|
330
|
+
points: l.points.filter((p) => lot.length < 3 || pointInPolygon(lot, p[0], p[1])) as Pt[],
|
|
331
|
+
index: isIndex(l.elevationFt),
|
|
332
|
+
}))
|
|
333
|
+
.filter((c) => c.points.length >= 2)
|
|
334
|
+
} else if (datumFt !== null) {
|
|
335
|
+
// shift the field by the datum's fraction of an interval so the lines
|
|
336
|
+
// land on whole absolute elevations
|
|
337
|
+
const frac = ((datumFt % stepFt) + stepFt) % stepFt
|
|
338
|
+
const shift = Math.round((frac * FT) / field.step)
|
|
339
|
+
const shifted: TerrainField =
|
|
340
|
+
shift === 0 ? field : { ...field, heights: field.heights.map((h) => h + shift) }
|
|
341
|
+
lines = terrainContours(shifted, stepFt * FT, lot).map((c) => {
|
|
342
|
+
const rel = c.levelM - shift * field.step
|
|
343
|
+
const absFt = Math.round((datumFt + rel / FT) / stepFt) * stepFt
|
|
344
|
+
return { levelM: rel, absFt, points: c.points, index: isIndex(absFt) }
|
|
345
|
+
})
|
|
346
|
+
} else {
|
|
347
|
+
lines = terrainContours(field, stepFt * FT, lot).map((c) => ({
|
|
348
|
+
levelM: c.levelM,
|
|
349
|
+
absFt: null,
|
|
350
|
+
points: c.points,
|
|
351
|
+
index: c.index,
|
|
352
|
+
}))
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const out: FloorplanGeometry[] = []
|
|
356
|
+
for (const c of lines) {
|
|
357
|
+
out.push({
|
|
358
|
+
kind: 'polyline',
|
|
359
|
+
points: c.points,
|
|
360
|
+
stroke: CONTOUR_INK,
|
|
361
|
+
strokeWidth: c.index ? 0.045 : 0.02,
|
|
362
|
+
opacity: c.index ? 0.85 : 0.6,
|
|
363
|
+
fill: 'none',
|
|
364
|
+
metadata: { sitePlan: 'contour', levelM: c.levelM },
|
|
365
|
+
})
|
|
366
|
+
}
|
|
367
|
+
// labels: per elevation, on its longest pieces — two on a long line, one otherwise
|
|
368
|
+
const byLevel = new Map<string, ContourLine[]>()
|
|
369
|
+
for (const c of lines) {
|
|
370
|
+
const key = c.levelM.toFixed(4)
|
|
371
|
+
const list = byLevel.get(key)
|
|
372
|
+
if (list) list.push(c)
|
|
373
|
+
else byLevel.set(key, [c])
|
|
374
|
+
}
|
|
375
|
+
for (const pieces of byLevel.values()) {
|
|
376
|
+
const ranked = pieces
|
|
377
|
+
.map((c) => ({ c, length: polylineLength(c.points) }))
|
|
378
|
+
.filter((p) => p.length >= fontSize * 6)
|
|
379
|
+
.sort((a, b) => b.length - a.length)
|
|
380
|
+
const first = ranked[0]
|
|
381
|
+
if (!first) continue
|
|
382
|
+
const text =
|
|
383
|
+
first.c.absFt !== null
|
|
384
|
+
? first.c.absFt.toFixed(stepFt < 1 ? 1 : 0)
|
|
385
|
+
: `${first.c.levelM >= 0 ? '+' : ''}${(first.c.levelM / FT).toFixed(1)}'`
|
|
386
|
+
// two labels on a long line, one otherwise; each at the first clear
|
|
387
|
+
// station near its mark (never on the house, a porch or the paving),
|
|
388
|
+
// the two kept apart
|
|
389
|
+
const wanted = first.length >= fontSize * 40 ? [0.28, 0.72] : [0.5]
|
|
390
|
+
const placed: Pt[] = []
|
|
391
|
+
for (const [k, t] of wanted.entries()) {
|
|
392
|
+
const candidates = [t, t - 0.1, t + 0.1, t - 0.2, t + 0.2, 0.5, 0.35, 0.65, 0.15, 0.85]
|
|
393
|
+
const pieceList = k === 1 || ranked.length === 1 ? [first] : ranked.slice(0, 3)
|
|
394
|
+
let done = false
|
|
395
|
+
for (const piece of pieceList) {
|
|
396
|
+
for (const u of candidates) {
|
|
397
|
+
if (u <= 0.02 || u >= 0.98) continue
|
|
398
|
+
const { at, angle } = alongPolyline(piece.c.points, u)
|
|
399
|
+
if (!clear(at) || placed.some((p) => len(sub(p, at)) < fontSize * 12)) continue
|
|
400
|
+
out.push(
|
|
401
|
+
runLabel(at, angle, text, fontSize, CONTOUR_INK, { sitePlan: 'contour-label' }, 500),
|
|
402
|
+
)
|
|
403
|
+
placed.push(at)
|
|
404
|
+
done = true
|
|
405
|
+
break
|
|
406
|
+
}
|
|
407
|
+
if (done) break
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return out
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/* ========================================================== flatwork */
|
|
415
|
+
|
|
416
|
+
type FlatworkDims = {
|
|
417
|
+
/** Level-local points across the driveway's throat at the garage door, and across its mouth at the street. */
|
|
418
|
+
throat?: [number, number][]
|
|
419
|
+
mouth?: [number, number][]
|
|
420
|
+
widthM?: number
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* The driveway and walks: a concrete tone under a fine edge, the driveway
|
|
425
|
+
* dimensioned across its throat (the garage door) and its flared mouth at
|
|
426
|
+
* the property line, the walk's width printed along it.
|
|
427
|
+
*/
|
|
428
|
+
export function flatworkPrimitives(
|
|
429
|
+
scene: SceneSnapshot,
|
|
430
|
+
building: BuildingNode | null,
|
|
431
|
+
parts: readonly OutdoorPart[],
|
|
432
|
+
fontSize: number,
|
|
433
|
+
): FloorplanGeometry[] {
|
|
434
|
+
const toSite = siteFrame(building)
|
|
435
|
+
const out: FloorplanGeometry[] = []
|
|
436
|
+
for (const part of parts) {
|
|
437
|
+
if (part.kind !== 'driveway' && part.kind !== 'walk') continue
|
|
438
|
+
out.push({
|
|
439
|
+
kind: 'polygon',
|
|
440
|
+
points: part.ring,
|
|
441
|
+
fill: '#e5e7eb',
|
|
442
|
+
fillOpacity: 0.95,
|
|
443
|
+
stroke: INK,
|
|
444
|
+
strokeWidth: 0.03,
|
|
445
|
+
metadata: { sitePlan: part.kind },
|
|
446
|
+
})
|
|
447
|
+
out.push({ kind: 'hatch', points: part.ring, color: '#9ca3af', opacity: 0.35 })
|
|
448
|
+
const node = scene.nodes[part.id as AnyNodeId] as
|
|
449
|
+
| (AnyNode & { metadata?: Record<string, unknown> })
|
|
450
|
+
| undefined
|
|
451
|
+
const dims = (node?.metadata?.flatworkDims ?? {}) as FlatworkDims
|
|
452
|
+
const c = polygonCentroid(part.ring)
|
|
453
|
+
if (part.kind === 'driveway') {
|
|
454
|
+
const lines = ['CONC. DRIVEWAY']
|
|
455
|
+
out.push(
|
|
456
|
+
runLabel(c, 0, lines[0] as string, fontSize, INK, { sitePlan: 'driveway-label' }, 700),
|
|
457
|
+
)
|
|
458
|
+
for (const [key, across] of [
|
|
459
|
+
['throat', dims.throat],
|
|
460
|
+
['mouth', dims.mouth],
|
|
461
|
+
] as const) {
|
|
462
|
+
if (across?.length !== 2) continue
|
|
463
|
+
const a = toSite(across[0]![0], across[0]![1])
|
|
464
|
+
const b = toSite(across[1]![0], across[1]![1])
|
|
465
|
+
const d = unit(sub(b, a))
|
|
466
|
+
// the dimension line sits just inside the driveway, the text toward its middle
|
|
467
|
+
const inward = key === 'throat' ? unit(sub(c, a)) : unit(sub(c, a))
|
|
468
|
+
const n: Pt = [-d[1], d[0]]
|
|
469
|
+
const sign = n[0] * inward[0] + n[1] * inward[1] >= 0 ? 1 : -1
|
|
470
|
+
out.push({
|
|
471
|
+
kind: 'dimension',
|
|
472
|
+
start: a,
|
|
473
|
+
end: b,
|
|
474
|
+
offsetNormal: [n[0] * sign, n[1] * sign],
|
|
475
|
+
offsetDistance: 0.6,
|
|
476
|
+
extensionOvershoot: 0.25,
|
|
477
|
+
stroke: '#334155',
|
|
478
|
+
terminator: 'architectural-tick',
|
|
479
|
+
text: formatFeetInches(len(sub(b, a))),
|
|
480
|
+
metadata: { sitePlan: `driveway-${key}` },
|
|
481
|
+
} as FloorplanGeometry)
|
|
482
|
+
}
|
|
483
|
+
} else {
|
|
484
|
+
// the walk's width along its longest leg
|
|
485
|
+
let best: { at: Pt; angle: number; l: number } | null = null
|
|
486
|
+
const ring = part.ring
|
|
487
|
+
for (let i = 0; i < ring.length; i++) {
|
|
488
|
+
const a = ring[i] as Pt
|
|
489
|
+
const b = ring[(i + 1) % ring.length] as Pt
|
|
490
|
+
const l = len(sub(b, a))
|
|
491
|
+
if (!best || l > best.l)
|
|
492
|
+
best = {
|
|
493
|
+
at: [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2],
|
|
494
|
+
angle: Math.atan2(b[1] - a[1], b[0] - a[0]),
|
|
495
|
+
l,
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
const width = typeof dims.widthM === 'number' ? dims.widthM : null
|
|
499
|
+
const text = width ? `${formatFeetInches(width)} CONC. WALK` : 'CONC. WALK'
|
|
500
|
+
if (best) {
|
|
501
|
+
// on the walk's centre line, along its longest leg
|
|
502
|
+
const inward = unit(sub(c, best.at))
|
|
503
|
+
const d = unit([Math.cos(best.angle), Math.sin(best.angle)])
|
|
504
|
+
const n: Pt = [-d[1], d[0]]
|
|
505
|
+
const sign = n[0] * inward[0] + n[1] * inward[1] >= 0 ? 1 : -1
|
|
506
|
+
const half = (width ?? 1.2) / 2
|
|
507
|
+
out.push(
|
|
508
|
+
runLabel(
|
|
509
|
+
add(best.at, mul(n, sign * half)),
|
|
510
|
+
best.angle,
|
|
511
|
+
text,
|
|
512
|
+
fontSize * 0.8,
|
|
513
|
+
INK,
|
|
514
|
+
{ sitePlan: 'walk-label' },
|
|
515
|
+
600,
|
|
516
|
+
),
|
|
517
|
+
)
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return out
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/* ========================================================= utilities */
|
|
525
|
+
|
|
526
|
+
export type ServiceRole = SitePlanServiceRole
|
|
527
|
+
|
|
528
|
+
type ServicePoint = { role: SitePlanServicePoint['role']; at: Pt; normal: Pt | null }
|
|
529
|
+
|
|
530
|
+
/** The registered service points of the building's ground storey, in site metres. */
|
|
531
|
+
export function servicePoints(
|
|
532
|
+
scene: SceneSnapshot,
|
|
533
|
+
level: LevelNode | null,
|
|
534
|
+
building: BuildingNode | null,
|
|
535
|
+
outline: readonly Pt[][],
|
|
536
|
+
): ServicePoint[] {
|
|
537
|
+
if (!level) return []
|
|
538
|
+
const toSite = siteFrame(building)
|
|
539
|
+
const out: ServicePoint[] = []
|
|
540
|
+
const byRole = new Map<ServicePoint['role'], SitePlanServicePoint>()
|
|
541
|
+
for (const points of sitePlanServiceAnswers((services) => services.points?.(scene, level.id))) {
|
|
542
|
+
for (const point of points) if (!byRole.has(point.role)) byRole.set(point.role, point)
|
|
543
|
+
}
|
|
544
|
+
const place = (node: SitePlanServicePoint): { at: Pt; normal: Pt | null } | null => {
|
|
545
|
+
const pos = node.position
|
|
546
|
+
const placed = Math.abs(pos[0] ?? 0) > 1e-6 || Math.abs(pos[2] ?? 0) > 1e-6
|
|
547
|
+
if (!placed && typeof node.wallId === 'string') {
|
|
548
|
+
const wall = scene.nodes[node.wallId] as
|
|
549
|
+
| { start?: number[]; end?: number[]; thickness?: number }
|
|
550
|
+
| undefined
|
|
551
|
+
if (!wall?.start || !wall.end) return null
|
|
552
|
+
const t = typeof node.wallT === 'number' ? node.wallT : 0.5
|
|
553
|
+
const p = toSite(
|
|
554
|
+
(wall.start[0] ?? 0) + ((wall.end[0] ?? 0) - (wall.start[0] ?? 0)) * t,
|
|
555
|
+
(wall.start[1] ?? 0) + ((wall.end[1] ?? 0) - (wall.start[1] ?? 0)) * t,
|
|
556
|
+
)
|
|
557
|
+
const a = toSite(wall.start[0] ?? 0, wall.start[1] ?? 0)
|
|
558
|
+
const b = toSite(wall.end[0] ?? 0, wall.end[1] ?? 0)
|
|
559
|
+
const d = unit(sub(b, a))
|
|
560
|
+
let n: Pt = [-d[1], d[0]]
|
|
561
|
+
const probe = add(p, mul(n, 0.4))
|
|
562
|
+
if (outline.some((ring) => pointInPolygon(ring, probe[0], probe[1]))) n = [-n[0], -n[1]]
|
|
563
|
+
return { at: add(p, mul(n, (wall.thickness ?? 0.15) / 2)), normal: n }
|
|
564
|
+
}
|
|
565
|
+
// a floor-placed point on (or just off) a wall — the sewer exit — leaves
|
|
566
|
+
// square from the nearest face; one standing clear of the house has no face
|
|
567
|
+
const at = toSite(pos[0] ?? 0, pos[2] ?? 0)
|
|
568
|
+
let near: { d: number; foot: Pt; n: Pt } | null = null
|
|
569
|
+
for (const ring of outline) {
|
|
570
|
+
for (let i = 0; i < ring.length; i++) {
|
|
571
|
+
const foot = footOn(at, ring[i] as Pt, ring[(i + 1) % ring.length] as Pt)
|
|
572
|
+
const d = len(sub(at, foot))
|
|
573
|
+
if (!near || d < near.d) near = { d, foot, n: outwardNormal(ring, i) }
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
return near && near.d <= 0.6
|
|
577
|
+
? { at: add(near.foot, mul(near.n, 0.02)), normal: near.n }
|
|
578
|
+
: { at, normal: null }
|
|
579
|
+
}
|
|
580
|
+
for (const role of ['electric', 'water', 'sewer', 'ac', 'pole'] as const) {
|
|
581
|
+
const node = byRole.get(role)
|
|
582
|
+
if (!node) continue
|
|
583
|
+
const spot = place(node)
|
|
584
|
+
if (spot) out.push({ role, ...spot })
|
|
585
|
+
}
|
|
586
|
+
return out
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/** The storey's electric service entrance: a registered plugin's choice, else the building's, else overhead. */
|
|
590
|
+
export function serviceEntranceOf(
|
|
591
|
+
scene: SceneSnapshot,
|
|
592
|
+
level: LevelNode | null,
|
|
593
|
+
building: BuildingNode | null,
|
|
594
|
+
): {
|
|
595
|
+
kind: 'overhead' | 'underground'
|
|
596
|
+
source: 'plugin' | 'building' | 'default'
|
|
597
|
+
} {
|
|
598
|
+
if (level) {
|
|
599
|
+
const [kind] = sitePlanServiceAnswers((services) => services.entrance?.(scene, level.id))
|
|
600
|
+
if (kind === 'overhead' || kind === 'underground') return { kind, source: 'plugin' }
|
|
601
|
+
}
|
|
602
|
+
const services = (building?.metadata as { services?: { serviceEntrance?: unknown } } | undefined)
|
|
603
|
+
?.services
|
|
604
|
+
if (services?.serviceEntrance === 'overhead' || services?.serviceEntrance === 'underground')
|
|
605
|
+
return { kind: services.serviceEntrance, source: 'building' }
|
|
606
|
+
return { kind: 'overhead', source: 'default' }
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const SERVICE_STYLE: Record<ServiceRole, { color: string; dash: string }> = {
|
|
610
|
+
electric: { color: '#dc2626', dash: '0.6 0.25' },
|
|
611
|
+
water: { color: '#2563eb', dash: '0.45 0.2' },
|
|
612
|
+
sewer: { color: '#65a30d', dash: '0.9 0.25' },
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* A schematic route from a service point to the street: out square from
|
|
617
|
+
* the wall, then to the nearest point of a street edge — around the house
|
|
618
|
+
* (a dogleg past its end) when the straight run would cross it.
|
|
619
|
+
*/
|
|
620
|
+
export function serviceRoute(
|
|
621
|
+
from: Pt,
|
|
622
|
+
normal: Pt | null,
|
|
623
|
+
lot: readonly Pt[],
|
|
624
|
+
streetEdges: readonly number[],
|
|
625
|
+
outline: readonly Pt[][],
|
|
626
|
+
): Pt[] | null {
|
|
627
|
+
const start = normal ? add(from, mul(normal, 0.9)) : from
|
|
628
|
+
const candidates: Pt[][] = []
|
|
629
|
+
const hull = outline.flat()
|
|
630
|
+
const b = hull.length > 0 ? polygonBounds(hull) : null
|
|
631
|
+
for (const i of streetEdges) {
|
|
632
|
+
const a = lot[i]
|
|
633
|
+
const c = lot[(i + 1) % lot.length]
|
|
634
|
+
if (!a || !c) continue
|
|
635
|
+
const direct = footOn(start, a, c)
|
|
636
|
+
if (!crossesRings(start, direct, outline)) {
|
|
637
|
+
candidates.push(normal ? [from, start, direct] : [from, direct])
|
|
638
|
+
continue
|
|
639
|
+
}
|
|
640
|
+
// a dogleg past either end of the house, along the wall's run
|
|
641
|
+
if (!b) continue
|
|
642
|
+
const run: Pt = normal ? [-normal[1], normal[0]] : unit(sub(c, a))
|
|
643
|
+
for (const s of [1, -1]) {
|
|
644
|
+
const r = mul(run, s)
|
|
645
|
+
// how far along `r` the house reaches past the start, plus a yard
|
|
646
|
+
let reach = 0
|
|
647
|
+
for (const p of hull)
|
|
648
|
+
reach = Math.max(reach, (p[0] - start[0]) * r[0] + (p[1] - start[1]) * r[1])
|
|
649
|
+
const corner = add(start, mul(r, reach + 1.2))
|
|
650
|
+
const end = footOn(corner, a, c)
|
|
651
|
+
if (crossesRings(start, corner, outline) || crossesRings(corner, end, outline)) continue
|
|
652
|
+
candidates.push(normal ? [from, start, corner, end] : [from, corner, end])
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
if (candidates.length === 0) return null
|
|
656
|
+
candidates.sort((p, q) => polylineLength(p) - polylineLength(q))
|
|
657
|
+
return candidates[0] as Pt[]
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* The utility services a plugin located, drawn schematically to the street:
|
|
662
|
+
* the electric service from the meter (to the utility pole overhead, or
|
|
663
|
+
* underground to the street), the water service from its entry to a meter
|
|
664
|
+
* box at the property line, the sewer lateral from its exit to a cleanout at
|
|
665
|
+
* the property line, and the A/C condenser on its pad. Sizes are the common
|
|
666
|
+
* residential ones and say "verify"; the utility sets the real ones.
|
|
667
|
+
*/
|
|
668
|
+
export function servicePrimitives(args: {
|
|
669
|
+
scene: SceneSnapshot
|
|
670
|
+
level: LevelNode | null
|
|
671
|
+
building: BuildingNode | null
|
|
672
|
+
lot: readonly Pt[]
|
|
673
|
+
streetEdges: readonly number[]
|
|
674
|
+
outline: readonly Pt[][]
|
|
675
|
+
fontSize: number
|
|
676
|
+
}): {
|
|
677
|
+
primitives: FloorplanGeometry[]
|
|
678
|
+
drawn: ServiceRole[]
|
|
679
|
+
entrance: 'overhead' | 'underground'
|
|
680
|
+
/** The condenser pad's ring, for the marks that should keep off it. */
|
|
681
|
+
pad: Pt[] | null
|
|
682
|
+
} {
|
|
683
|
+
const { scene, level, building, lot, streetEdges, outline, fontSize } = args
|
|
684
|
+
const points = servicePoints(scene, level, building, outline)
|
|
685
|
+
const entrance = serviceEntranceOf(scene, level, building)
|
|
686
|
+
const out: FloorplanGeometry[] = []
|
|
687
|
+
const drawn: ServiceRole[] = []
|
|
688
|
+
// a utility a plugin already runs as its own line is not drawn twice
|
|
689
|
+
const drawnRuns = new Set(
|
|
690
|
+
sitePlanServiceAnswers((services) => services.drawnRuns?.(scene)).flat(),
|
|
691
|
+
)
|
|
692
|
+
const pole = points.find((p) => p.role === 'pole')
|
|
693
|
+
|
|
694
|
+
const labels: Record<ServiceRole, string> = {
|
|
695
|
+
electric: `ELECTRIC SERVICE (${entrance.kind.toUpperCase()})`,
|
|
696
|
+
water: 'WATER SERVICE 3/4" (VERIFY)',
|
|
697
|
+
sewer: 'SEWER LATERAL 4" (VERIFY)',
|
|
698
|
+
}
|
|
699
|
+
for (const role of ['electric', 'water', 'sewer'] as const) {
|
|
700
|
+
const point = points.find((p) => p.role === role)
|
|
701
|
+
if (!point || drawnRuns.has(role)) continue
|
|
702
|
+
const route =
|
|
703
|
+
role === 'electric' && pole
|
|
704
|
+
? entrance.kind === 'overhead'
|
|
705
|
+
? [point.at, pole.at]
|
|
706
|
+
: (serviceRoute(point.at, point.normal, lot, streetEdges, outline) ?? [point.at, pole.at])
|
|
707
|
+
: serviceRoute(point.at, point.normal, lot, streetEdges, outline)
|
|
708
|
+
if (!route || route.length < 2) continue
|
|
709
|
+
drawn.push(role)
|
|
710
|
+
const style = SERVICE_STYLE[role]
|
|
711
|
+
out.push({
|
|
712
|
+
kind: 'polyline',
|
|
713
|
+
points: route,
|
|
714
|
+
fill: 'none',
|
|
715
|
+
stroke: style.color,
|
|
716
|
+
strokeWidth: 0.06,
|
|
717
|
+
strokeDasharray: style.dash,
|
|
718
|
+
metadata: { sitePlan: 'service', role },
|
|
719
|
+
})
|
|
720
|
+
// the label on the longest run
|
|
721
|
+
let best = { i: 1, l: 0 }
|
|
722
|
+
for (let i = 1; i < route.length; i++) {
|
|
723
|
+
const l = len(sub(route[i] as Pt, route[i - 1] as Pt))
|
|
724
|
+
if (l > best.l) best = { i, l }
|
|
725
|
+
}
|
|
726
|
+
const a = route[best.i - 1] as Pt
|
|
727
|
+
const b = route[best.i] as Pt
|
|
728
|
+
const angle = Math.atan2(b[1] - a[1], b[0] - a[0])
|
|
729
|
+
const n = unit([-(b[1] - a[1]), b[0] - a[0]])
|
|
730
|
+
out.push(
|
|
731
|
+
runLabel(
|
|
732
|
+
add(mul(add(a, b), 0.5), mul(n, fontSize * 0.75)),
|
|
733
|
+
angle,
|
|
734
|
+
labels[role],
|
|
735
|
+
fontSize,
|
|
736
|
+
style.color,
|
|
737
|
+
{ sitePlan: 'service-label', role },
|
|
738
|
+
),
|
|
739
|
+
)
|
|
740
|
+
// the ends: the meter at the house (electric), the meter box / cleanout at the line
|
|
741
|
+
const end = route[route.length - 1] as Pt
|
|
742
|
+
const tag = (at: Pt, text: string, circle: boolean) => {
|
|
743
|
+
const r = fontSize * 0.75
|
|
744
|
+
out.push(
|
|
745
|
+
circle
|
|
746
|
+
? {
|
|
747
|
+
kind: 'circle',
|
|
748
|
+
cx: at[0],
|
|
749
|
+
cy: at[1],
|
|
750
|
+
r,
|
|
751
|
+
fill: HALO,
|
|
752
|
+
stroke: style.color,
|
|
753
|
+
strokeWidth: 0.04,
|
|
754
|
+
}
|
|
755
|
+
: {
|
|
756
|
+
kind: 'rect',
|
|
757
|
+
x: at[0] - r,
|
|
758
|
+
y: at[1] - r * 0.75,
|
|
759
|
+
width: r * 2,
|
|
760
|
+
height: r * 1.5,
|
|
761
|
+
fill: HALO,
|
|
762
|
+
stroke: style.color,
|
|
763
|
+
strokeWidth: 0.04,
|
|
764
|
+
},
|
|
765
|
+
)
|
|
766
|
+
out.push({
|
|
767
|
+
kind: 'text',
|
|
768
|
+
x: at[0],
|
|
769
|
+
y: at[1],
|
|
770
|
+
text,
|
|
771
|
+
fontSize: fontSize * 0.7,
|
|
772
|
+
fill: style.color,
|
|
773
|
+
fontWeight: 700,
|
|
774
|
+
textAnchor: 'middle',
|
|
775
|
+
dominantBaseline: 'middle',
|
|
776
|
+
upright: true,
|
|
777
|
+
})
|
|
778
|
+
}
|
|
779
|
+
if (role === 'electric') tag(point.at, 'EM', false)
|
|
780
|
+
if (role === 'water') tag(end, 'WM', false)
|
|
781
|
+
if (role === 'sewer') {
|
|
782
|
+
tag(point.at, 'CO', true)
|
|
783
|
+
tag(end, 'CO', true)
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
if (pole && drawn.includes('electric')) {
|
|
787
|
+
out.push({
|
|
788
|
+
kind: 'circle',
|
|
789
|
+
cx: pole.at[0],
|
|
790
|
+
cy: pole.at[1],
|
|
791
|
+
r: 0.25,
|
|
792
|
+
fill: INK,
|
|
793
|
+
stroke: INK,
|
|
794
|
+
strokeWidth: 0.02,
|
|
795
|
+
})
|
|
796
|
+
out.push({
|
|
797
|
+
kind: 'text',
|
|
798
|
+
x: pole.at[0],
|
|
799
|
+
y: pole.at[1] - fontSize * 1.1,
|
|
800
|
+
text: entrance.kind === 'overhead' ? 'UTILITY POLE (VERIFY)' : 'PAD TRANSFORMER (VERIFY)',
|
|
801
|
+
fontSize: fontSize * 0.8,
|
|
802
|
+
fill: INK_SOFT,
|
|
803
|
+
fontWeight: 600,
|
|
804
|
+
textAnchor: 'middle',
|
|
805
|
+
dominantBaseline: 'middle',
|
|
806
|
+
upright: true,
|
|
807
|
+
stroke: HALO,
|
|
808
|
+
strokeWidth: fontSize * 0.2,
|
|
809
|
+
paintOrder: 'stroke',
|
|
810
|
+
})
|
|
811
|
+
}
|
|
812
|
+
const ac = points.find((p) => p.role === 'ac')
|
|
813
|
+
let pad: Pt[] | null = null
|
|
814
|
+
if (ac) {
|
|
815
|
+
const s = 0.9144 // a 36 in pad
|
|
816
|
+
pad = [
|
|
817
|
+
[ac.at[0] - s / 2, ac.at[1] - s / 2],
|
|
818
|
+
[ac.at[0] + s / 2, ac.at[1] - s / 2],
|
|
819
|
+
[ac.at[0] + s / 2, ac.at[1] + s / 2],
|
|
820
|
+
[ac.at[0] - s / 2, ac.at[1] + s / 2],
|
|
821
|
+
]
|
|
822
|
+
out.push({
|
|
823
|
+
kind: 'rect',
|
|
824
|
+
x: ac.at[0] - s / 2,
|
|
825
|
+
y: ac.at[1] - s / 2,
|
|
826
|
+
width: s,
|
|
827
|
+
height: s,
|
|
828
|
+
fill: '#f3f4f6',
|
|
829
|
+
stroke: INK,
|
|
830
|
+
strokeWidth: 0.03,
|
|
831
|
+
metadata: { sitePlan: 'ac-pad' },
|
|
832
|
+
})
|
|
833
|
+
out.push({
|
|
834
|
+
kind: 'line',
|
|
835
|
+
x1: ac.at[0] - s / 2,
|
|
836
|
+
y1: ac.at[1] - s / 2,
|
|
837
|
+
x2: ac.at[0] + s / 2,
|
|
838
|
+
y2: ac.at[1] + s / 2,
|
|
839
|
+
stroke: INK,
|
|
840
|
+
strokeWidth: 0.015,
|
|
841
|
+
})
|
|
842
|
+
out.push({
|
|
843
|
+
kind: 'line',
|
|
844
|
+
x1: ac.at[0] - s / 2,
|
|
845
|
+
y1: ac.at[1] + s / 2,
|
|
846
|
+
x2: ac.at[0] + s / 2,
|
|
847
|
+
y2: ac.at[1] - s / 2,
|
|
848
|
+
stroke: INK,
|
|
849
|
+
strokeWidth: 0.015,
|
|
850
|
+
})
|
|
851
|
+
// the label away from the house
|
|
852
|
+
const c = outline[0] ? polygonCentroid(outline[0]) : ac.at
|
|
853
|
+
const away = unit(sub(ac.at, c))
|
|
854
|
+
out.push({
|
|
855
|
+
kind: 'text',
|
|
856
|
+
x: ac.at[0] + away[0] * (s * 0.6 + fontSize * 2.6),
|
|
857
|
+
y: ac.at[1] + away[1] * (s * 0.6 + fontSize * 1.2),
|
|
858
|
+
text: 'A/C CONDENSER ON 36" PAD',
|
|
859
|
+
fontSize: fontSize * 0.8,
|
|
860
|
+
fill: INK,
|
|
861
|
+
fontWeight: 600,
|
|
862
|
+
textAnchor: 'middle',
|
|
863
|
+
dominantBaseline: 'middle',
|
|
864
|
+
upright: true,
|
|
865
|
+
stroke: HALO,
|
|
866
|
+
strokeWidth: fontSize * 0.2,
|
|
867
|
+
paintOrder: 'stroke',
|
|
868
|
+
metadata: { sitePlan: 'ac-label' },
|
|
869
|
+
})
|
|
870
|
+
}
|
|
871
|
+
return { primitives: out, drawn, entrance: entrance.kind, pad }
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/* ========================================================== drainage */
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* Drainage arrows off every face of the house: the finish grade falls away
|
|
878
|
+
* from the foundation — 6 in in the first 10 ft (FBC-R / IRC R401.3) — so
|
|
879
|
+
* each arrow runs from the wall out 10 ft. One arrow carries the rule.
|
|
880
|
+
*/
|
|
881
|
+
export function drainagePrimitives(
|
|
882
|
+
outline: readonly Pt[][],
|
|
883
|
+
fontSize: number,
|
|
884
|
+
obstacles: readonly Pt[][] = [],
|
|
885
|
+
): FloorplanGeometry[] {
|
|
886
|
+
const out: FloorplanGeometry[] = []
|
|
887
|
+
const raw = [...outline].sort((a, b) => polygonArea(b) - polygonArea(a))[0]
|
|
888
|
+
if (!raw || raw.length < 3) return out
|
|
889
|
+
// one arrow per face: a vertex in the middle of a straight run is not a corner
|
|
890
|
+
const ring = raw.filter((p, i) => {
|
|
891
|
+
const prev = raw[(i - 1 + raw.length) % raw.length] as Pt
|
|
892
|
+
const next = raw[(i + 1) % raw.length] as Pt
|
|
893
|
+
const d1 = unit(sub(p, prev))
|
|
894
|
+
const d2 = unit(sub(next, p))
|
|
895
|
+
return Math.abs(d1[0] * d2[1] - d1[1] * d2[0]) > 0.05
|
|
896
|
+
})
|
|
897
|
+
if (ring.length < 3) return out
|
|
898
|
+
const tenFt = 10 * FT
|
|
899
|
+
// the rule rides the arrow off the longest face
|
|
900
|
+
let longest = -1
|
|
901
|
+
let longestLen = 0
|
|
902
|
+
for (let i = 0; i < ring.length; i++) {
|
|
903
|
+
const l = len(sub(ring[(i + 1) % ring.length] as Pt, ring[i] as Pt))
|
|
904
|
+
if (l > longestLen) {
|
|
905
|
+
longest = i
|
|
906
|
+
longestLen = l
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
for (let i = 0; i < ring.length; i++) {
|
|
910
|
+
const a = ring[i] as Pt
|
|
911
|
+
const b = ring[(i + 1) % ring.length] as Pt
|
|
912
|
+
if (len(sub(b, a)) < 4) continue
|
|
913
|
+
const n = outwardNormal(ring, i)
|
|
914
|
+
// off the face where it crosses no porch, deck or paving (and clear of
|
|
915
|
+
// the yard dimension off the face's middle)
|
|
916
|
+
const run = (t: number) => {
|
|
917
|
+
const base = add(a, mul(sub(b, a), t))
|
|
918
|
+
return { tail: add(base, mul(n, 0.5)), tip: add(base, mul(n, tenFt)) }
|
|
919
|
+
}
|
|
920
|
+
const free = (r: { tail: Pt; tip: Pt }) =>
|
|
921
|
+
!crossesRings(r.tail, r.tip, obstacles) &&
|
|
922
|
+
!obstacles.some((o) => pointInPolygon(o, r.tail[0], r.tail[1]))
|
|
923
|
+
const spot = [0.36, 0.64, 0.22, 0.78, 0.12, 0.88].map(run).find(free)
|
|
924
|
+
if (!spot) continue
|
|
925
|
+
const { tail, tip } = spot
|
|
926
|
+
const d = unit(sub(tip, tail))
|
|
927
|
+
const side: Pt = [-d[1], d[0]]
|
|
928
|
+
const head = 0.45
|
|
929
|
+
out.push({
|
|
930
|
+
kind: 'line',
|
|
931
|
+
x1: tail[0],
|
|
932
|
+
y1: tail[1],
|
|
933
|
+
x2: tip[0],
|
|
934
|
+
y2: tip[1],
|
|
935
|
+
stroke: '#0e7490',
|
|
936
|
+
strokeWidth: 0.04,
|
|
937
|
+
metadata: { sitePlan: 'drainage' },
|
|
938
|
+
})
|
|
939
|
+
out.push({
|
|
940
|
+
kind: 'polygon',
|
|
941
|
+
points: [
|
|
942
|
+
tip,
|
|
943
|
+
add(sub(tip, mul(d, head)), mul(side, head * 0.4)),
|
|
944
|
+
add(sub(tip, mul(d, head)), mul(side, -head * 0.4)),
|
|
945
|
+
],
|
|
946
|
+
fill: '#0e7490',
|
|
947
|
+
stroke: '#0e7490',
|
|
948
|
+
strokeWidth: 0.01,
|
|
949
|
+
metadata: { sitePlan: 'drainage' },
|
|
950
|
+
})
|
|
951
|
+
if (i === longest) {
|
|
952
|
+
out.push(
|
|
953
|
+
runLabel(
|
|
954
|
+
add(mul(add(tail, tip), 0.5), mul(side, fontSize * 0.75)),
|
|
955
|
+
Math.atan2(d[1], d[0]),
|
|
956
|
+
'6" MIN. FALL IN 10\'',
|
|
957
|
+
fontSize * 0.75,
|
|
958
|
+
'#0e7490',
|
|
959
|
+
{ sitePlan: 'drainage-label' },
|
|
960
|
+
),
|
|
961
|
+
)
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
return out
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
/* ========================================================= elevations */
|
|
968
|
+
|
|
969
|
+
/** "174.25'" from an absolute elevation in feet, or "+0.42'" relative to the site datum. */
|
|
970
|
+
export function formatElevation(relM: number, datumFt: number | null): string {
|
|
971
|
+
const ft = relM / FT
|
|
972
|
+
return datumFt !== null
|
|
973
|
+
? `${(datumFt + ft).toFixed(2)}'`
|
|
974
|
+
: `${ft >= 0 ? '+' : ''}${ft.toFixed(2)}'`
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* Spot grades: an × and its elevation at each lot corner and just off each
|
|
979
|
+
* corner of the house (the finish grade the pad grading left), read from
|
|
980
|
+
* the site's terrain.
|
|
981
|
+
*/
|
|
982
|
+
export function spotElevationPrimitives(args: {
|
|
983
|
+
field: TerrainField
|
|
984
|
+
datumFt: number | null
|
|
985
|
+
lot: readonly Pt[]
|
|
986
|
+
outline: readonly Pt[][]
|
|
987
|
+
fontSize: number
|
|
988
|
+
}): FloorplanGeometry[] {
|
|
989
|
+
const { field, datumFt, lot, outline, fontSize } = args
|
|
990
|
+
const out: FloorplanGeometry[] = []
|
|
991
|
+
const spot = (at: Pt, labelAt: Pt, kind: string) => {
|
|
992
|
+
const s = fontSize * 0.35
|
|
993
|
+
out.push({
|
|
994
|
+
kind: 'line',
|
|
995
|
+
x1: at[0] - s,
|
|
996
|
+
y1: at[1] - s,
|
|
997
|
+
x2: at[0] + s,
|
|
998
|
+
y2: at[1] + s,
|
|
999
|
+
stroke: INK,
|
|
1000
|
+
strokeWidth: 0.02,
|
|
1001
|
+
})
|
|
1002
|
+
out.push({
|
|
1003
|
+
kind: 'line',
|
|
1004
|
+
x1: at[0] - s,
|
|
1005
|
+
y1: at[1] + s,
|
|
1006
|
+
x2: at[0] + s,
|
|
1007
|
+
y2: at[1] - s,
|
|
1008
|
+
stroke: INK,
|
|
1009
|
+
strokeWidth: 0.02,
|
|
1010
|
+
})
|
|
1011
|
+
out.push({
|
|
1012
|
+
kind: 'text',
|
|
1013
|
+
x: labelAt[0],
|
|
1014
|
+
y: labelAt[1],
|
|
1015
|
+
text: formatElevation(heightAt(field, at[0], at[1]), datumFt),
|
|
1016
|
+
fontSize,
|
|
1017
|
+
fill: INK,
|
|
1018
|
+
fontWeight: 500,
|
|
1019
|
+
fontFamily: 'Helvetica, Arial, sans-serif',
|
|
1020
|
+
textAnchor: 'middle',
|
|
1021
|
+
dominantBaseline: 'middle',
|
|
1022
|
+
upright: true,
|
|
1023
|
+
stroke: HALO,
|
|
1024
|
+
strokeWidth: fontSize * 0.25,
|
|
1025
|
+
paintOrder: 'stroke',
|
|
1026
|
+
metadata: { sitePlan: 'spot-elevation', at: kind },
|
|
1027
|
+
})
|
|
1028
|
+
}
|
|
1029
|
+
// lot corners: the × just inside the corner, its number further in
|
|
1030
|
+
const lc = lot.length >= 3 ? polygonCentroid(lot) : null
|
|
1031
|
+
if (lc) {
|
|
1032
|
+
for (const p of lot) {
|
|
1033
|
+
const inward = unit(sub(lc, p))
|
|
1034
|
+
spot(add(p, mul(inward, 0.9)), add(p, mul(inward, 0.9 + fontSize * 2.2)), 'lot')
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
// house corners: out along the corner's bisector
|
|
1038
|
+
const ring = [...outline].sort((a, b) => polygonArea(b) - polygonArea(a))[0]
|
|
1039
|
+
if (ring && ring.length >= 3) {
|
|
1040
|
+
for (let i = 0; i < ring.length; i++) {
|
|
1041
|
+
const prev = ring[(i - 1 + ring.length) % ring.length] as Pt
|
|
1042
|
+
const p = ring[i] as Pt
|
|
1043
|
+
const next = ring[(i + 1) % ring.length] as Pt
|
|
1044
|
+
const d1 = unit(sub(p, prev))
|
|
1045
|
+
const d2 = unit(sub(next, p))
|
|
1046
|
+
// a collinear vertex is not a corner
|
|
1047
|
+
if (Math.abs(d1[0] * d2[1] - d1[1] * d2[0]) < 0.2) continue
|
|
1048
|
+
const n1 = outwardNormal(ring, (i - 1 + ring.length) % ring.length)
|
|
1049
|
+
const n2 = outwardNormal(ring, i)
|
|
1050
|
+
const bis = unit(add(n1, n2))
|
|
1051
|
+
// a re-entrant corner's bisector points into the house: skip it
|
|
1052
|
+
const probe = add(p, mul(bis, 0.3))
|
|
1053
|
+
if (outline.some((r) => pointInPolygon(r, probe[0], probe[1]))) continue
|
|
1054
|
+
const at = add(p, mul(bis, 0.8))
|
|
1055
|
+
spot(at, add(at, mul(bis, fontSize * 2.4)), 'house')
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
return out
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/** The storey slab's top — the finish floor — level-local metres, and the garage pad's. */
|
|
1062
|
+
export function floorTops(
|
|
1063
|
+
scene: SceneSnapshot,
|
|
1064
|
+
level: LevelNode | null,
|
|
1065
|
+
): { floor: number | null; garage: number | null } {
|
|
1066
|
+
let floor: number | null = null
|
|
1067
|
+
let garage: number | null = null
|
|
1068
|
+
for (const childId of level?.children ?? []) {
|
|
1069
|
+
const n = scene.nodes[childId as AnyNodeId] as
|
|
1070
|
+
| (AnyNode & { elevation?: number; metadata?: { floor?: unknown } })
|
|
1071
|
+
| undefined
|
|
1072
|
+
if (n?.type !== 'slab') continue
|
|
1073
|
+
// the driveway and walks are paving at grade, never the finish floor
|
|
1074
|
+
if (flatworkKindOf(n)) continue
|
|
1075
|
+
const tag = n.metadata?.floor
|
|
1076
|
+
const top = typeof n.elevation === 'number' ? n.elevation : 0.05
|
|
1077
|
+
if (tag === 'garage-slab-at-grade') garage = garage === null ? top : Math.min(garage, top)
|
|
1078
|
+
else if (tag === 'slab-on-grade' || tag === 'platform' || tag === undefined) {
|
|
1079
|
+
if (tag === undefined && (n.metadata as { porch?: unknown } | undefined)?.porch) continue
|
|
1080
|
+
floor = floor === null ? top : Math.max(floor, top)
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
return { floor, garage }
|
|
1084
|
+
}
|