@pascal-app/editor 1.0.1 → 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 +10 -5
- package/src/components/editor/custom-camera-controls.tsx +22 -74
- package/src/components/editor/editor-layout-mobile.tsx +1 -0
- package/src/components/editor/floorplan-panel.tsx +210 -163
- 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/use-handle-drag.ts +75 -6
- package/src/components/editor/index.tsx +24 -9
- package/src/components/editor/node-action-menu.tsx +2 -0
- package/src/components/editor/node-arrow-handles.tsx +141 -7
- package/src/components/editor/registry-action-contributions.tsx +53 -0
- 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-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 +67 -21
- 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 +3 -9
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +3 -9
- package/src/components/tools/item/placement-strategies.ts +55 -43
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +3 -3
- package/src/components/tools/item/use-draft-node.ts +41 -2
- package/src/components/tools/item/use-placement-coordinator.tsx +166 -73
- package/src/components/tools/registry/item-surface-move.test.ts +10 -2
- package/src/components/tools/registry/item-surface-move.ts +46 -24
- package/src/components/tools/registry/item-surface-resolver.test.ts +2 -2
- package/src/components/tools/registry/move-registry-node-tool.tsx +23 -9
- 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 +17 -5
- package/src/components/tools/shared/surface-rejection.test.tsx +5 -4
- package/src/components/tools/shared/surface-rejection.tsx +14 -0
- package/src/components/tools/tool-manager.tsx +36 -3
- 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/slider-control.tsx +76 -31
- 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/parametric-inspector.tsx +3 -15
- 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 +585 -369
- 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.tsx +1 -1
- 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-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 +1 -0
- package/src/hooks/use-ceiling-events.ts +2 -0
- 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 +42 -29
- package/src/index.tsx +63 -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/floorplan/floorplan-export.test.ts +3 -1
- package/src/lib/floorplan/floorplan-extension.ts +16 -0
- package/src/lib/fresh-planar-placement.ts +168 -36
- package/src/lib/glb-export.test.ts +59 -16
- package/src/lib/glb-export.ts +26 -13
- 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/material-catalog-model.ts +72 -0
- package/src/lib/material-paint-panel-model.ts +76 -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/scene.test.ts +99 -0
- package/src/lib/scene.ts +21 -3
- 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/terrain-panel-model.ts +117 -0
- 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-interaction-scope.ts +155 -6
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
type AnyNode,
|
|
5
5
|
type AnyNodeId,
|
|
6
6
|
type CeilingEvent,
|
|
7
|
+
canHostSurfaceChild,
|
|
7
8
|
collectAlignmentAnchors,
|
|
8
9
|
emitter,
|
|
9
10
|
findLevelAncestorId,
|
|
@@ -48,6 +49,7 @@ import {
|
|
|
48
49
|
} from '../../../lib/active-placement-surface'
|
|
49
50
|
import { EDITOR_LAYER } from '../../../lib/constants'
|
|
50
51
|
import { formatLinearMeasurement } from '../../../lib/measurements'
|
|
52
|
+
import { isFreshPlacementMetadata } from '../../../lib/placement-metadata'
|
|
51
53
|
import { createMovementSfxTick } from '../../../lib/sfx/movement-tick'
|
|
52
54
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
53
55
|
import {
|
|
@@ -80,11 +82,13 @@ import {
|
|
|
80
82
|
} from '../shared/placement-box-geometry'
|
|
81
83
|
import {
|
|
82
84
|
type PointerSupportSurface,
|
|
83
|
-
resolvePointerSupportElevation,
|
|
84
85
|
resolvePointerSupportSurface,
|
|
85
86
|
} from '../shared/pointer-support-cap'
|
|
86
87
|
import { createShelfStickiness } from '../shared/shelf-stickiness'
|
|
87
|
-
import {
|
|
88
|
+
import {
|
|
89
|
+
createSurfaceEventOwnership,
|
|
90
|
+
createSurfaceRejectionFeedback,
|
|
91
|
+
} from '../shared/surface-rejection'
|
|
88
92
|
import { shouldCreateFloorDraft } from './draft-creation'
|
|
89
93
|
import { commitFaceHostClick, resolveFaceHostPreviewCommit } from './face-host-commit'
|
|
90
94
|
import {
|
|
@@ -597,6 +601,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
597
601
|
wasAdopted: boolean,
|
|
598
602
|
repeat: () => void,
|
|
599
603
|
) => {
|
|
604
|
+
if (!committedId) return
|
|
600
605
|
if (configRef.current.onCommitted()) {
|
|
601
606
|
repeat()
|
|
602
607
|
return
|
|
@@ -626,12 +631,19 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
626
631
|
) => {
|
|
627
632
|
const draftId = draftNode.current?.id ?? null
|
|
628
633
|
const wasAdopted = draftNode.isAdopted
|
|
629
|
-
const finalId = draftNode.commit(nodeUpdate,
|
|
630
|
-
|
|
634
|
+
const finalId = draftNode.commit(nodeUpdate, {
|
|
635
|
+
...options,
|
|
636
|
+
onReject: (reason) => {
|
|
637
|
+
feedback.reject(reason)
|
|
638
|
+
edgeMaterial.color.setHex(0xef_44_44)
|
|
639
|
+
basePlaneMaterial.color.setHex(0xef_44_44)
|
|
640
|
+
},
|
|
641
|
+
})
|
|
642
|
+
if (finalId && draftId) {
|
|
631
643
|
useLiveTransforms.getState().clear(draftId)
|
|
632
644
|
useLiveNodeOverrides.getState().clearFields(draftId, faceHostClearFields(draftNode.current))
|
|
633
645
|
}
|
|
634
|
-
return { committedId: finalId
|
|
646
|
+
return { committedId: finalId, wasAdopted }
|
|
635
647
|
}
|
|
636
648
|
|
|
637
649
|
const faceHostClearFields = (draft: ItemNode | null | undefined): Array<keyof ItemNode> => {
|
|
@@ -651,6 +663,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
651
663
|
}
|
|
652
664
|
|
|
653
665
|
const surfacePointer = createItemSurfacePointerArbitration()
|
|
666
|
+
const surfaceOwnership = createSurfaceEventOwnership()
|
|
654
667
|
const revalidate = (): boolean => {
|
|
655
668
|
const fits = checkCanPlace(getContext(), validators)
|
|
656
669
|
if (
|
|
@@ -681,11 +694,22 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
681
694
|
return placeable
|
|
682
695
|
}
|
|
683
696
|
|
|
684
|
-
const surfaceContext = (event: NodeEvent<AnyNode
|
|
697
|
+
const surfaceContext = (event: NodeEvent<AnyNode>, generic = false) => {
|
|
685
698
|
lastSurfaceEvent = event
|
|
686
699
|
return {
|
|
687
700
|
...getContext(),
|
|
688
701
|
onSurfaceReject: (reason: SurfaceRejectReason) => {
|
|
702
|
+
// A rejected generic acquisition leaves the paired pointer's floor route in charge.
|
|
703
|
+
if (
|
|
704
|
+
generic &&
|
|
705
|
+
placementState.current.surfaceItemId !== event.node.id &&
|
|
706
|
+
reason !== 'surface-cutout' &&
|
|
707
|
+
reason !== 'surface-occupied'
|
|
708
|
+
) {
|
|
709
|
+
return
|
|
710
|
+
}
|
|
711
|
+
surfaceOwnership.claim(event.node.id, event.nativeEvent.nativeEvent ?? event.nativeEvent)
|
|
712
|
+
event.stopPropagation()
|
|
689
713
|
if (
|
|
690
714
|
(reason === 'footprint-outside-surface' ||
|
|
691
715
|
reason === 'footprint-exceeds-host' ||
|
|
@@ -695,7 +719,9 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
695
719
|
) {
|
|
696
720
|
feedback.clear()
|
|
697
721
|
detachItemSurfaceToFloor(event as ItemEvent)
|
|
698
|
-
|
|
722
|
+
// A generic host exit yields positioning to this pointer's paired floor event.
|
|
723
|
+
if (!generic)
|
|
724
|
+
surfacePointer.hit(event.node.id, event.nativeEvent.nativeEvent ?? event.nativeEvent)
|
|
699
725
|
return
|
|
700
726
|
}
|
|
701
727
|
feedback.reject(reason, event.nativeEvent.nativeEvent ?? event.nativeEvent)
|
|
@@ -746,6 +772,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
746
772
|
|
|
747
773
|
const applyTransition = (result: TransitionResult, event?: NodeEvent<AnyNode>) => {
|
|
748
774
|
if (event) {
|
|
775
|
+
surfaceOwnership.claim(event.node.id, event.nativeEvent.nativeEvent ?? event.nativeEvent)
|
|
749
776
|
surfacePointer.hit(event.node.id, event.nativeEvent.nativeEvent ?? event.nativeEvent)
|
|
750
777
|
tickSurfaceMovementSfx(result.cursorPosition)
|
|
751
778
|
}
|
|
@@ -1054,7 +1081,9 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1054
1081
|
// perspective-skewed along the ray whenever the plane sits on a
|
|
1055
1082
|
// different storey than the pointed surface (the skew is what made
|
|
1056
1083
|
// a drag over a deck-above-a-floor hop between the two surfaces).
|
|
1057
|
-
const pointed = resolvePointerSupportSurface(cameraRef.current, event.position
|
|
1084
|
+
const pointed = resolvePointerSupportSurface(cameraRef.current, event.position, {
|
|
1085
|
+
pointerRay: event.nativeEvent.ray,
|
|
1086
|
+
})
|
|
1058
1087
|
pointerSupportCapRef.current = pointed?.elevation ?? null
|
|
1059
1088
|
pointerSupportSurfaceRef.current = pointed
|
|
1060
1089
|
const surfaceEvent: GridEvent =
|
|
@@ -1732,6 +1761,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1732
1761
|
if (draft) useScene.getState().updateNode(draft.id, result.nodeUpdate)
|
|
1733
1762
|
} else {
|
|
1734
1763
|
draftNode.destroy()
|
|
1764
|
+
gridPosition.current.set(...result.gridPosition)
|
|
1735
1765
|
Object.assign(placementState.current, result.stateUpdate)
|
|
1736
1766
|
}
|
|
1737
1767
|
}
|
|
@@ -1744,11 +1774,10 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1744
1774
|
// Landing back on the floor: refresh the pointer surface cap from
|
|
1745
1775
|
// this event's world hit so the first floor position already targets
|
|
1746
1776
|
// the aimed-at surface (not a deck above it).
|
|
1747
|
-
pointerSupportCapRef.current =
|
|
1748
|
-
event.position
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
])
|
|
1777
|
+
pointerSupportCapRef.current =
|
|
1778
|
+
resolvePointerSupportSurface(cameraRef.current, event.position, {
|
|
1779
|
+
pointerRay: event.nativeEvent.ray,
|
|
1780
|
+
})?.elevation ?? null
|
|
1752
1781
|
// Coming back from a host: forget the floor grab too, so the item
|
|
1753
1782
|
// centers under the cursor instead of restoring the pre-drag offset —
|
|
1754
1783
|
// and landing on the floor is "anchoring elsewhere", so a later return
|
|
@@ -1823,7 +1852,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1823
1852
|
revalidate()
|
|
1824
1853
|
}
|
|
1825
1854
|
|
|
1826
|
-
const onItemEnter = (event: NodeEvent<AnyNode
|
|
1855
|
+
const onItemEnter = (event: NodeEvent<AnyNode>, generic = false) => {
|
|
1827
1856
|
if (event.node.type === 'cabinet' && placementState.current.surfaceItemId === event.node.id) {
|
|
1828
1857
|
onItemMove(event)
|
|
1829
1858
|
return
|
|
@@ -1834,7 +1863,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1834
1863
|
}
|
|
1835
1864
|
if (event.node.id === draftNode.current?.id) return
|
|
1836
1865
|
has3DPointerDrivenMoveRef.current = true
|
|
1837
|
-
const result = itemSurfaceStrategy.enter(surfaceContext(event), event)
|
|
1866
|
+
const result = itemSurfaceStrategy.enter(surfaceContext(event, generic), event)
|
|
1838
1867
|
if (!result) return
|
|
1839
1868
|
feedback.clear()
|
|
1840
1869
|
|
|
@@ -1849,12 +1878,12 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1849
1878
|
}
|
|
1850
1879
|
}
|
|
1851
1880
|
|
|
1852
|
-
const onItemMove = (event: NodeEvent<AnyNode
|
|
1881
|
+
const onItemMove = (event: NodeEvent<AnyNode>, generic = false) => {
|
|
1853
1882
|
if (event.node.id === draftNode.current?.id) return
|
|
1854
1883
|
releaseCommit = () => onItemClick(event)
|
|
1855
1884
|
has3DPointerDrivenMoveRef.current = true
|
|
1856
1885
|
if (!cursorGroupRef.current) return
|
|
1857
|
-
const ctx = surfaceContext(event)
|
|
1886
|
+
const ctx = surfaceContext(event, generic)
|
|
1858
1887
|
if (event.node.type === 'cabinet') {
|
|
1859
1888
|
lastRawPos.current.set(...event.position)
|
|
1860
1889
|
surfacePointer.hit(event.node.id, event.nativeEvent.nativeEvent ?? event.nativeEvent)
|
|
@@ -1879,6 +1908,8 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1879
1908
|
event,
|
|
1880
1909
|
)
|
|
1881
1910
|
|
|
1911
|
+
// An unclaimed generic mesh must let its owning ancestor receive the same hit.
|
|
1912
|
+
if (generic && !enterResult) return
|
|
1882
1913
|
event.stopPropagation()
|
|
1883
1914
|
if (enterResult) {
|
|
1884
1915
|
applyTransition(enterResult, event)
|
|
@@ -1892,7 +1923,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1892
1923
|
}
|
|
1893
1924
|
|
|
1894
1925
|
if (!draftNode.current) {
|
|
1895
|
-
const enterResult = itemSurfaceStrategy.enter(surfaceContext(event), event)
|
|
1926
|
+
const enterResult = itemSurfaceStrategy.enter(surfaceContext(event, generic), event)
|
|
1896
1927
|
if (!enterResult) return
|
|
1897
1928
|
event.stopPropagation()
|
|
1898
1929
|
ensureDraft(enterResult)
|
|
@@ -2007,12 +2038,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2007
2038
|
// the host's own click event is blocked by the cursor preview.
|
|
2008
2039
|
if (ctx.state.surface === 'item-surface' && ctx.state.surfaceItemId) {
|
|
2009
2040
|
const hostNode = useScene.getState().nodes[ctx.state.surfaceItemId as AnyNodeId]
|
|
2010
|
-
if (
|
|
2011
|
-
hostNode &&
|
|
2012
|
-
(hostNode.type === 'item' ||
|
|
2013
|
-
hostNode.type === 'cabinet' ||
|
|
2014
|
-
hostNode.type === 'procedural-item')
|
|
2015
|
-
) {
|
|
2041
|
+
if (hostNode) {
|
|
2016
2042
|
const synthetic = { ...event, node: hostNode } as ItemEvent
|
|
2017
2043
|
const result = itemSurfaceStrategy.click(ctx, synthetic)
|
|
2018
2044
|
if (result) {
|
|
@@ -2386,7 +2412,49 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2386
2412
|
const currentRotation = draft.rotation
|
|
2387
2413
|
// Round to the nearest 45° then step, matching the placed-item R/T.
|
|
2388
2414
|
const newRotationY = steppedRotation(currentRotation[1] ?? 0, rotationDir)
|
|
2389
|
-
|
|
2415
|
+
const nextRotation: [number, number, number] = [
|
|
2416
|
+
currentRotation[0],
|
|
2417
|
+
newRotationY,
|
|
2418
|
+
currentRotation[2],
|
|
2419
|
+
]
|
|
2420
|
+
const surface = placementState.current.surface
|
|
2421
|
+
const surfaceMesh =
|
|
2422
|
+
surface === 'item-surface' && placementState.current.surfaceItemId
|
|
2423
|
+
? sceneRegistry.nodes.get(placementState.current.surfaceItemId)
|
|
2424
|
+
: undefined
|
|
2425
|
+
let surfacePosition: [number, number, number] | undefined
|
|
2426
|
+
if (surfaceMesh) {
|
|
2427
|
+
const localPos = surfaceMesh.worldToLocal(lastRawPos.current.clone())
|
|
2428
|
+
const [dimX, , dimZ] = getScaledDimensions(draft)
|
|
2429
|
+
const swapDims = Math.abs(Math.sin(newRotationY)) > 0.9
|
|
2430
|
+
surfacePosition = [
|
|
2431
|
+
snapToGrid(localPos.x, swapDims ? dimZ : dimX),
|
|
2432
|
+
gridPosition.current.y,
|
|
2433
|
+
snapToGrid(localPos.z, swapDims ? dimX : dimZ),
|
|
2434
|
+
]
|
|
2435
|
+
}
|
|
2436
|
+
if (surfaceAttachmentId(draft)) {
|
|
2437
|
+
let occupied = false
|
|
2438
|
+
checkCanPlace(
|
|
2439
|
+
{
|
|
2440
|
+
...getContext(),
|
|
2441
|
+
draftItem: { ...draft, rotation: nextRotation },
|
|
2442
|
+
gridPosition: surfacePosition
|
|
2443
|
+
? new Vector3(...surfacePosition)
|
|
2444
|
+
: gridPosition.current,
|
|
2445
|
+
onSurfaceReject: (reason) => {
|
|
2446
|
+
occupied = reason === 'surface-occupied'
|
|
2447
|
+
},
|
|
2448
|
+
},
|
|
2449
|
+
validators,
|
|
2450
|
+
)
|
|
2451
|
+
if (occupied) {
|
|
2452
|
+
feedback.reject('surface-occupied')
|
|
2453
|
+
revalidate()
|
|
2454
|
+
return
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
draft.rotation = nextRotation
|
|
2390
2458
|
|
|
2391
2459
|
// Rotate the building-local cursor by the same delta as the host-local
|
|
2392
2460
|
// draft. This preserves the host's composed yaw for items resting on a
|
|
@@ -2398,7 +2466,6 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2398
2466
|
if (mesh) mesh.rotation.y = newRotationY
|
|
2399
2467
|
|
|
2400
2468
|
// Re-snap position immediately with updated rotation (dimX/dimZ may swap at 90°)
|
|
2401
|
-
const surface = placementState.current.surface
|
|
2402
2469
|
if (surface === 'floor' || surface === 'ceiling') {
|
|
2403
2470
|
const dims = getScaledDimensions(draft)
|
|
2404
2471
|
const [dimX, , dimZ] = dims
|
|
@@ -2428,15 +2495,8 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2428
2495
|
}
|
|
2429
2496
|
}
|
|
2430
2497
|
} else if (surface === 'item-surface' && placementState.current.surfaceItemId) {
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
const localPos = surfaceMesh.worldToLocal(lastRawPos.current.clone())
|
|
2434
|
-
const dims = getScaledDimensions(draft)
|
|
2435
|
-
const [dimX, , dimZ] = dims
|
|
2436
|
-
const swapDims = Math.abs(Math.sin(newRotationY)) > 0.9
|
|
2437
|
-
const x = snapToGrid(localPos.x, swapDims ? dimZ : dimX)
|
|
2438
|
-
const z = snapToGrid(localPos.z, swapDims ? dimX : dimZ)
|
|
2439
|
-
const y = gridPosition.current.y
|
|
2498
|
+
if (surfaceMesh && surfacePosition) {
|
|
2499
|
+
const [x, y, z] = surfacePosition
|
|
2440
2500
|
gridPosition.current.set(x, y, z)
|
|
2441
2501
|
draft.position = [x, y, z]
|
|
2442
2502
|
const worldSnapped = surfaceMesh.localToWorld(new Vector3(x, y, z))
|
|
@@ -2594,7 +2654,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2594
2654
|
const unsubDraftWatch = useScene.subscribe((state) => {
|
|
2595
2655
|
if (tearingDown) return
|
|
2596
2656
|
const draft = draftNode.current
|
|
2597
|
-
if (draft === null) return
|
|
2657
|
+
if (draft === null || isFreshPlacementMetadata(draft.metadata)) return
|
|
2598
2658
|
if (draft.id in state.nodes) return
|
|
2599
2659
|
|
|
2600
2660
|
queueMicrotask(() => {
|
|
@@ -2609,41 +2669,73 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2609
2669
|
|
|
2610
2670
|
// ---- Subscribe ----
|
|
2611
2671
|
|
|
2672
|
+
const specializedKinds = new Set<string>()
|
|
2673
|
+
const subscribeSpecialized = <E,>(key: string, handler: (event: E) => void) => {
|
|
2674
|
+
specializedKinds.add(key.slice(0, key.lastIndexOf(':')))
|
|
2675
|
+
emitter.on(key as never, handler as never)
|
|
2676
|
+
}
|
|
2677
|
+
const genericRoute = (phase: 'enter' | 'move' | 'click' | 'leave') => (event: NodeEvent) => {
|
|
2678
|
+
if (specializedKinds.has(event.node.type)) return
|
|
2679
|
+
if (
|
|
2680
|
+
!surfaceOwnership.allows(event.node.id, event.nativeEvent.nativeEvent ?? event.nativeEvent)
|
|
2681
|
+
)
|
|
2682
|
+
return
|
|
2683
|
+
if (nodeRegistry.get(event.node.type)?.capabilities.faceHost) {
|
|
2684
|
+
const handlers = {
|
|
2685
|
+
enter: onFaceHostEnter,
|
|
2686
|
+
move: onFaceHostMove,
|
|
2687
|
+
click: onFaceHostClick,
|
|
2688
|
+
leave: onFaceHostLeave,
|
|
2689
|
+
}
|
|
2690
|
+
handlers[phase](event)
|
|
2691
|
+
return
|
|
2692
|
+
}
|
|
2693
|
+
if (!canHostSurfaceChild(event.node, 'item', draftNode.current?.id)) return
|
|
2694
|
+
if (phase === 'leave' && placementState.current.surfaceItemId !== event.node.id) return
|
|
2695
|
+
if (phase === 'enter') onItemEnter(event, true)
|
|
2696
|
+
else if (phase === 'move') onItemMove(event, true)
|
|
2697
|
+
else if (phase === 'click') onItemClick(event)
|
|
2698
|
+
else onItemLeave(event)
|
|
2699
|
+
}
|
|
2700
|
+
const onNodeEnter = genericRoute('enter')
|
|
2701
|
+
const onNodeMove = genericRoute('move')
|
|
2702
|
+
const onNodeClick = genericRoute('click')
|
|
2703
|
+
const onNodeLeave = genericRoute('leave')
|
|
2612
2704
|
const gridDispatch = createItemSurfaceGridDispatch(onGridMove)
|
|
2613
2705
|
emitter.on('grid:move', gridDispatch.schedule)
|
|
2614
2706
|
emitter.on('grid:click', onGridClick)
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
emitter.on('node:enter',
|
|
2628
|
-
emitter.on('node:move',
|
|
2629
|
-
emitter.on('node:click',
|
|
2630
|
-
emitter.on('node:leave',
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2707
|
+
subscribeSpecialized('item:enter', onItemEnter)
|
|
2708
|
+
subscribeSpecialized('item:move', onItemMove)
|
|
2709
|
+
subscribeSpecialized('item:leave', onItemLeave)
|
|
2710
|
+
subscribeSpecialized('item:click', onItemClick)
|
|
2711
|
+
subscribeSpecialized('wall:enter', onWallEnter)
|
|
2712
|
+
subscribeSpecialized('wall:move', onWallMove)
|
|
2713
|
+
subscribeSpecialized('wall:click', onWallClick)
|
|
2714
|
+
subscribeSpecialized('wall:leave', onWallLeave)
|
|
2715
|
+
subscribeSpecialized('roof:enter', onRoofWallEnter)
|
|
2716
|
+
subscribeSpecialized('roof:move', onRoofWallMove)
|
|
2717
|
+
subscribeSpecialized('roof:click', onRoofWallClick)
|
|
2718
|
+
subscribeSpecialized('roof:leave', onRoofWallLeave)
|
|
2719
|
+
emitter.on('node:enter', onNodeEnter)
|
|
2720
|
+
emitter.on('node:move', onNodeMove)
|
|
2721
|
+
emitter.on('node:click', onNodeClick)
|
|
2722
|
+
emitter.on('node:leave', onNodeLeave)
|
|
2723
|
+
subscribeSpecialized('ceiling:enter', onCeilingEnter)
|
|
2724
|
+
subscribeSpecialized('ceiling:move', onCeilingMove)
|
|
2725
|
+
subscribeSpecialized('ceiling:click', onCeilingClick)
|
|
2726
|
+
subscribeSpecialized('ceiling:leave', onCeilingLeave)
|
|
2727
|
+
subscribeSpecialized('cabinet:enter', onItemEnter)
|
|
2728
|
+
subscribeSpecialized('procedural-item:enter', onItemEnter)
|
|
2729
|
+
subscribeSpecialized('cabinet:move', onItemMove)
|
|
2730
|
+
subscribeSpecialized('procedural-item:move', onItemMove)
|
|
2731
|
+
subscribeSpecialized('cabinet:click', onItemClick)
|
|
2732
|
+
subscribeSpecialized('procedural-item:click', onItemClick)
|
|
2733
|
+
subscribeSpecialized('cabinet:leave', onItemLeave)
|
|
2734
|
+
subscribeSpecialized('procedural-item:leave', onItemLeave)
|
|
2735
|
+
subscribeSpecialized('shelf:enter', onShelfEnter)
|
|
2736
|
+
subscribeSpecialized('shelf:move', onShelfMove)
|
|
2737
|
+
subscribeSpecialized('shelf:click', onShelfClick)
|
|
2738
|
+
subscribeSpecialized('shelf:leave', onShelfLeave)
|
|
2647
2739
|
|
|
2648
2740
|
// A floor placement commits at the tracked floor cursor (`gridPosition`),
|
|
2649
2741
|
// which keeps following the floor even when the click ray lands on a wall
|
|
@@ -2689,10 +2781,10 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2689
2781
|
emitter.off('roof:move', onRoofWallMove)
|
|
2690
2782
|
emitter.off('roof:click', onRoofWallClick)
|
|
2691
2783
|
emitter.off('roof:leave', onRoofWallLeave)
|
|
2692
|
-
emitter.off('node:enter',
|
|
2693
|
-
emitter.off('node:move',
|
|
2694
|
-
emitter.off('node:click',
|
|
2695
|
-
emitter.off('node:leave',
|
|
2784
|
+
emitter.off('node:enter', onNodeEnter)
|
|
2785
|
+
emitter.off('node:move', onNodeMove)
|
|
2786
|
+
emitter.off('node:click', onNodeClick)
|
|
2787
|
+
emitter.off('node:leave', onNodeLeave)
|
|
2696
2788
|
emitter.off('ceiling:enter', onCeilingEnter)
|
|
2697
2789
|
emitter.off('ceiling:move', onCeilingMove)
|
|
2698
2790
|
emitter.off('ceiling:click', onCeilingClick)
|
|
@@ -2764,6 +2856,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2764
2856
|
draftParent?.type === 'shelf' ||
|
|
2765
2857
|
draftParent?.type === 'cabinet' ||
|
|
2766
2858
|
draftParent?.type === 'procedural-item' ||
|
|
2859
|
+
(draftParent && canHostSurfaceChild(draftParent, draft.type, draft.id)) ||
|
|
2767
2860
|
(draftParent &&
|
|
2768
2861
|
nodeRegistry.get(draftParent.type)?.capabilities.faceHost?.currentFaceId(draft))
|
|
2769
2862
|
)
|
|
@@ -2782,8 +2875,8 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2782
2875
|
// instead of drawing an inline triangle. Only this coordinator publishes — a
|
|
2783
2876
|
// moving existing node has no draft here, so the grid reads that case straight
|
|
2784
2877
|
// off the node's mesh. Cleared when idle.
|
|
2785
|
-
const surfaceNormalRef = useRef(new Vector3(0, 1, 0))
|
|
2786
2878
|
const surfaceWorldPointRef = useRef(new Vector3())
|
|
2879
|
+
const surfaceNormalRef = useRef(new Vector3(0, 1, 0))
|
|
2787
2880
|
const facingForwardRef = useRef(new Vector3(0, 0, 1))
|
|
2788
2881
|
const facingQuatRef = useRef(new Quaternion())
|
|
2789
2882
|
const ghostSurfaceQuatRef = useRef(new Quaternion())
|
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
ItemNode,
|
|
10
10
|
LevelNode,
|
|
11
11
|
nodeRegistry,
|
|
12
|
-
registerNode,
|
|
13
12
|
resolveSupportSlabPatch,
|
|
14
13
|
sceneRegistry,
|
|
15
14
|
spatialGridManager,
|
|
@@ -23,6 +22,7 @@ import {
|
|
|
23
22
|
} from '@pascal-app/core/procedural-items'
|
|
24
23
|
import { Group, Vector3 } from 'three'
|
|
25
24
|
import { commitFreshPlacementSubtree } from '../../../lib/fresh-planar-placement'
|
|
25
|
+
import { registerHostingTestNode } from '../__fixtures__/hosting'
|
|
26
26
|
import {
|
|
27
27
|
createItemSurfaceGridDispatch,
|
|
28
28
|
createItemSurfacePointerArbitration,
|
|
@@ -73,7 +73,15 @@ beforeEach(() => {
|
|
|
73
73
|
oldNodes = useScene.getState().nodes
|
|
74
74
|
oldRoots = useScene.getState().rootNodeIds
|
|
75
75
|
nodeRegistry._reset()
|
|
76
|
-
|
|
76
|
+
registerHostingTestNode({
|
|
77
|
+
kind: 'item',
|
|
78
|
+
schemaVersion: 1,
|
|
79
|
+
schema: ItemNode,
|
|
80
|
+
category: 'furnish',
|
|
81
|
+
defaults: () => ({}),
|
|
82
|
+
capabilities: {},
|
|
83
|
+
})
|
|
84
|
+
registerHostingTestNode({
|
|
77
85
|
kind: 'procedural-item',
|
|
78
86
|
schemaVersion: 1,
|
|
79
87
|
schema: ProceduralItemNode,
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
sceneRegistry,
|
|
12
12
|
useLiveTransforms,
|
|
13
13
|
useScene,
|
|
14
|
+
wouldCreateHostingCycle,
|
|
14
15
|
} from '@pascal-app/core'
|
|
15
16
|
import { boxCorners } from '@pascal-app/core/procedural-items'
|
|
16
17
|
import { type Camera, Euler, Quaternion, Vector3 } from 'three'
|
|
@@ -23,7 +24,10 @@ import {
|
|
|
23
24
|
import { snapToGrid, snapToHalf } from '../item/placement-math'
|
|
24
25
|
import { createShelfStickiness } from '../shared/shelf-stickiness'
|
|
25
26
|
import { itemEventToSurfaceHit } from '../shared/surface-hit'
|
|
26
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
createSurfaceEventOwnership,
|
|
29
|
+
createSurfaceRejectionFeedback,
|
|
30
|
+
} from '../shared/surface-rejection'
|
|
27
31
|
|
|
28
32
|
export function createItemSurfacePointerArbitration() {
|
|
29
33
|
let hostId: string | null = null
|
|
@@ -114,6 +118,7 @@ export function createRegistryItemSurfaceMove(node: AnyNode) {
|
|
|
114
118
|
const feedback = createSurfaceRejectionFeedback()
|
|
115
119
|
const scene = createSceneApi(useScene)
|
|
116
120
|
const pointer = createItemSurfacePointerArbitration()
|
|
121
|
+
const ownership = createSurfaceEventOwnership()
|
|
117
122
|
const originalParent = original.parentId
|
|
118
123
|
? useScene.getState().nodes[original.parentId as AnyNodeId]
|
|
119
124
|
: null
|
|
@@ -165,6 +170,14 @@ export function createRegistryItemSurfaceMove(node: AnyNode) {
|
|
|
165
170
|
surfaceId,
|
|
166
171
|
)
|
|
167
172
|
useLiveTransforms.getState().clear(node.id)
|
|
173
|
+
// A floor move and host return can share a React batch with unchanged final props.
|
|
174
|
+
// Restore the local mesh pose too, so an imperative floor preview cannot survive it.
|
|
175
|
+
const mesh = sceneRegistry.nodes.get(node.id)
|
|
176
|
+
if (mesh) {
|
|
177
|
+
mesh.position.set(...position)
|
|
178
|
+
const angles = fullRotation ?? rotation(yaw)
|
|
179
|
+
mesh.rotation.set(...(typeof angles === 'number' ? ([0, angles, 0] as const) : angles))
|
|
180
|
+
}
|
|
168
181
|
}
|
|
169
182
|
const session = {
|
|
170
183
|
get rejection() {
|
|
@@ -211,17 +224,14 @@ export function createRegistryItemSurfaceMove(node: AnyNode) {
|
|
|
211
224
|
}
|
|
212
225
|
},
|
|
213
226
|
enter(event: NodeEvent<AnyNode>, dimensions: [number, number, number], yaw: number) {
|
|
227
|
+
if (!ownership.allows(event.node.id, pointerEventOf(event))) return null
|
|
214
228
|
pointer.clear()
|
|
215
229
|
feedback.clear()
|
|
216
230
|
const live = liveNode()
|
|
217
231
|
if (floorPlaced.applies && !floorPlaced.applies(live)) return null
|
|
218
232
|
const host = useScene.getState().nodes[event.node.id]
|
|
219
233
|
if (!host || NON_PHYSICAL_HOST_KINDS.includes(host.type)) return null
|
|
220
|
-
|
|
221
|
-
while (ancestor) {
|
|
222
|
-
if (ancestor.id === node.id) return null
|
|
223
|
-
ancestor = ancestor.parentId ? scene.get(ancestor.parentId as AnyNodeId) : undefined
|
|
224
|
-
}
|
|
234
|
+
if (wouldCreateHostingCycle(node.id, host, scene)) return null
|
|
225
235
|
const mesh = sceneRegistry.nodes.get(host.id)
|
|
226
236
|
if (!mesh) return null
|
|
227
237
|
const raw = mesh.worldToLocal(new Vector3(...event.position)).toArray()
|
|
@@ -262,6 +272,7 @@ export function createRegistryItemSurfaceMove(node: AnyNode) {
|
|
|
262
272
|
const placement = resolveSurfacePlacement({
|
|
263
273
|
host,
|
|
264
274
|
childKind: node.type,
|
|
275
|
+
childId: node.id,
|
|
265
276
|
childFootprint: {
|
|
266
277
|
size: dimensions,
|
|
267
278
|
rotationY: localYaw,
|
|
@@ -278,7 +289,23 @@ export function createRegistryItemSurfaceMove(node: AnyNode) {
|
|
|
278
289
|
scene,
|
|
279
290
|
snapScalar: host.type !== 'item' ? undefined : snapToGrid,
|
|
280
291
|
checkFootprint: true,
|
|
281
|
-
onReject: (reason) =>
|
|
292
|
+
onReject: (reason) => {
|
|
293
|
+
if (
|
|
294
|
+
live.parentId !== host.id &&
|
|
295
|
+
!counterHit &&
|
|
296
|
+
host.type !== 'item' &&
|
|
297
|
+
host.type !== 'shelf' &&
|
|
298
|
+
reason !== 'surface-cutout' &&
|
|
299
|
+
reason !== 'surface-occupied'
|
|
300
|
+
)
|
|
301
|
+
return
|
|
302
|
+
if (reason === 'surface-cutout' || reason === 'surface-occupied') {
|
|
303
|
+
ownership.claim(host.id, pointerEventOf(event))
|
|
304
|
+
pointer.hit(host.id, pointerEventOf(event))
|
|
305
|
+
event.stopPropagation()
|
|
306
|
+
}
|
|
307
|
+
feedback.reject(reason, pointerEventOf(event))
|
|
308
|
+
},
|
|
282
309
|
})
|
|
283
310
|
if (!placement) return null
|
|
284
311
|
valid = true
|
|
@@ -291,6 +318,7 @@ export function createRegistryItemSurfaceMove(node: AnyNode) {
|
|
|
291
318
|
worldPosition: mesh.localToWorld(new Vector3(...placement.position)).toArray(),
|
|
292
319
|
}
|
|
293
320
|
grab = corrected.grab
|
|
321
|
+
ownership.claim(host.id, pointerEventOf(event))
|
|
294
322
|
pointer.hit(host.id, pointerEventOf(event))
|
|
295
323
|
event.stopPropagation()
|
|
296
324
|
write(
|
|
@@ -344,12 +372,7 @@ export function createRegistryItemSurfaceMove(node: AnyNode) {
|
|
|
344
372
|
false,
|
|
345
373
|
)
|
|
346
374
|
if (host?.type === 'procedural-item') position.splice(0, 3, ...local.position)
|
|
347
|
-
if (
|
|
348
|
-
host?.type === 'cabinet' ||
|
|
349
|
-
host?.type === 'shelf' ||
|
|
350
|
-
host?.type === 'item' ||
|
|
351
|
-
host?.type === 'procedural-item'
|
|
352
|
-
) {
|
|
375
|
+
if (host) {
|
|
353
376
|
const footprint = floorPlaced.footprint?.(live, { nodes: scene.nodes() })
|
|
354
377
|
const bounds:
|
|
355
378
|
| { size: [number, number, number]; center?: [number, number, number] }
|
|
@@ -376,6 +399,7 @@ export function createRegistryItemSurfaceMove(node: AnyNode) {
|
|
|
376
399
|
const placement = resolveSurfacePlacement({
|
|
377
400
|
host,
|
|
378
401
|
childKind: live.type,
|
|
402
|
+
childId: live.id,
|
|
379
403
|
childFootprint: {
|
|
380
404
|
size: bounds.size,
|
|
381
405
|
rotationY: Array.isArray(localRotation) ? localRotation[1] : localRotation,
|
|
@@ -394,7 +418,14 @@ export function createRegistryItemSurfaceMove(node: AnyNode) {
|
|
|
394
418
|
onReject: (reason) => feedback.reject(reason),
|
|
395
419
|
})
|
|
396
420
|
valid = !!placement
|
|
397
|
-
if (placement
|
|
421
|
+
if (!placement) {
|
|
422
|
+
// Null means free rotation to the caller, so return the unchanged stored pose.
|
|
423
|
+
return {
|
|
424
|
+
position: [...live.position] as [number, number, number],
|
|
425
|
+
rotationY: Array.isArray(live.rotation) ? live.rotation[1] : live.rotation,
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (host.type === 'procedural-item') {
|
|
398
429
|
const pose =
|
|
399
430
|
placement.childFrame === 'surface-local'
|
|
400
431
|
? placement.surfaceLocal!
|
|
@@ -405,16 +436,7 @@ export function createRegistryItemSurfaceMove(node: AnyNode) {
|
|
|
405
436
|
rotationY: pose.rotationY,
|
|
406
437
|
}
|
|
407
438
|
}
|
|
408
|
-
|
|
409
|
-
else if (
|
|
410
|
-
feedback.reason === 'footprint-outside-surface' ||
|
|
411
|
-
feedback.reason === 'footprint-exceeds-host' ||
|
|
412
|
-
feedback.reason === 'no-surface'
|
|
413
|
-
) {
|
|
414
|
-
const mesh = sceneRegistry.nodes.get(host.id)
|
|
415
|
-
if (mesh)
|
|
416
|
-
return session.detach(mesh.localToWorld(new Vector3(...position)).toArray(), yaw)
|
|
417
|
-
}
|
|
439
|
+
position[1] = placement.position[1]
|
|
418
440
|
}
|
|
419
441
|
write(live.parentId, position, yaw)
|
|
420
442
|
return { position, rotationY: yaw }
|
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
ItemNode,
|
|
8
8
|
LevelNode,
|
|
9
9
|
nodeRegistry,
|
|
10
|
-
registerNode,
|
|
11
10
|
type SurfacePlacement,
|
|
12
11
|
sceneRegistry,
|
|
13
12
|
useLiveTransforms,
|
|
@@ -17,6 +16,7 @@ import { Group, Vector3 } from 'three'
|
|
|
17
16
|
import { commitFreshPlacementSubtree } from '../../../lib/fresh-planar-placement'
|
|
18
17
|
import useEditor from '../../../store/use-editor'
|
|
19
18
|
import useInteractionScope from '../../../store/use-interaction-scope'
|
|
19
|
+
import { registerHostingTestNode } from '../__fixtures__/hosting'
|
|
20
20
|
import { createRegistryItemSurfaceMove } from './item-surface-move'
|
|
21
21
|
|
|
22
22
|
const level = LevelNode.parse({ id: 'level_resolver' })
|
|
@@ -69,7 +69,7 @@ beforeEach(() => {
|
|
|
69
69
|
savedRoots = useScene.getState().rootNodeIds
|
|
70
70
|
restoreRegistry = nodeRegistry._snapshot()
|
|
71
71
|
nodeRegistry._reset()
|
|
72
|
-
|
|
72
|
+
registerHostingTestNode({
|
|
73
73
|
kind: 'item',
|
|
74
74
|
schemaVersion: 1,
|
|
75
75
|
schema: ItemNode,
|