@pascal-app/editor 1.0.6 → 1.1.0
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 +18 -15
- package/src/components/editor/action-menu-button.tsx +4 -1
- package/src/components/editor/alignment-3d-guide-layer.tsx +102 -11
- package/src/components/editor/bake-thumbnail.tsx +3 -2
- package/src/components/editor/custom-camera-controls.tsx +110 -42
- package/src/components/editor/duplicate-node.ts +2 -1
- package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
- package/src/components/editor/first-person-controls.tsx +77 -68
- package/src/components/editor/fit-scene-framing.test.ts +318 -0
- package/src/components/editor/fit-scene-framing.ts +142 -0
- package/src/components/editor/floating-action-menu.tsx +31 -11
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floor-openings-overlay.tsx +1 -1
- package/src/components/editor/floor-region-controls.tsx +2 -2
- package/src/components/editor/floorplan-panel.test.ts +220 -0
- package/src/components/editor/floorplan-panel.tsx +458 -140
- package/src/components/editor/floorplan-wall-typing-keys.test.tsx +152 -0
- package/src/components/editor/floorplan-wall-typing-keys.ts +79 -0
- package/src/components/editor/footprint-height-handle.tsx +2 -2
- package/src/components/editor/grid.tsx +24 -15
- package/src/components/editor/group-actions.ts +19 -1
- package/src/components/editor/group-floating-action-menu.tsx +1 -1
- package/src/components/editor/group-move-3d.ts +27 -2
- package/src/components/editor/group-selection-box-3d.tsx +4 -1
- package/src/components/editor/group-transform-shared.ts +23 -1
- package/src/components/editor/index.tsx +150 -20
- package/src/components/editor/inspector-extension-actions.tsx +36 -0
- package/src/components/editor/localized-html.tsx +22 -0
- package/src/components/editor/measurement-pill.tsx +1 -1
- package/src/components/editor/node-action-menu.tsx +46 -12
- package/src/components/editor/node-arrow-handles.tsx +2 -3
- package/src/components/editor/open-wall-ends-3d-layer.tsx +2 -3
- package/src/components/editor/open-wall-ends-hint.tsx +1 -1
- package/src/components/editor/opening-guides-3d-layer.tsx +1 -1
- package/src/components/editor/quick-measurement-card.tsx +1 -1
- package/src/components/editor/registry-action-contributions.test.tsx +67 -6
- package/src/components/editor/registry-action-contributions.tsx +115 -15
- package/src/components/editor/riser-diagram-panel.tsx +59 -9
- package/src/components/editor/room-controls.tsx +4 -3
- package/src/components/editor/room-handles-2d.tsx +158 -7
- package/src/components/editor/room-handles.tsx +244 -54
- package/src/components/editor/room-highlight.tsx +2 -0
- package/src/components/editor/room-labels.tsx +33 -17
- package/src/components/editor/selection-manager.tsx +15 -102
- package/src/components/editor/separator-end-grip.tsx +74 -0
- package/src/components/editor/site-edge-labels.tsx +7 -3
- package/src/components/editor/thumbnail-generator.test.ts +15 -6
- package/src/components/editor/thumbnail-generator.tsx +26 -1
- package/src/components/editor/use-floorplan-background-placement.ts +55 -52
- package/src/components/editor/views/view-bar.tsx +111 -26
- package/src/components/editor/wall-measurement-label.tsx +31 -15
- package/src/components/editor/wall-move-side-handles.tsx +15 -7
- package/src/components/editor/wall-pull-grip.tsx +193 -0
- package/src/components/editor/wall-region-handles.tsx +2 -2
- package/src/components/editor/wall-snap-beacon-layer.tsx +134 -33
- package/src/components/editor-2d/floorplan-adjacent-walls-layer.test.ts +76 -0
- package/src/components/editor-2d/floorplan-adjacent-walls-layer.tsx +186 -0
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +63 -3
- package/src/components/editor-2d/floorplan-group-move.tsx +32 -4
- package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +6 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +33 -24
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +12 -5
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +2 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +29 -5
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +96 -4
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +52 -12
- package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +7 -1
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +13 -1
- package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.test.tsx +35 -0
- package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.tsx +65 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +34 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +211 -39
- package/src/components/procedural-items/inspector.tsx +59 -12
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +2 -0
- package/src/components/systems/ceiling/ceiling-system.test.ts +141 -0
- package/src/components/systems/ceiling/ceiling-system.tsx +53 -18
- package/src/components/systems/roof/roof-edit-system.test.tsx +15 -1
- package/src/components/systems/roof/roof-edit-system.tsx +30 -19
- package/src/components/tools/distribution/distribution-run-tool.test.tsx +371 -3
- package/src/components/tools/distribution/distribution-run-tool.tsx +200 -58
- package/src/components/tools/item/move-tool.tsx +1 -6
- package/src/components/tools/item/placement-history.test.tsx +1 -1
- package/src/components/tools/item/placement-strategies.test.ts +64 -1
- package/src/components/tools/item/placement-strategies.ts +5 -9
- package/src/components/tools/item/use-placement-coordinator.tsx +31 -35
- package/src/components/tools/paint-region/wall-paint-region-tool.tsx +1 -1
- package/src/components/tools/registry/item-surface-move.test.ts +11 -0
- package/src/components/tools/registry/move-registry-node-tool.test.ts +42 -1
- package/src/components/tools/registry/move-registry-node-tool.tsx +38 -14
- package/src/components/tools/select/select-candidates.test.ts +45 -0
- package/src/components/tools/select/select-candidates.ts +13 -2
- package/src/components/tools/shared/cursor-sphere.tsx +10 -3
- package/src/components/tools/shared/draft-measurement-label.tsx +1 -1
- package/src/components/tools/shared/placement-box.tsx +2 -6
- package/src/components/tools/shared/placement-coordinate-input.tsx +94 -0
- package/src/components/tools/shared/placement-dimension-guides.tsx +1 -1
- package/src/components/tools/shared/segment-angle.ts +0 -4
- package/src/components/tools/site/terrain-brush-cursor.tsx +1 -1
- package/src/components/tools/tool-manager.tsx +1 -24
- package/src/components/tools/wall/wall-drafting.test.ts +299 -23
- package/src/components/tools/wall/wall-drafting.ts +132 -24
- package/src/components/tools/wall/wall-pull-snap.test.ts +274 -0
- package/src/components/tools/wall/wall-pull-snap.ts +563 -0
- package/src/components/tools/wall/wall-snap-geometry.test.ts +113 -1
- package/src/components/tools/wall/wall-snap-geometry.ts +137 -0
- package/src/components/tools/wall/wall-typed-entry.test.ts +99 -0
- package/src/components/tools/wall/wall-typed-entry.ts +70 -0
- package/src/components/tools/zone/zone-tool.tsx +3 -3
- package/src/components/ui/action-menu/action-button.tsx +2 -0
- package/src/components/ui/action-menu/camera-actions.tsx +8 -7
- package/src/components/ui/action-menu/control-modes.tsx +9 -5
- package/src/components/ui/action-menu/follow-pascal-pill.tsx +3 -1
- package/src/components/ui/action-menu/follow-pascal-toggle.tsx +7 -1
- package/src/components/ui/action-menu/furnish-tools.tsx +7 -6
- package/src/components/ui/action-menu/index.tsx +1 -0
- package/src/components/ui/action-menu/measurement-control.tsx +35 -29
- package/src/components/ui/action-menu/point-ask-button.tsx +4 -3
- package/src/components/ui/action-menu/structure-tools.tsx +25 -23
- package/src/components/ui/action-menu/view-toggles.test.ts +18 -0
- package/src/components/ui/action-menu/view-toggles.tsx +256 -51
- package/src/components/ui/build-panel/build-panel.tsx +163 -25
- package/src/components/ui/command-palette/editor-commands.tsx +38 -11
- package/src/components/ui/controls/finish-swatch.tsx +45 -19
- package/src/components/ui/controls/material-picker.tsx +24 -17
- package/src/components/ui/controls/metric-control.test.ts +52 -0
- package/src/components/ui/controls/metric-control.tsx +62 -24
- package/src/components/ui/controls/segmented-control.tsx +5 -2
- package/src/components/ui/controls/slider-control.test.tsx +23 -0
- package/src/components/ui/controls/slider-control.tsx +47 -19
- package/src/components/ui/controls/tool-options-panel.tsx +74 -44
- package/src/components/ui/floating-level-selector.tsx +100 -48
- package/src/components/ui/helpers/building-helper.tsx +5 -3
- package/src/components/ui/helpers/contextual-helper-panel.tsx +97 -37
- package/src/components/ui/helpers/helper-manager.test.tsx +106 -3
- package/src/components/ui/helpers/helper-manager.tsx +136 -57
- package/src/components/ui/helpers/item-helper.tsx +6 -4
- package/src/components/ui/helpers/registered-tool-helper.tsx +3 -1
- package/src/components/ui/level-duplicate-dialog.tsx +32 -23
- package/src/components/ui/level-wall-copy-prompt.tsx +124 -0
- package/src/components/ui/panels/collections/collections-popover.tsx +11 -11
- package/src/components/ui/panels/deck-boards-fields.tsx +136 -0
- package/src/components/ui/panels/floor-foundation-panel.tsx +114 -39
- package/src/components/ui/panels/floor-opening-panel.tsx +74 -26
- package/src/components/ui/panels/inspector-extensions.tsx +66 -0
- package/src/components/ui/panels/mobile-panel-sheet.tsx +3 -1
- package/src/components/ui/panels/mobile-selection-bar.tsx +34 -28
- package/src/components/ui/panels/multi-field-value.test.ts +27 -0
- package/src/components/ui/panels/multi-field-value.ts +20 -1
- package/src/components/ui/panels/multi-height-mode.tsx +11 -10
- package/src/components/ui/panels/multi-parametric-inspector.tsx +122 -34
- package/src/components/ui/panels/multi-selection-panel.tsx +76 -36
- package/src/components/ui/panels/node-display.ts +35 -24
- package/src/components/ui/panels/panel-manager.tsx +9 -5
- package/src/components/ui/panels/panel-wrapper.tsx +63 -127
- package/src/components/ui/panels/parametric-field-control.tsx +13 -10
- package/src/components/ui/panels/parametric-inspector.tsx +18 -12
- package/src/components/ui/panels/reference-panel.tsx +51 -35
- package/src/components/ui/panels/room-construction-rows.tsx +465 -66
- package/src/components/ui/panels/room-name-options.test.ts +36 -3
- package/src/components/ui/panels/room-name-options.ts +110 -11
- package/src/components/ui/panels/room-openings-field.tsx +22 -13
- package/src/components/ui/panels/room-panel.tsx +207 -100
- package/src/components/ui/panels/shape-choice.tsx +7 -5
- package/src/components/ui/panels/wall-region-list.tsx +7 -7
- package/src/components/ui/primitives/shortcut-token.tsx +10 -6
- package/src/components/ui/right-stack.test.ts +13 -6
- package/src/components/ui/right-stack.tsx +21 -10
- package/src/components/ui/sidebar/icon-rail.tsx +11 -9
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +10 -10
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +10 -9
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +3 -0
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +102 -32
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +5 -3
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +7 -5
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/default-names.ts +75 -0
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +118 -72
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +36 -3
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +7 -4
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +8 -2
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +8 -5
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +9 -4
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +8 -7
- package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +7 -6
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +16 -1
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +15 -8
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +3 -1
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +19 -9
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +25 -14
- package/src/components/ui/sidebar/tab-bar.tsx +4 -1
- package/src/components/ui/sidebar/use-plugin-panels.tsx +19 -10
- package/src/components/ui/snap-target-badge.tsx +7 -6
- package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +721 -0
- package/src/components/ui/wall-type/wall-type-menu.tsx +167 -0
- package/src/components/viewer/floorplan-compass-button.tsx +12 -3
- package/src/components/viewer/floorplan-preview.tsx +27 -19
- package/src/components/viewer/render-settings-menu.tsx +294 -0
- package/src/components/viewer/sky-control.tsx +710 -0
- package/src/components/viewer/use-viewer-ui-sounds.test.ts +15 -0
- package/src/components/viewer/use-viewer-ui-sounds.ts +46 -0
- package/src/components/viewer/viewer-controls-bar.tsx +278 -260
- package/src/components/viewer/viewer-hover-label.tsx +12 -4
- package/src/components/viewer/viewer-rooms.tsx +62 -5
- package/src/components/viewer/viewer-scene-header.tsx +28 -17
- package/src/components/viewer/viewer-stage-switcher.tsx +4 -2
- package/src/components/viewer-overlay.tsx +8 -0
- package/src/components/walkthrough-hud.tsx +75 -2
- package/src/hooks/use-keyboard.test.ts +85 -4
- package/src/hooks/use-keyboard.ts +65 -7
- package/src/hooks/use-selected-room.ts +12 -1
- package/src/i18n/floor-footprint-name.ts +30 -0
- package/src/i18n/level-name.ts +39 -0
- package/src/i18n/locales/en/messages.po +15849 -0
- package/src/i18n/locales/en/messages.ts +1 -0
- package/src/i18n/locales/en-XA/messages.ts +1 -0
- package/src/i18n/locales/es/messages.po +39331 -0
- package/src/i18n/locales/es/messages.ts +1 -0
- package/src/i18n/locales/fr/messages.po +39331 -0
- package/src/i18n/locales/fr/messages.ts +1 -0
- package/src/i18n/locales/ja/messages.po +39331 -0
- package/src/i18n/locales/ja/messages.ts +1 -0
- package/src/i18n/locales/zh-CN/messages.po +39331 -0
- package/src/i18n/locales/zh-CN/messages.ts +1 -0
- package/src/i18n/locales.test.ts +63 -0
- package/src/i18n/locales.ts +32 -0
- package/src/i18n/message-slot.tsx +25 -0
- package/src/i18n/node-messages.ts +169 -0
- package/src/i18n/pseudo-locale.ts +17 -0
- package/src/i18n/published-entry.test.ts +17 -0
- package/src/i18n/scene-theme-name.ts +16 -0
- package/src/index.tsx +224 -45
- package/src/lib/add-storey.test.ts +3 -3
- package/src/lib/contextual-help.test.ts +20 -1
- package/src/lib/contextual-help.ts +101 -19
- package/src/lib/continuation.ts +38 -11
- package/src/lib/elevation-guides.ts +21 -6
- package/src/lib/elevator-support.ts +1 -72
- package/src/lib/floor-footprints.test.ts +3 -2
- package/src/lib/floor-footprints.ts +1 -9
- package/src/lib/floor-foundation-support.test.ts +15 -6
- package/src/lib/floor-opening-draft.ts +1 -1
- package/src/lib/floor-region-session.ts +1 -1
- package/src/lib/floorplan/dimension-drive.test.ts +36 -2
- package/src/lib/floorplan/dimension-drive.ts +68 -3
- package/src/lib/floorplan/floorplan-export.test.ts +16 -5
- package/src/lib/floorplan/floorplan-export.tsx +13 -16
- package/src/lib/floorplan/floorplan-extension.test.ts +11 -12
- package/src/lib/floorplan/floorplan-extension.ts +37 -18
- package/src/lib/floorplan/floorplan-mode.test.ts +13 -6
- package/src/lib/floorplan/floorplan-mode.ts +14 -12
- package/src/lib/floorplan/floorplan-plugin-install.test.tsx +1 -1
- package/src/lib/floorplan/floorplan-readonly.ts +3 -3
- package/src/lib/floorplan/open-wall-ends.ts +2 -1
- package/src/lib/floorplan/schedules.ts +10 -6
- package/src/lib/floorplan/site-plan/site-annotations.test.ts +46 -1
- package/src/lib/floorplan/site-plan/site-annotations.ts +3 -0
- package/src/lib/floorplan/site-plan/site-parts.ts +6 -3
- package/src/lib/geometry-script/author.test.ts +114 -0
- package/src/lib/geometry-script/author.ts +55 -23
- package/src/lib/geometry-script/client.ts +3 -0
- package/src/lib/geometry-script/finish-scripts.ts +7 -7
- package/src/lib/gesture-lifecycle.ts +3 -1
- package/src/lib/glb-export.test.ts +327 -0
- package/src/lib/glb-export.ts +70 -2
- package/src/lib/grid-frame.test.ts +56 -3
- package/src/lib/grid-frame.ts +28 -1
- package/src/lib/history.test.ts +2 -1
- package/src/lib/hud-title.ts +48 -24
- package/src/lib/ifc-export.test.ts +41 -1
- package/src/lib/inspector-card-mode.test.ts +3 -31
- package/src/lib/inspector-card-mode.ts +5 -10
- package/src/lib/inspector-expanded.test.ts +13 -0
- package/src/lib/inspector-expanded.ts +13 -1
- package/src/lib/interaction/scope.ts +1 -0
- package/src/lib/keyboard-pan.test.ts +7 -0
- package/src/lib/keyboard-pan.ts +9 -2
- package/src/lib/layer-thickness.test.ts +22 -0
- package/src/lib/layer-thickness.ts +79 -0
- package/src/lib/level-selection.test.ts +18 -2
- package/src/lib/level-selection.ts +31 -8
- package/src/lib/linear-display.ts +1 -1
- package/src/lib/live-gesture-round-trip.test.ts +1 -1
- package/src/lib/lot/drop-in.test.ts +59 -1
- package/src/lib/lot/drop-in.ts +42 -50
- package/src/lib/material-swatch-style.ts +24 -0
- package/src/lib/measurement-label.test.ts +1 -1
- package/src/lib/measurement-parser.ts +7 -3
- package/src/lib/measurements.test.ts +24 -6
- package/src/lib/measurements.ts +1 -203
- package/src/lib/mezzanine-selection.test.ts +3 -1
- package/src/lib/node-action-movement.test.tsx +115 -0
- package/src/lib/node-action-movement.ts +2 -0
- package/src/lib/orbit-near-plane.test.ts +12 -0
- package/src/lib/orbit-near-plane.ts +13 -0
- package/src/lib/paint-regions.test.ts +8 -5
- package/src/lib/paint-scope.test.ts +53 -0
- package/src/lib/paint-scope.ts +18 -4
- package/src/lib/panel-rows.ts +17 -8
- package/src/lib/placement-metadata.ts +4 -0
- package/src/lib/point-ask/describe-target.ts +6 -8
- package/src/lib/point-ask/region.ts +1 -1
- package/src/lib/print-shell-compiler.ts +12 -1
- package/src/lib/print-wall-solids.test.ts +11 -0
- package/src/lib/print-wall-solids.ts +9 -0
- package/src/lib/reference-scale.ts +83 -0
- package/src/lib/room-assembly-overlay.ts +1 -1
- package/src/lib/room-built-on.test.ts +28 -17
- package/src/lib/room-built-on.ts +21 -17
- package/src/lib/room-construction-commands.ts +3 -2
- package/src/lib/room-construction.test.ts +28 -1
- package/src/lib/room-construction.ts +14 -0
- package/src/lib/room-controls-phase6.test.ts +141 -1
- package/src/lib/room-handle-drag.ts +554 -58
- package/src/lib/room-name-catalog.test.ts +8 -0
- package/src/lib/room-name-catalog.ts +8 -1
- package/src/lib/room-name-messages.test.ts +132 -0
- package/src/lib/room-name-messages.ts +147 -0
- package/src/lib/room-name-storage.test.ts +334 -0
- package/src/lib/room-push-dimensions.test.ts +38 -1
- package/src/lib/room-push-dimensions.ts +226 -2
- package/src/lib/room-selection.test.ts +23 -6
- package/src/lib/scene-bounds.test.ts +144 -0
- package/src/lib/scene-bounds.ts +79 -15
- package/src/lib/scene-clipboard.test.ts +70 -49
- package/src/lib/scene-clipboard.ts +11 -5
- package/src/lib/selection-cycle.ts +63 -0
- package/src/lib/separator-edit.test.ts +431 -0
- package/src/lib/separator-edit.ts +179 -0
- package/src/lib/separator-lines.ts +38 -0
- package/src/lib/sfx-bus.ts +27 -1
- package/src/lib/sfx-player.ts +16 -0
- package/src/lib/snapping-mode.test.ts +23 -1
- package/src/lib/tool-cursor-icon.ts +5 -1
- package/src/lib/tool-shortcuts.ts +130 -0
- package/src/lib/viewer-selection.test.ts +28 -24
- package/src/lib/viewer-selection.ts +21 -6
- package/src/lib/wall-draw-variant.test.ts +7 -6
- package/src/lib/wall-draw-variant.ts +107 -6
- package/src/lib/wall-nudge.test.ts +44 -0
- package/src/lib/wall-nudge.ts +131 -0
- package/src/lib/wall-parallel-snap.test.ts +83 -0
- package/src/lib/wall-parallel-snap.ts +176 -0
- package/src/lib/wall-placement-pointer.test.ts +23 -0
- package/src/lib/wall-placement-pointer.ts +18 -0
- package/src/lib/wall-pull.test.ts +245 -0
- package/src/lib/wall-pull.ts +209 -0
- package/src/lib/wall-push-line-up.test.ts +132 -0
- package/src/lib/wall-region-list.ts +17 -6
- package/src/lib/wall-run-snap.test.ts +188 -0
- package/src/lib/wall-run-snap.ts +83 -0
- package/src/lib/wall-type.test.ts +125 -0
- package/src/lib/wall-type.ts +119 -0
- package/src/lib/window-interaction.test.ts +44 -0
- package/src/lib/world-grid-snap.test.ts +139 -2
- package/src/lib/world-grid-snap.ts +102 -105
- package/src/store/use-alignment-guides.ts +27 -1
- package/src/store/use-editor.tsx +53 -0
- package/src/store/use-floorplan-annotation-visibility.test.ts +64 -0
- package/src/store/use-floorplan-annotation-visibility.ts +47 -22
- package/src/store/use-placement-typing.test.ts +165 -0
- package/src/store/use-placement-typing.ts +186 -0
- package/src/store/use-wall-snap-indicator.ts +15 -2
- package/src/components/tools/elevator/elevator-defaults.ts +0 -8
- package/src/components/tools/elevator/elevator-tool.tsx +0 -372
- package/src/components/tools/elevator/move-elevator-tool.tsx +0 -262
- package/src/lib/door-interaction.ts +0 -102
- package/src/lib/measurement-label.ts +0 -68
- package/src/lib/window-interaction.ts +0 -100
|
@@ -16,13 +16,20 @@ import {
|
|
|
16
16
|
type Point,
|
|
17
17
|
planWallDivision,
|
|
18
18
|
resolveMovedWallSupportSlabPatch,
|
|
19
|
+
roomFloorElevation,
|
|
20
|
+
roomOwnPlate,
|
|
19
21
|
runAsSingleSceneHistoryStep,
|
|
22
|
+
SEPARATOR_JOIN_REACH,
|
|
23
|
+
type SeparatorNode,
|
|
24
|
+
type SeparatorSpan,
|
|
20
25
|
type StructureNodes,
|
|
21
26
|
type StructurePlan,
|
|
27
|
+
separatorFloorElevation,
|
|
22
28
|
setRoomFloorConstruction,
|
|
23
29
|
setWallGeometry,
|
|
24
30
|
setZoneEdges,
|
|
25
31
|
setZoneIntent,
|
|
32
|
+
translateSeparators,
|
|
26
33
|
upperFloorHeightControl,
|
|
27
34
|
useLiveNodeOverrides,
|
|
28
35
|
useScene,
|
|
@@ -42,23 +49,37 @@ import { clientToPlan } from './floorplan/plan-coords'
|
|
|
42
49
|
import { beginGesture } from './gesture-lifecycle'
|
|
43
50
|
import { isHistoryShortcut } from './history'
|
|
44
51
|
import { MEZZANINE_FAILED_MESSAGE, mezzanineConflictMessage } from './mezzanine-messages'
|
|
45
|
-
import { roomOwnPlate } from './room-built-on'
|
|
46
52
|
import {
|
|
47
53
|
mezzanineEdgeDimensions,
|
|
48
54
|
type RoomDimension,
|
|
55
|
+
sameRoomDimension,
|
|
56
|
+
separatorEdgeDimensionRooms,
|
|
57
|
+
type WallDimensionFaces,
|
|
58
|
+
type WallSelectionDimension,
|
|
49
59
|
wallPushDimensionRooms,
|
|
50
60
|
wallPushDimensions,
|
|
61
|
+
wallSelectionDimensions,
|
|
51
62
|
} from './room-push-dimensions'
|
|
52
63
|
import { applyRoomPlan } from './room-structure-commands'
|
|
64
|
+
import { levelSeparatorLines, separatorWallLine } from './separator-lines'
|
|
53
65
|
import { sfxEmitter } from './sfx-bus'
|
|
54
66
|
import { type SpatialPointerId, spatialPointerInput } from './spatial-pointer-input'
|
|
55
67
|
import { isTypingTarget } from './typing-target'
|
|
68
|
+
import {
|
|
69
|
+
nearestWallParallelTarget,
|
|
70
|
+
publishWallParallelGuide,
|
|
71
|
+
type WallParallelTarget,
|
|
72
|
+
wallParallelTargets,
|
|
73
|
+
} from './wall-parallel-snap'
|
|
74
|
+
import { isWallRunFree } from './wall-run-snap'
|
|
56
75
|
|
|
57
76
|
export const ROOM_ELEVATION_DRAG_LABEL = 'room-elevation'
|
|
58
77
|
/** Scope label of every wall push: a selected wall's side arrow and a room's boundary arrow. */
|
|
59
78
|
export const WALL_PUSH_DRAG_LABEL = 'wall-push'
|
|
60
79
|
/** Scope label of a mezzanine edge push (its plate grows or shrinks). */
|
|
61
80
|
export const MEZZANINE_EDGE_DRAG_LABEL = 'mezzanine-edge'
|
|
81
|
+
/** What a wall push the planner refuses reads. */
|
|
82
|
+
export const PUSH_REFUSED_MESSAGE = "Can't move this wall there"
|
|
62
83
|
|
|
63
84
|
/** A wall a push would create (a moved span piece, a bridge), drawn as a ghost. */
|
|
64
85
|
export type RoomPushGhost = { start: Point; end: Point; thickness: number; height: number }
|
|
@@ -93,6 +114,20 @@ export type RoomHandleDrag =
|
|
|
93
114
|
dimensions: RoomDimension[]
|
|
94
115
|
message?: string
|
|
95
116
|
}
|
|
117
|
+
| {
|
|
118
|
+
/** Several selected walls moved together, each along its own normal. */
|
|
119
|
+
kind: 'wall-group-push'
|
|
120
|
+
levelId: string
|
|
121
|
+
/** Where a refusal reads: over the moved walls' middle, in level plan coordinates. */
|
|
122
|
+
anchor: Point
|
|
123
|
+
height: number
|
|
124
|
+
/** The lowest moved wall's base, where the ghost pieces stand. */
|
|
125
|
+
base: number
|
|
126
|
+
ghosts: RoomPushGhost[]
|
|
127
|
+
/** Every moved wall's live room widths, as its own push shows them. */
|
|
128
|
+
dimensions: RoomDimension[]
|
|
129
|
+
message?: string
|
|
130
|
+
}
|
|
96
131
|
| {
|
|
97
132
|
kind: 'mezzanine-edge'
|
|
98
133
|
zoneId: string
|
|
@@ -113,25 +148,19 @@ export const useRoomHandleDrag = create<{ drag: RoomHandleDrag | null }>(() => (
|
|
|
113
148
|
|
|
114
149
|
type Nodes = Record<string, AnyNode>
|
|
115
150
|
|
|
116
|
-
/** The walking surface of a room: its own elevation, else its plate's. */
|
|
117
|
-
export function roomFloorElevation(nodes: Nodes, zoneId: string): number {
|
|
118
|
-
const zone = nodes[zoneId]
|
|
119
|
-
if (zone?.type !== 'zone') return 0
|
|
120
|
-
const own = roomOwnPlate(nodes, zoneId)
|
|
121
|
-
if (own) return own.elevation
|
|
122
|
-
if (typeof zone.floor?.elevation === 'number') return zone.floor.elevation
|
|
123
|
-
const plate = Object.values(nodes).find(
|
|
124
|
-
(node) => node.type === 'slab' && node.zoneIds?.includes(zoneId),
|
|
125
|
-
)
|
|
126
|
-
return plate?.type === 'slab' ? (plate.elevation ?? 0.05) : 0.05
|
|
127
|
-
}
|
|
128
|
-
|
|
129
151
|
/**
|
|
130
152
|
* The stretch of a wall a push moves: `[0, 1]` is the whole wall (its
|
|
131
153
|
* neighbours follow through the junction planner), anything shorter is split
|
|
132
|
-
* off first and only that piece moves.
|
|
154
|
+
* off first and only that piece moves. An edge of invisible walls carries its
|
|
155
|
+
* stretches in `separators` (`wallId` is the first one's): they move as one
|
|
156
|
+
* line, the invisible walls joined to them following (`translateSeparators`).
|
|
133
157
|
*/
|
|
134
|
-
export type WallPushSpan = {
|
|
158
|
+
export type WallPushSpan = {
|
|
159
|
+
wallId: string
|
|
160
|
+
t0: number
|
|
161
|
+
t1: number
|
|
162
|
+
separators?: readonly SeparatorSpan[]
|
|
163
|
+
}
|
|
135
164
|
|
|
136
165
|
/** One push/pull arrow: a selected wall's side arrow or a room's boundary arrow. */
|
|
137
166
|
export type WallPushHandle = WallPushSpan & {
|
|
@@ -166,7 +195,7 @@ function wallPushHandle(
|
|
|
166
195
|
const offsets = getWallFaceOffsets(wall)
|
|
167
196
|
const face = side === 'a' ? offsets.a : -offsets.b
|
|
168
197
|
const offset = Math.max(face + HANDLE_OFFSET, HANDLE_MIN_OFFSET)
|
|
169
|
-
const wallHeight = getWallEffectiveHeightForNodes(wall, nodes)
|
|
198
|
+
const wallHeight = getWallEffectiveHeightForNodes(wall, nodes, 0.5)
|
|
170
199
|
return {
|
|
171
200
|
key,
|
|
172
201
|
wallId: wall.id,
|
|
@@ -191,22 +220,27 @@ export function wallPushHandles(wall: WallNode, nodes: Nodes): WallPushHandle[]
|
|
|
191
220
|
})
|
|
192
221
|
}
|
|
193
222
|
|
|
223
|
+
type BoundarySpanRef = {
|
|
224
|
+
boundaryId: string
|
|
225
|
+
kind: string
|
|
226
|
+
face: 'a' | 'b'
|
|
227
|
+
t0: number
|
|
228
|
+
t1: number
|
|
229
|
+
}
|
|
230
|
+
|
|
194
231
|
/**
|
|
195
232
|
* One push/pull arrow per boundary wall span of the room, outside the wall's
|
|
196
233
|
* far face, pointing away from the room — where the wall's own side arrow for
|
|
197
|
-
* that face sits. `face` is the wall face the room lies on (`a` = front).
|
|
234
|
+
* that face sits. `face` is the wall face the room lies on (`a` = front). Its
|
|
235
|
+
* invisible walls get one arrow per straight edge (`separatorEdges`), lying
|
|
236
|
+
* at `floor` (the room's floor height), where their dashed line is drawn.
|
|
198
237
|
*/
|
|
199
238
|
export function roomPushHandles(
|
|
200
239
|
nodes: Nodes,
|
|
201
|
-
spans: ReadonlyArray<
|
|
202
|
-
|
|
203
|
-
kind: string
|
|
204
|
-
face: 'a' | 'b'
|
|
205
|
-
t0: number
|
|
206
|
-
t1: number
|
|
207
|
-
}>,
|
|
240
|
+
spans: ReadonlyArray<BoundarySpanRef>,
|
|
241
|
+
floor = 0,
|
|
208
242
|
): WallPushHandle[] {
|
|
209
|
-
|
|
243
|
+
const walls = spans.flatMap((span) => {
|
|
210
244
|
const wall = nodes[span.boundaryId]
|
|
211
245
|
if (span.kind !== 'wall' || wall?.type !== 'wall') return []
|
|
212
246
|
const handle = wallPushHandle(
|
|
@@ -218,6 +252,137 @@ export function roomPushHandles(
|
|
|
218
252
|
)
|
|
219
253
|
return handle ? [handle] : []
|
|
220
254
|
})
|
|
255
|
+
const edges = separatorEdges(nodes, spans).map((edge) => {
|
|
256
|
+
const first = edge.members[0]!
|
|
257
|
+
return separatorPushHandle(
|
|
258
|
+
edge,
|
|
259
|
+
edge.outward,
|
|
260
|
+
floor,
|
|
261
|
+
`${first.separatorId}:${edge.face}:${first.t0.toFixed(4)}`,
|
|
262
|
+
)
|
|
263
|
+
})
|
|
264
|
+
return [...walls, ...edges]
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** How far outside an invisible wall (m) its push arrow sits, as a mezzanine edge's does. */
|
|
268
|
+
const SEPARATOR_ARROW_OFFSET = 0.3
|
|
269
|
+
/** Its 3D arrow's lift over the floor: where the invisible wall's dashed line is drawn. */
|
|
270
|
+
const SEPARATOR_ARROW_LIFT = 0.02
|
|
271
|
+
/** An invisible-wall edge shorter than this (m) gets no arrow, as a mezzanine edge. */
|
|
272
|
+
const SEPARATOR_EDGE_MIN_LENGTH = 0.2
|
|
273
|
+
/** Stretches this far off each other's line (m) still run along one edge. */
|
|
274
|
+
const EDGE_IN_LINE = 0.01
|
|
275
|
+
|
|
276
|
+
type SeparatorEdge = {
|
|
277
|
+
/** The stretches along the edge, in order. */
|
|
278
|
+
members: SeparatorSpan[]
|
|
279
|
+
start: Point
|
|
280
|
+
end: Point
|
|
281
|
+
/** Away from the room. */
|
|
282
|
+
outward: Point
|
|
283
|
+
face: 'a' | 'b'
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* A room's invisible-wall stretches gathered into its straight edges: stretches
|
|
288
|
+
* end to end on one line, the room on the same side, are one edge and push as
|
|
289
|
+
* one (an edge drawn in two pieces, or broken where another one meets it). Ends
|
|
290
|
+
* meet as far as the room graph joins them (`SEPARATOR_JOIN_REACH`).
|
|
291
|
+
*/
|
|
292
|
+
function separatorEdges(nodes: Nodes, spans: ReadonlyArray<BoundarySpanRef>): SeparatorEdge[] {
|
|
293
|
+
const pieces = spans.flatMap((span): SeparatorEdge[] => {
|
|
294
|
+
const separator = nodes[span.boundaryId]
|
|
295
|
+
if (span.kind !== 'separator' || separator?.type !== 'separator') return []
|
|
296
|
+
const dx = separator.end[0] - separator.start[0]
|
|
297
|
+
const dz = separator.end[1] - separator.start[1]
|
|
298
|
+
const length = Math.hypot(dx, dz)
|
|
299
|
+
if (length < 1e-6 || span.t1 - span.t0 < WHOLE) return []
|
|
300
|
+
// Face `a` is the +normal side: the arrow points the other way, off the room.
|
|
301
|
+
const sign = span.face === 'a' ? -1 : 1
|
|
302
|
+
const at = (t: number): Point => [separator.start[0] + dx * t, separator.start[1] + dz * t]
|
|
303
|
+
return [
|
|
304
|
+
{
|
|
305
|
+
members: [{ separatorId: separator.id, t0: span.t0, t1: span.t1 }],
|
|
306
|
+
start: at(span.t0),
|
|
307
|
+
end: at(span.t1),
|
|
308
|
+
outward: [(-dz / length) * sign, (dx / length) * sign],
|
|
309
|
+
face: span.face,
|
|
310
|
+
},
|
|
311
|
+
]
|
|
312
|
+
})
|
|
313
|
+
const across = (edge: SeparatorEdge, [x, z]: Point) =>
|
|
314
|
+
Math.abs((x - edge.start[0]) * edge.outward[0] + (z - edge.start[1]) * edge.outward[1])
|
|
315
|
+
const joins = (a: SeparatorEdge, b: SeparatorEdge) =>
|
|
316
|
+
a.outward[0] * b.outward[0] + a.outward[1] * b.outward[1] > 1 - 1e-4 &&
|
|
317
|
+
across(a, b.start) < EDGE_IN_LINE &&
|
|
318
|
+
across(a, b.end) < EDGE_IN_LINE &&
|
|
319
|
+
[a.start, a.end].some((p) =>
|
|
320
|
+
[b.start, b.end].some((q) => Math.hypot(p[0] - q[0], p[1] - q[1]) <= SEPARATOR_JOIN_REACH),
|
|
321
|
+
)
|
|
322
|
+
let groups: SeparatorEdge[][] = []
|
|
323
|
+
for (const piece of pieces) {
|
|
324
|
+
const touching = groups.filter((group) => group.some((other) => joins(piece, other)))
|
|
325
|
+
groups = [...groups.filter((group) => !touching.includes(group)), [piece, ...touching.flat()]]
|
|
326
|
+
}
|
|
327
|
+
return groups.flatMap((group): SeparatorEdge[] => {
|
|
328
|
+
const { outward, face } = group[0]!
|
|
329
|
+
const dir: Point = [outward[1], -outward[0]]
|
|
330
|
+
const along = ([x, z]: Point) => x * dir[0] + z * dir[1]
|
|
331
|
+
const ends = group
|
|
332
|
+
.flatMap((piece) => [piece.start, piece.end])
|
|
333
|
+
.sort((p, q) => along(p) - along(q))
|
|
334
|
+
const start = ends[0]!
|
|
335
|
+
const end = ends.at(-1)!
|
|
336
|
+
if (Math.hypot(end[0] - start[0], end[1] - start[1]) < SEPARATOR_EDGE_MIN_LENGTH) return []
|
|
337
|
+
const middle = (piece: SeparatorEdge) => along(piece.start) + along(piece.end)
|
|
338
|
+
const members = [...group].sort((p, q) => middle(p) - middle(q)).flatMap((p) => p.members)
|
|
339
|
+
return [{ members, start, end, outward, face }]
|
|
340
|
+
})
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function separatorPushHandle(
|
|
344
|
+
edge: Pick<SeparatorEdge, 'members' | 'start' | 'end'>,
|
|
345
|
+
outward: Point,
|
|
346
|
+
floor: number,
|
|
347
|
+
key: string,
|
|
348
|
+
): WallPushHandle {
|
|
349
|
+
const first = edge.members[0]!
|
|
350
|
+
const middle: Point = [(edge.start[0] + edge.end[0]) / 2, (edge.start[1] + edge.end[1]) / 2]
|
|
351
|
+
return {
|
|
352
|
+
key,
|
|
353
|
+
wallId: first.separatorId,
|
|
354
|
+
t0: first.t0,
|
|
355
|
+
t1: first.t1,
|
|
356
|
+
separators: edge.members,
|
|
357
|
+
position: [
|
|
358
|
+
middle[0] + outward[0] * SEPARATOR_ARROW_OFFSET,
|
|
359
|
+
middle[1] + outward[1] * SEPARATOR_ARROW_OFFSET,
|
|
360
|
+
],
|
|
361
|
+
height: floor + SEPARATOR_ARROW_LIFT,
|
|
362
|
+
outward,
|
|
363
|
+
line: middle[0] * outward[0] + middle[1] * outward[1],
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* A selected invisible wall's two side arrows, one off each side, at `floor`:
|
|
369
|
+
* each pushes the whole separator, the ones joined to it following.
|
|
370
|
+
*/
|
|
371
|
+
export function separatorPushHandles(separator: SeparatorNode, floor = 0): WallPushHandle[] {
|
|
372
|
+
const dx = separator.end[0] - separator.start[0]
|
|
373
|
+
const dz = separator.end[1] - separator.start[1]
|
|
374
|
+
const length = Math.hypot(dx, dz)
|
|
375
|
+
if (length < 1e-6) return []
|
|
376
|
+
const edge = {
|
|
377
|
+
members: [{ separatorId: separator.id, t0: 0, t1: 1 }],
|
|
378
|
+
start: separator.start,
|
|
379
|
+
end: separator.end,
|
|
380
|
+
}
|
|
381
|
+
return (['a', 'b'] as const).map((side) => {
|
|
382
|
+
const sign = side === 'a' ? 1 : -1
|
|
383
|
+
const outward: Point = [(-dz / length) * sign, (dx / length) * sign]
|
|
384
|
+
return separatorPushHandle(edge, outward, floor, `${separator.id}:${side}`)
|
|
385
|
+
})
|
|
221
386
|
}
|
|
222
387
|
|
|
223
388
|
/**
|
|
@@ -294,7 +459,8 @@ function diffNodes(before: Scratch, after: Scratch): NodeChange[] {
|
|
|
294
459
|
* Pushes the room's stretch of a wall `distance` along `outward`. A span that
|
|
295
460
|
* covers the whole wall moves the wall; a partial span is split off at its
|
|
296
461
|
* ends first (hosted openings stay on their pieces) and only that piece moves,
|
|
297
|
-
* bridged back to the rest by the junction planner.
|
|
462
|
+
* bridged back to the rest by the junction planner. An edge of invisible walls
|
|
463
|
+
* moves as one line (`translateSeparators`). One plan, one undo step.
|
|
298
464
|
*/
|
|
299
465
|
export function planRoomPush(
|
|
300
466
|
nodes: StructureNodes,
|
|
@@ -303,6 +469,12 @@ export function planRoomPush(
|
|
|
303
469
|
distance: number,
|
|
304
470
|
mintId: (kind: 'zone' | 'wall' | 'separator') => string = generateId,
|
|
305
471
|
): StructurePlan {
|
|
472
|
+
if (span.separators?.length)
|
|
473
|
+
return translateSeparators(nodes, {
|
|
474
|
+
spans: span.separators,
|
|
475
|
+
offset: [outward[0] * distance, outward[1] * distance],
|
|
476
|
+
mintId,
|
|
477
|
+
})
|
|
306
478
|
const wall = nodes[span.wallId]
|
|
307
479
|
if (wall?.type !== 'wall') throw Error('Select a wall.')
|
|
308
480
|
const shift = ([x, z]: Point): Point => [x + outward[0] * distance, z + outward[1] * distance]
|
|
@@ -388,14 +560,18 @@ function clearPushPreview() {
|
|
|
388
560
|
function previewSurfaces(nodes: Nodes, levelId: string, after: Scratch) {
|
|
389
561
|
if (surfacePreview?.levelId !== levelId || surfacePreview.nodes !== nodes)
|
|
390
562
|
surfacePreview = { levelId, nodes, run: createLevelStructurePreview(levelId, nodes) }
|
|
391
|
-
const
|
|
392
|
-
(node): node is WallNode => node.type === 'wall' && node.parentId === levelId,
|
|
393
|
-
)
|
|
563
|
+
const level = Object.values(after).filter((node) => node.parentId === levelId)
|
|
394
564
|
return surfacePreview
|
|
395
|
-
.run(
|
|
565
|
+
.run(
|
|
566
|
+
level.filter((node): node is WallNode => node.type === 'wall'),
|
|
567
|
+
level.filter((node): node is SeparatorNode => node.type === 'separator'),
|
|
568
|
+
)
|
|
396
569
|
.map((patch) => [patch.id as AnyNodeId, patch.data as Partial<AnyNode>] as const)
|
|
397
570
|
}
|
|
398
571
|
|
|
572
|
+
/** The ghost of an invisible wall a push would create (a jog): a thin strip on the floor. */
|
|
573
|
+
const SEPARATOR_GHOST = { thickness: 0.03, height: 0.04 }
|
|
574
|
+
|
|
399
575
|
/**
|
|
400
576
|
* Shows the push live: the plan's updates to existing walls — and the room
|
|
401
577
|
* surfaces reshaped around them — ride `useLiveNodeOverrides` (renderers merge
|
|
@@ -410,9 +586,20 @@ export function previewRoomPush(
|
|
|
410
586
|
clearPushPreview()
|
|
411
587
|
if (Math.abs(distance) < 1e-6) return { ghosts: [] }
|
|
412
588
|
const nodes = useScene.getState().nodes
|
|
589
|
+
return previewPushPlan(nodes, nodes[span.wallId as AnyNodeId]?.parentId, () =>
|
|
590
|
+
planRoomPush(nodes, span, outward, distance, previewMint()),
|
|
591
|
+
)
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/** Shows a push plan live (`previewRoomPush`); the caller has cleared the last one. */
|
|
595
|
+
function previewPushPlan(
|
|
596
|
+
nodes: Nodes,
|
|
597
|
+
levelId: string | null | undefined,
|
|
598
|
+
planPush: () => StructurePlan,
|
|
599
|
+
): { code?: string; ghosts: RoomPushGhost[] } {
|
|
413
600
|
let plan: StructurePlan
|
|
414
601
|
try {
|
|
415
|
-
plan =
|
|
602
|
+
plan = planPush()
|
|
416
603
|
} catch {
|
|
417
604
|
return { code: 'invalid-geometry', ghosts: [] }
|
|
418
605
|
}
|
|
@@ -420,25 +607,27 @@ export function previewRoomPush(
|
|
|
420
607
|
const updates: Array<readonly [AnyNodeId, Partial<AnyNode>]> = plan.changes.flatMap((change) =>
|
|
421
608
|
change.op === 'update' ? [[change.id as AnyNodeId, change.data] as const] : [],
|
|
422
609
|
)
|
|
423
|
-
const levelId = nodes[span.wallId as AnyNodeId]?.parentId
|
|
424
610
|
if (levelId) updates.push(...previewSurfaces(nodes, levelId, applyChanges(nodes, plan.changes)))
|
|
425
611
|
useLiveNodeOverrides
|
|
426
612
|
.getState()
|
|
427
613
|
.setMany(updates.map(([id, data]) => [id, data] as [AnyNodeId, Partial<AnyNode>]))
|
|
428
614
|
previewIds = updates.map(([id]) => id)
|
|
429
615
|
for (const id of previewIds) useScene.getState().markDirty(id as AnyNodeId)
|
|
430
|
-
const ghosts = plan.changes.flatMap((change): RoomPushGhost[] =>
|
|
431
|
-
change.op
|
|
616
|
+
const ghosts = plan.changes.flatMap((change): RoomPushGhost[] => {
|
|
617
|
+
if (change.op !== 'create') return []
|
|
618
|
+
if (change.node.type === 'separator')
|
|
619
|
+
return [{ start: change.node.start, end: change.node.end, ...SEPARATOR_GHOST }]
|
|
620
|
+
return change.node.type === 'wall'
|
|
432
621
|
? [
|
|
433
622
|
{
|
|
434
623
|
start: change.node.start,
|
|
435
624
|
end: change.node.end,
|
|
436
625
|
thickness: change.node.thickness ?? 0.1,
|
|
437
|
-
height: getWallEffectiveHeightForNodes(change.node, nodes),
|
|
626
|
+
height: getWallEffectiveHeightForNodes(change.node, nodes, 0),
|
|
438
627
|
},
|
|
439
628
|
]
|
|
440
|
-
: []
|
|
441
|
-
)
|
|
629
|
+
: []
|
|
630
|
+
})
|
|
442
631
|
// The floor plan draws the same pieces through its bridge-ghost layer.
|
|
443
632
|
if (ghosts.length) {
|
|
444
633
|
useWallMoveGhosts.getState().setBridges(
|
|
@@ -468,9 +657,13 @@ export function endRoomPushPreview() {
|
|
|
468
657
|
export function commitRoomPush(span: WallPushSpan, outward: Point, distance: number) {
|
|
469
658
|
endRoomPushPreview()
|
|
470
659
|
if (Math.abs(distance) < 1e-6) return true
|
|
660
|
+
return commitPushPlan((nodes) => planRoomPush(nodes, span, outward, distance))
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function commitPushPlan(planPush: (nodes: StructureNodes) => StructurePlan) {
|
|
471
664
|
try {
|
|
472
665
|
return runAsSingleSceneHistoryStep(useScene, () => {
|
|
473
|
-
const plan =
|
|
666
|
+
const plan = planPush(useScene.getState().nodes)
|
|
474
667
|
if (!applyRoomPlan(plan)) return false
|
|
475
668
|
const nodes = useScene.getState().nodes
|
|
476
669
|
const patches = plan.changes.flatMap((change) => {
|
|
@@ -716,12 +909,13 @@ export function cancelRoomHandleDrag() {
|
|
|
716
909
|
/**
|
|
717
910
|
* Runs one handle drag on window listeners, independent of the handle's own
|
|
718
911
|
* mount (handles hide while any scope is active). `sample` maps a pointer to
|
|
719
|
-
* the drag's value (`spatial.sample` a controller ray, for a spatial pointer)
|
|
720
|
-
*
|
|
912
|
+
* the drag's value (`spatial.sample` a controller ray, for a spatial pointer):
|
|
913
|
+
* a distance or a height, or a plan point for an end dragged across the floor.
|
|
914
|
+
* Release commits, Escape / ⌘Z / the gesture owner cancel. Cleanup runs
|
|
721
915
|
* exactly once whatever happens, and releases only this drag's own scope;
|
|
722
916
|
* nothing republishes a preview after it.
|
|
723
917
|
*/
|
|
724
|
-
export function runRoomHandleDrag({
|
|
918
|
+
export function runRoomHandleDrag<T = number>({
|
|
725
919
|
label,
|
|
726
920
|
nodeId,
|
|
727
921
|
zoneId,
|
|
@@ -738,9 +932,9 @@ export function runRoomHandleDrag({
|
|
|
738
932
|
zoneId?: string
|
|
739
933
|
levelId: string
|
|
740
934
|
requires: string[]
|
|
741
|
-
sample: (clientX: number, clientY: number) =>
|
|
742
|
-
spatial?: { pointerId: SpatialPointerId; sample: (ray: Ray) =>
|
|
743
|
-
onValue: (value:
|
|
935
|
+
sample: (clientX: number, clientY: number) => T | null
|
|
936
|
+
spatial?: { pointerId: SpatialPointerId; sample: (ray: Ray) => T | null }
|
|
937
|
+
onValue: (value: T) => void
|
|
744
938
|
onCommit: () => void
|
|
745
939
|
onCancel: () => void
|
|
746
940
|
}) {
|
|
@@ -891,10 +1085,20 @@ export function runWallPushDrag({
|
|
|
891
1085
|
spatial?: { pointerId: SpatialPointerId; along: (ray: Ray) => number | null }
|
|
892
1086
|
}) {
|
|
893
1087
|
const nodes = useScene.getState().nodes
|
|
894
|
-
const
|
|
895
|
-
|
|
896
|
-
const
|
|
1088
|
+
const boundary = nodes[handle.wallId as AnyNodeId]
|
|
1089
|
+
// An edge of invisible walls is measured, and lines up, as the one line it moves as.
|
|
1090
|
+
const edge = handle.separators?.length ? separatorEdgeLine(nodes, handle.separators) : null
|
|
1091
|
+
const wall = edge ?? (boundary?.type === 'wall' ? boundary : null)
|
|
1092
|
+
if (!wall) return null
|
|
1093
|
+
const base =
|
|
1094
|
+
wall.type === 'wall'
|
|
1095
|
+
? getWallBaseElevationForNodes(wall, nodes)
|
|
1096
|
+
: handle.height - SEPARATOR_ARROW_LIFT
|
|
897
1097
|
const rooms = pushDimensionRooms(nodes, levelId, handle, base)
|
|
1098
|
+
const levelWalls = Object.values(nodes).filter(
|
|
1099
|
+
(node): node is WallNode => node.type === 'wall' && node.parentId === levelId,
|
|
1100
|
+
)
|
|
1101
|
+
const moving = new Set([handle.wallId, ...(handle.separators ?? []).map((s) => s.separatorId)])
|
|
898
1102
|
let distance = 0
|
|
899
1103
|
let blocked = false
|
|
900
1104
|
let ghosts: RoomPushGhost[] = []
|
|
@@ -921,15 +1125,49 @@ export function runWallPushDrag({
|
|
|
921
1125
|
message,
|
|
922
1126
|
},
|
|
923
1127
|
})
|
|
924
|
-
|
|
925
|
-
|
|
1128
|
+
// The pushed wall lines up with the parallel walls on its level
|
|
1129
|
+
// (`wall-parallel-snap`) before the grid; Alt pushes it free. Invisible walls
|
|
1130
|
+
// line up with the walls and with the level's other invisible walls.
|
|
1131
|
+
const lineUpTargets =
|
|
1132
|
+
wall.type === 'wall'
|
|
1133
|
+
? wallParallelTargets(wall, handle.outward, levelWalls)
|
|
1134
|
+
: wallParallelTargets(separatorWallLine(wall), handle.outward, [
|
|
1135
|
+
...levelWalls,
|
|
1136
|
+
...levelSeparatorLines(nodes, levelId, moving),
|
|
1137
|
+
])
|
|
1138
|
+
let lineUp: WallParallelTarget | null = null
|
|
1139
|
+
let altHeld = false
|
|
1140
|
+
const trackAlt = (event: KeyboardEvent | PointerEvent) => {
|
|
1141
|
+
altHeld = event.altKey
|
|
1142
|
+
}
|
|
1143
|
+
window.addEventListener('pointermove', trackAlt, true)
|
|
1144
|
+
window.addEventListener('keydown', trackAlt, true)
|
|
1145
|
+
window.addEventListener('keyup', trackAlt, true)
|
|
1146
|
+
const stopLineUp = () => {
|
|
1147
|
+
window.removeEventListener('pointermove', trackAlt, true)
|
|
1148
|
+
window.removeEventListener('keydown', trackAlt, true)
|
|
1149
|
+
window.removeEventListener('keyup', trackAlt, true)
|
|
1150
|
+
if (lineUp) publishWallParallelGuide(null)
|
|
1151
|
+
lineUp = null
|
|
1152
|
+
}
|
|
1153
|
+
const toDistance = (at: number | null) => {
|
|
1154
|
+
if (at === null) return null
|
|
1155
|
+
const raw = at - from
|
|
1156
|
+
const next = isWallRunFree({ altKey: altHeld })
|
|
1157
|
+
? null
|
|
1158
|
+
: nearestWallParallelTarget(lineUpTargets, handle.line + raw)
|
|
1159
|
+
if (next !== lineUp) publishWallParallelGuide(next)
|
|
1160
|
+
lineUp = next
|
|
1161
|
+
if (next) return next.position - handle.line
|
|
1162
|
+
return isWallRunFree({ altKey: altHeld }) ? raw : snapPushDistance(handle.line, raw)
|
|
1163
|
+
}
|
|
926
1164
|
publish()
|
|
927
1165
|
return runRoomHandleDrag({
|
|
928
1166
|
label: WALL_PUSH_DRAG_LABEL,
|
|
929
1167
|
nodeId: handle.wallId,
|
|
930
1168
|
zoneId,
|
|
931
1169
|
levelId,
|
|
932
|
-
requires: zoneId ? [zoneId,
|
|
1170
|
+
requires: zoneId ? [zoneId, ...moving] : [...moving],
|
|
933
1171
|
sample: (x, y) => toDistance(along(x, y)),
|
|
934
1172
|
spatial: spatial && {
|
|
935
1173
|
pointerId: spatial.pointerId,
|
|
@@ -942,13 +1180,17 @@ export function runWallPushDrag({
|
|
|
942
1180
|
const preview = previewRoomPush(handle, handle.outward, distance)
|
|
943
1181
|
ghosts = preview.ghosts
|
|
944
1182
|
blocked = !!preview.code
|
|
945
|
-
publish(preview.code ?
|
|
1183
|
+
publish(preview.code ? PUSH_REFUSED_MESSAGE : undefined)
|
|
946
1184
|
},
|
|
947
1185
|
onCommit: () => {
|
|
1186
|
+
stopLineUp()
|
|
948
1187
|
if (blocked) endRoomPushPreview()
|
|
949
1188
|
else commitRoomPush(handle, handle.outward, distance)
|
|
950
1189
|
},
|
|
951
|
-
onCancel: () =>
|
|
1190
|
+
onCancel: () => {
|
|
1191
|
+
stopLineUp()
|
|
1192
|
+
endRoomPushPreview()
|
|
1193
|
+
},
|
|
952
1194
|
})
|
|
953
1195
|
}
|
|
954
1196
|
|
|
@@ -960,10 +1202,258 @@ function pushDimensionRooms(nodes: Nodes, levelId: string, span: WallPushSpan, b
|
|
|
960
1202
|
const index = getRoomSelectionIndex(levelId)
|
|
961
1203
|
const records = index.update(nodes)
|
|
962
1204
|
const topology = index.topology.getLevelTopology(levelId)
|
|
963
|
-
|
|
1205
|
+
const elevationOf = (roomId: string) => {
|
|
964
1206
|
const zoneId = records.find((record) => record.id === roomId)?.zoneId
|
|
965
1207
|
return zoneId ? roomFloorElevation(nodes, zoneId) : base
|
|
1208
|
+
}
|
|
1209
|
+
const edge = span.separators?.length ? separatorEdgeLine(nodes, span.separators) : null
|
|
1210
|
+
return edge && span.separators
|
|
1211
|
+
? separatorEdgeDimensionRooms(
|
|
1212
|
+
topology?.rooms ?? [],
|
|
1213
|
+
{ line: edge, separators: span.separators },
|
|
1214
|
+
(id) => nodes[id],
|
|
1215
|
+
elevationOf,
|
|
1216
|
+
)
|
|
1217
|
+
: wallPushDimensionRooms(topology?.rooms ?? [], span, elevationOf)
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
/**
|
|
1221
|
+
* The one line an edge of invisible walls pushes as: its first separator,
|
|
1222
|
+
* running from the edge's first end to its last.
|
|
1223
|
+
*/
|
|
1224
|
+
function separatorEdgeLine(nodes: Nodes, spans: readonly SeparatorSpan[]): SeparatorNode | null {
|
|
1225
|
+
const first = nodes[spans[0]?.separatorId as AnyNodeId]
|
|
1226
|
+
if (first?.type !== 'separator') return null
|
|
1227
|
+
const dx = first.end[0] - first.start[0]
|
|
1228
|
+
const dz = first.end[1] - first.start[1]
|
|
1229
|
+
const length = Math.hypot(dx, dz)
|
|
1230
|
+
if (length < 1e-6) return null
|
|
1231
|
+
const along = ([x, z]: Point) =>
|
|
1232
|
+
((x - first.start[0]) * dx + (z - first.start[1]) * dz) / (length * length)
|
|
1233
|
+
let lo = Number.POSITIVE_INFINITY
|
|
1234
|
+
let hi = Number.NEGATIVE_INFINITY
|
|
1235
|
+
for (const span of spans) {
|
|
1236
|
+
const separator = nodes[span.separatorId as AnyNodeId]
|
|
1237
|
+
if (separator?.type !== 'separator') return null
|
|
1238
|
+
for (const t of [span.t0, span.t1]) {
|
|
1239
|
+
const u = along([
|
|
1240
|
+
separator.start[0] + (separator.end[0] - separator.start[0]) * t,
|
|
1241
|
+
separator.start[1] + (separator.end[1] - separator.start[1]) * t,
|
|
1242
|
+
])
|
|
1243
|
+
lo = Math.min(lo, u)
|
|
1244
|
+
hi = Math.max(hi, u)
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
const at = (u: number): Point => [first.start[0] + dx * u, first.start[1] + dz * u]
|
|
1248
|
+
return { ...first, start: at(lo), end: at(hi) }
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
/**
|
|
1252
|
+
* A selected wall's temporary dimensions as the scene stands: the room widths
|
|
1253
|
+
* its push shows, before any push, plus the outside size for an exterior wall.
|
|
1254
|
+
*/
|
|
1255
|
+
export function selectedWallDimensions(
|
|
1256
|
+
nodes: Nodes,
|
|
1257
|
+
wall: WallNode,
|
|
1258
|
+
faces: WallDimensionFaces,
|
|
1259
|
+
): WallSelectionDimension[] {
|
|
1260
|
+
const levelId = wall.parentId
|
|
1261
|
+
if (!levelId) return []
|
|
1262
|
+
const base = getWallBaseElevationForNodes(wall, nodes)
|
|
1263
|
+
const walls = Object.values(nodes).filter(
|
|
1264
|
+
(node): node is WallNode => node.type === 'wall' && node.parentId === levelId,
|
|
1265
|
+
)
|
|
1266
|
+
return wallSelectionDimensions({
|
|
1267
|
+
rooms: pushDimensionRooms(nodes, levelId, { wallId: wall.id, t0: 0, t1: 1 }, base),
|
|
1268
|
+
wall,
|
|
1269
|
+
walls,
|
|
1270
|
+
faces,
|
|
1271
|
+
resolve: (id) => nodes[id],
|
|
1272
|
+
baseElevation: base,
|
|
1273
|
+
})
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
/** An end this close to another moved wall is joined to it. */
|
|
1277
|
+
const GROUP_JOIN_REACH = 1e-3
|
|
1278
|
+
/** Walls whose directions cross by less than this (sine) run in line, never meet at a corner. */
|
|
1279
|
+
const GROUP_PARALLEL_SIN = 1e-4
|
|
1280
|
+
|
|
1281
|
+
/** The way a wall's push arrows point: its `a`-side normal at its middle. */
|
|
1282
|
+
function wallPushNormal(wall: WallNode): Point {
|
|
1283
|
+
const frame = getWallCurveFrameAt(wall, 0.5)
|
|
1284
|
+
return [frame.normal.x, frame.normal.y]
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
function wallDirection(wall: WallNode): Point {
|
|
1288
|
+
const dx = wall.end[0] - wall.start[0]
|
|
1289
|
+
const dz = wall.end[1] - wall.start[1]
|
|
1290
|
+
const length = Math.hypot(dx, dz) || 1
|
|
1291
|
+
return [dx / length, dz / length]
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
/** Whether `point` is on `wall`: one of its ends, or along a straight one. */
|
|
1295
|
+
function wallHoldsPoint(wall: WallNode, [x, z]: Point): boolean {
|
|
1296
|
+
const near = ([px, pz]: Point) => Math.hypot(px - x, pz - z) <= GROUP_JOIN_REACH
|
|
1297
|
+
if (near(wall.start) || near(wall.end)) return true
|
|
1298
|
+
if ((wall.curveOffset ?? 0) !== 0) return false
|
|
1299
|
+
const [ux, uz] = wallDirection(wall)
|
|
1300
|
+
const along = (x - wall.start[0]) * ux + (z - wall.start[1]) * uz
|
|
1301
|
+
const length = Math.hypot(wall.end[0] - wall.start[0], wall.end[1] - wall.start[1])
|
|
1302
|
+
return (
|
|
1303
|
+
along > 0 &&
|
|
1304
|
+
along < length &&
|
|
1305
|
+
Math.abs((z - wall.start[1]) * ux - (x - wall.start[0]) * uz) <= GROUP_JOIN_REACH
|
|
1306
|
+
)
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
/**
|
|
1310
|
+
* Moves several walls at once, each as its own push arrow would: along its
|
|
1311
|
+
* normal by `drag` projected on it, so it stays parallel to where it was, the
|
|
1312
|
+
* walls joined to it following through the junction planner. An end on
|
|
1313
|
+
* another moved wall that is not in line with it (a corner or a T between
|
|
1314
|
+
* them) sits on both moved lines, which is where it lands moved by the whole
|
|
1315
|
+
* drag, so they stay joined. Each wall is planned in turn while the others
|
|
1316
|
+
* already stand at their targets, so no moved wall is pulled along as
|
|
1317
|
+
* another's neighbour. One plan, one undo step.
|
|
1318
|
+
*/
|
|
1319
|
+
export function planWallGroupPush(
|
|
1320
|
+
nodes: StructureNodes,
|
|
1321
|
+
wallIds: readonly string[],
|
|
1322
|
+
drag: Point,
|
|
1323
|
+
mintId: (kind: 'zone' | 'wall' | 'separator') => string = generateId,
|
|
1324
|
+
): StructurePlan {
|
|
1325
|
+
const walls = wallIds.flatMap((id) => {
|
|
1326
|
+
const node = nodes[id]
|
|
1327
|
+
return node?.type === 'wall' ? [node] : []
|
|
1328
|
+
})
|
|
1329
|
+
const crosses = (a: WallNode, b: WallNode) => {
|
|
1330
|
+
const [ux, uz] = wallDirection(a)
|
|
1331
|
+
const [vx, vz] = wallDirection(b)
|
|
1332
|
+
return Math.abs(ux * vz - uz * vx) > GROUP_PARALLEL_SIN
|
|
1333
|
+
}
|
|
1334
|
+
let scratch: Scratch = { ...nodes }
|
|
1335
|
+
for (const wall of walls) {
|
|
1336
|
+
const [nx, nz] = wallPushNormal(wall)
|
|
1337
|
+
const distance = drag[0] * nx + drag[1] * nz
|
|
1338
|
+
const move = (point: Point): Point =>
|
|
1339
|
+
walls.some((other) => other !== wall && crosses(wall, other) && wallHoldsPoint(other, point))
|
|
1340
|
+
? [point[0] + drag[0], point[1] + drag[1]]
|
|
1341
|
+
: [point[0] + nx * distance, point[1] + nz * distance]
|
|
1342
|
+
scratch[wall.id] = { ...wall, start: move(wall.start), end: move(wall.end) }
|
|
1343
|
+
}
|
|
1344
|
+
for (const wall of walls) {
|
|
1345
|
+
const target = scratch[wall.id]
|
|
1346
|
+
if (target?.type !== 'wall') continue
|
|
1347
|
+
scratch[wall.id] = { ...target, start: wall.start, end: wall.end }
|
|
1348
|
+
const moved = setWallGeometry(scratch, {
|
|
1349
|
+
wallId: wall.id,
|
|
1350
|
+
start: target.start,
|
|
1351
|
+
end: target.end,
|
|
1352
|
+
mintId,
|
|
1353
|
+
})
|
|
1354
|
+
if (moved.conflicts?.length) return moved
|
|
1355
|
+
scratch = applyChanges(scratch, moved.changes)
|
|
1356
|
+
}
|
|
1357
|
+
return { changes: diffNodes(nodes, scratch) }
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
/** A multi-wall push in flight (`beginWallGroupPush`). */
|
|
1361
|
+
export type WallGroupPush = {
|
|
1362
|
+
/** Previews the walls moved by the level-plan drag. */
|
|
1363
|
+
preview: (drag: Point) => void
|
|
1364
|
+
/** Writes the last preview as one undo step; false when nothing was written. */
|
|
1365
|
+
commit: () => boolean
|
|
1366
|
+
/** Drops the preview; the scene is untouched. */
|
|
1367
|
+
cancel: () => void
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
/**
|
|
1371
|
+
* The live side of `planWallGroupPush`, for a group move whose selection is
|
|
1372
|
+
* walls only: the walls, their neighbours and the room surfaces preview
|
|
1373
|
+
* through overrides, created pieces as ghosts, and each wall's room widths
|
|
1374
|
+
* show as its own push shows them (`useRoomHandleDrag`, kind
|
|
1375
|
+
* `wall-group-push`). The caller owns the gesture: its pointer, keys and scope.
|
|
1376
|
+
*/
|
|
1377
|
+
export function beginWallGroupPush(wallIds: readonly string[], levelId: string): WallGroupPush {
|
|
1378
|
+
const nodes = useScene.getState().nodes
|
|
1379
|
+
const measured = wallIds.flatMap((id) => {
|
|
1380
|
+
const wall = nodes[id as AnyNodeId]
|
|
1381
|
+
if (wall?.type !== 'wall') return []
|
|
1382
|
+
const base = getWallBaseElevationForNodes(wall, nodes)
|
|
1383
|
+
return [
|
|
1384
|
+
{
|
|
1385
|
+
wall,
|
|
1386
|
+
base,
|
|
1387
|
+
top: base + getWallEffectiveHeightForNodes(wall, nodes, 0),
|
|
1388
|
+
normal: wallPushNormal(wall),
|
|
1389
|
+
rooms: pushDimensionRooms(nodes, levelId, { wallId: wall.id, t0: 0, t1: 1 }, base),
|
|
1390
|
+
},
|
|
1391
|
+
]
|
|
966
1392
|
})
|
|
1393
|
+
const middle: Point = [0, 0]
|
|
1394
|
+
for (const { wall } of measured) {
|
|
1395
|
+
middle[0] += (wall.start[0] + wall.end[0]) / 2 / measured.length
|
|
1396
|
+
middle[1] += (wall.start[1] + wall.end[1]) / 2 / measured.length
|
|
1397
|
+
}
|
|
1398
|
+
let drag: Point = [0, 0]
|
|
1399
|
+
let blocked = false
|
|
1400
|
+
let shown = false
|
|
1401
|
+
const dimensions = () => {
|
|
1402
|
+
const kept: RoomDimension[] = []
|
|
1403
|
+
for (const { wall, normal, rooms } of measured) {
|
|
1404
|
+
const distance = drag[0] * normal[0] + drag[1] * normal[1]
|
|
1405
|
+
// Two moved walls across one room both measure it: the width shows once.
|
|
1406
|
+
for (const dimension of wallPushDimensions({
|
|
1407
|
+
rooms,
|
|
1408
|
+
wall,
|
|
1409
|
+
outward: normal,
|
|
1410
|
+
distance,
|
|
1411
|
+
resolve: livePreviewNode,
|
|
1412
|
+
}))
|
|
1413
|
+
if (!kept.some((other) => sameRoomDimension(other, dimension)))
|
|
1414
|
+
kept.push({ ...dimension, key: `${wall.id}:${dimension.key}` })
|
|
1415
|
+
}
|
|
1416
|
+
return kept
|
|
1417
|
+
}
|
|
1418
|
+
const publish = (ghosts: RoomPushGhost[], message?: string) =>
|
|
1419
|
+
useRoomHandleDrag.setState({
|
|
1420
|
+
drag: {
|
|
1421
|
+
kind: 'wall-group-push',
|
|
1422
|
+
levelId,
|
|
1423
|
+
anchor: [middle[0] + drag[0], middle[1] + drag[1]],
|
|
1424
|
+
height: Math.max(...measured.map(({ top }) => top)),
|
|
1425
|
+
base: Math.min(...measured.map(({ base }) => base)),
|
|
1426
|
+
ghosts,
|
|
1427
|
+
dimensions: dimensions(),
|
|
1428
|
+
message,
|
|
1429
|
+
},
|
|
1430
|
+
})
|
|
1431
|
+
const end = () => {
|
|
1432
|
+
endRoomPushPreview()
|
|
1433
|
+
useRoomHandleDrag.setState({ drag: null })
|
|
1434
|
+
}
|
|
1435
|
+
return {
|
|
1436
|
+
preview: (next) => {
|
|
1437
|
+
if (shown && next[0] === drag[0] && next[1] === drag[1]) return
|
|
1438
|
+
shown = true
|
|
1439
|
+
drag = [next[0], next[1]]
|
|
1440
|
+
clearPushPreview()
|
|
1441
|
+
const preview =
|
|
1442
|
+
Math.hypot(drag[0], drag[1]) < 1e-6
|
|
1443
|
+
? { ghosts: [] }
|
|
1444
|
+
: previewPushPlan(useScene.getState().nodes, levelId, () =>
|
|
1445
|
+
planWallGroupPush(useScene.getState().nodes, wallIds, drag, previewMint()),
|
|
1446
|
+
)
|
|
1447
|
+
blocked = !!preview.code
|
|
1448
|
+
publish(preview.ghosts, preview.code ? PUSH_REFUSED_MESSAGE : undefined)
|
|
1449
|
+
},
|
|
1450
|
+
commit: () => {
|
|
1451
|
+
end()
|
|
1452
|
+
if (blocked || Math.hypot(drag[0], drag[1]) < 1e-6) return false
|
|
1453
|
+
return commitPushPlan((current) => planWallGroupPush(current, wallIds, drag))
|
|
1454
|
+
},
|
|
1455
|
+
cancel: end,
|
|
1456
|
+
}
|
|
967
1457
|
}
|
|
968
1458
|
|
|
969
1459
|
/** A node as the push preview draws it: the scene's, with its live override merged. */
|
|
@@ -986,6 +1476,7 @@ export function wallPointAt(wall: WallNode, t: number): Point {
|
|
|
986
1476
|
*/
|
|
987
1477
|
export const WALL_PUSH_AFFORDANCE = 'wall-push'
|
|
988
1478
|
|
|
1479
|
+
/** A wall's or an invisible wall's side arrow: `wallId` is either. */
|
|
989
1480
|
export type WallPushArrowPayload = { wallId: string; side: 'a' | 'b' }
|
|
990
1481
|
|
|
991
1482
|
type PlanPointer = (clientX: number, clientY: number) => readonly [number, number] | null
|
|
@@ -1001,8 +1492,9 @@ function planAlong(position: Point, outward: Point, toPlan: PlanPointer) {
|
|
|
1001
1492
|
}
|
|
1002
1493
|
|
|
1003
1494
|
/**
|
|
1004
|
-
* A floor-plan wall arrow: the 3D arrow's handle (`wallPushHandles
|
|
1005
|
-
*
|
|
1495
|
+
* A floor-plan wall arrow: the 3D arrow's handle (`wallPushHandles`, or a
|
|
1496
|
+
* selected invisible wall's `separatorPushHandles`), drag (`runWallPushDrag`)
|
|
1497
|
+
* and commit, with the pointer read in plan coordinates.
|
|
1006
1498
|
*/
|
|
1007
1499
|
export function runFloorplanWallPush(
|
|
1008
1500
|
payload: unknown,
|
|
@@ -1013,8 +1505,12 @@ export function runFloorplanWallPush(
|
|
|
1013
1505
|
const { wallId, side } = (payload ?? {}) as Partial<WallPushArrowPayload>
|
|
1014
1506
|
const nodes = useScene.getState().nodes
|
|
1015
1507
|
const wall = wallId ? nodes[wallId as AnyNodeId] : undefined
|
|
1016
|
-
if (wall?.type !== 'wall' || !wall.parentId) return null
|
|
1017
|
-
const
|
|
1508
|
+
if ((wall?.type !== 'wall' && wall?.type !== 'separator') || !wall.parentId) return null
|
|
1509
|
+
const handles =
|
|
1510
|
+
wall.type === 'wall'
|
|
1511
|
+
? wallPushHandles(wall, nodes)
|
|
1512
|
+
: separatorPushHandles(wall, separatorFloorElevation(nodes, wall.id))
|
|
1513
|
+
const handle = handles.find((h) => h.key === `${wall.id}:${side}`)
|
|
1018
1514
|
if (!handle) return null
|
|
1019
1515
|
return runFloorplanRoomPush({ handle, levelId: wall.parentId, clientX, clientY, toPlan })
|
|
1020
1516
|
}
|