@pascal-app/editor 0.9.1 → 0.9.2
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 +12 -9
- package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
- package/src/components/editor/bake-exporter.tsx +47 -0
- package/src/components/editor/custom-camera-controls.tsx +63 -25
- package/src/components/editor/editor-layout-mobile.tsx +5 -0
- package/src/components/editor/editor-layout-v2.tsx +18 -0
- package/src/components/editor/export-manager.tsx +71 -63
- package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
- package/src/components/editor/first-person/build-collider-world.test.ts +36 -3
- package/src/components/editor/first-person/build-collider-world.ts +32 -5
- package/src/components/editor/first-person-controls.tsx +118 -7
- package/src/components/editor/floating-action-menu.tsx +410 -33
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floorplan-background-selection.test.ts +58 -0
- package/src/components/editor/floorplan-background-selection.ts +3 -2
- package/src/components/editor/floorplan-panel.tsx +1804 -849
- package/src/components/editor/grid.tsx +166 -39
- package/src/components/editor/group-actions.ts +457 -0
- package/src/components/editor/group-floating-action-menu.tsx +130 -0
- package/src/components/editor/group-move-3d.ts +397 -0
- package/src/components/editor/group-rotate-handle.tsx +102 -64
- package/src/components/editor/group-selection-box-3d.tsx +173 -0
- package/src/components/editor/group-transform-shared.test.ts +192 -1
- package/src/components/editor/group-transform-shared.ts +240 -7
- package/src/components/editor/handles/handle-arrow.tsx +81 -23
- package/src/components/editor/handles/use-handle-drag.ts +37 -4
- package/src/components/editor/index.tsx +248 -27
- package/src/components/editor/measurement-pill.tsx +62 -22
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +366 -173
- package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
- package/src/components/editor/quick-measurement-card.tsx +70 -0
- package/src/components/editor/quick-measurement-hud.tsx +28 -0
- package/src/components/editor/riser-diagram-panel.tsx +137 -0
- package/src/components/editor/selection-manager.tsx +812 -466
- package/src/components/editor/site-edge-labels.tsx +4 -4
- package/src/components/editor/slab-hole-highlights.tsx +13 -5
- package/src/components/editor/snapshot-capture-overlay.tsx +255 -115
- package/src/components/editor/three-context-bridge.ts +22 -0
- package/src/components/editor/thumbnail-generator.tsx +136 -43
- package/src/components/editor/use-floorplan-background-placement.ts +113 -45
- package/src/components/editor/use-floorplan-scene-data.ts +5 -5
- package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
- package/src/components/editor/wall-measurement-label.tsx +3 -13
- package/src/components/editor/wall-move-side-handles.tsx +36 -21
- package/src/components/editor/wall-snap-beacon-layer.tsx +160 -6
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +1 -1
- package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
- package/src/components/editor-2d/floorplan-group-move.tsx +701 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1656 -0
- package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +169 -4
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +300 -59
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +15 -0
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +14 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +4 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1704 -342
- package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +419 -60
- package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
- package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
- package/src/components/systems/selection-affordance-manager.tsx +38 -0
- package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
- package/src/components/systems/zone/zone-system.tsx +42 -13
- package/src/components/tools/elevator/elevator-tool.tsx +34 -8
- package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
- package/src/components/tools/fence/fence-drafting.ts +80 -8
- package/src/components/tools/item/move-tool.tsx +17 -13
- package/src/components/tools/item/placement-math.test.ts +13 -1
- package/src/components/tools/item/placement-math.ts +28 -2
- package/src/components/tools/item/placement-strategies.ts +246 -5
- package/src/components/tools/item/placement-types.ts +13 -1
- package/src/components/tools/item/use-draft-node.ts +31 -1
- package/src/components/tools/item/use-placement-coordinator.tsx +761 -121
- package/src/components/tools/registry/move-registry-node-tool.tsx +561 -99
- package/src/components/tools/roof/roof-tool.tsx +320 -94
- package/src/components/tools/select/box-select-state.ts +18 -7
- package/src/components/tools/select/box-select-tool.tsx +176 -40
- package/src/components/tools/select/marquee-geometry.test.ts +161 -0
- package/src/components/tools/select/marquee-geometry.ts +155 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +1 -8
- package/src/components/tools/select/select-candidates.ts +1 -1
- package/src/components/tools/shared/cursor-sphere.tsx +62 -26
- package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
- package/src/components/tools/shared/facing-indicator.tsx +87 -0
- package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
- package/src/components/tools/shared/placement-box.tsx +183 -1
- package/src/components/tools/shared/polygon-editor.tsx +276 -35
- package/src/components/tools/site/site-boundary-editor.tsx +88 -36
- package/src/components/tools/stair/stair-defaults.ts +1 -0
- package/src/components/tools/stair/stair-tool.tsx +82 -11
- package/src/components/tools/tool-manager.tsx +86 -25
- package/src/components/tools/wall/wall-drafting.test.ts +330 -0
- package/src/components/tools/wall/wall-drafting.ts +180 -103
- package/src/components/tools/wall/wall-snap-geometry.test.ts +44 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +67 -6
- package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
- package/src/components/tools/zone/zone-tool.tsx +82 -88
- package/src/components/ui/action-menu/camera-actions.tsx +24 -17
- package/src/components/ui/action-menu/control-modes.tsx +40 -93
- package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
- package/src/components/ui/action-menu/index.tsx +2 -4
- package/src/components/ui/action-menu/measurement-control.tsx +188 -0
- package/src/components/ui/action-menu/structure-tools.tsx +22 -13
- package/src/components/ui/action-menu/view-toggles.tsx +42 -90
- package/src/components/ui/command-palette/editor-commands.tsx +2 -2
- package/src/components/ui/command-palette/index.tsx +4 -3
- package/src/components/ui/controls/material-paint-panel.tsx +86 -17
- package/src/components/ui/controls/material-picker.tsx +83 -152
- package/src/components/ui/controls/material-properties-editor.tsx +108 -0
- package/src/components/ui/controls/metric-control.tsx +118 -44
- package/src/components/ui/controls/scene-material-list.tsx +247 -0
- package/src/components/ui/controls/slider-control.tsx +88 -38
- package/src/components/ui/floating-level-selector.tsx +1 -1
- package/src/components/ui/helpers/building-helper.tsx +10 -23
- package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
- package/src/components/ui/helpers/helper-manager.tsx +227 -14
- package/src/components/ui/helpers/item-helper.tsx +28 -32
- package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
- package/src/components/ui/helpers/roof-helper.tsx +10 -12
- package/src/components/ui/icon-ref.tsx +47 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +39 -1
- package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/panels/node-display.ts +17 -17
- package/src/components/ui/panels/panel-manager.tsx +22 -13
- package/src/components/ui/panels/panel-wrapper.tsx +24 -3
- package/src/components/ui/panels/parametric-inspector.tsx +15 -2
- package/src/components/ui/panels/reference-panel.tsx +54 -19
- package/src/components/ui/primitives/shortcut-token.tsx +39 -3
- package/src/components/ui/primitives/sidebar.tsx +1 -0
- package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
- package/src/components/ui/sidebar/icon-rail.tsx +50 -31
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +71 -40
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +60 -6
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +40 -17
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +44 -3
- package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
- package/src/components/ui/sidebar/tab-bar.tsx +42 -28
- package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
- package/src/components/ui/snap-target-badge.test.tsx +40 -0
- package/src/components/ui/snap-target-badge.tsx +88 -0
- package/src/components/viewer-overlay.tsx +85 -10
- package/src/components/viewer-zone-system.tsx +6 -1
- package/src/hooks/use-auto-save.test.ts +14 -0
- package/src/hooks/use-auto-save.ts +54 -7
- package/src/hooks/use-ceiling-events.ts +3 -2
- package/src/hooks/use-drag-action.ts +4 -1
- package/src/hooks/use-keyboard.ts +385 -40
- package/src/index.tsx +219 -6
- package/src/lib/active-placement-surface.ts +35 -0
- package/src/lib/ceiling-plan-snap.ts +24 -0
- package/src/lib/contextual-help.test.ts +112 -0
- package/src/lib/contextual-help.ts +144 -0
- package/src/lib/continuation.ts +64 -0
- package/src/lib/direct-manipulation.test.ts +192 -0
- package/src/lib/direct-manipulation.ts +109 -0
- package/src/lib/editor-api.ts +23 -35
- package/src/lib/floorplan/apply-alignment.test.ts +25 -0
- package/src/lib/floorplan/apply-alignment.ts +18 -12
- package/src/lib/floorplan/floorplan-export.tsx +364 -0
- package/src/lib/floorplan/geometry.ts +53 -0
- package/src/lib/floorplan/index.ts +3 -0
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/plan-coords.ts +21 -0
- package/src/lib/fresh-planar-placement.test.ts +240 -3
- package/src/lib/fresh-planar-placement.ts +68 -1
- package/src/lib/glb-export.test.ts +441 -0
- package/src/lib/glb-export.ts +874 -0
- package/src/lib/history.ts +9 -0
- package/src/lib/interaction/hot-set.test.ts +133 -0
- package/src/lib/interaction/hot-set.ts +67 -0
- package/src/lib/interaction/overlay-policy.test.ts +51 -0
- package/src/lib/interaction/overlay-policy.ts +59 -0
- package/src/lib/interaction/scope.ts +211 -0
- package/src/lib/level-selection.ts +2 -2
- package/src/lib/material-paint.ts +36 -49
- package/src/lib/measurement-kind.test.ts +30 -0
- package/src/lib/measurement-kind.ts +19 -0
- package/src/lib/measurement-label.test.ts +47 -0
- package/src/lib/measurement-label.ts +68 -0
- package/src/lib/measurement-parser.ts +116 -0
- package/src/lib/measurements.test.ts +183 -0
- package/src/lib/measurements.ts +199 -0
- package/src/lib/paint-scope.test.ts +454 -0
- package/src/lib/paint-scope.ts +461 -0
- package/src/lib/placement-drag-release.ts +23 -0
- package/src/lib/planar-cursor-placement.test.ts +57 -0
- package/src/lib/plugin-panels.test.ts +19 -0
- package/src/lib/plugin-panels.ts +91 -0
- package/src/lib/quick-measurement.test.ts +77 -0
- package/src/lib/quick-measurement.ts +109 -0
- package/src/lib/roof-duplication.ts +6 -6
- package/src/lib/roof-hover-outline-proxy.ts +22 -0
- package/src/lib/roof-wall-hit.ts +164 -0
- package/src/lib/scene-clipboard.test.ts +196 -0
- package/src/lib/scene-clipboard.ts +73 -8
- package/src/lib/scene.ts +28 -5
- package/src/lib/selection-routing.test.ts +298 -0
- package/src/lib/selection-routing.ts +175 -0
- package/src/lib/sfx/index.ts +1 -0
- package/src/lib/sfx/movement-tick.test.ts +65 -0
- package/src/lib/sfx/movement-tick.ts +18 -0
- package/src/lib/sfx-bus.ts +50 -13
- package/src/lib/sfx-player.test.ts +124 -0
- package/src/lib/sfx-player.ts +111 -66
- package/src/lib/slab-plan-snap.test.ts +93 -0
- package/src/lib/slab-plan-snap.ts +108 -0
- package/src/lib/snapping-mode.test.ts +138 -0
- package/src/lib/snapping-mode.ts +179 -0
- package/src/lib/stair-duplication.ts +4 -4
- package/src/lib/surface-plan-snap.test.ts +71 -0
- package/src/lib/surface-plan-snap.ts +256 -0
- package/src/lib/use-linear-display.ts +40 -0
- package/src/lib/world-grid-snap.ts +37 -5
- package/src/store/use-direct-manipulation-feedback.ts +20 -0
- package/src/store/use-editor.tsx +484 -114
- package/src/store/use-facing-pose.ts +44 -0
- package/src/store/use-fence-curve-draft.ts +21 -0
- package/src/store/use-floorplan-draft-preview.ts +101 -0
- package/src/store/use-floorplan-marquee.ts +50 -0
- package/src/store/use-interaction-scope.test.ts +186 -0
- package/src/store/use-interaction-scope.ts +132 -0
- package/src/store/use-measurement-draft.test.ts +530 -0
- package/src/store/use-measurement-draft.ts +543 -0
- package/src/store/use-opening-guides.ts +41 -0
- package/src/store/use-placement-preview.ts +11 -3
- package/src/store/use-quick-measurement-hud.test.ts +53 -0
- package/src/store/use-quick-measurement-hud.ts +77 -0
- package/src/store/use-segment-draft-chain.ts +28 -0
- package/src/store/use-stair-build-preview.ts +43 -0
- package/src/store/use-wall-snap-indicator.ts +2 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
- package/src/components/editor/group-move-handle.tsx +0 -316
- package/src/components/ui/panels/paint-panel.tsx +0 -163
- package/src/lib/level-name.ts +0 -11
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { type AnyNode, snapSlabEdgeToWallBand, useScene } from '@pascal-app/core'
|
|
2
|
+
import { WALL_CONNECT_SNAP_RADIUS } from '../components/tools/wall/wall-drafting'
|
|
3
|
+
import useAlignmentGuides from '../store/use-alignment-guides'
|
|
4
|
+
import { isMagneticSnapActive } from '../store/use-editor'
|
|
5
|
+
import useWallSnapIndicator from '../store/use-wall-snap-indicator'
|
|
6
|
+
import {
|
|
7
|
+
clearSurfacePlanSnapFeedback,
|
|
8
|
+
getLevelWalls,
|
|
9
|
+
resolveSurfacePlanPointSnap,
|
|
10
|
+
SURFACE_ALIGNMENT_THRESHOLD_M,
|
|
11
|
+
type SurfacePlanSnapInput,
|
|
12
|
+
type SurfacePlanSnapResult,
|
|
13
|
+
} from './surface-plan-snap'
|
|
14
|
+
|
|
15
|
+
const SLAB_SNAP_MOVING_ID = '__slab_snap__'
|
|
16
|
+
|
|
17
|
+
export const SLAB_ALIGNMENT_THRESHOLD_M = SURFACE_ALIGNMENT_THRESHOLD_M
|
|
18
|
+
export type SlabPlanSnapInput = SurfacePlanSnapInput
|
|
19
|
+
export type SlabPlanSnapResult = SurfacePlanSnapResult
|
|
20
|
+
|
|
21
|
+
export function clearSlabSnapFeedback() {
|
|
22
|
+
clearSurfacePlanSnapFeedback()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function resolveSlabPlanPointSnap(input: SlabPlanSnapInput): SlabPlanSnapResult {
|
|
26
|
+
return resolveSurfacePlanPointSnap({
|
|
27
|
+
...input,
|
|
28
|
+
highlightWalls: input.highlightWalls ?? false,
|
|
29
|
+
movingId: input.movingId ?? SLAB_SNAP_MOVING_ID,
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type SlabEdgeBandSnapInput = {
|
|
34
|
+
/** Candidate edge endpoints after the raw/grid perpendicular translation. */
|
|
35
|
+
edge: [[number, number], [number, number]]
|
|
36
|
+
levelId?: string | null
|
|
37
|
+
nodes?: Readonly<Record<string, AnyNode>>
|
|
38
|
+
/**
|
|
39
|
+
* Plan point (typically the cursor) the snap beacon should hug along
|
|
40
|
+
* the wall. Falls back to the snapped edge's midpoint.
|
|
41
|
+
*/
|
|
42
|
+
referencePoint?: [number, number]
|
|
43
|
+
/** Override the mode-driven magnetic gate (tests). */
|
|
44
|
+
magnetic?: boolean
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type SlabEdgeBandSnapResult = {
|
|
48
|
+
/** The candidate edge translated onto the wall centerline. */
|
|
49
|
+
edge: [[number, number], [number, number]]
|
|
50
|
+
wallId: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Edge-level slab reshape snap against wall footprint bands. Unlike the
|
|
55
|
+
* cursor-based `resolveSlabPlanPointSnap`, this tests the DRAGGED EDGE
|
|
56
|
+
* itself (band adoption, span overlap — same permissiveness as the
|
|
57
|
+
* render rule) and sticks it onto the wall CENTERLINE, the canonical
|
|
58
|
+
* stored position; the render rule then places it flush with the face.
|
|
59
|
+
* The beacon, the live preview and the committed polygon therefore all
|
|
60
|
+
* agree. In non-magnetic modes only a tight connect-radius stick
|
|
61
|
+
* remains, mirroring the sanctioned wall connect snap. Publishes /
|
|
62
|
+
* clears the wall-snap beacon as a side effect.
|
|
63
|
+
*/
|
|
64
|
+
export function resolveSlabEdgeBandSnap(
|
|
65
|
+
input: SlabEdgeBandSnapInput,
|
|
66
|
+
): SlabEdgeBandSnapResult | null {
|
|
67
|
+
const nodes = input.nodes ?? useScene.getState().nodes
|
|
68
|
+
const walls = getLevelWalls(nodes, input.levelId)
|
|
69
|
+
const magnetic = input.magnetic ?? isMagneticSnapActive()
|
|
70
|
+
|
|
71
|
+
const snap = snapSlabEdgeToWallBand(
|
|
72
|
+
input.edge[0],
|
|
73
|
+
input.edge[1],
|
|
74
|
+
walls,
|
|
75
|
+
magnetic ? undefined : { maxLateral: WALL_CONNECT_SNAP_RADIUS },
|
|
76
|
+
)
|
|
77
|
+
if (!snap) {
|
|
78
|
+
useWallSnapIndicator.getState().clear()
|
|
79
|
+
useAlignmentGuides.getState().clear()
|
|
80
|
+
return null
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const [a, b] = snap.edge
|
|
84
|
+
const dx = b[0] - a[0]
|
|
85
|
+
const dz = b[1] - a[1]
|
|
86
|
+
const lengthSquared = dx * dx + dz * dz
|
|
87
|
+
let beacon: [number, number] = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2]
|
|
88
|
+
if (input.referencePoint && lengthSquared > 1e-12) {
|
|
89
|
+
const t = Math.max(
|
|
90
|
+
0,
|
|
91
|
+
Math.min(
|
|
92
|
+
1,
|
|
93
|
+
((input.referencePoint[0] - a[0]) * dx + (input.referencePoint[1] - a[1]) * dz) /
|
|
94
|
+
lengthSquared,
|
|
95
|
+
),
|
|
96
|
+
)
|
|
97
|
+
beacon = [a[0] + dx * t, a[1] + dz * t]
|
|
98
|
+
}
|
|
99
|
+
useWallSnapIndicator.getState().set({
|
|
100
|
+
x: beacon[0],
|
|
101
|
+
z: beacon[1],
|
|
102
|
+
kind: 'wall',
|
|
103
|
+
wallIds: [snap.wallId],
|
|
104
|
+
})
|
|
105
|
+
useAlignmentGuides.getState().clear()
|
|
106
|
+
|
|
107
|
+
return { edge: snap.edge, wallId: snap.wallId }
|
|
108
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
cycleSnappingModeIn,
|
|
4
|
+
DEFAULT_SNAPPING_MODE,
|
|
5
|
+
defaultSnappingModeFor,
|
|
6
|
+
nextSnappingMode,
|
|
7
|
+
resolveSnapFlags,
|
|
8
|
+
SNAPPING_MODES,
|
|
9
|
+
snapContextOf,
|
|
10
|
+
snappingModesFor,
|
|
11
|
+
} from './snapping-mode'
|
|
12
|
+
|
|
13
|
+
describe('resolveSnapFlags', () => {
|
|
14
|
+
it('default mode is grid', () => {
|
|
15
|
+
expect(DEFAULT_SNAPPING_MODE).toBe('grid')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it("modes are exclusive: 'grid' snaps to the lattice only", () => {
|
|
19
|
+
expect(resolveSnapFlags('grid')).toEqual({ grid: true, magnetic: false, angles: false })
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it("'off' disables grid, magnetic, and angles", () => {
|
|
23
|
+
expect(resolveSnapFlags('off')).toEqual({ grid: false, magnetic: false, angles: false })
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it("'lines' keeps magnetic but drops the grid lattice and angle lock", () => {
|
|
27
|
+
expect(resolveSnapFlags('lines')).toEqual({ grid: false, magnetic: true, angles: false })
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it("'angles' keeps the angle lock but drops grid and magnetic", () => {
|
|
31
|
+
expect(resolveSnapFlags('angles')).toEqual({ grid: false, magnetic: false, angles: true })
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it("'lines' and 'angles' are distinct", () => {
|
|
35
|
+
expect(resolveSnapFlags('lines')).not.toEqual(resolveSnapFlags('angles'))
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('cycles through every mode and wraps', () => {
|
|
39
|
+
const seen = [DEFAULT_SNAPPING_MODE]
|
|
40
|
+
let mode = DEFAULT_SNAPPING_MODE
|
|
41
|
+
for (let i = 0; i < SNAPPING_MODES.length - 1; i += 1) {
|
|
42
|
+
mode = nextSnappingMode(mode)
|
|
43
|
+
seen.push(mode)
|
|
44
|
+
}
|
|
45
|
+
expect(seen).toEqual(SNAPPING_MODES)
|
|
46
|
+
expect(nextSnappingMode(mode)).toBe(DEFAULT_SNAPPING_MODE)
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe('per-context snapping', () => {
|
|
51
|
+
it('items default to grid with no angle lock', () => {
|
|
52
|
+
expect(defaultSnappingModeFor('item')).toBe('grid')
|
|
53
|
+
expect(snappingModesFor('item')).toEqual(['lines', 'grid', 'off'])
|
|
54
|
+
expect(snappingModesFor('item')).not.toContain('angles')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('walls default to grid and expose the angle lock; polygons do NOT', () => {
|
|
58
|
+
expect(defaultSnappingModeFor('wall')).toBe('grid')
|
|
59
|
+
expect(defaultSnappingModeFor('polygon')).toBe('grid')
|
|
60
|
+
expect(snappingModesFor('wall')).toContain('angles')
|
|
61
|
+
// Angle lock is wall/fence-only — slabs, curves and translates never get it.
|
|
62
|
+
expect(snappingModesFor('polygon')).not.toContain('angles')
|
|
63
|
+
expect(snappingModesFor('polygon')).toEqual(['grid', 'lines', 'off'])
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('cycles within the context set and clamps a foreign value', () => {
|
|
67
|
+
expect(cycleSnappingModeIn('item', 'lines')).toBe('grid')
|
|
68
|
+
expect(cycleSnappingModeIn('item', 'off')).toBe('lines')
|
|
69
|
+
// 'angles' isn't an item mode → restart at the first entry
|
|
70
|
+
expect(cycleSnappingModeIn('item', 'angles')).toBe('lines')
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
describe('snapContextOf (profile-driven, node-declared)', () => {
|
|
75
|
+
// Stands in for the registry's declared `def.snapProfile` (the only per-kind
|
|
76
|
+
// data) — the resolver itself has no kind switch.
|
|
77
|
+
const declared: Record<string, 'item' | 'structural'> = {
|
|
78
|
+
wall: 'structural',
|
|
79
|
+
fence: 'structural',
|
|
80
|
+
item: 'item',
|
|
81
|
+
slab: 'structural',
|
|
82
|
+
ceiling: 'structural',
|
|
83
|
+
roof: 'structural',
|
|
84
|
+
zone: 'structural',
|
|
85
|
+
}
|
|
86
|
+
const profileOf = (t: string) => declared[t]
|
|
87
|
+
const ctx = (
|
|
88
|
+
scope: { kind: string; nodeType?: string; reshape?: string; tool?: string },
|
|
89
|
+
mode = 'select',
|
|
90
|
+
tool: string | null = null,
|
|
91
|
+
) => snapContextOf({ scope, mode, tool, profileOf })
|
|
92
|
+
|
|
93
|
+
it('translating a whole structural node has no angle (polygon, not wall)', () => {
|
|
94
|
+
expect(ctx({ kind: 'moving', nodeType: 'wall' })).toBe('polygon')
|
|
95
|
+
expect(ctx({ kind: 'moving', nodeType: 'slab' })).toBe('polygon')
|
|
96
|
+
expect(ctx({ kind: 'placing', nodeType: 'item' }, 'build', 'item')).toBe('item')
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('endpoint reshape is angle-bearing (wall); curve + polygon vertex edits are not', () => {
|
|
100
|
+
expect(ctx({ kind: 'reshaping', reshape: 'endpoint' })).toBe('wall')
|
|
101
|
+
expect(ctx({ kind: 'reshaping', reshape: 'curve' })).toBe('polygon')
|
|
102
|
+
expect(ctx({ kind: 'reshaping', reshape: 'boundary' })).toBe('polygon')
|
|
103
|
+
expect(ctx({ kind: 'reshaping', reshape: 'hole' })).toBe('polygon')
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('drafting a structural kind (wall OR slab) is angle-bearing (wall)', () => {
|
|
107
|
+
expect(ctx({ kind: 'idle' }, 'build', 'wall')).toBe('wall')
|
|
108
|
+
expect(ctx({ kind: 'idle' }, 'build', 'slab')).toBe('wall')
|
|
109
|
+
expect(ctx({ kind: 'idle' }, 'build', 'item')).toBe('item')
|
|
110
|
+
expect(ctx({ kind: 'idle' }, 'select', null)).toBeNull()
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('an undeclared kind (no snapProfile) gets no snap context', () => {
|
|
114
|
+
expect(ctx({ kind: 'moving', nodeType: 'door' })).toBeNull()
|
|
115
|
+
expect(ctx({ kind: 'idle' }, 'build', 'shelf')).toBeNull()
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('drafting a non-directional structural kind is angle-less (polygon, not wall)', () => {
|
|
119
|
+
// Roof / stair / elevator are placed as footprints, not directional draws →
|
|
120
|
+
// declared `snapDraftDirectional: false`, so their draft context drops the
|
|
121
|
+
// angle-lock mode. Directional structural kinds (no flag) stay `wall`.
|
|
122
|
+
const draftDirectionalOf = (t: string) => t !== 'roof'
|
|
123
|
+
const draftCtx = (tool: string) =>
|
|
124
|
+
snapContextOf({ scope: { kind: 'idle' }, mode: 'build', tool, profileOf, draftDirectionalOf })
|
|
125
|
+
expect(draftCtx('roof')).toBe('polygon')
|
|
126
|
+
expect(draftCtx('wall')).toBe('wall')
|
|
127
|
+
// Also via the explicit `drafting` scope path.
|
|
128
|
+
expect(
|
|
129
|
+
snapContextOf({
|
|
130
|
+
scope: { kind: 'drafting', tool: 'roof' },
|
|
131
|
+
mode: 'build',
|
|
132
|
+
tool: 'roof',
|
|
133
|
+
profileOf,
|
|
134
|
+
draftDirectionalOf,
|
|
135
|
+
}),
|
|
136
|
+
).toBe('polygon')
|
|
137
|
+
})
|
|
138
|
+
})
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import type { SnapProfile } from '@pascal-app/core'
|
|
2
|
+
import { GROUP_MOVE_DRAG_LABEL } from './contextual-help'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Snapping mode is a single global, user-cyclable control that maps onto the
|
|
6
|
+
* two pre-existing snap knobs (`gridSnapStep` grid snap + `magneticSnap`).
|
|
7
|
+
* The default `'grid'` resolves to the exact pair the editor shipped with
|
|
8
|
+
* before this control existed (grid on, magnetic on), so the default path is
|
|
9
|
+
* behaviourally unchanged — only when a user opts into `'lines'` or `'off'`
|
|
10
|
+
* does any snap math get suppressed.
|
|
11
|
+
*/
|
|
12
|
+
export type SnappingMode = 'grid' | 'lines' | 'angles' | 'off'
|
|
13
|
+
|
|
14
|
+
export const SNAPPING_MODES: SnappingMode[] = ['grid', 'lines', 'angles', 'off']
|
|
15
|
+
|
|
16
|
+
export const DEFAULT_SNAPPING_MODE: SnappingMode = 'grid'
|
|
17
|
+
|
|
18
|
+
export type SnapFlags = {
|
|
19
|
+
grid: boolean
|
|
20
|
+
magnetic: boolean
|
|
21
|
+
angles: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Pure mapping from the mode enum onto the individual snap knobs. Modes are
|
|
26
|
+
* EXCLUSIVE — each does exactly what its chip label says, one guide at a time,
|
|
27
|
+
* so the HUD is honest:
|
|
28
|
+
*
|
|
29
|
+
* - `grid` → grid lattice only.
|
|
30
|
+
* - `lines` → magnetic only: alignment axes + wall corner-join (connectivity
|
|
31
|
+
* is part of the "lines" magnetic snap, not a separate always-on behaviour).
|
|
32
|
+
* - `angles` → angle lock only (15°/45° rays).
|
|
33
|
+
* - `off` → nothing snaps (raw cursor).
|
|
34
|
+
*/
|
|
35
|
+
export function resolveSnapFlags(mode: SnappingMode): SnapFlags {
|
|
36
|
+
switch (mode) {
|
|
37
|
+
case 'grid':
|
|
38
|
+
return { grid: true, magnetic: false, angles: false }
|
|
39
|
+
case 'lines':
|
|
40
|
+
return { grid: false, magnetic: true, angles: false }
|
|
41
|
+
case 'angles':
|
|
42
|
+
return { grid: false, magnetic: false, angles: true }
|
|
43
|
+
case 'off':
|
|
44
|
+
return { grid: false, magnetic: false, angles: false }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const SNAPPING_MODE_LABELS: Record<SnappingMode, string> = {
|
|
49
|
+
grid: 'Grid',
|
|
50
|
+
lines: 'Lines',
|
|
51
|
+
angles: 'Angles',
|
|
52
|
+
off: 'Off',
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function getSnappingModeLabel(mode: SnappingMode): string {
|
|
56
|
+
return SNAPPING_MODE_LABELS[mode]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function nextSnappingMode(mode: SnappingMode): SnappingMode {
|
|
60
|
+
const index = SNAPPING_MODES.indexOf(mode)
|
|
61
|
+
return SNAPPING_MODES[(index + 1) % SNAPPING_MODES.length] ?? DEFAULT_SNAPPING_MODE
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── Per-context snapping ──────────────────────────────────────────────────────
|
|
65
|
+
//
|
|
66
|
+
// Snapping is no longer one global value: each *activity* has its own mode set
|
|
67
|
+
// and default, because they want different behaviour (drawing a wall wants a
|
|
68
|
+
// grid + angle lock; nudging an item wants free movement that only catches on
|
|
69
|
+
// alignment lines). The mode is remembered per context and shown live, so it's
|
|
70
|
+
// never a silent surprise — it just matches what you're doing.
|
|
71
|
+
|
|
72
|
+
export type SnapContext = 'wall' | 'item' | 'polygon'
|
|
73
|
+
|
|
74
|
+
// The cyclable mode-set for a context (distinct from the node's `SnapProfile`).
|
|
75
|
+
type SnapModeSet = { modes: SnappingMode[]; default: SnappingMode }
|
|
76
|
+
|
|
77
|
+
// `modes[0]` is the cycle's first entry; `default` is what a context starts at.
|
|
78
|
+
// The 'wall' set is the ONLY one with an angle lock — it applies solely when
|
|
79
|
+
// you're setting a segment's DIRECTION (wall/fence drafting + endpoint drag).
|
|
80
|
+
// Translating a whole wall, curving it, or drawing/moving a slab can't change an
|
|
81
|
+
// angle, so those use the no-angle 'polygon' set.
|
|
82
|
+
const SNAP_PROFILES: Record<SnapContext, SnapModeSet> = {
|
|
83
|
+
// Wall / fence drafting + endpoint reshape: direction matters → angle lock.
|
|
84
|
+
wall: { modes: ['grid', 'lines', 'angles', 'off'], default: 'grid' },
|
|
85
|
+
// Item placement / move: grid by default; lines = magnetic alignment only (no
|
|
86
|
+
// grid lattice), no angle lock (meaningless for a footprint).
|
|
87
|
+
item: { modes: ['lines', 'grid', 'off'], default: 'grid' },
|
|
88
|
+
// Structural / surface, no direction to set: slab / ceiling / roof draft+move,
|
|
89
|
+
// whole wall/fence translate, curve reshape, polygon boundary edit. Grid by
|
|
90
|
+
// default, NO angle lock.
|
|
91
|
+
polygon: { modes: ['grid', 'lines', 'off'], default: 'grid' },
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export const SNAP_CONTEXTS: SnapContext[] = ['wall', 'item', 'polygon']
|
|
95
|
+
|
|
96
|
+
export function snappingModesFor(context: SnapContext): SnappingMode[] {
|
|
97
|
+
return SNAP_PROFILES[context].modes
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function defaultSnappingModeFor(context: SnapContext): SnappingMode {
|
|
101
|
+
return SNAP_PROFILES[context].default
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Cycle within the context's own set (clamps a foreign value to the first entry).
|
|
105
|
+
export function cycleSnappingModeIn(context: SnapContext, mode: SnappingMode): SnappingMode {
|
|
106
|
+
const modes = SNAP_PROFILES[context].modes
|
|
107
|
+
const index = modes.indexOf(mode)
|
|
108
|
+
return modes[(index + 1) % modes.length] ?? modes[0] ?? DEFAULT_SNAPPING_MODE
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// The kind's declared `snapProfile` (from the registry) → the active mode-set
|
|
112
|
+
// context. The only behaviour difference is the angle lock, which a `structural`
|
|
113
|
+
// kind gets while SETTING DIRECTION (drafting a run/polygon, dragging an endpoint
|
|
114
|
+
// or a polygon vertex) — never while translating or curving. A node with no
|
|
115
|
+
// declared profile has no snapping UI (chip) yet — returns null.
|
|
116
|
+
function contextForProfile(
|
|
117
|
+
profile: SnapProfile | undefined,
|
|
118
|
+
directionSetting: boolean,
|
|
119
|
+
): SnapContext | null {
|
|
120
|
+
if (profile === 'item') return 'item'
|
|
121
|
+
if (profile === 'structural') return directionSetting ? 'wall' : 'polygon'
|
|
122
|
+
return null
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The active snapping context, derived from what the user is doing — fully
|
|
127
|
+
* node-declared: the kind's `snapProfile` (looked up via the injected
|
|
128
|
+
* `profileOf`) supplies the data, and this maps (profile × action) to the
|
|
129
|
+
* mode-set. No per-kind switch lives here. `profileOf` is injected so this stays
|
|
130
|
+
* pure + testable and `snapping-mode` need not import the registry.
|
|
131
|
+
*
|
|
132
|
+
* Prefers the authoritative interaction scope; falls back to the build tool
|
|
133
|
+
* because the `drafting` scope isn't wired yet (wall/slab draw runs idle).
|
|
134
|
+
* Returns null when nothing snappable is active → no chip, safe-default snap.
|
|
135
|
+
*/
|
|
136
|
+
export function snapContextOf(args: {
|
|
137
|
+
scope: {
|
|
138
|
+
kind: string
|
|
139
|
+
nodeType?: string
|
|
140
|
+
reshape?: string
|
|
141
|
+
nodeId?: string
|
|
142
|
+
tool?: string
|
|
143
|
+
handle?: string
|
|
144
|
+
}
|
|
145
|
+
mode: string
|
|
146
|
+
tool: string | null
|
|
147
|
+
profileOf: (typeOrTool: string) => SnapProfile | undefined
|
|
148
|
+
// Whether drafting a kind sets a direction (angle-lock meaningful). Injected
|
|
149
|
+
// like `profileOf` so `snapping-mode` need not import the registry; defaults
|
|
150
|
+
// to `true` (the structural draw default) when not supplied.
|
|
151
|
+
draftDirectionalOf?: (typeOrTool: string) => boolean
|
|
152
|
+
}): SnapContext | null {
|
|
153
|
+
const { scope, mode, tool, profileOf, draftDirectionalOf } = args
|
|
154
|
+
// The group-move gizmo translates the whole selection — same no-angle
|
|
155
|
+
// treatment as a single-node move, so Shift cycles the 'item' modes and the
|
|
156
|
+
// HUD shows the item snapping chips for the drag.
|
|
157
|
+
if (scope.kind === 'handle-drag' && scope.handle === GROUP_MOVE_DRAG_LABEL) {
|
|
158
|
+
return 'item'
|
|
159
|
+
}
|
|
160
|
+
switch (scope.kind) {
|
|
161
|
+
case 'placing':
|
|
162
|
+
case 'moving':
|
|
163
|
+
// A whole-node translate never sets direction → no angle.
|
|
164
|
+
return scope.nodeType ? contextForProfile(profileOf(scope.nodeType), false) : null
|
|
165
|
+
case 'reshaping':
|
|
166
|
+
// Dragging a wall ENDPOINT sets the segment's direction → angle-bearing
|
|
167
|
+
// 'wall'. Curving, and polygon vertex/edge edits (boundary / hole), don't
|
|
168
|
+
// — they use the no-angle 'polygon' set (grid / lines / off).
|
|
169
|
+
return scope.reshape === 'endpoint' ? 'wall' : 'polygon'
|
|
170
|
+
case 'drafting':
|
|
171
|
+
return scope.tool
|
|
172
|
+
? contextForProfile(profileOf(scope.tool), draftDirectionalOf?.(scope.tool) ?? true)
|
|
173
|
+
: null
|
|
174
|
+
default:
|
|
175
|
+
return mode === 'build' && tool
|
|
176
|
+
? contextForProfile(profileOf(tool), draftDirectionalOf?.(tool) ?? true)
|
|
177
|
+
: null
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -37,7 +37,7 @@ function stripDuplicateFlags(metadata: unknown) {
|
|
|
37
37
|
|
|
38
38
|
function moveStairWhenRegistered(stairId: StairNode['id'], attempt = 0) {
|
|
39
39
|
const latestStair = useScene.getState().nodes[stairId as AnyNodeId]
|
|
40
|
-
if (
|
|
40
|
+
if (latestStair?.type !== 'stair') {
|
|
41
41
|
return
|
|
42
42
|
}
|
|
43
43
|
|
|
@@ -64,7 +64,7 @@ export function duplicateStairSubtree(
|
|
|
64
64
|
const scene = useScene.getState()
|
|
65
65
|
const sourceStair = scene.nodes[sourceStairId]
|
|
66
66
|
|
|
67
|
-
if (
|
|
67
|
+
if (sourceStair?.type !== 'stair') {
|
|
68
68
|
throw new Error(`Node "${sourceStairId}" is not a stair`)
|
|
69
69
|
}
|
|
70
70
|
|
|
@@ -89,7 +89,7 @@ export function duplicateStairSubtree(
|
|
|
89
89
|
const segmentClones: StairSegmentNode[] = []
|
|
90
90
|
for (const childId of sourceStair.children ?? []) {
|
|
91
91
|
const childNode = scene.nodes[childId as AnyNodeId]
|
|
92
|
-
if (
|
|
92
|
+
if (childNode?.type !== 'stair-segment') {
|
|
93
93
|
continue
|
|
94
94
|
}
|
|
95
95
|
|
|
@@ -108,7 +108,7 @@ export function duplicateStairSubtree(
|
|
|
108
108
|
])
|
|
109
109
|
|
|
110
110
|
const createdStair = useScene.getState().nodes[stairClone.id as AnyNodeId]
|
|
111
|
-
if (
|
|
111
|
+
if (createdStair?.type !== 'stair') {
|
|
112
112
|
throw new Error(`Duplicated stair "${stairClone.id}" was not created`)
|
|
113
113
|
}
|
|
114
114
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import type { AnyNode, WallNode } from '@pascal-app/core'
|
|
3
|
+
import { resolveSurfacePlanPointSnap } from './surface-plan-snap'
|
|
4
|
+
|
|
5
|
+
const wall = (id: string, start: [number, number], end: [number, number]): WallNode =>
|
|
6
|
+
({
|
|
7
|
+
id,
|
|
8
|
+
type: 'wall',
|
|
9
|
+
start,
|
|
10
|
+
end,
|
|
11
|
+
visible: true,
|
|
12
|
+
}) as WallNode
|
|
13
|
+
|
|
14
|
+
const nodesOf = (...walls: WallNode[]): Record<string, AnyNode> =>
|
|
15
|
+
Object.fromEntries(walls.map((node) => [node.id, node as AnyNode]))
|
|
16
|
+
|
|
17
|
+
// The measurement tool passes `magnetic` explicitly so its snapping never
|
|
18
|
+
// depends on the construction snapping-mode chip (whose default 'grid' mode
|
|
19
|
+
// turns `isMagneticSnapActive()` off). These tests pin that seam.
|
|
20
|
+
describe('resolveSurfacePlanPointSnap magnetic override', () => {
|
|
21
|
+
const walls = [wall('wall-a', [0, 0], [4, 0]), wall('wall-b', [0, 0], [0, 4])]
|
|
22
|
+
const nodes = nodesOf(...walls)
|
|
23
|
+
|
|
24
|
+
test('magnetic: true acquires a shared wall corner from the full endpoint radius', () => {
|
|
25
|
+
const result = resolveSurfacePlanPointSnap({
|
|
26
|
+
rawPoint: [0.3, 0.2],
|
|
27
|
+
nodes,
|
|
28
|
+
magnetic: true,
|
|
29
|
+
align: false,
|
|
30
|
+
})
|
|
31
|
+
expect(result.point).toEqual([0, 0])
|
|
32
|
+
expect(result.wallSnap).toBe('endpoint')
|
|
33
|
+
expect(result.wallIds.sort()).toEqual(['wall-a', 'wall-b'])
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('magnetic: true acquires a T-junction crossing on a wall body', () => {
|
|
37
|
+
const crossing = [wall('wall-a', [0, 0], [4, 0]), wall('wall-c', [3, -1], [3, 3])]
|
|
38
|
+
const result = resolveSurfacePlanPointSnap({
|
|
39
|
+
rawPoint: [3.05, 0.1],
|
|
40
|
+
nodes: nodesOf(...crossing),
|
|
41
|
+
magnetic: true,
|
|
42
|
+
align: false,
|
|
43
|
+
})
|
|
44
|
+
expect(result.point).toEqual([3, 0])
|
|
45
|
+
expect(result.wallSnap).toBe('intersection')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('magnetic: false keeps the fallback point outside the tight connect radius', () => {
|
|
49
|
+
const result = resolveSurfacePlanPointSnap({
|
|
50
|
+
rawPoint: [0.3, 0.2],
|
|
51
|
+
fallbackPoint: [0.3, 0.2],
|
|
52
|
+
nodes,
|
|
53
|
+
magnetic: false,
|
|
54
|
+
align: false,
|
|
55
|
+
})
|
|
56
|
+
expect(result.point).toEqual([0.3, 0.2])
|
|
57
|
+
expect(result.wallSnap).toBeNull()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('magnetic: false still sticks within the connect radius so ends can meet', () => {
|
|
61
|
+
const result = resolveSurfacePlanPointSnap({
|
|
62
|
+
rawPoint: [0.03, 0.02],
|
|
63
|
+
fallbackPoint: [0.03, 0.02],
|
|
64
|
+
nodes,
|
|
65
|
+
magnetic: false,
|
|
66
|
+
align: false,
|
|
67
|
+
})
|
|
68
|
+
expect(result.point).toEqual([0, 0])
|
|
69
|
+
expect(result.wallSnap).toBe('endpoint')
|
|
70
|
+
})
|
|
71
|
+
})
|