@pascal-app/editor 1.0.0 → 1.0.3
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 -6
- package/src/components/editor/bake-exporter.tsx +4 -0
- package/src/components/editor/bake-thumbnail.tsx +4 -1
- package/src/components/editor/custom-camera-controls.tsx +22 -74
- package/src/components/editor/editor-layout-mobile.tsx +1 -0
- package/src/components/editor/floating-action-menu.tsx +14 -1
- package/src/components/editor/floorplan-panel.tsx +233 -166
- package/src/components/editor/grid.tsx +31 -7
- package/src/components/editor/group-actions.ts +5 -9
- package/src/components/editor/group-floating-action-menu.tsx +19 -9
- package/src/components/editor/group-move-3d.ts +4 -6
- package/src/components/editor/group-rotate-handle.tsx +81 -35
- package/src/components/editor/group-selection-box-3d.tsx +25 -17
- package/src/components/editor/group-transform-shared.test.ts +126 -1
- package/src/components/editor/group-transform-shared.ts +79 -73
- package/src/components/editor/handles/handle-arrow.tsx +72 -2
- package/src/components/editor/handles/handle-placement.test.ts +111 -0
- package/src/components/editor/handles/handle-placement.ts +69 -0
- package/src/components/editor/handles/linear-resize-drag.ts +13 -2
- package/src/components/editor/handles/use-handle-drag.ts +80 -8
- package/src/components/editor/index.tsx +36 -13
- package/src/components/editor/node-action-menu.tsx +2 -0
- package/src/components/editor/node-arrow-handles.tsx +162 -69
- package/src/components/editor/registry-action-contributions.tsx +53 -0
- package/src/components/editor/selection-manager.tsx +49 -0
- package/src/components/editor/thumbnail-generator.tsx +11 -2
- package/src/components/editor/wall-move-side-handles.tsx +71 -21
- package/src/components/editor/wall-opening-highlights.tsx +2 -3
- package/src/components/editor-2d/floorplan-group-move.tsx +17 -24
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +33 -12
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +12 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +154 -49
- package/src/components/editor-2d/renderers/floorplan-draft-wall-measurement.tsx +166 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +49 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +113 -24
- package/src/components/procedural-items/inspector.tsx +223 -0
- package/src/components/systems/ceiling/ceiling-system.tsx +3 -1
- package/src/components/systems/zone/zone-system.tsx +8 -4
- package/src/components/tools/__fixtures__/hosting.ts +10 -0
- package/src/components/tools/item/item-surface-placement.test.ts +169 -0
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +204 -0
- package/src/components/tools/item/placement-strategies.ts +212 -200
- package/src/components/tools/item/placement-types.ts +4 -0
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +257 -0
- package/src/components/tools/item/use-draft-node.ts +136 -47
- package/src/components/tools/item/use-placement-coordinator.tsx +391 -157
- package/src/components/tools/registry/item-surface-child-frame.test.ts +90 -0
- package/src/components/tools/registry/item-surface-move.test.ts +509 -0
- package/src/components/tools/registry/item-surface-move.ts +463 -0
- package/src/components/tools/registry/item-surface-resolver.test.ts +257 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +237 -78
- package/src/components/tools/shared/affordance-dispatch.test.ts +24 -0
- package/src/components/tools/shared/affordance-dispatch.ts +13 -0
- package/src/components/tools/shared/draft-measurement-label.tsx +38 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +27 -0
- package/src/components/tools/shared/pointer-support-cap.ts +27 -13
- package/src/components/tools/shared/shelf-stickiness.ts +54 -0
- package/src/components/tools/shared/surface-hit.test.ts +56 -0
- package/src/components/tools/shared/surface-hit.ts +23 -0
- package/src/components/tools/shared/surface-rejection.test.tsx +107 -0
- package/src/components/tools/shared/surface-rejection.tsx +43 -0
- package/src/components/tools/tool-manager.tsx +36 -3
- package/src/components/tools/zone/zone-tool.tsx +13 -4
- package/src/components/ui/command-palette/editor-commands.tsx +54 -1
- package/src/components/ui/controls/material-paint-panel.tsx +6 -67
- package/src/components/ui/controls/material-picker.tsx +5 -67
- package/src/components/ui/controls/metric-control.tsx +29 -13
- package/src/components/ui/controls/slider-control.tsx +87 -35
- package/src/components/ui/floating-level-selector.tsx +81 -5
- package/src/components/ui/helpers/helper-manager.tsx +24 -6
- package/src/components/ui/helpers/registered-tool-helper.tsx +2 -32
- package/src/components/ui/item-catalog/item-catalog.tsx +5 -30
- package/src/components/ui/panels/multi-height-mode.tsx +27 -21
- package/src/components/ui/panels/multi-parametric-inspector.tsx +6 -3
- package/src/components/ui/panels/parametric-field-control.tsx +1 -1
- package/src/components/ui/panels/parametric-inspector.tsx +9 -17
- package/src/components/ui/panels/reference-panel.tsx +3 -3
- package/src/components/ui/primitives/checkbox.tsx +34 -0
- package/src/components/ui/primitives/shortcut-token.tsx +4 -0
- package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +669 -336
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts +31 -4
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +30 -11
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +8 -1
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +106 -4
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +2 -7
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +3 -8
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +6 -1
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +224 -0
- package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +22 -0
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +24 -7
- package/src/components/ui/snap-target-badge.tsx +2 -0
- package/src/components/viewer/floorplan-preview-visibility.test.ts +35 -0
- package/src/components/viewer/floorplan-preview-visibility.ts +19 -0
- package/src/components/viewer/floorplan-preview.tsx +1 -12
- package/src/components/viewer/viewer-scene-header.tsx +3 -0
- package/src/components/viewer/viewer-units-panel.tsx +116 -0
- package/src/components/viewer-zone-system.tsx +7 -2
- package/src/hooks/use-auto-save.integration.test.tsx +197 -0
- package/src/hooks/use-auto-save.ts +21 -6
- package/src/hooks/use-ceiling-events.test.ts +25 -2
- package/src/hooks/use-ceiling-events.ts +7 -1
- package/src/hooks/use-grid-events.test.ts +2 -0
- package/src/hooks/use-grid-events.ts +16 -11
- package/src/hooks/use-keyboard.test.ts +7 -0
- package/src/hooks/use-keyboard.ts +43 -28
- package/src/index.tsx +68 -3
- package/src/lib/catalog-panel-model.ts +39 -0
- package/src/lib/catalog-selection.test.ts +27 -0
- package/src/lib/catalog-selection.ts +8 -0
- package/src/lib/contextual-help.test.ts +27 -1
- package/src/lib/contextual-help.ts +16 -9
- package/src/lib/export-texture-utils.test.ts +167 -0
- package/src/lib/export-texture-utils.ts +121 -0
- package/src/lib/export-user-data.test.ts +48 -0
- package/src/lib/export-user-data.ts +69 -0
- package/src/lib/floorplan/floorplan-export.test.ts +142 -2
- package/src/lib/floorplan/floorplan-export.tsx +9 -3
- package/src/lib/floorplan/floorplan-extension.ts +16 -0
- package/src/lib/floorplan/floorplan-readonly.ts +4 -0
- package/src/lib/fresh-planar-placement.ts +173 -33
- package/src/lib/glb-export-serialize.test.ts +280 -0
- package/src/lib/glb-export.test.ts +265 -16
- package/src/lib/glb-export.ts +197 -37
- package/src/lib/grid-frame.test.ts +19 -0
- package/src/lib/grid-frame.ts +18 -0
- package/src/lib/interaction/scope.ts +9 -1
- package/src/lib/keyboard-pan.test.ts +62 -0
- package/src/lib/keyboard-pan.ts +89 -0
- package/src/lib/level-duplication.test.ts +73 -0
- package/src/lib/level-duplication.ts +32 -2
- package/src/lib/linear-display.test.ts +59 -0
- package/src/lib/linear-display.ts +39 -0
- package/src/lib/local-guide-image.test.ts +24 -0
- package/src/lib/local-guide-image.ts +34 -1
- package/src/lib/material-catalog-model.ts +72 -0
- package/src/lib/material-paint-panel-model.ts +76 -0
- package/src/lib/node-action-movement.ts +21 -0
- package/src/lib/overlay-transform.test.ts +28 -0
- package/src/lib/overlay-transform.ts +12 -0
- package/src/lib/panel-rows.ts +75 -0
- package/src/lib/panel-tool-options.test.ts +34 -0
- package/src/lib/panel-tool-options.ts +46 -0
- package/src/lib/parametric-node-update.ts +37 -0
- package/src/lib/portable-export.test.ts +89 -0
- package/src/lib/portable-export.ts +84 -11
- package/src/lib/print-shell-compiler-manifold-core.ts +59 -10
- package/src/lib/print-shell-compiler-manifold-worker.ts +14 -1
- package/src/lib/print-shell-compiler-manifold.worker.ts +1 -1
- package/src/lib/print-shell-compiler-protocol.ts +8 -0
- package/src/lib/scene-clipboard.ts +1 -1
- package/src/lib/scene.test.ts +99 -0
- package/src/lib/scene.ts +21 -3
- package/src/lib/send-to-app.test.ts +108 -0
- package/src/lib/send-to-app.ts +121 -0
- package/src/lib/sfx/movement-tick.ts +37 -0
- package/src/lib/snapping-mode.test.ts +13 -2
- package/src/lib/snapping-mode.ts +11 -6
- package/src/lib/spatial-drag-plane.test.ts +53 -0
- package/src/lib/spatial-drag-plane.ts +19 -0
- package/src/lib/spatial-pointer-input.test.ts +72 -0
- package/src/lib/spatial-pointer-input.ts +62 -0
- package/src/lib/spatial-snap.test.ts +47 -0
- package/src/lib/surface-attachment.ts +77 -0
- package/src/lib/terrain-panel-model.ts +117 -0
- package/src/lib/units.ts +254 -0
- package/src/lib/usdz-export.ts +43 -16
- package/src/lib/use-linear-display.ts +8 -32
- package/src/lib/world-grid-snap.test.ts +48 -0
- package/src/lib/world-grid-snap.ts +9 -5
- package/src/store/rotation-snapping.test.ts +53 -0
- package/src/store/use-editor.tsx +10 -2
- package/src/store/use-floorplan-draft-preview.ts +10 -1
- package/src/store/use-handle-group.ts +4 -0
- package/src/store/use-interaction-scope.ts +155 -6
- package/src/store/use-send-to-app.ts +21 -0
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
type AnyNodeId,
|
|
9
9
|
analyzePortConnectivity,
|
|
10
10
|
bboxCornerAnchors,
|
|
11
|
+
cascadeDirty,
|
|
11
12
|
collectAlignmentAnchors,
|
|
12
13
|
createSceneApi,
|
|
13
14
|
emitter,
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
getFloorPlacedFootprints,
|
|
19
20
|
type MovableConfig,
|
|
20
21
|
movingFootprintAnchors,
|
|
22
|
+
NON_PHYSICAL_HOST_KINDS,
|
|
21
23
|
type NodeEvent,
|
|
22
24
|
nodeRegistry,
|
|
23
25
|
type ParentFrameSnapMatch,
|
|
@@ -27,6 +29,7 @@ import {
|
|
|
27
29
|
resolveFacingIndicator,
|
|
28
30
|
resolveFrozenFloorPlacementPatch,
|
|
29
31
|
resolveSupportSlabPatch,
|
|
32
|
+
type SurfaceRejectReason,
|
|
30
33
|
sceneRegistry,
|
|
31
34
|
spatialGridManager,
|
|
32
35
|
useLiveNodeOverrides,
|
|
@@ -36,19 +39,26 @@ import {
|
|
|
36
39
|
import { useViewer } from '@pascal-app/viewer'
|
|
37
40
|
import { useFrame, useThree } from '@react-three/fiber'
|
|
38
41
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
39
|
-
import type
|
|
42
|
+
import { type Group, Vector3 } from 'three'
|
|
40
43
|
import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
|
|
41
44
|
import { commitFreshPlacementSubtree } from '../../../lib/fresh-planar-placement'
|
|
42
|
-
import {
|
|
45
|
+
import {
|
|
46
|
+
isFreshPlacementMetadata,
|
|
47
|
+
stripPlacementMetadataFlags,
|
|
48
|
+
} from '../../../lib/placement-metadata'
|
|
43
49
|
import {
|
|
44
50
|
offsetPlanPositionByLocalCenter,
|
|
45
51
|
resolvePrioritizedPlanarCursorPosition,
|
|
46
52
|
} from '../../../lib/planar-cursor-placement'
|
|
47
53
|
import { resolveAttachmentPreviewRotation } from '../../../lib/rigid-plan-svg-transform'
|
|
48
|
-
import {
|
|
54
|
+
import { createMovementSfxTick } from '../../../lib/sfx/movement-tick'
|
|
49
55
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
50
56
|
import { resolveSnapFlags } from '../../../lib/snapping-mode'
|
|
51
|
-
|
|
57
|
+
import {
|
|
58
|
+
surfaceAttachmentId,
|
|
59
|
+
surfaceAttachmentUpdates,
|
|
60
|
+
updateSurfaceNode,
|
|
61
|
+
} from '../../../lib/surface-attachment'
|
|
52
62
|
import useAlignmentGuides from '../../../store/use-alignment-guides'
|
|
53
63
|
import useEditor, {
|
|
54
64
|
getActiveSnappingMode,
|
|
@@ -56,8 +66,8 @@ import useEditor, {
|
|
|
56
66
|
isGridSnapActive,
|
|
57
67
|
isMagneticSnapActive,
|
|
58
68
|
} from '../../../store/use-editor'
|
|
59
|
-
|
|
60
69
|
import useFacingPose from '../../../store/use-facing-pose'
|
|
70
|
+
import useInteractionScope from '../../../store/use-interaction-scope'
|
|
61
71
|
import { swallowNextClick } from '../../editor/node-arrow-handles'
|
|
62
72
|
import { CursorSphere } from '../shared/cursor-sphere'
|
|
63
73
|
import { DragBoundingBox } from '../shared/drag-bounding-box'
|
|
@@ -68,6 +78,7 @@ import {
|
|
|
68
78
|
type PointerSupportSurface,
|
|
69
79
|
resolvePointerSupportSurface,
|
|
70
80
|
} from '../shared/pointer-support-cap'
|
|
81
|
+
import { createItemSurfaceGridDispatch, createRegistryItemSurfaceMove } from './item-surface-move'
|
|
71
82
|
|
|
72
83
|
/** Snap a world-plan coordinate to the editor's active grid step (0.5 / 0.25
|
|
73
84
|
* / 0.1 / 0.05), read live so changing the step mid-drag takes effect. */
|
|
@@ -197,18 +208,6 @@ const ALIGNMENT_THRESHOLD_M = 0.08
|
|
|
197
208
|
/**
|
|
198
209
|
* Generic move tool for any registry-backed kind.
|
|
199
210
|
*
|
|
200
|
-
* Imperative-only motion during drag:
|
|
201
|
-
* - On every `grid:move` we mutate `sceneRegistry.nodes.get(id).position`
|
|
202
|
-
* directly. The node's store data is unchanged → the renderer doesn't
|
|
203
|
-
* re-render → R3F doesn't reapply `position={node.position}` → the
|
|
204
|
-
* imperative mutation sticks. Movement is smooth, framerate-locked,
|
|
205
|
-
* and React-free.
|
|
206
|
-
*
|
|
207
|
-
* Store update happens only on commit (single undoable action).
|
|
208
|
-
*
|
|
209
|
-
* Cancel imperatively snaps the mesh back to its original position and
|
|
210
|
-
* resumes history without ever having touched the store mid-drag.
|
|
211
|
-
*
|
|
212
211
|
* **Commit triggers**: the tool listens for `grid:click` and the generic
|
|
213
212
|
* `node:click` event. A click on the grid plane fires `grid:click`; a click
|
|
214
213
|
* on the moved node itself (or any other 3D geometry the ray happens to land
|
|
@@ -223,9 +222,19 @@ const ALIGNMENT_THRESHOLD_M = 0.08
|
|
|
223
222
|
*/
|
|
224
223
|
type ClickTriggerEvent = GridEvent | NodeEvent<AnyNode>
|
|
225
224
|
|
|
226
|
-
export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
225
|
+
export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
|
|
226
|
+
const node = useMemo(
|
|
227
|
+
() =>
|
|
228
|
+
isFreshPlacementMetadata(source.metadata)
|
|
229
|
+
? (useScene.getState().nodes[source.id] ?? source)
|
|
230
|
+
: source,
|
|
231
|
+
[source],
|
|
232
|
+
)
|
|
233
|
+
const itemSurfaceMove = useMemo(() => createRegistryItemSurfaceMove(node), [node])
|
|
234
|
+
const suppressRaycastsRef = useRef<(() => void) | null>(null)
|
|
227
235
|
const previewGroupRef = useRef<Group>(null)
|
|
228
236
|
useFrame(() => {
|
|
237
|
+
suppressRaycastsRef.current?.()
|
|
229
238
|
if (!previewGroupRef.current) return
|
|
230
239
|
const nodes = useScene.getState().nodes
|
|
231
240
|
const parentId = nodes[node.id]?.parentId ?? node.parentId
|
|
@@ -282,7 +291,6 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
282
291
|
return 0
|
|
283
292
|
}, [node])
|
|
284
293
|
const [cursorPosition, setCursorPosition] = useState<[number, number, number]>(originalPosition)
|
|
285
|
-
const previousSnapRef = useRef<string | null>(null)
|
|
286
294
|
/**
|
|
287
295
|
* The latest snapped cursor position from `grid:move`. We commit at
|
|
288
296
|
* THIS position regardless of which event variant fires the click —
|
|
@@ -362,20 +370,24 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
362
370
|
[collides, parentFrameCollides, resolvedFootprint],
|
|
363
371
|
)
|
|
364
372
|
const [valid, setValid] = useState(true)
|
|
373
|
+
const [surfaceRejection, setSurfaceRejection] = useState<SurfaceRejectReason | null>(null)
|
|
365
374
|
const previewRotationY = useCallback(
|
|
366
375
|
(rotationY = rotationRef.current) =>
|
|
367
376
|
parentFrame && frameParent
|
|
368
377
|
? parentFrame.parentRotationY(frameParent, useScene.getState().nodes) + rotationY
|
|
369
|
-
:
|
|
370
|
-
|
|
378
|
+
: itemSurfaceMove?.hosted
|
|
379
|
+
? itemSurfaceMove.planPose(lastCursorRef.current, rotationY).rotationY
|
|
380
|
+
: rotationY,
|
|
381
|
+
[parentFrame, frameParent, itemSurfaceMove],
|
|
371
382
|
)
|
|
372
383
|
const visualPositionFor = useCallback(
|
|
373
384
|
(position: [number, number, number], rotationY = rotationRef.current) => {
|
|
374
385
|
if (parentFrame && frameParent) {
|
|
375
386
|
return parentFrame.localToPlan(frameParent, position, useScene.getState().nodes)
|
|
376
387
|
}
|
|
388
|
+
if (itemSurfaceMove?.hosted) return itemSurfaceMove.planPose(position, rotationY).position
|
|
377
389
|
return getFloorStackPreviewPosition({
|
|
378
|
-
node,
|
|
390
|
+
node: useScene.getState().nodes[node.id] ?? node,
|
|
379
391
|
position,
|
|
380
392
|
rotation: (() => {
|
|
381
393
|
const r = (node as { rotation?: unknown }).rotation
|
|
@@ -386,7 +398,7 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
386
398
|
maxElevation: supportCapRef.current,
|
|
387
399
|
})
|
|
388
400
|
},
|
|
389
|
-
[parentFrame, frameParent, node],
|
|
401
|
+
[parentFrame, frameParent, node, itemSurfaceMove],
|
|
390
402
|
)
|
|
391
403
|
const canonicalPositionFromPlan = useCallback(
|
|
392
404
|
(planX: number, localY: number, planZ: number): [number, number, number] =>
|
|
@@ -431,8 +443,9 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
431
443
|
}, [])
|
|
432
444
|
|
|
433
445
|
useEffect(() => {
|
|
446
|
+
const ownsSubtree = useInteractionScope.getState().adoptSubtree(node.id)
|
|
434
447
|
useScene.temporal.getState().pause()
|
|
435
|
-
|
|
448
|
+
const movementSfx = createMovementSfxTick()
|
|
436
449
|
dragAnchorRef.current = null
|
|
437
450
|
hasMovedRef.current = false
|
|
438
451
|
rotationRef.current = originalRotationY
|
|
@@ -451,14 +464,22 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
451
464
|
// would keep the previous clone's rotation/position until the next R/T.
|
|
452
465
|
setCursorRotationY(previewRotationY(originalRotationY))
|
|
453
466
|
lastCursorRef.current = originalPosition
|
|
467
|
+
let floorY = originalPosition[1]
|
|
468
|
+
let centeredAfterHost = false
|
|
454
469
|
let committed = false
|
|
455
470
|
const isNew = isFreshPlacement
|
|
456
471
|
|
|
457
472
|
const baseRotation = (node as { rotation?: unknown }).rotation
|
|
458
|
-
const toCommitRotation = (y: number): number | [number, number, number] =>
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
:
|
|
473
|
+
const toCommitRotation = (y: number): number | [number, number, number] => {
|
|
474
|
+
const live = useScene.getState().nodes[node.id]
|
|
475
|
+
const rotation =
|
|
476
|
+
live && surfaceAttachmentId(live) && 'rotation' in live ? live.rotation : baseRotation
|
|
477
|
+
return Array.isArray(rotation) ? [rotation[0] ?? 0, y, rotation[2] ?? 0] : y
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const currentLevelId = () =>
|
|
481
|
+
useViewer.getState().selection.levelId ??
|
|
482
|
+
(itemSurfaceMove ? findLevelAncestorId(node.id, useScene.getState().nodes) : node.parentId)
|
|
462
483
|
|
|
463
484
|
const getVisualPosition = visualPositionFor
|
|
464
485
|
const applyMeshPose = (position: [number, number, number], rotationY = rotationRef.current) => {
|
|
@@ -469,7 +490,8 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
469
490
|
}
|
|
470
491
|
const markMovedNodeDirty = () => {
|
|
471
492
|
if (useScene.getState().nodes[node.id]) {
|
|
472
|
-
|
|
493
|
+
for (const id of cascadeDirty(node.id as AnyNodeId, { scene: createSceneApi(useScene) }))
|
|
494
|
+
useScene.getState().markDirty(id)
|
|
473
495
|
}
|
|
474
496
|
}
|
|
475
497
|
|
|
@@ -563,10 +585,18 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
563
585
|
// override so the user can drop on top of an existing item on purpose. Only
|
|
564
586
|
// shelves show the box, so this no-ops for every other movable kind.
|
|
565
587
|
const recomputeValidity = () => {
|
|
588
|
+
const rejection = itemSurfaceMove?.rejection ?? null
|
|
589
|
+
setSurfaceRejection(rejection)
|
|
590
|
+
if (rejection) {
|
|
591
|
+
validRef.current = false
|
|
592
|
+
setValid(false)
|
|
593
|
+
return
|
|
594
|
+
}
|
|
566
595
|
if (!boxDimensions && !movableValidityConfig) return
|
|
567
|
-
if (altRef.current) {
|
|
568
|
-
|
|
569
|
-
|
|
596
|
+
if (altRef.current || itemSurfaceMove?.hosted) {
|
|
597
|
+
const valid = !itemSurfaceMove?.hosted || itemSurfaceMove.valid
|
|
598
|
+
validRef.current = valid
|
|
599
|
+
setValid(valid)
|
|
570
600
|
return
|
|
571
601
|
}
|
|
572
602
|
if (parentFrameCollides && frameParent && parentFrame?.isValidPosition) {
|
|
@@ -584,7 +614,7 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
584
614
|
setValid(validPosition)
|
|
585
615
|
return
|
|
586
616
|
}
|
|
587
|
-
const levelId =
|
|
617
|
+
const levelId = currentLevelId()
|
|
588
618
|
if (!levelId) {
|
|
589
619
|
validRef.current = true
|
|
590
620
|
setValid(true)
|
|
@@ -594,7 +624,7 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
594
624
|
const liveRotation = previewRotationY(rotationRef.current)
|
|
595
625
|
const floorPlaced = nodeRegistry.get(node.type)?.capabilities?.floorPlaced
|
|
596
626
|
const effectiveNode = {
|
|
597
|
-
...(node as Record<string, unknown>),
|
|
627
|
+
...((useScene.getState().nodes[node.id] ?? node) as Record<string, unknown>),
|
|
598
628
|
position: livePosition,
|
|
599
629
|
rotation: Array.isArray((node as { rotation?: unknown }).rotation)
|
|
600
630
|
? [
|
|
@@ -651,10 +681,12 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
651
681
|
// passes through the moved mesh and continues to the grid plane,
|
|
652
682
|
// so `grid:move` keeps firing and the cursor tracks correctly.
|
|
653
683
|
// We restore the original raycast on cleanup.
|
|
654
|
-
const mesh = sceneRegistry.nodes.get(node.id)
|
|
655
684
|
const restoreRaycasts: Array<() => void> = []
|
|
656
|
-
|
|
657
|
-
|
|
685
|
+
const suppressed = new WeakSet<object>()
|
|
686
|
+
const suppressRaycasts = () => {
|
|
687
|
+
sceneRegistry.nodes.get(node.id)?.traverse((child) => {
|
|
688
|
+
if (suppressed.has(child)) return
|
|
689
|
+
suppressed.add(child)
|
|
658
690
|
const original = child.raycast
|
|
659
691
|
child.raycast = () => {}
|
|
660
692
|
restoreRaycasts.push(() => {
|
|
@@ -662,6 +694,9 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
662
694
|
})
|
|
663
695
|
})
|
|
664
696
|
}
|
|
697
|
+
// Reparenting remounts the renderer, so new meshes must also let the cursor ray through.
|
|
698
|
+
suppressRaycastsRef.current = suppressRaycasts
|
|
699
|
+
suppressRaycasts()
|
|
665
700
|
|
|
666
701
|
// Static alignment candidates — anchors of every OTHER alignable object
|
|
667
702
|
// (items, walls, fences, slabs, ceilings, columns) ON THE SAME LEVEL,
|
|
@@ -673,7 +708,7 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
673
708
|
const alignmentCandidates = collectAlignmentAnchors(
|
|
674
709
|
useScene.getState().nodes,
|
|
675
710
|
node.id,
|
|
676
|
-
|
|
711
|
+
currentLevelId(),
|
|
677
712
|
)
|
|
678
713
|
|
|
679
714
|
// Connectivity snapshot (existing port-bearing nodes only — fresh
|
|
@@ -686,7 +721,76 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
686
721
|
if (snapshot.connections.length > 0) connectivityRef.current = snapshot
|
|
687
722
|
}
|
|
688
723
|
|
|
724
|
+
const applySurfacePose = (pose: { position: [number, number, number]; rotationY: number }) => {
|
|
725
|
+
lastCursorRef.current = pose.position
|
|
726
|
+
rotationRef.current = pose.rotationY
|
|
727
|
+
freeRotationRef.current = pose.rotationY
|
|
728
|
+
attachmentRotationRef.current = null
|
|
729
|
+
centeredAfterHost = true
|
|
730
|
+
dragAnchorRef.current = null
|
|
731
|
+
hasMovedRef.current = true
|
|
732
|
+
revealFreshPlacement()
|
|
733
|
+
setCursorPosition(getVisualPosition(pose.position))
|
|
734
|
+
setCursorRotationY(previewRotationY(pose.rotationY))
|
|
735
|
+
useAlignmentGuides.getState().clear()
|
|
736
|
+
recomputeValidity()
|
|
737
|
+
}
|
|
738
|
+
const detachSurface = (
|
|
739
|
+
worldPosition: [number, number, number],
|
|
740
|
+
leaveEvent?: NodeEvent<AnyNode>,
|
|
741
|
+
) => {
|
|
742
|
+
const pose = leaveEvent
|
|
743
|
+
? itemSurfaceMove?.leave(leaveEvent, rotationRef.current)
|
|
744
|
+
: itemSurfaceMove?.detach(worldPosition, rotationRef.current)
|
|
745
|
+
if (!pose) return
|
|
746
|
+
floorY = 0
|
|
747
|
+
const pointed = resolvePointerSupportSurface(cameraRef.current, worldPosition)
|
|
748
|
+
supportCapRef.current = pointed?.elevation ?? null
|
|
749
|
+
supportSurfaceRef.current = pointed
|
|
750
|
+
applySurfacePose(pose)
|
|
751
|
+
applyMeshPose(pose.position)
|
|
752
|
+
}
|
|
753
|
+
let lastSurfaceEvent: NodeEvent<AnyNode> | null = null
|
|
754
|
+
const onItemMove = (event: NodeEvent<AnyNode>) => {
|
|
755
|
+
if (committed || !resolvedFootprint || !itemSurfaceMove) return
|
|
756
|
+
lastSurfaceEvent = event
|
|
757
|
+
const pose = itemSurfaceMove.enter(event, resolvedFootprint, rotationRef.current)
|
|
758
|
+
if (pose) {
|
|
759
|
+
applySurfacePose(pose)
|
|
760
|
+
// Compare floor and host movement in the same plan frame, never host-local storage.
|
|
761
|
+
const point = new Vector3(...pose.worldPosition)
|
|
762
|
+
const levelId = currentLevelId()
|
|
763
|
+
if (levelId) sceneRegistry.nodes.get(levelId)?.worldToLocal(point)
|
|
764
|
+
movementSfx.tick({
|
|
765
|
+
coords: [point.x, point.z],
|
|
766
|
+
gridSnapActive: isGridSnapActive(),
|
|
767
|
+
gridStep: useEditor.getState().gridSnapStep,
|
|
768
|
+
})
|
|
769
|
+
} else {
|
|
770
|
+
if (
|
|
771
|
+
itemSurfaceMove.hosted &&
|
|
772
|
+
(itemSurfaceMove.rejection === 'footprint-outside-surface' ||
|
|
773
|
+
itemSurfaceMove.rejection === 'footprint-exceeds-host' ||
|
|
774
|
+
itemSurfaceMove.rejection === 'no-surface')
|
|
775
|
+
)
|
|
776
|
+
detachSurface(event.position)
|
|
777
|
+
recomputeValidity()
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
const onItemLeave = (event: NodeEvent<AnyNode>) => {
|
|
781
|
+
if (!itemSurfaceMove?.hosted || committed) return
|
|
782
|
+
if (event.node.id !== useScene.getState().nodes[node.id]?.parentId) return
|
|
783
|
+
event.stopPropagation()
|
|
784
|
+
detachSurface(event.position, event)
|
|
785
|
+
}
|
|
786
|
+
|
|
689
787
|
const onGridMove = (event: GridEvent) => {
|
|
788
|
+
const blocked = Boolean(itemSurfaceMove?.blocksGrid(event, cameraRef.current))
|
|
789
|
+
if (!committed && !blocked) {
|
|
790
|
+
itemSurfaceMove?.clearRejectionForGrid(event)
|
|
791
|
+
detachSurface(event.position)
|
|
792
|
+
}
|
|
793
|
+
if (committed || blocked) return
|
|
690
794
|
// The pointer decides the target surface AND the cursor plan point,
|
|
691
795
|
// both resolved from the true camera ray in one place. The event's
|
|
692
796
|
// own hit can't be used directly: its plane rides at the ghost's
|
|
@@ -707,7 +811,7 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
707
811
|
|
|
708
812
|
const magnetic = isMagneticSnapActive()
|
|
709
813
|
const attachmentEnabled = magnetic || isGridSnapActive()
|
|
710
|
-
const absolute = useAbsoluteCursorPlacement || cursorAttached
|
|
814
|
+
const absolute = useAbsoluteCursorPlacement || cursorAttached || centeredAfterHost
|
|
711
815
|
const centerOffset: [number, number, number] =
|
|
712
816
|
absolute && dragBounds?.center
|
|
713
817
|
? offsetPlanPositionByLocalCenter(
|
|
@@ -734,16 +838,13 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
734
838
|
// Kind-owned grid hooks exchange origins and apply their own footprint offsets.
|
|
735
839
|
candidatePosition: canonicalPositionFromPlan(
|
|
736
840
|
planX - centerOffset[0],
|
|
737
|
-
|
|
841
|
+
floorY,
|
|
738
842
|
planZ - centerOffset[2],
|
|
739
843
|
),
|
|
740
844
|
candidateRotation: freeRotationRef.current,
|
|
741
845
|
movingIds: [node.id as AnyNodeId],
|
|
742
846
|
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,
|
|
847
|
+
levelId: (currentLevelId() as AnyNodeId | null) ?? null,
|
|
747
848
|
gridStep: useEditor.getState().gridSnapStep,
|
|
748
849
|
})
|
|
749
850
|
const snappedPlanPosition = getVisualPosition(
|
|
@@ -761,15 +862,12 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
761
862
|
? ([planX, planZ]) => {
|
|
762
863
|
const snapArgs: Parameters<NonNullable<typeof groupMoveSnapPoseConfig>>[0] = {
|
|
763
864
|
node,
|
|
764
|
-
candidatePosition: canonicalPositionFromPlan(planX,
|
|
865
|
+
candidatePosition: canonicalPositionFromPlan(planX, floorY, planZ),
|
|
765
866
|
candidateRotation:
|
|
766
867
|
absolute && dragBounds?.center ? freeRotationRef.current : rotationRef.current,
|
|
767
868
|
movingIds: [node.id as AnyNodeId],
|
|
768
869
|
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,
|
|
870
|
+
levelId: (currentLevelId() as AnyNodeId | null) ?? null,
|
|
773
871
|
}
|
|
774
872
|
const snappedPosition: GroupMoveSnapResult | null = groupMoveSnapPoseConfig
|
|
775
873
|
? groupMoveSnapPoseConfig(snapArgs)
|
|
@@ -845,7 +943,7 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
845
943
|
}
|
|
846
944
|
}
|
|
847
945
|
|
|
848
|
-
let position = canonicalPositionFromPlan(x,
|
|
946
|
+
let position = canonicalPositionFromPlan(x, floorY, z)
|
|
849
947
|
if (
|
|
850
948
|
!attachmentSnapped &&
|
|
851
949
|
(magnetic || isGridSnapActive()) &&
|
|
@@ -885,7 +983,7 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
885
983
|
if (!parentFrame && pointed?.sourceNodeId) {
|
|
886
984
|
const rotation = toCommitRotation(rotationRef.current)
|
|
887
985
|
const effectiveNode = {
|
|
888
|
-
...(node as Record<string, unknown>),
|
|
986
|
+
...((useScene.getState().nodes[node.id] ?? node) as Record<string, unknown>),
|
|
889
987
|
position,
|
|
890
988
|
rotation,
|
|
891
989
|
} as AnyNode
|
|
@@ -930,15 +1028,26 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
930
1028
|
// Carry connected ductwork along (preview only — committed on drop).
|
|
931
1029
|
previewConnectivity(position, rotationRef.current)
|
|
932
1030
|
|
|
933
|
-
const
|
|
1031
|
+
const soundStep = {
|
|
934
1032
|
coords: [x, z],
|
|
935
1033
|
gridSnapActive: isGridSnapActive() && !attachmentSnapped,
|
|
936
1034
|
gridStep: useEditor.getState().gridSnapStep,
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
1035
|
+
}
|
|
1036
|
+
if (itemSurfaceMove) {
|
|
1037
|
+
movementSfx.schedule(
|
|
1038
|
+
soundStep,
|
|
1039
|
+
() => !itemSurfaceMove.blocksGrid(event, cameraRef.current),
|
|
1040
|
+
true,
|
|
1041
|
+
)
|
|
1042
|
+
} else movementSfx.tick(soundStep, true)
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
const gridDispatch = createItemSurfaceGridDispatch(onGridMove)
|
|
1046
|
+
const receiveGridMove = (event: GridEvent) => {
|
|
1047
|
+
if (itemSurfaceMove?.hosted) gridDispatch.schedule(event)
|
|
1048
|
+
else {
|
|
1049
|
+
gridDispatch.cancel()
|
|
1050
|
+
onGridMove(event)
|
|
942
1051
|
}
|
|
943
1052
|
}
|
|
944
1053
|
|
|
@@ -967,6 +1076,9 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
967
1076
|
// finds the fresh draft already deleted and takes the orphan re-create
|
|
968
1077
|
// path below, minting a hidden ghost copy and replaying the SFX.
|
|
969
1078
|
if (committed) return
|
|
1079
|
+
gridDispatch.flush()
|
|
1080
|
+
movementSfx.flush()
|
|
1081
|
+
if (itemSurfaceMove?.rejection || (itemSurfaceMove?.hosted && !itemSurfaceMove.valid)) return
|
|
970
1082
|
// Ignore a commit that fires before the cursor has moved into place —
|
|
971
1083
|
// it's the stray trailing click of whatever armed this move, not a
|
|
972
1084
|
// deliberate drop. Prevents preset re-arm from double-placing.
|
|
@@ -982,11 +1094,12 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
982
1094
|
|
|
983
1095
|
if (useScene.getState().nodes[node.id]) {
|
|
984
1096
|
const effectiveNode = {
|
|
985
|
-
...(node as Record<string, unknown>),
|
|
1097
|
+
...((useScene.getState().nodes[node.id] ?? node) as Record<string, unknown>),
|
|
986
1098
|
position,
|
|
987
1099
|
rotation,
|
|
988
1100
|
} as AnyNode
|
|
989
1101
|
const data = {
|
|
1102
|
+
...(itemSurfaceMove ? { parentId: effectiveNode.parentId } : {}),
|
|
990
1103
|
position,
|
|
991
1104
|
rotation,
|
|
992
1105
|
// The pointer cap makes the persisted host reproduce the capped
|
|
@@ -1013,11 +1126,13 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
1013
1126
|
} as Partial<AnyNode>
|
|
1014
1127
|
|
|
1015
1128
|
if (isNew) {
|
|
1016
|
-
const finalId = commitFreshPlacementSubtree(node.id as AnyNodeId, data)
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1129
|
+
const finalId = commitFreshPlacementSubtree(node.id as AnyNodeId, data, (reason) => {
|
|
1130
|
+
setSurfaceRejection(reason)
|
|
1131
|
+
setValid(false)
|
|
1132
|
+
})
|
|
1133
|
+
if (!finalId) return
|
|
1134
|
+
committed = true
|
|
1135
|
+
committedId = finalId
|
|
1021
1136
|
} else {
|
|
1022
1137
|
// Fold the connected-ductwork follow-updates into the SAME
|
|
1023
1138
|
// batch as the moved node so the whole thing is one undo step.
|
|
@@ -1027,10 +1142,16 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
1027
1142
|
buildPreviewNode(position, rotationRef.current),
|
|
1028
1143
|
).filter((u) => useScene.getState().nodes[u.id])
|
|
1029
1144
|
: []
|
|
1145
|
+
const surfaceId = surfaceAttachmentId(effectiveNode)
|
|
1146
|
+
itemSurfaceMove?.restore()
|
|
1030
1147
|
useScene.temporal.getState().resume()
|
|
1031
1148
|
useScene
|
|
1032
1149
|
.getState()
|
|
1033
|
-
.updateNodes([
|
|
1150
|
+
.updateNodes([
|
|
1151
|
+
{ id: node.id as AnyNodeId, data },
|
|
1152
|
+
...surfaceAttachmentUpdates(node.id, effectiveNode.parentId, surfaceId),
|
|
1153
|
+
...connectivityUpdates,
|
|
1154
|
+
])
|
|
1034
1155
|
// Kind-owned derived-state maintenance after a parent-frame move
|
|
1035
1156
|
// (cabinet run re-flow + linked corner-run re-anchor). Runs in the
|
|
1036
1157
|
// resumed window so its writes are undoable alongside the move.
|
|
@@ -1144,8 +1265,10 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
1144
1265
|
)
|
|
1145
1266
|
if (nextFreeRotation === null) return
|
|
1146
1267
|
sfxEmitter.emit('sfx:item-rotate')
|
|
1147
|
-
|
|
1268
|
+
const hostedPose = itemSurfaceMove?.rotate(nextFreeRotation)
|
|
1269
|
+
let position = hostedPose?.position ?? lastCursorRef.current
|
|
1148
1270
|
if (
|
|
1271
|
+
!hostedPose &&
|
|
1149
1272
|
hasMovedRef.current &&
|
|
1150
1273
|
(useAbsoluteCursorPlacement || cursorAttached) &&
|
|
1151
1274
|
dragBounds?.center
|
|
@@ -1161,25 +1284,28 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
1161
1284
|
previewRotationY(nextFreeRotation),
|
|
1162
1285
|
)
|
|
1163
1286
|
position = canonicalPositionFromPlan(planOrigin[0], position[1], planOrigin[2])
|
|
1164
|
-
lastCursorRef.current = position
|
|
1165
1287
|
}
|
|
1166
|
-
|
|
1288
|
+
lastCursorRef.current = position
|
|
1289
|
+
freeRotationRef.current = hostedPose?.rotationY ?? nextFreeRotation
|
|
1167
1290
|
rotationRef.current = freeRotationRef.current
|
|
1168
1291
|
setCursorRotationY(previewRotationY(rotationRef.current))
|
|
1169
1292
|
const visualPosition = getVisualPosition(position)
|
|
1170
1293
|
setCursorPosition(visualPosition)
|
|
1171
1294
|
applyMeshPose(position)
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1295
|
+
if (!itemSurfaceMove?.hosted)
|
|
1296
|
+
useLiveTransforms.getState().set(node.id, {
|
|
1297
|
+
position,
|
|
1298
|
+
rotation: rotationRef.current,
|
|
1299
|
+
supportElevationCap: supportCapRef.current ?? undefined,
|
|
1300
|
+
})
|
|
1177
1301
|
syncParentFramePreview(position)
|
|
1178
1302
|
markMovedNodeDirty()
|
|
1179
1303
|
// Rotating the fitting swings its collars — connected ducts follow.
|
|
1180
1304
|
previewConnectivity(position, rotationRef.current)
|
|
1181
1305
|
// Rotation changes the footprint's collision span — re-check validity.
|
|
1182
|
-
|
|
1306
|
+
if (!itemSurfaceMove?.hosted && itemSurfaceMove?.rejection && lastSurfaceEvent)
|
|
1307
|
+
onItemMove(lastSurfaceEvent)
|
|
1308
|
+
else recomputeValidity()
|
|
1183
1309
|
}
|
|
1184
1310
|
const onKeyUp = (e: KeyboardEvent) => {
|
|
1185
1311
|
if (e.key === 'Alt') {
|
|
@@ -1190,7 +1316,16 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
1190
1316
|
window.addEventListener('keydown', onKeyDown)
|
|
1191
1317
|
window.addEventListener('keyup', onKeyUp)
|
|
1192
1318
|
|
|
1193
|
-
|
|
1319
|
+
const surfaceKinds = Array.from(nodeRegistry.entries(), ([kind]) => kind).filter(
|
|
1320
|
+
(kind) => !NON_PHYSICAL_HOST_KINDS.includes(kind),
|
|
1321
|
+
)
|
|
1322
|
+
for (const kind of surfaceKinds) {
|
|
1323
|
+
emitter.on(`${kind}:enter` as never, onItemMove)
|
|
1324
|
+
emitter.on(`${kind}:move` as never, onItemMove)
|
|
1325
|
+
emitter.on(`${kind}:leave` as never, onItemLeave)
|
|
1326
|
+
emitter.on(`${kind}:click` as never, commitAtCursor)
|
|
1327
|
+
}
|
|
1328
|
+
emitter.on('grid:move', receiveGridMove)
|
|
1194
1329
|
emitter.on('grid:click', commitAtCursor)
|
|
1195
1330
|
|
|
1196
1331
|
const onPlacementDragPointerUp = (event: PointerEvent) => {
|
|
@@ -1211,12 +1346,16 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
1211
1346
|
emitter.on('node:click', commitAtCursor)
|
|
1212
1347
|
|
|
1213
1348
|
const onCancel = () => {
|
|
1349
|
+
movementSfx.cancel()
|
|
1350
|
+
gridDispatch.cancel()
|
|
1214
1351
|
useLiveTransforms.getState().clear(node.id)
|
|
1215
1352
|
clearConnectivityOverrides()
|
|
1216
1353
|
clearParentFramePreview()
|
|
1217
1354
|
if (isNew) {
|
|
1218
|
-
|
|
1355
|
+
updateSurfaceNode(node.id, {}, null)
|
|
1356
|
+
if (!ownsSubtree) useScene.getState().deleteNode(node.id)
|
|
1219
1357
|
} else {
|
|
1358
|
+
itemSurfaceMove?.restore()
|
|
1220
1359
|
applyMeshPose(originalPosition, originalRotationY)
|
|
1221
1360
|
markMovedNodeDirty()
|
|
1222
1361
|
}
|
|
@@ -1228,9 +1367,18 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
1228
1367
|
emitter.on('tool:cancel', onCancel)
|
|
1229
1368
|
|
|
1230
1369
|
return () => {
|
|
1370
|
+
movementSfx.cancel()
|
|
1371
|
+
gridDispatch.cancel()
|
|
1231
1372
|
window.removeEventListener('keydown', onKeyDown)
|
|
1232
1373
|
window.removeEventListener('keyup', onKeyUp)
|
|
1233
|
-
|
|
1374
|
+
suppressRaycastsRef.current = null
|
|
1375
|
+
for (const kind of surfaceKinds) {
|
|
1376
|
+
emitter.off(`${kind}:enter` as never, onItemMove)
|
|
1377
|
+
emitter.off(`${kind}:move` as never, onItemMove)
|
|
1378
|
+
emitter.off(`${kind}:leave` as never, onItemLeave)
|
|
1379
|
+
emitter.off(`${kind}:click` as never, commitAtCursor)
|
|
1380
|
+
}
|
|
1381
|
+
emitter.off('grid:move', receiveGridMove)
|
|
1234
1382
|
emitter.off('grid:click', commitAtCursor)
|
|
1235
1383
|
window.removeEventListener('pointerup', onPlacementDragPointerUp)
|
|
1236
1384
|
emitter.off('node:click', commitAtCursor)
|
|
@@ -1242,10 +1390,11 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
1242
1390
|
// unmount / commit paths uniformly.
|
|
1243
1391
|
useAlignmentGuides.getState().clear()
|
|
1244
1392
|
const finalisedBy2D = useEditor.getState().movingNodeOrigin === '2d'
|
|
1245
|
-
if (!
|
|
1393
|
+
if (!committed && !finalisedBy2D && (!isNew || (!ownsSubtree && itemSurfaceMove))) {
|
|
1246
1394
|
useLiveTransforms.getState().clear(node.id)
|
|
1247
1395
|
clearConnectivityOverrides()
|
|
1248
1396
|
clearParentFramePreview()
|
|
1397
|
+
itemSurfaceMove?.restore()
|
|
1249
1398
|
applyMeshPose(originalPosition, originalRotationY)
|
|
1250
1399
|
markMovedNodeDirty()
|
|
1251
1400
|
}
|
|
@@ -1253,6 +1402,8 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
1253
1402
|
}
|
|
1254
1403
|
}, [
|
|
1255
1404
|
boxDimensions,
|
|
1405
|
+
resolvedFootprint,
|
|
1406
|
+
itemSurfaceMove,
|
|
1256
1407
|
dragBounds,
|
|
1257
1408
|
canonicalPositionFromPlan,
|
|
1258
1409
|
parentFrame,
|
|
@@ -1320,7 +1471,15 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
1320
1471
|
<DragBoundingBox
|
|
1321
1472
|
center={dragBounds?.center}
|
|
1322
1473
|
centerY={dragBounds?.centerY}
|
|
1323
|
-
color={
|
|
1474
|
+
color={
|
|
1475
|
+
surfaceRejection
|
|
1476
|
+
? INVALID_COLOR
|
|
1477
|
+
: boxDimensions
|
|
1478
|
+
? valid
|
|
1479
|
+
? VALID_COLOR
|
|
1480
|
+
: INVALID_COLOR
|
|
1481
|
+
: undefined
|
|
1482
|
+
}
|
|
1324
1483
|
nodeId={node.id}
|
|
1325
1484
|
position={cursorPosition}
|
|
1326
1485
|
rotationY={cursorRotationY}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { afterAll, afterEach, expect, spyOn, test } from 'bun:test'
|
|
2
|
+
import { type AnyNodeDefinition, nodeRegistry } from '@pascal-app/core'
|
|
3
|
+
import { getRegistryAffordanceTool, preloadRegistryAffordanceTools } from './affordance-dispatch'
|
|
4
|
+
|
|
5
|
+
const get = spyOn(nodeRegistry, 'get')
|
|
6
|
+
afterEach(() => get.mockReset())
|
|
7
|
+
afterAll(() => get.mockRestore())
|
|
8
|
+
|
|
9
|
+
test('a preloaded drag tool mounts directly without first-gesture suspense', async () => {
|
|
10
|
+
const Tool = () => null
|
|
11
|
+
const loader = async () => ({ default: Tool })
|
|
12
|
+
get.mockReturnValue({ affordanceTools: { move: loader } } as unknown as AnyNodeDefinition)
|
|
13
|
+
await preloadRegistryAffordanceTools('wall')
|
|
14
|
+
expect(getRegistryAffordanceTool('wall', 'move')).toBe(Tool)
|
|
15
|
+
expect(getRegistryAffordanceTool('wall', 'move')).toBe(Tool)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('preloading does not change an already mounted lazy component identity', async () => {
|
|
19
|
+
const loader = async () => ({ default: () => null })
|
|
20
|
+
get.mockReturnValue({ affordanceTools: { move: loader } } as unknown as AnyNodeDefinition)
|
|
21
|
+
const mounted = getRegistryAffordanceTool('wall', 'move')
|
|
22
|
+
await preloadRegistryAffordanceTools('wall')
|
|
23
|
+
expect(getRegistryAffordanceTool('wall', 'move')).toBe(mounted)
|
|
24
|
+
})
|