@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
|
@@ -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,
|
|
@@ -17,6 +18,7 @@ import {
|
|
|
17
18
|
resolveFrozenFloorPlacementPatch,
|
|
18
19
|
resolveLevelId,
|
|
19
20
|
type ShelfEvent,
|
|
21
|
+
type SurfaceRejectReason,
|
|
20
22
|
sceneRegistry,
|
|
21
23
|
useLiveNodeOverrides,
|
|
22
24
|
useLiveTransforms,
|
|
@@ -30,16 +32,13 @@ import { Html } from '@react-three/drei'
|
|
|
30
32
|
import { useFrame, useThree } from '@react-three/fiber'
|
|
31
33
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
32
34
|
import {
|
|
33
|
-
Box3,
|
|
34
35
|
Euler,
|
|
35
36
|
type Group,
|
|
36
37
|
type LineSegments,
|
|
37
|
-
Matrix4,
|
|
38
38
|
type Mesh,
|
|
39
39
|
type Object3D,
|
|
40
40
|
PlaneGeometry,
|
|
41
41
|
Quaternion,
|
|
42
|
-
Ray,
|
|
43
42
|
Vector3,
|
|
44
43
|
} from 'three'
|
|
45
44
|
import { distance, smoothstep, uv, vec2 } from 'three/tsl'
|
|
@@ -50,17 +49,30 @@ import {
|
|
|
50
49
|
} from '../../../lib/active-placement-surface'
|
|
51
50
|
import { EDITOR_LAYER } from '../../../lib/constants'
|
|
52
51
|
import { formatLinearMeasurement } from '../../../lib/measurements'
|
|
52
|
+
import { isFreshPlacementMetadata } from '../../../lib/placement-metadata'
|
|
53
|
+
import { createMovementSfxTick } from '../../../lib/sfx/movement-tick'
|
|
53
54
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
54
|
-
|
|
55
|
+
import {
|
|
56
|
+
surfaceAttachmentId,
|
|
57
|
+
surfaceFramePose,
|
|
58
|
+
updateSurfaceNode,
|
|
59
|
+
} from '../../../lib/surface-attachment'
|
|
55
60
|
import {
|
|
56
61
|
projectAlignmentGuidesWorldToActiveBuildingLocal,
|
|
57
62
|
resolveAlignmentForActiveBuilding,
|
|
58
63
|
} from '../../../lib/world-grid-snap'
|
|
59
64
|
import useAlignmentGuides from '../../../store/use-alignment-guides'
|
|
60
|
-
import useEditor, {
|
|
61
|
-
|
|
65
|
+
import useEditor, {
|
|
66
|
+
isAlignmentGuideActive,
|
|
67
|
+
isGridSnapActive,
|
|
68
|
+
isMagneticSnapActive,
|
|
69
|
+
} from '../../../store/use-editor'
|
|
62
70
|
import useFacingPose from '../../../store/use-facing-pose'
|
|
63
71
|
import usePlacementPreview from '../../../store/use-placement-preview'
|
|
72
|
+
import {
|
|
73
|
+
createItemSurfaceGridDispatch,
|
|
74
|
+
createItemSurfacePointerArbitration,
|
|
75
|
+
} from '../registry/item-surface-move'
|
|
64
76
|
import { getFloorStackPreviewPosition } from '../shared/floor-stack-preview'
|
|
65
77
|
import {
|
|
66
78
|
createLineGeometry,
|
|
@@ -70,9 +82,13 @@ import {
|
|
|
70
82
|
} from '../shared/placement-box-geometry'
|
|
71
83
|
import {
|
|
72
84
|
type PointerSupportSurface,
|
|
73
|
-
resolvePointerSupportElevation,
|
|
74
85
|
resolvePointerSupportSurface,
|
|
75
86
|
} from '../shared/pointer-support-cap'
|
|
87
|
+
import { createShelfStickiness } from '../shared/shelf-stickiness'
|
|
88
|
+
import {
|
|
89
|
+
createSurfaceEventOwnership,
|
|
90
|
+
createSurfaceRejectionFeedback,
|
|
91
|
+
} from '../shared/surface-rejection'
|
|
76
92
|
import { shouldCreateFloorDraft } from './draft-creation'
|
|
77
93
|
import { commitFaceHostClick, resolveFaceHostPreviewCommit } from './face-host-commit'
|
|
78
94
|
import {
|
|
@@ -547,10 +563,15 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
547
563
|
}
|
|
548
564
|
}
|
|
549
565
|
|
|
566
|
+
let lastSurfaceEvent: NodeEvent<AnyNode> | null = null
|
|
567
|
+
const feedback = createSurfaceRejectionFeedback()
|
|
568
|
+
feedback.clear()
|
|
569
|
+
|
|
550
570
|
// ---- Helpers ----
|
|
551
571
|
|
|
552
572
|
const getContext = () => ({
|
|
553
573
|
asset,
|
|
574
|
+
onSurfaceReject: (reason: SurfaceRejectReason) => feedback.reject(reason),
|
|
554
575
|
levelId: useViewer.getState().selection.levelId,
|
|
555
576
|
draftItem: draftNode.current,
|
|
556
577
|
gridPosition: gridPosition.current,
|
|
@@ -580,6 +601,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
580
601
|
wasAdopted: boolean,
|
|
581
602
|
repeat: () => void,
|
|
582
603
|
) => {
|
|
604
|
+
if (!committedId) return
|
|
583
605
|
if (configRef.current.onCommitted()) {
|
|
584
606
|
repeat()
|
|
585
607
|
return
|
|
@@ -609,12 +631,19 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
609
631
|
) => {
|
|
610
632
|
const draftId = draftNode.current?.id ?? null
|
|
611
633
|
const wasAdopted = draftNode.isAdopted
|
|
612
|
-
const finalId = draftNode.commit(nodeUpdate,
|
|
613
|
-
|
|
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) {
|
|
614
643
|
useLiveTransforms.getState().clear(draftId)
|
|
615
644
|
useLiveNodeOverrides.getState().clearFields(draftId, faceHostClearFields(draftNode.current))
|
|
616
645
|
}
|
|
617
|
-
return { committedId: finalId
|
|
646
|
+
return { committedId: finalId, wasAdopted }
|
|
618
647
|
}
|
|
619
648
|
|
|
620
649
|
const faceHostClearFields = (draft: ItemNode | null | undefined): Array<keyof ItemNode> => {
|
|
@@ -633,14 +662,74 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
633
662
|
: null
|
|
634
663
|
}
|
|
635
664
|
|
|
665
|
+
const surfacePointer = createItemSurfacePointerArbitration()
|
|
666
|
+
const surfaceOwnership = createSurfaceEventOwnership()
|
|
636
667
|
const revalidate = (): boolean => {
|
|
637
|
-
const
|
|
668
|
+
const fits = checkCanPlace(getContext(), validators)
|
|
669
|
+
if (
|
|
670
|
+
!fits &&
|
|
671
|
+
lastSurfaceEvent &&
|
|
672
|
+
(feedback.reason === 'footprint-outside-surface' ||
|
|
673
|
+
feedback.reason === 'footprint-exceeds-host' ||
|
|
674
|
+
feedback.reason === 'no-surface') &&
|
|
675
|
+
(placementState.current.surface === 'item-surface' ||
|
|
676
|
+
placementState.current.surface === 'shelf-surface')
|
|
677
|
+
) {
|
|
678
|
+
const hostId =
|
|
679
|
+
placementState.current.surface === 'shelf-surface'
|
|
680
|
+
? placementState.current.shelfId
|
|
681
|
+
: placementState.current.surfaceItemId
|
|
682
|
+
const mesh = hostId ? sceneRegistry.nodes.get(hostId) : undefined
|
|
683
|
+
if (mesh) {
|
|
684
|
+
const position = mesh.localToWorld(gridPosition.current.clone()).toArray()
|
|
685
|
+
feedback.clear()
|
|
686
|
+
detachItemSurfaceToFloor({ ...lastSurfaceEvent, position } as ItemEvent)
|
|
687
|
+
return revalidate()
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
const placeable = !feedback.reason && (altFreeRef.current || fits)
|
|
638
691
|
const color = placeable ? 0x22_c5_5e : 0xef_44_44 // green-500 : red-500
|
|
639
692
|
edgeMaterial.color.setHex(color)
|
|
640
693
|
basePlaneMaterial.color.setHex(color)
|
|
641
694
|
return placeable
|
|
642
695
|
}
|
|
643
696
|
|
|
697
|
+
const surfaceContext = (event: NodeEvent<AnyNode>, generic = false) => {
|
|
698
|
+
lastSurfaceEvent = event
|
|
699
|
+
return {
|
|
700
|
+
...getContext(),
|
|
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()
|
|
713
|
+
if (
|
|
714
|
+
(reason === 'footprint-outside-surface' ||
|
|
715
|
+
reason === 'footprint-exceeds-host' ||
|
|
716
|
+
reason === 'no-surface') &&
|
|
717
|
+
(placementState.current.surface === 'item-surface' ||
|
|
718
|
+
placementState.current.surface === 'shelf-surface')
|
|
719
|
+
) {
|
|
720
|
+
feedback.clear()
|
|
721
|
+
detachItemSurfaceToFloor(event as ItemEvent)
|
|
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)
|
|
725
|
+
return
|
|
726
|
+
}
|
|
727
|
+
feedback.reject(reason, event.nativeEvent.nativeEvent ?? event.nativeEvent)
|
|
728
|
+
revalidate()
|
|
729
|
+
},
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
644
733
|
// Tool visuals are rendered inside the building-local ToolManager group, so all cursor
|
|
645
734
|
// positions must be in building-local space. Wall/ceiling/item-surface strategies return
|
|
646
735
|
// world-space cursor positions (from their event.position); convert them here.
|
|
@@ -671,7 +760,23 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
671
760
|
return [localRotation.x, localRotation.y, localRotation.z]
|
|
672
761
|
}
|
|
673
762
|
|
|
674
|
-
const
|
|
763
|
+
const { tick: tickMovementSfx } = createMovementSfxTick()
|
|
764
|
+
const tickSurfaceMovementSfx = (position: [number, number, number]) => {
|
|
765
|
+
const point = worldToBuildingLocal(...position)
|
|
766
|
+
tickMovementSfx({
|
|
767
|
+
coords: [point.x, point.z],
|
|
768
|
+
gridSnapActive: isGridSnapActive(),
|
|
769
|
+
gridStep: useEditor.getState().gridSnapStep,
|
|
770
|
+
})
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const applyTransition = (result: TransitionResult, event?: NodeEvent<AnyNode>) => {
|
|
774
|
+
if (event) {
|
|
775
|
+
surfaceOwnership.claim(event.node.id, event.nativeEvent.nativeEvent ?? event.nativeEvent)
|
|
776
|
+
surfacePointer.hit(event.node.id, event.nativeEvent.nativeEvent ?? event.nativeEvent)
|
|
777
|
+
tickSurfaceMovementSfx(result.cursorPosition)
|
|
778
|
+
}
|
|
779
|
+
feedback.clear()
|
|
675
780
|
// Alignment guides are floor-only; clear them when the cursor moves
|
|
676
781
|
// onto a wall / ceiling / item surface (only those paths call this).
|
|
677
782
|
useAlignmentGuides.getState().clear()
|
|
@@ -734,7 +839,9 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
734
839
|
if (draft) {
|
|
735
840
|
Object.assign(draft, result.nodeUpdate)
|
|
736
841
|
// One-time setup: put node in the right parent so it renders correctly
|
|
737
|
-
|
|
842
|
+
if (result.surfaceId !== undefined)
|
|
843
|
+
draftNode.updateSurface(result.nodeUpdate, result.surfaceId)
|
|
844
|
+
else updateSurfaceNode(draft.id, result.nodeUpdate)
|
|
738
845
|
disableDraftRaycastNow()
|
|
739
846
|
}
|
|
740
847
|
|
|
@@ -753,6 +860,8 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
753
860
|
|
|
754
861
|
// ---- Init draft ----
|
|
755
862
|
configRef.current.initDraft(gridPosition.current)
|
|
863
|
+
if (draftNode.current && surfaceAttachmentId(draftNode.current))
|
|
864
|
+
gridPosition.current.set(...draftNode.current.position)
|
|
756
865
|
const preserveDragOffset = configRef.current.preserveDragOffset === true
|
|
757
866
|
// The host the item was grabbed from + its pre-drag host-local position.
|
|
758
867
|
// Each surface's grab anchor preserves the grab offset only on THAT host,
|
|
@@ -939,6 +1048,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
939
1048
|
setTimeout(() => window.removeEventListener('click', swallow, { capture: true }), 300)
|
|
940
1049
|
}
|
|
941
1050
|
const onReleaseCommit = () => {
|
|
1051
|
+
gridDispatch.flush()
|
|
942
1052
|
if (!releaseCommit) return
|
|
943
1053
|
const commit = releaseCommit
|
|
944
1054
|
releaseCommit = null
|
|
@@ -948,53 +1058,10 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
948
1058
|
|
|
949
1059
|
// ---- Floor Handlers ----
|
|
950
1060
|
|
|
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
|
-
}
|
|
1061
|
+
const cursorRayIntersectsShelf = createShelfStickiness()
|
|
996
1062
|
|
|
997
1063
|
const onGridMove = (event: GridEvent) => {
|
|
1064
|
+
if (surfacePointer.blocksGrid(event.nativeEvent.nativeEvent ?? event.nativeEvent)) return
|
|
998
1065
|
releaseCommit = () => onGridClick(event)
|
|
999
1066
|
// Lazy draft creation: if no draft yet (e.g. level wasn't ready during init), create now
|
|
1000
1067
|
if (
|
|
@@ -1014,7 +1081,9 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1014
1081
|
// perspective-skewed along the ray whenever the plane sits on a
|
|
1015
1082
|
// different storey than the pointed surface (the skew is what made
|
|
1016
1083
|
// a drag over a deck-above-a-floor hop between the two surfaces).
|
|
1017
|
-
const pointed = resolvePointerSupportSurface(cameraRef.current, event.position
|
|
1084
|
+
const pointed = resolvePointerSupportSurface(cameraRef.current, event.position, {
|
|
1085
|
+
pointerRay: event.nativeEvent.ray,
|
|
1086
|
+
})
|
|
1018
1087
|
pointerSupportCapRef.current = pointed?.elevation ?? null
|
|
1019
1088
|
pointerSupportSurfaceRef.current = pointed
|
|
1020
1089
|
const surfaceEvent: GridEvent =
|
|
@@ -1027,13 +1096,34 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1027
1096
|
// off a board / through a gap and hit the floor behind). Detach to the
|
|
1028
1097
|
// floor only once the ray misses the shelf entirely — without this the
|
|
1029
1098
|
// item oscillates between the shelf row and the floor on every micro-move.
|
|
1099
|
+
const counterId =
|
|
1100
|
+
placementState.current.surface === 'item-surface'
|
|
1101
|
+
? placementState.current.surfaceItemId
|
|
1102
|
+
: null
|
|
1103
|
+
if (counterId && useScene.getState().nodes[counterId as AnyNodeId]?.type === 'cabinet') {
|
|
1104
|
+
if (
|
|
1105
|
+
surfacePointer.blocksGrid(event.nativeEvent.nativeEvent ?? event.nativeEvent) ||
|
|
1106
|
+
cursorRayIntersectsShelf(counterId, cameraRef.current, event.position)
|
|
1107
|
+
)
|
|
1108
|
+
return
|
|
1109
|
+
detachItemSurfaceToFloor(surfaceEvent as unknown as ItemEvent)
|
|
1110
|
+
}
|
|
1030
1111
|
if (placementState.current.surface === 'shelf-surface') {
|
|
1031
|
-
if (
|
|
1112
|
+
if (
|
|
1113
|
+
cursorRayIntersectsShelf(
|
|
1114
|
+
placementState.current.shelfId,
|
|
1115
|
+
cameraRef.current,
|
|
1116
|
+
event.position,
|
|
1117
|
+
)
|
|
1118
|
+
)
|
|
1119
|
+
return
|
|
1032
1120
|
// Land at the pointed surface's plan point — the raw grid hit is
|
|
1033
1121
|
// still skewed by the plane riding at the shelf-surface height.
|
|
1034
1122
|
detachItemSurfaceToFloor(surfaceEvent as unknown as ItemEvent)
|
|
1035
1123
|
}
|
|
1036
1124
|
|
|
1125
|
+
if (placementState.current.surface === 'floor')
|
|
1126
|
+
feedback.grid(event.nativeEvent.nativeEvent ?? event.nativeEvent)
|
|
1037
1127
|
const floorEvent = applyFloorGrabOffset(surfaceEvent)
|
|
1038
1128
|
|
|
1039
1129
|
lastRawPos.current.set(
|
|
@@ -1120,15 +1210,11 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1120
1210
|
frozenSupportSlabIdRef.current = frozenPatch.supportSlabId
|
|
1121
1211
|
}
|
|
1122
1212
|
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
(
|
|
1127
|
-
)
|
|
1128
|
-
sfxEmitter.emit('sfx:grid-snap')
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
previousGridPos = [...gridPos]
|
|
1213
|
+
tickMovementSfx({
|
|
1214
|
+
coords: [gridPos[0], gridPos[2]],
|
|
1215
|
+
gridSnapActive: isGridSnapActive(),
|
|
1216
|
+
gridStep: useEditor.getState().gridSnapStep,
|
|
1217
|
+
})
|
|
1132
1218
|
gridPosition.current.set(...gridPos)
|
|
1133
1219
|
const cursorPosition = getFloorVisualPosition(gridPos)
|
|
1134
1220
|
if (!draft && asset.attachTo) {
|
|
@@ -1161,6 +1247,8 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1161
1247
|
}
|
|
1162
1248
|
|
|
1163
1249
|
const onGridClick = (event: GridEvent) => {
|
|
1250
|
+
gridDispatch.flush()
|
|
1251
|
+
if (feedback.reason) return
|
|
1164
1252
|
// Drop alignment guides on click — the move commits (guides done) or
|
|
1165
1253
|
// placement re-arms (the next move republishes them).
|
|
1166
1254
|
useAlignmentGuides.getState().clear()
|
|
@@ -1222,7 +1310,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1222
1310
|
ensureDraft(result)
|
|
1223
1311
|
} else if (result.nodeUpdate.parentId) {
|
|
1224
1312
|
// Existing draft (move mode): reparent to new wall
|
|
1225
|
-
|
|
1313
|
+
updateSurfaceNode(draftNode.current.id, result.nodeUpdate)
|
|
1226
1314
|
if (result.stateUpdate.wallId) {
|
|
1227
1315
|
useScene.getState().dirtyNodes.add(result.stateUpdate.wallId as AnyNodeId)
|
|
1228
1316
|
}
|
|
@@ -1249,7 +1337,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1249
1337
|
event.stopPropagation()
|
|
1250
1338
|
applyTransition(enterResult)
|
|
1251
1339
|
if (draftNode.current && enterResult.nodeUpdate.parentId) {
|
|
1252
|
-
|
|
1340
|
+
updateSurfaceNode(draftNode.current.id, enterResult.nodeUpdate)
|
|
1253
1341
|
if (enterResult.stateUpdate.wallId) {
|
|
1254
1342
|
useScene.getState().dirtyNodes.add(enterResult.stateUpdate.wallId as AnyNodeId)
|
|
1255
1343
|
}
|
|
@@ -1446,7 +1534,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1446
1534
|
ensureDraft(result)
|
|
1447
1535
|
} else if (result.nodeUpdate.parentId) {
|
|
1448
1536
|
// Existing draft (move mode): reparent to the segment
|
|
1449
|
-
|
|
1537
|
+
updateSurfaceNode(draftNode.current.id, result.nodeUpdate)
|
|
1450
1538
|
}
|
|
1451
1539
|
return true
|
|
1452
1540
|
}
|
|
@@ -1575,7 +1663,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1575
1663
|
if (!draftNode.current) {
|
|
1576
1664
|
ensureDraft(result)
|
|
1577
1665
|
} else if (result.nodeUpdate.parentId) {
|
|
1578
|
-
|
|
1666
|
+
updateSurfaceNode(draftNode.current.id, result.nodeUpdate)
|
|
1579
1667
|
disableDraftRaycastNow()
|
|
1580
1668
|
}
|
|
1581
1669
|
if (draftNode.current) useLiveTransforms.getState().clear(draftNode.current.id)
|
|
@@ -1673,22 +1761,23 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1673
1761
|
if (draft) useScene.getState().updateNode(draft.id, result.nodeUpdate)
|
|
1674
1762
|
} else {
|
|
1675
1763
|
draftNode.destroy()
|
|
1764
|
+
gridPosition.current.set(...result.gridPosition)
|
|
1676
1765
|
Object.assign(placementState.current, result.stateUpdate)
|
|
1677
1766
|
}
|
|
1678
1767
|
}
|
|
1679
1768
|
|
|
1680
1769
|
// ---- Item Surface Handlers ----
|
|
1681
1770
|
|
|
1682
|
-
const detachItemSurfaceToFloor = (event:
|
|
1771
|
+
const detachItemSurfaceToFloor = (event: NodeEvent<AnyNode>) => {
|
|
1683
1772
|
hostSurfaceDragAnchor = null
|
|
1773
|
+
surfacePointer.clear()
|
|
1684
1774
|
// Landing back on the floor: refresh the pointer surface cap from
|
|
1685
1775
|
// this event's world hit so the first floor position already targets
|
|
1686
1776
|
// the aimed-at surface (not a deck above it).
|
|
1687
|
-
pointerSupportCapRef.current =
|
|
1688
|
-
event.position
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
])
|
|
1777
|
+
pointerSupportCapRef.current =
|
|
1778
|
+
resolvePointerSupportSurface(cameraRef.current, event.position, {
|
|
1779
|
+
pointerRay: event.nativeEvent.ray,
|
|
1780
|
+
})?.elevation ?? null
|
|
1692
1781
|
// Coming back from a host: forget the floor grab too, so the item
|
|
1693
1782
|
// centers under the cursor instead of restoring the pre-drag offset —
|
|
1694
1783
|
// and landing on the floor is "anchoring elsewhere", so a later return
|
|
@@ -1753,7 +1842,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1753
1842
|
// parent-must-be-a-level guard and the item + grid stopped following
|
|
1754
1843
|
// slab elevations for the rest of the drag).
|
|
1755
1844
|
if (levelId) draft.parentId = levelId
|
|
1756
|
-
|
|
1845
|
+
updateSurfaceNode(draft.id, {
|
|
1757
1846
|
parentId: useViewer.getState().selection.levelId as string,
|
|
1758
1847
|
position: floorPos,
|
|
1759
1848
|
rotation,
|
|
@@ -1763,29 +1852,42 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1763
1852
|
revalidate()
|
|
1764
1853
|
}
|
|
1765
1854
|
|
|
1766
|
-
const onItemEnter = (event:
|
|
1855
|
+
const onItemEnter = (event: NodeEvent<AnyNode>, generic = false) => {
|
|
1856
|
+
if (event.node.type === 'cabinet' && placementState.current.surfaceItemId === event.node.id) {
|
|
1857
|
+
onItemMove(event)
|
|
1858
|
+
return
|
|
1859
|
+
}
|
|
1860
|
+
if (event.node.type === 'cabinet') {
|
|
1861
|
+
lastRawPos.current.set(...event.position)
|
|
1862
|
+
surfacePointer.hit(event.node.id, event.nativeEvent.nativeEvent ?? event.nativeEvent)
|
|
1863
|
+
}
|
|
1767
1864
|
if (event.node.id === draftNode.current?.id) return
|
|
1768
1865
|
has3DPointerDrivenMoveRef.current = true
|
|
1769
|
-
const result = itemSurfaceStrategy.enter(
|
|
1866
|
+
const result = itemSurfaceStrategy.enter(surfaceContext(event, generic), event)
|
|
1770
1867
|
if (!result) return
|
|
1868
|
+
feedback.clear()
|
|
1771
1869
|
|
|
1772
1870
|
event.stopPropagation()
|
|
1773
|
-
applyTransition(result)
|
|
1871
|
+
applyTransition(result, event)
|
|
1774
1872
|
|
|
1775
1873
|
if (!draftNode.current) {
|
|
1776
1874
|
ensureDraft(result)
|
|
1777
1875
|
} else if (result.nodeUpdate.parentId) {
|
|
1778
1876
|
// Existing draft (move mode): reparent to surface item
|
|
1779
|
-
|
|
1877
|
+
draftNode.updateSurface(result.nodeUpdate, result.surfaceId ?? null)
|
|
1780
1878
|
}
|
|
1781
1879
|
}
|
|
1782
1880
|
|
|
1783
|
-
const onItemMove = (event:
|
|
1881
|
+
const onItemMove = (event: NodeEvent<AnyNode>, generic = false) => {
|
|
1784
1882
|
if (event.node.id === draftNode.current?.id) return
|
|
1785
1883
|
releaseCommit = () => onItemClick(event)
|
|
1786
1884
|
has3DPointerDrivenMoveRef.current = true
|
|
1787
1885
|
if (!cursorGroupRef.current) return
|
|
1788
|
-
const ctx =
|
|
1886
|
+
const ctx = surfaceContext(event, generic)
|
|
1887
|
+
if (event.node.type === 'cabinet') {
|
|
1888
|
+
lastRawPos.current.set(...event.position)
|
|
1889
|
+
surfacePointer.hit(event.node.id, event.nativeEvent.nativeEvent ?? event.nativeEvent)
|
|
1890
|
+
}
|
|
1789
1891
|
|
|
1790
1892
|
if (ctx.state.surface !== 'item-surface') {
|
|
1791
1893
|
// Try entering surface mode
|
|
@@ -1793,9 +1895,9 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1793
1895
|
if (!enterResult) return
|
|
1794
1896
|
|
|
1795
1897
|
event.stopPropagation()
|
|
1796
|
-
applyTransition(enterResult)
|
|
1898
|
+
applyTransition(enterResult, event)
|
|
1797
1899
|
if (draftNode.current && enterResult.nodeUpdate.parentId) {
|
|
1798
|
-
|
|
1900
|
+
draftNode.updateSurface(enterResult.nodeUpdate, enterResult.surfaceId ?? null)
|
|
1799
1901
|
}
|
|
1800
1902
|
return
|
|
1801
1903
|
}
|
|
@@ -1806,20 +1908,22 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1806
1908
|
event,
|
|
1807
1909
|
)
|
|
1808
1910
|
|
|
1911
|
+
// An unclaimed generic mesh must let its owning ancestor receive the same hit.
|
|
1912
|
+
if (generic && !enterResult) return
|
|
1809
1913
|
event.stopPropagation()
|
|
1810
1914
|
if (enterResult) {
|
|
1811
|
-
applyTransition(enterResult)
|
|
1915
|
+
applyTransition(enterResult, event)
|
|
1812
1916
|
if (draftNode.current && enterResult.nodeUpdate.parentId) {
|
|
1813
|
-
|
|
1917
|
+
draftNode.updateSurface(enterResult.nodeUpdate, enterResult.surfaceId ?? null)
|
|
1814
1918
|
}
|
|
1815
|
-
} else {
|
|
1919
|
+
} else if (!feedback.reason) {
|
|
1816
1920
|
detachItemSurfaceToFloor(event)
|
|
1817
1921
|
}
|
|
1818
1922
|
return
|
|
1819
1923
|
}
|
|
1820
1924
|
|
|
1821
1925
|
if (!draftNode.current) {
|
|
1822
|
-
const enterResult = itemSurfaceStrategy.enter(
|
|
1926
|
+
const enterResult = itemSurfaceStrategy.enter(surfaceContext(event, generic), event)
|
|
1823
1927
|
if (!enterResult) return
|
|
1824
1928
|
event.stopPropagation()
|
|
1825
1929
|
ensureDraft(enterResult)
|
|
@@ -1836,11 +1940,17 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1836
1940
|
itemMoveEvent.position[1],
|
|
1837
1941
|
itemMoveEvent.position[2],
|
|
1838
1942
|
)
|
|
1839
|
-
const result = itemSurfaceStrategy.move(ctx, itemMoveEvent)
|
|
1840
|
-
if (!result)
|
|
1943
|
+
const result = itemSurfaceStrategy.move(ctx, itemMoveEvent, event)
|
|
1944
|
+
if (!result) {
|
|
1945
|
+
revalidate()
|
|
1946
|
+
return
|
|
1947
|
+
}
|
|
1948
|
+
feedback.clear()
|
|
1841
1949
|
|
|
1842
1950
|
event.stopPropagation()
|
|
1843
1951
|
|
|
1952
|
+
surfacePointer.hit(event.node.id, event.nativeEvent.nativeEvent ?? event.nativeEvent)
|
|
1953
|
+
tickSurfaceMovementSfx(result.cursorPosition)
|
|
1844
1954
|
gridPosition.current.set(...result.gridPosition)
|
|
1845
1955
|
const ic = worldToBuildingLocal(...result.cursorPosition)
|
|
1846
1956
|
cursorGroupRef.current.position.set(ic.x, ic.y, ic.z)
|
|
@@ -1849,8 +1959,13 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1849
1959
|
const draft = draftNode.current
|
|
1850
1960
|
if (draft) {
|
|
1851
1961
|
draft.position = result.gridPosition
|
|
1962
|
+
if (event.node.type === 'procedural-item')
|
|
1963
|
+
draftNode.updateSurface(result.nodeUpdate ?? {}, result.surfaceId ?? null)
|
|
1852
1964
|
const mesh = sceneRegistry.nodes.get(draft.id)
|
|
1853
|
-
if (mesh)
|
|
1965
|
+
if (mesh)
|
|
1966
|
+
mesh.position.set(
|
|
1967
|
+
...surfaceFramePose(draft.parentId, surfaceAttachmentId(draft), draft, true).position,
|
|
1968
|
+
)
|
|
1854
1969
|
|
|
1855
1970
|
// Publish live transform for 2D floorplan
|
|
1856
1971
|
useLiveTransforms.getState().set(draft.id, {
|
|
@@ -1862,7 +1977,8 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1862
1977
|
revalidate()
|
|
1863
1978
|
}
|
|
1864
1979
|
|
|
1865
|
-
const onItemLeave = (event:
|
|
1980
|
+
const onItemLeave = (event: NodeEvent<AnyNode>) => {
|
|
1981
|
+
if (event.node.type === 'cabinet') return
|
|
1866
1982
|
if (event.node.id === draftNode.current?.id) return
|
|
1867
1983
|
if (placementState.current.surface !== 'item-surface') return
|
|
1868
1984
|
|
|
@@ -1876,7 +1992,9 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1876
1992
|
detachItemSurfaceToFloor(event)
|
|
1877
1993
|
}
|
|
1878
1994
|
|
|
1879
|
-
const onItemClick = (event:
|
|
1995
|
+
const onItemClick = (event: NodeEvent<AnyNode>) => {
|
|
1996
|
+
gridDispatch.flush()
|
|
1997
|
+
if (feedback.reason) return
|
|
1880
1998
|
// Click on the draft item itself. R3F dispatches click events to
|
|
1881
1999
|
// the closest intersected mesh only — when the draft is hovering
|
|
1882
2000
|
// on a host (shelf / table / etc.) the draft's mesh is *above*
|
|
@@ -1885,7 +2003,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1885
2003
|
// self-click as a commit on the active shelf so the user doesn't
|
|
1886
2004
|
// have to aim around the cursor preview to drop the item.
|
|
1887
2005
|
if (event.node.id === draftNode.current?.id) {
|
|
1888
|
-
const ctx =
|
|
2006
|
+
const ctx = surfaceContext(event)
|
|
1889
2007
|
if (ctx.state.surface === 'block-face') {
|
|
1890
2008
|
const result = resolveFaceHostPreviewCommit(ctx)
|
|
1891
2009
|
if (result) {
|
|
@@ -1920,7 +2038,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1920
2038
|
// the host's own click event is blocked by the cursor preview.
|
|
1921
2039
|
if (ctx.state.surface === 'item-surface' && ctx.state.surfaceItemId) {
|
|
1922
2040
|
const hostNode = useScene.getState().nodes[ctx.state.surfaceItemId as AnyNodeId]
|
|
1923
|
-
if (hostNode
|
|
2041
|
+
if (hostNode) {
|
|
1924
2042
|
const synthetic = { ...event, node: hostNode } as ItemEvent
|
|
1925
2043
|
const result = itemSurfaceStrategy.click(ctx, synthetic)
|
|
1926
2044
|
if (result) {
|
|
@@ -1955,7 +2073,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1955
2073
|
finishCommittedPlacement(committedId, wasAdopted, () => {
|
|
1956
2074
|
const nodes = useScene.getState().nodes
|
|
1957
2075
|
const enterResult = ceilingStrategy.enter(
|
|
1958
|
-
|
|
2076
|
+
surfaceContext(event),
|
|
1959
2077
|
synthetic,
|
|
1960
2078
|
resolveLevelId,
|
|
1961
2079
|
nodes,
|
|
@@ -1973,7 +2091,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1973
2091
|
return
|
|
1974
2092
|
}
|
|
1975
2093
|
|
|
1976
|
-
const result = itemSurfaceStrategy.click(
|
|
2094
|
+
const result = itemSurfaceStrategy.click(surfaceContext(event), event)
|
|
1977
2095
|
if (!result) return
|
|
1978
2096
|
|
|
1979
2097
|
event.stopPropagation()
|
|
@@ -1981,7 +2099,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1981
2099
|
|
|
1982
2100
|
finishCommittedPlacement(committedId, wasAdopted, () => {
|
|
1983
2101
|
// Try to set up next draft on the same surface
|
|
1984
|
-
const enterResult = itemSurfaceStrategy.enter(
|
|
2102
|
+
const enterResult = itemSurfaceStrategy.enter(surfaceContext(event), event)
|
|
1985
2103
|
if (enterResult) {
|
|
1986
2104
|
applyTransition(enterResult)
|
|
1987
2105
|
} else {
|
|
@@ -2005,7 +2123,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2005
2123
|
ensureDraft(result)
|
|
2006
2124
|
} else if (result.nodeUpdate.parentId) {
|
|
2007
2125
|
// Existing draft (move mode): reparent to new ceiling
|
|
2008
|
-
|
|
2126
|
+
updateSurfaceNode(draftNode.current.id, result.nodeUpdate)
|
|
2009
2127
|
if (result.stateUpdate.ceilingId) {
|
|
2010
2128
|
useScene.getState().dirtyNodes.add(result.stateUpdate.ceilingId as AnyNodeId)
|
|
2011
2129
|
}
|
|
@@ -2155,16 +2273,16 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2155
2273
|
|
|
2156
2274
|
const onShelfEnter = (event: ShelfEvent) => {
|
|
2157
2275
|
has3DPointerDrivenMoveRef.current = true
|
|
2158
|
-
const result = shelfSurfaceStrategy.enter(
|
|
2276
|
+
const result = shelfSurfaceStrategy.enter(surfaceContext(event), event)
|
|
2159
2277
|
if (!result) return
|
|
2160
2278
|
|
|
2161
2279
|
event.stopPropagation()
|
|
2162
|
-
applyTransition(result)
|
|
2280
|
+
applyTransition(result, event)
|
|
2163
2281
|
|
|
2164
2282
|
if (!draftNode.current) {
|
|
2165
2283
|
ensureDraft(result)
|
|
2166
2284
|
} else if (result.nodeUpdate.parentId) {
|
|
2167
|
-
|
|
2285
|
+
updateSurfaceNode(draftNode.current.id, result.nodeUpdate)
|
|
2168
2286
|
}
|
|
2169
2287
|
}
|
|
2170
2288
|
|
|
@@ -2174,7 +2292,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2174
2292
|
// A shelf event can fire before the cursor group mounts or after
|
|
2175
2293
|
// teardown, leaving the ref null; bail before dereferencing it below.
|
|
2176
2294
|
if (!cursorGroupRef.current) return
|
|
2177
|
-
const ctx =
|
|
2295
|
+
const ctx = surfaceContext(event)
|
|
2178
2296
|
if (ctx.state.surface !== 'shelf-surface') {
|
|
2179
2297
|
// Cursor entered via a move event without an enter — try
|
|
2180
2298
|
// transitioning in so the user doesn't need to mouse out + back
|
|
@@ -2182,11 +2300,11 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2182
2300
|
const enterResult = shelfSurfaceStrategy.enter(ctx, event)
|
|
2183
2301
|
if (!enterResult) return
|
|
2184
2302
|
event.stopPropagation()
|
|
2185
|
-
applyTransition(enterResult)
|
|
2303
|
+
applyTransition(enterResult, event)
|
|
2186
2304
|
if (!draftNode.current) {
|
|
2187
2305
|
ensureDraft(enterResult)
|
|
2188
2306
|
} else if (enterResult.nodeUpdate.parentId) {
|
|
2189
|
-
|
|
2307
|
+
updateSurfaceNode(draftNode.current.id, enterResult.nodeUpdate)
|
|
2190
2308
|
}
|
|
2191
2309
|
return
|
|
2192
2310
|
}
|
|
@@ -2198,8 +2316,11 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2198
2316
|
const result = shelfSurfaceStrategy.move(ctx, shelfMoveEvent)
|
|
2199
2317
|
if (!result) return
|
|
2200
2318
|
|
|
2319
|
+
feedback.clear()
|
|
2201
2320
|
event.stopPropagation()
|
|
2202
2321
|
|
|
2322
|
+
surfacePointer.hit(event.node.id, event.nativeEvent.nativeEvent ?? event.nativeEvent)
|
|
2323
|
+
tickSurfaceMovementSfx(result.cursorPosition)
|
|
2203
2324
|
gridPosition.current.set(...result.gridPosition)
|
|
2204
2325
|
const ic = worldToBuildingLocal(...result.cursorPosition)
|
|
2205
2326
|
cursorGroupRef.current.position.set(ic.x, ic.y, ic.z)
|
|
@@ -2209,7 +2330,10 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2209
2330
|
if (draft) {
|
|
2210
2331
|
draft.position = result.gridPosition
|
|
2211
2332
|
const mesh = sceneRegistry.nodes.get(draft.id)
|
|
2212
|
-
if (mesh)
|
|
2333
|
+
if (mesh)
|
|
2334
|
+
mesh.position.set(
|
|
2335
|
+
...surfaceFramePose(draft.parentId, surfaceAttachmentId(draft), draft, true).position,
|
|
2336
|
+
)
|
|
2213
2337
|
useLiveTransforms.getState().set(draft.id, {
|
|
2214
2338
|
position: result.cursorPosition,
|
|
2215
2339
|
rotation: result.cursorRotationY,
|
|
@@ -2233,14 +2357,16 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2233
2357
|
}
|
|
2234
2358
|
|
|
2235
2359
|
const onShelfClick = (event: ShelfEvent) => {
|
|
2236
|
-
|
|
2360
|
+
gridDispatch.flush()
|
|
2361
|
+
if (feedback.reason) return
|
|
2362
|
+
const result = shelfSurfaceStrategy.click(surfaceContext(event), event)
|
|
2237
2363
|
if (!result) return
|
|
2238
2364
|
|
|
2239
2365
|
event.stopPropagation()
|
|
2240
2366
|
const { committedId, wasAdopted } = commitDraft(result.nodeUpdate)
|
|
2241
2367
|
|
|
2242
2368
|
finishCommittedPlacement(committedId, wasAdopted, () => {
|
|
2243
|
-
const enterResult = shelfSurfaceStrategy.enter(
|
|
2369
|
+
const enterResult = shelfSurfaceStrategy.enter(surfaceContext(event), event)
|
|
2244
2370
|
if (enterResult) {
|
|
2245
2371
|
applyTransition(enterResult)
|
|
2246
2372
|
} else {
|
|
@@ -2286,7 +2412,49 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2286
2412
|
const currentRotation = draft.rotation
|
|
2287
2413
|
// Round to the nearest 45° then step, matching the placed-item R/T.
|
|
2288
2414
|
const newRotationY = steppedRotation(currentRotation[1] ?? 0, rotationDir)
|
|
2289
|
-
|
|
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
|
|
2290
2458
|
|
|
2291
2459
|
// Rotate the building-local cursor by the same delta as the host-local
|
|
2292
2460
|
// draft. This preserves the host's composed yaw for items resting on a
|
|
@@ -2298,7 +2466,6 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2298
2466
|
if (mesh) mesh.rotation.y = newRotationY
|
|
2299
2467
|
|
|
2300
2468
|
// Re-snap position immediately with updated rotation (dimX/dimZ may swap at 90°)
|
|
2301
|
-
const surface = placementState.current.surface
|
|
2302
2469
|
if (surface === 'floor' || surface === 'ceiling') {
|
|
2303
2470
|
const dims = getScaledDimensions(draft)
|
|
2304
2471
|
const [dimX, , dimZ] = dims
|
|
@@ -2328,15 +2495,8 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2328
2495
|
}
|
|
2329
2496
|
}
|
|
2330
2497
|
} else if (surface === 'item-surface' && placementState.current.surfaceItemId) {
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
const localPos = surfaceMesh.worldToLocal(lastRawPos.current.clone())
|
|
2334
|
-
const dims = getScaledDimensions(draft)
|
|
2335
|
-
const [dimX, , dimZ] = dims
|
|
2336
|
-
const swapDims = Math.abs(Math.sin(newRotationY)) > 0.9
|
|
2337
|
-
const x = snapToGrid(localPos.x, swapDims ? dimZ : dimX)
|
|
2338
|
-
const z = snapToGrid(localPos.z, swapDims ? dimX : dimZ)
|
|
2339
|
-
const y = gridPosition.current.y
|
|
2498
|
+
if (surfaceMesh && surfacePosition) {
|
|
2499
|
+
const [x, y, z] = surfacePosition
|
|
2340
2500
|
gridPosition.current.set(x, y, z)
|
|
2341
2501
|
draft.position = [x, y, z]
|
|
2342
2502
|
const worldSnapped = surfaceMesh.localToWorld(new Vector3(x, y, z))
|
|
@@ -2362,7 +2522,21 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2362
2522
|
// Keep both preview renderers on the rotated draft. The item renderer
|
|
2363
2523
|
// consumes live node overrides, while the floor-plan renderer consumes
|
|
2364
2524
|
// the live transform and placement-preview snapshot.
|
|
2365
|
-
|
|
2525
|
+
const storedSurfacePose = surfaceFramePose(
|
|
2526
|
+
draft.parentId,
|
|
2527
|
+
surfaceAttachmentId(draft),
|
|
2528
|
+
draft,
|
|
2529
|
+
true,
|
|
2530
|
+
)
|
|
2531
|
+
if (surfaceAttachmentId(draft)) {
|
|
2532
|
+
draftNode.updateSurface(
|
|
2533
|
+
{ position: draft.position, rotation: draft.rotation },
|
|
2534
|
+
surfaceAttachmentId(draft),
|
|
2535
|
+
)
|
|
2536
|
+
mesh?.position.set(...storedSurfacePose.position)
|
|
2537
|
+
mesh?.rotation.set(...storedSurfacePose.rotation)
|
|
2538
|
+
}
|
|
2539
|
+
useLiveNodeOverrides.getState().set(draft.id, { rotation: storedSurfacePose.rotation })
|
|
2366
2540
|
const currentLive = useLiveTransforms.getState().get(draft.id)
|
|
2367
2541
|
const livePosition: [number, number, number] =
|
|
2368
2542
|
surface === 'floor'
|
|
@@ -2395,7 +2569,13 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2395
2569
|
)
|
|
2396
2570
|
}
|
|
2397
2571
|
|
|
2398
|
-
|
|
2572
|
+
if (feedback.reason && lastSurfaceEvent) {
|
|
2573
|
+
if (lastSurfaceEvent.node.type === 'shelf') onShelfMove(lastSurfaceEvent as ShelfEvent)
|
|
2574
|
+
else onItemMove(lastSurfaceEvent as NodeEvent<AnyNode>)
|
|
2575
|
+
} else {
|
|
2576
|
+
feedback.clear()
|
|
2577
|
+
revalidate()
|
|
2578
|
+
}
|
|
2399
2579
|
}
|
|
2400
2580
|
}
|
|
2401
2581
|
|
|
@@ -2474,7 +2654,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2474
2654
|
const unsubDraftWatch = useScene.subscribe((state) => {
|
|
2475
2655
|
if (tearingDown) return
|
|
2476
2656
|
const draft = draftNode.current
|
|
2477
|
-
if (draft === null) return
|
|
2657
|
+
if (draft === null || isFreshPlacementMetadata(draft.metadata)) return
|
|
2478
2658
|
if (draft.id in state.nodes) return
|
|
2479
2659
|
|
|
2480
2660
|
queueMicrotask(() => {
|
|
@@ -2489,32 +2669,73 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2489
2669
|
|
|
2490
2670
|
// ---- Subscribe ----
|
|
2491
2671
|
|
|
2492
|
-
|
|
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')
|
|
2704
|
+
const gridDispatch = createItemSurfaceGridDispatch(onGridMove)
|
|
2705
|
+
emitter.on('grid:move', gridDispatch.schedule)
|
|
2493
2706
|
emitter.on('grid:click', onGridClick)
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
emitter.on('node:enter',
|
|
2507
|
-
emitter.on('node:move',
|
|
2508
|
-
emitter.on('node:click',
|
|
2509
|
-
emitter.on('node:leave',
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
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)
|
|
2518
2739
|
|
|
2519
2740
|
// A floor placement commits at the tracked floor cursor (`gridPosition`),
|
|
2520
2741
|
// which keeps following the floor even when the click ray lands on a wall
|
|
@@ -2534,6 +2755,7 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2534
2755
|
|
|
2535
2756
|
return () => {
|
|
2536
2757
|
tearingDown = true
|
|
2758
|
+
feedback.clear()
|
|
2537
2759
|
if (dragMode) window.removeEventListener('pointerup', onReleaseCommit)
|
|
2538
2760
|
unsubDraftWatch()
|
|
2539
2761
|
useAlignmentGuides.getState().clear()
|
|
@@ -2544,7 +2766,8 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2544
2766
|
}
|
|
2545
2767
|
draftNode.destroy()
|
|
2546
2768
|
useScene.temporal.getState().resume()
|
|
2547
|
-
|
|
2769
|
+
gridDispatch.cancel()
|
|
2770
|
+
emitter.off('grid:move', gridDispatch.schedule)
|
|
2548
2771
|
emitter.off('grid:click', onGridClick)
|
|
2549
2772
|
emitter.off('item:enter', onItemEnter)
|
|
2550
2773
|
emitter.off('item:move', onItemMove)
|
|
@@ -2558,14 +2781,22 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2558
2781
|
emitter.off('roof:move', onRoofWallMove)
|
|
2559
2782
|
emitter.off('roof:click', onRoofWallClick)
|
|
2560
2783
|
emitter.off('roof:leave', onRoofWallLeave)
|
|
2561
|
-
emitter.off('node:enter',
|
|
2562
|
-
emitter.off('node:move',
|
|
2563
|
-
emitter.off('node:click',
|
|
2564
|
-
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)
|
|
2565
2788
|
emitter.off('ceiling:enter', onCeilingEnter)
|
|
2566
2789
|
emitter.off('ceiling:move', onCeilingMove)
|
|
2567
2790
|
emitter.off('ceiling:click', onCeilingClick)
|
|
2568
2791
|
emitter.off('ceiling:leave', onCeilingLeave)
|
|
2792
|
+
emitter.off('cabinet:enter', onItemEnter)
|
|
2793
|
+
emitter.off('procedural-item:enter' as never, onItemEnter)
|
|
2794
|
+
emitter.off('cabinet:move', onItemMove)
|
|
2795
|
+
emitter.off('procedural-item:move' as never, onItemMove)
|
|
2796
|
+
emitter.off('cabinet:click', onItemClick)
|
|
2797
|
+
emitter.off('procedural-item:click' as never, onItemClick)
|
|
2798
|
+
emitter.off('cabinet:leave', onItemLeave)
|
|
2799
|
+
emitter.off('procedural-item:leave' as never, onItemLeave)
|
|
2569
2800
|
emitter.off('shelf:enter', onShelfEnter)
|
|
2570
2801
|
emitter.off('shelf:move', onShelfMove)
|
|
2571
2802
|
emitter.off('shelf:click', onShelfClick)
|
|
@@ -2623,6 +2854,9 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2623
2854
|
if (
|
|
2624
2855
|
draftParent?.type === 'item' ||
|
|
2625
2856
|
draftParent?.type === 'shelf' ||
|
|
2857
|
+
draftParent?.type === 'cabinet' ||
|
|
2858
|
+
draftParent?.type === 'procedural-item' ||
|
|
2859
|
+
(draftParent && canHostSurfaceChild(draftParent, draft.type, draft.id)) ||
|
|
2626
2860
|
(draftParent &&
|
|
2627
2861
|
nodeRegistry.get(draftParent.type)?.capabilities.faceHost?.currentFaceId(draft))
|
|
2628
2862
|
)
|
|
@@ -2641,8 +2875,8 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2641
2875
|
// instead of drawing an inline triangle. Only this coordinator publishes — a
|
|
2642
2876
|
// moving existing node has no draft here, so the grid reads that case straight
|
|
2643
2877
|
// off the node's mesh. Cleared when idle.
|
|
2644
|
-
const surfaceNormalRef = useRef(new Vector3(0, 1, 0))
|
|
2645
2878
|
const surfaceWorldPointRef = useRef(new Vector3())
|
|
2879
|
+
const surfaceNormalRef = useRef(new Vector3(0, 1, 0))
|
|
2646
2880
|
const facingForwardRef = useRef(new Vector3(0, 0, 1))
|
|
2647
2881
|
const facingQuatRef = useRef(new Quaternion())
|
|
2648
2882
|
const ghostSurfaceQuatRef = useRef(new Quaternion())
|