@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
|
@@ -3,30 +3,52 @@
|
|
|
3
3
|
import '../../../three-types'
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
|
+
type AlignmentGuide,
|
|
6
7
|
type AnyNode,
|
|
7
8
|
type AnyNodeId,
|
|
9
|
+
analyzePortConnectivity,
|
|
10
|
+
bboxCornerAnchors,
|
|
8
11
|
collectAlignmentAnchors,
|
|
12
|
+
createSceneApi,
|
|
9
13
|
type EventSuffix,
|
|
10
14
|
emitter,
|
|
15
|
+
footprintAABBFrom,
|
|
11
16
|
type GridEvent,
|
|
17
|
+
getFloorPlacedFootprints,
|
|
12
18
|
movingFootprintAnchors,
|
|
13
19
|
type NodeEvent,
|
|
14
20
|
nodeRegistry,
|
|
21
|
+
type ParentFrameSnapMatch,
|
|
22
|
+
type PortConnectivity,
|
|
15
23
|
resolveAlignment,
|
|
24
|
+
resolveConnectivityUpdates,
|
|
25
|
+
resolveFacingIndicator,
|
|
16
26
|
sceneRegistry,
|
|
17
27
|
spatialGridManager,
|
|
28
|
+
useLiveNodeOverrides,
|
|
18
29
|
useLiveTransforms,
|
|
19
30
|
useScene,
|
|
20
31
|
} from '@pascal-app/core'
|
|
21
|
-
import { useAlignmentGuides } from '@pascal-app/editor'
|
|
22
32
|
import { useViewer } from '@pascal-app/viewer'
|
|
23
33
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
24
34
|
import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
|
|
25
35
|
import { commitFreshPlacementSubtree } from '../../../lib/fresh-planar-placement'
|
|
26
36
|
import { stripPlacementMetadataFlags } from '../../../lib/placement-metadata'
|
|
27
37
|
import { resolvePlanarCursorPosition } from '../../../lib/planar-cursor-placement'
|
|
38
|
+
import { movementSfxStepKey } from '../../../lib/sfx/movement-tick'
|
|
28
39
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
29
|
-
import
|
|
40
|
+
import { resolveSnapFlags } from '../../../lib/snapping-mode'
|
|
41
|
+
|
|
42
|
+
import useAlignmentGuides from '../../../store/use-alignment-guides'
|
|
43
|
+
import useEditor, {
|
|
44
|
+
getActiveSnappingMode,
|
|
45
|
+
isAlignmentGuideActive,
|
|
46
|
+
isGridSnapActive,
|
|
47
|
+
isMagneticSnapActive,
|
|
48
|
+
} from '../../../store/use-editor'
|
|
49
|
+
|
|
50
|
+
import useFacingPose from '../../../store/use-facing-pose'
|
|
51
|
+
import { swallowNextClick } from '../../editor/node-arrow-handles'
|
|
30
52
|
import { CursorSphere } from '../shared/cursor-sphere'
|
|
31
53
|
import { DragBoundingBox } from '../shared/drag-bounding-box'
|
|
32
54
|
import { getFloorStackPreviewPosition } from '../shared/floor-stack-preview'
|
|
@@ -36,12 +58,127 @@ import { PlacementBox } from '../shared/placement-box'
|
|
|
36
58
|
/** Snap a world-plan coordinate to the editor's active grid step (0.5 / 0.25
|
|
37
59
|
* / 0.1 / 0.05), read live so changing the step mid-drag takes effect. */
|
|
38
60
|
const snapToGridStep = (value: number) => {
|
|
61
|
+
if (!resolveSnapFlags(getActiveSnappingMode()).grid) return value
|
|
39
62
|
const step = useEditor.getState().gridSnapStep
|
|
40
63
|
return Math.round(value / step) * step
|
|
41
64
|
}
|
|
42
65
|
|
|
43
|
-
/**
|
|
44
|
-
const ROTATION_STEP = Math.PI /
|
|
66
|
+
/** 45° steps, matching the GLB item placement rotation. */
|
|
67
|
+
const ROTATION_STEP = Math.PI / 4
|
|
68
|
+
|
|
69
|
+
/** Default magnetic radius (meters, XZ) for `movable.portSnap`. */
|
|
70
|
+
const PORT_SNAP_RADIUS_M = 0.5
|
|
71
|
+
const VALID_COLOR = 0x22_c5_5e
|
|
72
|
+
const INVALID_COLOR = 0xef_44_44
|
|
73
|
+
|
|
74
|
+
type DragBoundsOverride = {
|
|
75
|
+
size: [number, number, number]
|
|
76
|
+
center?: [number, number, number]
|
|
77
|
+
centerY?: number
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function offsetPlanPositionByLocalCenter(
|
|
81
|
+
position: [number, number, number],
|
|
82
|
+
center: [number, number, number],
|
|
83
|
+
rotationY: number,
|
|
84
|
+
): [number, number, number] {
|
|
85
|
+
const cos = Math.cos(rotationY)
|
|
86
|
+
const sin = Math.sin(rotationY)
|
|
87
|
+
return [
|
|
88
|
+
position[0] + center[0] * cos + center[2] * sin,
|
|
89
|
+
position[1] + center[1],
|
|
90
|
+
position[2] - center[0] * sin + center[2] * cos,
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Alignment anchors for the moving node. When the kind declares
|
|
96
|
+
* `capabilities.dragBounds` with an off-origin `center` (a composite cabinet
|
|
97
|
+
* run whose modules extend past the node origin), the anchors come from that
|
|
98
|
+
* declared box instead of the origin-centred footprint.
|
|
99
|
+
*/
|
|
100
|
+
function movingDragBoundsAnchors(
|
|
101
|
+
node: AnyNode,
|
|
102
|
+
bounds: DragBoundsOverride | null,
|
|
103
|
+
x: number,
|
|
104
|
+
z: number,
|
|
105
|
+
rotationY: number,
|
|
106
|
+
) {
|
|
107
|
+
if (!bounds?.center) return movingFootprintAnchors(node, x, z, rotationY)
|
|
108
|
+
const center = offsetPlanPositionByLocalCenter([x, 0, z], bounds.center, rotationY)
|
|
109
|
+
const aabb = footprintAABBFrom(center, bounds.size, rotationY)
|
|
110
|
+
return bboxCornerAnchors(node.id, aabb.minX, aabb.minZ, aabb.maxX, aabb.maxZ)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function alignmentGuideFromParentFrameMatch(match: ParentFrameSnapMatch): AlignmentGuide {
|
|
114
|
+
return {
|
|
115
|
+
axis: match.axis,
|
|
116
|
+
coord: match.axis === 'x' ? match.from.x : match.from.z,
|
|
117
|
+
from: match.from,
|
|
118
|
+
to: match.to,
|
|
119
|
+
anchor: match.from,
|
|
120
|
+
movingAnchorKind: 'corner',
|
|
121
|
+
candidateAnchorKind: 'corner',
|
|
122
|
+
candidateNodeId: match.candidateNodeId,
|
|
123
|
+
distance: Math.hypot(match.to.x - match.from.x, match.to.z - match.from.z),
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Magnetic port snap for a dragged node: if one of the node's own ports
|
|
129
|
+
* (read live from `def.ports`) lands within `radius` of a matching scene
|
|
130
|
+
* port at the candidate XZ, return the node XZ that mates them exactly.
|
|
131
|
+
*
|
|
132
|
+
* Pure core: ports come through `nodeRegistry` so this stays layer-clean.
|
|
133
|
+
* Ports are level-local meters — the same frame as the cursor's
|
|
134
|
+
* `localPosition`, so no extra transform is needed. The dragged node's
|
|
135
|
+
* ports move rigidly with its position, so a port at candidate `(x,z)`
|
|
136
|
+
* sits at `portStored + (candidate - nodeStored)`. We pick the closest
|
|
137
|
+
* (own-port, target-port) pair and shift the node so they coincide in XZ.
|
|
138
|
+
*/
|
|
139
|
+
function resolvePortSnap(
|
|
140
|
+
node: AnyNode,
|
|
141
|
+
candidate: [number, number],
|
|
142
|
+
config: { systems?: readonly string[]; radius?: number },
|
|
143
|
+
): [number, number] | null {
|
|
144
|
+
const nodePos = (node as { position?: [number, number, number] }).position
|
|
145
|
+
if (!nodePos) return null
|
|
146
|
+
const ownPorts = nodeRegistry.get(node.type)?.ports?.(node)
|
|
147
|
+
if (!ownPorts || ownPorts.length === 0) return null
|
|
148
|
+
|
|
149
|
+
const radius = config.radius ?? PORT_SNAP_RADIUS_M
|
|
150
|
+
const radiusSq = radius * radius
|
|
151
|
+
const { systems } = config
|
|
152
|
+
const dragDx = candidate[0] - nodePos[0]
|
|
153
|
+
const dragDz = candidate[1] - nodePos[2]
|
|
154
|
+
|
|
155
|
+
const nodes = useScene.getState().nodes
|
|
156
|
+
let bestDistSq = radiusSq
|
|
157
|
+
let snap: [number, number] | null = null
|
|
158
|
+
|
|
159
|
+
for (const node2 of Object.values(nodes)) {
|
|
160
|
+
if (!node2 || node2.id === node.id) continue
|
|
161
|
+
const targets = nodeRegistry.get(node2.type)?.ports?.(node2)
|
|
162
|
+
if (!targets) continue
|
|
163
|
+
for (const target of targets) {
|
|
164
|
+
if (systems && target.system !== undefined && !systems.includes(target.system)) continue
|
|
165
|
+
for (const own of ownPorts) {
|
|
166
|
+
// Own port at the candidate position = stored port + drag delta.
|
|
167
|
+
const ownX = own.position[0] + dragDx
|
|
168
|
+
const ownZ = own.position[2] + dragDz
|
|
169
|
+
const dx = target.position[0] - ownX
|
|
170
|
+
const dz = target.position[2] - ownZ
|
|
171
|
+
const distSq = dx * dx + dz * dz
|
|
172
|
+
if (distSq <= bestDistSq) {
|
|
173
|
+
bestDistSq = distSq
|
|
174
|
+
// Shift the node so this own port lands on the target (XZ only).
|
|
175
|
+
snap = [candidate[0] + dx, candidate[1] + dz]
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return snap
|
|
181
|
+
}
|
|
45
182
|
|
|
46
183
|
/** Figma-style alignment-snap threshold (meters), matching the 2D
|
|
47
184
|
* floor-plan overlay's `ALIGNMENT_THRESHOLD_M`. 8 cm gives a magnetic pull
|
|
@@ -94,6 +231,15 @@ const CLICK_TRIGGER_KINDS = [
|
|
|
94
231
|
] as const
|
|
95
232
|
|
|
96
233
|
export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
234
|
+
// Kinds whose `position` lives in a host parent's local frame declare
|
|
235
|
+
// `movable.parentFrame` (cabinet module ↔ its run). The tool converts the
|
|
236
|
+
// plan-frame cursor through the capability's hooks and previews via
|
|
237
|
+
// `useLiveNodeOverrides` so the parent's composite geometry re-flows.
|
|
238
|
+
const parentFrame = nodeRegistry.get(node.type)?.capabilities?.movable?.parentFrame ?? null
|
|
239
|
+
const frameParent = useMemo(
|
|
240
|
+
() => parentFrame?.resolveParent(node, useScene.getState().nodes) ?? null,
|
|
241
|
+
[parentFrame, node],
|
|
242
|
+
)
|
|
97
243
|
const originalPosition: [number, number, number] = useMemo(
|
|
98
244
|
() =>
|
|
99
245
|
'position' in node && Array.isArray((node as { position?: unknown }).position)
|
|
@@ -119,7 +265,7 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
119
265
|
return 0
|
|
120
266
|
}, [node])
|
|
121
267
|
const [cursorPosition, setCursorPosition] = useState<[number, number, number]>(originalPosition)
|
|
122
|
-
const previousSnapRef = useRef<
|
|
268
|
+
const previousSnapRef = useRef<string | null>(null)
|
|
123
269
|
/**
|
|
124
270
|
* The latest snapped cursor position from `grid:move`. We commit at
|
|
125
271
|
* THIS position regardless of which event variant fires the click —
|
|
@@ -144,28 +290,107 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
144
290
|
// and bumped by R/T. Applied imperatively + mirrored to `useLiveTransforms`,
|
|
145
291
|
// and committed to the scene on drop.
|
|
146
292
|
const rotationRef = useRef(originalRotationY)
|
|
293
|
+
// Snapshot of which ducts / fittings are mated to this node's ports at
|
|
294
|
+
// drag-start (duct fittings only). Drives the "connected ductwork follows"
|
|
295
|
+
// behaviour: connected nodes preview through `useLiveNodeOverrides` during
|
|
296
|
+
// the drag and commit alongside the moved node on drop. Null for kinds with
|
|
297
|
+
// no ports, so every other movable kind is unaffected.
|
|
298
|
+
const connectivityRef = useRef<PortConnectivity | null>(null)
|
|
299
|
+
// Node ids this drag has pushed live overrides onto — cleared on
|
|
300
|
+
// commit / cancel / unmount so a follow-on drag starts clean.
|
|
301
|
+
const overriddenIdsRef = useRef<AnyNodeId[]>([])
|
|
147
302
|
|
|
148
|
-
//
|
|
149
|
-
// (instead of the vertical-arrow cursor) and
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
|
|
153
|
-
|
|
303
|
+
// Colliding floor kinds (item / shelf / column) show the same green/red
|
|
304
|
+
// footprint box GLB items use (instead of the vertical-arrow cursor) and
|
|
305
|
+
// refuse an invalid drop unless Alt forces it. The gate + footprint both come
|
|
306
|
+
// from the kind's declarative `floorPlaced` capability, so opting a new kind
|
|
307
|
+
// in is just `collides: true` — no change here.
|
|
308
|
+
// Parent-frame kinds skip the world-frame floor-collision box — their
|
|
309
|
+
// position isn't in the level frame the spatial grid indexes.
|
|
310
|
+
const collides =
|
|
311
|
+
!frameParent && nodeRegistry.get(node.type)?.capabilities?.floorPlaced?.collides === true
|
|
312
|
+
// Snapshot the scene once at drag-start — bounds depend on `node` (locked
|
|
313
|
+
// for the lifetime of this tool) and any sibling state the kind reads. If a
|
|
314
|
+
// future kind needs live sibling state mid-drag, switch to a subscribed
|
|
315
|
+
// selector; for v1 (elevator shaft, cabinet run) start-time is correct and
|
|
316
|
+
// avoids subscribing the whole `nodes` map.
|
|
317
|
+
const dragBounds = useMemo(
|
|
318
|
+
(): DragBoundsOverride | null =>
|
|
319
|
+
(nodeRegistry.get(node.type)?.capabilities?.dragBounds?.(node, useScene.getState().nodes) as
|
|
320
|
+
| DragBoundsOverride
|
|
321
|
+
| undefined) ?? null,
|
|
322
|
+
[node],
|
|
323
|
+
)
|
|
324
|
+
// Collision extents: the declared drag bounds (composite kinds — a cabinet
|
|
325
|
+
// run spans its modules) win over the single-node footprint.
|
|
326
|
+
const resolvedFootprint = useMemo(
|
|
154
327
|
() =>
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
328
|
+
dragBounds?.size ??
|
|
329
|
+
nodeRegistry.get(node.type)?.capabilities?.floorPlaced?.footprint?.(node)?.dimensions ??
|
|
330
|
+
null,
|
|
331
|
+
[dragBounds, node],
|
|
332
|
+
)
|
|
333
|
+
const boxDimensions = useMemo(
|
|
334
|
+
() => (collides ? resolvedFootprint : null),
|
|
335
|
+
[collides, resolvedFootprint],
|
|
160
336
|
)
|
|
161
337
|
const [valid, setValid] = useState(true)
|
|
162
|
-
const
|
|
338
|
+
const previewRotationY = useCallback(
|
|
339
|
+
(rotationY = rotationRef.current) =>
|
|
340
|
+
parentFrame && frameParent
|
|
341
|
+
? parentFrame.parentRotationY(frameParent, useScene.getState().nodes) + rotationY
|
|
342
|
+
: rotationY,
|
|
343
|
+
[parentFrame, frameParent],
|
|
344
|
+
)
|
|
345
|
+
const visualPositionFor = useCallback(
|
|
346
|
+
(position: [number, number, number], rotationY = rotationRef.current) => {
|
|
347
|
+
if (parentFrame && frameParent) {
|
|
348
|
+
return parentFrame.localToPlan(frameParent, position, useScene.getState().nodes)
|
|
349
|
+
}
|
|
350
|
+
return getFloorStackPreviewPosition({
|
|
351
|
+
node,
|
|
352
|
+
position,
|
|
353
|
+
rotation: (() => {
|
|
354
|
+
const r = (node as { rotation?: unknown }).rotation
|
|
355
|
+
return Array.isArray(r)
|
|
356
|
+
? [(r[0] as number) ?? 0, rotationY, (r[2] as number) ?? 0]
|
|
357
|
+
: rotationY
|
|
358
|
+
})(),
|
|
359
|
+
})
|
|
360
|
+
},
|
|
361
|
+
[parentFrame, frameParent, node],
|
|
362
|
+
)
|
|
363
|
+
const canonicalPositionFromPlan = useCallback(
|
|
364
|
+
(planX: number, localY: number, planZ: number): [number, number, number] =>
|
|
365
|
+
parentFrame && frameParent
|
|
366
|
+
? parentFrame.planToLocal(frameParent, planX, localY, planZ, useScene.getState().nodes)
|
|
367
|
+
: [planX, localY, planZ],
|
|
368
|
+
[parentFrame, frameParent],
|
|
369
|
+
)
|
|
370
|
+
const originalPlanPosition = useMemo(
|
|
371
|
+
() => visualPositionFor(originalPosition, originalRotationY),
|
|
372
|
+
[originalPosition, originalRotationY, visualPositionFor],
|
|
373
|
+
)
|
|
374
|
+
const [cursorRotationY, setCursorRotationY] = useState(() => previewRotationY(originalRotationY))
|
|
163
375
|
const { isFreshPlacement, previewVisible, revealFreshPlacement, useAbsoluteCursorPlacement } =
|
|
164
376
|
useFreshPlacementVisibility({ node })
|
|
165
|
-
//
|
|
377
|
+
// Kinds that declare `movable.cursorAttached` (duct fittings) pin to the
|
|
378
|
+
// cursor instead of preserving the grab offset — small connector-like
|
|
379
|
+
// nodes read an offset drag as "lagging behind the mouse".
|
|
380
|
+
const cursorAttached = nodeRegistry.get(node.type)?.capabilities?.movable?.cursorAttached === true
|
|
381
|
+
// Kinds that declare `movable.portSnap` (duct terminals) magnetically
|
|
382
|
+
// mate one of their own ports onto a nearby scene port while dragging —
|
|
383
|
+
// a register collar drops onto a duct run end. Reads `def.ports` through
|
|
384
|
+
// the core registry, so it stays layer-clean (no @pascal-app/nodes import).
|
|
385
|
+
const portSnapConfig = nodeRegistry.get(node.type)?.capabilities?.movable?.portSnap ?? null
|
|
386
|
+
// Kind-owned magnetic snap for the generic 3D move path. Cabinets use this
|
|
387
|
+
// to settle a dragged run flush against a wall without forking the move tool.
|
|
388
|
+
const groupMoveSnapConfig =
|
|
389
|
+
nodeRegistry.get(node.type)?.capabilities?.movable?.groupMoveSnap ?? null
|
|
390
|
+
// Mirrors of `valid` / Alt for the event handlers inside the effect, which
|
|
166
391
|
// can't read React state without stale closures.
|
|
167
392
|
const validRef = useRef(true)
|
|
168
|
-
const
|
|
393
|
+
const altRef = useRef(false)
|
|
169
394
|
|
|
170
395
|
const exitMoveMode = useCallback(() => {
|
|
171
396
|
useEditor.getState().setMovingNode(null)
|
|
@@ -177,14 +402,14 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
177
402
|
dragAnchorRef.current = null
|
|
178
403
|
hasMovedRef.current = false
|
|
179
404
|
rotationRef.current = originalRotationY
|
|
180
|
-
|
|
405
|
+
altRef.current = false
|
|
181
406
|
validRef.current = true
|
|
182
407
|
// Re-sync the box transform to the (possibly new) node. `node` changes
|
|
183
408
|
// without this component remounting whenever a positioned preset re-arms a
|
|
184
409
|
// fresh clone after a drop, or the user picks a different catalog tile —
|
|
185
410
|
// and `useState` only honours its initial value, so without this the box
|
|
186
411
|
// would keep the previous clone's rotation/position until the next R/T.
|
|
187
|
-
setCursorRotationY(originalRotationY)
|
|
412
|
+
setCursorRotationY(previewRotationY(originalRotationY))
|
|
188
413
|
lastCursorRef.current = originalPosition
|
|
189
414
|
let committed = false
|
|
190
415
|
const isNew = isFreshPlacement
|
|
@@ -195,15 +420,12 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
195
420
|
? [(baseRotation[0] as number) ?? 0, y, (baseRotation[2] as number) ?? 0]
|
|
196
421
|
: y
|
|
197
422
|
|
|
198
|
-
const getVisualPosition =
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
position,
|
|
205
|
-
rotation: toCommitRotation(rotationY),
|
|
206
|
-
})
|
|
423
|
+
const getVisualPosition = visualPositionFor
|
|
424
|
+
const applyMeshPose = (position: [number, number, number], rotationY = rotationRef.current) => {
|
|
425
|
+
const object = sceneRegistry.nodes.get(node.id)
|
|
426
|
+
if (!object) return
|
|
427
|
+
object.position.set(...position)
|
|
428
|
+
object.rotation.y = rotationY
|
|
207
429
|
}
|
|
208
430
|
const markMovedNodeDirty = () => {
|
|
209
431
|
if (useScene.getState().nodes[node.id]) {
|
|
@@ -211,15 +433,69 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
211
433
|
}
|
|
212
434
|
}
|
|
213
435
|
|
|
436
|
+
// Connectivity follow (duct fittings): the moved node with its live drag
|
|
437
|
+
// transform, so `def.ports` recomputes for `resolveConnectivityUpdates`.
|
|
438
|
+
// Uses the logical (un-stacked) position + Y rotation that commit writes,
|
|
439
|
+
// not the floor-lifted visual position.
|
|
440
|
+
const buildPreviewNode = (position: [number, number, number], rotationY: number): AnyNode =>
|
|
441
|
+
({
|
|
442
|
+
...(node as Record<string, unknown>),
|
|
443
|
+
position,
|
|
444
|
+
rotation: toCommitRotation(rotationY),
|
|
445
|
+
}) as AnyNode
|
|
446
|
+
|
|
447
|
+
// Resolve the patches that keep connected ductwork attached and preview
|
|
448
|
+
// them through `useLiveNodeOverrides` (transient — no history churn;
|
|
449
|
+
// GeometrySystem merges overrides via getEffectiveNode). Each connected
|
|
450
|
+
// node is re-dirtied so its geometry rebuilds against the new override.
|
|
451
|
+
const previewConnectivity = (position: [number, number, number], rotationY: number) => {
|
|
452
|
+
const connectivity = connectivityRef.current
|
|
453
|
+
if (!connectivity) return
|
|
454
|
+
const updates = resolveConnectivityUpdates(
|
|
455
|
+
connectivity,
|
|
456
|
+
buildPreviewNode(position, rotationY),
|
|
457
|
+
)
|
|
458
|
+
if (updates.length === 0) return
|
|
459
|
+
useLiveNodeOverrides
|
|
460
|
+
.getState()
|
|
461
|
+
.setMany(updates.map((u) => [u.id, u.data as Record<string, unknown>] as const))
|
|
462
|
+
overriddenIdsRef.current = updates.map((u) => u.id)
|
|
463
|
+
for (const u of updates) {
|
|
464
|
+
if (useScene.getState().nodes[u.id]) useScene.getState().markDirty(u.id)
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const clearConnectivityOverrides = () => {
|
|
469
|
+
for (const id of overriddenIdsRef.current) {
|
|
470
|
+
useLiveNodeOverrides.getState().clear(id)
|
|
471
|
+
if (useScene.getState().nodes[id]) useScene.getState().markDirty(id)
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const syncParentFramePreview = (position: [number, number, number]) => {
|
|
476
|
+
if (!frameParent) return
|
|
477
|
+
useLiveNodeOverrides.getState().set(node.id, {
|
|
478
|
+
position,
|
|
479
|
+
rotation: rotationRef.current,
|
|
480
|
+
})
|
|
481
|
+
useScene.getState().markDirty(frameParent.id as AnyNodeId)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const clearParentFramePreview = () => {
|
|
485
|
+
if (!frameParent) return
|
|
486
|
+
useLiveNodeOverrides.getState().clear(node.id)
|
|
487
|
+
useScene.getState().markDirty(frameParent.id as AnyNodeId)
|
|
488
|
+
}
|
|
489
|
+
|
|
214
490
|
setCursorPosition(getVisualPosition(originalPosition, originalRotationY))
|
|
215
491
|
|
|
216
492
|
// Re-run the floor-collision check at the live cursor + rotation and push
|
|
217
|
-
// the result to the box colour.
|
|
218
|
-
// the user can drop on top of an existing item on purpose. Only
|
|
219
|
-
// show the box, so this no-ops for every other movable kind.
|
|
493
|
+
// the result to the box colour. Alt (free place) forces a valid (green)
|
|
494
|
+
// override so the user can drop on top of an existing item on purpose. Only
|
|
495
|
+
// shelves show the box, so this no-ops for every other movable kind.
|
|
220
496
|
const recomputeValidity = () => {
|
|
221
497
|
if (!boxDimensions) return
|
|
222
|
-
if (
|
|
498
|
+
if (altRef.current) {
|
|
223
499
|
validRef.current = true
|
|
224
500
|
setValid(true)
|
|
225
501
|
return
|
|
@@ -230,14 +506,42 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
230
506
|
setValid(true)
|
|
231
507
|
return
|
|
232
508
|
}
|
|
233
|
-
const
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
509
|
+
const livePosition = lastCursorRef.current
|
|
510
|
+
const liveRotation = previewRotationY(rotationRef.current)
|
|
511
|
+
const floorPlaced = nodeRegistry.get(node.type)?.capabilities?.floorPlaced
|
|
512
|
+
const effectiveNode = {
|
|
513
|
+
...(node as Record<string, unknown>),
|
|
514
|
+
position: livePosition,
|
|
515
|
+
rotation: Array.isArray((node as { rotation?: unknown }).rotation)
|
|
516
|
+
? [
|
|
517
|
+
((node as { rotation?: unknown }).rotation as [number?, number?, number?])[0] ?? 0,
|
|
518
|
+
rotationRef.current,
|
|
519
|
+
((node as { rotation?: unknown }).rotation as [number?, number?, number?])[2] ?? 0,
|
|
520
|
+
]
|
|
521
|
+
: rotationRef.current,
|
|
522
|
+
} as AnyNode
|
|
523
|
+
const footprints = floorPlaced
|
|
524
|
+
? getFloorPlacedFootprints(floorPlaced, effectiveNode, { nodes: useScene.getState().nodes })
|
|
525
|
+
: []
|
|
526
|
+
const resolvedFootprints: Array<{
|
|
527
|
+
position: [number, number, number]
|
|
528
|
+
dimensions: [number, number, number]
|
|
529
|
+
rotation: [number, number, number]
|
|
530
|
+
}> = footprints.map((footprint) => ({
|
|
531
|
+
position: footprint.position ?? livePosition,
|
|
532
|
+
dimensions: footprint.dimensions,
|
|
533
|
+
rotation: footprint.rotation,
|
|
534
|
+
}))
|
|
535
|
+
const { valid: placeable } =
|
|
536
|
+
resolvedFootprints.length > 0
|
|
537
|
+
? spatialGridManager.canPlaceOnFloorFootprints(levelId, resolvedFootprints, [node.id])
|
|
538
|
+
: spatialGridManager.canPlaceOnFloor(
|
|
539
|
+
levelId,
|
|
540
|
+
getVisualPosition(livePosition),
|
|
541
|
+
boxDimensions,
|
|
542
|
+
[0, liveRotation, 0],
|
|
543
|
+
[node.id],
|
|
544
|
+
)
|
|
241
545
|
validRef.current = placeable
|
|
242
546
|
setValid(placeable)
|
|
243
547
|
}
|
|
@@ -276,6 +580,16 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
276
580
|
useViewer.getState().selection.levelId ?? node.parentId,
|
|
277
581
|
)
|
|
278
582
|
|
|
583
|
+
// Connectivity snapshot (existing port-bearing nodes only — fresh
|
|
584
|
+
// placements aren't connected to anything yet). Records which ducts /
|
|
585
|
+
// fittings are mated to this node's ports so they can follow the drag.
|
|
586
|
+
connectivityRef.current = null
|
|
587
|
+
overriddenIdsRef.current = []
|
|
588
|
+
if (!isNew && nodeRegistry.get(node.type)?.ports) {
|
|
589
|
+
const snapshot = analyzePortConnectivity(node, useScene.getState().nodes)
|
|
590
|
+
if (snapshot.connections.length > 0) connectivityRef.current = snapshot
|
|
591
|
+
}
|
|
592
|
+
|
|
279
593
|
const onGridMove = (event: GridEvent) => {
|
|
280
594
|
const rawX = event.localPosition[0]
|
|
281
595
|
const rawZ = event.localPosition[2]
|
|
@@ -283,9 +597,10 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
283
597
|
|
|
284
598
|
const resolved = resolvePlanarCursorPosition({
|
|
285
599
|
cursor: [rawX, rawZ],
|
|
286
|
-
original: [
|
|
600
|
+
original: [originalPlanPosition[0], originalPlanPosition[2]],
|
|
287
601
|
anchor: dragAnchorRef.current,
|
|
288
|
-
mode: useAbsoluteCursorPlacement ? 'absolute' : 'relative',
|
|
602
|
+
mode: useAbsoluteCursorPlacement || cursorAttached ? 'absolute' : 'relative',
|
|
603
|
+
// Snap follows the mode (raw in Off via snapToGridStep); Alt = force only.
|
|
289
604
|
snap: snapToGridStep,
|
|
290
605
|
})
|
|
291
606
|
dragAnchorRef.current = resolved.anchor
|
|
@@ -293,17 +608,25 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
293
608
|
|
|
294
609
|
// Figma-style alignment snap layered on top of grid snap: when the
|
|
295
610
|
// moving item's edge lines up (on X or Z) with another item's edge,
|
|
296
|
-
//
|
|
297
|
-
//
|
|
298
|
-
//
|
|
299
|
-
|
|
300
|
-
|
|
611
|
+
// publish a guide. The guide connects to the nearest real corner of the
|
|
612
|
+
// candidate (resolver tie-break), so the dot always sits on an actual
|
|
613
|
+
// point. Alignment "lines" are DISPLAYED in every mode except Off
|
|
614
|
+
// (isAlignmentGuideActive); the magnetic pull toward them applies only in
|
|
615
|
+
// 'lines' mode (magnetic). Alt is force-place, not a snap bypass.
|
|
616
|
+
const magnetic = isMagneticSnapActive()
|
|
617
|
+
if (isAlignmentGuideActive() && alignmentCandidates.length > 0) {
|
|
301
618
|
const result = resolveAlignment({
|
|
302
|
-
moving:
|
|
619
|
+
moving: movingDragBoundsAnchors(
|
|
620
|
+
node,
|
|
621
|
+
dragBounds,
|
|
622
|
+
x,
|
|
623
|
+
z,
|
|
624
|
+
previewRotationY(rotationRef.current),
|
|
625
|
+
),
|
|
303
626
|
candidates: alignmentCandidates,
|
|
304
627
|
threshold: ALIGNMENT_THRESHOLD_M,
|
|
305
628
|
})
|
|
306
|
-
if (result.snap) {
|
|
629
|
+
if (result.snap && magnetic) {
|
|
307
630
|
x += result.snap.dx
|
|
308
631
|
z += result.snap.dz
|
|
309
632
|
}
|
|
@@ -312,7 +635,74 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
312
635
|
useAlignmentGuides.getState().clear()
|
|
313
636
|
}
|
|
314
637
|
|
|
315
|
-
|
|
638
|
+
// Kind-owned attachment snap (cabinet → wall): an attach behavior like
|
|
639
|
+
// door/window wall placement, not an alignment guide — active in every
|
|
640
|
+
// snapping mode except Off.
|
|
641
|
+
if ((magnetic || isGridSnapActive()) && groupMoveSnapConfig) {
|
|
642
|
+
const snappedPosition = groupMoveSnapConfig({
|
|
643
|
+
node,
|
|
644
|
+
candidatePosition: canonicalPositionFromPlan(x, originalPosition[1], z),
|
|
645
|
+
movingIds: [node.id as AnyNodeId],
|
|
646
|
+
nodes: useScene.getState().nodes as Record<string, AnyNode>,
|
|
647
|
+
levelId: (useViewer.getState().selection.levelId as AnyNodeId | null) ?? null,
|
|
648
|
+
})
|
|
649
|
+
if (snappedPosition) {
|
|
650
|
+
const snappedPlanPosition = getVisualPosition(snappedPosition)
|
|
651
|
+
x = snappedPlanPosition[0]
|
|
652
|
+
z = snappedPlanPosition[2]
|
|
653
|
+
useAlignmentGuides.getState().clear()
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Magnetic port snap (duct terminals): mate a collar onto a nearby
|
|
658
|
+
// duct run end. Takes precedence over grid / alignment snap; Alt
|
|
659
|
+
// bypasses. Only kinds that opted in via `movable.portSnap`.
|
|
660
|
+
if (magnetic && portSnapConfig) {
|
|
661
|
+
// Build the preview node at the ORIGINAL position but with the LIVE
|
|
662
|
+
// rotation so `def.ports` reflects any mid-drag R/T rotation. Without
|
|
663
|
+
// this the snap solver mates the pre-rotation collar and commit then
|
|
664
|
+
// writes the rotated node offset from the port it visually snapped to.
|
|
665
|
+
const snapNode = buildPreviewNode(originalPosition, rotationRef.current)
|
|
666
|
+
const mated = resolvePortSnap(snapNode, [x, z], portSnapConfig)
|
|
667
|
+
if (mated) {
|
|
668
|
+
x = mated[0]
|
|
669
|
+
z = mated[1]
|
|
670
|
+
useAlignmentGuides.getState().clear()
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
let position = canonicalPositionFromPlan(x, originalPosition[1], z)
|
|
675
|
+
if ((magnetic || isGridSnapActive()) && parentFrame?.magneticSnap && frameParent) {
|
|
676
|
+
const preSnapPosition = position
|
|
677
|
+
const snappedPosition = parentFrame.magneticSnap(
|
|
678
|
+
node,
|
|
679
|
+
frameParent,
|
|
680
|
+
position,
|
|
681
|
+
useScene.getState().nodes,
|
|
682
|
+
)
|
|
683
|
+
if (
|
|
684
|
+
snappedPosition[0] !== position[0] ||
|
|
685
|
+
snappedPosition[1] !== position[1] ||
|
|
686
|
+
snappedPosition[2] !== position[2]
|
|
687
|
+
) {
|
|
688
|
+
position = snappedPosition
|
|
689
|
+
const snappedPlanPosition = getVisualPosition(position)
|
|
690
|
+
x = snappedPlanPosition[0]
|
|
691
|
+
z = snappedPlanPosition[2]
|
|
692
|
+
}
|
|
693
|
+
if (isAlignmentGuideActive() && parentFrame.magneticSnapMatches) {
|
|
694
|
+
const guides = parentFrame
|
|
695
|
+
.magneticSnapMatches(
|
|
696
|
+
node,
|
|
697
|
+
frameParent,
|
|
698
|
+
preSnapPosition,
|
|
699
|
+
snappedPosition,
|
|
700
|
+
useScene.getState().nodes,
|
|
701
|
+
)
|
|
702
|
+
.map(alignmentGuideFromParentFrameMatch)
|
|
703
|
+
if (guides.length > 0) useAlignmentGuides.getState().set(guides)
|
|
704
|
+
}
|
|
705
|
+
}
|
|
316
706
|
const visualPosition = getVisualPosition(position)
|
|
317
707
|
hasMovedRef.current = true
|
|
318
708
|
setCursorPosition(visualPosition)
|
|
@@ -320,7 +710,7 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
320
710
|
recomputeValidity()
|
|
321
711
|
|
|
322
712
|
// Pure imperative: move the mesh via its registered Object3D ref.
|
|
323
|
-
|
|
713
|
+
applyMeshPose(position)
|
|
324
714
|
// Publish to `useLiveTransforms` so the 2D floor plan can mirror
|
|
325
715
|
// the drag in real-time (the floor-plan layer subscribes to this
|
|
326
716
|
// store and overrides the node's rendered position when an entry
|
|
@@ -335,12 +725,20 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
335
725
|
position,
|
|
336
726
|
rotation: rotationRef.current,
|
|
337
727
|
})
|
|
728
|
+
syncParentFramePreview(position)
|
|
338
729
|
markMovedNodeDirty()
|
|
730
|
+
// Carry connected ductwork along (preview only — committed on drop).
|
|
731
|
+
previewConnectivity(position, rotationRef.current)
|
|
339
732
|
|
|
733
|
+
const nextSnapKey = movementSfxStepKey({
|
|
734
|
+
coords: [x, z],
|
|
735
|
+
gridSnapActive: isGridSnapActive(),
|
|
736
|
+
gridStep: useEditor.getState().gridSnapStep,
|
|
737
|
+
})
|
|
340
738
|
const prev = previousSnapRef.current
|
|
341
|
-
if (
|
|
739
|
+
if (prev !== nextSnapKey) {
|
|
342
740
|
sfxEmitter.emit('sfx:grid-snap')
|
|
343
|
-
previousSnapRef.current =
|
|
741
|
+
previousSnapRef.current = nextSnapKey
|
|
344
742
|
}
|
|
345
743
|
}
|
|
346
744
|
|
|
@@ -360,18 +758,26 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
360
758
|
* AND scene updated) — never the original.
|
|
361
759
|
*/
|
|
362
760
|
const commitAtCursor = (event: ClickTriggerEvent) => {
|
|
761
|
+
// One physical click can reach here twice: node clicks (`slab:click`,
|
|
762
|
+
// `item:click`, …) are synthesized on *pointerup* (`use-node-events`),
|
|
763
|
+
// while `grid:click` rides the browser's native *click* event from a
|
|
764
|
+
// canvas DOM listener (`use-grid-events`) that deliberately ignores
|
|
765
|
+
// stopPropagation — and this effect stays subscribed until React
|
|
766
|
+
// re-renders after `exitMoveMode`. Without this guard the second pass
|
|
767
|
+
// finds the fresh draft already deleted and takes the orphan re-create
|
|
768
|
+
// path below, minting a hidden ghost copy and replaying the SFX.
|
|
769
|
+
if (committed) return
|
|
363
770
|
// Ignore a commit that fires before the cursor has moved into place —
|
|
364
771
|
// it's the stray trailing click of whatever armed this move, not a
|
|
365
772
|
// deliberate drop. Prevents preset re-arm from double-placing.
|
|
366
773
|
if (!hasMovedRef.current) return
|
|
367
774
|
// Refuse a drop on an invalid (red) footprint, matching the GLB item
|
|
368
|
-
// tool — unless
|
|
369
|
-
// validity box (`validRef` stays true), so they're never blocked.
|
|
370
|
-
if (!validRef.current && !
|
|
775
|
+
// tool — unless Alt (free place) is held to force placement. Other kinds
|
|
776
|
+
// carry no validity box (`validRef` stays true), so they're never blocked.
|
|
777
|
+
if (!validRef.current && !altRef.current) return
|
|
371
778
|
const position: [number, number, number] = [...lastCursorRef.current]
|
|
372
779
|
|
|
373
780
|
const rotation = toCommitRotation(rotationRef.current)
|
|
374
|
-
const visualPosition = getVisualPosition(position)
|
|
375
781
|
let committedId = node.id as AnyNodeId
|
|
376
782
|
|
|
377
783
|
if (useScene.getState().nodes[node.id]) {
|
|
@@ -393,8 +799,28 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
393
799
|
committedId = finalId
|
|
394
800
|
}
|
|
395
801
|
} else {
|
|
802
|
+
// Fold the connected-ductwork follow-updates into the SAME
|
|
803
|
+
// batch as the moved node so the whole thing is one undo step.
|
|
804
|
+
const connectivityUpdates = connectivityRef.current
|
|
805
|
+
? resolveConnectivityUpdates(
|
|
806
|
+
connectivityRef.current,
|
|
807
|
+
buildPreviewNode(position, rotationRef.current),
|
|
808
|
+
).filter((u) => useScene.getState().nodes[u.id])
|
|
809
|
+
: []
|
|
396
810
|
useScene.temporal.getState().resume()
|
|
397
|
-
useScene
|
|
811
|
+
useScene
|
|
812
|
+
.getState()
|
|
813
|
+
.updateNodes([{ id: node.id as AnyNodeId, data }, ...connectivityUpdates])
|
|
814
|
+
// Kind-owned derived-state maintenance after a parent-frame move
|
|
815
|
+
// (cabinet run re-flow + linked corner-run re-anchor). Runs in the
|
|
816
|
+
// resumed window so its writes are undoable alongside the move.
|
|
817
|
+
if (parentFrame?.onCommit && frameParent) {
|
|
818
|
+
const liveNode = useScene.getState().nodes[node.id as AnyNodeId]
|
|
819
|
+
const liveParent = useScene.getState().nodes[frameParent.id as AnyNodeId]
|
|
820
|
+
if (liveNode && liveParent) {
|
|
821
|
+
parentFrame.onCommit(liveNode, liveParent, createSceneApi(useScene))
|
|
822
|
+
}
|
|
823
|
+
}
|
|
398
824
|
useScene.temporal.getState().pause()
|
|
399
825
|
committed = true
|
|
400
826
|
}
|
|
@@ -420,11 +846,11 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
420
846
|
// canonical position, then restamp the lifted presentation Y for the
|
|
421
847
|
// current frame.
|
|
422
848
|
useLiveTransforms.getState().clear(node.id)
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
849
|
+
// Connected ductwork is now committed to the store — drop its live
|
|
850
|
+
// overrides so the renderers read the canonical path/position.
|
|
851
|
+
clearConnectivityOverrides()
|
|
852
|
+
clearParentFramePreview()
|
|
853
|
+
applyMeshPose(position)
|
|
428
854
|
|
|
429
855
|
useAlignmentGuides.getState().clear()
|
|
430
856
|
if (isNew && committed) {
|
|
@@ -448,14 +874,14 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
448
874
|
if (typeof direct === 'function') direct.call(event)
|
|
449
875
|
}
|
|
450
876
|
|
|
451
|
-
// R / T rotate the dragged node about Y in
|
|
877
|
+
// R / T rotate the dragged node about Y in 45° steps — matching the GLB
|
|
452
878
|
// item placement keys (and the "Rotate" hints the move HUD shows). Applied
|
|
453
879
|
// imperatively + mirrored to the live transform; committed on drop.
|
|
454
880
|
const onKeyDown = (e: KeyboardEvent) => {
|
|
455
|
-
// Hold
|
|
456
|
-
// the GLB item tool. Recolour the box to green while held.
|
|
457
|
-
if (e.key === '
|
|
458
|
-
|
|
881
|
+
// Hold Alt (free place) to force placement on an invalid (red) footprint,
|
|
882
|
+
// matching the GLB item tool. Recolour the box to green while held.
|
|
883
|
+
if (e.key === 'Alt') {
|
|
884
|
+
altRef.current = true
|
|
459
885
|
recomputeValidity()
|
|
460
886
|
return
|
|
461
887
|
}
|
|
@@ -467,26 +893,25 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
467
893
|
e.preventDefault()
|
|
468
894
|
sfxEmitter.emit('sfx:item-rotate')
|
|
469
895
|
rotationRef.current += delta
|
|
470
|
-
setCursorRotationY(rotationRef.current)
|
|
896
|
+
setCursorRotationY(previewRotationY(rotationRef.current))
|
|
471
897
|
const position = lastCursorRef.current
|
|
472
898
|
const visualPosition = getVisualPosition(position)
|
|
473
899
|
setCursorPosition(visualPosition)
|
|
474
|
-
|
|
475
|
-
if (m) {
|
|
476
|
-
m.position.set(...visualPosition)
|
|
477
|
-
m.rotation.y = rotationRef.current
|
|
478
|
-
}
|
|
900
|
+
applyMeshPose(position)
|
|
479
901
|
useLiveTransforms.getState().set(node.id, {
|
|
480
902
|
position,
|
|
481
903
|
rotation: rotationRef.current,
|
|
482
904
|
})
|
|
905
|
+
syncParentFramePreview(position)
|
|
483
906
|
markMovedNodeDirty()
|
|
907
|
+
// Rotating the fitting swings its collars — connected ducts follow.
|
|
908
|
+
previewConnectivity(position, rotationRef.current)
|
|
484
909
|
// Rotation changes the footprint's collision span — re-check validity.
|
|
485
910
|
recomputeValidity()
|
|
486
911
|
}
|
|
487
912
|
const onKeyUp = (e: KeyboardEvent) => {
|
|
488
|
-
if (e.key === '
|
|
489
|
-
|
|
913
|
+
if (e.key === 'Alt') {
|
|
914
|
+
altRef.current = false
|
|
490
915
|
recomputeValidity()
|
|
491
916
|
}
|
|
492
917
|
}
|
|
@@ -496,6 +921,21 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
496
921
|
emitter.on('grid:move', onGridMove)
|
|
497
922
|
emitter.on('grid:click', commitAtCursor)
|
|
498
923
|
|
|
924
|
+
const onPlacementDragPointerUp = (event: PointerEvent) => {
|
|
925
|
+
if (!useEditor.getState().placementDragMode) return
|
|
926
|
+
if (event.button !== 0) return
|
|
927
|
+
swallowNextClick()
|
|
928
|
+
if (!hasMovedRef.current) {
|
|
929
|
+
exitMoveMode()
|
|
930
|
+
return
|
|
931
|
+
}
|
|
932
|
+
commitAtCursor({
|
|
933
|
+
nativeEvent: event,
|
|
934
|
+
stopPropagation: () => event.stopPropagation(),
|
|
935
|
+
} as unknown as ClickTriggerEvent)
|
|
936
|
+
}
|
|
937
|
+
window.addEventListener('pointerup', onPlacementDragPointerUp)
|
|
938
|
+
|
|
499
939
|
// Listen on every common kind's click event too. mitt's typing keeps
|
|
500
940
|
// `${kind}:click` as a fixed union so the cast is safe at runtime —
|
|
501
941
|
// we're just routing them through the shared commit path.
|
|
@@ -508,14 +948,12 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
508
948
|
|
|
509
949
|
const onCancel = () => {
|
|
510
950
|
useLiveTransforms.getState().clear(node.id)
|
|
951
|
+
clearConnectivityOverrides()
|
|
952
|
+
clearParentFramePreview()
|
|
511
953
|
if (isNew) {
|
|
512
954
|
useScene.getState().deleteNode(node.id as AnyNodeId)
|
|
513
955
|
} else {
|
|
514
|
-
|
|
515
|
-
if (m) {
|
|
516
|
-
m.position.set(...getVisualPosition(originalPosition, originalRotationY))
|
|
517
|
-
m.rotation.y = originalRotationY
|
|
518
|
-
}
|
|
956
|
+
applyMeshPose(originalPosition, originalRotationY)
|
|
519
957
|
markMovedNodeDirty()
|
|
520
958
|
}
|
|
521
959
|
useAlignmentGuides.getState().clear()
|
|
@@ -530,6 +968,7 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
530
968
|
window.removeEventListener('keyup', onKeyUp)
|
|
531
969
|
emitter.off('grid:move', onGridMove)
|
|
532
970
|
emitter.off('grid:click', commitAtCursor)
|
|
971
|
+
window.removeEventListener('pointerup', onPlacementDragPointerUp)
|
|
533
972
|
for (const kind of CLICK_TRIGGER_KINDS) {
|
|
534
973
|
const key = `${kind}:click` as ClickKey
|
|
535
974
|
emitter.off(key, commitAtCursor as never)
|
|
@@ -544,39 +983,55 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
544
983
|
const finalisedBy2D = useEditor.getState().movingNodeOrigin === '2d'
|
|
545
984
|
if (!(committed || isNew || finalisedBy2D)) {
|
|
546
985
|
useLiveTransforms.getState().clear(node.id)
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
986
|
+
clearConnectivityOverrides()
|
|
987
|
+
clearParentFramePreview()
|
|
988
|
+
applyMeshPose(originalPosition, originalRotationY)
|
|
550
989
|
markMovedNodeDirty()
|
|
551
990
|
}
|
|
552
991
|
useScene.temporal.getState().resume()
|
|
553
992
|
}
|
|
554
993
|
}, [
|
|
555
994
|
boxDimensions,
|
|
995
|
+
dragBounds,
|
|
996
|
+
canonicalPositionFromPlan,
|
|
997
|
+
parentFrame,
|
|
998
|
+
frameParent,
|
|
999
|
+
cursorAttached,
|
|
1000
|
+
portSnapConfig,
|
|
1001
|
+
groupMoveSnapConfig,
|
|
556
1002
|
exitMoveMode,
|
|
557
1003
|
isFreshPlacement,
|
|
558
1004
|
node,
|
|
559
1005
|
originalPosition,
|
|
1006
|
+
originalPlanPosition,
|
|
560
1007
|
originalRotationY,
|
|
1008
|
+
previewRotationY,
|
|
561
1009
|
revealFreshPlacement,
|
|
562
1010
|
useAbsoluteCursorPlacement,
|
|
1011
|
+
visualPositionFor,
|
|
563
1012
|
])
|
|
564
1013
|
|
|
565
|
-
//
|
|
566
|
-
//
|
|
567
|
-
//
|
|
568
|
-
//
|
|
569
|
-
//
|
|
570
|
-
const
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
1014
|
+
// Forward-facing triangle for the footprint-box branch (item / shelf / column
|
|
1015
|
+
// — anything that renders `<PlacementBox>`). Published to the editor-side
|
|
1016
|
+
// overlay; the `<DragBoundingBox>` branch (e.g. stair, which has no centred
|
|
1017
|
+
// footprint) publishes its own. The box is centred on `cursorPosition`, so
|
|
1018
|
+
// the footprint centre is the origin. Clears on unmount.
|
|
1019
|
+
const facing = resolveFacingIndicator(node.type)
|
|
1020
|
+
useEffect(() => {
|
|
1021
|
+
if (!previewVisible || !facing || !boxDimensions) return
|
|
1022
|
+
useFacingPose.getState().set({
|
|
1023
|
+
position: cursorPosition,
|
|
1024
|
+
rotationY: cursorRotationY,
|
|
1025
|
+
depth: boxDimensions[2],
|
|
1026
|
+
center: dragBounds?.center ? [dragBounds.center[0], dragBounds.center[2]] : undefined,
|
|
1027
|
+
reversed: facing.reversed,
|
|
1028
|
+
})
|
|
1029
|
+
}, [previewVisible, facing, boxDimensions, dragBounds, cursorPosition, cursorRotationY])
|
|
1030
|
+
useEffect(() => () => useFacingPose.getState().clear(), [])
|
|
576
1031
|
|
|
577
1032
|
if (!previewVisible) return null
|
|
578
1033
|
|
|
579
|
-
if (boxDimensions) {
|
|
1034
|
+
if (boxDimensions && !dragBounds?.center) {
|
|
580
1035
|
return (
|
|
581
1036
|
<PlacementBox
|
|
582
1037
|
dimensions={boxDimensions}
|
|
@@ -587,11 +1042,18 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
587
1042
|
)
|
|
588
1043
|
}
|
|
589
1044
|
|
|
1045
|
+
const dragCenterPosition =
|
|
1046
|
+
dragBounds?.center && dragBounds.size
|
|
1047
|
+
? offsetPlanPositionByLocalCenter(cursorPosition, dragBounds.center, cursorRotationY)
|
|
1048
|
+
: cursorPosition
|
|
1049
|
+
|
|
590
1050
|
return (
|
|
591
1051
|
<>
|
|
592
|
-
<CursorSphere color="#a78bfa" height={2.5} position={
|
|
1052
|
+
<CursorSphere color="#a78bfa" height={2.5} position={dragCenterPosition} />
|
|
593
1053
|
<DragBoundingBox
|
|
1054
|
+
center={dragBounds?.center}
|
|
594
1055
|
centerY={dragBounds?.centerY}
|
|
1056
|
+
color={boxDimensions ? (valid ? VALID_COLOR : INVALID_COLOR) : undefined}
|
|
595
1057
|
nodeId={node.id}
|
|
596
1058
|
position={cursorPosition}
|
|
597
1059
|
rotationY={cursorRotationY}
|