@pascal-app/editor 1.0.6 → 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 +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 +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
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Arrow keys nudge the selected wall square to itself in the floor plan: one
|
|
3
|
+
* inch a press (a centimetre in metric), or less when a parallel wall it lines
|
|
4
|
+
* up with comes first (`wall-parallel-snap`), so it stops flush. The arrow's
|
|
5
|
+
* direction on screen picks the way; an arrow along the wall does nothing.
|
|
6
|
+
* The nudge is the side arrows' push (`commitRoomPush`), so joined walls
|
|
7
|
+
* follow, jogs and overlaps come out as when the wall is pushed, and each
|
|
8
|
+
* press is one undo step.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
type AnyNodeId,
|
|
13
|
+
getPerpendicularWallMoveAxis,
|
|
14
|
+
useScene,
|
|
15
|
+
type WallNode,
|
|
16
|
+
type WallPlanPoint,
|
|
17
|
+
} from '@pascal-app/core'
|
|
18
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
19
|
+
import { commitRoomPush } from './room-handle-drag'
|
|
20
|
+
import { levelSeparatorLines, separatorWallLine } from './separator-lines'
|
|
21
|
+
import { nudgeWallPosition, wallParallelTargets } from './wall-parallel-snap'
|
|
22
|
+
|
|
23
|
+
/** One nudge in feet and inches, in metres. */
|
|
24
|
+
export const WALL_NUDGE_IMPERIAL = 0.0254
|
|
25
|
+
/** One nudge in metric, in metres. */
|
|
26
|
+
export const WALL_NUDGE_METRIC = 0.01
|
|
27
|
+
|
|
28
|
+
const ARROW_SCREEN_DIRECTIONS: Record<string, [number, number]> = {
|
|
29
|
+
ArrowUp: [0, -1],
|
|
30
|
+
ArrowDown: [0, 1],
|
|
31
|
+
ArrowLeft: [-1, 0],
|
|
32
|
+
ArrowRight: [1, 0],
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// An arrow within about 67° of the wall's own direction runs along it.
|
|
36
|
+
const MIN_ACROSS = 0.4
|
|
37
|
+
|
|
38
|
+
/** True for the four bare arrow keys. */
|
|
39
|
+
export function isWallNudgeKey(key: string): boolean {
|
|
40
|
+
return key in ARROW_SCREEN_DIRECTIONS
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The plan direction an arrow points on screen: the shown floor plan's scene
|
|
45
|
+
* transform, turned or not, maps it back. Null when no floor plan is shown.
|
|
46
|
+
*/
|
|
47
|
+
export function floorplanArrowDirection(key: string): WallPlanPoint | null {
|
|
48
|
+
const screen = ARROW_SCREEN_DIRECTIONS[key]
|
|
49
|
+
if (!screen || typeof document === 'undefined') return null
|
|
50
|
+
const scenes = document.querySelectorAll<SVGGraphicsElement>('[data-floorplan-scene]')
|
|
51
|
+
for (const scene of scenes) {
|
|
52
|
+
const rect = scene.ownerSVGElement?.getBoundingClientRect()
|
|
53
|
+
const ctm = scene.getScreenCTM()
|
|
54
|
+
if (!ctm || !rect || rect.width === 0 || rect.height === 0) continue
|
|
55
|
+
const inverse = ctm.inverse()
|
|
56
|
+
const origin = new DOMPoint(0, 0).matrixTransform(inverse)
|
|
57
|
+
const tip = new DOMPoint(screen[0], screen[1]).matrixTransform(inverse)
|
|
58
|
+
const dx = tip.x - origin.x
|
|
59
|
+
const dz = tip.y - origin.y
|
|
60
|
+
const length = Math.hypot(dx, dz)
|
|
61
|
+
if (length > 1e-12) return [dx / length, dz / length]
|
|
62
|
+
}
|
|
63
|
+
return null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* How far (signed, along the wall's move axis) one nudge of `wall` in plan
|
|
68
|
+
* direction `direction` goes; null when the direction runs along the wall.
|
|
69
|
+
*/
|
|
70
|
+
export function wallNudgeDistance(
|
|
71
|
+
wall: WallNode,
|
|
72
|
+
direction: WallPlanPoint,
|
|
73
|
+
walls: readonly WallNode[],
|
|
74
|
+
step: number,
|
|
75
|
+
): { axis: WallPlanPoint; distance: number } | null {
|
|
76
|
+
const axis = getPerpendicularWallMoveAxis(wall.start, wall.end)
|
|
77
|
+
if (!axis) return null
|
|
78
|
+
const across = direction[0] * axis[0] + direction[1] * axis[1]
|
|
79
|
+
if (Math.abs(across) < MIN_ACROSS) return null
|
|
80
|
+
const position = wall.start[0] * axis[0] + wall.start[1] * axis[1]
|
|
81
|
+
const end = nudgeWallPosition(
|
|
82
|
+
wallParallelTargets(wall, axis, walls),
|
|
83
|
+
position,
|
|
84
|
+
Math.sign(across) * step,
|
|
85
|
+
)
|
|
86
|
+
return { axis, distance: end - position }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Nudge the one selected wall for an arrow key while the floor plan is shown.
|
|
91
|
+
* An invisible wall nudges the same way, as a line: it stops flush with the
|
|
92
|
+
* walls and the other invisible walls, and those joined to it follow.
|
|
93
|
+
* Returns true when the key was used.
|
|
94
|
+
*/
|
|
95
|
+
export function nudgeSelectedWall(key: string): boolean {
|
|
96
|
+
const { selectedIds } = useViewer.getState().selection
|
|
97
|
+
if (selectedIds.length !== 1) return false
|
|
98
|
+
const nodes = useScene.getState().nodes
|
|
99
|
+
const selected = nodes[selectedIds[0] as AnyNodeId]
|
|
100
|
+
if (selected?.type !== 'wall' && selected?.type !== 'separator') return false
|
|
101
|
+
const direction = floorplanArrowDirection(key)
|
|
102
|
+
if (!direction) return false
|
|
103
|
+
|
|
104
|
+
const walls = Object.values(nodes).filter(
|
|
105
|
+
(entry): entry is WallNode => entry?.type === 'wall' && entry.parentId === selected.parentId,
|
|
106
|
+
)
|
|
107
|
+
const step = useViewer.getState().unit === 'imperial' ? WALL_NUDGE_IMPERIAL : WALL_NUDGE_METRIC
|
|
108
|
+
const nudge =
|
|
109
|
+
selected.type === 'wall'
|
|
110
|
+
? wallNudgeDistance(selected, direction, walls, step)
|
|
111
|
+
: wallNudgeDistance(
|
|
112
|
+
separatorWallLine(selected),
|
|
113
|
+
direction,
|
|
114
|
+
[...walls, ...levelSeparatorLines(nodes, selected.parentId, new Set([selected.id]))],
|
|
115
|
+
step,
|
|
116
|
+
)
|
|
117
|
+
if (!nudge) return false
|
|
118
|
+
commitRoomPush(
|
|
119
|
+
selected.type === 'wall'
|
|
120
|
+
? { wallId: selected.id, t0: 0, t1: 1 }
|
|
121
|
+
: {
|
|
122
|
+
wallId: selected.id,
|
|
123
|
+
t0: 0,
|
|
124
|
+
t1: 1,
|
|
125
|
+
separators: [{ separatorId: selected.id, t0: 0, t1: 1 }],
|
|
126
|
+
},
|
|
127
|
+
nudge.axis,
|
|
128
|
+
nudge.distance,
|
|
129
|
+
)
|
|
130
|
+
return true
|
|
131
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { WallNode } from '@pascal-app/core'
|
|
3
|
+
import {
|
|
4
|
+
nearestWallParallelTarget,
|
|
5
|
+
nudgeWallPosition,
|
|
6
|
+
WALL_PARALLEL_SNAP_RADIUS,
|
|
7
|
+
wallParallelTargets,
|
|
8
|
+
} from './wall-parallel-snap'
|
|
9
|
+
|
|
10
|
+
const wall = (id: string, start: [number, number], end: [number, number], extra = {}) =>
|
|
11
|
+
WallNode.parse({ id: `wall_${id}`, start, end, thickness: 0.15, ...extra })
|
|
12
|
+
const outside = { frontSide: 'exterior', backSide: 'interior' } as const
|
|
13
|
+
const DOWN: [number, number] = [0, 1]
|
|
14
|
+
|
|
15
|
+
describe('wallParallelTargets', () => {
|
|
16
|
+
test('an outside wall lines up with a parallel outside wall, not a partition', () => {
|
|
17
|
+
const moving = wall('moving', [5, 0.3], [8, 0.3], outside)
|
|
18
|
+
const top = wall('top', [0, 0.07], [4, 0.07], outside)
|
|
19
|
+
const partition = wall('partition', [1, 0.2], [3, 0.2])
|
|
20
|
+
const side = wall('side', [4, 0.07], [4, 4], outside)
|
|
21
|
+
|
|
22
|
+
const targets = wallParallelTargets(moving, DOWN, [moving, top, partition, side])
|
|
23
|
+
|
|
24
|
+
expect(targets.map((target) => target.wallId)).toEqual(['wall_top'])
|
|
25
|
+
expect(targets).toHaveLength(1)
|
|
26
|
+
expect(targets[0]!.position).toBeCloseTo(0.07)
|
|
27
|
+
// The guide runs along both walls, on the top wall's outdoor face: frontSide
|
|
28
|
+
// is the left-normal side, +z for a wall running +x.
|
|
29
|
+
const [from, to] = [targets[0]!.from, targets[0]!.to]
|
|
30
|
+
expect([from[0], to[0]]).toEqual([0, 8])
|
|
31
|
+
expect(from[1]).toBeCloseTo(0.07 + 0.075)
|
|
32
|
+
expect(to[1]).toBeCloseTo(0.07 + 0.075)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('a partition lines up with every parallel wall', () => {
|
|
36
|
+
const moving = wall('moving', [1, 2], [3, 2])
|
|
37
|
+
const top = wall('top', [0, 0], [4, 0], outside)
|
|
38
|
+
const other = wall('other', [1, 3], [3, 3])
|
|
39
|
+
const ids = wallParallelTargets(moving, DOWN, [moving, top, other]).map((t) => t.wallId)
|
|
40
|
+
expect(new Set(ids)).toEqual(new Set(['wall_top', 'wall_other']))
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('walls of different thickness line up face to face and centre to centre', () => {
|
|
44
|
+
const moving = wall('moving', [5, 1], [8, 1], { ...outside, thickness: 0.2 })
|
|
45
|
+
const top = wall('top', [0, 0], [4, 0], { ...outside, thickness: 0.1 })
|
|
46
|
+
const positions = wallParallelTargets(moving, DOWN, [top]).map((t) => t.position)
|
|
47
|
+
// Faces at ±0.05 against ±0.1, and the centres.
|
|
48
|
+
expect(positions.map((p) => Number(p.toFixed(3))).sort()).toEqual([-0.05, 0, 0.05])
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('a reversed or justified wall lines up by its faces', () => {
|
|
52
|
+
const moving = wall('moving', [8, 1], [5, 1], { ...outside, justification: 'a' })
|
|
53
|
+
const top = wall('top', [0, 0], [4, 0], outside)
|
|
54
|
+
const positions = wallParallelTargets(moving, DOWN, [top]).map((t) => t.position)
|
|
55
|
+
// Justified 'a' on a wall running -x puts its body on the -z side of its
|
|
56
|
+
// line, so its faces and centre all line up with the top wall's when that
|
|
57
|
+
// line sits half a thickness below it.
|
|
58
|
+
expect(positions.map((p) => Number(p.toFixed(3)))).toEqual([0.075])
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
describe('nearestWallParallelTarget', () => {
|
|
63
|
+
test('snaps within the radius and lets go past it', () => {
|
|
64
|
+
const moving = wall('moving', [5, 0.3], [8, 0.3], outside)
|
|
65
|
+
const targets = wallParallelTargets(moving, DOWN, [wall('top', [0, 0], [4, 0], outside)])
|
|
66
|
+
expect(nearestWallParallelTarget(targets, 0.1)?.position).toBeCloseTo(0)
|
|
67
|
+
expect(nearestWallParallelTarget(targets, WALL_PARALLEL_SNAP_RADIUS + 0.01)).toBeNull()
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
describe('nudgeWallPosition', () => {
|
|
72
|
+
const moving = wall('moving', [5, 0.3], [8, 0.3], outside)
|
|
73
|
+
const targets = wallParallelTargets(moving, DOWN, [wall('top', [0, 0.29], [4, 0.29], outside)])
|
|
74
|
+
|
|
75
|
+
test('stops on a line-up it would pass', () => {
|
|
76
|
+
expect(nudgeWallPosition(targets, 0.3, -0.0254)).toBeCloseTo(0.29)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('otherwise moves the whole step', () => {
|
|
80
|
+
expect(nudgeWallPosition(targets, 0.3, 0.0254)).toBeCloseTo(0.3254)
|
|
81
|
+
expect(nudgeWallPosition(targets, 0.29, -0.0254)).toBeCloseTo(0.2646)
|
|
82
|
+
})
|
|
83
|
+
})
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lines a wall that is moving sideways up with the parallel walls around it:
|
|
3
|
+
* a face onto the face on the same side of another wall, or its body centre
|
|
4
|
+
* onto theirs. An outside wall broken up by partitions moves one piece at a
|
|
5
|
+
* time, so each piece can be pulled back flush with the rest of the outside.
|
|
6
|
+
*
|
|
7
|
+
* A wall with an outdoor face lines up only with other outside walls; any
|
|
8
|
+
* other wall lines up with every wall on its level. The 2D and 3D wall moves
|
|
9
|
+
* snap through here, and the arrow-key nudge stops on these positions.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
getWallFaceOffsets,
|
|
14
|
+
isCurvedWall,
|
|
15
|
+
type WallNode,
|
|
16
|
+
type WallPlanPoint,
|
|
17
|
+
} from '@pascal-app/core'
|
|
18
|
+
import useAlignmentGuides from '../store/use-alignment-guides'
|
|
19
|
+
|
|
20
|
+
/** How near (metres) a moving wall comes to lining up before it snaps there. */
|
|
21
|
+
export const WALL_PARALLEL_SNAP_RADIUS = 0.15
|
|
22
|
+
|
|
23
|
+
// Walls within about 1° of each other count as parallel.
|
|
24
|
+
const PARALLEL_TOLERANCE = Math.sin(Math.PI / 180)
|
|
25
|
+
const SAME_POSITION = 1e-6
|
|
26
|
+
|
|
27
|
+
type MovingWall = Pick<
|
|
28
|
+
WallNode,
|
|
29
|
+
'id' | 'start' | 'end' | 'thickness' | 'justification' | 'frontSide' | 'backSide'
|
|
30
|
+
> &
|
|
31
|
+
Partial<Pick<WallNode, 'curveOffset'>>
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A position where the moving wall lines up with another: `position` is where
|
|
35
|
+
* its reference line sits along the move axis, `from` → `to` the shared face
|
|
36
|
+
* or centre line, drawn along both walls.
|
|
37
|
+
*/
|
|
38
|
+
export type WallParallelTarget = {
|
|
39
|
+
position: number
|
|
40
|
+
wallId: string
|
|
41
|
+
from: WallPlanPoint
|
|
42
|
+
to: WallPlanPoint
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isOutsideWall(wall: Pick<WallNode, 'frontSide' | 'backSide'>): boolean {
|
|
46
|
+
return wall.frontSide === 'exterior' || wall.backSide === 'exterior'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function direction(wall: Pick<WallNode, 'start' | 'end'>): WallPlanPoint | null {
|
|
50
|
+
const dx = wall.end[0] - wall.start[0]
|
|
51
|
+
const dz = wall.end[1] - wall.start[1]
|
|
52
|
+
const length = Math.hypot(dx, dz)
|
|
53
|
+
return length > 1e-9 ? [dx / length, dz / length] : null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const dot = (a: readonly [number, number], b: readonly [number, number]) =>
|
|
57
|
+
a[0] * b[0] + a[1] * b[1]
|
|
58
|
+
|
|
59
|
+
// Which way the wall's left normal (-dy, dx) runs along `axis`.
|
|
60
|
+
function normalSign(dir: WallPlanPoint, axis: WallPlanPoint): number {
|
|
61
|
+
return Math.sign(dot([-dir[1], dir[0]], axis)) || 1
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The wall's faces and body centre along `axis`, relative to its reference
|
|
66
|
+
* line: the face on the axis's positive side, the other face, the centre.
|
|
67
|
+
*/
|
|
68
|
+
function sidePlanes(wall: MovingWall, dir: WallPlanPoint, axis: WallPlanPoint) {
|
|
69
|
+
const sign = normalSign(dir, axis)
|
|
70
|
+
const { a, b } = getWallFaceOffsets(wall)
|
|
71
|
+
const high = Math.max(a * sign, b * sign)
|
|
72
|
+
const low = Math.min(a * sign, b * sign)
|
|
73
|
+
return [high, low, (high + low) / 2] as const
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The order to try a wall's planes in, so the guide of planes that coincide
|
|
78
|
+
* is drawn on its outdoor face (frontSide is the left-normal side), else on
|
|
79
|
+
* its centre.
|
|
80
|
+
*/
|
|
81
|
+
function planeOrder(wall: MovingWall, dir: WallPlanPoint, axis: WallPlanPoint): number[] {
|
|
82
|
+
const outdoor = wall.frontSide === 'exterior' ? 1 : wall.backSide === 'exterior' ? -1 : 0
|
|
83
|
+
const outward = outdoor * normalSign(dir, axis)
|
|
84
|
+
return outward > 0 ? [0, 1, 2] : outward < 0 ? [1, 0, 2] : [2, 0, 1]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Every position along `axis` (a unit vector square to the wall) where `wall`
|
|
89
|
+
* lines up with a parallel wall in `walls`.
|
|
90
|
+
*/
|
|
91
|
+
export function wallParallelTargets(
|
|
92
|
+
wall: MovingWall,
|
|
93
|
+
axis: WallPlanPoint,
|
|
94
|
+
walls: readonly WallNode[],
|
|
95
|
+
): WallParallelTarget[] {
|
|
96
|
+
const dir = direction(wall)
|
|
97
|
+
if (!dir || isCurvedWall(wall)) return []
|
|
98
|
+
const own = sidePlanes(wall, dir, axis)
|
|
99
|
+
const outsideOnly = isOutsideWall(wall)
|
|
100
|
+
const ownSpan = [dot(wall.start, dir), dot(wall.end, dir)]
|
|
101
|
+
const targets: WallParallelTarget[] = []
|
|
102
|
+
|
|
103
|
+
for (const other of walls) {
|
|
104
|
+
if (other.id === wall.id || isCurvedWall(other)) continue
|
|
105
|
+
if (outsideOnly && !isOutsideWall(other)) continue
|
|
106
|
+
const otherDir = direction(other)
|
|
107
|
+
if (!otherDir || Math.abs(otherDir[0] * dir[1] - otherDir[1] * dir[0]) > PARALLEL_TOLERANCE) {
|
|
108
|
+
continue
|
|
109
|
+
}
|
|
110
|
+
const line = dot(other.start, axis)
|
|
111
|
+
const planes = sidePlanes(other, otherDir, axis)
|
|
112
|
+
const along = [...ownSpan, dot(other.start, dir), dot(other.end, dir)]
|
|
113
|
+
const lo = Math.min(...along)
|
|
114
|
+
const hi = Math.max(...along)
|
|
115
|
+
const seen: number[] = []
|
|
116
|
+
for (const index of planeOrder(other, otherDir, axis)) {
|
|
117
|
+
const shared = line + planes[index]!
|
|
118
|
+
const position = shared - own[index]!
|
|
119
|
+
if (seen.some((value) => Math.abs(value - position) < SAME_POSITION)) continue
|
|
120
|
+
seen.push(position)
|
|
121
|
+
targets.push({
|
|
122
|
+
position,
|
|
123
|
+
wallId: other.id,
|
|
124
|
+
from: [dir[0] * lo + axis[0] * shared, dir[1] * lo + axis[1] * shared],
|
|
125
|
+
to: [dir[0] * hi + axis[0] * shared, dir[1] * hi + axis[1] * shared],
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return targets
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** The target nearest `position`, if one is within `radius`. */
|
|
133
|
+
export function nearestWallParallelTarget(
|
|
134
|
+
targets: readonly WallParallelTarget[],
|
|
135
|
+
position: number,
|
|
136
|
+
radius = WALL_PARALLEL_SNAP_RADIUS,
|
|
137
|
+
): WallParallelTarget | null {
|
|
138
|
+
let best: WallParallelTarget | null = null
|
|
139
|
+
for (const target of targets) {
|
|
140
|
+
const gap = Math.abs(target.position - position)
|
|
141
|
+
if (gap > radius) continue
|
|
142
|
+
if (!best || gap < Math.abs(best.position - position)) best = target
|
|
143
|
+
}
|
|
144
|
+
return best
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Where a nudge of `step` (signed, along the axis) from `position` ends: the
|
|
149
|
+
* full step, or the first line-up it passes on the way.
|
|
150
|
+
*/
|
|
151
|
+
export function nudgeWallPosition(
|
|
152
|
+
targets: readonly WallParallelTarget[],
|
|
153
|
+
position: number,
|
|
154
|
+
step: number,
|
|
155
|
+
): number {
|
|
156
|
+
let end = position + step
|
|
157
|
+
for (const target of targets) {
|
|
158
|
+
const travel = (target.position - position) * Math.sign(step)
|
|
159
|
+
if (travel > SAME_POSITION && travel < Math.abs(step) - SAME_POSITION) {
|
|
160
|
+
if (Math.abs(target.position - position) < Math.abs(end - position)) end = target.position
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return end
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Show the line a moving wall has snapped onto (null clears it). */
|
|
167
|
+
export function publishWallParallelGuide(target: WallParallelTarget | null): void {
|
|
168
|
+
if (!target) {
|
|
169
|
+
useAlignmentGuides.getState().clear()
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
useAlignmentGuides.getState().setDraft({
|
|
173
|
+
lines: [{ kind: 'alignment', from: target.from, to: target.to }],
|
|
174
|
+
intersections: [],
|
|
175
|
+
})
|
|
176
|
+
}
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
createWallPointerTracker,
|
|
5
5
|
wallPointerInParent,
|
|
6
6
|
wallSideAtPointer,
|
|
7
|
+
wallSideOfPlanPoint,
|
|
7
8
|
} from './wall-placement-pointer'
|
|
8
9
|
|
|
9
10
|
test('keeps the grab offset across a pointer gap on the original face', () => {
|
|
@@ -40,3 +41,25 @@ test('reads the pointer in the rotated wall parent and identifies its face', ()
|
|
|
40
41
|
expect(local[1]).toBeCloseTo(3)
|
|
41
42
|
expect(wallSideAtPointer(local)).toBe('back')
|
|
42
43
|
})
|
|
44
|
+
|
|
45
|
+
test('a plan point reads the wall face it is on, as a 3D hover does', () => {
|
|
46
|
+
// Along +X the frame's +Z (front) is the plan's +Z side.
|
|
47
|
+
const wall = {
|
|
48
|
+
start: [0, 0] as [number, number],
|
|
49
|
+
end: [4, 0] as [number, number],
|
|
50
|
+
thickness: 0.2,
|
|
51
|
+
}
|
|
52
|
+
expect(wallSideOfPlanPoint(wall, [2, 0.3])).toBe('front')
|
|
53
|
+
expect(wallSideOfPlanPoint(wall, [2, -0.3])).toBe('back')
|
|
54
|
+
// Drawn the other way, the same point is on its back.
|
|
55
|
+
expect(
|
|
56
|
+
wallSideOfPlanPoint(
|
|
57
|
+
{ ...wall, start: [4, 0] as [number, number], end: [0, 0] as [number, number] },
|
|
58
|
+
[2, 0.3],
|
|
59
|
+
),
|
|
60
|
+
).toBe('back')
|
|
61
|
+
// A justified wall splits at its body's middle, not its reference line.
|
|
62
|
+
const justified = { ...wall, justification: 'a' as const }
|
|
63
|
+
expect(wallSideOfPlanPoint(justified, [2, 0.05])).toBe('back')
|
|
64
|
+
expect(wallSideOfPlanPoint(justified, [2, 0.15])).toBe('front')
|
|
65
|
+
})
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getWallBodyCenterOffset, type WallNode } from '@pascal-app/core'
|
|
1
2
|
import { type Object3D, Vector3 } from 'three'
|
|
2
3
|
|
|
3
4
|
type WallSide = 'front' | 'back'
|
|
@@ -55,3 +56,20 @@ export function wallPointerInParent(
|
|
|
55
56
|
export function wallSideAtPointer(point: readonly number[]): WallSide {
|
|
56
57
|
return point[2]! >= 0 ? 'front' : 'back'
|
|
57
58
|
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The face of a straight wall a plan point is on, as a 3D hover reports it: `front` is the wall
|
|
62
|
+
* frame's +Z (the plan side `(-dz, dx)` of start → end), measured from the wall body's centre so a justified
|
|
63
|
+
* wall splits at its middle. The plan's stand-in for the hovered face's normal.
|
|
64
|
+
*/
|
|
65
|
+
export function wallSideOfPlanPoint(
|
|
66
|
+
wall: Pick<WallNode, 'start' | 'end' | 'thickness' | 'justification'>,
|
|
67
|
+
point: readonly [number, number],
|
|
68
|
+
): WallSide {
|
|
69
|
+
const dx = wall.end[0] - wall.start[0]
|
|
70
|
+
const dz = wall.end[1] - wall.start[1]
|
|
71
|
+
const length = Math.hypot(dx, dz)
|
|
72
|
+
if (length < 1e-9) return 'front'
|
|
73
|
+
const across = ((point[0] - wall.start[0]) * -dz + (point[1] - wall.start[1]) * dx) / length
|
|
74
|
+
return across >= getWallBodyCenterOffset(wall) ? 'front' : 'back'
|
|
75
|
+
}
|