@pascal-app/editor 1.0.1 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +16 -9
- package/src/components/editor/action-menu-button.tsx +83 -0
- package/src/components/editor/bake-thumbnail.tsx +315 -4
- package/src/components/editor/capture-level-picker.tsx +72 -0
- package/src/components/editor/custom-camera-controls.tsx +221 -128
- package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
- package/src/components/editor/duplicate-node.ts +244 -0
- package/src/components/editor/editor-layout-mobile.tsx +37 -25
- package/src/components/editor/editor-layout-v2.tsx +131 -58
- package/src/components/editor/export-manager.tsx +34 -8
- package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
- package/src/components/editor/first-person/build-collider-world.ts +6 -2
- package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
- package/src/components/editor/first-person/mechanism-targets.ts +36 -0
- package/src/components/editor/first-person-controls.tsx +205 -2
- package/src/components/editor/floating-action-menu.tsx +63 -207
- package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
- package/src/components/editor/floor-aware-camera-controls.ts +324 -0
- package/src/components/editor/floor-openings-overlay.tsx +243 -0
- package/src/components/editor/floor-region-controls.tsx +846 -0
- package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
- package/src/components/editor/floor-surface.test.ts +165 -0
- package/src/components/editor/floor-surface.ts +182 -0
- package/src/components/editor/floorplan-background-selection.test.ts +11 -2
- package/src/components/editor/floorplan-background-selection.ts +1 -3
- package/src/components/editor/floorplan-panel.tsx +536 -487
- package/src/components/editor/footprint-height-handle.tsx +204 -0
- package/src/components/editor/grid.tsx +31 -7
- package/src/components/editor/group-actions.ts +59 -11
- package/src/components/editor/group-floating-action-menu.tsx +19 -9
- package/src/components/editor/group-move-3d.ts +4 -6
- package/src/components/editor/group-rotate-handle.tsx +81 -35
- package/src/components/editor/group-selection-box-3d.tsx +25 -17
- package/src/components/editor/group-transform-shared.test.ts +126 -1
- package/src/components/editor/group-transform-shared.ts +79 -73
- package/src/components/editor/handles/handle-arrow.tsx +72 -2
- package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
- package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
- package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
- package/src/components/editor/handles/use-handle-drag.ts +272 -131
- package/src/components/editor/index.tsx +316 -176
- package/src/components/editor/join-walls-pill.tsx +44 -0
- package/src/components/editor/node-action-menu.tsx +70 -70
- package/src/components/editor/node-arrow-handles.tsx +167 -14
- package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
- package/src/components/editor/open-wall-ends-hint.tsx +89 -0
- package/src/components/editor/quick-measurement-hud.tsx +3 -2
- package/src/components/editor/registry-action-contributions.test.tsx +63 -0
- package/src/components/editor/registry-action-contributions.tsx +132 -0
- package/src/components/editor/room-controls.tsx +847 -0
- package/src/components/editor/room-floor-highlight.tsx +128 -0
- package/src/components/editor/room-handles-2d.tsx +354 -0
- package/src/components/editor/room-handles.tsx +675 -0
- package/src/components/editor/room-highlight.tsx +225 -0
- package/src/components/editor/room-labels.test.ts +357 -0
- package/src/components/editor/room-labels.tsx +990 -0
- package/src/components/editor/room-transform-ghost.tsx +256 -0
- package/src/components/editor/room-webgpu-materials.test.ts +27 -0
- package/src/components/editor/selection-manager.tsx +451 -317
- package/src/components/editor/slab-hole-highlights.tsx +3 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
- package/src/components/editor/snapshot-capture.test.ts +52 -4
- package/src/components/editor/snapshot-capture.ts +30 -5
- package/src/components/editor/thumbnail-generator.test.ts +308 -0
- package/src/components/editor/thumbnail-generator.tsx +1035 -229
- package/src/components/editor/use-floorplan-background-placement.ts +31 -11
- package/src/components/editor/vector-edge-extractor.tsx +121 -0
- package/src/components/editor/views/host-view.tsx +61 -0
- package/src/components/editor/views/plugins-view.tsx +670 -0
- package/src/components/editor/views/scene-region.tsx +59 -0
- package/src/components/editor/views/use-editor-views.ts +57 -0
- package/src/components/editor/views/view-bar.tsx +220 -0
- package/src/components/editor/wall-justification.test.ts +140 -0
- package/src/components/editor/wall-move-side-handles.tsx +114 -163
- package/src/components/editor/wall-opening-highlights.tsx +2 -3
- package/src/components/editor/wall-push-arrow.tsx +92 -0
- package/src/components/editor/wall-region-handles.tsx +509 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
- package/src/components/editor-2d/floorplan-group-move.tsx +17 -24
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +33 -12
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +242 -126
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
- package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +166 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +87 -8
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +355 -245
- package/src/components/editor-2d/svg-paths.ts +12 -15
- package/src/components/procedural-items/inspector.tsx +40 -15
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
- package/src/components/systems/zone/zone-system.test.ts +47 -0
- package/src/components/systems/zone/zone-system.tsx +31 -66
- package/src/components/tools/__fixtures__/hosting.ts +10 -0
- package/src/components/tools/item/item-surface-placement.test.ts +3 -9
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +3 -9
- package/src/components/tools/item/placement-history.test.tsx +474 -0
- package/src/components/tools/item/placement-strategies.ts +63 -47
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +3 -3
- package/src/components/tools/item/use-draft-node.test.tsx +432 -1
- package/src/components/tools/item/use-draft-node.ts +207 -97
- package/src/components/tools/item/use-placement-coordinator.tsx +251 -111
- package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
- package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
- package/src/components/tools/registry/item-surface-move.test.ts +10 -2
- package/src/components/tools/registry/item-surface-move.ts +46 -24
- package/src/components/tools/registry/item-surface-resolver.test.ts +2 -2
- package/src/components/tools/registry/move-registry-node-tool.tsx +26 -10
- package/src/components/tools/select/box-select-tool.tsx +2 -12
- package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
- package/src/components/tools/select/select-candidates.test.ts +37 -4
- package/src/components/tools/select/select-candidates.ts +15 -11
- package/src/components/tools/shared/affordance-dispatch.test.ts +24 -0
- package/src/components/tools/shared/affordance-dispatch.ts +13 -0
- package/src/components/tools/shared/cursor-sphere.tsx +36 -12
- package/src/components/tools/shared/draft-measurement-label.tsx +38 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +27 -0
- package/src/components/tools/shared/pointer-support-cap.ts +17 -5
- package/src/components/tools/shared/surface-rejection.test.tsx +5 -4
- package/src/components/tools/shared/surface-rejection.tsx +14 -0
- package/src/components/tools/site/site-boundary-editor.tsx +1 -2
- package/src/components/tools/stair/stair-defaults.ts +0 -3
- package/src/components/tools/stair/stair-tool.tsx +90 -42
- package/src/components/tools/tool-manager.tsx +82 -23
- package/src/components/tools/wall/wall-drafting.test.ts +19 -4
- package/src/components/tools/zone/zone-tool.tsx +4 -12
- package/src/components/ui/action-menu/action-button.tsx +3 -0
- package/src/components/ui/action-menu/camera-actions.tsx +2 -1
- package/src/components/ui/action-menu/control-modes.tsx +2 -35
- package/src/components/ui/action-menu/measurement-control.tsx +3 -10
- package/src/components/ui/build-panel/build-panel.tsx +235 -0
- package/src/components/ui/command-palette/editor-commands.tsx +4 -30
- package/src/components/ui/command-palette/index.tsx +5 -24
- package/src/components/ui/controls/finish-swatch.tsx +110 -0
- package/src/components/ui/controls/material-paint-panel.tsx +101 -91
- package/src/components/ui/controls/material-picker.tsx +8 -67
- package/src/components/ui/controls/scene-material-list.tsx +4 -32
- package/src/components/ui/controls/select-control.tsx +65 -0
- package/src/components/ui/controls/slider-control.tsx +76 -31
- package/src/components/ui/floating-level-selector.tsx +9 -14
- package/src/components/ui/helpers/building-helper.tsx +4 -1
- package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
- package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
- package/src/components/ui/helpers/helper-manager.tsx +348 -19
- package/src/components/ui/helpers/item-helper.tsx +8 -0
- package/src/components/ui/helpers/registered-tool-helper.tsx +10 -33
- package/src/components/ui/icon-ref.tsx +45 -1
- package/src/components/ui/item-catalog/item-catalog.tsx +5 -30
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/node-icon-image.tsx +16 -0
- package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
- package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
- package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
- package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
- package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
- package/src/components/ui/panels/multi-field-value.test.ts +26 -3
- package/src/components/ui/panels/multi-field-value.ts +6 -3
- package/src/components/ui/panels/multi-height-mode.tsx +27 -21
- package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
- package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
- package/src/components/ui/panels/node-display.ts +8 -3
- package/src/components/ui/panels/panel-manager.tsx +103 -68
- package/src/components/ui/panels/panel-wrapper.tsx +78 -36
- package/src/components/ui/panels/parametric-field-control.tsx +1 -1
- package/src/components/ui/panels/parametric-inspector.tsx +7 -19
- package/src/components/ui/panels/reference-panel.tsx +2 -3
- package/src/components/ui/panels/room-construction-rows.tsx +774 -0
- package/src/components/ui/panels/room-name-options.test.ts +55 -0
- package/src/components/ui/panels/room-name-options.ts +47 -0
- package/src/components/ui/panels/room-openings-field.tsx +110 -0
- package/src/components/ui/panels/room-panel.tsx +725 -0
- package/src/components/ui/panels/shape-choice.tsx +58 -0
- package/src/components/ui/panels/wall-region-list.tsx +109 -0
- package/src/components/ui/primitives/color-dot.tsx +36 -24
- package/src/components/ui/primitives/shortcut-token.tsx +4 -0
- package/src/components/ui/primitives/sidebar.tsx +2 -0
- package/src/components/ui/right-stack.test.ts +29 -0
- package/src/components/ui/right-stack.tsx +145 -0
- package/src/components/ui/sidebar/icon-rail.tsx +5 -0
- package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +615 -372
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +7 -7
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
- package/src/components/ui/sidebar/tab-bar.tsx +10 -0
- package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
- package/src/components/viewer/floorplan-compass-button.tsx +9 -1
- package/src/components/viewer/floorplan-preview-visibility.test.ts +35 -0
- package/src/components/viewer/floorplan-preview-visibility.ts +19 -0
- package/src/components/viewer/floorplan-preview.tsx +6 -18
- package/src/components/viewer/viewer-hover-label.tsx +34 -0
- package/src/components/viewer/viewer-rooms.tsx +175 -0
- package/src/components/viewer/viewer-scene-header.tsx +181 -145
- package/src/components/viewer/viewer-selection-manager.tsx +124 -0
- package/src/components/viewer/viewer-stage.tsx +2 -2
- package/src/components/viewer-zone-system.tsx +9 -9
- package/src/hooks/use-auto-save.integration.test.tsx +311 -0
- package/src/hooks/use-auto-save.ts +55 -14
- package/src/hooks/use-ceiling-events.test.ts +1 -0
- package/src/hooks/use-ceiling-events.ts +2 -0
- package/src/hooks/use-grid-events.test.ts +2 -0
- package/src/hooks/use-grid-events.ts +16 -11
- package/src/hooks/use-keyboard.test.ts +182 -0
- package/src/hooks/use-keyboard.ts +122 -91
- package/src/hooks/use-open-wall-ends.test.ts +99 -0
- package/src/hooks/use-open-wall-ends.ts +238 -0
- package/src/hooks/use-placement-notice.test.ts +48 -0
- package/src/hooks/use-placement-notice.ts +42 -0
- package/src/hooks/use-selected-room.ts +143 -0
- package/src/index.tsx +306 -39
- package/src/lib/catalog-panel-model.ts +39 -0
- package/src/lib/catalog-selection.test.ts +27 -0
- package/src/lib/catalog-selection.ts +8 -0
- package/src/lib/ceiling-edit-session.test.ts +321 -0
- package/src/lib/ceiling-edit-session.ts +206 -0
- package/src/lib/ceiling-region-session.test.ts +179 -0
- package/src/lib/contextual-help.test.ts +27 -1
- package/src/lib/contextual-help.ts +16 -9
- package/src/lib/continuation.test.ts +19 -1
- package/src/lib/continuation.ts +25 -4
- package/src/lib/door-interaction.ts +14 -0
- package/src/lib/editor-level-display.test.ts +96 -0
- package/src/lib/editor-level-display.ts +48 -0
- package/src/lib/editor-views.ts +144 -0
- package/src/lib/export-geometry-wait.test.ts +90 -0
- package/src/lib/floor-edit-session.ts +153 -0
- package/src/lib/floor-footprints.test.ts +563 -0
- package/src/lib/floor-footprints.ts +395 -0
- package/src/lib/floor-foundation-support.test.ts +283 -0
- package/src/lib/floor-opening-draft.ts +218 -0
- package/src/lib/floor-openings.test.ts +216 -0
- package/src/lib/floor-region-geometry.ts +191 -0
- package/src/lib/floor-region-session.test.ts +485 -0
- package/src/lib/floor-region-session.ts +589 -0
- package/src/lib/floor-region-snap.ts +101 -0
- package/src/lib/floor-step-hover.test.ts +32 -0
- package/src/lib/floorplan/annotation-visibility.ts +17 -0
- package/src/lib/floorplan/dimension-drive.test.ts +360 -0
- package/src/lib/floorplan/dimension-drive.ts +479 -0
- package/src/lib/floorplan/floorplan-export.test.ts +15 -9
- package/src/lib/floorplan/floorplan-export.tsx +32 -8
- package/src/lib/floorplan/floorplan-extension.ts +48 -0
- package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
- package/src/lib/floorplan/floorplan-mode.ts +2 -0
- package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
- package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
- package/src/lib/floorplan/floorplan-readonly.ts +6 -0
- package/src/lib/floorplan/geometry.ts +13 -6
- package/src/lib/floorplan/index.ts +0 -9
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/marks.test.ts +213 -0
- package/src/lib/floorplan/marks.ts +238 -0
- package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
- package/src/lib/floorplan/open-wall-ends.ts +162 -0
- package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
- package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
- package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
- package/src/lib/floorplan/schedules.test.ts +189 -0
- package/src/lib/floorplan/schedules.ts +339 -0
- package/src/lib/floorplan/sheet-export.test.ts +100 -0
- package/src/lib/floorplan/sheet-export.ts +318 -0
- package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
- package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
- package/src/lib/floorplan/site-plan/contributors.ts +97 -0
- package/src/lib/floorplan/site-plan/coverage.ts +232 -0
- package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
- package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
- package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
- package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
- package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
- package/src/lib/floorplan/site-plan/geometry.ts +557 -0
- package/src/lib/floorplan/site-plan/index.ts +71 -0
- package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
- package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
- package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
- package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
- package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
- package/src/lib/floorplan/site-plan/street-name.ts +302 -0
- package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
- package/src/lib/floorplan/types.ts +1 -27
- package/src/lib/fresh-planar-placement.test.ts +36 -0
- package/src/lib/fresh-planar-placement.ts +191 -36
- package/src/lib/geometry-script/author.ts +152 -0
- package/src/lib/geometry-script/client.ts +152 -0
- package/src/lib/geometry-script/protocol.ts +12 -0
- package/src/lib/gesture-lifecycle.test.ts +595 -0
- package/src/lib/gesture-lifecycle.ts +290 -0
- package/src/lib/glb-export-zone-holes.test.ts +41 -0
- package/src/lib/glb-export.test.ts +544 -16
- package/src/lib/glb-export.ts +256 -125
- package/src/lib/grid-frame.test.ts +19 -0
- package/src/lib/grid-frame.ts +18 -0
- package/src/lib/history.test.ts +6 -0
- package/src/lib/history.ts +7 -0
- package/src/lib/hud-title.ts +109 -0
- package/src/lib/hydration-gated-storage.ts +49 -0
- package/src/lib/ifc-export.test.ts +174 -0
- package/src/lib/ifc-export.ts +201 -0
- package/src/lib/ifc-parts.ts +15 -0
- package/src/lib/inspector-expanded.test.ts +44 -0
- package/src/lib/inspector-expanded.ts +36 -0
- package/src/lib/interaction/scope.ts +27 -1
- package/src/lib/keyboard-pan.test.ts +184 -0
- package/src/lib/keyboard-pan.ts +238 -0
- package/src/lib/live-gesture-round-trip.test.ts +360 -0
- package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
- package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
- package/src/lib/lot/clean-ring.test.ts +186 -0
- package/src/lib/lot/clean-ring.ts +217 -0
- package/src/lib/lot/dossier.test.ts +202 -0
- package/src/lib/lot/dossier.ts +498 -0
- package/src/lib/lot/drop-in.test.ts +326 -0
- package/src/lib/lot/drop-in.ts +407 -0
- package/src/lib/lot/index.ts +43 -0
- package/src/lib/lot/lot-patch.test.ts +340 -0
- package/src/lib/lot/lot-patch.ts +394 -0
- package/src/lib/lot/parcel-provider.ts +43 -0
- package/src/lib/lot/terrain.test.ts +161 -0
- package/src/lib/lot/terrain.ts +266 -0
- package/src/lib/material-catalog-model.ts +74 -0
- package/src/lib/material-paint-panel-model.ts +80 -0
- package/src/lib/material-paint.ts +4 -0
- package/src/lib/mezzanine-actions.test.ts +342 -0
- package/src/lib/mezzanine-draft.test.ts +501 -0
- package/src/lib/mezzanine-draft.ts +278 -0
- package/src/lib/mezzanine-messages.ts +26 -0
- package/src/lib/mezzanine-selection.test.ts +207 -0
- package/src/lib/mezzanine-stairs.ts +46 -0
- package/src/lib/model-export.ts +3 -1
- package/src/lib/node-action-movement.ts +17 -6
- package/src/lib/node-display-label.ts +37 -0
- package/src/lib/orbit-floor-clearance.test.ts +165 -0
- package/src/lib/orbit-floor-clearance.ts +106 -0
- package/src/lib/overlay-transform.test.ts +28 -0
- package/src/lib/overlay-transform.ts +12 -0
- package/src/lib/paint-commit-route.test.ts +37 -0
- package/src/lib/paint-commit-route.ts +17 -0
- package/src/lib/paint-eyedropper.test.tsx +165 -0
- package/src/lib/paint-eyedropper.ts +99 -0
- package/src/lib/paint-pass-through.test.ts +111 -0
- package/src/lib/paint-pass-through.ts +76 -0
- package/src/lib/paint-region-hover.ts +25 -0
- package/src/lib/paint-region-mode.ts +154 -0
- package/src/lib/paint-regions.test.ts +166 -0
- package/src/lib/paint-regions.ts +192 -0
- package/src/lib/paint-scope.test.ts +317 -35
- package/src/lib/paint-scope.ts +266 -25
- package/src/lib/panel-rows.ts +96 -0
- package/src/lib/panel-tool-options.test.ts +34 -0
- package/src/lib/panel-tool-options.ts +46 -0
- package/src/lib/parametric-node-update.ts +37 -0
- package/src/lib/plate-paint-affected.test.ts +300 -0
- package/src/lib/plate-paint-affected.ts +177 -0
- package/src/lib/plugin-chat.ts +50 -0
- package/src/lib/plugin-panels.test.ts +25 -0
- package/src/lib/plugin-panels.ts +94 -0
- package/src/lib/print-shell-compiler.ts +1 -0
- package/src/lib/registered-draft-snap.ts +12 -0
- package/src/lib/roof-duplication.ts +10 -2
- package/src/lib/room-assembly-overlay.test.ts +275 -0
- package/src/lib/room-assembly-overlay.ts +266 -0
- package/src/lib/room-built-on.test.ts +379 -0
- package/src/lib/room-built-on.ts +138 -0
- package/src/lib/room-construction-commands.ts +712 -0
- package/src/lib/room-construction.test.ts +746 -0
- package/src/lib/room-construction.ts +385 -0
- package/src/lib/room-controls-phase6.test.ts +630 -0
- package/src/lib/room-divide-session.ts +523 -0
- package/src/lib/room-handle-drag.ts +1068 -0
- package/src/lib/room-name-catalog.test.ts +90 -0
- package/src/lib/room-name-catalog.ts +168 -0
- package/src/lib/room-push-dimensions.test.ts +311 -0
- package/src/lib/room-push-dimensions.ts +251 -0
- package/src/lib/room-selection-commands.ts +62 -0
- package/src/lib/room-selection.test.ts +973 -0
- package/src/lib/room-selection.ts +417 -0
- package/src/lib/room-structure-commands.ts +94 -0
- package/src/lib/room-structure.test.ts +921 -0
- package/src/lib/room-transform-session.ts +570 -0
- package/src/lib/room-zone-routing.test.ts +294 -0
- package/src/lib/room-zone-routing.ts +197 -0
- package/src/lib/scene-clipboard.test.ts +364 -0
- package/src/lib/scene-clipboard.ts +229 -48
- package/src/lib/scene-material-refs.test.ts +79 -0
- package/src/lib/scene-material-refs.ts +79 -0
- package/src/lib/scene.test.ts +87 -0
- package/src/lib/scene.ts +39 -45
- package/src/lib/selection-routing.test.ts +129 -45
- package/src/lib/selection-routing.ts +53 -74
- package/src/lib/session-writes.test.ts +120 -0
- package/src/lib/session-writes.ts +205 -0
- package/src/lib/sidebar-panel.ts +62 -0
- package/src/lib/site-boundary.test.ts +4 -6
- package/src/lib/site-boundary.ts +1 -1
- package/src/lib/snapping-mode.test.ts +17 -2
- package/src/lib/snapping-mode.ts +19 -7
- package/src/lib/spatial-drag-plane.test.ts +53 -0
- package/src/lib/spatial-drag-plane.ts +19 -0
- package/src/lib/spatial-pointer-input.test.ts +72 -0
- package/src/lib/spatial-pointer-input.ts +62 -0
- package/src/lib/spatial-snap.test.ts +47 -0
- package/src/lib/terrace-draft.test.ts +145 -0
- package/src/lib/terrace-draft.ts +213 -0
- package/src/lib/terrain-panel-model.ts +117 -0
- package/src/lib/tool-cursor-icon.test.ts +16 -0
- package/src/lib/tool-cursor-icon.ts +11 -0
- package/src/lib/typing-target.ts +9 -0
- package/src/lib/units.ts +77 -39
- package/src/lib/vector-edges.test.ts +58 -0
- package/src/lib/vector-edges.ts +400 -0
- package/src/lib/viewer-selection.test.ts +577 -0
- package/src/lib/viewer-selection.ts +569 -0
- package/src/lib/wall-draw-variant.test.ts +71 -0
- package/src/lib/wall-draw-variant.ts +91 -0
- package/src/lib/wall-paint-region-session.ts +246 -0
- package/src/lib/wall-paint-region.test.ts +373 -0
- package/src/lib/wall-polygon-draft.test.ts +262 -0
- package/src/lib/wall-polygon-draft.ts +286 -0
- package/src/lib/wall-push.test.ts +453 -0
- package/src/lib/wall-region-face.ts +352 -0
- package/src/lib/wall-region-handles.test.ts +304 -0
- package/src/lib/wall-region-handles.ts +368 -0
- package/src/lib/wall-region-list.test.ts +119 -0
- package/src/lib/wall-region-list.ts +136 -0
- package/src/lib/wall-region-snap.ts +110 -0
- package/src/lib/window-interaction.ts +14 -0
- package/src/lib/world-grid-snap.test.ts +48 -0
- package/src/lib/world-grid-snap.ts +9 -5
- package/src/lib/zone-content.ts +4 -1
- package/src/lib/zone-name.test.ts +17 -0
- package/src/lib/zone-name.ts +17 -0
- package/src/store/opens-in-select-mode.test.ts +223 -0
- package/src/store/paint-mode.test.ts +188 -0
- package/src/store/rotation-snapping.test.ts +53 -0
- package/src/store/terrain-sculpt-mode.test.ts +19 -21
- package/src/store/tool-mode.test.ts +137 -6
- package/src/store/use-delete-confirmation.ts +28 -1
- package/src/store/use-drawing-view.test.ts +24 -3
- package/src/store/use-drawing-view.ts +29 -1
- package/src/store/use-editor.tsx +442 -323
- package/src/store/use-floorplan-draft-preview.ts +23 -1
- package/src/store/use-floorplan-marquee.ts +12 -2
- package/src/store/use-interaction-scope.ts +155 -6
- package/src/store/use-quick-measurement-hud.ts +3 -3
- package/src/store/use-stair-build-preview.ts +13 -5
- package/src/store/use-wall-move-ghosts.ts +4 -2
- package/src/store/use-wall-region-handles-selection.test.ts +222 -0
- package/src/store/use-wall-region-handles-selection.ts +139 -0
- package/src/store/view-layout.test.ts +175 -0
- package/src/store/view-layout.ts +196 -0
- package/src/test-utils/immediate-animation-frames.ts +16 -0
- package/src/test-utils/live-gesture-round-trip.ts +224 -0
- package/src/test-utils/selection-harness.ts +82 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
- package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
- package/src/components/tools/fence/fence-drafting.test.ts +0 -100
- package/src/components/tools/fence/fence-drafting.ts +0 -266
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
- package/src/components/viewer/viewer-units-panel.tsx +0 -116
- package/src/lib/floorplan/stairs.ts +0 -489
- package/src/lib/level-duplication.test.ts +0 -144
- package/src/lib/level-duplication.ts +0 -195
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { createTerrainField, quantize, terrainContours } from '@pascal-app/core'
|
|
3
|
+
|
|
4
|
+
/** A 20 × 20 m field at 1 m spacing rising 2 m from x = −10 to x = +10 (a plane). */
|
|
5
|
+
function ramp() {
|
|
6
|
+
const field = createTerrainField({ origin: [-10, -10], spacing: 1, cols: 21, rows: 21 })
|
|
7
|
+
const heights = new Int16Array(field.heights)
|
|
8
|
+
for (let row = 0; row < 21; row++)
|
|
9
|
+
for (let col = 0; col < 21; col++) heights[row * 21 + col] = quantize(field, (col / 20) * 2)
|
|
10
|
+
return { ...field, heights }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const LOT: [number, number][] = [
|
|
14
|
+
[-8, -8],
|
|
15
|
+
[8, -8],
|
|
16
|
+
[8, 8],
|
|
17
|
+
[-8, 8],
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
describe('terrainContours', () => {
|
|
21
|
+
test('a plane rising along x: straight north–south lines at every interval, clipped to the lot, every fifth an index', () => {
|
|
22
|
+
const contours = terrainContours(ramp(), 0.3048, LOT) // 1 ft interval on a 2 m rise → levels 0.30 … 1.83
|
|
23
|
+
expect(contours.length).toBeGreaterThanOrEqual(5)
|
|
24
|
+
for (const c of contours) {
|
|
25
|
+
// a level line of a plane rising in x is a vertical line x = const
|
|
26
|
+
const xs = c.points.map((p) => p[0])
|
|
27
|
+
expect(Math.max(...xs) - Math.min(...xs)).toBeLessThan(1e-6)
|
|
28
|
+
expect(Math.abs(xs[0]! - (-10 + (c.levelM / 2) * 20))).toBeLessThan(1e-6)
|
|
29
|
+
// clipped to the lot: no point outside z ∈ [−8, 8] by more than a cell
|
|
30
|
+
for (const p of c.points) expect(Math.abs(p[1])).toBeLessThanOrEqual(9)
|
|
31
|
+
}
|
|
32
|
+
expect(contours.some((c) => c.index)).toBe(true)
|
|
33
|
+
// one polyline per level: the chaining joined the cell segments
|
|
34
|
+
const levels = new Set(contours.map((c) => c.levelM.toFixed(4)))
|
|
35
|
+
expect(levels.size).toBe(contours.length)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('no interval, or flat ground, gives no lines', () => {
|
|
39
|
+
expect(terrainContours(ramp(), 0, LOT)).toEqual([])
|
|
40
|
+
const flat = createTerrainField({ origin: [-10, -10], spacing: 1, cols: 21, rows: 21 })
|
|
41
|
+
expect(terrainContours(flat, 0.3048, LOT)).toEqual([])
|
|
42
|
+
})
|
|
43
|
+
})
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Site-plan contributors — how plugin packages put their own kinds on the
|
|
3
|
+
* site plan without the editor depending on them.
|
|
4
|
+
*
|
|
5
|
+
* The site-plan layer draws only what `buildSitePlanDrawing` returns (it
|
|
6
|
+
* replaces the registry layer, so `def.floorplan` is not consulted in site
|
|
7
|
+
* view). A plugin whose kinds live in SITE metres (utilities: poles, runs,
|
|
8
|
+
* service points) registers a contributor at bootstrap; its primitives are
|
|
9
|
+
* appended to every site-plan drawing, on screen and on sheets alike. A
|
|
10
|
+
* plugin that locates the house's utility services also passes `services`,
|
|
11
|
+
* and the site plan runs them schematically to the street.
|
|
12
|
+
*/
|
|
13
|
+
import type { AnyNodeId, FloorplanGeometry, SceneSnapshot } from '@pascal-app/core'
|
|
14
|
+
|
|
15
|
+
export type SitePlanContributor = (
|
|
16
|
+
scene: SceneSnapshot,
|
|
17
|
+
) => { primitives: FloorplanGeometry[] } | null
|
|
18
|
+
|
|
19
|
+
export type SitePlanServiceRole = 'electric' | 'water' | 'sewer'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Where one service leaves the house, in the storey's plan frame. A point
|
|
23
|
+
* hosted by a wall with no position of its own sits `wallT` along `wallId`.
|
|
24
|
+
*/
|
|
25
|
+
export type SitePlanServicePoint = {
|
|
26
|
+
role: SitePlanServiceRole | 'ac' | 'pole'
|
|
27
|
+
position: readonly [number, number, number]
|
|
28
|
+
wallId?: AnyNodeId
|
|
29
|
+
wallT?: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type SitePlanServices = {
|
|
33
|
+
/** The storey's service points; the first point of each role is drawn. */
|
|
34
|
+
points?: (scene: SceneSnapshot, levelId: AnyNodeId) => readonly SitePlanServicePoint[]
|
|
35
|
+
/** The storey's electric service entrance, when the plugin decides it. */
|
|
36
|
+
entrance?: (scene: SceneSnapshot, levelId: AnyNodeId) => 'overhead' | 'underground' | null
|
|
37
|
+
/** Services the plugin already draws as its own run, so the site plan does not route them. */
|
|
38
|
+
drawnRuns?: (scene: SceneSnapshot) => readonly SitePlanServiceRole[]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type Registration = { draw: SitePlanContributor | null; services?: SitePlanServices }
|
|
42
|
+
|
|
43
|
+
const contributors = new Map<string, Registration>()
|
|
44
|
+
const listeners = new Set<() => void>()
|
|
45
|
+
|
|
46
|
+
export function registerSitePlanContributor(
|
|
47
|
+
key: string,
|
|
48
|
+
contributor: SitePlanContributor | null,
|
|
49
|
+
services?: SitePlanServices,
|
|
50
|
+
): void {
|
|
51
|
+
contributors.set(key, { draw: contributor, services })
|
|
52
|
+
for (const listener of listeners) listener()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Whether any plugin has put its kinds on the site plan. */
|
|
56
|
+
export function hasSitePlanContributors(): boolean {
|
|
57
|
+
return contributors.size > 0
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function subscribeSitePlanContributors(listener: () => void): () => void {
|
|
61
|
+
listeners.add(listener)
|
|
62
|
+
return () => {
|
|
63
|
+
listeners.delete(listener)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function sitePlanContributions(scene: SceneSnapshot): FloorplanGeometry[] {
|
|
68
|
+
const out: FloorplanGeometry[] = []
|
|
69
|
+
for (const [key, { draw }] of contributors) {
|
|
70
|
+
if (!draw) continue
|
|
71
|
+
try {
|
|
72
|
+
const result = draw(scene)
|
|
73
|
+
if (result) out.push(...result.primitives)
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (typeof console !== 'undefined')
|
|
76
|
+
console.warn(`[site-plan] contributor ${key} failed`, error)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return out
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Every registered `services` hook's answer, in registration order; a throwing hook is skipped. */
|
|
83
|
+
export function sitePlanServiceAnswers<T>(
|
|
84
|
+
ask: (services: SitePlanServices) => T | null | undefined,
|
|
85
|
+
): T[] {
|
|
86
|
+
const out: T[] = []
|
|
87
|
+
for (const [key, { services }] of contributors) {
|
|
88
|
+
if (!services) continue
|
|
89
|
+
try {
|
|
90
|
+
const answer = ask(services)
|
|
91
|
+
if (answer !== null && answer !== undefined) out.push(answer)
|
|
92
|
+
} catch (error) {
|
|
93
|
+
if (typeof console !== 'undefined') console.warn(`[site-plan] services ${key} failed`, error)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return out
|
|
97
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lot coverage and impervious surface — the ONE computation the site plan
|
|
3
|
+
* (A1.0) and the cover's PROJECT DATA both print, so the two sheets can never
|
|
4
|
+
* disagree again (QA 2026-09-23: the cover said 24.6 %, the site plan 45.6 %
|
|
5
|
+
* — the site plan had counted the house's own floor platform and garage pad
|
|
6
|
+
* as "porches" on top of the footprint).
|
|
7
|
+
*
|
|
8
|
+
* The two figures a Florida plans examiner asks for (Alachua County / the
|
|
9
|
+
* City of Gainesville ask for both on a new single-family site plan; the
|
|
10
|
+
* zoning district caps the first, the stormwater review reads the second):
|
|
11
|
+
*
|
|
12
|
+
* BUILDING COVERAGE the lot area under roofed structure: the building's
|
|
13
|
+
* footprint to the OUTSIDE FACE of its exterior walls —
|
|
14
|
+
* every storey at or above grade projected, the attached
|
|
15
|
+
* garage included — plus the roofed porches, patios and
|
|
16
|
+
* decks (a slab at least half under a roof outline).
|
|
17
|
+
* Eaves and the roof overhang are not counted.
|
|
18
|
+
* IMPERVIOUS SURFACE building coverage plus the paving ON THE LOT (an apron
|
|
19
|
+
* drawn out into the right-of-way is not counted): the driveway, the
|
|
20
|
+
* walks, and the uncovered patios, landings and decks
|
|
21
|
+
* (a slatted deck over pervious ground is counted —
|
|
22
|
+
* the conservative reading; some jurisdictions exclude
|
|
23
|
+
* it, verify).
|
|
24
|
+
*
|
|
25
|
+
* Both are divided by the LOT AREA: the county parcel record when the lot
|
|
26
|
+
* drop-in resolved one (`site.parcel.lotAreaSqFt`), else the drawn lot
|
|
27
|
+
* polygon. Pure: reads a scene snapshot, touches no store.
|
|
28
|
+
*/
|
|
29
|
+
import type { SceneSnapshot } from '@pascal-app/core'
|
|
30
|
+
import { METRES_PER_FOOT, type Pt, pointInPolygon, polygonArea, polygonBounds } from './geometry'
|
|
31
|
+
import {
|
|
32
|
+
aboveGradeLevels,
|
|
33
|
+
findBuilding,
|
|
34
|
+
findLowestLevel,
|
|
35
|
+
findSite,
|
|
36
|
+
footprintOutline,
|
|
37
|
+
levelFootprintLoops,
|
|
38
|
+
type OutdoorPart,
|
|
39
|
+
outdoorParts,
|
|
40
|
+
roofOutlineRings,
|
|
41
|
+
} from './site-parts'
|
|
42
|
+
|
|
43
|
+
const SQFT_PER_SQM = 1 / (METRES_PER_FOOT * METRES_PER_FOOT)
|
|
44
|
+
|
|
45
|
+
export type ImperviousRowKey = 'building' | 'porches' | 'driveway' | 'walks' | 'patios'
|
|
46
|
+
|
|
47
|
+
export interface ImperviousRow {
|
|
48
|
+
key: ImperviousRowKey
|
|
49
|
+
label: string
|
|
50
|
+
sqFt: number
|
|
51
|
+
/** What the row holds, e.g. "2 covered: porch, rear deck". */
|
|
52
|
+
note: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface SiteCoverage {
|
|
56
|
+
/** Lot area, SF, and where it came from; null without a lot. */
|
|
57
|
+
lotSqFt: number | null
|
|
58
|
+
lotSource: 'parcel' | 'polygon' | null
|
|
59
|
+
/** The building's footprint to the outside face of its walls (garage included), SF. */
|
|
60
|
+
buildingSqFt: number
|
|
61
|
+
/** Roofed porches, patios and decks, SF. */
|
|
62
|
+
coveredOutdoorSqFt: number
|
|
63
|
+
/** Building coverage = footprint + roofed porches, SF. */
|
|
64
|
+
buildingCoverageSqFt: number
|
|
65
|
+
/** Building coverage ÷ lot, 0–1; null without a lot or a building. */
|
|
66
|
+
buildingCoverageRatio: number | null
|
|
67
|
+
drivewaySqFt: number
|
|
68
|
+
walksSqFt: number
|
|
69
|
+
/** Uncovered patios, landings and decks, SF. */
|
|
70
|
+
openOutdoorSqFt: number
|
|
71
|
+
/** Everything impervious, SF. */
|
|
72
|
+
imperviousSqFt: number
|
|
73
|
+
imperviousRatio: number | null
|
|
74
|
+
/** The IMPERVIOUS AREA table's rows, in print order (total and % are the caller's). */
|
|
75
|
+
rows: ImperviousRow[]
|
|
76
|
+
/** One line each: what the two figures are measured to — printed with them. */
|
|
77
|
+
basis: { building: string; impervious: string; lot: string }
|
|
78
|
+
/** The parts, for the drawing: the walls' outline and every outdoor slab (site metres). */
|
|
79
|
+
outline: Pt[][]
|
|
80
|
+
parts: OutdoorPart[]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const sf = (m2: number) => m2 * SQFT_PER_SQM
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The part of a slab's area on the lot: a driveway apron drawn out into the
|
|
87
|
+
* right-of-way is the road authority's paving, not the lot's. Whole when
|
|
88
|
+
* every corner is on the lot, else sampled on a fine grid.
|
|
89
|
+
*/
|
|
90
|
+
function onLotArea(ring: readonly Pt[], lot: readonly Pt[]): number {
|
|
91
|
+
const area = polygonArea(ring)
|
|
92
|
+
if (lot.length < 3) return area
|
|
93
|
+
const c = ring.reduce<[number, number]>(
|
|
94
|
+
(s, p) => [s[0] + p[0] / ring.length, s[1] + p[1] / ring.length],
|
|
95
|
+
[0, 0],
|
|
96
|
+
)
|
|
97
|
+
// a corner ON the lot line (the generator's driveway mouth) counts as on the lot
|
|
98
|
+
const onLot = (p: Pt) =>
|
|
99
|
+
pointInPolygon(lot, p[0] + (c[0] - p[0]) * 1e-3, p[1] + (c[1] - p[1]) * 1e-3)
|
|
100
|
+
if (ring.every(onLot)) return area
|
|
101
|
+
const b = polygonBounds(ring)
|
|
102
|
+
const N = 24
|
|
103
|
+
let inside = 0
|
|
104
|
+
let kept = 0
|
|
105
|
+
for (let i = 0; i < N; i++) {
|
|
106
|
+
for (let j = 0; j < N; j++) {
|
|
107
|
+
const x = b.minX + ((i + 0.5) / N) * (b.maxX - b.minX)
|
|
108
|
+
const y = b.minY + ((j + 0.5) / N) * (b.maxY - b.minY)
|
|
109
|
+
if (!pointInPolygon(ring, x, y)) continue
|
|
110
|
+
inside++
|
|
111
|
+
if (pointInPolygon(lot, x, y)) kept++
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return inside > 0 ? (area * kept) / inside : area
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** "12.3 %" — the one way both sheets print a ratio. */
|
|
118
|
+
export function formatCoveragePercent(ratio: number | null): string {
|
|
119
|
+
return ratio === null ? '—' : `${(ratio * 100).toFixed(1)} %`
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** "2,739 SF" — the one way both sheets print an area. */
|
|
123
|
+
export function formatSqFt(sqFt: number): string {
|
|
124
|
+
return `${Math.round(sqFt).toLocaleString('en-US')} SF`
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function computeSiteCoverage(scene: Pick<SceneSnapshot, 'nodes'>): SiteCoverage {
|
|
128
|
+
const snapshot = scene as SceneSnapshot
|
|
129
|
+
const site = findSite(snapshot)
|
|
130
|
+
const building = findBuilding(snapshot, site)
|
|
131
|
+
const ground = findLowestLevel(snapshot, building)
|
|
132
|
+
const storeys = aboveGradeLevels(snapshot, building)
|
|
133
|
+
const levels = storeys.length > 0 ? storeys : ground ? [ground] : []
|
|
134
|
+
|
|
135
|
+
// the footprint: every storey's wall bands, unioned, outer rings only
|
|
136
|
+
const loops = levels.flatMap((level) => levelFootprintLoops(snapshot, level, building))
|
|
137
|
+
const outline = loops.length > 0 ? footprintOutline(loops) : []
|
|
138
|
+
const buildingM2 = outline.reduce((s, ring) => s + polygonArea(ring), 0)
|
|
139
|
+
|
|
140
|
+
const roofs = levels.flatMap((level) => roofOutlineRings(snapshot, level, building))
|
|
141
|
+
const parts = outdoorParts(snapshot, ground, building, outline, roofs)
|
|
142
|
+
const lotRing = (site?.polygon?.points ?? []) as Pt[]
|
|
143
|
+
const sum = (list: readonly OutdoorPart[]) =>
|
|
144
|
+
list.reduce((s, p) => s + onLotArea(p.ring, lotRing), 0)
|
|
145
|
+
const porches = parts.filter((p) => p.kind !== 'driveway' && p.kind !== 'walk')
|
|
146
|
+
const covered = porches.filter((p) => p.covered)
|
|
147
|
+
const open = porches.filter((p) => !p.covered)
|
|
148
|
+
const driveway = parts.filter((p) => p.kind === 'driveway')
|
|
149
|
+
const walks = parts.filter((p) => p.kind === 'walk')
|
|
150
|
+
|
|
151
|
+
const lotPoly = lotRing
|
|
152
|
+
const parcelSqFt =
|
|
153
|
+
typeof site?.parcel?.lotAreaSqFt === 'number' && site.parcel.lotAreaSqFt > 0
|
|
154
|
+
? site.parcel.lotAreaSqFt
|
|
155
|
+
: null
|
|
156
|
+
const polygonSqFt = lotPoly.length >= 3 ? sf(polygonArea(lotPoly)) : 0
|
|
157
|
+
const lotSqFt = parcelSqFt ?? (polygonSqFt > 0 ? polygonSqFt : null)
|
|
158
|
+
const lotSource = parcelSqFt !== null ? 'parcel' : polygonSqFt > 0 ? 'polygon' : null
|
|
159
|
+
|
|
160
|
+
const buildingSqFt = sf(buildingM2)
|
|
161
|
+
const coveredOutdoorSqFt = sf(sum(covered))
|
|
162
|
+
const buildingCoverageSqFt = buildingSqFt + coveredOutdoorSqFt
|
|
163
|
+
const drivewaySqFt = sf(sum(driveway))
|
|
164
|
+
const walksSqFt = sf(sum(walks))
|
|
165
|
+
const openOutdoorSqFt = sf(sum(open))
|
|
166
|
+
const imperviousSqFt = buildingCoverageSqFt + drivewaySqFt + walksSqFt + openOutdoorSqFt
|
|
167
|
+
const ratio = (x: number) =>
|
|
168
|
+
lotSqFt !== null && lotSqFt > 0 && buildingSqFt > 0 ? x / lotSqFt : null
|
|
169
|
+
|
|
170
|
+
const names = (list: readonly OutdoorPart[]) => list.map((p) => p.label.toLowerCase()).join(', ')
|
|
171
|
+
const rows: ImperviousRow[] = [
|
|
172
|
+
{
|
|
173
|
+
key: 'building',
|
|
174
|
+
label: 'BUILDING (INCL. GARAGE)',
|
|
175
|
+
sqFt: buildingSqFt,
|
|
176
|
+
note: 'outside face of the exterior walls',
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
key: 'porches',
|
|
180
|
+
label: 'COVERED PORCHES / PATIOS',
|
|
181
|
+
sqFt: coveredOutdoorSqFt,
|
|
182
|
+
note: covered.length > 0 ? names(covered) : 'none',
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
key: 'driveway',
|
|
186
|
+
label: 'DRIVEWAY',
|
|
187
|
+
sqFt: drivewaySqFt,
|
|
188
|
+
note: driveway.length > 0 ? 'concrete' : 'none drawn',
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
key: 'walks',
|
|
192
|
+
label: 'WALKS',
|
|
193
|
+
sqFt: walksSqFt,
|
|
194
|
+
note: walks.length > 0 ? 'concrete' : 'none drawn',
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
key: 'patios',
|
|
198
|
+
label: 'OPEN PATIOS / DECKS / LANDINGS',
|
|
199
|
+
sqFt: openOutdoorSqFt,
|
|
200
|
+
note: open.length > 0 ? names(open) : 'none',
|
|
201
|
+
},
|
|
202
|
+
]
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
lotSqFt,
|
|
206
|
+
lotSource,
|
|
207
|
+
buildingSqFt,
|
|
208
|
+
coveredOutdoorSqFt,
|
|
209
|
+
buildingCoverageSqFt,
|
|
210
|
+
buildingCoverageRatio: ratio(buildingCoverageSqFt),
|
|
211
|
+
drivewaySqFt,
|
|
212
|
+
walksSqFt,
|
|
213
|
+
openOutdoorSqFt,
|
|
214
|
+
imperviousSqFt,
|
|
215
|
+
imperviousRatio: ratio(imperviousSqFt),
|
|
216
|
+
rows,
|
|
217
|
+
basis: {
|
|
218
|
+
building:
|
|
219
|
+
'Building coverage is the roofed area: the footprint to the outside face of the exterior walls (garage included, every storey projected) plus the roofed porches and patios; eaves excluded.',
|
|
220
|
+
impervious:
|
|
221
|
+
'Impervious surface is the building coverage plus the driveway, walks and the open patios, landings and decks (decks counted — verify the local rule).',
|
|
222
|
+
lot:
|
|
223
|
+
lotSource === 'parcel'
|
|
224
|
+
? 'Lot area is the county parcel record.'
|
|
225
|
+
: lotSource === 'polygon'
|
|
226
|
+
? 'Lot area is the drawn lot polygon (not a survey).'
|
|
227
|
+
: 'No lot area.',
|
|
228
|
+
},
|
|
229
|
+
outline,
|
|
230
|
+
parts,
|
|
231
|
+
}
|
|
232
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { isSitePlanAvailable } from './drawing-type-switch'
|
|
3
|
+
|
|
4
|
+
describe('isSitePlanAvailable', () => {
|
|
5
|
+
test('a site with neither a parcel nor setbacks has no site plan', () => {
|
|
6
|
+
expect(isSitePlanAvailable(null, false)).toBe(false)
|
|
7
|
+
expect(isSitePlanAvailable({}, false)).toBe(false)
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
test('a resolved lot or a contributing plugin offers it', () => {
|
|
11
|
+
expect(isSitePlanAvailable({ parcel: { apn: '06075' } }, false)).toBe(true)
|
|
12
|
+
expect(isSitePlanAvailable({ setbacks: { front: 6, side: 1.5, rear: 3 } }, false)).toBe(true)
|
|
13
|
+
expect(isSitePlanAvailable({}, true)).toBe(true)
|
|
14
|
+
})
|
|
15
|
+
})
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { type SiteNode, useScene } from '@pascal-app/core'
|
|
4
|
+
import { useEffect, useSyncExternalStore } from 'react'
|
|
5
|
+
import useDrawingView, { EDITOR_DRAWING_TYPE_OPTIONS } from '../../../store/use-drawing-view'
|
|
6
|
+
import { cn } from '../../utils'
|
|
7
|
+
import { hasSitePlanContributors, subscribeSitePlanContributors } from './contributors'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The site plan only has something to draw for a resolved lot (a parcel or
|
|
11
|
+
* setbacks on the site) or when a plugin contributes site-plan content; every
|
|
12
|
+
* other scene would get the default 30 m square.
|
|
13
|
+
*/
|
|
14
|
+
export function isSitePlanAvailable(
|
|
15
|
+
site: Pick<SiteNode, 'parcel' | 'setbacks'> | null,
|
|
16
|
+
pluginContributes: boolean,
|
|
17
|
+
): boolean {
|
|
18
|
+
return pluginContributes || Boolean(site?.parcel || site?.setbacks)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function useSitePlanAvailable(): boolean {
|
|
22
|
+
const pluginContributes = useSyncExternalStore(
|
|
23
|
+
subscribeSitePlanContributors,
|
|
24
|
+
hasSitePlanContributors,
|
|
25
|
+
hasSitePlanContributors,
|
|
26
|
+
)
|
|
27
|
+
const siteHasLot = useScene((state) => {
|
|
28
|
+
for (const id of state.rootNodeIds) {
|
|
29
|
+
const node = state.nodes[id]
|
|
30
|
+
if (node?.type === 'site') return isSitePlanAvailable(node as SiteNode, false)
|
|
31
|
+
}
|
|
32
|
+
return false
|
|
33
|
+
})
|
|
34
|
+
return pluginContributes || siteHasLot
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Floor plan ⇄ Site plan switch for the 2D editor.
|
|
39
|
+
*
|
|
40
|
+
* Sits with the other floating plan controls in the floor-plan viewport
|
|
41
|
+
* (bottom-left, beside the compass). Theme tokens only. Hidden, and a
|
|
42
|
+
* persisted site-plan choice reset to the floor plan, when the scene has no
|
|
43
|
+
* site plan to show.
|
|
44
|
+
*/
|
|
45
|
+
export function FloorplanDrawingTypeSwitch({ className }: { className?: string }) {
|
|
46
|
+
const drawingType = useDrawingView((s) => s.drawingType)
|
|
47
|
+
const setDrawingType = useDrawingView((s) => s.setDrawingType)
|
|
48
|
+
const available = useSitePlanAvailable()
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (!available && drawingType === 'site-plan') setDrawingType('floor-plan')
|
|
52
|
+
}, [available, drawingType, setDrawingType])
|
|
53
|
+
|
|
54
|
+
if (!available) return null
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div
|
|
58
|
+
className={cn(
|
|
59
|
+
'pointer-events-auto absolute bottom-3 left-14 z-30 flex items-center gap-0.5 rounded-full border border-border bg-card/90 p-0.5 shadow-sm backdrop-blur-md',
|
|
60
|
+
className,
|
|
61
|
+
)}
|
|
62
|
+
>
|
|
63
|
+
{EDITOR_DRAWING_TYPE_OPTIONS.map((option) => (
|
|
64
|
+
<button
|
|
65
|
+
aria-pressed={drawingType === option.id}
|
|
66
|
+
className={cn(
|
|
67
|
+
'rounded-full px-2.5 py-1 font-medium text-xs transition',
|
|
68
|
+
drawingType === option.id
|
|
69
|
+
? 'bg-primary text-primary-foreground'
|
|
70
|
+
: 'text-muted-foreground hover:text-foreground',
|
|
71
|
+
)}
|
|
72
|
+
key={option.id}
|
|
73
|
+
onClick={(event) => {
|
|
74
|
+
event.preventDefault()
|
|
75
|
+
event.stopPropagation()
|
|
76
|
+
setDrawingType(option.id)
|
|
77
|
+
}}
|
|
78
|
+
onPointerDown={(event) => event.stopPropagation()}
|
|
79
|
+
type="button"
|
|
80
|
+
>
|
|
81
|
+
{option.label}
|
|
82
|
+
</button>
|
|
83
|
+
))}
|
|
84
|
+
</div>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { detectFrontEdgeFromRoads, streetCore } from './front-edge'
|
|
3
|
+
import type { Pt } from './geometry'
|
|
4
|
+
|
|
5
|
+
// 20 m × 30 m lot centred on the origin, y south. Edge 0 = north side,
|
|
6
|
+
// 1 = east, 2 = south, 3 = west.
|
|
7
|
+
const LOT: Pt[] = [
|
|
8
|
+
[-10, -15],
|
|
9
|
+
[10, -15],
|
|
10
|
+
[10, 15],
|
|
11
|
+
[-10, 15],
|
|
12
|
+
]
|
|
13
|
+
const road = (name: string, ...pts: Pt[]) => ({ name, centerline: pts })
|
|
14
|
+
|
|
15
|
+
describe('streetCore', () => {
|
|
16
|
+
test('drops the number, the direction and the suffix', () => {
|
|
17
|
+
expect(streetCore('2600 Castro Way')).toBe('castro')
|
|
18
|
+
expect(streetCore('S Castro')).toBe('castro')
|
|
19
|
+
expect(streetCore('W Cass St.')).toBe('cass')
|
|
20
|
+
expect(streetCore('1200 W Cass St')).toBe('cass')
|
|
21
|
+
expect(streetCore('North Troy Street')).toBe('troy')
|
|
22
|
+
// OSM spells the quadrant out
|
|
23
|
+
expect(streetCore('Northeast 109th Street')).toBe('109th')
|
|
24
|
+
expect(streetCore('1247 NE 109th St')).toBe('109th')
|
|
25
|
+
expect(streetCore('')).toBe('')
|
|
26
|
+
expect(streetCore(null)).toBe('')
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
describe('detectFrontEdgeFromRoads', () => {
|
|
31
|
+
test('the nearest parallel street on the outside picks its edge', () => {
|
|
32
|
+
const roads = [road('Castro Way', [-60, -25], [60, -25])]
|
|
33
|
+
const m = detectFrontEdgeFromRoads(LOT, roads)
|
|
34
|
+
expect(m?.index).toBe(0)
|
|
35
|
+
expect(m?.name).toBe('Castro Way')
|
|
36
|
+
expect(m?.distance).toBeCloseTo(10, 9)
|
|
37
|
+
expect(m?.named).toBe(false)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('a road perpendicular to an edge never fronts it', () => {
|
|
41
|
+
// runs north-south past the middle of the north edge, 25 m out
|
|
42
|
+
const roads = [road('Stab St', [0, -60], [0, -25])]
|
|
43
|
+
expect(detectFrontEdgeFromRoads(LOT, roads)).toBeNull()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('a road through the lot (inside the edge) is rejected', () => {
|
|
47
|
+
const roads = [road('Driveway', [-60, -5], [60, -5])]
|
|
48
|
+
expect(detectFrontEdgeFromRoads(LOT, roads)).toBeNull()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('corner lot: the addressed street wins over a closer cross street', () => {
|
|
52
|
+
const roads = [
|
|
53
|
+
road('Castro Way', [-60, -30], [60, -30]), // north, 15 m off
|
|
54
|
+
road('Elm St', [18, -60], [18, 60]), // east, 8 m off — closer
|
|
55
|
+
]
|
|
56
|
+
expect(detectFrontEdgeFromRoads(LOT, roads)?.index).toBe(1) // closest wins with no address
|
|
57
|
+
const named = detectFrontEdgeFromRoads(LOT, roads, '2600 Castro Way')
|
|
58
|
+
expect(named?.index).toBe(0)
|
|
59
|
+
expect(named?.named).toBe(true)
|
|
60
|
+
// an address on a street that is not mapped falls back to the closest
|
|
61
|
+
expect(detectFrontEdgeFromRoads(LOT, roads, '9 Nowhere Ln')?.index).toBe(1)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('through lot: a street of the same number but another type is not the addressed street', () => {
|
|
65
|
+
const roads = [
|
|
66
|
+
road('Northwest 34th Terrace', [-18, -60], [-18, 60]), // west, 8 m off — closer
|
|
67
|
+
road('Northwest 34th Street', [25, -60], [25, 60]), // east, 15 m off
|
|
68
|
+
]
|
|
69
|
+
const m = detectFrontEdgeFromRoads(LOT, roads, '4121 NW 34th St')
|
|
70
|
+
expect(m?.index).toBe(1)
|
|
71
|
+
expect(m?.name).toBe('Northwest 34th Street')
|
|
72
|
+
expect(m?.named).toBe(true)
|
|
73
|
+
expect(m?.streetNames).toEqual({ '1': 'Northwest 34th Street', '3': 'Northwest 34th Terrace' })
|
|
74
|
+
// addressed on the Terrace, the Terrace fronts it
|
|
75
|
+
expect(detectFrontEdgeFromRoads(LOT, roads, '4120 NW 34th Ter')?.index).toBe(3)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('a bent road counts by its nearest segment; slight skew within 30° passes', () => {
|
|
79
|
+
const roads = [road('Bend Rd', [-80, -40], [-30, -26], [30, -24], [80, -40])]
|
|
80
|
+
expect(detectFrontEdgeFromRoads(LOT, roads)?.index).toBe(0)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('winding does not matter', () => {
|
|
84
|
+
const cw = [...LOT].reverse() as Pt[]
|
|
85
|
+
const roads = [road('Castro Way', [-60, -25], [60, -25])]
|
|
86
|
+
const m = detectFrontEdgeFromRoads(cw, roads)
|
|
87
|
+
// reversed ring: the north edge is now points[2] → points[3]
|
|
88
|
+
expect(m?.index).toBe(2)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('no roads, no answer', () => {
|
|
92
|
+
expect(detectFrontEdgeFromRoads(LOT, [])).toBeNull()
|
|
93
|
+
expect(
|
|
94
|
+
detectFrontEdgeFromRoads(
|
|
95
|
+
[
|
|
96
|
+
[0, 0],
|
|
97
|
+
[1, 0],
|
|
98
|
+
],
|
|
99
|
+
[road('x', [0, -5], [1, -5])],
|
|
100
|
+
),
|
|
101
|
+
).toBeNull()
|
|
102
|
+
})
|
|
103
|
+
})
|