@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
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
type AnyNodeId,
|
|
9
9
|
analyzePortConnectivity,
|
|
10
10
|
bboxCornerAnchors,
|
|
11
|
+
cascadeDirty,
|
|
11
12
|
collectAlignmentAnchors,
|
|
12
13
|
createSceneApi,
|
|
13
14
|
emitter,
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
getFloorPlacedFootprints,
|
|
19
20
|
type MovableConfig,
|
|
20
21
|
movingFootprintAnchors,
|
|
22
|
+
NON_PHYSICAL_HOST_KINDS,
|
|
21
23
|
type NodeEvent,
|
|
22
24
|
nodeRegistry,
|
|
23
25
|
type ParentFrameSnapMatch,
|
|
@@ -27,6 +29,7 @@ import {
|
|
|
27
29
|
resolveFacingIndicator,
|
|
28
30
|
resolveFrozenFloorPlacementPatch,
|
|
29
31
|
resolveSupportSlabPatch,
|
|
32
|
+
type SurfaceRejectReason,
|
|
30
33
|
sceneRegistry,
|
|
31
34
|
spatialGridManager,
|
|
32
35
|
useLiveNodeOverrides,
|
|
@@ -36,7 +39,7 @@ import {
|
|
|
36
39
|
import { useViewer } from '@pascal-app/viewer'
|
|
37
40
|
import { useFrame, useThree } from '@react-three/fiber'
|
|
38
41
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
39
|
-
import type
|
|
42
|
+
import { type Group, Vector3 } from 'three'
|
|
40
43
|
import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
|
|
41
44
|
import { commitFreshPlacementSubtree } from '../../../lib/fresh-planar-placement'
|
|
42
45
|
import { stripPlacementMetadataFlags } from '../../../lib/placement-metadata'
|
|
@@ -45,10 +48,14 @@ import {
|
|
|
45
48
|
resolvePrioritizedPlanarCursorPosition,
|
|
46
49
|
} from '../../../lib/planar-cursor-placement'
|
|
47
50
|
import { resolveAttachmentPreviewRotation } from '../../../lib/rigid-plan-svg-transform'
|
|
48
|
-
import {
|
|
51
|
+
import { createMovementSfxTick } from '../../../lib/sfx/movement-tick'
|
|
49
52
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
50
53
|
import { resolveSnapFlags } from '../../../lib/snapping-mode'
|
|
51
|
-
|
|
54
|
+
import {
|
|
55
|
+
surfaceAttachmentId,
|
|
56
|
+
surfaceAttachmentUpdates,
|
|
57
|
+
updateSurfaceNode,
|
|
58
|
+
} from '../../../lib/surface-attachment'
|
|
52
59
|
import useAlignmentGuides from '../../../store/use-alignment-guides'
|
|
53
60
|
import useEditor, {
|
|
54
61
|
getActiveSnappingMode,
|
|
@@ -56,7 +63,6 @@ import useEditor, {
|
|
|
56
63
|
isGridSnapActive,
|
|
57
64
|
isMagneticSnapActive,
|
|
58
65
|
} from '../../../store/use-editor'
|
|
59
|
-
|
|
60
66
|
import useFacingPose from '../../../store/use-facing-pose'
|
|
61
67
|
import { swallowNextClick } from '../../editor/node-arrow-handles'
|
|
62
68
|
import { CursorSphere } from '../shared/cursor-sphere'
|
|
@@ -68,6 +74,7 @@ import {
|
|
|
68
74
|
type PointerSupportSurface,
|
|
69
75
|
resolvePointerSupportSurface,
|
|
70
76
|
} from '../shared/pointer-support-cap'
|
|
77
|
+
import { createItemSurfaceGridDispatch, createRegistryItemSurfaceMove } from './item-surface-move'
|
|
71
78
|
|
|
72
79
|
/** Snap a world-plan coordinate to the editor's active grid step (0.5 / 0.25
|
|
73
80
|
* / 0.1 / 0.05), read live so changing the step mid-drag takes effect. */
|
|
@@ -197,18 +204,6 @@ const ALIGNMENT_THRESHOLD_M = 0.08
|
|
|
197
204
|
/**
|
|
198
205
|
* Generic move tool for any registry-backed kind.
|
|
199
206
|
*
|
|
200
|
-
* Imperative-only motion during drag:
|
|
201
|
-
* - On every `grid:move` we mutate `sceneRegistry.nodes.get(id).position`
|
|
202
|
-
* directly. The node's store data is unchanged → the renderer doesn't
|
|
203
|
-
* re-render → R3F doesn't reapply `position={node.position}` → the
|
|
204
|
-
* imperative mutation sticks. Movement is smooth, framerate-locked,
|
|
205
|
-
* and React-free.
|
|
206
|
-
*
|
|
207
|
-
* Store update happens only on commit (single undoable action).
|
|
208
|
-
*
|
|
209
|
-
* Cancel imperatively snaps the mesh back to its original position and
|
|
210
|
-
* resumes history without ever having touched the store mid-drag.
|
|
211
|
-
*
|
|
212
207
|
* **Commit triggers**: the tool listens for `grid:click` and the generic
|
|
213
208
|
* `node:click` event. A click on the grid plane fires `grid:click`; a click
|
|
214
209
|
* on the moved node itself (or any other 3D geometry the ray happens to land
|
|
@@ -224,8 +219,11 @@ const ALIGNMENT_THRESHOLD_M = 0.08
|
|
|
224
219
|
type ClickTriggerEvent = GridEvent | NodeEvent<AnyNode>
|
|
225
220
|
|
|
226
221
|
export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
222
|
+
const itemSurfaceMove = useMemo(() => createRegistryItemSurfaceMove(node), [node])
|
|
223
|
+
const suppressRaycastsRef = useRef<(() => void) | null>(null)
|
|
227
224
|
const previewGroupRef = useRef<Group>(null)
|
|
228
225
|
useFrame(() => {
|
|
226
|
+
suppressRaycastsRef.current?.()
|
|
229
227
|
if (!previewGroupRef.current) return
|
|
230
228
|
const nodes = useScene.getState().nodes
|
|
231
229
|
const parentId = nodes[node.id]?.parentId ?? node.parentId
|
|
@@ -282,7 +280,6 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
282
280
|
return 0
|
|
283
281
|
}, [node])
|
|
284
282
|
const [cursorPosition, setCursorPosition] = useState<[number, number, number]>(originalPosition)
|
|
285
|
-
const previousSnapRef = useRef<string | null>(null)
|
|
286
283
|
/**
|
|
287
284
|
* The latest snapped cursor position from `grid:move`. We commit at
|
|
288
285
|
* THIS position regardless of which event variant fires the click —
|
|
@@ -362,20 +359,24 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
362
359
|
[collides, parentFrameCollides, resolvedFootprint],
|
|
363
360
|
)
|
|
364
361
|
const [valid, setValid] = useState(true)
|
|
362
|
+
const [surfaceRejection, setSurfaceRejection] = useState<SurfaceRejectReason | null>(null)
|
|
365
363
|
const previewRotationY = useCallback(
|
|
366
364
|
(rotationY = rotationRef.current) =>
|
|
367
365
|
parentFrame && frameParent
|
|
368
366
|
? parentFrame.parentRotationY(frameParent, useScene.getState().nodes) + rotationY
|
|
369
|
-
:
|
|
370
|
-
|
|
367
|
+
: itemSurfaceMove?.hosted
|
|
368
|
+
? itemSurfaceMove.planPose(lastCursorRef.current, rotationY).rotationY
|
|
369
|
+
: rotationY,
|
|
370
|
+
[parentFrame, frameParent, itemSurfaceMove],
|
|
371
371
|
)
|
|
372
372
|
const visualPositionFor = useCallback(
|
|
373
373
|
(position: [number, number, number], rotationY = rotationRef.current) => {
|
|
374
374
|
if (parentFrame && frameParent) {
|
|
375
375
|
return parentFrame.localToPlan(frameParent, position, useScene.getState().nodes)
|
|
376
376
|
}
|
|
377
|
+
if (itemSurfaceMove?.hosted) return itemSurfaceMove.planPose(position, rotationY).position
|
|
377
378
|
return getFloorStackPreviewPosition({
|
|
378
|
-
node,
|
|
379
|
+
node: useScene.getState().nodes[node.id] ?? node,
|
|
379
380
|
position,
|
|
380
381
|
rotation: (() => {
|
|
381
382
|
const r = (node as { rotation?: unknown }).rotation
|
|
@@ -386,7 +387,7 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
386
387
|
maxElevation: supportCapRef.current,
|
|
387
388
|
})
|
|
388
389
|
},
|
|
389
|
-
[parentFrame, frameParent, node],
|
|
390
|
+
[parentFrame, frameParent, node, itemSurfaceMove],
|
|
390
391
|
)
|
|
391
392
|
const canonicalPositionFromPlan = useCallback(
|
|
392
393
|
(planX: number, localY: number, planZ: number): [number, number, number] =>
|
|
@@ -432,7 +433,7 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
432
433
|
|
|
433
434
|
useEffect(() => {
|
|
434
435
|
useScene.temporal.getState().pause()
|
|
435
|
-
|
|
436
|
+
const movementSfx = createMovementSfxTick()
|
|
436
437
|
dragAnchorRef.current = null
|
|
437
438
|
hasMovedRef.current = false
|
|
438
439
|
rotationRef.current = originalRotationY
|
|
@@ -451,14 +452,22 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
451
452
|
// would keep the previous clone's rotation/position until the next R/T.
|
|
452
453
|
setCursorRotationY(previewRotationY(originalRotationY))
|
|
453
454
|
lastCursorRef.current = originalPosition
|
|
455
|
+
let floorY = originalPosition[1]
|
|
456
|
+
let centeredAfterHost = false
|
|
454
457
|
let committed = false
|
|
455
458
|
const isNew = isFreshPlacement
|
|
456
459
|
|
|
457
460
|
const baseRotation = (node as { rotation?: unknown }).rotation
|
|
458
|
-
const toCommitRotation = (y: number): number | [number, number, number] =>
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
:
|
|
461
|
+
const toCommitRotation = (y: number): number | [number, number, number] => {
|
|
462
|
+
const live = useScene.getState().nodes[node.id]
|
|
463
|
+
const rotation =
|
|
464
|
+
live && surfaceAttachmentId(live) && 'rotation' in live ? live.rotation : baseRotation
|
|
465
|
+
return Array.isArray(rotation) ? [rotation[0] ?? 0, y, rotation[2] ?? 0] : y
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const currentLevelId = () =>
|
|
469
|
+
useViewer.getState().selection.levelId ??
|
|
470
|
+
(itemSurfaceMove ? findLevelAncestorId(node.id, useScene.getState().nodes) : node.parentId)
|
|
462
471
|
|
|
463
472
|
const getVisualPosition = visualPositionFor
|
|
464
473
|
const applyMeshPose = (position: [number, number, number], rotationY = rotationRef.current) => {
|
|
@@ -469,7 +478,8 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
469
478
|
}
|
|
470
479
|
const markMovedNodeDirty = () => {
|
|
471
480
|
if (useScene.getState().nodes[node.id]) {
|
|
472
|
-
|
|
481
|
+
for (const id of cascadeDirty(node.id as AnyNodeId, { scene: createSceneApi(useScene) }))
|
|
482
|
+
useScene.getState().markDirty(id)
|
|
473
483
|
}
|
|
474
484
|
}
|
|
475
485
|
|
|
@@ -563,10 +573,18 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
563
573
|
// override so the user can drop on top of an existing item on purpose. Only
|
|
564
574
|
// shelves show the box, so this no-ops for every other movable kind.
|
|
565
575
|
const recomputeValidity = () => {
|
|
576
|
+
const rejection = itemSurfaceMove?.rejection ?? null
|
|
577
|
+
setSurfaceRejection(rejection)
|
|
578
|
+
if (rejection) {
|
|
579
|
+
validRef.current = false
|
|
580
|
+
setValid(false)
|
|
581
|
+
return
|
|
582
|
+
}
|
|
566
583
|
if (!boxDimensions && !movableValidityConfig) return
|
|
567
|
-
if (altRef.current) {
|
|
568
|
-
|
|
569
|
-
|
|
584
|
+
if (altRef.current || itemSurfaceMove?.hosted) {
|
|
585
|
+
const valid = !itemSurfaceMove?.hosted || itemSurfaceMove.valid
|
|
586
|
+
validRef.current = valid
|
|
587
|
+
setValid(valid)
|
|
570
588
|
return
|
|
571
589
|
}
|
|
572
590
|
if (parentFrameCollides && frameParent && parentFrame?.isValidPosition) {
|
|
@@ -584,7 +602,7 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
584
602
|
setValid(validPosition)
|
|
585
603
|
return
|
|
586
604
|
}
|
|
587
|
-
const levelId =
|
|
605
|
+
const levelId = currentLevelId()
|
|
588
606
|
if (!levelId) {
|
|
589
607
|
validRef.current = true
|
|
590
608
|
setValid(true)
|
|
@@ -594,7 +612,7 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
594
612
|
const liveRotation = previewRotationY(rotationRef.current)
|
|
595
613
|
const floorPlaced = nodeRegistry.get(node.type)?.capabilities?.floorPlaced
|
|
596
614
|
const effectiveNode = {
|
|
597
|
-
...(node as Record<string, unknown>),
|
|
615
|
+
...((useScene.getState().nodes[node.id] ?? node) as Record<string, unknown>),
|
|
598
616
|
position: livePosition,
|
|
599
617
|
rotation: Array.isArray((node as { rotation?: unknown }).rotation)
|
|
600
618
|
? [
|
|
@@ -651,10 +669,12 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
651
669
|
// passes through the moved mesh and continues to the grid plane,
|
|
652
670
|
// so `grid:move` keeps firing and the cursor tracks correctly.
|
|
653
671
|
// We restore the original raycast on cleanup.
|
|
654
|
-
const mesh = sceneRegistry.nodes.get(node.id)
|
|
655
672
|
const restoreRaycasts: Array<() => void> = []
|
|
656
|
-
|
|
657
|
-
|
|
673
|
+
const suppressed = new WeakSet<object>()
|
|
674
|
+
const suppressRaycasts = () => {
|
|
675
|
+
sceneRegistry.nodes.get(node.id)?.traverse((child) => {
|
|
676
|
+
if (suppressed.has(child)) return
|
|
677
|
+
suppressed.add(child)
|
|
658
678
|
const original = child.raycast
|
|
659
679
|
child.raycast = () => {}
|
|
660
680
|
restoreRaycasts.push(() => {
|
|
@@ -662,6 +682,9 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
662
682
|
})
|
|
663
683
|
})
|
|
664
684
|
}
|
|
685
|
+
// Reparenting remounts the renderer, so new meshes must also let the cursor ray through.
|
|
686
|
+
suppressRaycastsRef.current = suppressRaycasts
|
|
687
|
+
suppressRaycasts()
|
|
665
688
|
|
|
666
689
|
// Static alignment candidates — anchors of every OTHER alignable object
|
|
667
690
|
// (items, walls, fences, slabs, ceilings, columns) ON THE SAME LEVEL,
|
|
@@ -673,7 +696,7 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
673
696
|
const alignmentCandidates = collectAlignmentAnchors(
|
|
674
697
|
useScene.getState().nodes,
|
|
675
698
|
node.id,
|
|
676
|
-
|
|
699
|
+
currentLevelId(),
|
|
677
700
|
)
|
|
678
701
|
|
|
679
702
|
// Connectivity snapshot (existing port-bearing nodes only — fresh
|
|
@@ -686,7 +709,76 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
686
709
|
if (snapshot.connections.length > 0) connectivityRef.current = snapshot
|
|
687
710
|
}
|
|
688
711
|
|
|
712
|
+
const applySurfacePose = (pose: { position: [number, number, number]; rotationY: number }) => {
|
|
713
|
+
lastCursorRef.current = pose.position
|
|
714
|
+
rotationRef.current = pose.rotationY
|
|
715
|
+
freeRotationRef.current = pose.rotationY
|
|
716
|
+
attachmentRotationRef.current = null
|
|
717
|
+
centeredAfterHost = true
|
|
718
|
+
dragAnchorRef.current = null
|
|
719
|
+
hasMovedRef.current = true
|
|
720
|
+
revealFreshPlacement()
|
|
721
|
+
setCursorPosition(getVisualPosition(pose.position))
|
|
722
|
+
setCursorRotationY(previewRotationY(pose.rotationY))
|
|
723
|
+
useAlignmentGuides.getState().clear()
|
|
724
|
+
recomputeValidity()
|
|
725
|
+
}
|
|
726
|
+
const detachSurface = (
|
|
727
|
+
worldPosition: [number, number, number],
|
|
728
|
+
leaveEvent?: NodeEvent<AnyNode>,
|
|
729
|
+
) => {
|
|
730
|
+
const pose = leaveEvent
|
|
731
|
+
? itemSurfaceMove?.leave(leaveEvent, rotationRef.current)
|
|
732
|
+
: itemSurfaceMove?.detach(worldPosition, rotationRef.current)
|
|
733
|
+
if (!pose) return
|
|
734
|
+
floorY = 0
|
|
735
|
+
const pointed = resolvePointerSupportSurface(cameraRef.current, worldPosition)
|
|
736
|
+
supportCapRef.current = pointed?.elevation ?? null
|
|
737
|
+
supportSurfaceRef.current = pointed
|
|
738
|
+
applySurfacePose(pose)
|
|
739
|
+
applyMeshPose(pose.position)
|
|
740
|
+
}
|
|
741
|
+
let lastSurfaceEvent: NodeEvent<AnyNode> | null = null
|
|
742
|
+
const onItemMove = (event: NodeEvent<AnyNode>) => {
|
|
743
|
+
if (committed || !resolvedFootprint || !itemSurfaceMove) return
|
|
744
|
+
lastSurfaceEvent = event
|
|
745
|
+
const pose = itemSurfaceMove.enter(event, resolvedFootprint, rotationRef.current)
|
|
746
|
+
if (pose) {
|
|
747
|
+
applySurfacePose(pose)
|
|
748
|
+
// Compare floor and host movement in the same plan frame, never host-local storage.
|
|
749
|
+
const point = new Vector3(...pose.worldPosition)
|
|
750
|
+
const levelId = currentLevelId()
|
|
751
|
+
if (levelId) sceneRegistry.nodes.get(levelId)?.worldToLocal(point)
|
|
752
|
+
movementSfx.tick({
|
|
753
|
+
coords: [point.x, point.z],
|
|
754
|
+
gridSnapActive: isGridSnapActive(),
|
|
755
|
+
gridStep: useEditor.getState().gridSnapStep,
|
|
756
|
+
})
|
|
757
|
+
} else {
|
|
758
|
+
if (
|
|
759
|
+
itemSurfaceMove.hosted &&
|
|
760
|
+
(itemSurfaceMove.rejection === 'footprint-outside-surface' ||
|
|
761
|
+
itemSurfaceMove.rejection === 'footprint-exceeds-host' ||
|
|
762
|
+
itemSurfaceMove.rejection === 'no-surface')
|
|
763
|
+
)
|
|
764
|
+
detachSurface(event.position)
|
|
765
|
+
recomputeValidity()
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
const onItemLeave = (event: NodeEvent<AnyNode>) => {
|
|
769
|
+
if (!itemSurfaceMove?.hosted || committed) return
|
|
770
|
+
if (event.node.id !== useScene.getState().nodes[node.id]?.parentId) return
|
|
771
|
+
event.stopPropagation()
|
|
772
|
+
detachSurface(event.position, event)
|
|
773
|
+
}
|
|
774
|
+
|
|
689
775
|
const onGridMove = (event: GridEvent) => {
|
|
776
|
+
const blocked = Boolean(itemSurfaceMove?.blocksGrid(event, cameraRef.current))
|
|
777
|
+
if (!committed && !blocked) {
|
|
778
|
+
itemSurfaceMove?.clearRejectionForGrid(event)
|
|
779
|
+
detachSurface(event.position)
|
|
780
|
+
}
|
|
781
|
+
if (committed || blocked) return
|
|
690
782
|
// The pointer decides the target surface AND the cursor plan point,
|
|
691
783
|
// both resolved from the true camera ray in one place. The event's
|
|
692
784
|
// own hit can't be used directly: its plane rides at the ghost's
|
|
@@ -707,7 +799,7 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
707
799
|
|
|
708
800
|
const magnetic = isMagneticSnapActive()
|
|
709
801
|
const attachmentEnabled = magnetic || isGridSnapActive()
|
|
710
|
-
const absolute = useAbsoluteCursorPlacement || cursorAttached
|
|
802
|
+
const absolute = useAbsoluteCursorPlacement || cursorAttached || centeredAfterHost
|
|
711
803
|
const centerOffset: [number, number, number] =
|
|
712
804
|
absolute && dragBounds?.center
|
|
713
805
|
? offsetPlanPositionByLocalCenter(
|
|
@@ -734,16 +826,13 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
734
826
|
// Kind-owned grid hooks exchange origins and apply their own footprint offsets.
|
|
735
827
|
candidatePosition: canonicalPositionFromPlan(
|
|
736
828
|
planX - centerOffset[0],
|
|
737
|
-
|
|
829
|
+
floorY,
|
|
738
830
|
planZ - centerOffset[2],
|
|
739
831
|
),
|
|
740
832
|
candidateRotation: freeRotationRef.current,
|
|
741
833
|
movingIds: [node.id as AnyNodeId],
|
|
742
834
|
nodes: useScene.getState().nodes as Record<string, AnyNode>,
|
|
743
|
-
levelId:
|
|
744
|
-
(useViewer.getState().selection.levelId as AnyNodeId | null) ??
|
|
745
|
-
(node.parentId as AnyNodeId | undefined) ??
|
|
746
|
-
null,
|
|
835
|
+
levelId: (currentLevelId() as AnyNodeId | null) ?? null,
|
|
747
836
|
gridStep: useEditor.getState().gridSnapStep,
|
|
748
837
|
})
|
|
749
838
|
const snappedPlanPosition = getVisualPosition(
|
|
@@ -761,15 +850,12 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
761
850
|
? ([planX, planZ]) => {
|
|
762
851
|
const snapArgs: Parameters<NonNullable<typeof groupMoveSnapPoseConfig>>[0] = {
|
|
763
852
|
node,
|
|
764
|
-
candidatePosition: canonicalPositionFromPlan(planX,
|
|
853
|
+
candidatePosition: canonicalPositionFromPlan(planX, floorY, planZ),
|
|
765
854
|
candidateRotation:
|
|
766
855
|
absolute && dragBounds?.center ? freeRotationRef.current : rotationRef.current,
|
|
767
856
|
movingIds: [node.id as AnyNodeId],
|
|
768
857
|
nodes: useScene.getState().nodes as Record<string, AnyNode>,
|
|
769
|
-
levelId:
|
|
770
|
-
(useViewer.getState().selection.levelId as AnyNodeId | null) ??
|
|
771
|
-
(node.parentId as AnyNodeId | undefined) ??
|
|
772
|
-
null,
|
|
858
|
+
levelId: (currentLevelId() as AnyNodeId | null) ?? null,
|
|
773
859
|
}
|
|
774
860
|
const snappedPosition: GroupMoveSnapResult | null = groupMoveSnapPoseConfig
|
|
775
861
|
? groupMoveSnapPoseConfig(snapArgs)
|
|
@@ -845,7 +931,7 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
845
931
|
}
|
|
846
932
|
}
|
|
847
933
|
|
|
848
|
-
let position = canonicalPositionFromPlan(x,
|
|
934
|
+
let position = canonicalPositionFromPlan(x, floorY, z)
|
|
849
935
|
if (
|
|
850
936
|
!attachmentSnapped &&
|
|
851
937
|
(magnetic || isGridSnapActive()) &&
|
|
@@ -885,7 +971,7 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
885
971
|
if (!parentFrame && pointed?.sourceNodeId) {
|
|
886
972
|
const rotation = toCommitRotation(rotationRef.current)
|
|
887
973
|
const effectiveNode = {
|
|
888
|
-
...(node as Record<string, unknown>),
|
|
974
|
+
...((useScene.getState().nodes[node.id] ?? node) as Record<string, unknown>),
|
|
889
975
|
position,
|
|
890
976
|
rotation,
|
|
891
977
|
} as AnyNode
|
|
@@ -930,15 +1016,26 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
930
1016
|
// Carry connected ductwork along (preview only — committed on drop).
|
|
931
1017
|
previewConnectivity(position, rotationRef.current)
|
|
932
1018
|
|
|
933
|
-
const
|
|
1019
|
+
const soundStep = {
|
|
934
1020
|
coords: [x, z],
|
|
935
1021
|
gridSnapActive: isGridSnapActive() && !attachmentSnapped,
|
|
936
1022
|
gridStep: useEditor.getState().gridSnapStep,
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
1023
|
+
}
|
|
1024
|
+
if (itemSurfaceMove) {
|
|
1025
|
+
movementSfx.schedule(
|
|
1026
|
+
soundStep,
|
|
1027
|
+
() => !itemSurfaceMove.blocksGrid(event, cameraRef.current),
|
|
1028
|
+
true,
|
|
1029
|
+
)
|
|
1030
|
+
} else movementSfx.tick(soundStep, true)
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
const gridDispatch = createItemSurfaceGridDispatch(onGridMove)
|
|
1034
|
+
const receiveGridMove = (event: GridEvent) => {
|
|
1035
|
+
if (itemSurfaceMove?.hosted) gridDispatch.schedule(event)
|
|
1036
|
+
else {
|
|
1037
|
+
gridDispatch.cancel()
|
|
1038
|
+
onGridMove(event)
|
|
942
1039
|
}
|
|
943
1040
|
}
|
|
944
1041
|
|
|
@@ -967,6 +1064,9 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
967
1064
|
// finds the fresh draft already deleted and takes the orphan re-create
|
|
968
1065
|
// path below, minting a hidden ghost copy and replaying the SFX.
|
|
969
1066
|
if (committed) return
|
|
1067
|
+
gridDispatch.flush()
|
|
1068
|
+
movementSfx.flush()
|
|
1069
|
+
if (itemSurfaceMove?.rejection || (itemSurfaceMove?.hosted && !itemSurfaceMove.valid)) return
|
|
970
1070
|
// Ignore a commit that fires before the cursor has moved into place —
|
|
971
1071
|
// it's the stray trailing click of whatever armed this move, not a
|
|
972
1072
|
// deliberate drop. Prevents preset re-arm from double-placing.
|
|
@@ -982,11 +1082,12 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
982
1082
|
|
|
983
1083
|
if (useScene.getState().nodes[node.id]) {
|
|
984
1084
|
const effectiveNode = {
|
|
985
|
-
...(node as Record<string, unknown>),
|
|
1085
|
+
...((useScene.getState().nodes[node.id] ?? node) as Record<string, unknown>),
|
|
986
1086
|
position,
|
|
987
1087
|
rotation,
|
|
988
1088
|
} as AnyNode
|
|
989
1089
|
const data = {
|
|
1090
|
+
...(itemSurfaceMove ? { parentId: effectiveNode.parentId } : {}),
|
|
990
1091
|
position,
|
|
991
1092
|
rotation,
|
|
992
1093
|
// The pointer cap makes the persisted host reproduce the capped
|
|
@@ -1027,10 +1128,16 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
1027
1128
|
buildPreviewNode(position, rotationRef.current),
|
|
1028
1129
|
).filter((u) => useScene.getState().nodes[u.id])
|
|
1029
1130
|
: []
|
|
1131
|
+
const surfaceId = surfaceAttachmentId(effectiveNode)
|
|
1132
|
+
itemSurfaceMove?.restore()
|
|
1030
1133
|
useScene.temporal.getState().resume()
|
|
1031
1134
|
useScene
|
|
1032
1135
|
.getState()
|
|
1033
|
-
.updateNodes([
|
|
1136
|
+
.updateNodes([
|
|
1137
|
+
{ id: node.id as AnyNodeId, data },
|
|
1138
|
+
...surfaceAttachmentUpdates(node.id, effectiveNode.parentId, surfaceId),
|
|
1139
|
+
...connectivityUpdates,
|
|
1140
|
+
])
|
|
1034
1141
|
// Kind-owned derived-state maintenance after a parent-frame move
|
|
1035
1142
|
// (cabinet run re-flow + linked corner-run re-anchor). Runs in the
|
|
1036
1143
|
// resumed window so its writes are undoable alongside the move.
|
|
@@ -1144,8 +1251,10 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
1144
1251
|
)
|
|
1145
1252
|
if (nextFreeRotation === null) return
|
|
1146
1253
|
sfxEmitter.emit('sfx:item-rotate')
|
|
1147
|
-
|
|
1254
|
+
const hostedPose = itemSurfaceMove?.rotate(nextFreeRotation)
|
|
1255
|
+
let position = hostedPose?.position ?? lastCursorRef.current
|
|
1148
1256
|
if (
|
|
1257
|
+
!hostedPose &&
|
|
1149
1258
|
hasMovedRef.current &&
|
|
1150
1259
|
(useAbsoluteCursorPlacement || cursorAttached) &&
|
|
1151
1260
|
dragBounds?.center
|
|
@@ -1161,25 +1270,28 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
1161
1270
|
previewRotationY(nextFreeRotation),
|
|
1162
1271
|
)
|
|
1163
1272
|
position = canonicalPositionFromPlan(planOrigin[0], position[1], planOrigin[2])
|
|
1164
|
-
lastCursorRef.current = position
|
|
1165
1273
|
}
|
|
1166
|
-
|
|
1274
|
+
lastCursorRef.current = position
|
|
1275
|
+
freeRotationRef.current = hostedPose?.rotationY ?? nextFreeRotation
|
|
1167
1276
|
rotationRef.current = freeRotationRef.current
|
|
1168
1277
|
setCursorRotationY(previewRotationY(rotationRef.current))
|
|
1169
1278
|
const visualPosition = getVisualPosition(position)
|
|
1170
1279
|
setCursorPosition(visualPosition)
|
|
1171
1280
|
applyMeshPose(position)
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1281
|
+
if (!itemSurfaceMove?.hosted)
|
|
1282
|
+
useLiveTransforms.getState().set(node.id, {
|
|
1283
|
+
position,
|
|
1284
|
+
rotation: rotationRef.current,
|
|
1285
|
+
supportElevationCap: supportCapRef.current ?? undefined,
|
|
1286
|
+
})
|
|
1177
1287
|
syncParentFramePreview(position)
|
|
1178
1288
|
markMovedNodeDirty()
|
|
1179
1289
|
// Rotating the fitting swings its collars — connected ducts follow.
|
|
1180
1290
|
previewConnectivity(position, rotationRef.current)
|
|
1181
1291
|
// Rotation changes the footprint's collision span — re-check validity.
|
|
1182
|
-
|
|
1292
|
+
if (!itemSurfaceMove?.hosted && itemSurfaceMove?.rejection && lastSurfaceEvent)
|
|
1293
|
+
onItemMove(lastSurfaceEvent)
|
|
1294
|
+
else recomputeValidity()
|
|
1183
1295
|
}
|
|
1184
1296
|
const onKeyUp = (e: KeyboardEvent) => {
|
|
1185
1297
|
if (e.key === 'Alt') {
|
|
@@ -1190,7 +1302,16 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
1190
1302
|
window.addEventListener('keydown', onKeyDown)
|
|
1191
1303
|
window.addEventListener('keyup', onKeyUp)
|
|
1192
1304
|
|
|
1193
|
-
|
|
1305
|
+
const surfaceKinds = Array.from(nodeRegistry.entries(), ([kind]) => kind).filter(
|
|
1306
|
+
(kind) => !NON_PHYSICAL_HOST_KINDS.includes(kind),
|
|
1307
|
+
)
|
|
1308
|
+
for (const kind of surfaceKinds) {
|
|
1309
|
+
emitter.on(`${kind}:enter` as never, onItemMove)
|
|
1310
|
+
emitter.on(`${kind}:move` as never, onItemMove)
|
|
1311
|
+
emitter.on(`${kind}:leave` as never, onItemLeave)
|
|
1312
|
+
emitter.on(`${kind}:click` as never, commitAtCursor)
|
|
1313
|
+
}
|
|
1314
|
+
emitter.on('grid:move', receiveGridMove)
|
|
1194
1315
|
emitter.on('grid:click', commitAtCursor)
|
|
1195
1316
|
|
|
1196
1317
|
const onPlacementDragPointerUp = (event: PointerEvent) => {
|
|
@@ -1211,12 +1332,16 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
1211
1332
|
emitter.on('node:click', commitAtCursor)
|
|
1212
1333
|
|
|
1213
1334
|
const onCancel = () => {
|
|
1335
|
+
movementSfx.cancel()
|
|
1336
|
+
gridDispatch.cancel()
|
|
1214
1337
|
useLiveTransforms.getState().clear(node.id)
|
|
1215
1338
|
clearConnectivityOverrides()
|
|
1216
1339
|
clearParentFramePreview()
|
|
1217
1340
|
if (isNew) {
|
|
1341
|
+
updateSurfaceNode(node.id, {}, null)
|
|
1218
1342
|
useScene.getState().deleteNode(node.id as AnyNodeId)
|
|
1219
1343
|
} else {
|
|
1344
|
+
itemSurfaceMove?.restore()
|
|
1220
1345
|
applyMeshPose(originalPosition, originalRotationY)
|
|
1221
1346
|
markMovedNodeDirty()
|
|
1222
1347
|
}
|
|
@@ -1228,9 +1353,18 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
1228
1353
|
emitter.on('tool:cancel', onCancel)
|
|
1229
1354
|
|
|
1230
1355
|
return () => {
|
|
1356
|
+
movementSfx.cancel()
|
|
1357
|
+
gridDispatch.cancel()
|
|
1231
1358
|
window.removeEventListener('keydown', onKeyDown)
|
|
1232
1359
|
window.removeEventListener('keyup', onKeyUp)
|
|
1233
|
-
|
|
1360
|
+
suppressRaycastsRef.current = null
|
|
1361
|
+
for (const kind of surfaceKinds) {
|
|
1362
|
+
emitter.off(`${kind}:enter` as never, onItemMove)
|
|
1363
|
+
emitter.off(`${kind}:move` as never, onItemMove)
|
|
1364
|
+
emitter.off(`${kind}:leave` as never, onItemLeave)
|
|
1365
|
+
emitter.off(`${kind}:click` as never, commitAtCursor)
|
|
1366
|
+
}
|
|
1367
|
+
emitter.off('grid:move', receiveGridMove)
|
|
1234
1368
|
emitter.off('grid:click', commitAtCursor)
|
|
1235
1369
|
window.removeEventListener('pointerup', onPlacementDragPointerUp)
|
|
1236
1370
|
emitter.off('node:click', commitAtCursor)
|
|
@@ -1242,10 +1376,11 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
1242
1376
|
// unmount / commit paths uniformly.
|
|
1243
1377
|
useAlignmentGuides.getState().clear()
|
|
1244
1378
|
const finalisedBy2D = useEditor.getState().movingNodeOrigin === '2d'
|
|
1245
|
-
if (!
|
|
1379
|
+
if (!committed && !finalisedBy2D && (!isNew || itemSurfaceMove)) {
|
|
1246
1380
|
useLiveTransforms.getState().clear(node.id)
|
|
1247
1381
|
clearConnectivityOverrides()
|
|
1248
1382
|
clearParentFramePreview()
|
|
1383
|
+
itemSurfaceMove?.restore()
|
|
1249
1384
|
applyMeshPose(originalPosition, originalRotationY)
|
|
1250
1385
|
markMovedNodeDirty()
|
|
1251
1386
|
}
|
|
@@ -1253,6 +1388,8 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
1253
1388
|
}
|
|
1254
1389
|
}, [
|
|
1255
1390
|
boxDimensions,
|
|
1391
|
+
resolvedFootprint,
|
|
1392
|
+
itemSurfaceMove,
|
|
1256
1393
|
dragBounds,
|
|
1257
1394
|
canonicalPositionFromPlan,
|
|
1258
1395
|
parentFrame,
|
|
@@ -1320,7 +1457,15 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
1320
1457
|
<DragBoundingBox
|
|
1321
1458
|
center={dragBounds?.center}
|
|
1322
1459
|
centerY={dragBounds?.centerY}
|
|
1323
|
-
color={
|
|
1460
|
+
color={
|
|
1461
|
+
surfaceRejection
|
|
1462
|
+
? INVALID_COLOR
|
|
1463
|
+
: boxDimensions
|
|
1464
|
+
? valid
|
|
1465
|
+
? VALID_COLOR
|
|
1466
|
+
: INVALID_COLOR
|
|
1467
|
+
: undefined
|
|
1468
|
+
}
|
|
1324
1469
|
nodeId={node.id}
|
|
1325
1470
|
position={cursorPosition}
|
|
1326
1471
|
rotationY={cursorRotationY}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
type AnyNode,
|
|
2
3
|
type AnyNodeId,
|
|
3
4
|
canHostOnTop,
|
|
4
5
|
GROUND_SUPPORT_ID,
|
|
@@ -10,10 +11,11 @@ import {
|
|
|
10
11
|
useScene,
|
|
11
12
|
} from '@pascal-app/core'
|
|
12
13
|
import { setSurfaceRaycastLayers, useViewer } from '@pascal-app/viewer'
|
|
13
|
-
import { type Camera,
|
|
14
|
+
import { type Camera, type Object3D, Raycaster, Vector3 } from 'three'
|
|
14
15
|
import { resolveTerrainGroundHit } from '../../../lib/ground-surface'
|
|
15
16
|
import { scopeNodeId } from '../../../lib/interaction/scope'
|
|
16
17
|
import useInteractionScope from '../../../store/use-interaction-scope'
|
|
18
|
+
import { surfaceWorldNormalY } from './surface-hit'
|
|
17
19
|
|
|
18
20
|
const originScratch = new Vector3()
|
|
19
21
|
const hitScratch = new Vector3()
|
|
@@ -23,8 +25,6 @@ const worldRayOrigin = new Vector3()
|
|
|
23
25
|
const worldRayDirection = new Vector3()
|
|
24
26
|
const nodeTopRaycaster = new Raycaster()
|
|
25
27
|
setSurfaceRaycastLayers(nodeTopRaycaster.layers)
|
|
26
|
-
const nodeTopNormal = new Vector3()
|
|
27
|
-
const nodeTopNormalMatrix = new Matrix3()
|
|
28
28
|
|
|
29
29
|
export type PointerSupportSurface = {
|
|
30
30
|
/** Level-local elevation of the pointed surface — the election cap. */
|
|
@@ -194,7 +194,7 @@ export function resolvePointerSupportSurface(
|
|
|
194
194
|
// convention — the election owns the invariant.
|
|
195
195
|
const interactingNodeId = scopeNodeId(useInteractionScope.getState().scope)
|
|
196
196
|
const isEligibleCandidate = (nodeId: AnyNodeId) => {
|
|
197
|
-
let current = nodes[nodeId]
|
|
197
|
+
let current: AnyNode | undefined = nodes[nodeId]
|
|
198
198
|
const visited = new Set<AnyNodeId>()
|
|
199
199
|
while (current && !visited.has(current.id)) {
|
|
200
200
|
if (current.id === interactingNodeId) return false
|
|
@@ -247,11 +247,13 @@ export function resolvePointerSupportSurface(
|
|
|
247
247
|
ownerObject = ownerObject.parent
|
|
248
248
|
}
|
|
249
249
|
if (belongsToNestedNode) continue
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
250
|
+
if (
|
|
251
|
+
surfaceWorldNormalY(
|
|
252
|
+
intersection.face.normal.toArray(),
|
|
253
|
+
intersection.object.matrixWorld,
|
|
254
|
+
) < 0.75
|
|
255
|
+
)
|
|
256
|
+
continue
|
|
255
257
|
nearest = {
|
|
256
258
|
distance: intersection.distance,
|
|
257
259
|
nodeId,
|