@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,379 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the site plan reads off the scene — the site, the building on it, its
|
|
3
|
+
* storeys, the walls' outline, the roof's outline and the outdoor slabs
|
|
4
|
+
* (porches, decks, patios, and the flatwork: driveway and walks) — in SITE
|
|
5
|
+
* metres. Shared by the drawing (`build-site-plan-drawing.ts`) and the
|
|
6
|
+
* coverage / impervious-area figures (`coverage.ts`) so both read the same
|
|
7
|
+
* parts. Pure: no store.
|
|
8
|
+
*/
|
|
9
|
+
import {
|
|
10
|
+
type AnyNode,
|
|
11
|
+
type AnyNodeId,
|
|
12
|
+
type BuildingNode,
|
|
13
|
+
calculateLevelMiters,
|
|
14
|
+
getWallPlanFootprint,
|
|
15
|
+
type LevelNode,
|
|
16
|
+
migrateSiteMetadata,
|
|
17
|
+
type SceneSnapshot,
|
|
18
|
+
type SiteNode,
|
|
19
|
+
unionPolygons,
|
|
20
|
+
type WallNode,
|
|
21
|
+
} from '@pascal-app/core'
|
|
22
|
+
import { type Pt, pointInPolygon, polygonArea, polygonBounds } from './geometry'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The scene's site node, with legacy `metadata.setbacks / zone / apn` lifted
|
|
26
|
+
* onto the real fields. This is a READ-side lift only — nothing is written
|
|
27
|
+
* back to the store, so a scene that was never re-saved keeps its metadata.
|
|
28
|
+
* A persistence-side migration would belong in
|
|
29
|
+
* `packages/core/src/utils/scene-migrations.ts` (not owned by this
|
|
30
|
+
* workstream); see the note in `migrateSiteMetadata`.
|
|
31
|
+
*/
|
|
32
|
+
export function findSite(scene: SceneSnapshot): SiteNode | null {
|
|
33
|
+
for (const node of Object.values(scene.nodes)) {
|
|
34
|
+
if (node.type !== 'site') continue
|
|
35
|
+
const site = node as SiteNode
|
|
36
|
+
const patch = migrateSiteMetadata(site)
|
|
37
|
+
return Object.keys(patch).length > 0 ? { ...site, ...patch } : site
|
|
38
|
+
}
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function findBuilding(scene: SceneSnapshot, site: SiteNode | null): BuildingNode | null {
|
|
43
|
+
if (site) {
|
|
44
|
+
for (const childId of site.children ?? []) {
|
|
45
|
+
const child = scene.nodes[childId as AnyNodeId]
|
|
46
|
+
if (child?.type === 'building') return child as BuildingNode
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
for (const node of Object.values(scene.nodes)) {
|
|
50
|
+
if (node.type === 'building') return node as BuildingNode
|
|
51
|
+
}
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Lowest `level` number among the building's level children. */
|
|
56
|
+
export function findLowestLevel(
|
|
57
|
+
scene: SceneSnapshot,
|
|
58
|
+
building: BuildingNode | null,
|
|
59
|
+
): LevelNode | null {
|
|
60
|
+
if (!building) return null
|
|
61
|
+
let best: LevelNode | null = null
|
|
62
|
+
for (const childId of building.children ?? []) {
|
|
63
|
+
const child = scene.nodes[childId as AnyNodeId] as AnyNode | undefined
|
|
64
|
+
if (child?.type !== 'level') continue
|
|
65
|
+
const level = child as LevelNode
|
|
66
|
+
if (!best || level.level < best.level) best = level
|
|
67
|
+
}
|
|
68
|
+
return best
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Per-wall plan footprints for a level, mitred, then transformed by the
|
|
73
|
+
* building's site placement. `position` is `[x, y, z]` in site metres (y is
|
|
74
|
+
* height, ignored in plan); `rotation[1]` is the yaw in radians.
|
|
75
|
+
*/
|
|
76
|
+
export function levelFootprintLoops(
|
|
77
|
+
scene: SceneSnapshot,
|
|
78
|
+
level: LevelNode | null,
|
|
79
|
+
building: BuildingNode | null,
|
|
80
|
+
): Pt[][] {
|
|
81
|
+
if (!level) return []
|
|
82
|
+
const walls: WallNode[] = []
|
|
83
|
+
for (const childId of level.children ?? []) {
|
|
84
|
+
const child = scene.nodes[childId as AnyNodeId] as AnyNode | undefined
|
|
85
|
+
if (child?.type === 'wall') walls.push(child as WallNode)
|
|
86
|
+
}
|
|
87
|
+
if (walls.length === 0) return []
|
|
88
|
+
|
|
89
|
+
const miters = calculateLevelMiters(walls)
|
|
90
|
+
const yaw = building?.rotation?.[1] ?? 0
|
|
91
|
+
const cos = Math.cos(yaw)
|
|
92
|
+
const sin = Math.sin(yaw)
|
|
93
|
+
const ox = building?.position?.[0] ?? 0
|
|
94
|
+
const oz = building?.position?.[2] ?? 0
|
|
95
|
+
|
|
96
|
+
const loops: Pt[][] = []
|
|
97
|
+
for (const wall of walls) {
|
|
98
|
+
const poly = getWallPlanFootprint(wall, miters)
|
|
99
|
+
if (poly.length < 3) continue
|
|
100
|
+
// three.js Y rotation, the convention BuildingRenderer applies:
|
|
101
|
+
// world = (cos·lx + sin·lz, −sin·lx + cos·lz) + position
|
|
102
|
+
loops.push(poly.map((p) => [ox + p.x * cos + p.y * sin, oz - p.x * sin + p.y * cos] as Pt))
|
|
103
|
+
}
|
|
104
|
+
return loops
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The outer ring(s) of a set of wall bands: their union, minus any ring that
|
|
109
|
+
* lies inside another (a room enclosed by partitions is a hole in the union,
|
|
110
|
+
* not a second building). Falls back to the bands themselves when the union
|
|
111
|
+
* yields nothing.
|
|
112
|
+
*/
|
|
113
|
+
export function footprintOutline(loops: readonly Pt[][]): Pt[][] {
|
|
114
|
+
const rings = unionPolygons(loops.map((loop) => loop.map((p) => [p[0], p[1]]))) as Pt[][]
|
|
115
|
+
if (rings.length === 0) return [...loops]
|
|
116
|
+
const inside = (p: Pt, ring: Pt[]): boolean => {
|
|
117
|
+
let hit = false
|
|
118
|
+
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
|
|
119
|
+
const a = ring[i] as Pt
|
|
120
|
+
const b = ring[j] as Pt
|
|
121
|
+
if (
|
|
122
|
+
a[1] > p[1] !== b[1] > p[1] &&
|
|
123
|
+
p[0] < ((b[0] - a[0]) * (p[1] - a[1])) / (b[1] - a[1]) + a[0]
|
|
124
|
+
) {
|
|
125
|
+
hit = !hit
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return hit
|
|
129
|
+
}
|
|
130
|
+
return rings.filter(
|
|
131
|
+
(ring, i) =>
|
|
132
|
+
!rings.some((other, j) => j !== i && ring[0] !== undefined && inside(ring[0], other)),
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** A three.js Y rotation of a plan point: local (x, z) turned by `yaw`. */
|
|
137
|
+
export function turn(x: number, z: number, yaw: number): Pt {
|
|
138
|
+
const c = Math.cos(yaw)
|
|
139
|
+
const s = Math.sin(yaw)
|
|
140
|
+
return [c * x + s * z, -s * x + c * z]
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function yawOf(rotation: unknown): number {
|
|
144
|
+
if (typeof rotation === 'number') return rotation
|
|
145
|
+
if (Array.isArray(rotation)) return Number(rotation[1] ?? 0) || 0
|
|
146
|
+
return 0
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Level-local plan (x, z) → site metres, through the building's placement (see `levelFootprintLoops`). */
|
|
150
|
+
export function siteFrame(building: BuildingNode | null): (x: number, z: number) => Pt {
|
|
151
|
+
const yaw = building?.rotation?.[1] ?? 0
|
|
152
|
+
const ox = building?.position?.[0] ?? 0
|
|
153
|
+
const oz = building?.position?.[2] ?? 0
|
|
154
|
+
return (x, z) => {
|
|
155
|
+
const [tx, tz] = turn(x, z, yaw)
|
|
156
|
+
return [ox + tx, oz + tz]
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The roof's outline on the lot — every roof segment's plan rectangle plus
|
|
162
|
+
* its overhang, through the segment's, the roof's and the building's turns,
|
|
163
|
+
* unioned into the outer ring(s). Dashed on the site plan: what the eye sees
|
|
164
|
+
* from above is the roof, and the setback is measured to the wall under it.
|
|
165
|
+
*/
|
|
166
|
+
export function roofOutlineRings(
|
|
167
|
+
scene: SceneSnapshot,
|
|
168
|
+
level: LevelNode | null,
|
|
169
|
+
building: BuildingNode | null,
|
|
170
|
+
): Pt[][] {
|
|
171
|
+
if (!level) return []
|
|
172
|
+
const toSite = siteFrame(building)
|
|
173
|
+
const roofs = new Map<string, { position?: number[]; rotation?: unknown }>()
|
|
174
|
+
for (const childId of level.children ?? []) {
|
|
175
|
+
const child = scene.nodes[childId as AnyNodeId] as AnyNode | undefined
|
|
176
|
+
if (child?.type === 'roof')
|
|
177
|
+
roofs.set(String(child.id), child as { position?: number[]; rotation?: unknown })
|
|
178
|
+
}
|
|
179
|
+
if (roofs.size === 0) return []
|
|
180
|
+
const rects: Pt[][] = []
|
|
181
|
+
for (const node of Object.values(scene.nodes)) {
|
|
182
|
+
if (node.type !== 'roof-segment') continue
|
|
183
|
+
const roof = roofs.get(String((node as { parentId?: unknown }).parentId))
|
|
184
|
+
if (!roof) continue
|
|
185
|
+
const seg = node as unknown as {
|
|
186
|
+
position?: number[]
|
|
187
|
+
rotation?: unknown
|
|
188
|
+
width?: number
|
|
189
|
+
depth?: number
|
|
190
|
+
overhang?: number
|
|
191
|
+
}
|
|
192
|
+
const w = (seg.width ?? 0) / 2 + (seg.overhang ?? 0)
|
|
193
|
+
const d = (seg.depth ?? 0) / 2 + (seg.overhang ?? 0)
|
|
194
|
+
if (w <= 0 || d <= 0) continue
|
|
195
|
+
const segYaw = yawOf(seg.rotation)
|
|
196
|
+
const roofYaw = yawOf(roof.rotation)
|
|
197
|
+
const sx = seg.position?.[0] ?? 0
|
|
198
|
+
const sz = seg.position?.[2] ?? 0
|
|
199
|
+
const rx = roof.position?.[0] ?? 0
|
|
200
|
+
const rz = roof.position?.[2] ?? 0
|
|
201
|
+
const local: Pt[] = [
|
|
202
|
+
[-w, -d],
|
|
203
|
+
[w, -d],
|
|
204
|
+
[w, d],
|
|
205
|
+
[-w, d],
|
|
206
|
+
]
|
|
207
|
+
rects.push(
|
|
208
|
+
local.map(([lx, lz]) => {
|
|
209
|
+
const [ax, az] = turn(lx, lz, segYaw)
|
|
210
|
+
const [bx, bz] = turn(ax + sx, az + sz, roofYaw)
|
|
211
|
+
return toSite(bx + rx, bz + rz)
|
|
212
|
+
}),
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
if (rects.length === 0) return []
|
|
216
|
+
const rings = unionPolygons(rects.map((r) => r.map((p) => [p[0], p[1]]))) as Pt[][]
|
|
217
|
+
return rings.length > 0 ? rings : rects
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Every storey of the building at or above grade (level ≥ 0), lowest first. */
|
|
221
|
+
export function aboveGradeLevels(scene: SceneSnapshot, building: BuildingNode | null): LevelNode[] {
|
|
222
|
+
if (!building) return []
|
|
223
|
+
const out: LevelNode[] = []
|
|
224
|
+
for (const childId of building.children ?? []) {
|
|
225
|
+
const child = scene.nodes[childId as AnyNodeId] as AnyNode | undefined
|
|
226
|
+
if (child?.type === 'level' && (child as LevelNode).level >= 0) out.push(child as LevelNode)
|
|
227
|
+
}
|
|
228
|
+
return out.sort((a, b) => a.level - b.level)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/* ------------------------------------------------------ outdoor slabs */
|
|
232
|
+
|
|
233
|
+
/** What an outdoor slab is on the site plan and in the impervious table. */
|
|
234
|
+
export type OutdoorKind = 'porch' | 'deck' | 'patio' | 'landing' | 'driveway' | 'walk'
|
|
235
|
+
|
|
236
|
+
export interface OutdoorPart {
|
|
237
|
+
id: string
|
|
238
|
+
kind: OutdoorKind
|
|
239
|
+
/** Upper-case label for the plan ("PORCH", "REAR DECK", "DRIVEWAY"). */
|
|
240
|
+
label: string
|
|
241
|
+
ring: Pt[]
|
|
242
|
+
/** Plan area, m². */
|
|
243
|
+
area: number
|
|
244
|
+
/** Under a roof (≥ half its area inside a roof outline): building coverage, not open paving. */
|
|
245
|
+
covered: boolean
|
|
246
|
+
/** Wood decking (a deck): open boards over grade. */
|
|
247
|
+
wood: boolean
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Flatwork: a slab that is site paving, not part of the building — the
|
|
252
|
+
* generator's driveway and walks (`metadata.flatwork`), and a hand-drawn slab
|
|
253
|
+
* named like one (the floor plans leave the same slabs out).
|
|
254
|
+
*/
|
|
255
|
+
const HARDSCAPE =
|
|
256
|
+
/driveway|drive\s*way|\bdrive\b|approach|walkway|sidewalk|\bwalk\b|\bpath\b|apron|parking|\bpavers?\b/i
|
|
257
|
+
export function flatworkKindOf(node: {
|
|
258
|
+
type?: string
|
|
259
|
+
name?: unknown
|
|
260
|
+
metadata?: unknown
|
|
261
|
+
}): 'driveway' | 'walk' | null {
|
|
262
|
+
if (node.type !== 'slab') return null
|
|
263
|
+
const meta = (node.metadata ?? {}) as { flatwork?: unknown; floor?: unknown }
|
|
264
|
+
if (meta.flatwork === 'driveway' || meta.flatwork === 'walk') return meta.flatwork
|
|
265
|
+
const name = typeof node.name === 'string' ? node.name : ''
|
|
266
|
+
const floor = typeof meta.floor === 'string' ? meta.floor : ''
|
|
267
|
+
const text = `${name} ${floor}`
|
|
268
|
+
if (!HARDSCAPE.test(text)) return null
|
|
269
|
+
return /drive|approach|apron|parking/i.test(text) ? 'driveway' : 'walk'
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** The house's own floors: the storey slab, a raised platform, the garage pad, an upper floor. */
|
|
273
|
+
const HOUSE_FLOORS = new Set(['slab-on-grade', 'platform', 'garage-slab-at-grade', 'floor'])
|
|
274
|
+
|
|
275
|
+
/** Share of `ring`'s area inside any of `covers` (a 6 × 6 sample grid). */
|
|
276
|
+
function coveredShare(ring: readonly Pt[], covers: readonly Pt[][]): number {
|
|
277
|
+
if (covers.length === 0) return 0
|
|
278
|
+
const b = polygonBounds(ring)
|
|
279
|
+
let inside = 0
|
|
280
|
+
let under = 0
|
|
281
|
+
const N = 6
|
|
282
|
+
for (let i = 0; i < N; i++) {
|
|
283
|
+
for (let j = 0; j < N; j++) {
|
|
284
|
+
const x = b.minX + ((i + 0.5) / N) * (b.maxX - b.minX)
|
|
285
|
+
const y = b.minY + ((j + 0.5) / N) * (b.maxY - b.minY)
|
|
286
|
+
if (!pointInPolygon(ring, x, y)) continue
|
|
287
|
+
inside++
|
|
288
|
+
if (covers.some((c) => pointInPolygon(c, x, y))) under++
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return inside > 0 ? under / inside : 0
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* The level's outdoor slabs in site metres: porches, decks, patios and
|
|
296
|
+
* landings (a generator `metadata.porch`, or a deck / porch-slab floor, or a
|
|
297
|
+
* slab lying outside the house's walls) and the flatwork. The house's own
|
|
298
|
+
* floors (the storey slab, a raised platform, the garage pad) and a porch
|
|
299
|
+
* cover's beam are not outdoor parts. `roofs` decides which are covered.
|
|
300
|
+
*/
|
|
301
|
+
export function outdoorParts(
|
|
302
|
+
scene: SceneSnapshot,
|
|
303
|
+
level: LevelNode | null,
|
|
304
|
+
building: BuildingNode | null,
|
|
305
|
+
walls: readonly Pt[][],
|
|
306
|
+
roofs: readonly Pt[][],
|
|
307
|
+
): OutdoorPart[] {
|
|
308
|
+
if (!level) return []
|
|
309
|
+
const toSite = siteFrame(building)
|
|
310
|
+
const out: OutdoorPart[] = []
|
|
311
|
+
for (const childId of level.children ?? []) {
|
|
312
|
+
const child = scene.nodes[childId as AnyNodeId] as AnyNode | undefined
|
|
313
|
+
if (child?.type !== 'slab' || child.visible === false) continue
|
|
314
|
+
const slab = child as unknown as {
|
|
315
|
+
id: string
|
|
316
|
+
polygon?: number[][]
|
|
317
|
+
metadata?: Record<string, unknown>
|
|
318
|
+
name?: string
|
|
319
|
+
}
|
|
320
|
+
const poly = slab.polygon ?? []
|
|
321
|
+
if (poly.length < 3) continue
|
|
322
|
+
const ring = poly.map((p) => toSite(p[0] ?? 0, p[1] ?? 0))
|
|
323
|
+
const area = polygonArea(ring)
|
|
324
|
+
const meta = slab.metadata ?? {}
|
|
325
|
+
const floor = typeof meta.floor === 'string' ? meta.floor : ''
|
|
326
|
+
const flat = flatworkKindOf(child as never)
|
|
327
|
+
if (flat) {
|
|
328
|
+
out.push({
|
|
329
|
+
id: slab.id,
|
|
330
|
+
kind: flat,
|
|
331
|
+
label: flat === 'driveway' ? 'DRIVEWAY' : 'WALK',
|
|
332
|
+
ring,
|
|
333
|
+
area,
|
|
334
|
+
covered: false,
|
|
335
|
+
wood: false,
|
|
336
|
+
})
|
|
337
|
+
continue
|
|
338
|
+
}
|
|
339
|
+
// a porch cover's beam box is not a floor
|
|
340
|
+
if (/beam/i.test(floor) || /beam/i.test(slab.name ?? '')) continue
|
|
341
|
+
const porch = meta.porch as { policy?: string; entrance?: string } | undefined
|
|
342
|
+
const outdoorFloor = floor === 'deck' || floor === 'porch-slab'
|
|
343
|
+
if (!porch && !outdoorFloor) {
|
|
344
|
+
// a hand-drawn slab: the house's floor when it lies under the walls
|
|
345
|
+
if (HOUSE_FLOORS.has(floor)) continue
|
|
346
|
+
const c = ring.reduce<[number, number]>(
|
|
347
|
+
(s, p) => [s[0] + p[0] / ring.length, s[1] + p[1] / ring.length],
|
|
348
|
+
[0, 0],
|
|
349
|
+
)
|
|
350
|
+
if (walls.length === 0 || walls.some((w) => pointInPolygon(w, c[0], c[1]))) continue
|
|
351
|
+
}
|
|
352
|
+
const wood = floor === 'deck'
|
|
353
|
+
const policy = porch?.policy ?? ''
|
|
354
|
+
const kind: OutdoorKind =
|
|
355
|
+
policy === 'deck' || (wood && policy !== 'full' && policy !== 'entry')
|
|
356
|
+
? 'deck'
|
|
357
|
+
: policy === 'landing'
|
|
358
|
+
? 'landing'
|
|
359
|
+
: policy === 'patio'
|
|
360
|
+
? 'patio'
|
|
361
|
+
: 'porch'
|
|
362
|
+
const label =
|
|
363
|
+
porch?.policy === 'entry'
|
|
364
|
+
? 'PORCH'
|
|
365
|
+
: porch?.policy === 'landing'
|
|
366
|
+
? 'LANDING'
|
|
367
|
+
: (slab.name ?? kind).toUpperCase()
|
|
368
|
+
out.push({
|
|
369
|
+
id: slab.id,
|
|
370
|
+
kind,
|
|
371
|
+
label,
|
|
372
|
+
ring,
|
|
373
|
+
area,
|
|
374
|
+
covered: coveredShare(ring, roofs) >= 0.5,
|
|
375
|
+
wood,
|
|
376
|
+
})
|
|
377
|
+
}
|
|
378
|
+
return out
|
|
379
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type AnyNodeId,
|
|
5
|
+
type BuildingNode,
|
|
6
|
+
pauseSceneHistory,
|
|
7
|
+
resumeSceneHistory,
|
|
8
|
+
useScene,
|
|
9
|
+
} from '@pascal-app/core'
|
|
10
|
+
import { type PointerEvent as ReactPointerEvent, useCallback, useMemo, useRef } from 'react'
|
|
11
|
+
import { FloorplanGeometryRenderer } from '../../../components/editor-2d/renderers/floorplan-geometry-renderer'
|
|
12
|
+
import { clientToPlan } from '../plan-coords'
|
|
13
|
+
import { buildSitePlanDrawing } from './build-site-plan-drawing'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Site-plan view of the 2D editor.
|
|
17
|
+
*
|
|
18
|
+
* Mounted INSTEAD of `<FloorplanRegistryLayer>` when
|
|
19
|
+
* `useDrawingView().drawingType === 'site-plan'`, inside the same scene `<g>`,
|
|
20
|
+
* so the coordinates it draws in (site metres, origin = the geocoded point,
|
|
21
|
+
* x east, y south) are the ones `clientToPlan` reports back.
|
|
22
|
+
*
|
|
23
|
+
* Everything is recomputed from the scene snapshot on each store change, so
|
|
24
|
+
* the lot line, envelope, footprint and yard dimensions are live.
|
|
25
|
+
*/
|
|
26
|
+
export function FloorplanSitePlanLayer() {
|
|
27
|
+
// Subscribing to the whole node map is what the site plan actually depends
|
|
28
|
+
// on (walls, building placement, site fields); the drawing is cheap enough
|
|
29
|
+
// (tens of primitives) that memoising on the map identity is sufficient.
|
|
30
|
+
const nodes = useScene((s) => s.nodes)
|
|
31
|
+
|
|
32
|
+
const drawing = useMemo(() => {
|
|
33
|
+
const state = useScene.getState()
|
|
34
|
+
return buildSitePlanDrawing({
|
|
35
|
+
nodes,
|
|
36
|
+
rootNodeIds: state.rootNodeIds,
|
|
37
|
+
collections: state.collections,
|
|
38
|
+
materials: state.materials,
|
|
39
|
+
installedPlugins: state.installedPlugins,
|
|
40
|
+
})
|
|
41
|
+
}, [nodes])
|
|
42
|
+
|
|
43
|
+
const dragRef = useRef<{
|
|
44
|
+
id: AnyNodeId
|
|
45
|
+
pointerId: number
|
|
46
|
+
startPlan: [number, number]
|
|
47
|
+
startPosition: [number, number, number]
|
|
48
|
+
} | null>(null)
|
|
49
|
+
|
|
50
|
+
const buildingId = drawing.meta.buildingId
|
|
51
|
+
|
|
52
|
+
const handlePointerDown = useCallback(
|
|
53
|
+
(event: ReactPointerEvent<SVGGElement>) => {
|
|
54
|
+
if (!buildingId || event.button !== 0) return
|
|
55
|
+
const building = useScene.getState().nodes[buildingId] as BuildingNode | undefined
|
|
56
|
+
if (!building) return
|
|
57
|
+
const startPlan = clientToPlan(event.clientX, event.clientY)
|
|
58
|
+
if (!startPlan) return
|
|
59
|
+
event.preventDefault()
|
|
60
|
+
event.stopPropagation()
|
|
61
|
+
event.currentTarget.setPointerCapture(event.pointerId)
|
|
62
|
+
dragRef.current = {
|
|
63
|
+
id: buildingId,
|
|
64
|
+
pointerId: event.pointerId,
|
|
65
|
+
startPlan: [startPlan[0], startPlan[1]],
|
|
66
|
+
startPosition: [...building.position] as [number, number, number],
|
|
67
|
+
}
|
|
68
|
+
// One history entry for the whole drag, committed on pointer-up.
|
|
69
|
+
pauseSceneHistory(useScene)
|
|
70
|
+
},
|
|
71
|
+
[buildingId],
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
const handlePointerMove = useCallback((event: ReactPointerEvent<SVGGElement>) => {
|
|
75
|
+
const drag = dragRef.current
|
|
76
|
+
if (!drag || drag.pointerId !== event.pointerId) return
|
|
77
|
+
const plan = clientToPlan(event.clientX, event.clientY)
|
|
78
|
+
if (!plan) return
|
|
79
|
+
const dx = plan[0] - drag.startPlan[0]
|
|
80
|
+
const dz = plan[1] - drag.startPlan[1]
|
|
81
|
+
useScene.getState().updateNode(drag.id, {
|
|
82
|
+
position: [drag.startPosition[0] + dx, drag.startPosition[1], drag.startPosition[2] + dz],
|
|
83
|
+
})
|
|
84
|
+
}, [])
|
|
85
|
+
|
|
86
|
+
const endDrag = useCallback((event: ReactPointerEvent<SVGGElement>) => {
|
|
87
|
+
const drag = dragRef.current
|
|
88
|
+
if (!drag || drag.pointerId !== event.pointerId) return
|
|
89
|
+
dragRef.current = null
|
|
90
|
+
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
|
|
91
|
+
event.currentTarget.releasePointerCapture(event.pointerId)
|
|
92
|
+
}
|
|
93
|
+
resumeSceneHistory(useScene)
|
|
94
|
+
}, [])
|
|
95
|
+
|
|
96
|
+
const footprintIndexes = new Set<number>()
|
|
97
|
+
drawing.primitives.forEach((p, i) => {
|
|
98
|
+
const meta = (p as { metadata?: Record<string, unknown> }).metadata
|
|
99
|
+
if (meta?.sitePlan === 'building-footprint') footprintIndexes.add(i)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<g data-site-plan="">
|
|
104
|
+
{drawing.primitives.map((geometry, index) =>
|
|
105
|
+
footprintIndexes.has(index) ? null : (
|
|
106
|
+
<FloorplanGeometryRenderer
|
|
107
|
+
geometry={geometry}
|
|
108
|
+
// eslint-disable-next-line react/no-array-index-key -- primitives are positional, rebuilt wholesale each render
|
|
109
|
+
key={`site-plan-${index}`}
|
|
110
|
+
/>
|
|
111
|
+
),
|
|
112
|
+
)}
|
|
113
|
+
{/* Footprint bands live in their own group so the whole building drags
|
|
114
|
+
as one target, writing `building.position` — the same field the 3D
|
|
115
|
+
move tool writes. */}
|
|
116
|
+
<g
|
|
117
|
+
data-site-plan-building=""
|
|
118
|
+
onPointerCancel={endDrag}
|
|
119
|
+
onPointerDown={handlePointerDown}
|
|
120
|
+
onPointerMove={handlePointerMove}
|
|
121
|
+
onPointerUp={endDrag}
|
|
122
|
+
style={{ cursor: buildingId ? 'move' : undefined }}
|
|
123
|
+
>
|
|
124
|
+
{drawing.primitives.map((geometry, index) =>
|
|
125
|
+
footprintIndexes.has(index) ? (
|
|
126
|
+
<FloorplanGeometryRenderer
|
|
127
|
+
geometry={geometry}
|
|
128
|
+
// eslint-disable-next-line react/no-array-index-key -- see above
|
|
129
|
+
key={`site-plan-fp-${index}`}
|
|
130
|
+
/>
|
|
131
|
+
) : null,
|
|
132
|
+
)}
|
|
133
|
+
</g>
|
|
134
|
+
</g>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export default FloorplanSitePlanLayer
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { parseStreetName, sameStreet } from './street-name'
|
|
3
|
+
|
|
4
|
+
describe('parseStreetName (USPS Pub. 28)', () => {
|
|
5
|
+
test('the number dropped, the directional and the type standardised', () => {
|
|
6
|
+
expect(parseStreetName('4121 NW 34th St')).toEqual({
|
|
7
|
+
pre: 'nw',
|
|
8
|
+
name: '34th',
|
|
9
|
+
type: 'st',
|
|
10
|
+
post: '',
|
|
11
|
+
})
|
|
12
|
+
expect(parseStreetName('Northwest 34th Terrace')).toEqual({
|
|
13
|
+
pre: 'nw',
|
|
14
|
+
name: '34th',
|
|
15
|
+
type: 'ter',
|
|
16
|
+
post: '',
|
|
17
|
+
})
|
|
18
|
+
expect(parseStreetName('N.W. 34th Terr.')).toEqual({
|
|
19
|
+
pre: 'nw',
|
|
20
|
+
name: '34th',
|
|
21
|
+
type: 'ter',
|
|
22
|
+
post: '',
|
|
23
|
+
})
|
|
24
|
+
expect(parseStreetName('2715 LAKE HUNTER DR')).toEqual({
|
|
25
|
+
pre: '',
|
|
26
|
+
name: 'lake hunter',
|
|
27
|
+
type: 'dr',
|
|
28
|
+
post: '',
|
|
29
|
+
})
|
|
30
|
+
expect(parseStreetName('100 1st Ave N')).toEqual({
|
|
31
|
+
pre: '',
|
|
32
|
+
name: '1st',
|
|
33
|
+
type: 'ave',
|
|
34
|
+
post: 'n',
|
|
35
|
+
})
|
|
36
|
+
expect(parseStreetName('Del Prado Boulevard South').type).toBe('blvd')
|
|
37
|
+
expect(parseStreetName('Pine Island Pkwy').type).toBe('pkwy')
|
|
38
|
+
expect(parseStreetName('Old Dixie Highway').type).toBe('hwy')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('a word is only a type or a directional when a name is left beside it', () => {
|
|
42
|
+
expect(parseStreetName('North St')).toEqual({ pre: '', name: 'north', type: 'st', post: '' })
|
|
43
|
+
expect(parseStreetName('Broadway')).toEqual({ pre: '', name: 'broadway', type: '', post: '' })
|
|
44
|
+
expect(parseStreetName('')).toEqual({ pre: '', name: '', type: '', post: '' })
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe('sameStreet', () => {
|
|
49
|
+
test('abbreviated and spelled-out lines of one street match', () => {
|
|
50
|
+
expect(sameStreet('4121 NW 34th St', 'Northwest 34th Street')).toBe(true)
|
|
51
|
+
expect(sameStreet('1247 NE 109th St', 'Northeast 109th Street')).toBe(true)
|
|
52
|
+
expect(sameStreet('2600 Castro Way', 'Castro Way')).toBe(true)
|
|
53
|
+
expect(sameStreet('44 Oak Cir', 'Oak Circle')).toBe(true)
|
|
54
|
+
expect(sameStreet('9 Elm Ct', 'Elm Court')).toBe(true)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('a different street type or quadrant is a different street', () => {
|
|
58
|
+
expect(sameStreet('4121 NW 34th St', 'Northwest 34th Terrace')).toBe(false)
|
|
59
|
+
expect(sameStreet('4121 NW 34th St', 'Southwest 34th Street')).toBe(false)
|
|
60
|
+
expect(sameStreet('12 Oak Ln', 'Oak Place')).toBe(false)
|
|
61
|
+
expect(sameStreet('12 Oak Rd', 'Oak Road')).toBe(true)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('a line that leaves the type or the directional out does not rule a street out', () => {
|
|
65
|
+
expect(sameStreet('S Castro', 'Castro Way')).toBe(true)
|
|
66
|
+
expect(sameStreet('4121 NW 34th St', '34th Street')).toBe(true)
|
|
67
|
+
expect(sameStreet('', 'Castro Way')).toBe(false)
|
|
68
|
+
})
|
|
69
|
+
})
|