@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,712 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyNode,
|
|
3
|
+
type AnyNodeId,
|
|
4
|
+
area,
|
|
5
|
+
type CeilingNode,
|
|
6
|
+
checkRoomFloor,
|
|
7
|
+
clampRoomFloorHandle,
|
|
8
|
+
containsPoint,
|
|
9
|
+
GROUND_SUPPORT_ID,
|
|
10
|
+
generateId,
|
|
11
|
+
getRoomBaseElevation,
|
|
12
|
+
getRoomRelativeFloorElevation,
|
|
13
|
+
intersection,
|
|
14
|
+
isDerivedNode,
|
|
15
|
+
justificationForFaceOnLine,
|
|
16
|
+
lockOutsideFaces,
|
|
17
|
+
type NodeChange,
|
|
18
|
+
planWallJustification,
|
|
19
|
+
resolveCeilingHeight,
|
|
20
|
+
roomFloorElevationFromRelative,
|
|
21
|
+
roomSideFaces,
|
|
22
|
+
runAsSingleSceneHistoryStep,
|
|
23
|
+
type StructureConflict,
|
|
24
|
+
type StructureNodes,
|
|
25
|
+
type StructurePlan,
|
|
26
|
+
setZoneEdges,
|
|
27
|
+
setZoneIntent,
|
|
28
|
+
useScene,
|
|
29
|
+
} from '@pascal-app/core'
|
|
30
|
+
import useDeleteConfirmation, {
|
|
31
|
+
type DeleteConfirmationRequest,
|
|
32
|
+
} from '../store/use-delete-confirmation'
|
|
33
|
+
import useEditor from '../store/use-editor'
|
|
34
|
+
import { footprintHeightValue } from './floor-footprints'
|
|
35
|
+
import { roomOwnPlate, roomSharedFootprint } from './room-built-on'
|
|
36
|
+
import { roomClearPolygon, roomConstructionState, roomFloorContents } from './room-construction'
|
|
37
|
+
import {
|
|
38
|
+
mezzanineElevationBounds,
|
|
39
|
+
ownFloorElevationBounds,
|
|
40
|
+
planRoomElevation,
|
|
41
|
+
roomFloorElevation,
|
|
42
|
+
} from './room-handle-drag'
|
|
43
|
+
import { applyRoomPlan } from './room-structure-commands'
|
|
44
|
+
import { showRoomNotice } from './room-transform-session'
|
|
45
|
+
|
|
46
|
+
export type RoomConstructionPart = 'floor' | 'walls' | 'ceiling'
|
|
47
|
+
export type RoomConstructionResult =
|
|
48
|
+
| { status: 'applied' | 'unchanged' | 'confirming' }
|
|
49
|
+
| { status: 'conflict'; message: string }
|
|
50
|
+
|
|
51
|
+
// Planner messages address callers ("Pass dropOpenings…"); the panel speaks to people.
|
|
52
|
+
const CONFLICT_MESSAGES: Record<string, string> = {
|
|
53
|
+
'covered-existing-wall': 'A wall already runs along this edge.',
|
|
54
|
+
'segment-too-short': 'An edge is too short for a wall.',
|
|
55
|
+
'occupied-split': 'An opening sits where the wall would split.',
|
|
56
|
+
'overlapping-spans': 'The room changed. Try again.',
|
|
57
|
+
'invalid-span': 'The room changed. Try again.',
|
|
58
|
+
'stale-span': 'The room changed. Try again.',
|
|
59
|
+
'mixed-exterior-faces': 'A wall is outside on both faces. Divide it first.',
|
|
60
|
+
'junction-conflict': 'Locking would break a room on this floor.',
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Clear floor area new walls may take before Add walls asks first, m². */
|
|
64
|
+
export const TRIVIAL_AREA_LOSS = 0.01
|
|
65
|
+
|
|
66
|
+
export function roomConflictMessage(conflicts: readonly StructureConflict[] = []) {
|
|
67
|
+
return [...new Set(conflicts.map((c) => CONFLICT_MESSAGES[c.code] ?? c.message))].join(' ')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function commit(plan: StructurePlan): RoomConstructionResult {
|
|
71
|
+
if (plan.conflicts?.length)
|
|
72
|
+
return { status: 'conflict', message: roomConflictMessage(plan.conflicts) }
|
|
73
|
+
if (!plan.changes.length) return { status: 'unchanged' }
|
|
74
|
+
applyRoomPlan(plan)
|
|
75
|
+
return { status: 'applied' }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const nodes = (): StructureNodes => useScene.getState().nodes
|
|
79
|
+
const sorted = (ids: readonly string[]) => [...ids].sort()
|
|
80
|
+
const deletes = (ids: readonly string[]) =>
|
|
81
|
+
ids.map((id): NodeChange => ({ op: 'delete', id: id as AnyNodeId }))
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Opens the house confirmation for one described change. At confirm time the
|
|
85
|
+
* choice re-derives what the dialog showed; when the scene moved on (a door
|
|
86
|
+
* added, an item placed) it re-asks with the new picture instead of applying.
|
|
87
|
+
*/
|
|
88
|
+
function ask(
|
|
89
|
+
zoneId: string,
|
|
90
|
+
request: Omit<DeleteConfirmationRequest, 'onConfirm' | 'onKeepContents'>,
|
|
91
|
+
choices: {
|
|
92
|
+
/** What the dialog shows, recomputed from the scene at the moment of choice. */
|
|
93
|
+
shown: () => unknown
|
|
94
|
+
reask: () => RoomConstructionResult
|
|
95
|
+
confirm: () => void
|
|
96
|
+
keep?: () => void
|
|
97
|
+
},
|
|
98
|
+
): RoomConstructionResult {
|
|
99
|
+
const shown = JSON.stringify(choices.shown())
|
|
100
|
+
const guarded = (apply: () => void) => () => {
|
|
101
|
+
if (!nodes()[zoneId as AnyNodeId]) return
|
|
102
|
+
if (JSON.stringify(choices.shown()) !== shown) {
|
|
103
|
+
choices.reask()
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
apply()
|
|
107
|
+
}
|
|
108
|
+
useDeleteConfirmation.getState().requestConfirmation({
|
|
109
|
+
...request,
|
|
110
|
+
onConfirm: guarded(choices.confirm),
|
|
111
|
+
onKeepContents: choices.keep && guarded(choices.keep),
|
|
112
|
+
})
|
|
113
|
+
return { status: 'confirming' }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Turns the room's unshared wall spans into separators; shared walls stay. */
|
|
117
|
+
export function roomWallRemovalPlan(
|
|
118
|
+
scene: StructureNodes,
|
|
119
|
+
zoneId: string,
|
|
120
|
+
dropOpenings: boolean,
|
|
121
|
+
mintId = generateId,
|
|
122
|
+
): StructurePlan {
|
|
123
|
+
const removable = roomConstructionState(scene, zoneId)?.walls.removable ?? []
|
|
124
|
+
if (!removable.length) return { changes: [] }
|
|
125
|
+
return setZoneEdges(scene, {
|
|
126
|
+
zoneId,
|
|
127
|
+
edges: removable.map((spanRef) => ({ spanRef, kind: 'separator' as const })),
|
|
128
|
+
dropOpenings,
|
|
129
|
+
mintId,
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Turns the room's separator spans into walls with the wall tool's thickness and height. */
|
|
134
|
+
export function roomWallAdditionPlan(
|
|
135
|
+
scene: StructureNodes,
|
|
136
|
+
zoneId: string,
|
|
137
|
+
mintId = generateId,
|
|
138
|
+
): StructurePlan {
|
|
139
|
+
const separators = roomConstructionState(scene, zoneId)?.walls.separators ?? []
|
|
140
|
+
if (!separators.length) return { changes: [] }
|
|
141
|
+
const defaults = useEditor.getState().toolDefaults.wall
|
|
142
|
+
const wall = {
|
|
143
|
+
...(typeof defaults?.thickness === 'number' && { thickness: defaults.thickness }),
|
|
144
|
+
...(typeof defaults?.height === 'number' && { height: defaults.height }),
|
|
145
|
+
}
|
|
146
|
+
return setZoneEdges(scene, {
|
|
147
|
+
zoneId,
|
|
148
|
+
edges: separators.map((spanRef) => ({ spanRef, kind: 'wall' as const, wall })),
|
|
149
|
+
mintId,
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function applyChanges(scene: StructureNodes, changes: readonly NodeChange[]): StructureNodes {
|
|
154
|
+
const next: Record<string, AnyNode> = { ...scene }
|
|
155
|
+
for (const change of changes) {
|
|
156
|
+
if (change.op === 'delete') delete next[change.id]
|
|
157
|
+
else if (change.op === 'create') next[change.node.id] = change.node
|
|
158
|
+
else if (next[change.id]) next[change.id] = { ...next[change.id], ...change.data } as AnyNode
|
|
159
|
+
}
|
|
160
|
+
return next
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const clearArea = (scene: StructureNodes, zoneId: string) => {
|
|
164
|
+
const clear = roomClearPolygon(scene, zoneId)
|
|
165
|
+
return clear ? area([clear]) : 0
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Add walls, with how many walls it builds and the clear floor area the room gives up to them. */
|
|
169
|
+
export function roomWallAdditionPreview(
|
|
170
|
+
scene: StructureNodes,
|
|
171
|
+
zoneId: string,
|
|
172
|
+
mintId = generateId,
|
|
173
|
+
) {
|
|
174
|
+
const plan = roomWallAdditionPlan(scene, zoneId, mintId)
|
|
175
|
+
if (plan.conflicts?.length || !plan.changes.length) return { plan, wallCount: 0, areaLoss: 0 }
|
|
176
|
+
const after = applyChanges(scene, plan.changes)
|
|
177
|
+
return {
|
|
178
|
+
plan,
|
|
179
|
+
// One per separator built on; splitting the walls it meets is not news.
|
|
180
|
+
wallCount: roomConstructionState(scene, zoneId)?.walls.separators.length ?? 0,
|
|
181
|
+
areaLoss: Math.max(0, clearArea(scene, zoneId) - clearArea(after, zoneId)),
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function requestWallAddition(zoneId: string): RoomConstructionResult {
|
|
186
|
+
const preview = roomWallAdditionPreview(nodes(), zoneId)
|
|
187
|
+
if (preview.areaLoss <= TRIVIAL_AREA_LOSS) return commit(preview.plan)
|
|
188
|
+
const state = roomConstructionState(nodes(), zoneId)!
|
|
189
|
+
return ask(
|
|
190
|
+
zoneId,
|
|
191
|
+
{
|
|
192
|
+
count: preview.wallCount,
|
|
193
|
+
construction: {
|
|
194
|
+
part: 'walls',
|
|
195
|
+
action: 'add',
|
|
196
|
+
roomName: state.name,
|
|
197
|
+
hostedIds: [],
|
|
198
|
+
wallCount: preview.wallCount,
|
|
199
|
+
areaLoss: preview.areaLoss,
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
shown: () => {
|
|
204
|
+
const current = roomWallAdditionPreview(nodes(), zoneId)
|
|
205
|
+
return {
|
|
206
|
+
separators: roomConstructionState(nodes(), zoneId)?.walls.separators,
|
|
207
|
+
walls: current.wallCount,
|
|
208
|
+
loss: current.areaLoss.toFixed(2),
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
reask: () => requestWallAddition(zoneId),
|
|
212
|
+
confirm: () => commit(roomWallAdditionPlan(nodes(), zoneId)),
|
|
213
|
+
},
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** What a wall removal takes along: the openings/objects it hosts and every node it deletes. */
|
|
218
|
+
function wallRemovalOutcome(zoneId: string) {
|
|
219
|
+
const scene = nodes()
|
|
220
|
+
const preview = roomWallRemovalPlan(scene, zoneId, false)
|
|
221
|
+
const final = roomWallRemovalPlan(scene, zoneId, true)
|
|
222
|
+
const deleted = final.changes.flatMap((c) => (c.op === 'delete' ? [c.id as string] : []))
|
|
223
|
+
// The planner stops at the first wall that hosts something, so its conflict
|
|
224
|
+
// names only that wall's openings; the confirmed plan's deletions name them all.
|
|
225
|
+
const asks = preview.conflicts?.some((c) => c.code === 'hosted-openings')
|
|
226
|
+
return {
|
|
227
|
+
preview,
|
|
228
|
+
final,
|
|
229
|
+
hostedIds: asks
|
|
230
|
+
? deleted.filter((id) => scene[id]?.type !== 'wall' && scene[id]?.type !== 'separator')
|
|
231
|
+
: null,
|
|
232
|
+
deleted: sorted(deleted),
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function requestWallRemoval(zoneId: string): RoomConstructionResult {
|
|
237
|
+
const outcome = wallRemovalOutcome(zoneId)
|
|
238
|
+
if (!outcome.hostedIds) return commit(outcome.preview)
|
|
239
|
+
const state = roomConstructionState(nodes(), zoneId)!
|
|
240
|
+
return ask(
|
|
241
|
+
zoneId,
|
|
242
|
+
{
|
|
243
|
+
count: outcome.hostedIds.length,
|
|
244
|
+
construction: {
|
|
245
|
+
part: 'walls',
|
|
246
|
+
roomName: state.name,
|
|
247
|
+
hostedIds: outcome.hostedIds,
|
|
248
|
+
wallCount: new Set(state.walls.removable.map((span) => span.boundaryId)).size,
|
|
249
|
+
sharedWalls: state.walls.sharedWallIds.length > 0,
|
|
250
|
+
},
|
|
251
|
+
conflict: outcome.final.conflicts?.length
|
|
252
|
+
? roomConflictMessage(outcome.final.conflicts)
|
|
253
|
+
: undefined,
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
// A door placed while the dialog is open changes both; neither may slip through.
|
|
257
|
+
shown: () => {
|
|
258
|
+
const current = wallRemovalOutcome(zoneId)
|
|
259
|
+
return { hosted: sorted(current.hostedIds ?? []), deleted: current.deleted }
|
|
260
|
+
},
|
|
261
|
+
reask: () => requestWallRemoval(zoneId),
|
|
262
|
+
confirm: () => commit(roomWallRemovalPlan(nodes(), zoneId, true)),
|
|
263
|
+
},
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Moves a ceiling-hung node onto the level at the height it hangs at now. */
|
|
268
|
+
function dropFromCeiling(scene: StructureNodes, id: string): NodeChange[] {
|
|
269
|
+
const node = scene[id]
|
|
270
|
+
const ceiling = node?.parentId ? scene[node.parentId] : undefined
|
|
271
|
+
if (!(node && ceiling?.type === 'ceiling' && 'position' in node)) return []
|
|
272
|
+
const [x, y, z] = node.position as [number, number, number]
|
|
273
|
+
return [
|
|
274
|
+
{
|
|
275
|
+
op: 'update',
|
|
276
|
+
id: node.id,
|
|
277
|
+
data: {
|
|
278
|
+
parentId: ceiling.parentId,
|
|
279
|
+
position: [x, y + resolveCeilingHeight(ceiling, scene), z],
|
|
280
|
+
} as Partial<AnyNode>,
|
|
281
|
+
},
|
|
282
|
+
]
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function ceilingRemovalOutcome(zoneId: string) {
|
|
286
|
+
const ceiling = roomConstructionState(nodes(), zoneId)?.ceiling
|
|
287
|
+
return {
|
|
288
|
+
manualIds: ceiling?.manualIds ?? [],
|
|
289
|
+
hostedIds: [...(ceiling?.hostedIds ?? []), ...(ceiling?.manualHostedIds ?? [])],
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Names of the other rooms a hand-drawn ceiling also covers. */
|
|
294
|
+
function alsoCovers(scene: StructureNodes, zoneId: string, ceilings: CeilingNode[]) {
|
|
295
|
+
const outline = ceilings.map((c) => ({ outer: c.polygon, holes: c.holes }))
|
|
296
|
+
return Object.values(scene).flatMap((node) =>
|
|
297
|
+
node.type === 'zone' &&
|
|
298
|
+
node.spaceRole === 'room' &&
|
|
299
|
+
node.id !== zoneId &&
|
|
300
|
+
node.parentId === scene[zoneId]?.parentId &&
|
|
301
|
+
area(intersection({ outer: node.polygon, holes: node.holes ?? [] }, outline)) >
|
|
302
|
+
TRIVIAL_AREA_LOSS
|
|
303
|
+
? [node.name.trim() || 'another room']
|
|
304
|
+
: [],
|
|
305
|
+
)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function requestCeilingRemoval(zoneId: string): RoomConstructionResult {
|
|
309
|
+
const state = roomConstructionState(nodes(), zoneId)
|
|
310
|
+
if (!state) return { status: 'unchanged' }
|
|
311
|
+
const { manualIds, hostedIds } = ceilingRemovalOutcome(zoneId)
|
|
312
|
+
const plan = (keep: boolean): StructurePlan => {
|
|
313
|
+
const scene = nodes()
|
|
314
|
+
const intent = setZoneIntent(scene, { zoneId, patch: { hasCeiling: false } })
|
|
315
|
+
return {
|
|
316
|
+
...intent,
|
|
317
|
+
changes: [
|
|
318
|
+
...intent.changes,
|
|
319
|
+
...(keep ? hostedIds.flatMap((id) => dropFromCeiling(scene, id)) : deletes(hostedIds)),
|
|
320
|
+
...deletes(manualIds),
|
|
321
|
+
],
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (!(hostedIds.length || manualIds.length)) return commit(plan(false))
|
|
325
|
+
const manual = manualIds.map((id) => nodes()[id] as CeilingNode)
|
|
326
|
+
return ask(
|
|
327
|
+
zoneId,
|
|
328
|
+
{
|
|
329
|
+
count: hostedIds.length + manualIds.length,
|
|
330
|
+
construction: {
|
|
331
|
+
part: 'ceiling',
|
|
332
|
+
roomName: state.name,
|
|
333
|
+
hostedIds,
|
|
334
|
+
manualCeilings: manual.map((c) => c.name?.trim() ?? ''),
|
|
335
|
+
alsoCovers: alsoCovers(nodes(), zoneId, manual),
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
shown: () => {
|
|
340
|
+
const current = ceilingRemovalOutcome(zoneId)
|
|
341
|
+
return { manual: sorted(current.manualIds), hosted: sorted(current.hostedIds) }
|
|
342
|
+
},
|
|
343
|
+
reask: () => requestCeilingRemoval(zoneId),
|
|
344
|
+
confirm: () => commit(plan(false)),
|
|
345
|
+
keep: hostedIds.length ? () => commit(plan(true)) : undefined,
|
|
346
|
+
},
|
|
347
|
+
)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function requestFloorRemoval(zoneId: string): RoomConstructionResult {
|
|
351
|
+
const state = roomConstructionState(nodes(), zoneId)
|
|
352
|
+
if (!state) return { status: 'unchanged' }
|
|
353
|
+
const contents = roomFloorContents(nodes(), zoneId)
|
|
354
|
+
const plan = (keep: boolean): StructurePlan => {
|
|
355
|
+
const scene = nodes()
|
|
356
|
+
const intent = setZoneIntent(scene, { zoneId, patch: { hasFloor: false } })
|
|
357
|
+
const levelId = scene[zoneId]?.parentId
|
|
358
|
+
// Kept items rest on the level base instead, lifted by the plate they stood on.
|
|
359
|
+
const kept = contents.flatMap(({ id, elevation }): NodeChange[] => {
|
|
360
|
+
const node = scene[id]
|
|
361
|
+
if (!(node && 'position' in node)) return []
|
|
362
|
+
const [x, y, z] = node.position as [number, number, number]
|
|
363
|
+
return [
|
|
364
|
+
{
|
|
365
|
+
op: 'update',
|
|
366
|
+
id: node.id,
|
|
367
|
+
data: {
|
|
368
|
+
position: [x, y + elevation, z],
|
|
369
|
+
supportSlabId: GROUND_SUPPORT_ID,
|
|
370
|
+
...(node.parentId !== levelId && { parentId: levelId }),
|
|
371
|
+
} as Partial<AnyNode>,
|
|
372
|
+
},
|
|
373
|
+
]
|
|
374
|
+
})
|
|
375
|
+
return {
|
|
376
|
+
...intent,
|
|
377
|
+
changes: [
|
|
378
|
+
...intent.changes,
|
|
379
|
+
...(keep ? kept : deletes(contents.map((content) => content.id))),
|
|
380
|
+
],
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (!contents.length) return commit(plan(false))
|
|
384
|
+
const ids = contents.map((content) => content.id)
|
|
385
|
+
return ask(
|
|
386
|
+
zoneId,
|
|
387
|
+
{ count: ids.length, construction: { part: 'floor', roomName: state.name, hostedIds: ids } },
|
|
388
|
+
{
|
|
389
|
+
shown: () => roomFloorContents(nodes(), zoneId),
|
|
390
|
+
reask: () => requestFloorRemoval(zoneId),
|
|
391
|
+
confirm: () => commit(plan(false)),
|
|
392
|
+
keep: () => commit(plan(true)),
|
|
393
|
+
},
|
|
394
|
+
)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Adds a construction part in one undo step: clears the room's opt-out
|
|
399
|
+
* (floor, ceiling) or builds walls on its separators. Walls that would eat
|
|
400
|
+
* into the room's clear floor ask first (`status: 'confirming'`).
|
|
401
|
+
*/
|
|
402
|
+
export function addRoomConstruction(
|
|
403
|
+
zoneId: string,
|
|
404
|
+
part: RoomConstructionPart,
|
|
405
|
+
): RoomConstructionResult {
|
|
406
|
+
if (part === 'walls') return requestWallAddition(zoneId)
|
|
407
|
+
const patch = part === 'floor' ? { hasFloor: true } : { hasCeiling: true }
|
|
408
|
+
return commit(setZoneIntent(nodes(), { zoneId, patch }))
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Removes a construction part in one undo step. Walls that would take doors,
|
|
413
|
+
* windows or objects with them, ceilings holding items or drawn by hand, and
|
|
414
|
+
* floors with furniture standing on them ask first through the house
|
|
415
|
+
* confirmation (`status: 'confirming'`).
|
|
416
|
+
*/
|
|
417
|
+
export function removeRoomConstruction(
|
|
418
|
+
zoneId: string,
|
|
419
|
+
part: RoomConstructionPart,
|
|
420
|
+
): RoomConstructionResult {
|
|
421
|
+
if (part === 'walls') return requestWallRemoval(zoneId)
|
|
422
|
+
if (part === 'ceiling') return requestCeilingRemoval(zoneId)
|
|
423
|
+
return requestFloorRemoval(zoneId)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Keeps the hand-drawn ceiling over the room as its ceiling: clears the
|
|
428
|
+
* opt-out, and the reconciler, seeing the room covered, generates none.
|
|
429
|
+
*/
|
|
430
|
+
export function adoptExistingCeiling(zoneId: string): RoomConstructionResult {
|
|
431
|
+
return commit(setZoneIntent(nodes(), { zoneId, patch: { hasCeiling: true } }))
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Swaps the hand-drawn ceiling(s) over the room for generated room ceilings
|
|
436
|
+
* in one undo step. What hung from them moves onto whichever new ceiling
|
|
437
|
+
* covers it, at the same drop; anything left uncovered stays at its height.
|
|
438
|
+
*/
|
|
439
|
+
export function replaceRoomCeiling(zoneId: string): RoomConstructionResult {
|
|
440
|
+
const state = roomConstructionState(nodes(), zoneId)
|
|
441
|
+
if (!state?.ceiling.manualIds.length) return { status: 'unchanged' }
|
|
442
|
+
const scene = nodes()
|
|
443
|
+
const { manualIds, manualHostedIds } = state.ceiling
|
|
444
|
+
const intent = setZoneIntent(scene, { zoneId, patch: { hasCeiling: true } })
|
|
445
|
+
return runAsSingleSceneHistoryStep(useScene, () => {
|
|
446
|
+
const result = commit({
|
|
447
|
+
changes: [
|
|
448
|
+
...intent.changes,
|
|
449
|
+
...manualHostedIds.flatMap((id) => dropFromCeiling(scene, id)),
|
|
450
|
+
...deletes(manualIds),
|
|
451
|
+
],
|
|
452
|
+
})
|
|
453
|
+
if (result.status !== 'applied') return result
|
|
454
|
+
const after = nodes()
|
|
455
|
+
const generated = Object.values(after).filter(
|
|
456
|
+
(node): node is CeilingNode =>
|
|
457
|
+
node.type === 'ceiling' && node.parentId === scene[zoneId]?.parentId && isDerivedNode(node),
|
|
458
|
+
)
|
|
459
|
+
const rehung = manualHostedIds.flatMap((id): NodeChange[] => {
|
|
460
|
+
const original = scene[id]
|
|
461
|
+
if (!(original && after[id] && 'position' in original)) return []
|
|
462
|
+
const [x, , z] = original.position as [number, number, number]
|
|
463
|
+
const host = generated.find((ceiling) =>
|
|
464
|
+
containsPoint([{ outer: ceiling.polygon, holes: ceiling.holes }], [x, z]),
|
|
465
|
+
)
|
|
466
|
+
return host
|
|
467
|
+
? [
|
|
468
|
+
{
|
|
469
|
+
op: 'update',
|
|
470
|
+
id: original.id,
|
|
471
|
+
data: { parentId: host.id, position: original.position } as Partial<AnyNode>,
|
|
472
|
+
},
|
|
473
|
+
]
|
|
474
|
+
: []
|
|
475
|
+
})
|
|
476
|
+
if (rehung.length) applyRoomPlan({ changes: rehung })
|
|
477
|
+
return result
|
|
478
|
+
})
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** Justifies the room's exterior walls so their outside faces stay put. One undo step. */
|
|
482
|
+
export function lockRoomOutsideFaces(zoneId: string): RoomConstructionResult {
|
|
483
|
+
return commit(lockOutsideFaces(nodes(), { zoneIds: [zoneId] }))
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* The room's walls with an outside (a face no room covers), and whether each
|
|
488
|
+
* already keeps that face on its drawn line — what Keep outside dimensions sets.
|
|
489
|
+
*/
|
|
490
|
+
function outsideWalls(scene: StructureNodes, wallIds: readonly string[]) {
|
|
491
|
+
return wallIds.flatMap((wallId) => {
|
|
492
|
+
const wall = scene[wallId]
|
|
493
|
+
const { outside } = wall?.type === 'wall' ? roomSideFaces(scene, wallId) : { outside: null }
|
|
494
|
+
if (wall?.type !== 'wall' || !outside) return []
|
|
495
|
+
return [{ wallId, locked: wall.justification === justificationForFaceOnLine(outside) }]
|
|
496
|
+
})
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Keep outside dimensions as a switch: `null` when the room has no wall with an
|
|
501
|
+
* outside (nothing to lock), else whether every such wall is locked.
|
|
502
|
+
*/
|
|
503
|
+
export function roomOutsideFacesLocked(
|
|
504
|
+
scene: StructureNodes,
|
|
505
|
+
wallIds: readonly string[],
|
|
506
|
+
): boolean | null {
|
|
507
|
+
const walls = outsideWalls(scene, wallIds)
|
|
508
|
+
return walls.length ? walls.every((wall) => wall.locked) : null
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/** Switches Keep outside dimensions off: the locked walls go back to centred on their line. One undo step. */
|
|
512
|
+
export function unlockRoomOutsideFaces(wallIds: readonly string[]): RoomConstructionResult {
|
|
513
|
+
const scene = nodes()
|
|
514
|
+
return commit({
|
|
515
|
+
changes: outsideWalls(scene, wallIds)
|
|
516
|
+
.filter((wall) => wall.locked)
|
|
517
|
+
.flatMap((wall) => planWallJustification(scene, wall.wallId, undefined))
|
|
518
|
+
.map((patch): NodeChange => ({ op: 'update', ...patch })),
|
|
519
|
+
})
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Moves the room onto another floor, in one undo step (core keeps every
|
|
524
|
+
* surface where it is): a floor key joins that floor, null the shared floor,
|
|
525
|
+
* 'new' a new floor of its own. A refusal says why.
|
|
526
|
+
*/
|
|
527
|
+
export function setRoomFloor(zoneId: string, key: string | null): RoomConstructionResult {
|
|
528
|
+
return commit(setZoneIntent(nodes(), { zoneId, patch: { floor: { footprint: key } } }))
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* The floor a room's height is measured from: its footprint's floor (a room
|
|
533
|
+
* on a raised house is 0 on the house floor), or for a mezzanine the floor of
|
|
534
|
+
* the room it stands in. A room on its own floor measures from the shared
|
|
535
|
+
* floor beside it, so its height reads the same before and after detaching
|
|
536
|
+
* (from the ground when there is none).
|
|
537
|
+
*/
|
|
538
|
+
export function roomFloorBase(scene: StructureNodes, zoneId: string): number {
|
|
539
|
+
const own = roomOwnPlate(scene, zoneId)
|
|
540
|
+
if (own) {
|
|
541
|
+
const shared = roomSharedFootprint(scene, zoneId)
|
|
542
|
+
return shared ? shared.elevation : own.elevation - footprintHeightValue(scene, own)
|
|
543
|
+
}
|
|
544
|
+
const zone = scene[zoneId]
|
|
545
|
+
if (zone?.type === 'zone' && zone.floor?.support === 'open' && zone.hostZoneId) {
|
|
546
|
+
const host = scene[zone.hostZoneId]
|
|
547
|
+
return host?.type === 'zone'
|
|
548
|
+
? (host.floor?.elevation ?? getRoomBaseElevation(scene, host.id))
|
|
549
|
+
: 0
|
|
550
|
+
}
|
|
551
|
+
return getRoomBaseElevation(scene, zoneId)
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/** How high the room's floor sits above the floor it is measured from (F − L). */
|
|
555
|
+
export function roomRelativeFloorHeight(scene: StructureNodes, zoneId: string): number {
|
|
556
|
+
const zone = scene[zoneId]
|
|
557
|
+
if (roomOwnPlate(scene, zoneId) || (zone?.type === 'zone' && zone.floor?.support === 'open'))
|
|
558
|
+
return (
|
|
559
|
+
Math.round((roomFloorElevation(scene, zoneId) - roomFloorBase(scene, zoneId)) * 1000) / 1000
|
|
560
|
+
)
|
|
561
|
+
return getRoomRelativeFloorElevation(scene, zoneId)
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Sets the room's floor to `height` above the floor it is measured from, in
|
|
566
|
+
* one undo step. A refused height leaves the scene alone and says why.
|
|
567
|
+
*/
|
|
568
|
+
export function setRoomRelativeFloorHeight(
|
|
569
|
+
zoneId: string,
|
|
570
|
+
height: number,
|
|
571
|
+
): RoomConstructionResult | FloorHeightRefusal {
|
|
572
|
+
const scene = nodes()
|
|
573
|
+
const zone = scene[zoneId]
|
|
574
|
+
const elevation =
|
|
575
|
+
roomOwnPlate(scene, zoneId) || (zone?.type === 'zone' && zone.floor?.support === 'open')
|
|
576
|
+
? Math.round((roomFloorBase(scene, zoneId) + height) * 1000) / 1000
|
|
577
|
+
: roomFloorElevationFromRelative(scene, zoneId, height)
|
|
578
|
+
return setRoomFloorElevation(zoneId, elevation)
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Sets the room's floor to `elevation` (level-local), in one undo step. A
|
|
583
|
+
* refused elevation leaves the scene alone and says why, a floor limit in
|
|
584
|
+
* heights above the floor the room is measured from.
|
|
585
|
+
*/
|
|
586
|
+
export function setRoomFloorElevation(
|
|
587
|
+
zoneId: string,
|
|
588
|
+
elevation: number,
|
|
589
|
+
): RoomConstructionResult | FloorHeightRefusal {
|
|
590
|
+
const scene = nodes()
|
|
591
|
+
const zone = scene[zoneId]
|
|
592
|
+
const base = roomFloorBase(scene, zoneId)
|
|
593
|
+
const own = roomOwnPlate(scene, zoneId) !== null
|
|
594
|
+
const plan = planRoomElevation(scene, zoneId, elevation)
|
|
595
|
+
const result = commit(plan)
|
|
596
|
+
if (result.status !== 'conflict') return result
|
|
597
|
+
// Core states its limits as floor elevations; the panel speaks in heights
|
|
598
|
+
// above the floor the room stands on, so it gets them in those terms.
|
|
599
|
+
const code = plan.conflicts?.find((c) => FLOOR_LIMIT_CODES.has(c.code))?.code
|
|
600
|
+
if (!code || own || zone?.type !== 'zone' || zone.floor?.support === 'open') return result
|
|
601
|
+
const check = checkRoomFloor(scene, zoneId, elevation)
|
|
602
|
+
return {
|
|
603
|
+
...result,
|
|
604
|
+
code,
|
|
605
|
+
min: Number.isFinite(check.minElevation) ? check.minElevation - base : null,
|
|
606
|
+
max: Number.isFinite(check.maxElevation) ? check.maxElevation - base : null,
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/** Refusals whose message states a floor limit, rephrased from `min` / `max`. */
|
|
611
|
+
const FLOOR_LIMIT_CODES = new Set(['floor-headroom', 'floor-sunken-depth', 'floor-opening-fit'])
|
|
612
|
+
|
|
613
|
+
/** A refused room height, with the allowed range above the floor it stands on. */
|
|
614
|
+
export type FloorHeightRefusal = {
|
|
615
|
+
status: 'conflict'
|
|
616
|
+
message: string
|
|
617
|
+
code: string
|
|
618
|
+
min: number | null
|
|
619
|
+
max: number | null
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* A refused room height in the panel's terms: the limit as a height above the
|
|
624
|
+
* floor the room stands on (core states it as a floor elevation).
|
|
625
|
+
*/
|
|
626
|
+
export function floorHeightRefusalText(
|
|
627
|
+
result: { message: string; code?: string; min?: number | null; max?: number | null },
|
|
628
|
+
length: (meters: number) => string,
|
|
629
|
+
) {
|
|
630
|
+
const { code, min, max } = result
|
|
631
|
+
if (code === 'floor-sunken-depth' && typeof min === 'number')
|
|
632
|
+
return min < 0
|
|
633
|
+
? `Too low: the floor can sink at most ${length(-min)} here.`
|
|
634
|
+
: `Too low: the floor must stay at least ${length(min)} up.`
|
|
635
|
+
if ((code === 'floor-headroom' || code === 'floor-opening-fit') && typeof max === 'number') {
|
|
636
|
+
const limit = max < 0 ? `sink at least ${length(-max)}` : `rise at most ${length(max)}`
|
|
637
|
+
return code === 'floor-headroom'
|
|
638
|
+
? `Too high: the floor can ${limit} and keep headroom.`
|
|
639
|
+
: `Too high: the floor can ${limit} for the doors and windows to fit.`
|
|
640
|
+
}
|
|
641
|
+
return result.message
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* The plan stepper's and the 3D elevation handle's commit: the room's floor to
|
|
646
|
+
* `elevation`, a refusal said under the room's pill as the other room actions
|
|
647
|
+
* say theirs.
|
|
648
|
+
*/
|
|
649
|
+
export function commitRoomFloorElevation(
|
|
650
|
+
zoneId: string,
|
|
651
|
+
elevation: number,
|
|
652
|
+
length: (meters: number) => string,
|
|
653
|
+
) {
|
|
654
|
+
const result = setRoomFloorElevation(zoneId, elevation)
|
|
655
|
+
if (result.status === 'conflict')
|
|
656
|
+
showRoomNotice({ zoneId, message: floorHeightRefusalText(result, length) })
|
|
657
|
+
return result
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* How far a room on a separate floor walks above (+) or below (−) the shared
|
|
662
|
+
* floor beside it, m: its walking surface against that of the shared-floor
|
|
663
|
+
* room it shares the most walls or open sides with (the shared plate's top
|
|
664
|
+
* when none), not plate against plate. Null on the shared floor, or with no
|
|
665
|
+
* shared floor beside it.
|
|
666
|
+
*/
|
|
667
|
+
export function separateFloorOffset(scene: StructureNodes, zoneId: string): number | null {
|
|
668
|
+
const zone = scene[zoneId]
|
|
669
|
+
if (zone?.type !== 'zone' || !zone.floor?.footprint) return null
|
|
670
|
+
const shared = roomSharedFootprint(scene, zoneId)
|
|
671
|
+
if (!shared) return null
|
|
672
|
+
const sides = new Set([...zone.boundaryWallIds, ...zone.boundarySeparatorIds])
|
|
673
|
+
let neighbour: { id: string; count: number } | null = null
|
|
674
|
+
for (const id of shared.zoneIds ?? []) {
|
|
675
|
+
const room = scene[id]
|
|
676
|
+
if (room?.type !== 'zone' || room.spaceRole !== 'room' || room.floor?.support === 'open')
|
|
677
|
+
continue
|
|
678
|
+
const count = [...room.boundaryWallIds, ...room.boundarySeparatorIds].filter((side) =>
|
|
679
|
+
sides.has(side),
|
|
680
|
+
).length
|
|
681
|
+
if (count > (neighbour?.count ?? 0)) neighbour = { id, count }
|
|
682
|
+
}
|
|
683
|
+
const sharedTop = neighbour ? roomFloorElevation(scene, neighbour.id) : shared.elevation
|
|
684
|
+
return roomFloorElevation(scene, zoneId) - sharedTop
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* The floor elevation one plan stepper click lands on: the room's height above
|
|
689
|
+
* the floor it is measured from moved to the next `step` multiple, held to what
|
|
690
|
+
* the 3D handle accepts (a mezzanine's bounds, else the room floor clamp). Null
|
|
691
|
+
* when the clamp leaves the floor where it is.
|
|
692
|
+
*/
|
|
693
|
+
export function stepRoomFloorElevation(
|
|
694
|
+
nodes: StructureNodes,
|
|
695
|
+
zoneId: string,
|
|
696
|
+
step: number,
|
|
697
|
+
): number | null {
|
|
698
|
+
const zone = nodes[zoneId]
|
|
699
|
+
if (zone?.type !== 'zone') return null
|
|
700
|
+
const current = roomFloorElevation(nodes, zoneId)
|
|
701
|
+
const base = roomFloorBase(nodes, zoneId)
|
|
702
|
+
const size = Math.abs(step)
|
|
703
|
+
const relative = (current - base) / size
|
|
704
|
+
const next =
|
|
705
|
+
base + (step > 0 ? Math.floor(relative + 1e-6) + 1 : Math.ceil(relative - 1e-6) - 1) * size
|
|
706
|
+
const bounds = mezzanineElevationBounds(nodes, zoneId) ?? ownFloorElevationBounds(nodes, zoneId)
|
|
707
|
+
const clamped = bounds
|
|
708
|
+
? Math.min(bounds.max, Math.max(bounds.min, next))
|
|
709
|
+
: clampRoomFloorHandle(nodes, zoneId, next)
|
|
710
|
+
const rounded = Math.round(clamped * 1000) / 1000
|
|
711
|
+
return Math.abs(rounded - current) < 1e-6 ? null : rounded
|
|
712
|
+
}
|