@pascal-app/editor 1.0.5 → 1.0.7
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 +20 -17
- 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 +20 -8
- package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
- package/src/components/editor/first-person-controls.tsx +72 -61
- package/src/components/editor/floating-action-menu.tsx +27 -9
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floorplan-panel.test.ts +220 -0
- package/src/components/editor/floorplan-panel.tsx +285 -132
- package/src/components/editor/footprint-height-handle.tsx +1 -1
- 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 +20 -0
- package/src/components/editor/index.tsx +130 -20
- package/src/components/editor/inspector-extension-actions.tsx +36 -0
- package/src/components/editor/localized-html.tsx +22 -0
- package/src/components/editor/node-action-menu.tsx +46 -12
- package/src/components/editor/node-arrow-handles.tsx +1 -2
- package/src/components/editor/open-wall-ends-3d-layer.tsx +1 -1
- package/src/components/editor/opening-guides-3d-layer.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 +1 -1
- package/src/components/editor/room-handles-2d.tsx +156 -6
- package/src/components/editor/room-handles.tsx +241 -51
- package/src/components/editor/room-highlight.tsx +2 -0
- package/src/components/editor/room-labels.tsx +29 -15
- 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 +1 -1
- 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 +1 -1
- package/src/components/editor/wall-move-side-handles.tsx +5 -1
- package/src/components/editor/wall-pull-grip.tsx +193 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +81 -1
- 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 +31 -3
- package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +6 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +22 -13
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +11 -3
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +7 -1
- 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-registry-layer.test.ts +12 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +197 -37
- 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/tools/distribution/distribution-run-tool.tsx +50 -26
- 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 +30 -34
- package/src/components/tools/registry/item-surface-move.test.ts +11 -0
- 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 +1 -1
- package/src/components/tools/shared/placement-dimension-guides.tsx +1 -1
- package/src/components/tools/site/terrain-brush-cursor.tsx +1 -1
- 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/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 +30 -14
- 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 +53 -3
- package/src/components/ui/helpers/helper-manager.tsx +82 -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 +113 -38
- package/src/components/ui/panels/floor-opening-panel.tsx +73 -25
- 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 +8 -6
- 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 +8 -7
- package/src/components/ui/panels/multi-parametric-inspector.tsx +122 -34
- package/src/components/ui/panels/multi-selection-panel.tsx +75 -35
- package/src/components/ui/panels/node-display.ts +35 -24
- package/src/components/ui/panels/panel-manager.tsx +5 -3
- package/src/components/ui/panels/panel-wrapper.tsx +22 -109
- 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 +464 -65
- 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 +21 -12
- package/src/components/ui/panels/room-panel.tsx +206 -99
- 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/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/site-panel/building-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -2
- 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 +117 -64
- 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 +8 -3
- 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 +18 -8
- 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 +61 -4
- 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 +15117 -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 +37501 -0
- package/src/i18n/locales/es/messages.ts +1 -0
- package/src/i18n/locales/fr/messages.po +37501 -0
- package/src/i18n/locales/fr/messages.ts +1 -0
- package/src/i18n/locales/ja/messages.po +37501 -0
- package/src/i18n/locales/ja/messages.ts +1 -0
- package/src/i18n/locales/zh-CN/messages.po +37501 -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 +156 -4
- package/src/lib/add-storey.test.ts +3 -3
- package/src/lib/contextual-help.test.ts +20 -1
- package/src/lib/contextual-help.ts +95 -19
- package/src/lib/continuation.ts +38 -11
- package/src/lib/floor-footprints.test.ts +2 -1
- package/src/lib/floor-foundation-support.test.ts +7 -3
- 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/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/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/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/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-parser.ts +7 -3
- package/src/lib/measurements.test.ts +17 -0
- package/src/lib/measurements.ts +20 -5
- package/src/lib/mezzanine-selection.test.ts +3 -1
- 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/reference-scale.ts +83 -0
- package/src/lib/room-built-on.test.ts +26 -12
- package/src/lib/room-built-on.ts +20 -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 +140 -0
- package/src/lib/room-handle-drag.ts +557 -42
- 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 +37 -0
- package/src/lib/room-push-dimensions.ts +226 -2
- package/src/lib/room-selection.test.ts +23 -6
- package/src/lib/scene-clipboard.test.ts +70 -49
- package/src/lib/scene-clipboard.ts +9 -4
- 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/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 +20 -5
- 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 +14 -5
- 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 +42 -0
- package/src/lib/window-interaction.ts +47 -5
- 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 +48 -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-wall-snap-indicator.ts +15 -2
|
@@ -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)!
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import type { WallNode } from '@pascal-app/core'
|
|
3
|
+
import { lockWallPullDirection, snapWallPullPoint, type WallPullSnapInput } from './wall-pull-snap'
|
|
4
|
+
import type { WallPlanPoint } from './wall-snap-geometry'
|
|
5
|
+
|
|
6
|
+
function makeWall(start: WallPlanPoint, end: WallPlanPoint, id?: string): WallNode {
|
|
7
|
+
return {
|
|
8
|
+
object: 'node',
|
|
9
|
+
id: (id ?? `wall_${start.join('_')}_${end.join('_')}`) as WallNode['id'],
|
|
10
|
+
type: 'wall',
|
|
11
|
+
name: 'Wall',
|
|
12
|
+
parentId: null,
|
|
13
|
+
visible: true,
|
|
14
|
+
metadata: {},
|
|
15
|
+
children: [],
|
|
16
|
+
start,
|
|
17
|
+
end,
|
|
18
|
+
thickness: 0.1,
|
|
19
|
+
frontSide: 'unknown',
|
|
20
|
+
backSide: 'unknown',
|
|
21
|
+
} as unknown as WallNode
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const deg = (degrees: number) => (degrees * Math.PI) / 180
|
|
25
|
+
const polar = (from: WallPlanPoint, degrees: number, length: number): WallPlanPoint => [
|
|
26
|
+
from[0] + Math.cos(deg(degrees)) * length,
|
|
27
|
+
from[1] + Math.sin(deg(degrees)) * length,
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
function expectPoint(actual: WallPlanPoint, expected: WallPlanPoint) {
|
|
31
|
+
expect(actual[0]).toBeCloseTo(expected[0], 6)
|
|
32
|
+
expect(actual[1]).toBeCloseTo(expected[1], 6)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// A 4 × 3 m room; the pull starts at the south wall's east end.
|
|
36
|
+
const ROOM = [
|
|
37
|
+
makeWall([0, 0], [4, 0], 'south'),
|
|
38
|
+
makeWall([4, 0], [4, 3], 'east'),
|
|
39
|
+
makeWall([4, 3], [0, 3], 'north'),
|
|
40
|
+
makeWall([0, 3], [0, 0], 'west'),
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
function pullFromRoom(point: WallPlanPoint, extra: Partial<WallPullSnapInput> = {}) {
|
|
44
|
+
return snapWallPullPoint({
|
|
45
|
+
point,
|
|
46
|
+
start: [4, 0],
|
|
47
|
+
runStart: [4, 0],
|
|
48
|
+
reference: [1, 0],
|
|
49
|
+
walls: ROOM,
|
|
50
|
+
gridStep: 0.5,
|
|
51
|
+
...extra,
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe('lockWallPullDirection', () => {
|
|
56
|
+
test('holds straight on from the wall it comes from within about 7°', () => {
|
|
57
|
+
expect(lockWallPullDirection(polar([0, 0], 6, 3), [1, 0])).toEqual({
|
|
58
|
+
direction: [1, 0],
|
|
59
|
+
lock: 'straight',
|
|
60
|
+
})
|
|
61
|
+
expect(lockWallPullDirection(polar([0, 0], 8, 3), [1, 0]).lock).toBe('angle')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('holds square to the wall it comes from, either side', () => {
|
|
65
|
+
const left = lockWallPullDirection(polar([0, 0], 85, 3), [1, 0])
|
|
66
|
+
expect(left.lock).toBe('square')
|
|
67
|
+
expectPoint(left.direction, [0, 1])
|
|
68
|
+
const right = lockWallPullDirection(polar([0, 0], -96, 3), [1, 0])
|
|
69
|
+
expect(right.lock).toBe('square')
|
|
70
|
+
expectPoint(right.direction, [0, -1])
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('holds straight on to a skewed wall, and the house axes beside it', () => {
|
|
74
|
+
const skew: WallPlanPoint = [Math.cos(deg(20)), Math.sin(deg(20))]
|
|
75
|
+
const straight = lockWallPullDirection(polar([0, 0], 23, 3), skew)
|
|
76
|
+
expect(straight.lock).toBe('straight')
|
|
77
|
+
expectPoint(straight.direction, skew)
|
|
78
|
+
const axis = lockWallPullDirection(polar([0, 0], 4, 3), skew)
|
|
79
|
+
expect(axis.lock).toBe('axis')
|
|
80
|
+
expectPoint(axis.direction, [1, 0])
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('past the lock it steps by 15° on the house axes', () => {
|
|
84
|
+
for (const [cursor, expected] of [
|
|
85
|
+
[40, 45],
|
|
86
|
+
[28, 30],
|
|
87
|
+
[52, 45],
|
|
88
|
+
[-118, -120],
|
|
89
|
+
] as const) {
|
|
90
|
+
const { direction, lock } = lockWallPullDirection(polar([0, 0], cursor, 3), [1, 0])
|
|
91
|
+
expect(lock).toBe('angle')
|
|
92
|
+
expectPoint(direction, polar([0, 0], expected, 1))
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
describe('snapWallPullPoint', () => {
|
|
98
|
+
test('a straight pull steps its length on the grid', () => {
|
|
99
|
+
const result = pullFromRoom([6.6, 0.2])
|
|
100
|
+
expect(result.lock).toBe('straight')
|
|
101
|
+
expect(result.snap).toBe('grid')
|
|
102
|
+
expect(result.label).toBe('Grid')
|
|
103
|
+
expectPoint(result.point, [6.5, 0])
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test('a square pull holds 90° to the pulled wall', () => {
|
|
107
|
+
const result = pullFromRoom([4.25, -2.6])
|
|
108
|
+
expect(result.lock).toBe('square')
|
|
109
|
+
expectPoint(result.point, [4, -2.5])
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('further round it snaps to 15° steps, with a grid length along them', () => {
|
|
113
|
+
const result = pullFromRoom(polar([4, 0], -40, 3))
|
|
114
|
+
expect(result.lock).toBe('angle')
|
|
115
|
+
// 3 m at 40° is 2.99 m along 45°: 3 m on the 0.5 m grid.
|
|
116
|
+
expectPoint(result.point, polar([4, 0], -45, 3))
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test('Alt is fully free', () => {
|
|
120
|
+
const cursor = polar([4, 0], -40, 3.13)
|
|
121
|
+
const result = pullFromRoom(cursor, { free: true })
|
|
122
|
+
expect(result.lock).toBe('free')
|
|
123
|
+
expect(result.snap).toBeNull()
|
|
124
|
+
expectPoint(result.point, cursor)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
test('the extension of the pulled wall is always drawn', () => {
|
|
128
|
+
const result = pullFromRoom([5.3, -1.9])
|
|
129
|
+
const extension = result.guides.find((guide) => guide.kind === 'extension')
|
|
130
|
+
expect(extension?.from).toEqual([4, 0])
|
|
131
|
+
expect(extension?.to[1]).toBeCloseTo(0, 9)
|
|
132
|
+
expect(extension?.to[0] ?? 0).toBeGreaterThan(5.3)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test('an existing wall end wins over the held direction', () => {
|
|
136
|
+
const result = snapWallPullPoint({
|
|
137
|
+
point: [4.15, 3.2],
|
|
138
|
+
start: [7, 3],
|
|
139
|
+
runStart: [4, 0],
|
|
140
|
+
reference: [0, 1],
|
|
141
|
+
walls: [...ROOM, makeWall([4, 0], [7, 0], 'p1'), makeWall([7, 0], [7, 3], 'p2')],
|
|
142
|
+
gridStep: 0.5,
|
|
143
|
+
})
|
|
144
|
+
expect(result.snap).toBe('endpoint')
|
|
145
|
+
expect(result.label).toBe('Endpoint')
|
|
146
|
+
expect(result.point).toEqual([4, 3])
|
|
147
|
+
expect(result.targetWallIds.sort()).toEqual(['east', 'north'])
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
test('a held direction running into a wall lands on its line, square', () => {
|
|
151
|
+
// From the east of the room, back west at the north wall's height: the
|
|
152
|
+
// wall runs into the east wall at (4, 1.5), square to it.
|
|
153
|
+
const result = snapWallPullPoint({
|
|
154
|
+
point: [4.2, 1.52],
|
|
155
|
+
start: [7, 1.5],
|
|
156
|
+
runStart: [7, -1],
|
|
157
|
+
reference: [0, 1],
|
|
158
|
+
walls: ROOM,
|
|
159
|
+
gridStep: 0.5,
|
|
160
|
+
})
|
|
161
|
+
expect(result.snap).toBe('perpendicular')
|
|
162
|
+
expect(result.label).toBe('Perpendicular')
|
|
163
|
+
expectPoint(result.point, [4, 1.5])
|
|
164
|
+
expect(result.targetWallIds).toEqual(['east'])
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
test('a run lines up with its start, then closes on it', () => {
|
|
168
|
+
// Pulled east out of a lone wall's end S = (0, 0): east to (5, 0), north
|
|
169
|
+
// to (5, 4), then west — the run's start guide catches x = 0 …
|
|
170
|
+
const source = makeWall([-4, 0], [0, 0], 'source')
|
|
171
|
+
const walls = [source, makeWall([0, 0], [5, 0], 'r1'), makeWall([5, 0], [5, 4], 'r2')]
|
|
172
|
+
const across = snapWallPullPoint({
|
|
173
|
+
point: [0.1, 4.05],
|
|
174
|
+
start: [5, 4],
|
|
175
|
+
runStart: [0, 0],
|
|
176
|
+
reference: [0, 1],
|
|
177
|
+
walls,
|
|
178
|
+
gridStep: 0,
|
|
179
|
+
})
|
|
180
|
+
expect(across.snap).toBe('alignment')
|
|
181
|
+
expect(across.label).toBe('In line with start')
|
|
182
|
+
expectPoint(across.point, [0, 4])
|
|
183
|
+
const startGuide = across.guides.find((guide) => guide.kind === 'run-start')
|
|
184
|
+
expect(startGuide?.from).toEqual([0, 0])
|
|
185
|
+
expect(startGuide?.to[0]).toBeCloseTo(0, 9)
|
|
186
|
+
expect(startGuide?.to[1] ?? 0).toBeGreaterThan(4)
|
|
187
|
+
// … and the last wall closes square back onto it.
|
|
188
|
+
const close = snapWallPullPoint({
|
|
189
|
+
point: [0.12, 0.2],
|
|
190
|
+
start: [0, 4],
|
|
191
|
+
runStart: [0, 0],
|
|
192
|
+
reference: [-1, 0],
|
|
193
|
+
walls: [...walls, makeWall([5, 4], [0, 4], 'r3')],
|
|
194
|
+
gridStep: 0.5,
|
|
195
|
+
})
|
|
196
|
+
expect(close.snap).toBe('endpoint')
|
|
197
|
+
expect(close.point).toEqual([0, 0])
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
test('guides cross near the cursor: the crossing is marked and snaps', () => {
|
|
201
|
+
// The run's start S = (0, 0) and another wall's end P = (2, 6): S's
|
|
202
|
+
// vertical and P's horizontal cross at (0, 6).
|
|
203
|
+
const walls = [
|
|
204
|
+
makeWall([-4, 0], [0, 0], 'source'),
|
|
205
|
+
makeWall([0, 0], [5, 0], 'r1'),
|
|
206
|
+
makeWall([5, 0], [5, 4], 'r2'),
|
|
207
|
+
makeWall([2, 6], [2, 9], 'p'),
|
|
208
|
+
]
|
|
209
|
+
const near = snapWallPullPoint({
|
|
210
|
+
point: [0.45, 5.6],
|
|
211
|
+
start: [5, 4],
|
|
212
|
+
runStart: [0, 0],
|
|
213
|
+
reference: [0, 1],
|
|
214
|
+
walls,
|
|
215
|
+
gridStep: 0.5,
|
|
216
|
+
})
|
|
217
|
+
expect(near.intersections.some((point) => point[0] === 0 && point[1] === 6)).toBe(true)
|
|
218
|
+
expect(near.snap).not.toBe('intersection')
|
|
219
|
+
|
|
220
|
+
const on = snapWallPullPoint({
|
|
221
|
+
point: [0.1, 5.9],
|
|
222
|
+
start: [5, 4],
|
|
223
|
+
runStart: [0, 0],
|
|
224
|
+
reference: [0, 1],
|
|
225
|
+
walls,
|
|
226
|
+
gridStep: 0.5,
|
|
227
|
+
})
|
|
228
|
+
expect(on.snap).toBe('intersection')
|
|
229
|
+
expect(on.label).toBe('Intersection')
|
|
230
|
+
expect(on.point).toEqual([0, 6])
|
|
231
|
+
expect(on.guides.map((guide) => guide.kind).sort()).toEqual([
|
|
232
|
+
'alignment',
|
|
233
|
+
'extension',
|
|
234
|
+
'run-start',
|
|
235
|
+
])
|
|
236
|
+
const vertical = on.guides.find((guide) => guide.kind === 'run-start')
|
|
237
|
+
expect(vertical?.from).toEqual([0, 0])
|
|
238
|
+
const horizontal = on.guides.find((guide) => guide.kind === 'alignment')
|
|
239
|
+
expect(horizontal?.from).toEqual([2, 6])
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
test('a wall middle and a wall crossing snap too', () => {
|
|
243
|
+
const midpoint = pullFromRoom([2.1, 3.15], { start: [4, -2], reference: [0, -1] })
|
|
244
|
+
expect(midpoint.snap).toBe('midpoint')
|
|
245
|
+
expect(midpoint.point).toEqual([2, 3])
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
test("on a turned building the lock follows the house's axes, not the world's", () => {
|
|
249
|
+
// The house is turned 30° on its lot. Plan points are in its own frame,
|
|
250
|
+
// as both views hand them to the tools; a world cursor along the house's
|
|
251
|
+
// east side comes in along the house's X, and the held wall goes back out
|
|
252
|
+
// along it.
|
|
253
|
+
const yaw = deg(30)
|
|
254
|
+
const origin: WallPlanPoint = [12, -7]
|
|
255
|
+
const toWorld = ([x, z]: WallPlanPoint): WallPlanPoint => [
|
|
256
|
+
origin[0] + x * Math.cos(yaw) + z * Math.sin(yaw),
|
|
257
|
+
origin[1] - x * Math.sin(yaw) + z * Math.cos(yaw),
|
|
258
|
+
]
|
|
259
|
+
const toLocal = ([x, z]: WallPlanPoint): WallPlanPoint => {
|
|
260
|
+
const dx = x - origin[0]
|
|
261
|
+
const dz = z - origin[1]
|
|
262
|
+
return [dx * Math.cos(yaw) - dz * Math.sin(yaw), dx * Math.sin(yaw) + dz * Math.cos(yaw)]
|
|
263
|
+
}
|
|
264
|
+
const worldCursor = toWorld([6.6, 0.25])
|
|
265
|
+
const result = pullFromRoom(toLocal(worldCursor))
|
|
266
|
+
expect(result.lock).toBe('straight')
|
|
267
|
+
expectPoint(result.point, [6.5, 0])
|
|
268
|
+
const worldStart = toWorld([4, 0])
|
|
269
|
+
const worldEnd = toWorld(result.point)
|
|
270
|
+
const bearing = Math.atan2(worldEnd[1] - worldStart[1], worldEnd[0] - worldStart[0])
|
|
271
|
+
// Along the house's X, which sits 30° off the world's.
|
|
272
|
+
expect(bearing).toBeCloseTo(-yaw, 9)
|
|
273
|
+
})
|
|
274
|
+
})
|