@pascal-app/editor 1.0.0-beta.5 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/bunfig.toml +4 -0
- package/package.json +13 -7
- package/scripts/generate-print-golden-house.ts +89 -0
- package/src/components/editor/bake-exporter.tsx +1 -0
- package/src/components/editor/camera-dragging-lifecycle.test.ts +41 -0
- package/src/components/editor/camera-dragging-lifecycle.ts +10 -2
- package/src/components/editor/capture-camera-rig.tsx +59 -0
- package/src/components/editor/custom-camera-controls.tsx +20 -11
- package/src/components/editor/editor-layout-mobile.tsx +6 -6
- package/src/components/editor/editor-layout-v2.tsx +9 -2
- package/src/components/editor/export-manager.tsx +175 -33
- package/src/components/editor/first-person/build-collider-world.test.ts +30 -0
- package/src/components/editor/first-person-controls.tsx +180 -12
- package/src/components/editor/floating-action-menu.tsx +42 -51
- package/src/components/editor/floorplan-panel.tsx +76 -12
- package/src/components/editor/grid.tsx +12 -7
- package/src/components/editor/group-actions.ts +34 -50
- package/src/components/editor/group-move-3d.ts +51 -21
- package/src/components/editor/group-transform-shared.test.ts +78 -0
- package/src/components/editor/group-transform-shared.ts +74 -7
- package/src/components/editor/handles/handle-arrow-raycast.test.ts +86 -0
- package/src/components/editor/handles/handle-arrow.tsx +109 -48
- package/src/components/editor/handles/linear-resize-drag.ts +67 -0
- package/src/components/editor/handles/resize-snap.test.ts +46 -0
- package/src/components/editor/handles/resize-snap.ts +10 -1
- package/src/components/editor/handles/use-handle-drag.ts +18 -1
- package/src/components/editor/index.tsx +140 -21
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +47 -58
- package/src/components/editor/selection-manager.tsx +104 -42
- package/src/components/editor/snapshot-capture-overlay.tsx +298 -39
- package/src/components/editor/snapshot-capture.test.ts +422 -0
- package/src/components/editor/snapshot-capture.ts +145 -0
- package/src/components/editor/thumbnail-generator.tsx +284 -237
- package/src/components/editor/use-floorplan-background-placement.ts +53 -5
- package/src/components/editor/wall-move-side-handles.tsx +299 -7
- package/src/components/editor-2d/floorplan-group-move.tsx +67 -33
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +3 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +129 -38
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +14 -1
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +16 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +2 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +52 -3
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +196 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +180 -32
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +402 -0
- package/src/components/systems/ceiling/ceiling-bracket-batch.ts +371 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +347 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +281 -305
- package/src/components/systems/roof/roof-edit-system.test.tsx +120 -0
- package/src/components/systems/roof/roof-edit-system.tsx +10 -19
- package/src/components/systems/selection-affordance-manager.tsx +59 -9
- package/src/components/systems/selection-affordance-services.ts +19 -0
- package/src/components/systems/stair/stair-edit-system.tsx +5 -2
- package/src/components/tools/fence/fence-drafting.test.ts +100 -0
- package/src/components/tools/fence/fence-drafting.ts +6 -25
- package/src/components/tools/item/draft-creation.test.ts +12 -0
- package/src/components/tools/item/draft-creation.ts +10 -0
- package/src/components/tools/item/face-host-commit.test.ts +65 -0
- package/src/components/tools/item/face-host-commit.ts +56 -0
- package/src/components/tools/item/face-host-preview.test.ts +77 -0
- package/src/components/tools/item/face-host-preview.ts +82 -0
- package/src/components/tools/item/move-tool.tsx +4 -3
- package/src/components/tools/item/placement-math.test.ts +11 -1
- package/src/components/tools/item/placement-math.ts +0 -20
- package/src/components/tools/item/placement-strategies.test.ts +331 -4
- package/src/components/tools/item/placement-strategies.ts +135 -2
- package/src/components/tools/item/placement-surface.test.ts +61 -0
- package/src/components/tools/item/placement-surface.ts +24 -0
- package/src/components/tools/item/placement-types.ts +6 -0
- package/src/components/tools/item/test-face-host.ts +121 -0
- package/src/components/tools/item/use-draft-node.test.tsx +336 -0
- package/src/components/tools/item/use-draft-node.ts +26 -4
- package/src/components/tools/item/use-placement-coordinator.tsx +428 -153
- package/src/components/tools/registered-tool-install-lifecycle.test.tsx +256 -0
- package/src/components/tools/registry/move-registry-node-tool.test.ts +10 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +306 -110
- package/src/components/tools/registry-tool-context.tsx +30 -0
- package/src/components/tools/select/box-select-tool.tsx +3 -1
- package/src/components/tools/select/marquee-footprint.test.ts +53 -0
- package/src/components/tools/select/marquee-footprint.ts +29 -0
- package/src/components/tools/shared/placement-box.tsx +57 -7
- package/src/components/tools/shared/placement-dimension-guides.tsx +121 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +319 -17
- package/src/components/tools/shared/pointer-support-cap.ts +45 -8
- package/src/components/tools/site/terrain-sculpt-grid.tsx +9 -5
- package/src/components/tools/site/terrain-sculpt-tool.tsx +1 -1
- package/src/components/tools/stair/stair-tool.tsx +127 -74
- package/src/components/tools/tool-manager.tsx +25 -8
- package/src/components/tools/wall/wall-drafting.test.ts +320 -2
- package/src/components/tools/wall/wall-drafting.ts +23 -114
- package/src/components/ui/action-menu/control-modes.tsx +9 -6
- package/src/components/ui/action-menu/index.tsx +11 -1
- package/src/components/ui/command-palette/editor-commands.tsx +9 -12
- package/src/components/ui/command-palette/index.tsx +7 -2
- package/src/components/ui/controls/material-paint-panel.tsx +6 -4
- package/src/components/ui/controls/material-picker.tsx +4 -4
- package/src/components/ui/controls/scene-material-list.tsx +6 -5
- package/src/components/ui/controls/segmented-control.tsx +8 -1
- package/src/components/ui/controls/slider-control.tsx +19 -3
- package/src/components/ui/controls/toggle-control.tsx +18 -5
- package/src/components/ui/controls/tool-options-panel.tsx +126 -0
- package/src/components/ui/floating-level-selector.tsx +22 -5
- package/src/components/ui/helpers/contextual-helper-panel.tsx +9 -0
- package/src/components/ui/helpers/helper-manager.tsx +47 -11
- package/src/components/ui/helpers/item-helper.tsx +1 -1
- package/src/components/ui/helpers/registered-tool-helper.tsx +21 -0
- package/src/components/ui/panels/homogeneous-selection.test.ts +80 -0
- package/src/components/ui/panels/homogeneous-selection.ts +47 -0
- package/src/components/ui/panels/mobile-selection-bar.tsx +11 -5
- package/src/components/ui/panels/multi-field-value.test.ts +209 -0
- package/src/components/ui/panels/multi-field-value.ts +163 -0
- package/src/components/ui/panels/multi-height-mode.tsx +185 -0
- package/src/components/ui/panels/multi-parametric-inspector.tsx +263 -0
- package/src/components/ui/panels/multi-selection-panel.tsx +48 -37
- package/src/components/ui/panels/node-display.ts +4 -0
- package/src/components/ui/panels/panel-manager.tsx +77 -3
- package/src/components/ui/panels/parametric-field-control.tsx +173 -0
- package/src/components/ui/panels/parametric-field-utils.ts +18 -0
- package/src/components/ui/panels/parametric-inspector.tsx +29 -162
- package/src/components/ui/panels/reference-panel.tsx +31 -1
- package/src/components/ui/primitives/shortcut-token.tsx +23 -1
- package/src/components/ui/scene-loader.tsx +35 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +390 -36
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +20 -23
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts +140 -0
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +109 -0
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +19 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +116 -46
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +28 -11
- package/src/components/ui/sidebar/panels/site-panel/tree-node.test.ts +8 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +14 -6
- package/src/components/ui/sidebar/tab-bar.tsx +2 -0
- package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
- package/src/components/viewer/viewer-scene-header.tsx +2 -1
- package/src/components/viewer/viewer-stage.tsx +4 -1
- package/src/components/viewer-overlay.tsx +1 -22
- package/src/components/viewer-zone-system.tsx +6 -2
- package/src/components/walkthrough-hud.tsx +19 -10
- package/src/hooks/use-auto-save.test.ts +73 -1
- package/src/hooks/use-auto-save.ts +76 -5
- package/src/hooks/use-ceiling-events.test.ts +122 -0
- package/src/hooks/use-drag-action.ts +17 -3
- package/src/hooks/use-grid-events.test.ts +240 -0
- package/src/hooks/use-grid-events.ts +190 -11
- package/src/hooks/use-keyboard.test.ts +244 -0
- package/src/hooks/use-keyboard.ts +129 -62
- package/src/hooks/use-registered-tool-enabled.ts +20 -0
- package/src/hooks/use-save-shortcut.ts +33 -0
- package/src/index.tsx +82 -4
- package/src/lib/active-placement-surface.test.ts +16 -0
- package/src/lib/active-placement-surface.ts +14 -0
- package/src/lib/ceiling-surface-raycast.test.ts +64 -0
- package/src/lib/ceiling-surface-raycast.ts +12 -0
- package/src/lib/contextual-help-extension.ts +17 -0
- package/src/lib/continuation.test.ts +11 -0
- package/src/lib/continuation.ts +8 -1
- package/src/lib/direct-manipulation.test.ts +132 -0
- package/src/lib/direct-manipulation.ts +38 -1
- package/src/lib/editor-api.ts +6 -3
- package/src/lib/elevation-guides.test.ts +28 -1
- package/src/lib/elevation-guides.ts +22 -7
- package/src/lib/floating-menu-scale.test.ts +29 -0
- package/src/lib/floating-menu-scale.ts +14 -0
- package/src/lib/floorplan/apply-alignment.test.ts +47 -2
- package/src/lib/floorplan/apply-alignment.ts +10 -1
- package/src/lib/floorplan/floorplan-export.test.ts +397 -1
- package/src/lib/floorplan/floorplan-export.tsx +111 -16
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +26 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +32 -24
- package/src/lib/floorplan/floorplan-readonly.ts +1 -0
- package/src/lib/floorplan-grid-event-point.test.ts +29 -0
- package/src/lib/floorplan-grid-event-point.ts +13 -0
- package/src/lib/glb-export.test.ts +1169 -21
- package/src/lib/glb-export.ts +629 -101
- package/src/lib/grid-event-presentation.ts +16 -0
- package/src/lib/history.test.ts +528 -147
- package/src/lib/history.ts +81 -7
- package/src/lib/host-tree-children.test.ts +27 -0
- package/src/lib/host-tree-children.ts +79 -0
- package/src/lib/interaction/hot-set.test.ts +10 -1
- package/src/lib/interaction/hot-set.ts +3 -1
- package/src/lib/interaction/overlay-policy.test.ts +33 -1
- package/src/lib/interaction/overlay-policy.ts +16 -0
- package/src/lib/interaction/registered-drafting.ts +36 -0
- package/src/lib/interaction/scope.ts +31 -0
- package/src/lib/level-print-export.test.ts +484 -0
- package/src/lib/level-print-export.ts +526 -0
- package/src/lib/local-project-presentation-persistence.test.ts +302 -0
- package/src/lib/local-project-presentation-persistence.ts +335 -0
- package/src/lib/material-paint.ts +2 -0
- package/src/lib/model-export.ts +29 -0
- package/src/lib/paint-preview-owner.ts +71 -0
- package/src/lib/planar-cursor-placement.test.ts +178 -1
- package/src/lib/planar-cursor-placement.ts +61 -5
- package/src/lib/plugin-panels.test.ts +92 -1
- package/src/lib/plugin-panels.ts +48 -0
- package/src/lib/portable-export.test.ts +366 -0
- package/src/lib/portable-export.ts +969 -0
- package/src/lib/print-3mf.test.ts +86 -0
- package/src/lib/print-3mf.ts +161 -0
- package/src/lib/print-content-scope.test.ts +107 -0
- package/src/lib/print-content-scope.ts +90 -0
- package/src/lib/print-export.test.ts +294 -0
- package/src/lib/print-export.ts +735 -0
- package/src/lib/print-feature-thickness.test.ts +147 -0
- package/src/lib/print-feature-thickness.ts +180 -0
- package/src/lib/print-golden-house.test-fixture.ts +352 -0
- package/src/lib/print-golden-house.test.ts +249 -0
- package/src/lib/print-roof-solids.test.ts +133 -0
- package/src/lib/print-roof-solids.ts +630 -0
- package/src/lib/print-shell-compiler-baseline.test.ts +419 -0
- package/src/lib/print-shell-compiler-baseline.ts +225 -0
- package/src/lib/print-shell-compiler-manifold-core.ts +330 -0
- package/src/lib/print-shell-compiler-manifold-worker.ts +187 -0
- package/src/lib/print-shell-compiler-manifold.worker.ts +20 -0
- package/src/lib/print-shell-compiler-mesh-data.ts +32 -0
- package/src/lib/print-shell-compiler-protocol.ts +30 -0
- package/src/lib/print-shell-compiler.ts +273 -0
- package/src/lib/print-wall-solids.test.ts +194 -0
- package/src/lib/print-wall-solids.ts +328 -0
- package/src/lib/rigid-plan-svg-transform.test.ts +35 -0
- package/src/lib/rigid-plan-svg-transform.ts +24 -0
- package/src/lib/scene.ts +22 -8
- package/src/lib/selection-routing.test.ts +105 -1
- package/src/lib/selection-routing.ts +19 -0
- package/src/lib/sfx-bus.ts +4 -0
- package/src/lib/sfx-player.ts +8 -0
- package/src/lib/snapping-mode.test.ts +32 -4
- package/src/lib/snapping-mode.ts +11 -7
- package/src/lib/usdz-export.ts +38 -0
- package/src/lib/walkthrough-pointer-lock.ts +47 -0
- package/src/store/live-draft-preview-stores.test.ts +3 -0
- package/src/store/terrain-sculpt-mode.test.ts +3 -6
- package/src/store/tool-mode.test.ts +87 -0
- package/src/store/use-camera-hint-focus.ts +48 -0
- package/src/store/use-editor.tsx +299 -188
- package/src/store/use-floorplan-draft-preview.ts +9 -0
- package/src/store/use-interaction-scope.test.ts +23 -1
- package/src/store/use-interaction-scope.ts +63 -2
- package/src/store/use-placement-preview.ts +56 -3
- package/src/components/tools/roof/roof-tool.tsx +0 -656
- package/src/components/ui/helpers/roof-helper.tsx +0 -14
|
@@ -10,11 +10,13 @@ import {
|
|
|
10
10
|
bboxCornerAnchors,
|
|
11
11
|
collectAlignmentAnchors,
|
|
12
12
|
createSceneApi,
|
|
13
|
-
type EventSuffix,
|
|
14
13
|
emitter,
|
|
14
|
+
findLevelAncestorId,
|
|
15
15
|
footprintAABBFrom,
|
|
16
16
|
type GridEvent,
|
|
17
|
+
type GroupMoveSnapResult,
|
|
17
18
|
getFloorPlacedFootprints,
|
|
19
|
+
type MovableConfig,
|
|
18
20
|
movingFootprintAnchors,
|
|
19
21
|
type NodeEvent,
|
|
20
22
|
nodeRegistry,
|
|
@@ -23,6 +25,7 @@ import {
|
|
|
23
25
|
resolveAlignment,
|
|
24
26
|
resolveConnectivityUpdates,
|
|
25
27
|
resolveFacingIndicator,
|
|
28
|
+
resolveFrozenFloorPlacementPatch,
|
|
26
29
|
resolveSupportSlabPatch,
|
|
27
30
|
sceneRegistry,
|
|
28
31
|
spatialGridManager,
|
|
@@ -31,12 +34,17 @@ import {
|
|
|
31
34
|
useScene,
|
|
32
35
|
} from '@pascal-app/core'
|
|
33
36
|
import { useViewer } from '@pascal-app/viewer'
|
|
34
|
-
import { useThree } from '@react-three/fiber'
|
|
37
|
+
import { useFrame, useThree } from '@react-three/fiber'
|
|
35
38
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
39
|
+
import type { Group } from 'three'
|
|
36
40
|
import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
|
|
37
41
|
import { commitFreshPlacementSubtree } from '../../../lib/fresh-planar-placement'
|
|
38
42
|
import { stripPlacementMetadataFlags } from '../../../lib/placement-metadata'
|
|
39
|
-
import {
|
|
43
|
+
import {
|
|
44
|
+
offsetPlanPositionByLocalCenter,
|
|
45
|
+
resolvePrioritizedPlanarCursorPosition,
|
|
46
|
+
} from '../../../lib/planar-cursor-placement'
|
|
47
|
+
import { resolveAttachmentPreviewRotation } from '../../../lib/rigid-plan-svg-transform'
|
|
40
48
|
import { movementSfxStepKey } from '../../../lib/sfx/movement-tick'
|
|
41
49
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
42
50
|
import { resolveSnapFlags } from '../../../lib/snapping-mode'
|
|
@@ -56,7 +64,10 @@ import { DragBoundingBox } from '../shared/drag-bounding-box'
|
|
|
56
64
|
import { getFloorStackPreviewPosition } from '../shared/floor-stack-preview'
|
|
57
65
|
import { useFreshPlacementVisibility } from '../shared/fresh-placement-visibility'
|
|
58
66
|
import { PlacementBox } from '../shared/placement-box'
|
|
59
|
-
import {
|
|
67
|
+
import {
|
|
68
|
+
type PointerSupportSurface,
|
|
69
|
+
resolvePointerSupportSurface,
|
|
70
|
+
} from '../shared/pointer-support-cap'
|
|
60
71
|
|
|
61
72
|
/** Snap a world-plan coordinate to the editor's active grid step (0.5 / 0.25
|
|
62
73
|
* / 0.1 / 0.05), read live so changing the step mid-drag takes effect. */
|
|
@@ -69,6 +80,15 @@ const snapToGridStep = (value: number) => {
|
|
|
69
80
|
/** 45° steps, matching the GLB item placement rotation. */
|
|
70
81
|
const ROTATION_STEP = Math.PI / 4
|
|
71
82
|
|
|
83
|
+
export function resolveMoveRotationStep(
|
|
84
|
+
freeRotation: number,
|
|
85
|
+
delta: number,
|
|
86
|
+
attachmentRotation: number | null,
|
|
87
|
+
): number | null {
|
|
88
|
+
if (attachmentRotation !== null) return null
|
|
89
|
+
return freeRotation + delta
|
|
90
|
+
}
|
|
91
|
+
|
|
72
92
|
/** Default magnetic radius (meters, XZ) for `movable.portSnap`. */
|
|
73
93
|
const PORT_SNAP_RADIUS_M = 0.5
|
|
74
94
|
const VALID_COLOR = 0x22_c5_5e
|
|
@@ -80,20 +100,6 @@ type DragBoundsOverride = {
|
|
|
80
100
|
centerY?: number
|
|
81
101
|
}
|
|
82
102
|
|
|
83
|
-
function offsetPlanPositionByLocalCenter(
|
|
84
|
-
position: [number, number, number],
|
|
85
|
-
center: [number, number, number],
|
|
86
|
-
rotationY: number,
|
|
87
|
-
): [number, number, number] {
|
|
88
|
-
const cos = Math.cos(rotationY)
|
|
89
|
-
const sin = Math.sin(rotationY)
|
|
90
|
-
return [
|
|
91
|
-
position[0] + center[0] * cos + center[2] * sin,
|
|
92
|
-
position[1] + center[1],
|
|
93
|
-
position[2] - center[0] * sin + center[2] * cos,
|
|
94
|
-
]
|
|
95
|
-
}
|
|
96
|
-
|
|
97
103
|
/**
|
|
98
104
|
* Alignment anchors for the moving node. When the kind declares
|
|
99
105
|
* `capabilities.dragBounds` with an off-origin `center` (a composite cabinet
|
|
@@ -203,12 +209,10 @@ const ALIGNMENT_THRESHOLD_M = 0.08
|
|
|
203
209
|
* Cancel imperatively snaps the mesh back to its original position and
|
|
204
210
|
* resumes history without ever having touched the store mid-drag.
|
|
205
211
|
*
|
|
206
|
-
* **Commit triggers**: the tool listens for `grid:click`
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
* `
|
|
210
|
-
* geometry the ray happens to land on) fires the corresponding node
|
|
211
|
-
* click event. Without the node-click listeners, clicking on the
|
|
212
|
+
* **Commit triggers**: the tool listens for `grid:click` and the generic
|
|
213
|
+
* `node:click` event. A click on the grid plane fires `grid:click`; a click
|
|
214
|
+
* on the moved node itself (or any other 3D geometry the ray happens to land
|
|
215
|
+
* on) fires `node:click`. Without the node-click listener, clicking on the
|
|
212
216
|
* cursor's own mesh during a move would silently drop the commit —
|
|
213
217
|
* the user perceives "click did nothing" because the click hit the
|
|
214
218
|
* vertical face of e.g. a shelf instead of the grid plane below it.
|
|
@@ -219,21 +223,21 @@ const ALIGNMENT_THRESHOLD_M = 0.08
|
|
|
219
223
|
*/
|
|
220
224
|
type ClickTriggerEvent = GridEvent | NodeEvent<AnyNode>
|
|
221
225
|
|
|
222
|
-
const CLICK_TRIGGER_KINDS = [
|
|
223
|
-
'shelf',
|
|
224
|
-
'item',
|
|
225
|
-
'slab',
|
|
226
|
-
'ceiling',
|
|
227
|
-
'wall',
|
|
228
|
-
'fence',
|
|
229
|
-
'column',
|
|
230
|
-
'roof',
|
|
231
|
-
'roof-segment',
|
|
232
|
-
'stair',
|
|
233
|
-
'stair-segment',
|
|
234
|
-
] as const
|
|
235
|
-
|
|
236
226
|
export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
227
|
+
const previewGroupRef = useRef<Group>(null)
|
|
228
|
+
useFrame(() => {
|
|
229
|
+
if (!previewGroupRef.current) return
|
|
230
|
+
const nodes = useScene.getState().nodes
|
|
231
|
+
const parentId = nodes[node.id]?.parentId ?? node.parentId
|
|
232
|
+
const parent = parentId ? nodes[parentId as AnyNodeId] : undefined
|
|
233
|
+
// Building-parented kinds already preview in the tool group's frame.
|
|
234
|
+
const levelId =
|
|
235
|
+
(parentId ? findLevelAncestorId(parentId as AnyNodeId, nodes) : null) ??
|
|
236
|
+
(parent?.type === 'building' ? null : useViewer.getState().selection.levelId)
|
|
237
|
+
previewGroupRef.current.position.y = levelId
|
|
238
|
+
? (sceneRegistry.nodes.get(levelId)?.position.y ?? 0)
|
|
239
|
+
: 0
|
|
240
|
+
})
|
|
237
241
|
// Live camera ref — the pointer-surface cap reconstructs the cursor world
|
|
238
242
|
// ray (camera → grid hit) to find which walking surface is aimed at.
|
|
239
243
|
const camera = useThree((s) => s.camera)
|
|
@@ -243,6 +247,7 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
243
247
|
// refreshed per grid move. Caps the floor-support election so a deck
|
|
244
248
|
// hanging above the aimed-at floor never lifts the dragged node.
|
|
245
249
|
const supportCapRef = useRef<number | null>(null)
|
|
250
|
+
const supportSurfaceRef = useRef<PointerSupportSurface | null>(null)
|
|
246
251
|
// Kinds whose `position` lives in a host parent's local frame declare
|
|
247
252
|
// `movable.parentFrame` (cabinet module ↔ its run). The tool converts the
|
|
248
253
|
// plan-frame cursor through the capability's hooks and previews via
|
|
@@ -302,12 +307,18 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
302
307
|
// and bumped by R/T. Applied imperatively + mirrored to `useLiveTransforms`,
|
|
303
308
|
// and committed to the scene on drop.
|
|
304
309
|
const rotationRef = useRef(originalRotationY)
|
|
310
|
+
const freeRotationRef = useRef(originalRotationY)
|
|
311
|
+
const attachmentRotationRef = useRef<number | null>(null)
|
|
305
312
|
// Snapshot of which ducts / fittings are mated to this node's ports at
|
|
306
313
|
// drag-start (duct fittings only). Drives the "connected ductwork follows"
|
|
307
314
|
// behaviour: connected nodes preview through `useLiveNodeOverrides` during
|
|
308
315
|
// the drag and commit alongside the moved node on drop. Null for kinds with
|
|
309
316
|
// no ports, so every other movable kind is unaffected.
|
|
310
317
|
const connectivityRef = useRef<PortConnectivity | null>(null)
|
|
318
|
+
// Node ids touched by a parent-frame kind's derived live preview, such as
|
|
319
|
+
// linked cabinet corner runs. This is separate from port connectivity so
|
|
320
|
+
// each preview channel can be cleared independently.
|
|
321
|
+
const parentFramePreviewIdsRef = useRef<AnyNodeId[]>([])
|
|
311
322
|
// Node ids this drag has pushed live overrides onto — cleared on
|
|
312
323
|
// commit / cancel / unmount so a follow-on drag starts clean.
|
|
313
324
|
const overriddenIdsRef = useRef<AnyNodeId[]>([])
|
|
@@ -317,8 +328,9 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
317
328
|
// refuse an invalid drop unless Alt forces it. The gate + footprint both come
|
|
318
329
|
// from the kind's declarative `floorPlaced` capability, so opting a new kind
|
|
319
330
|
// in is just `collides: true` — no change here.
|
|
320
|
-
// Parent-frame kinds skip the world-frame floor-collision
|
|
321
|
-
// position isn't in the level frame the spatial grid indexes.
|
|
331
|
+
// Parent-frame kinds skip the world-frame floor-collision check — their
|
|
332
|
+
// position isn't in the level frame the spatial grid indexes. They may
|
|
333
|
+
// still provide a parent-frame collision check and use the same bounds box.
|
|
322
334
|
const collides =
|
|
323
335
|
!frameParent && nodeRegistry.get(node.type)?.capabilities?.floorPlaced?.collides === true
|
|
324
336
|
// Snapshot the scene once at drag-start — bounds depend on `node` (locked
|
|
@@ -333,6 +345,9 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
333
345
|
| undefined) ?? null,
|
|
334
346
|
[node],
|
|
335
347
|
)
|
|
348
|
+
const parentFrameCollides = Boolean(
|
|
349
|
+
frameParent && parentFrame?.isValidPosition && dragBounds?.size,
|
|
350
|
+
)
|
|
336
351
|
// Collision extents: the declared drag bounds (composite kinds — a cabinet
|
|
337
352
|
// run spans its modules) win over the single-node footprint.
|
|
338
353
|
const resolvedFootprint = useMemo(
|
|
@@ -343,8 +358,8 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
343
358
|
[dragBounds, node],
|
|
344
359
|
)
|
|
345
360
|
const boxDimensions = useMemo(
|
|
346
|
-
() => (collides ? resolvedFootprint : null),
|
|
347
|
-
[collides, resolvedFootprint],
|
|
361
|
+
() => (collides || parentFrameCollides ? resolvedFootprint : null),
|
|
362
|
+
[collides, parentFrameCollides, resolvedFootprint],
|
|
348
363
|
)
|
|
349
364
|
const [valid, setValid] = useState(true)
|
|
350
365
|
const previewRotationY = useCallback(
|
|
@@ -400,6 +415,12 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
400
415
|
// to settle a dragged run flush against a wall without forking the move tool.
|
|
401
416
|
const groupMoveSnapConfig =
|
|
402
417
|
nodeRegistry.get(node.type)?.capabilities?.movable?.groupMoveSnap ?? null
|
|
418
|
+
const groupMoveSnapPoseConfig =
|
|
419
|
+
nodeRegistry.get(node.type)?.capabilities?.movable?.groupMoveSnapPose ?? null
|
|
420
|
+
const movableValidityConfig =
|
|
421
|
+
(nodeRegistry.get(node.type)?.capabilities?.movable as MovableConfig | undefined) ?? null
|
|
422
|
+
const gridSnapPositionConfig =
|
|
423
|
+
nodeRegistry.get(node.type)?.capabilities?.movable?.gridSnapPosition ?? null
|
|
403
424
|
// Mirrors of `valid` / Alt for the event handlers inside the effect, which
|
|
404
425
|
// can't read React state without stale closures.
|
|
405
426
|
const validRef = useRef(true)
|
|
@@ -415,11 +436,14 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
415
436
|
dragAnchorRef.current = null
|
|
416
437
|
hasMovedRef.current = false
|
|
417
438
|
rotationRef.current = originalRotationY
|
|
439
|
+
freeRotationRef.current = originalRotationY
|
|
440
|
+
attachmentRotationRef.current = null
|
|
418
441
|
altRef.current = false
|
|
419
442
|
validRef.current = true
|
|
420
443
|
// No pointer surface known yet — uncapped election (the node keeps its
|
|
421
444
|
// persisted host / committed elevation until the first grid move).
|
|
422
445
|
supportCapRef.current = null
|
|
446
|
+
supportSurfaceRef.current = null
|
|
423
447
|
// Re-sync the box transform to the (possibly new) node. `node` changes
|
|
424
448
|
// without this component remounting whenever a positioned preset re-arms a
|
|
425
449
|
// fresh clone after a drop, or the user picks a different catalog tile —
|
|
@@ -490,17 +514,46 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
490
514
|
|
|
491
515
|
const syncParentFramePreview = (position: [number, number, number]) => {
|
|
492
516
|
if (!frameParent) return
|
|
493
|
-
|
|
517
|
+
const entries: Array<readonly [AnyNodeId, Record<string, unknown>]> = [
|
|
518
|
+
[node.id as AnyNodeId, { position, rotation: rotationRef.current }],
|
|
519
|
+
]
|
|
520
|
+
const derivedEntries = parentFrame?.previewOverrides?.({
|
|
521
|
+
node,
|
|
522
|
+
parent: frameParent,
|
|
494
523
|
position,
|
|
495
|
-
|
|
524
|
+
sceneApi: createSceneApi(useScene),
|
|
496
525
|
})
|
|
497
|
-
|
|
526
|
+
if (derivedEntries) {
|
|
527
|
+
for (const [id, values] of derivedEntries) {
|
|
528
|
+
if (id === node.id) continue
|
|
529
|
+
entries.push([id, values as Record<string, unknown>])
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const nextIds = new Set(entries.map(([id]) => id))
|
|
534
|
+
for (const id of parentFramePreviewIdsRef.current) {
|
|
535
|
+
if (!nextIds.has(id)) useLiveNodeOverrides.getState().clear(id)
|
|
536
|
+
}
|
|
537
|
+
useLiveNodeOverrides.getState().setMany(entries)
|
|
538
|
+
parentFramePreviewIdsRef.current = [...nextIds]
|
|
539
|
+
|
|
540
|
+
const scene = useScene.getState()
|
|
541
|
+
for (const [id] of entries) {
|
|
542
|
+
if (scene.nodes[id]) scene.markDirty(id)
|
|
543
|
+
}
|
|
544
|
+
if (frameParent.id !== node.id) scene.markDirty(frameParent.id as AnyNodeId)
|
|
498
545
|
}
|
|
499
546
|
|
|
500
547
|
const clearParentFramePreview = () => {
|
|
501
548
|
if (!frameParent) return
|
|
502
|
-
|
|
503
|
-
useScene.getState()
|
|
549
|
+
const ids = new Set<AnyNodeId>([node.id as AnyNodeId, ...parentFramePreviewIdsRef.current])
|
|
550
|
+
const scene = useScene.getState()
|
|
551
|
+
for (const id of ids) {
|
|
552
|
+
useLiveNodeOverrides.getState().clear(id)
|
|
553
|
+
if (scene.nodes[id]) scene.markDirty(id)
|
|
554
|
+
}
|
|
555
|
+
parentFramePreviewIdsRef.current = []
|
|
556
|
+
scene.markDirty(frameParent.id as AnyNodeId)
|
|
504
557
|
}
|
|
505
558
|
|
|
506
559
|
setCursorPosition(getVisualPosition(originalPosition, originalRotationY))
|
|
@@ -510,12 +563,27 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
510
563
|
// override so the user can drop on top of an existing item on purpose. Only
|
|
511
564
|
// shelves show the box, so this no-ops for every other movable kind.
|
|
512
565
|
const recomputeValidity = () => {
|
|
513
|
-
if (!boxDimensions) return
|
|
566
|
+
if (!boxDimensions && !movableValidityConfig) return
|
|
514
567
|
if (altRef.current) {
|
|
515
568
|
validRef.current = true
|
|
516
569
|
setValid(true)
|
|
517
570
|
return
|
|
518
571
|
}
|
|
572
|
+
if (parentFrameCollides && frameParent && parentFrame?.isValidPosition) {
|
|
573
|
+
const candidate = {
|
|
574
|
+
...(node as Record<string, unknown>),
|
|
575
|
+
position: lastCursorRef.current,
|
|
576
|
+
} as AnyNode
|
|
577
|
+
const validPosition = parentFrame.isValidPosition({
|
|
578
|
+
node: candidate,
|
|
579
|
+
parent: frameParent,
|
|
580
|
+
position: lastCursorRef.current,
|
|
581
|
+
nodes: useScene.getState().nodes as Record<string, AnyNode>,
|
|
582
|
+
})
|
|
583
|
+
validRef.current = validPosition
|
|
584
|
+
setValid(validPosition)
|
|
585
|
+
return
|
|
586
|
+
}
|
|
519
587
|
const levelId = useViewer.getState().selection.levelId ?? node.parentId
|
|
520
588
|
if (!levelId) {
|
|
521
589
|
validRef.current = true
|
|
@@ -551,15 +619,27 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
551
619
|
const { valid: placeable } =
|
|
552
620
|
resolvedFootprints.length > 0
|
|
553
621
|
? spatialGridManager.canPlaceOnFloorFootprints(levelId, resolvedFootprints, [node.id])
|
|
554
|
-
:
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
622
|
+
: boxDimensions
|
|
623
|
+
? spatialGridManager.canPlaceOnFloor(
|
|
624
|
+
levelId,
|
|
625
|
+
getVisualPosition(livePosition),
|
|
626
|
+
boxDimensions,
|
|
627
|
+
[0, liveRotation, 0],
|
|
628
|
+
[node.id],
|
|
629
|
+
)
|
|
630
|
+
: { valid: true }
|
|
631
|
+
const kindValid = movableValidityConfig?.isValidPosition
|
|
632
|
+
? movableValidityConfig.isValidPosition({
|
|
633
|
+
node: effectiveNode,
|
|
634
|
+
position: livePosition,
|
|
635
|
+
rotation: rotationRef.current,
|
|
636
|
+
levelId: levelId as AnyNodeId | null,
|
|
637
|
+
nodes: useScene.getState().nodes as Record<string, AnyNode>,
|
|
638
|
+
})
|
|
639
|
+
: true
|
|
640
|
+
const positionValid = placeable && kindValid
|
|
641
|
+
validRef.current = positionValid
|
|
642
|
+
setValid(positionValid)
|
|
563
643
|
}
|
|
564
644
|
recomputeValidity()
|
|
565
645
|
|
|
@@ -620,20 +700,105 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
620
700
|
// single fixed point per pointer ray.
|
|
621
701
|
const pointed = resolvePointerSupportSurface(cameraRef.current, event.position)
|
|
622
702
|
supportCapRef.current = pointed?.elevation ?? null
|
|
703
|
+
supportSurfaceRef.current = pointed
|
|
623
704
|
const rawX = pointed?.localPoint?.[0] ?? event.localPosition[0]
|
|
624
705
|
const rawZ = pointed?.localPoint?.[2] ?? event.localPosition[2]
|
|
625
706
|
revealFreshPlacement()
|
|
626
707
|
|
|
627
|
-
const
|
|
708
|
+
const magnetic = isMagneticSnapActive()
|
|
709
|
+
const attachmentEnabled = magnetic || isGridSnapActive()
|
|
710
|
+
const absolute = useAbsoluteCursorPlacement || cursorAttached
|
|
711
|
+
const centerOffset: [number, number, number] =
|
|
712
|
+
absolute && dragBounds?.center
|
|
713
|
+
? offsetPlanPositionByLocalCenter(
|
|
714
|
+
[0, 0, 0],
|
|
715
|
+
dragBounds.center,
|
|
716
|
+
previewRotationY(freeRotationRef.current),
|
|
717
|
+
)
|
|
718
|
+
: [0, 0, 0]
|
|
719
|
+
let attachmentRotationY: number | null = null
|
|
720
|
+
const resolved = resolvePrioritizedPlanarCursorPosition({
|
|
628
721
|
cursor: [rawX, rawZ],
|
|
629
722
|
original: [originalPlanPosition[0], originalPlanPosition[2]],
|
|
630
723
|
anchor: dragAnchorRef.current,
|
|
631
|
-
mode:
|
|
724
|
+
mode: absolute ? 'absolute' : 'relative',
|
|
725
|
+
localCenter: dragBounds?.center,
|
|
726
|
+
rotationY: previewRotationY(freeRotationRef.current),
|
|
632
727
|
// Snap follows the mode (raw in Off via snapToGridStep); Alt = force only.
|
|
633
|
-
snap: snapToGridStep,
|
|
728
|
+
snap: gridSnapPositionConfig ? undefined : snapToGridStep,
|
|
729
|
+
snapPoint:
|
|
730
|
+
isGridSnapActive() && gridSnapPositionConfig
|
|
731
|
+
? ([planX, planZ]) => {
|
|
732
|
+
const snappedPosition = gridSnapPositionConfig({
|
|
733
|
+
node,
|
|
734
|
+
// Kind-owned grid hooks exchange origins and apply their own footprint offsets.
|
|
735
|
+
candidatePosition: canonicalPositionFromPlan(
|
|
736
|
+
planX - centerOffset[0],
|
|
737
|
+
originalPosition[1],
|
|
738
|
+
planZ - centerOffset[2],
|
|
739
|
+
),
|
|
740
|
+
candidateRotation: freeRotationRef.current,
|
|
741
|
+
movingIds: [node.id as AnyNodeId],
|
|
742
|
+
nodes: useScene.getState().nodes as Record<string, AnyNode>,
|
|
743
|
+
levelId:
|
|
744
|
+
(useViewer.getState().selection.levelId as AnyNodeId | null) ??
|
|
745
|
+
(node.parentId as AnyNodeId | undefined) ??
|
|
746
|
+
null,
|
|
747
|
+
gridStep: useEditor.getState().gridSnapStep,
|
|
748
|
+
})
|
|
749
|
+
const snappedPlanPosition = getVisualPosition(
|
|
750
|
+
snappedPosition,
|
|
751
|
+
freeRotationRef.current,
|
|
752
|
+
)
|
|
753
|
+
return [
|
|
754
|
+
snappedPlanPosition[0] + centerOffset[0],
|
|
755
|
+
snappedPlanPosition[2] + centerOffset[2],
|
|
756
|
+
]
|
|
757
|
+
}
|
|
758
|
+
: undefined,
|
|
759
|
+
resolveAttachment:
|
|
760
|
+
attachmentEnabled && (groupMoveSnapPoseConfig || groupMoveSnapConfig)
|
|
761
|
+
? ([planX, planZ]) => {
|
|
762
|
+
const snapArgs: Parameters<NonNullable<typeof groupMoveSnapPoseConfig>>[0] = {
|
|
763
|
+
node,
|
|
764
|
+
candidatePosition: canonicalPositionFromPlan(planX, originalPosition[1], planZ),
|
|
765
|
+
candidateRotation:
|
|
766
|
+
absolute && dragBounds?.center ? freeRotationRef.current : rotationRef.current,
|
|
767
|
+
movingIds: [node.id as AnyNodeId],
|
|
768
|
+
nodes: useScene.getState().nodes as Record<string, AnyNode>,
|
|
769
|
+
levelId:
|
|
770
|
+
(useViewer.getState().selection.levelId as AnyNodeId | null) ??
|
|
771
|
+
(node.parentId as AnyNodeId | undefined) ??
|
|
772
|
+
null,
|
|
773
|
+
}
|
|
774
|
+
const snappedPosition: GroupMoveSnapResult | null = groupMoveSnapPoseConfig
|
|
775
|
+
? groupMoveSnapPoseConfig(snapArgs)
|
|
776
|
+
: (() => {
|
|
777
|
+
const position = groupMoveSnapConfig?.(snapArgs)
|
|
778
|
+
return position ? { position } : null
|
|
779
|
+
})()
|
|
780
|
+
if (!snappedPosition) return null
|
|
781
|
+
attachmentRotationY = snappedPosition.rotation ?? null
|
|
782
|
+
const snappedPlanPosition = getVisualPosition(
|
|
783
|
+
snappedPosition.position,
|
|
784
|
+
snappedPosition.rotation ?? rotationRef.current,
|
|
785
|
+
)
|
|
786
|
+
return [snappedPlanPosition[0], snappedPlanPosition[2]]
|
|
787
|
+
}
|
|
788
|
+
: undefined,
|
|
634
789
|
})
|
|
635
790
|
dragAnchorRef.current = resolved.anchor
|
|
636
791
|
let [x, z] = resolved.point
|
|
792
|
+
const attachmentSnapped = resolved.attachmentSnapped
|
|
793
|
+
attachmentRotationRef.current = attachmentSnapped ? attachmentRotationY : null
|
|
794
|
+
const nextRotationY = resolveAttachmentPreviewRotation(
|
|
795
|
+
freeRotationRef.current,
|
|
796
|
+
attachmentRotationRef.current,
|
|
797
|
+
)
|
|
798
|
+
if (nextRotationY !== rotationRef.current) {
|
|
799
|
+
rotationRef.current = nextRotationY
|
|
800
|
+
setCursorRotationY(previewRotationY(nextRotationY))
|
|
801
|
+
}
|
|
637
802
|
|
|
638
803
|
// Figma-style alignment snap layered on top of grid snap: when the
|
|
639
804
|
// moving item's edge lines up (on X or Z) with another item's edge,
|
|
@@ -642,8 +807,7 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
642
807
|
// point. Alignment "lines" are DISPLAYED in every mode except Off
|
|
643
808
|
// (isAlignmentGuideActive); the magnetic pull toward them applies only in
|
|
644
809
|
// 'lines' mode (magnetic). Alt is force-place, not a snap bypass.
|
|
645
|
-
|
|
646
|
-
if (isAlignmentGuideActive() && alignmentCandidates.length > 0) {
|
|
810
|
+
if (!attachmentSnapped && isAlignmentGuideActive() && alignmentCandidates.length > 0) {
|
|
647
811
|
const result = resolveAlignment({
|
|
648
812
|
moving: movingDragBoundsAnchors(
|
|
649
813
|
node,
|
|
@@ -664,25 +828,6 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
664
828
|
useAlignmentGuides.getState().clear()
|
|
665
829
|
}
|
|
666
830
|
|
|
667
|
-
// Kind-owned attachment snap (cabinet → wall): an attach behavior like
|
|
668
|
-
// door/window wall placement, not an alignment guide — active in every
|
|
669
|
-
// snapping mode except Off.
|
|
670
|
-
if ((magnetic || isGridSnapActive()) && groupMoveSnapConfig) {
|
|
671
|
-
const snappedPosition = groupMoveSnapConfig({
|
|
672
|
-
node,
|
|
673
|
-
candidatePosition: canonicalPositionFromPlan(x, originalPosition[1], z),
|
|
674
|
-
movingIds: [node.id as AnyNodeId],
|
|
675
|
-
nodes: useScene.getState().nodes as Record<string, AnyNode>,
|
|
676
|
-
levelId: (useViewer.getState().selection.levelId as AnyNodeId | null) ?? null,
|
|
677
|
-
})
|
|
678
|
-
if (snappedPosition) {
|
|
679
|
-
const snappedPlanPosition = getVisualPosition(snappedPosition)
|
|
680
|
-
x = snappedPlanPosition[0]
|
|
681
|
-
z = snappedPlanPosition[2]
|
|
682
|
-
useAlignmentGuides.getState().clear()
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
|
|
686
831
|
// Magnetic port snap (duct terminals): mate a collar onto a nearby
|
|
687
832
|
// duct run end. Takes precedence over grid / alignment snap; Alt
|
|
688
833
|
// bypasses. Only kinds that opted in via `movable.portSnap`.
|
|
@@ -701,7 +846,12 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
701
846
|
}
|
|
702
847
|
|
|
703
848
|
let position = canonicalPositionFromPlan(x, originalPosition[1], z)
|
|
704
|
-
if (
|
|
849
|
+
if (
|
|
850
|
+
!attachmentSnapped &&
|
|
851
|
+
(magnetic || isGridSnapActive()) &&
|
|
852
|
+
parentFrame?.magneticSnap &&
|
|
853
|
+
frameParent
|
|
854
|
+
) {
|
|
705
855
|
const preSnapPosition = position
|
|
706
856
|
const snappedPosition = parentFrame.magneticSnap(
|
|
707
857
|
node,
|
|
@@ -732,6 +882,24 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
732
882
|
if (guides.length > 0) useAlignmentGuides.getState().set(guides)
|
|
733
883
|
}
|
|
734
884
|
}
|
|
885
|
+
if (!parentFrame && pointed?.sourceNodeId) {
|
|
886
|
+
const rotation = toCommitRotation(rotationRef.current)
|
|
887
|
+
const effectiveNode = {
|
|
888
|
+
...(node as Record<string, unknown>),
|
|
889
|
+
position,
|
|
890
|
+
rotation,
|
|
891
|
+
} as AnyNode
|
|
892
|
+
position = resolveFrozenFloorPlacementPatch(
|
|
893
|
+
effectiveNode,
|
|
894
|
+
{ ...useScene.getState().nodes, [node.id]: effectiveNode },
|
|
895
|
+
{
|
|
896
|
+
position,
|
|
897
|
+
rotation,
|
|
898
|
+
elevation: pointed.elevation,
|
|
899
|
+
preferredSlabId: pointed.supportSlabId,
|
|
900
|
+
},
|
|
901
|
+
).position
|
|
902
|
+
}
|
|
735
903
|
const visualPosition = getVisualPosition(position)
|
|
736
904
|
hasMovedRef.current = true
|
|
737
905
|
setCursorPosition(visualPosition)
|
|
@@ -764,7 +932,7 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
764
932
|
|
|
765
933
|
const nextSnapKey = movementSfxStepKey({
|
|
766
934
|
coords: [x, z],
|
|
767
|
-
gridSnapActive: isGridSnapActive(),
|
|
935
|
+
gridSnapActive: isGridSnapActive() && !attachmentSnapped,
|
|
768
936
|
gridStep: useEditor.getState().gridSnapStep,
|
|
769
937
|
})
|
|
770
938
|
const prev = previousSnapRef.current
|
|
@@ -790,8 +958,8 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
790
958
|
* AND scene updated) — never the original.
|
|
791
959
|
*/
|
|
792
960
|
const commitAtCursor = (event: ClickTriggerEvent) => {
|
|
793
|
-
// One physical click can reach here twice: node
|
|
794
|
-
//
|
|
961
|
+
// One physical click can reach here twice: `node:click` is synthesized
|
|
962
|
+
// on *pointerup* (`use-node-events`),
|
|
795
963
|
// while `grid:click` rides the browser's native *click* event from a
|
|
796
964
|
// canvas DOM listener (`use-grid-events`) that deliberately ignores
|
|
797
965
|
// stopPropagation — and this effect stays subscribed until React
|
|
@@ -830,7 +998,11 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
830
998
|
...useScene.getState().nodes,
|
|
831
999
|
[node.id]: effectiveNode,
|
|
832
1000
|
},
|
|
833
|
-
{
|
|
1001
|
+
{
|
|
1002
|
+
maxElevation: supportCapRef.current,
|
|
1003
|
+
preferredSlabId: supportSurfaceRef.current?.supportSlabId,
|
|
1004
|
+
pinSupport: supportSurfaceRef.current?.sourceNodeId != null,
|
|
1005
|
+
},
|
|
834
1006
|
),
|
|
835
1007
|
...(isNew
|
|
836
1008
|
? {
|
|
@@ -902,7 +1074,11 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
902
1074
|
...useScene.getState().nodes,
|
|
903
1075
|
[reparsed.id]: reparsed,
|
|
904
1076
|
},
|
|
905
|
-
{
|
|
1077
|
+
{
|
|
1078
|
+
maxElevation: supportCapRef.current,
|
|
1079
|
+
preferredSlabId: supportSurfaceRef.current?.supportSlabId,
|
|
1080
|
+
pinSupport: supportSurfaceRef.current?.sourceNodeId != null,
|
|
1081
|
+
},
|
|
906
1082
|
),
|
|
907
1083
|
}) as AnyNode
|
|
908
1084
|
useScene.temporal.getState().resume()
|
|
@@ -961,10 +1137,35 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
961
1137
|
else if (e.key === 't' || e.key === 'T') delta = -ROTATION_STEP
|
|
962
1138
|
else return
|
|
963
1139
|
e.preventDefault()
|
|
1140
|
+
const nextFreeRotation = resolveMoveRotationStep(
|
|
1141
|
+
freeRotationRef.current,
|
|
1142
|
+
delta,
|
|
1143
|
+
attachmentRotationRef.current,
|
|
1144
|
+
)
|
|
1145
|
+
if (nextFreeRotation === null) return
|
|
964
1146
|
sfxEmitter.emit('sfx:item-rotate')
|
|
965
|
-
|
|
1147
|
+
let position = lastCursorRef.current
|
|
1148
|
+
if (
|
|
1149
|
+
hasMovedRef.current &&
|
|
1150
|
+
(useAbsoluteCursorPlacement || cursorAttached) &&
|
|
1151
|
+
dragBounds?.center
|
|
1152
|
+
) {
|
|
1153
|
+
const planCenter = offsetPlanPositionByLocalCenter(
|
|
1154
|
+
getVisualPosition(position),
|
|
1155
|
+
dragBounds.center,
|
|
1156
|
+
previewRotationY(rotationRef.current),
|
|
1157
|
+
)
|
|
1158
|
+
const planOrigin = offsetPlanPositionByLocalCenter(
|
|
1159
|
+
planCenter,
|
|
1160
|
+
[-dragBounds.center[0], 0, -dragBounds.center[2]],
|
|
1161
|
+
previewRotationY(nextFreeRotation),
|
|
1162
|
+
)
|
|
1163
|
+
position = canonicalPositionFromPlan(planOrigin[0], position[1], planOrigin[2])
|
|
1164
|
+
lastCursorRef.current = position
|
|
1165
|
+
}
|
|
1166
|
+
freeRotationRef.current = nextFreeRotation
|
|
1167
|
+
rotationRef.current = freeRotationRef.current
|
|
966
1168
|
setCursorRotationY(previewRotationY(rotationRef.current))
|
|
967
|
-
const position = lastCursorRef.current
|
|
968
1169
|
const visualPosition = getVisualPosition(position)
|
|
969
1170
|
setCursorPosition(visualPosition)
|
|
970
1171
|
applyMeshPose(position)
|
|
@@ -1007,15 +1208,7 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
1007
1208
|
}
|
|
1008
1209
|
window.addEventListener('pointerup', onPlacementDragPointerUp)
|
|
1009
1210
|
|
|
1010
|
-
|
|
1011
|
-
// `${kind}:click` as a fixed union so the cast is safe at runtime —
|
|
1012
|
-
// we're just routing them through the shared commit path.
|
|
1013
|
-
type SuffixedKey<K extends string> = `${K}:${EventSuffix}`
|
|
1014
|
-
type ClickKey = SuffixedKey<(typeof CLICK_TRIGGER_KINDS)[number]>
|
|
1015
|
-
for (const kind of CLICK_TRIGGER_KINDS) {
|
|
1016
|
-
const key = `${kind}:click` as ClickKey
|
|
1017
|
-
emitter.on(key, commitAtCursor as never)
|
|
1018
|
-
}
|
|
1211
|
+
emitter.on('node:click', commitAtCursor)
|
|
1019
1212
|
|
|
1020
1213
|
const onCancel = () => {
|
|
1021
1214
|
useLiveTransforms.getState().clear(node.id)
|
|
@@ -1040,10 +1233,7 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
1040
1233
|
emitter.off('grid:move', onGridMove)
|
|
1041
1234
|
emitter.off('grid:click', commitAtCursor)
|
|
1042
1235
|
window.removeEventListener('pointerup', onPlacementDragPointerUp)
|
|
1043
|
-
|
|
1044
|
-
const key = `${kind}:click` as ClickKey
|
|
1045
|
-
emitter.off(key, commitAtCursor as never)
|
|
1046
|
-
}
|
|
1236
|
+
emitter.off('node:click', commitAtCursor)
|
|
1047
1237
|
emitter.off('tool:cancel', onCancel)
|
|
1048
1238
|
// Restore the moved meshes' raycast so they're hoverable / selectable
|
|
1049
1239
|
// again after the drag ends.
|
|
@@ -1067,9 +1257,13 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
1067
1257
|
canonicalPositionFromPlan,
|
|
1068
1258
|
parentFrame,
|
|
1069
1259
|
frameParent,
|
|
1260
|
+
parentFrameCollides,
|
|
1070
1261
|
cursorAttached,
|
|
1071
1262
|
portSnapConfig,
|
|
1072
1263
|
groupMoveSnapConfig,
|
|
1264
|
+
groupMoveSnapPoseConfig,
|
|
1265
|
+
movableValidityConfig,
|
|
1266
|
+
gridSnapPositionConfig,
|
|
1073
1267
|
exitMoveMode,
|
|
1074
1268
|
isFreshPlacement,
|
|
1075
1269
|
node,
|
|
@@ -1104,12 +1298,14 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
1104
1298
|
|
|
1105
1299
|
if (boxDimensions && !dragBounds?.center) {
|
|
1106
1300
|
return (
|
|
1107
|
-
<
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1301
|
+
<group ref={previewGroupRef}>
|
|
1302
|
+
<PlacementBox
|
|
1303
|
+
dimensions={boxDimensions}
|
|
1304
|
+
position={cursorPosition}
|
|
1305
|
+
rotationY={cursorRotationY}
|
|
1306
|
+
valid={valid}
|
|
1307
|
+
/>
|
|
1308
|
+
</group>
|
|
1113
1309
|
)
|
|
1114
1310
|
}
|
|
1115
1311
|
|
|
@@ -1119,7 +1315,7 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
1119
1315
|
: cursorPosition
|
|
1120
1316
|
|
|
1121
1317
|
return (
|
|
1122
|
-
|
|
1318
|
+
<group ref={previewGroupRef}>
|
|
1123
1319
|
<CursorSphere color="#a78bfa" height={2.5} position={dragCenterPosition} />
|
|
1124
1320
|
<DragBoundingBox
|
|
1125
1321
|
center={dragBounds?.center}
|
|
@@ -1130,6 +1326,6 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
1130
1326
|
rotationY={cursorRotationY}
|
|
1131
1327
|
size={dragBounds?.size}
|
|
1132
1328
|
/>
|
|
1133
|
-
|
|
1329
|
+
</group>
|
|
1134
1330
|
)
|
|
1135
1331
|
}
|