@pascal-app/editor 1.0.0 → 1.0.1
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 +7 -6
- package/src/components/editor/bake-exporter.tsx +4 -0
- package/src/components/editor/bake-thumbnail.tsx +4 -1
- package/src/components/editor/floating-action-menu.tsx +14 -1
- package/src/components/editor/floorplan-panel.tsx +23 -3
- 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 +5 -2
- package/src/components/editor/index.tsx +12 -4
- package/src/components/editor/node-arrow-handles.tsx +21 -62
- package/src/components/editor/selection-manager.tsx +49 -0
- package/src/components/editor/thumbnail-generator.tsx +11 -2
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +12 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +46 -3
- package/src/components/procedural-items/inspector.tsx +223 -0
- package/src/components/systems/ceiling/ceiling-system.tsx +3 -1
- package/src/components/tools/item/item-surface-placement.test.ts +175 -0
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +210 -0
- package/src/components/tools/item/placement-strategies.ts +169 -169
- 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 +95 -45
- package/src/components/tools/item/use-placement-coordinator.tsx +247 -106
- package/src/components/tools/registry/item-surface-child-frame.test.ts +90 -0
- package/src/components/tools/registry/item-surface-move.test.ts +501 -0
- package/src/components/tools/registry/item-surface-move.ts +441 -0
- package/src/components/tools/registry/item-surface-resolver.test.ts +257 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +215 -70
- package/src/components/tools/shared/pointer-support-cap.ts +11 -9
- 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 +106 -0
- package/src/components/tools/shared/surface-rejection.tsx +29 -0
- 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/metric-control.tsx +29 -13
- package/src/components/ui/controls/slider-control.tsx +25 -18
- package/src/components/ui/floating-level-selector.tsx +81 -5
- 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 +6 -2
- package/src/components/ui/panels/reference-panel.tsx +3 -3
- package/src/components/ui/primitives/checkbox.tsx +34 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +120 -3
- 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 +29 -10
- 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/viewer-scene-header.tsx +3 -0
- package/src/components/viewer/viewer-units-panel.tsx +116 -0
- package/src/hooks/use-ceiling-events.test.ts +24 -2
- package/src/hooks/use-ceiling-events.ts +5 -1
- package/src/hooks/use-keyboard.ts +3 -1
- package/src/index.tsx +5 -0
- 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 +139 -1
- package/src/lib/floorplan/floorplan-export.tsx +9 -3
- package/src/lib/floorplan/floorplan-readonly.ts +4 -0
- package/src/lib/fresh-planar-placement.ts +13 -5
- package/src/lib/glb-export-serialize.test.ts +280 -0
- package/src/lib/glb-export.test.ts +206 -0
- package/src/lib/glb-export.ts +171 -24
- 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/node-action-movement.ts +21 -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/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/surface-attachment.ts +77 -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/store/use-handle-group.ts +4 -0
- package/src/store/use-send-to-app.ts +21 -0
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
resolveFrozenFloorPlacementPatch,
|
|
18
18
|
resolveLevelId,
|
|
19
19
|
type ShelfEvent,
|
|
20
|
+
type SurfaceRejectReason,
|
|
20
21
|
sceneRegistry,
|
|
21
22
|
useLiveNodeOverrides,
|
|
22
23
|
useLiveTransforms,
|
|
@@ -30,16 +31,13 @@ import { Html } from '@react-three/drei'
|
|
|
30
31
|
import { useFrame, useThree } from '@react-three/fiber'
|
|
31
32
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
32
33
|
import {
|
|
33
|
-
Box3,
|
|
34
34
|
Euler,
|
|
35
35
|
type Group,
|
|
36
36
|
type LineSegments,
|
|
37
|
-
Matrix4,
|
|
38
37
|
type Mesh,
|
|
39
38
|
type Object3D,
|
|
40
39
|
PlaneGeometry,
|
|
41
40
|
Quaternion,
|
|
42
|
-
Ray,
|
|
43
41
|
Vector3,
|
|
44
42
|
} from 'three'
|
|
45
43
|
import { distance, smoothstep, uv, vec2 } from 'three/tsl'
|
|
@@ -50,17 +48,29 @@ import {
|
|
|
50
48
|
} from '../../../lib/active-placement-surface'
|
|
51
49
|
import { EDITOR_LAYER } from '../../../lib/constants'
|
|
52
50
|
import { formatLinearMeasurement } from '../../../lib/measurements'
|
|
51
|
+
import { createMovementSfxTick } from '../../../lib/sfx/movement-tick'
|
|
53
52
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
54
|
-
|
|
53
|
+
import {
|
|
54
|
+
surfaceAttachmentId,
|
|
55
|
+
surfaceFramePose,
|
|
56
|
+
updateSurfaceNode,
|
|
57
|
+
} from '../../../lib/surface-attachment'
|
|
55
58
|
import {
|
|
56
59
|
projectAlignmentGuidesWorldToActiveBuildingLocal,
|
|
57
60
|
resolveAlignmentForActiveBuilding,
|
|
58
61
|
} from '../../../lib/world-grid-snap'
|
|
59
62
|
import useAlignmentGuides from '../../../store/use-alignment-guides'
|
|
60
|
-
import useEditor, {
|
|
61
|
-
|
|
63
|
+
import useEditor, {
|
|
64
|
+
isAlignmentGuideActive,
|
|
65
|
+
isGridSnapActive,
|
|
66
|
+
isMagneticSnapActive,
|
|
67
|
+
} from '../../../store/use-editor'
|
|
62
68
|
import useFacingPose from '../../../store/use-facing-pose'
|
|
63
69
|
import usePlacementPreview from '../../../store/use-placement-preview'
|
|
70
|
+
import {
|
|
71
|
+
createItemSurfaceGridDispatch,
|
|
72
|
+
createItemSurfacePointerArbitration,
|
|
73
|
+
} from '../registry/item-surface-move'
|
|
64
74
|
import { getFloorStackPreviewPosition } from '../shared/floor-stack-preview'
|
|
65
75
|
import {
|
|
66
76
|
createLineGeometry,
|
|
@@ -73,6 +83,8 @@ import {
|
|
|
73
83
|
resolvePointerSupportElevation,
|
|
74
84
|
resolvePointerSupportSurface,
|
|
75
85
|
} from '../shared/pointer-support-cap'
|
|
86
|
+
import { createShelfStickiness } from '../shared/shelf-stickiness'
|
|
87
|
+
import { createSurfaceRejectionFeedback } from '../shared/surface-rejection'
|
|
76
88
|
import { shouldCreateFloorDraft } from './draft-creation'
|
|
77
89
|
import { commitFaceHostClick, resolveFaceHostPreviewCommit } from './face-host-commit'
|
|
78
90
|
import {
|
|
@@ -547,10 +559,15 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
547
559
|
}
|
|
548
560
|
}
|
|
549
561
|
|
|
562
|
+
let lastSurfaceEvent: NodeEvent<AnyNode> | null = null
|
|
563
|
+
const feedback = createSurfaceRejectionFeedback()
|
|
564
|
+
feedback.clear()
|
|
565
|
+
|
|
550
566
|
// ---- Helpers ----
|
|
551
567
|
|
|
552
568
|
const getContext = () => ({
|
|
553
569
|
asset,
|
|
570
|
+
onSurfaceReject: (reason: SurfaceRejectReason) => feedback.reject(reason),
|
|
554
571
|
levelId: useViewer.getState().selection.levelId,
|
|
555
572
|
draftItem: draftNode.current,
|
|
556
573
|
gridPosition: gridPosition.current,
|
|
@@ -633,14 +650,60 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
633
650
|
: null
|
|
634
651
|
}
|
|
635
652
|
|
|
653
|
+
const surfacePointer = createItemSurfacePointerArbitration()
|
|
636
654
|
const revalidate = (): boolean => {
|
|
637
|
-
const
|
|
655
|
+
const fits = checkCanPlace(getContext(), validators)
|
|
656
|
+
if (
|
|
657
|
+
!fits &&
|
|
658
|
+
lastSurfaceEvent &&
|
|
659
|
+
(feedback.reason === 'footprint-outside-surface' ||
|
|
660
|
+
feedback.reason === 'footprint-exceeds-host' ||
|
|
661
|
+
feedback.reason === 'no-surface') &&
|
|
662
|
+
(placementState.current.surface === 'item-surface' ||
|
|
663
|
+
placementState.current.surface === 'shelf-surface')
|
|
664
|
+
) {
|
|
665
|
+
const hostId =
|
|
666
|
+
placementState.current.surface === 'shelf-surface'
|
|
667
|
+
? placementState.current.shelfId
|
|
668
|
+
: placementState.current.surfaceItemId
|
|
669
|
+
const mesh = hostId ? sceneRegistry.nodes.get(hostId) : undefined
|
|
670
|
+
if (mesh) {
|
|
671
|
+
const position = mesh.localToWorld(gridPosition.current.clone()).toArray()
|
|
672
|
+
feedback.clear()
|
|
673
|
+
detachItemSurfaceToFloor({ ...lastSurfaceEvent, position } as ItemEvent)
|
|
674
|
+
return revalidate()
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
const placeable = !feedback.reason && (altFreeRef.current || fits)
|
|
638
678
|
const color = placeable ? 0x22_c5_5e : 0xef_44_44 // green-500 : red-500
|
|
639
679
|
edgeMaterial.color.setHex(color)
|
|
640
680
|
basePlaneMaterial.color.setHex(color)
|
|
641
681
|
return placeable
|
|
642
682
|
}
|
|
643
683
|
|
|
684
|
+
const surfaceContext = (event: NodeEvent<AnyNode>) => {
|
|
685
|
+
lastSurfaceEvent = event
|
|
686
|
+
return {
|
|
687
|
+
...getContext(),
|
|
688
|
+
onSurfaceReject: (reason: SurfaceRejectReason) => {
|
|
689
|
+
if (
|
|
690
|
+
(reason === 'footprint-outside-surface' ||
|
|
691
|
+
reason === 'footprint-exceeds-host' ||
|
|
692
|
+
reason === 'no-surface') &&
|
|
693
|
+
(placementState.current.surface === 'item-surface' ||
|
|
694
|
+
placementState.current.surface === 'shelf-surface')
|
|
695
|
+
) {
|
|
696
|
+
feedback.clear()
|
|
697
|
+
detachItemSurfaceToFloor(event as ItemEvent)
|
|
698
|
+
surfacePointer.hit(event.node.id, event.nativeEvent.nativeEvent ?? event.nativeEvent)
|
|
699
|
+
return
|
|
700
|
+
}
|
|
701
|
+
feedback.reject(reason, event.nativeEvent.nativeEvent ?? event.nativeEvent)
|
|
702
|
+
revalidate()
|
|
703
|
+
},
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
644
707
|
// Tool visuals are rendered inside the building-local ToolManager group, so all cursor
|
|
645
708
|
// positions must be in building-local space. Wall/ceiling/item-surface strategies return
|
|
646
709
|
// world-space cursor positions (from their event.position); convert them here.
|
|
@@ -671,7 +734,22 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
671
734
|
return [localRotation.x, localRotation.y, localRotation.z]
|
|
672
735
|
}
|
|
673
736
|
|
|
674
|
-
const
|
|
737
|
+
const { tick: tickMovementSfx } = createMovementSfxTick()
|
|
738
|
+
const tickSurfaceMovementSfx = (position: [number, number, number]) => {
|
|
739
|
+
const point = worldToBuildingLocal(...position)
|
|
740
|
+
tickMovementSfx({
|
|
741
|
+
coords: [point.x, point.z],
|
|
742
|
+
gridSnapActive: isGridSnapActive(),
|
|
743
|
+
gridStep: useEditor.getState().gridSnapStep,
|
|
744
|
+
})
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const applyTransition = (result: TransitionResult, event?: NodeEvent<AnyNode>) => {
|
|
748
|
+
if (event) {
|
|
749
|
+
surfacePointer.hit(event.node.id, event.nativeEvent.nativeEvent ?? event.nativeEvent)
|
|
750
|
+
tickSurfaceMovementSfx(result.cursorPosition)
|
|
751
|
+
}
|
|
752
|
+
feedback.clear()
|
|
675
753
|
// Alignment guides are floor-only; clear them when the cursor moves
|
|
676
754
|
// onto a wall / ceiling / item surface (only those paths call this).
|
|
677
755
|
useAlignmentGuides.getState().clear()
|
|
@@ -734,7 +812,9 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
734
812
|
if (draft) {
|
|
735
813
|
Object.assign(draft, result.nodeUpdate)
|
|
736
814
|
// One-time setup: put node in the right parent so it renders correctly
|
|
737
|
-
|
|
815
|
+
if (result.surfaceId !== undefined)
|
|
816
|
+
draftNode.updateSurface(result.nodeUpdate, result.surfaceId)
|
|
817
|
+
else updateSurfaceNode(draft.id, result.nodeUpdate)
|
|
738
818
|
disableDraftRaycastNow()
|
|
739
819
|
}
|
|
740
820
|
|
|
@@ -753,6 +833,8 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
753
833
|
|
|
754
834
|
// ---- Init draft ----
|
|
755
835
|
configRef.current.initDraft(gridPosition.current)
|
|
836
|
+
if (draftNode.current && surfaceAttachmentId(draftNode.current))
|
|
837
|
+
gridPosition.current.set(...draftNode.current.position)
|
|
756
838
|
const preserveDragOffset = configRef.current.preserveDragOffset === true
|
|
757
839
|
// The host the item was grabbed from + its pre-drag host-local position.
|
|
758
840
|
// Each surface's grab anchor preserves the grab offset only on THAT host,
|
|
@@ -939,6 +1021,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
939
1021
|
setTimeout(() => window.removeEventListener('click', swallow, { capture: true }), 300)
|
|
940
1022
|
}
|
|
941
1023
|
const onReleaseCommit = () => {
|
|
1024
|
+
gridDispatch.flush()
|
|
942
1025
|
if (!releaseCommit) return
|
|
943
1026
|
const commit = releaseCommit
|
|
944
1027
|
releaseCommit = null
|
|
@@ -948,53 +1031,10 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
948
1031
|
|
|
949
1032
|
// ---- Floor Handlers ----
|
|
950
1033
|
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
// Scratch objects reused by the stickiness test (runs per grid:move).
|
|
954
|
-
const stickyRay = new Ray()
|
|
955
|
-
const stickyBox = new Box3()
|
|
956
|
-
const stickyMat = new Matrix4()
|
|
957
|
-
const stickyCamPos = new Vector3()
|
|
958
|
-
|
|
959
|
-
// True while the cursor ray still points at the active shelf's volume.
|
|
960
|
-
// Used to keep an item hosted on a shelf "sticky": from an angled camera
|
|
961
|
-
// the cursor ray slips off the shelf's thin boards / through its gaps and
|
|
962
|
-
// lands on the floor *behind* the shelf, which would otherwise thrash the
|
|
963
|
-
// placement between the shelf row and the floor on every micro-move. We
|
|
964
|
-
// reconstruct the world ray (camera → grid hit point) and test it against
|
|
965
|
-
// the shelf's bounding box — so a ray that passes *through* the shelf but
|
|
966
|
-
// lands behind it still counts as "on the shelf". Only a ray that misses
|
|
967
|
-
// the shelf box entirely means the user genuinely moved off it. A simple
|
|
968
|
-
// footprint test on the floor hit point can't distinguish those.
|
|
969
|
-
const cursorRayIntersectsActiveShelf = (gridWorldPoint: [number, number, number]): boolean => {
|
|
970
|
-
const shelfId = placementState.current.shelfId
|
|
971
|
-
if (!shelfId) return false
|
|
972
|
-
const shelfMesh = sceneRegistry.nodes.get(shelfId as AnyNodeId)
|
|
973
|
-
const shelfNode = useScene.getState().nodes[shelfId as AnyNodeId] as
|
|
974
|
-
| { width?: number; depth?: number; height?: number }
|
|
975
|
-
| undefined
|
|
976
|
-
if (!(shelfMesh && shelfNode?.width && shelfNode?.depth && shelfNode?.height)) return false
|
|
977
|
-
|
|
978
|
-
cameraRef.current.getWorldPosition(stickyCamPos)
|
|
979
|
-
stickyRay.origin.copy(stickyCamPos)
|
|
980
|
-
stickyRay.direction
|
|
981
|
-
.set(
|
|
982
|
-
gridWorldPoint[0] - stickyCamPos.x,
|
|
983
|
-
gridWorldPoint[1] - stickyCamPos.y,
|
|
984
|
-
gridWorldPoint[2] - stickyCamPos.z,
|
|
985
|
-
)
|
|
986
|
-
.normalize()
|
|
987
|
-
|
|
988
|
-
// Into shelf-local space, then test the shelf's local AABB (origin at the
|
|
989
|
-
// base: y ∈ [0, height]) with a small margin.
|
|
990
|
-
stickyRay.applyMatrix4(stickyMat.copy(shelfMesh.matrixWorld).invert())
|
|
991
|
-
const m = 0.08
|
|
992
|
-
stickyBox.min.set(-shelfNode.width / 2 - m, -m, -shelfNode.depth / 2 - m)
|
|
993
|
-
stickyBox.max.set(shelfNode.width / 2 + m, shelfNode.height + m, shelfNode.depth / 2 + m)
|
|
994
|
-
return stickyRay.intersectsBox(stickyBox)
|
|
995
|
-
}
|
|
1034
|
+
const cursorRayIntersectsShelf = createShelfStickiness()
|
|
996
1035
|
|
|
997
1036
|
const onGridMove = (event: GridEvent) => {
|
|
1037
|
+
if (surfacePointer.blocksGrid(event.nativeEvent.nativeEvent ?? event.nativeEvent)) return
|
|
998
1038
|
releaseCommit = () => onGridClick(event)
|
|
999
1039
|
// Lazy draft creation: if no draft yet (e.g. level wasn't ready during init), create now
|
|
1000
1040
|
if (
|
|
@@ -1027,13 +1067,34 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1027
1067
|
// off a board / through a gap and hit the floor behind). Detach to the
|
|
1028
1068
|
// floor only once the ray misses the shelf entirely — without this the
|
|
1029
1069
|
// item oscillates between the shelf row and the floor on every micro-move.
|
|
1070
|
+
const counterId =
|
|
1071
|
+
placementState.current.surface === 'item-surface'
|
|
1072
|
+
? placementState.current.surfaceItemId
|
|
1073
|
+
: null
|
|
1074
|
+
if (counterId && useScene.getState().nodes[counterId as AnyNodeId]?.type === 'cabinet') {
|
|
1075
|
+
if (
|
|
1076
|
+
surfacePointer.blocksGrid(event.nativeEvent.nativeEvent ?? event.nativeEvent) ||
|
|
1077
|
+
cursorRayIntersectsShelf(counterId, cameraRef.current, event.position)
|
|
1078
|
+
)
|
|
1079
|
+
return
|
|
1080
|
+
detachItemSurfaceToFloor(surfaceEvent as unknown as ItemEvent)
|
|
1081
|
+
}
|
|
1030
1082
|
if (placementState.current.surface === 'shelf-surface') {
|
|
1031
|
-
if (
|
|
1083
|
+
if (
|
|
1084
|
+
cursorRayIntersectsShelf(
|
|
1085
|
+
placementState.current.shelfId,
|
|
1086
|
+
cameraRef.current,
|
|
1087
|
+
event.position,
|
|
1088
|
+
)
|
|
1089
|
+
)
|
|
1090
|
+
return
|
|
1032
1091
|
// Land at the pointed surface's plan point — the raw grid hit is
|
|
1033
1092
|
// still skewed by the plane riding at the shelf-surface height.
|
|
1034
1093
|
detachItemSurfaceToFloor(surfaceEvent as unknown as ItemEvent)
|
|
1035
1094
|
}
|
|
1036
1095
|
|
|
1096
|
+
if (placementState.current.surface === 'floor')
|
|
1097
|
+
feedback.grid(event.nativeEvent.nativeEvent ?? event.nativeEvent)
|
|
1037
1098
|
const floorEvent = applyFloorGrabOffset(surfaceEvent)
|
|
1038
1099
|
|
|
1039
1100
|
lastRawPos.current.set(
|
|
@@ -1120,15 +1181,11 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1120
1181
|
frozenSupportSlabIdRef.current = frozenPatch.supportSlabId
|
|
1121
1182
|
}
|
|
1122
1183
|
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
(
|
|
1127
|
-
)
|
|
1128
|
-
sfxEmitter.emit('sfx:grid-snap')
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
previousGridPos = [...gridPos]
|
|
1184
|
+
tickMovementSfx({
|
|
1185
|
+
coords: [gridPos[0], gridPos[2]],
|
|
1186
|
+
gridSnapActive: isGridSnapActive(),
|
|
1187
|
+
gridStep: useEditor.getState().gridSnapStep,
|
|
1188
|
+
})
|
|
1132
1189
|
gridPosition.current.set(...gridPos)
|
|
1133
1190
|
const cursorPosition = getFloorVisualPosition(gridPos)
|
|
1134
1191
|
if (!draft && asset.attachTo) {
|
|
@@ -1161,6 +1218,8 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1161
1218
|
}
|
|
1162
1219
|
|
|
1163
1220
|
const onGridClick = (event: GridEvent) => {
|
|
1221
|
+
gridDispatch.flush()
|
|
1222
|
+
if (feedback.reason) return
|
|
1164
1223
|
// Drop alignment guides on click — the move commits (guides done) or
|
|
1165
1224
|
// placement re-arms (the next move republishes them).
|
|
1166
1225
|
useAlignmentGuides.getState().clear()
|
|
@@ -1222,7 +1281,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1222
1281
|
ensureDraft(result)
|
|
1223
1282
|
} else if (result.nodeUpdate.parentId) {
|
|
1224
1283
|
// Existing draft (move mode): reparent to new wall
|
|
1225
|
-
|
|
1284
|
+
updateSurfaceNode(draftNode.current.id, result.nodeUpdate)
|
|
1226
1285
|
if (result.stateUpdate.wallId) {
|
|
1227
1286
|
useScene.getState().dirtyNodes.add(result.stateUpdate.wallId as AnyNodeId)
|
|
1228
1287
|
}
|
|
@@ -1249,7 +1308,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1249
1308
|
event.stopPropagation()
|
|
1250
1309
|
applyTransition(enterResult)
|
|
1251
1310
|
if (draftNode.current && enterResult.nodeUpdate.parentId) {
|
|
1252
|
-
|
|
1311
|
+
updateSurfaceNode(draftNode.current.id, enterResult.nodeUpdate)
|
|
1253
1312
|
if (enterResult.stateUpdate.wallId) {
|
|
1254
1313
|
useScene.getState().dirtyNodes.add(enterResult.stateUpdate.wallId as AnyNodeId)
|
|
1255
1314
|
}
|
|
@@ -1446,7 +1505,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1446
1505
|
ensureDraft(result)
|
|
1447
1506
|
} else if (result.nodeUpdate.parentId) {
|
|
1448
1507
|
// Existing draft (move mode): reparent to the segment
|
|
1449
|
-
|
|
1508
|
+
updateSurfaceNode(draftNode.current.id, result.nodeUpdate)
|
|
1450
1509
|
}
|
|
1451
1510
|
return true
|
|
1452
1511
|
}
|
|
@@ -1575,7 +1634,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1575
1634
|
if (!draftNode.current) {
|
|
1576
1635
|
ensureDraft(result)
|
|
1577
1636
|
} else if (result.nodeUpdate.parentId) {
|
|
1578
|
-
|
|
1637
|
+
updateSurfaceNode(draftNode.current.id, result.nodeUpdate)
|
|
1579
1638
|
disableDraftRaycastNow()
|
|
1580
1639
|
}
|
|
1581
1640
|
if (draftNode.current) useLiveTransforms.getState().clear(draftNode.current.id)
|
|
@@ -1679,8 +1738,9 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1679
1738
|
|
|
1680
1739
|
// ---- Item Surface Handlers ----
|
|
1681
1740
|
|
|
1682
|
-
const detachItemSurfaceToFloor = (event:
|
|
1741
|
+
const detachItemSurfaceToFloor = (event: NodeEvent<AnyNode>) => {
|
|
1683
1742
|
hostSurfaceDragAnchor = null
|
|
1743
|
+
surfacePointer.clear()
|
|
1684
1744
|
// Landing back on the floor: refresh the pointer surface cap from
|
|
1685
1745
|
// this event's world hit so the first floor position already targets
|
|
1686
1746
|
// the aimed-at surface (not a deck above it).
|
|
@@ -1753,7 +1813,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1753
1813
|
// parent-must-be-a-level guard and the item + grid stopped following
|
|
1754
1814
|
// slab elevations for the rest of the drag).
|
|
1755
1815
|
if (levelId) draft.parentId = levelId
|
|
1756
|
-
|
|
1816
|
+
updateSurfaceNode(draft.id, {
|
|
1757
1817
|
parentId: useViewer.getState().selection.levelId as string,
|
|
1758
1818
|
position: floorPos,
|
|
1759
1819
|
rotation,
|
|
@@ -1763,29 +1823,42 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1763
1823
|
revalidate()
|
|
1764
1824
|
}
|
|
1765
1825
|
|
|
1766
|
-
const onItemEnter = (event:
|
|
1826
|
+
const onItemEnter = (event: NodeEvent<AnyNode>) => {
|
|
1827
|
+
if (event.node.type === 'cabinet' && placementState.current.surfaceItemId === event.node.id) {
|
|
1828
|
+
onItemMove(event)
|
|
1829
|
+
return
|
|
1830
|
+
}
|
|
1831
|
+
if (event.node.type === 'cabinet') {
|
|
1832
|
+
lastRawPos.current.set(...event.position)
|
|
1833
|
+
surfacePointer.hit(event.node.id, event.nativeEvent.nativeEvent ?? event.nativeEvent)
|
|
1834
|
+
}
|
|
1767
1835
|
if (event.node.id === draftNode.current?.id) return
|
|
1768
1836
|
has3DPointerDrivenMoveRef.current = true
|
|
1769
|
-
const result = itemSurfaceStrategy.enter(
|
|
1837
|
+
const result = itemSurfaceStrategy.enter(surfaceContext(event), event)
|
|
1770
1838
|
if (!result) return
|
|
1839
|
+
feedback.clear()
|
|
1771
1840
|
|
|
1772
1841
|
event.stopPropagation()
|
|
1773
|
-
applyTransition(result)
|
|
1842
|
+
applyTransition(result, event)
|
|
1774
1843
|
|
|
1775
1844
|
if (!draftNode.current) {
|
|
1776
1845
|
ensureDraft(result)
|
|
1777
1846
|
} else if (result.nodeUpdate.parentId) {
|
|
1778
1847
|
// Existing draft (move mode): reparent to surface item
|
|
1779
|
-
|
|
1848
|
+
draftNode.updateSurface(result.nodeUpdate, result.surfaceId ?? null)
|
|
1780
1849
|
}
|
|
1781
1850
|
}
|
|
1782
1851
|
|
|
1783
|
-
const onItemMove = (event:
|
|
1852
|
+
const onItemMove = (event: NodeEvent<AnyNode>) => {
|
|
1784
1853
|
if (event.node.id === draftNode.current?.id) return
|
|
1785
1854
|
releaseCommit = () => onItemClick(event)
|
|
1786
1855
|
has3DPointerDrivenMoveRef.current = true
|
|
1787
1856
|
if (!cursorGroupRef.current) return
|
|
1788
|
-
const ctx =
|
|
1857
|
+
const ctx = surfaceContext(event)
|
|
1858
|
+
if (event.node.type === 'cabinet') {
|
|
1859
|
+
lastRawPos.current.set(...event.position)
|
|
1860
|
+
surfacePointer.hit(event.node.id, event.nativeEvent.nativeEvent ?? event.nativeEvent)
|
|
1861
|
+
}
|
|
1789
1862
|
|
|
1790
1863
|
if (ctx.state.surface !== 'item-surface') {
|
|
1791
1864
|
// Try entering surface mode
|
|
@@ -1793,9 +1866,9 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1793
1866
|
if (!enterResult) return
|
|
1794
1867
|
|
|
1795
1868
|
event.stopPropagation()
|
|
1796
|
-
applyTransition(enterResult)
|
|
1869
|
+
applyTransition(enterResult, event)
|
|
1797
1870
|
if (draftNode.current && enterResult.nodeUpdate.parentId) {
|
|
1798
|
-
|
|
1871
|
+
draftNode.updateSurface(enterResult.nodeUpdate, enterResult.surfaceId ?? null)
|
|
1799
1872
|
}
|
|
1800
1873
|
return
|
|
1801
1874
|
}
|
|
@@ -1808,18 +1881,18 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1808
1881
|
|
|
1809
1882
|
event.stopPropagation()
|
|
1810
1883
|
if (enterResult) {
|
|
1811
|
-
applyTransition(enterResult)
|
|
1884
|
+
applyTransition(enterResult, event)
|
|
1812
1885
|
if (draftNode.current && enterResult.nodeUpdate.parentId) {
|
|
1813
|
-
|
|
1886
|
+
draftNode.updateSurface(enterResult.nodeUpdate, enterResult.surfaceId ?? null)
|
|
1814
1887
|
}
|
|
1815
|
-
} else {
|
|
1888
|
+
} else if (!feedback.reason) {
|
|
1816
1889
|
detachItemSurfaceToFloor(event)
|
|
1817
1890
|
}
|
|
1818
1891
|
return
|
|
1819
1892
|
}
|
|
1820
1893
|
|
|
1821
1894
|
if (!draftNode.current) {
|
|
1822
|
-
const enterResult = itemSurfaceStrategy.enter(
|
|
1895
|
+
const enterResult = itemSurfaceStrategy.enter(surfaceContext(event), event)
|
|
1823
1896
|
if (!enterResult) return
|
|
1824
1897
|
event.stopPropagation()
|
|
1825
1898
|
ensureDraft(enterResult)
|
|
@@ -1836,11 +1909,17 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1836
1909
|
itemMoveEvent.position[1],
|
|
1837
1910
|
itemMoveEvent.position[2],
|
|
1838
1911
|
)
|
|
1839
|
-
const result = itemSurfaceStrategy.move(ctx, itemMoveEvent)
|
|
1840
|
-
if (!result)
|
|
1912
|
+
const result = itemSurfaceStrategy.move(ctx, itemMoveEvent, event)
|
|
1913
|
+
if (!result) {
|
|
1914
|
+
revalidate()
|
|
1915
|
+
return
|
|
1916
|
+
}
|
|
1917
|
+
feedback.clear()
|
|
1841
1918
|
|
|
1842
1919
|
event.stopPropagation()
|
|
1843
1920
|
|
|
1921
|
+
surfacePointer.hit(event.node.id, event.nativeEvent.nativeEvent ?? event.nativeEvent)
|
|
1922
|
+
tickSurfaceMovementSfx(result.cursorPosition)
|
|
1844
1923
|
gridPosition.current.set(...result.gridPosition)
|
|
1845
1924
|
const ic = worldToBuildingLocal(...result.cursorPosition)
|
|
1846
1925
|
cursorGroupRef.current.position.set(ic.x, ic.y, ic.z)
|
|
@@ -1849,8 +1928,13 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1849
1928
|
const draft = draftNode.current
|
|
1850
1929
|
if (draft) {
|
|
1851
1930
|
draft.position = result.gridPosition
|
|
1931
|
+
if (event.node.type === 'procedural-item')
|
|
1932
|
+
draftNode.updateSurface(result.nodeUpdate ?? {}, result.surfaceId ?? null)
|
|
1852
1933
|
const mesh = sceneRegistry.nodes.get(draft.id)
|
|
1853
|
-
if (mesh)
|
|
1934
|
+
if (mesh)
|
|
1935
|
+
mesh.position.set(
|
|
1936
|
+
...surfaceFramePose(draft.parentId, surfaceAttachmentId(draft), draft, true).position,
|
|
1937
|
+
)
|
|
1854
1938
|
|
|
1855
1939
|
// Publish live transform for 2D floorplan
|
|
1856
1940
|
useLiveTransforms.getState().set(draft.id, {
|
|
@@ -1862,7 +1946,8 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1862
1946
|
revalidate()
|
|
1863
1947
|
}
|
|
1864
1948
|
|
|
1865
|
-
const onItemLeave = (event:
|
|
1949
|
+
const onItemLeave = (event: NodeEvent<AnyNode>) => {
|
|
1950
|
+
if (event.node.type === 'cabinet') return
|
|
1866
1951
|
if (event.node.id === draftNode.current?.id) return
|
|
1867
1952
|
if (placementState.current.surface !== 'item-surface') return
|
|
1868
1953
|
|
|
@@ -1876,7 +1961,9 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1876
1961
|
detachItemSurfaceToFloor(event)
|
|
1877
1962
|
}
|
|
1878
1963
|
|
|
1879
|
-
const onItemClick = (event:
|
|
1964
|
+
const onItemClick = (event: NodeEvent<AnyNode>) => {
|
|
1965
|
+
gridDispatch.flush()
|
|
1966
|
+
if (feedback.reason) return
|
|
1880
1967
|
// Click on the draft item itself. R3F dispatches click events to
|
|
1881
1968
|
// the closest intersected mesh only — when the draft is hovering
|
|
1882
1969
|
// on a host (shelf / table / etc.) the draft's mesh is *above*
|
|
@@ -1885,7 +1972,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1885
1972
|
// self-click as a commit on the active shelf so the user doesn't
|
|
1886
1973
|
// have to aim around the cursor preview to drop the item.
|
|
1887
1974
|
if (event.node.id === draftNode.current?.id) {
|
|
1888
|
-
const ctx =
|
|
1975
|
+
const ctx = surfaceContext(event)
|
|
1889
1976
|
if (ctx.state.surface === 'block-face') {
|
|
1890
1977
|
const result = resolveFaceHostPreviewCommit(ctx)
|
|
1891
1978
|
if (result) {
|
|
@@ -1920,7 +2007,12 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1920
2007
|
// the host's own click event is blocked by the cursor preview.
|
|
1921
2008
|
if (ctx.state.surface === 'item-surface' && ctx.state.surfaceItemId) {
|
|
1922
2009
|
const hostNode = useScene.getState().nodes[ctx.state.surfaceItemId as AnyNodeId]
|
|
1923
|
-
if (
|
|
2010
|
+
if (
|
|
2011
|
+
hostNode &&
|
|
2012
|
+
(hostNode.type === 'item' ||
|
|
2013
|
+
hostNode.type === 'cabinet' ||
|
|
2014
|
+
hostNode.type === 'procedural-item')
|
|
2015
|
+
) {
|
|
1924
2016
|
const synthetic = { ...event, node: hostNode } as ItemEvent
|
|
1925
2017
|
const result = itemSurfaceStrategy.click(ctx, synthetic)
|
|
1926
2018
|
if (result) {
|
|
@@ -1955,7 +2047,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1955
2047
|
finishCommittedPlacement(committedId, wasAdopted, () => {
|
|
1956
2048
|
const nodes = useScene.getState().nodes
|
|
1957
2049
|
const enterResult = ceilingStrategy.enter(
|
|
1958
|
-
|
|
2050
|
+
surfaceContext(event),
|
|
1959
2051
|
synthetic,
|
|
1960
2052
|
resolveLevelId,
|
|
1961
2053
|
nodes,
|
|
@@ -1973,7 +2065,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1973
2065
|
return
|
|
1974
2066
|
}
|
|
1975
2067
|
|
|
1976
|
-
const result = itemSurfaceStrategy.click(
|
|
2068
|
+
const result = itemSurfaceStrategy.click(surfaceContext(event), event)
|
|
1977
2069
|
if (!result) return
|
|
1978
2070
|
|
|
1979
2071
|
event.stopPropagation()
|
|
@@ -1981,7 +2073,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1981
2073
|
|
|
1982
2074
|
finishCommittedPlacement(committedId, wasAdopted, () => {
|
|
1983
2075
|
// Try to set up next draft on the same surface
|
|
1984
|
-
const enterResult = itemSurfaceStrategy.enter(
|
|
2076
|
+
const enterResult = itemSurfaceStrategy.enter(surfaceContext(event), event)
|
|
1985
2077
|
if (enterResult) {
|
|
1986
2078
|
applyTransition(enterResult)
|
|
1987
2079
|
} else {
|
|
@@ -2005,7 +2097,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2005
2097
|
ensureDraft(result)
|
|
2006
2098
|
} else if (result.nodeUpdate.parentId) {
|
|
2007
2099
|
// Existing draft (move mode): reparent to new ceiling
|
|
2008
|
-
|
|
2100
|
+
updateSurfaceNode(draftNode.current.id, result.nodeUpdate)
|
|
2009
2101
|
if (result.stateUpdate.ceilingId) {
|
|
2010
2102
|
useScene.getState().dirtyNodes.add(result.stateUpdate.ceilingId as AnyNodeId)
|
|
2011
2103
|
}
|
|
@@ -2155,16 +2247,16 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2155
2247
|
|
|
2156
2248
|
const onShelfEnter = (event: ShelfEvent) => {
|
|
2157
2249
|
has3DPointerDrivenMoveRef.current = true
|
|
2158
|
-
const result = shelfSurfaceStrategy.enter(
|
|
2250
|
+
const result = shelfSurfaceStrategy.enter(surfaceContext(event), event)
|
|
2159
2251
|
if (!result) return
|
|
2160
2252
|
|
|
2161
2253
|
event.stopPropagation()
|
|
2162
|
-
applyTransition(result)
|
|
2254
|
+
applyTransition(result, event)
|
|
2163
2255
|
|
|
2164
2256
|
if (!draftNode.current) {
|
|
2165
2257
|
ensureDraft(result)
|
|
2166
2258
|
} else if (result.nodeUpdate.parentId) {
|
|
2167
|
-
|
|
2259
|
+
updateSurfaceNode(draftNode.current.id, result.nodeUpdate)
|
|
2168
2260
|
}
|
|
2169
2261
|
}
|
|
2170
2262
|
|
|
@@ -2174,7 +2266,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2174
2266
|
// A shelf event can fire before the cursor group mounts or after
|
|
2175
2267
|
// teardown, leaving the ref null; bail before dereferencing it below.
|
|
2176
2268
|
if (!cursorGroupRef.current) return
|
|
2177
|
-
const ctx =
|
|
2269
|
+
const ctx = surfaceContext(event)
|
|
2178
2270
|
if (ctx.state.surface !== 'shelf-surface') {
|
|
2179
2271
|
// Cursor entered via a move event without an enter — try
|
|
2180
2272
|
// transitioning in so the user doesn't need to mouse out + back
|
|
@@ -2182,11 +2274,11 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2182
2274
|
const enterResult = shelfSurfaceStrategy.enter(ctx, event)
|
|
2183
2275
|
if (!enterResult) return
|
|
2184
2276
|
event.stopPropagation()
|
|
2185
|
-
applyTransition(enterResult)
|
|
2277
|
+
applyTransition(enterResult, event)
|
|
2186
2278
|
if (!draftNode.current) {
|
|
2187
2279
|
ensureDraft(enterResult)
|
|
2188
2280
|
} else if (enterResult.nodeUpdate.parentId) {
|
|
2189
|
-
|
|
2281
|
+
updateSurfaceNode(draftNode.current.id, enterResult.nodeUpdate)
|
|
2190
2282
|
}
|
|
2191
2283
|
return
|
|
2192
2284
|
}
|
|
@@ -2198,8 +2290,11 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2198
2290
|
const result = shelfSurfaceStrategy.move(ctx, shelfMoveEvent)
|
|
2199
2291
|
if (!result) return
|
|
2200
2292
|
|
|
2293
|
+
feedback.clear()
|
|
2201
2294
|
event.stopPropagation()
|
|
2202
2295
|
|
|
2296
|
+
surfacePointer.hit(event.node.id, event.nativeEvent.nativeEvent ?? event.nativeEvent)
|
|
2297
|
+
tickSurfaceMovementSfx(result.cursorPosition)
|
|
2203
2298
|
gridPosition.current.set(...result.gridPosition)
|
|
2204
2299
|
const ic = worldToBuildingLocal(...result.cursorPosition)
|
|
2205
2300
|
cursorGroupRef.current.position.set(ic.x, ic.y, ic.z)
|
|
@@ -2209,7 +2304,10 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2209
2304
|
if (draft) {
|
|
2210
2305
|
draft.position = result.gridPosition
|
|
2211
2306
|
const mesh = sceneRegistry.nodes.get(draft.id)
|
|
2212
|
-
if (mesh)
|
|
2307
|
+
if (mesh)
|
|
2308
|
+
mesh.position.set(
|
|
2309
|
+
...surfaceFramePose(draft.parentId, surfaceAttachmentId(draft), draft, true).position,
|
|
2310
|
+
)
|
|
2213
2311
|
useLiveTransforms.getState().set(draft.id, {
|
|
2214
2312
|
position: result.cursorPosition,
|
|
2215
2313
|
rotation: result.cursorRotationY,
|
|
@@ -2233,14 +2331,16 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2233
2331
|
}
|
|
2234
2332
|
|
|
2235
2333
|
const onShelfClick = (event: ShelfEvent) => {
|
|
2236
|
-
|
|
2334
|
+
gridDispatch.flush()
|
|
2335
|
+
if (feedback.reason) return
|
|
2336
|
+
const result = shelfSurfaceStrategy.click(surfaceContext(event), event)
|
|
2237
2337
|
if (!result) return
|
|
2238
2338
|
|
|
2239
2339
|
event.stopPropagation()
|
|
2240
2340
|
const { committedId, wasAdopted } = commitDraft(result.nodeUpdate)
|
|
2241
2341
|
|
|
2242
2342
|
finishCommittedPlacement(committedId, wasAdopted, () => {
|
|
2243
|
-
const enterResult = shelfSurfaceStrategy.enter(
|
|
2343
|
+
const enterResult = shelfSurfaceStrategy.enter(surfaceContext(event), event)
|
|
2244
2344
|
if (enterResult) {
|
|
2245
2345
|
applyTransition(enterResult)
|
|
2246
2346
|
} else {
|
|
@@ -2362,7 +2462,21 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2362
2462
|
// Keep both preview renderers on the rotated draft. The item renderer
|
|
2363
2463
|
// consumes live node overrides, while the floor-plan renderer consumes
|
|
2364
2464
|
// the live transform and placement-preview snapshot.
|
|
2365
|
-
|
|
2465
|
+
const storedSurfacePose = surfaceFramePose(
|
|
2466
|
+
draft.parentId,
|
|
2467
|
+
surfaceAttachmentId(draft),
|
|
2468
|
+
draft,
|
|
2469
|
+
true,
|
|
2470
|
+
)
|
|
2471
|
+
if (surfaceAttachmentId(draft)) {
|
|
2472
|
+
draftNode.updateSurface(
|
|
2473
|
+
{ position: draft.position, rotation: draft.rotation },
|
|
2474
|
+
surfaceAttachmentId(draft),
|
|
2475
|
+
)
|
|
2476
|
+
mesh?.position.set(...storedSurfacePose.position)
|
|
2477
|
+
mesh?.rotation.set(...storedSurfacePose.rotation)
|
|
2478
|
+
}
|
|
2479
|
+
useLiveNodeOverrides.getState().set(draft.id, { rotation: storedSurfacePose.rotation })
|
|
2366
2480
|
const currentLive = useLiveTransforms.getState().get(draft.id)
|
|
2367
2481
|
const livePosition: [number, number, number] =
|
|
2368
2482
|
surface === 'floor'
|
|
@@ -2395,7 +2509,13 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2395
2509
|
)
|
|
2396
2510
|
}
|
|
2397
2511
|
|
|
2398
|
-
|
|
2512
|
+
if (feedback.reason && lastSurfaceEvent) {
|
|
2513
|
+
if (lastSurfaceEvent.node.type === 'shelf') onShelfMove(lastSurfaceEvent as ShelfEvent)
|
|
2514
|
+
else onItemMove(lastSurfaceEvent as NodeEvent<AnyNode>)
|
|
2515
|
+
} else {
|
|
2516
|
+
feedback.clear()
|
|
2517
|
+
revalidate()
|
|
2518
|
+
}
|
|
2399
2519
|
}
|
|
2400
2520
|
}
|
|
2401
2521
|
|
|
@@ -2489,7 +2609,8 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2489
2609
|
|
|
2490
2610
|
// ---- Subscribe ----
|
|
2491
2611
|
|
|
2492
|
-
|
|
2612
|
+
const gridDispatch = createItemSurfaceGridDispatch(onGridMove)
|
|
2613
|
+
emitter.on('grid:move', gridDispatch.schedule)
|
|
2493
2614
|
emitter.on('grid:click', onGridClick)
|
|
2494
2615
|
emitter.on('item:enter', onItemEnter)
|
|
2495
2616
|
emitter.on('item:move', onItemMove)
|
|
@@ -2511,6 +2632,14 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2511
2632
|
emitter.on('ceiling:move', onCeilingMove)
|
|
2512
2633
|
emitter.on('ceiling:click', onCeilingClick)
|
|
2513
2634
|
emitter.on('ceiling:leave', onCeilingLeave)
|
|
2635
|
+
emitter.on('cabinet:enter', onItemEnter)
|
|
2636
|
+
emitter.on('procedural-item:enter' as never, onItemEnter)
|
|
2637
|
+
emitter.on('cabinet:move', onItemMove)
|
|
2638
|
+
emitter.on('procedural-item:move' as never, onItemMove)
|
|
2639
|
+
emitter.on('cabinet:click', onItemClick)
|
|
2640
|
+
emitter.on('procedural-item:click' as never, onItemClick)
|
|
2641
|
+
emitter.on('cabinet:leave', onItemLeave)
|
|
2642
|
+
emitter.on('procedural-item:leave' as never, onItemLeave)
|
|
2514
2643
|
emitter.on('shelf:enter', onShelfEnter)
|
|
2515
2644
|
emitter.on('shelf:move', onShelfMove)
|
|
2516
2645
|
emitter.on('shelf:click', onShelfClick)
|
|
@@ -2534,6 +2663,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2534
2663
|
|
|
2535
2664
|
return () => {
|
|
2536
2665
|
tearingDown = true
|
|
2666
|
+
feedback.clear()
|
|
2537
2667
|
if (dragMode) window.removeEventListener('pointerup', onReleaseCommit)
|
|
2538
2668
|
unsubDraftWatch()
|
|
2539
2669
|
useAlignmentGuides.getState().clear()
|
|
@@ -2544,7 +2674,8 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2544
2674
|
}
|
|
2545
2675
|
draftNode.destroy()
|
|
2546
2676
|
useScene.temporal.getState().resume()
|
|
2547
|
-
|
|
2677
|
+
gridDispatch.cancel()
|
|
2678
|
+
emitter.off('grid:move', gridDispatch.schedule)
|
|
2548
2679
|
emitter.off('grid:click', onGridClick)
|
|
2549
2680
|
emitter.off('item:enter', onItemEnter)
|
|
2550
2681
|
emitter.off('item:move', onItemMove)
|
|
@@ -2566,6 +2697,14 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2566
2697
|
emitter.off('ceiling:move', onCeilingMove)
|
|
2567
2698
|
emitter.off('ceiling:click', onCeilingClick)
|
|
2568
2699
|
emitter.off('ceiling:leave', onCeilingLeave)
|
|
2700
|
+
emitter.off('cabinet:enter', onItemEnter)
|
|
2701
|
+
emitter.off('procedural-item:enter' as never, onItemEnter)
|
|
2702
|
+
emitter.off('cabinet:move', onItemMove)
|
|
2703
|
+
emitter.off('procedural-item:move' as never, onItemMove)
|
|
2704
|
+
emitter.off('cabinet:click', onItemClick)
|
|
2705
|
+
emitter.off('procedural-item:click' as never, onItemClick)
|
|
2706
|
+
emitter.off('cabinet:leave', onItemLeave)
|
|
2707
|
+
emitter.off('procedural-item:leave' as never, onItemLeave)
|
|
2569
2708
|
emitter.off('shelf:enter', onShelfEnter)
|
|
2570
2709
|
emitter.off('shelf:move', onShelfMove)
|
|
2571
2710
|
emitter.off('shelf:click', onShelfClick)
|
|
@@ -2623,6 +2762,8 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2623
2762
|
if (
|
|
2624
2763
|
draftParent?.type === 'item' ||
|
|
2625
2764
|
draftParent?.type === 'shelf' ||
|
|
2765
|
+
draftParent?.type === 'cabinet' ||
|
|
2766
|
+
draftParent?.type === 'procedural-item' ||
|
|
2626
2767
|
(draftParent &&
|
|
2627
2768
|
nodeRegistry.get(draftParent.type)?.capabilities.faceHost?.currentFaceId(draft))
|
|
2628
2769
|
)
|