@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
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
useLiveTerrain,
|
|
11
11
|
} from '@pascal-app/core'
|
|
12
12
|
import { useViewer } from '@pascal-app/viewer'
|
|
13
|
-
import { Html } from '@react-three/drei'
|
|
14
13
|
import { useFrame, useThree } from '@react-three/fiber'
|
|
15
14
|
import { type MutableRefObject, useEffect, useMemo, useRef } from 'react'
|
|
16
15
|
import {
|
|
@@ -25,6 +24,7 @@ import { EDITOR_LAYER } from '../../../lib/constants'
|
|
|
25
24
|
import { sculptFieldForSite, terrainPointInsideSite } from '../../../lib/terrain-sculpt'
|
|
26
25
|
import { brushRingColor } from '../../../lib/terrain-verb-color'
|
|
27
26
|
import useEditor from '../../../store/use-editor'
|
|
27
|
+
import { Html } from '../../editor/localized-html'
|
|
28
28
|
import { formatMeasurement } from '../../editor/measurement-pill'
|
|
29
29
|
|
|
30
30
|
/** Segments in the cursor ring. 64 reads as a circle at any usable brush size. */
|
|
@@ -41,7 +41,6 @@ import { FloorRegionControls3D } from '../editor/floor-region-controls'
|
|
|
41
41
|
import { OpenWallEnds3DLayer } from '../editor/open-wall-ends-3d-layer'
|
|
42
42
|
import { OpeningGuides3DLayer } from '../editor/opening-guides-3d-layer'
|
|
43
43
|
import { WallSnapBeaconLayer } from '../editor/wall-snap-beacon-layer'
|
|
44
|
-
import { ElevatorTool } from './elevator/elevator-tool'
|
|
45
44
|
import { MoveTool } from './item/move-tool'
|
|
46
45
|
import { WallPaintRegionTool } from './paint-region/wall-paint-region-tool'
|
|
47
46
|
import { RegistryToolProvider } from './registry-tool-context'
|
|
@@ -305,21 +304,6 @@ export const ToolManager: React.FC = () => {
|
|
|
305
304
|
const handlePlacedNodeSelected = (nodeId: AnyNodeId) => {
|
|
306
305
|
setSelection({ selectedIds: [nodeId] })
|
|
307
306
|
}
|
|
308
|
-
const handlePlacedElevatorSelected = (
|
|
309
|
-
nodeId: AnyNodeId,
|
|
310
|
-
elevatorBuildingId: BuildingNode['id'],
|
|
311
|
-
) => {
|
|
312
|
-
// Preserve the active level. `setSelection`'s hierarchy guard nulls
|
|
313
|
-
// `levelId` whenever `buildingId` is passed without an explicit
|
|
314
|
-
// `levelId` — which deselected the current floor plan the moment an
|
|
315
|
-
// elevator was placed. Pass the current level through so the floor
|
|
316
|
-
// plan stays selected.
|
|
317
|
-
setSelection({
|
|
318
|
-
buildingId: elevatorBuildingId,
|
|
319
|
-
levelId: activeLevelId ?? null,
|
|
320
|
-
selectedIds: [nodeId],
|
|
321
|
-
})
|
|
322
|
-
}
|
|
323
307
|
|
|
324
308
|
return (
|
|
325
309
|
<RegistryToolProvider value={registryToolContext}>
|
|
@@ -463,14 +447,7 @@ export const ToolManager: React.FC = () => {
|
|
|
463
447
|
<RegistryToolComponent />
|
|
464
448
|
</Suspense>
|
|
465
449
|
)}
|
|
466
|
-
{!movingNode &&
|
|
467
|
-
<ElevatorTool
|
|
468
|
-
buildingId={buildingId as BuildingNode['id'] | null}
|
|
469
|
-
levelId={activeLevelId ?? null}
|
|
470
|
-
onPlaced={handlePlacedElevatorSelected}
|
|
471
|
-
/>
|
|
472
|
-
)}
|
|
473
|
-
{!movingNode && BuildToolComponent && tool !== 'elevator' ? <BuildToolComponent /> : null}
|
|
450
|
+
{!movingNode && BuildToolComponent ? <BuildToolComponent /> : null}
|
|
474
451
|
{/* Figma-style alignment guides published by the move / placement
|
|
475
452
|
tools above. Lives inside the building-local group so the
|
|
476
453
|
building-local guide coords render at the right world position. */}
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
createTerrainField,
|
|
10
10
|
DoorNode as DoorSchema,
|
|
11
11
|
encodeTerrainField,
|
|
12
|
+
findOpenWallEnds,
|
|
12
13
|
flattenPatch,
|
|
13
14
|
GROUND_SUPPORT_ID,
|
|
14
15
|
getFloorPlacedElevation,
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
resolveTerrainWallConstructionOptions,
|
|
20
21
|
runAsSingleSceneHistoryStep,
|
|
21
22
|
SlabNode,
|
|
23
|
+
snapWorldXZToBuildingLocal,
|
|
22
24
|
spatialGridManager,
|
|
23
25
|
useScene,
|
|
24
26
|
type WallNode,
|
|
@@ -30,11 +32,15 @@ import {
|
|
|
30
32
|
migrateRoomZones,
|
|
31
33
|
} from '@pascal-app/core/scene-migrations'
|
|
32
34
|
import { useViewer } from '@pascal-app/viewer'
|
|
33
|
-
import
|
|
35
|
+
import type { SnappingMode } from '../../../lib/snapping-mode'
|
|
36
|
+
import { snapBuildingLocalToWorldGrid } from '../../../lib/world-grid-snap'
|
|
37
|
+
import useEditor, { isAngleSnapActive, isMagneticSnapActive } from '../../../store/use-editor'
|
|
34
38
|
import useInteractionScope from '../../../store/use-interaction-scope'
|
|
35
39
|
import {
|
|
36
40
|
createWallOnCurrentLevel,
|
|
41
|
+
getSegmentGridStep,
|
|
37
42
|
resolveEndpointWallSplit,
|
|
43
|
+
snapWallDraftPoint,
|
|
38
44
|
snapWallDraftPointDetailed,
|
|
39
45
|
} from './wall-drafting'
|
|
40
46
|
import type { WallPlanPoint } from './wall-snap-geometry'
|
|
@@ -758,27 +764,64 @@ describe('createWallOnCurrentLevel', () => {
|
|
|
758
764
|
expect(levelWalls()).toHaveLength(2)
|
|
759
765
|
})
|
|
760
766
|
|
|
761
|
-
test('
|
|
762
|
-
|
|
767
|
+
test.each(['grid', 'lines', 'angles', 'off'] as const)(
|
|
768
|
+
'%s drafting splits the host at a mid-wall junction',
|
|
769
|
+
(mode) => {
|
|
770
|
+
useEditor.getState().setSnappingMode('wall', mode)
|
|
771
|
+
const created = createWallOnCurrentLevel([2, 2], [2, 0])
|
|
772
|
+
|
|
773
|
+
expect(created?.end).toEqual([2, 0])
|
|
774
|
+
expect(useScene.getState().nodes['wall_a' as AnyNodeId]).toBeUndefined()
|
|
775
|
+
expect(
|
|
776
|
+
levelWalls()
|
|
777
|
+
.filter((wall) => wall.id !== created?.id)
|
|
778
|
+
.map(({ start, end }) => [start, end]),
|
|
779
|
+
).toEqual([
|
|
780
|
+
[
|
|
781
|
+
[0, 0],
|
|
782
|
+
[2, 0],
|
|
783
|
+
],
|
|
784
|
+
[
|
|
785
|
+
[2, 0],
|
|
786
|
+
[4, 0],
|
|
787
|
+
],
|
|
788
|
+
])
|
|
789
|
+
},
|
|
790
|
+
)
|
|
763
791
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
const
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
792
|
+
test('a wall drawn exactly over one built another way takes its place', () => {
|
|
793
|
+
// The seed wall is plain; the wall tool draws a thicker one.
|
|
794
|
+
const previousDefaults = useEditor.getState().toolDefaults
|
|
795
|
+
useEditor.setState({ toolDefaults: { ...previousDefaults, wall: { thickness: 0.3 } } } as never)
|
|
796
|
+
try {
|
|
797
|
+
const drawn = createWallOnCurrentLevel([0, 0], [4, 0])
|
|
798
|
+
expect(drawn).not.toBeNull()
|
|
799
|
+
expect(useScene.getState().nodes['wall_a' as AnyNodeId]).toBeUndefined()
|
|
800
|
+
expect(levelWalls().map((wall) => wall.id)).toEqual([drawn!.id])
|
|
801
|
+
} finally {
|
|
802
|
+
useEditor.setState({ toolDefaults: previousDefaults } as never)
|
|
803
|
+
}
|
|
774
804
|
})
|
|
775
805
|
|
|
776
|
-
test('
|
|
806
|
+
test('a wall drawn exactly over one built the same is refused', () => {
|
|
777
807
|
expect(createWallOnCurrentLevel([0, 0], [4, 0])).toBeNull()
|
|
778
808
|
expect(levelWalls()).toHaveLength(1)
|
|
779
809
|
})
|
|
780
810
|
|
|
781
|
-
test('
|
|
811
|
+
test('a wall drawn on along part of another takes that stretch over', () => {
|
|
812
|
+
const drawn = createWallOnCurrentLevel([2, 0], [6, 0])
|
|
813
|
+
expect(drawn).not.toBeNull()
|
|
814
|
+
const walls = levelWalls()
|
|
815
|
+
// The seed wall keeps [0, 2]; nothing else runs along [2, 4].
|
|
816
|
+
expect(walls).toHaveLength(2)
|
|
817
|
+
const seed = walls.find((wall) => wall.id !== drawn!.id)!
|
|
818
|
+
expect([seed.start, seed.end]).toEqual([
|
|
819
|
+
[0, 0],
|
|
820
|
+
[2, 0],
|
|
821
|
+
])
|
|
822
|
+
})
|
|
823
|
+
|
|
824
|
+
test('grid mode: endpoint resolved onto a wall body splits its host', () => {
|
|
782
825
|
useEditor.getState().setSnappingMode('wall', 'grid')
|
|
783
826
|
|
|
784
827
|
const created = createWallOnCurrentLevel([2, 2], [2, 0])
|
|
@@ -798,7 +841,7 @@ describe('createWallOnCurrentLevel', () => {
|
|
|
798
841
|
expect(levelWalls()).toHaveLength(2)
|
|
799
842
|
})
|
|
800
843
|
|
|
801
|
-
test('
|
|
844
|
+
test('a T into a wall rehosts its door at the same position', () => {
|
|
802
845
|
const door = DoorSchema.parse({
|
|
803
846
|
position: [1, 1.05, 0],
|
|
804
847
|
parentId: 'wall_a',
|
|
@@ -809,12 +852,12 @@ describe('createWallOnCurrentLevel', () => {
|
|
|
809
852
|
const created = createWallOnCurrentLevel([2, 2], [2, 0])
|
|
810
853
|
|
|
811
854
|
expect(created?.end).toEqual([2, 0])
|
|
812
|
-
const
|
|
813
|
-
|
|
814
|
-
expect(
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
855
|
+
const kept = useScene.getState().nodes[door.id as AnyNodeId] as DoorSchema
|
|
856
|
+
expect(kept.parentId).not.toBe('wall_a')
|
|
857
|
+
expect(kept.position[0]).toBe(1)
|
|
858
|
+
expect((useScene.getState().nodes[kept.parentId as AnyNodeId] as WallNode).children).toContain(
|
|
859
|
+
door.id,
|
|
860
|
+
)
|
|
818
861
|
})
|
|
819
862
|
|
|
820
863
|
test('a splitting commit lands as a single undo step', () => {
|
|
@@ -826,7 +869,7 @@ describe('createWallOnCurrentLevel', () => {
|
|
|
826
869
|
expect(useScene.temporal.getState().pastStates.length - before).toBe(1)
|
|
827
870
|
})
|
|
828
871
|
|
|
829
|
-
test('a crossing splits both
|
|
872
|
+
test('a crossing splits both walls in one undo step', () => {
|
|
830
873
|
const before = useScene.temporal.getState().pastStates.length
|
|
831
874
|
|
|
832
875
|
const created = createWallOnCurrentLevel([2, -2], [2, 2])
|
|
@@ -1095,4 +1138,237 @@ describe('snapWallDraftPointDetailed', () => {
|
|
|
1095
1138
|
expect(freed.point).toEqual([2, 0])
|
|
1096
1139
|
expect(freed.snap).toBeNull()
|
|
1097
1140
|
})
|
|
1141
|
+
|
|
1142
|
+
test('an end aimed into an L corner block joins the corner and reads as one', () => {
|
|
1143
|
+
const south = { ...makeWall([0, 0], [4, 0], 'wall_south'), thickness: 0.2 }
|
|
1144
|
+
const west = { ...makeWall([0, 4], [0, 0], 'wall_west'), thickness: 0.2 }
|
|
1145
|
+
const result = snapWallDraftPointDetailed({
|
|
1146
|
+
point: [0.08, 0.08],
|
|
1147
|
+
walls: [south, west],
|
|
1148
|
+
start: [2, 3],
|
|
1149
|
+
magnetic: false,
|
|
1150
|
+
step: 0,
|
|
1151
|
+
captureWallBodies: true,
|
|
1152
|
+
})
|
|
1153
|
+
expect(result.point).toEqual([0, 0])
|
|
1154
|
+
expect(result.snap).toBe('endpoint')
|
|
1155
|
+
expect(result.targetWallIds).toEqual(['wall_south', 'wall_west'])
|
|
1156
|
+
})
|
|
1157
|
+
|
|
1158
|
+
// The 3D tool snaps to the floor below as a reference but can't join it: an
|
|
1159
|
+
// end by a lower wall's face stays put, while its corner still snaps.
|
|
1160
|
+
test('walls left out of bodyWalls never capture an end, but still snap corners', () => {
|
|
1161
|
+
const lower = { ...makeWall([0, 0.4], [4, 0.4], 'wall_lower'), thickness: 0.2 }
|
|
1162
|
+
const snap = (point: WallPlanPoint, bodyWalls?: WallNode[]) =>
|
|
1163
|
+
snapWallDraftPointDetailed({
|
|
1164
|
+
point,
|
|
1165
|
+
walls: [lower],
|
|
1166
|
+
start: [1, 3],
|
|
1167
|
+
magnetic: false,
|
|
1168
|
+
step: 0,
|
|
1169
|
+
captureWallBodies: true,
|
|
1170
|
+
bodyWalls,
|
|
1171
|
+
})
|
|
1172
|
+
|
|
1173
|
+
const captured = snap([1, 0.5])
|
|
1174
|
+
expect(captured.snap).toBe('wall')
|
|
1175
|
+
expect(captured.point[1]).toBeCloseTo(0.4, 12)
|
|
1176
|
+
|
|
1177
|
+
const free = snap([1, 0.5], [])
|
|
1178
|
+
expect(free.point).toEqual([1, 0.5])
|
|
1179
|
+
expect(free.snap).toBeNull()
|
|
1180
|
+
|
|
1181
|
+
const corner = snap([0.02, 0.42], [])
|
|
1182
|
+
expect(corner.point).toEqual([0, 0.4])
|
|
1183
|
+
expect(corner.snap).toBe('endpoint')
|
|
1184
|
+
})
|
|
1098
1185
|
})
|
|
1186
|
+
|
|
1187
|
+
// The site planner sets a house on its lot turned and offset. Its walls are
|
|
1188
|
+
// drawn square to the house, in building-local metres. An interior wall drawn
|
|
1189
|
+
// across it has to meet both exterior walls exactly and stay square to the
|
|
1190
|
+
// house however the building is turned, in 3D and on the plan alike.
|
|
1191
|
+
const HOUSE_BUILDING_ID = 'building_lot'
|
|
1192
|
+
// A 30' × 24' house of 8" walls, 2' × 1' off the building origin, so no wall
|
|
1193
|
+
// line or face lies on the 0.5 m lattice.
|
|
1194
|
+
const HOUSE_MIN: WallPlanPoint = [0.6096, 0.3048]
|
|
1195
|
+
const HOUSE_MAX: WallPlanPoint = [9.7536, 7.62]
|
|
1196
|
+
const EXTERIOR_THICKNESS = 0.2032
|
|
1197
|
+
// Where a builder aims an interior wall's end: just inside the exterior
|
|
1198
|
+
// wall's interior face (1.18" off it).
|
|
1199
|
+
const FACE_AIM = EXTERIOR_THICKNESS / 2 + 0.03
|
|
1200
|
+
|
|
1201
|
+
const BUILDING_POSES: Array<{
|
|
1202
|
+
label: string
|
|
1203
|
+
yaw: number
|
|
1204
|
+
position: [number, number, number]
|
|
1205
|
+
}> = [
|
|
1206
|
+
{ label: 'an unturned building at the origin', yaw: 0, position: [0, 0, 0] },
|
|
1207
|
+
{
|
|
1208
|
+
label: 'a building turned 17° and offset on its lot',
|
|
1209
|
+
yaw: (17 * Math.PI) / 180,
|
|
1210
|
+
position: [12.37, 0, -8.91],
|
|
1211
|
+
},
|
|
1212
|
+
]
|
|
1213
|
+
|
|
1214
|
+
function seedHouse({ yaw, position }: { yaw: number; position: [number, number, number] }) {
|
|
1215
|
+
const [x0, z0] = HOUSE_MIN
|
|
1216
|
+
const [x1, z1] = HOUSE_MAX
|
|
1217
|
+
const walls = [
|
|
1218
|
+
makeWall([x0, z0], [x1, z0], 'wall_south'),
|
|
1219
|
+
makeWall([x1, z0], [x1, z1], 'wall_east'),
|
|
1220
|
+
makeWall([x1, z1], [x0, z1], 'wall_north'),
|
|
1221
|
+
makeWall([x0, z1], [x0, z0], 'wall_west'),
|
|
1222
|
+
].map((wall) => ({ ...wall, thickness: EXTERIOR_THICKNESS }))
|
|
1223
|
+
const building = BuildingNode.parse({
|
|
1224
|
+
id: HOUSE_BUILDING_ID,
|
|
1225
|
+
position,
|
|
1226
|
+
rotation: [0, yaw, 0],
|
|
1227
|
+
children: [LEVEL_ID],
|
|
1228
|
+
})
|
|
1229
|
+
const level = LevelSchema.parse({ id: LEVEL_ID, parentId: building.id, level: 0, height: 2.5 })
|
|
1230
|
+
useScene.setState({
|
|
1231
|
+
nodes: Object.fromEntries([
|
|
1232
|
+
[building.id, building],
|
|
1233
|
+
[level.id, { ...level, children: walls.map((wall) => wall.id) }],
|
|
1234
|
+
...walls.map((wall) => [wall.id, wall] as const),
|
|
1235
|
+
]),
|
|
1236
|
+
rootNodeIds: [building.id],
|
|
1237
|
+
dirtyNodes: new Set(),
|
|
1238
|
+
collections: {},
|
|
1239
|
+
} as never)
|
|
1240
|
+
useViewer.setState({
|
|
1241
|
+
selection: {
|
|
1242
|
+
buildingId: HOUSE_BUILDING_ID,
|
|
1243
|
+
levelId: LEVEL_ID,
|
|
1244
|
+
zoneId: null,
|
|
1245
|
+
selectedIds: [],
|
|
1246
|
+
},
|
|
1247
|
+
} as never)
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
for (const pose of BUILDING_POSES) {
|
|
1251
|
+
describe(`an interior wall drawn across a house in ${pose.label}`, () => {
|
|
1252
|
+
const [, z0] = HOUSE_MIN
|
|
1253
|
+
const [, z1] = HOUSE_MAX
|
|
1254
|
+
|
|
1255
|
+
beforeEach(() => {
|
|
1256
|
+
useEditor.getState().setGridSnapStep(0.5)
|
|
1257
|
+
// A reshaping-endpoint scope resolves to the 'wall' snap context
|
|
1258
|
+
// without the node registry, as in `createWallOnCurrentLevel` above.
|
|
1259
|
+
useInteractionScope
|
|
1260
|
+
.getState()
|
|
1261
|
+
.begin({ kind: 'reshaping', nodeId: 'wall_south', reshape: 'endpoint', driver: 'tool' })
|
|
1262
|
+
seedHouse(pose)
|
|
1263
|
+
useScene.temporal.getState().clear()
|
|
1264
|
+
useScene.temporal.getState().resume()
|
|
1265
|
+
})
|
|
1266
|
+
|
|
1267
|
+
afterEach(() => {
|
|
1268
|
+
useEditor.getState().setSnappingMode('wall', 'grid')
|
|
1269
|
+
})
|
|
1270
|
+
|
|
1271
|
+
const setMode = (mode: SnappingMode) => useEditor.getState().setSnappingMode('wall', mode)
|
|
1272
|
+
|
|
1273
|
+
// The cursor as a grid event hands it over: a hit on the world plane,
|
|
1274
|
+
// taken into the building's frame.
|
|
1275
|
+
const cursorAt = (local: WallPlanPoint): WallPlanPoint => {
|
|
1276
|
+
const cos = Math.cos(pose.yaw)
|
|
1277
|
+
const sin = Math.sin(pose.yaw)
|
|
1278
|
+
const worldX = pose.position[0] + local[0] * cos + local[1] * sin
|
|
1279
|
+
const worldZ = pose.position[2] - local[0] * sin + local[1] * cos
|
|
1280
|
+
return snapWorldXZToBuildingLocal(worldX, worldZ, pose.position, pose.yaw, 0).local
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
// The 3D wall tool's snap call.
|
|
1284
|
+
const snapIn3d = (point: WallPlanPoint, start?: WallPlanPoint, captureWallBodies = true) =>
|
|
1285
|
+
snapWallDraftPointDetailed({
|
|
1286
|
+
point,
|
|
1287
|
+
walls: levelWalls(),
|
|
1288
|
+
start,
|
|
1289
|
+
angleSnap: start !== undefined && isAngleSnapActive(),
|
|
1290
|
+
magnetic: isMagneticSnapActive(),
|
|
1291
|
+
captureWallBodies,
|
|
1292
|
+
})
|
|
1293
|
+
|
|
1294
|
+
// The plan's click: the same cursor through the plan's grid callback.
|
|
1295
|
+
const snapOnPlan = (point: WallPlanPoint, start?: WallPlanPoint) =>
|
|
1296
|
+
snapWallDraftPoint({
|
|
1297
|
+
point,
|
|
1298
|
+
walls: levelWalls(),
|
|
1299
|
+
start,
|
|
1300
|
+
angleSnap: start !== undefined && isAngleSnapActive(),
|
|
1301
|
+
magnetic: isMagneticSnapActive(),
|
|
1302
|
+
gridSnap: (p) => snapBuildingLocalToWorldGrid(p, getSegmentGridStep()),
|
|
1303
|
+
captureWallBodies: true,
|
|
1304
|
+
})
|
|
1305
|
+
|
|
1306
|
+
const partitionOpenEnds = (partition: WallNode) =>
|
|
1307
|
+
findOpenWallEnds(useScene.getState().nodes, LEVEL_ID).filter(
|
|
1308
|
+
(end) => end.wallId === partition.id,
|
|
1309
|
+
)
|
|
1310
|
+
|
|
1311
|
+
test('grid: both ends land on the exterior walls, square to the house, with no gap', () => {
|
|
1312
|
+
setMode('grid')
|
|
1313
|
+
const startAim = cursorAt([4.2, z0 + FACE_AIM])
|
|
1314
|
+
const start = snapIn3d(startAim)
|
|
1315
|
+
expect(start.snap).toBe('wall')
|
|
1316
|
+
expect(start.targetWallIds).toEqual(['wall_south'])
|
|
1317
|
+
expect(start.point[0]).toBeCloseTo(4, 9)
|
|
1318
|
+
expect(start.point[1]).toBeCloseTo(z0, 9)
|
|
1319
|
+
|
|
1320
|
+
// The cursor drifts a little sideways on its way across the house.
|
|
1321
|
+
const endAim = cursorAt([4.1, z1 - FACE_AIM])
|
|
1322
|
+
const end = snapIn3d(endAim, start.point)
|
|
1323
|
+
expect(end.snap).toBe('wall')
|
|
1324
|
+
expect(end.targetWallIds).toEqual(['wall_north'])
|
|
1325
|
+
expect(end.point[0]).toBeCloseTo(start.point[0], 9)
|
|
1326
|
+
expect(end.point[1]).toBeCloseTo(z1, 9)
|
|
1327
|
+
|
|
1328
|
+
// The plan snaps the same cursor to the same points.
|
|
1329
|
+
expect(snapOnPlan(startAim)).toEqual(start.point)
|
|
1330
|
+
expect(snapOnPlan(endAim, start.point)).toEqual(end.point)
|
|
1331
|
+
|
|
1332
|
+
const partition = createWallOnCurrentLevel(start.point, end.point)
|
|
1333
|
+
expect(partition).not.toBeNull()
|
|
1334
|
+
expect(partitionOpenEnds(partition!)).toEqual([])
|
|
1335
|
+
})
|
|
1336
|
+
|
|
1337
|
+
test('ortho holds the wall square to the house and lands it on the far wall', () => {
|
|
1338
|
+
setMode('angles')
|
|
1339
|
+
const startAim = cursorAt([4.2, z0 + FACE_AIM])
|
|
1340
|
+
const start = snapIn3d(startAim)
|
|
1341
|
+
expect(start.point[0]).toBeCloseTo(4.2, 9)
|
|
1342
|
+
expect(start.point[1]).toBeCloseTo(z0, 9)
|
|
1343
|
+
|
|
1344
|
+
// 4° off square: the 15° lock squares it to the house.
|
|
1345
|
+
const endAim = cursorAt([3.7, z1 - FACE_AIM])
|
|
1346
|
+
const end = snapIn3d(endAim, start.point)
|
|
1347
|
+
expect(end.snap).toBe('wall')
|
|
1348
|
+
expect(end.point[0]).toBeCloseTo(start.point[0], 9)
|
|
1349
|
+
expect(end.point[1]).toBeCloseTo(z1, 9)
|
|
1350
|
+
|
|
1351
|
+
const onPlan = snapOnPlan(endAim, start.point)
|
|
1352
|
+
expect(onPlan[0]).toBeCloseTo(end.point[0], 9)
|
|
1353
|
+
expect(onPlan[1]).toBeCloseTo(end.point[1], 9)
|
|
1354
|
+
|
|
1355
|
+
const partition = createWallOnCurrentLevel(start.point, end.point)
|
|
1356
|
+
expect(partition).not.toBeNull()
|
|
1357
|
+
expect(partitionOpenEnds(partition!)).toEqual([])
|
|
1358
|
+
})
|
|
1359
|
+
|
|
1360
|
+
test('without the body snap the lattice stops the end short of the face: an open gap', () => {
|
|
1361
|
+
setMode('grid')
|
|
1362
|
+
const start = snapIn3d(cursorAt([4.2, z0 + FACE_AIM]))
|
|
1363
|
+
const end = snapIn3d(cursorAt([4.1, z1 - FACE_AIM]), start.point, false)
|
|
1364
|
+
// The lattice line under the cursor, 0.72" short of the north wall's face.
|
|
1365
|
+
expect(end.snap).toBeNull()
|
|
1366
|
+
expect(end.point[1]).toBeCloseTo(7.5, 9)
|
|
1367
|
+
|
|
1368
|
+
const partition = createWallOnCurrentLevel(start.point, end.point)
|
|
1369
|
+
const [open] = partitionOpenEnds(partition!)
|
|
1370
|
+
expect(open?.reason).toBe('gap')
|
|
1371
|
+
expect(open?.gap).toBeCloseTo(z1 - EXTERIOR_THICKNESS / 2 - 7.5, 3)
|
|
1372
|
+
})
|
|
1373
|
+
})
|
|
1374
|
+
}
|
|
@@ -1,21 +1,30 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type AnyNodeId,
|
|
3
3
|
DEFAULT_ANGLE_STEP,
|
|
4
|
+
finishWallEdit,
|
|
5
|
+
generateId,
|
|
6
|
+
planSeparatorInsertion,
|
|
4
7
|
planWallInsertion,
|
|
5
8
|
planWallSplitAtPoint,
|
|
9
|
+
railingWallDefaults,
|
|
6
10
|
resolveWallConstruction,
|
|
7
11
|
runAsSingleSceneHistoryStep,
|
|
12
|
+
type SeparatorNode,
|
|
13
|
+
sameWallConstruction,
|
|
8
14
|
snapPointAlongAngleRay,
|
|
9
15
|
useScene,
|
|
10
16
|
type WallConstructionOptions,
|
|
11
17
|
type WallNode,
|
|
18
|
+
wallsRunAlong,
|
|
12
19
|
} from '@pascal-app/core'
|
|
13
20
|
import { useViewer } from '@pascal-app/viewer'
|
|
14
21
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
15
22
|
import { resolveSnapFlags } from '../../../lib/snapping-mode'
|
|
23
|
+
import { getWallLineKind } from '../../../lib/wall-draw-variant'
|
|
16
24
|
import useEditor, { getActiveSnappingMode, isMagneticSnapActive } from '../../../store/use-editor'
|
|
17
25
|
import {
|
|
18
26
|
distanceSquared,
|
|
27
|
+
findWallBodySnapTarget,
|
|
19
28
|
findWallSnapTarget,
|
|
20
29
|
findWallSpecialPointSnap,
|
|
21
30
|
WALL_CONNECT_SNAP_RADIUS,
|
|
@@ -109,14 +118,30 @@ type SnapWallDraftArgs = {
|
|
|
109
118
|
*/
|
|
110
119
|
magnetic?: boolean
|
|
111
120
|
/**
|
|
112
|
-
* Optional grid-snap override
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
* local-axis grid at `step`.
|
|
121
|
+
* Optional grid-snap override, in the caller's plan frame. When omitted,
|
|
122
|
+
* falls back to the building-local grid at `step` — the lattice the wall
|
|
123
|
+
* tools draft on, square to the house however the building is turned.
|
|
116
124
|
*/
|
|
117
125
|
gridSnap?: (point: WallPlanPoint) => WallPlanPoint
|
|
118
126
|
/** Optional magnetic snap radii. Omitted means wall tools keep their defaults. */
|
|
119
127
|
snapRadii?: WallSnapRadii
|
|
128
|
+
/**
|
|
129
|
+
* The point is a wall end (drawn or dragged): in the non-magnetic modes an
|
|
130
|
+
* end aimed into another wall's drawn body, or within the connect radius of
|
|
131
|
+
* either face, lands on that wall's reference line — where the wall
|
|
132
|
+
* topology T-joins it — ahead of the grid / angle placement, keeping the
|
|
133
|
+
* segment's direction from `start` (see `findWallBodySnapTarget`). Without
|
|
134
|
+
* it the grid quantises an end aimed at a face to the nearest lattice point,
|
|
135
|
+
* a fraction short of the face, and the end stays open with a gap.
|
|
136
|
+
*/
|
|
137
|
+
captureWallBodies?: boolean
|
|
138
|
+
/**
|
|
139
|
+
* The walls `captureWallBodies` may join an end into; `walls` when omitted.
|
|
140
|
+
* A body capture T-joins the wall, so a caller that adds reference walls it
|
|
141
|
+
* can't join (the floor below) keeps them out of this list while they still
|
|
142
|
+
* serve the corner and edge snaps.
|
|
143
|
+
*/
|
|
144
|
+
bodyWalls?: WallNode[]
|
|
120
145
|
}
|
|
121
146
|
|
|
122
147
|
export function snapWallDraftPointDetailed(args: SnapWallDraftArgs): WallDraftSnapResult {
|
|
@@ -131,6 +156,8 @@ export function snapWallDraftPointDetailed(args: SnapWallDraftArgs): WallDraftSn
|
|
|
131
156
|
magnetic = true,
|
|
132
157
|
gridSnap,
|
|
133
158
|
snapRadii,
|
|
159
|
+
captureWallBodies = false,
|
|
160
|
+
bodyWalls = walls,
|
|
134
161
|
} = args
|
|
135
162
|
|
|
136
163
|
if (bypassSnap) return { point, snap: null, targetWallIds: [] }
|
|
@@ -144,9 +171,8 @@ export function snapWallDraftPointDetailed(args: SnapWallDraftArgs): WallDraftSn
|
|
|
144
171
|
}
|
|
145
172
|
|
|
146
173
|
const step = overrideStep ?? getSegmentGridStep()
|
|
147
|
-
// The angle path snaps the distance ALONG the 15° ray
|
|
148
|
-
//
|
|
149
|
-
// only applies when the angle lock is off.
|
|
174
|
+
// The angle path snaps the distance ALONG the 15° ray from `start`, so the
|
|
175
|
+
// `gridSnap` override only applies when the angle lock is off.
|
|
150
176
|
const basePoint: WallPlanPoint =
|
|
151
177
|
start && angleSnap
|
|
152
178
|
? [...snapPointAlongAngleRay(start, point, DEFAULT_ANGLE_STEP, step)]
|
|
@@ -182,6 +208,30 @@ export function snapWallDraftPointDetailed(args: SnapWallDraftArgs): WallDraftSn
|
|
|
182
208
|
}
|
|
183
209
|
const connectSpecial = findWallSpecialPointSnap(basePoint, walls, ignoreWallIds, connectRadii)
|
|
184
210
|
if (connectSpecial) return connectSpecial
|
|
211
|
+
// A wall end aimed into a wall's body joins it, whichever lattice point or
|
|
212
|
+
// ray station the mode picked near its face.
|
|
213
|
+
const bodySnap = captureWallBodies
|
|
214
|
+
? findWallBodySnapTarget(point, bodyWalls, {
|
|
215
|
+
ignoreWallIds,
|
|
216
|
+
placed: basePoint,
|
|
217
|
+
start,
|
|
218
|
+
margin: WALL_CONNECT_SNAP_RADIUS,
|
|
219
|
+
})
|
|
220
|
+
: null
|
|
221
|
+
if (bodySnap) {
|
|
222
|
+
// An end aimed into a corner block joins the corner, and reads as one.
|
|
223
|
+
const atCorner = bodyWalls.some(
|
|
224
|
+
(wall) =>
|
|
225
|
+
!ignoreWallIds?.includes(wall.id) &&
|
|
226
|
+
(distanceSquared(bodySnap, wall.start) <= 1e-12 ||
|
|
227
|
+
distanceSquared(bodySnap, wall.end) <= 1e-12),
|
|
228
|
+
)
|
|
229
|
+
return {
|
|
230
|
+
point: bodySnap,
|
|
231
|
+
snap: atCorner ? 'endpoint' : 'wall',
|
|
232
|
+
targetWallIds: wallIdsAtSnapPoint(bodySnap, walls, ignoreWallIds),
|
|
233
|
+
}
|
|
234
|
+
}
|
|
185
235
|
const connectWall = findWallSnapTarget(basePoint, walls, {
|
|
186
236
|
ignoreWallIds,
|
|
187
237
|
radius: WALL_CONNECT_SNAP_RADIUS,
|
|
@@ -205,6 +255,36 @@ export function isSegmentLongEnough(start: WallPlanPoint, end: WallPlanPoint): b
|
|
|
205
255
|
return distanceSquared(start, end) >= WALL_MIN_LENGTH * WALL_MIN_LENGTH
|
|
206
256
|
}
|
|
207
257
|
|
|
258
|
+
/**
|
|
259
|
+
* An invisible wall drawn `start` → `end` on the current level (the Walls variant's Invisible, a room
|
|
260
|
+
* divider): room separators, each end joined to a wall it lands on and broken
|
|
261
|
+
* either side of a wall it crosses (`planSeparatorInsertion`), as one undo step. Rooms close on
|
|
262
|
+
* it; a room it closes off outside the house becomes a porch. The last piece, for the run to go
|
|
263
|
+
* on from its end; null when it would be too short.
|
|
264
|
+
*/
|
|
265
|
+
export function createInvisibleWallOnCurrentLevel(
|
|
266
|
+
start: WallPlanPoint,
|
|
267
|
+
end: WallPlanPoint,
|
|
268
|
+
): SeparatorNode | null {
|
|
269
|
+
const currentLevelId = useViewer.getState().selection.levelId
|
|
270
|
+
const { nodes, applyNodeChanges } = useScene.getState()
|
|
271
|
+
if (!(currentLevelId && isSegmentLongEnough(start, end))) return null
|
|
272
|
+
const joinRadius = isMagneticSnapActive() ? WALL_JOIN_SNAP_RADIUS : WALL_CONNECT_SNAP_RADIUS
|
|
273
|
+
return runAsSingleSceneHistoryStep(useScene, () => {
|
|
274
|
+
const plan = planSeparatorInsertion(nodes, {
|
|
275
|
+
levelId: currentLevelId as AnyNodeId,
|
|
276
|
+
start,
|
|
277
|
+
end,
|
|
278
|
+
joinRadius,
|
|
279
|
+
mintId: () => generateId('separator'),
|
|
280
|
+
})
|
|
281
|
+
if (!plan) return null
|
|
282
|
+
applyNodeChanges(plan.changes)
|
|
283
|
+
sfxEmitter.emit('sfx:structure-build')
|
|
284
|
+
return plan.separators.at(-1) ?? null
|
|
285
|
+
})
|
|
286
|
+
}
|
|
287
|
+
|
|
208
288
|
export function createWallOnCurrentLevel(
|
|
209
289
|
start: WallPlanPoint,
|
|
210
290
|
end: WallPlanPoint,
|
|
@@ -220,13 +300,33 @@ export function createWallOnCurrentLevel(
|
|
|
220
300
|
const joinRadius = isMagneticSnapActive() ? WALL_JOIN_SNAP_RADIUS : WALL_CONNECT_SNAP_RADIUS
|
|
221
301
|
|
|
222
302
|
return runAsSingleSceneHistoryStep(useScene, () => {
|
|
223
|
-
|
|
303
|
+
// The Walls variant's Railing draws railings: a guard of their own.
|
|
304
|
+
const insertion = {
|
|
224
305
|
levelId: currentLevelId as AnyNodeId,
|
|
225
306
|
start,
|
|
226
307
|
end,
|
|
227
308
|
joinRadius,
|
|
228
|
-
wallDefaults:
|
|
229
|
-
|
|
309
|
+
wallDefaults:
|
|
310
|
+
getWallLineKind() === 'railing'
|
|
311
|
+
? railingWallDefaults()
|
|
312
|
+
: (useEditor.getState().toolDefaults.wall ?? {}),
|
|
313
|
+
}
|
|
314
|
+
let result = planWallInsertion(nodes, insertion)
|
|
315
|
+
if (!result.ok && result.reason === 'covered-existing-wall') {
|
|
316
|
+
// Drawn over walls built another way (an outside wall over an inside
|
|
317
|
+
// jog), the new wall takes their stretch over; over walls built the
|
|
318
|
+
// same, it would change nothing, so it stays refused.
|
|
319
|
+
const replacing = planWallInsertion(nodes, { ...insertion, replaceCovered: true })
|
|
320
|
+
if (
|
|
321
|
+
replacing.ok &&
|
|
322
|
+
replacing.plan.insertedWalls.some((wall) =>
|
|
323
|
+
wallsRunAlong(nodes, { ...wall, parentId: currentLevelId } as WallNode).some(
|
|
324
|
+
(covered) => !sameWallConstruction(covered, wall),
|
|
325
|
+
),
|
|
326
|
+
)
|
|
327
|
+
)
|
|
328
|
+
result = replacing
|
|
329
|
+
}
|
|
230
330
|
if (!result.ok) return null
|
|
231
331
|
const { plan } = result
|
|
232
332
|
|
|
@@ -237,20 +337,28 @@ export function createWallOnCurrentLevel(
|
|
|
237
337
|
const sourceAlreadyUpdated = sourceUpdate
|
|
238
338
|
? plan.changes.update.some((operation) => operation.id === sourceUpdate.id)
|
|
239
339
|
: false
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
340
|
+
// A wall drawn along part of a parallel wall takes that stretch over, so
|
|
341
|
+
// drawing on from a wall never doubles the one it runs along.
|
|
342
|
+
applyNodeChanges(
|
|
343
|
+
finishWallEdit(
|
|
344
|
+
nodes,
|
|
345
|
+
{
|
|
346
|
+
...plan.changes,
|
|
347
|
+
update: plan.changes.update
|
|
348
|
+
.map((operation) =>
|
|
349
|
+
sourceUpdate?.id === operation.id
|
|
350
|
+
? { ...operation, data: { ...operation.data, ...sourceUpdate.data } }
|
|
351
|
+
: operation,
|
|
352
|
+
)
|
|
353
|
+
.concat(sourceUpdate && !sourceAlreadyUpdated ? [sourceUpdate] : []),
|
|
354
|
+
create: plan.changes.create.map((operation) => ({
|
|
355
|
+
...operation,
|
|
356
|
+
node: finalizedWallsById.get(operation.node.id as WallNode['id']) ?? operation.node,
|
|
357
|
+
})),
|
|
358
|
+
},
|
|
359
|
+
{ winnerIds: finalizedWalls.map((wall) => wall.id), mintId: () => generateId('wall') },
|
|
360
|
+
),
|
|
361
|
+
)
|
|
254
362
|
sfxEmitter.emit('sfx:structure-build')
|
|
255
363
|
|
|
256
364
|
const terminalWall = finalizedWalls.at(-1)!
|