Skip to content

Commit 37b9cef

Browse files
committed
raycasting undraggable layer bug finally fixed!
1 parent 062d269 commit 37b9cef

2 files changed

Lines changed: 28 additions & 2 deletions

File tree

frontend/src/actions/drag_controls.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,19 @@ describe('layer drag', () => {
5656
expect(ctx.renderNodeLabelsFlag).toBe(true)
5757
})
5858

59+
it('drags a plane sitting behind the ortho ray origin (zoomed-in regression)', () => {
60+
// Zooming in pushes a tilted layer's world z past the ortho ray origin
61+
// (~camera z=100). THREE.Ray.intersectPlane rejects t<0, which used to
62+
// silently freeze the drag even though hover still highlighted the plane.
63+
ctx.layers[0].plane.position.z = 300 // behind the origin
64+
ctx.scene!.THREE_Object.updateMatrixWorld(true)
65+
ctx.lastHoveredLayerIndex = 0
66+
onPointerDown({ clientX: 400, clientY: 400 })
67+
ctx.scene!.leftClickPressed = true
68+
onPointerMove({ clientX: 500, clientY: 400, pointerType: 'mouse' })
69+
expect(ctx.layers[0].plane.position.x).toBeCloseTo(100)
70+
})
71+
5972
it('does not engage while a node is hovered (v2 gate)', () => {
6073
ctx.lastHoveredLayerIndex = 0
6174
ctx.lastHoveredNodeIndex = 3

frontend/src/actions/drag_controls.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,19 @@ function layerPlanes(): THREE.Object3D[] {
5959
return ctx.layers.map(({ plane }) => plane)
6060
}
6161

62+
// Orthographic `setFromCamera` puts the ray origin at the mid-depth plane
63+
// (NDC z=0, world z ~= camera z). A layer tilted + zoom-scaled far enough sits
64+
// BEHIND that origin, and THREE.Ray.intersectPlane rejects t<0, so layer drag
65+
// silently stopped working once zoomed in. The screen-facing drag plane is
66+
// crossed by the cursor ray exactly once — take that point regardless of sign.
67+
function rayPlanePoint(target: THREE.Vector3): THREE.Vector3 | null {
68+
const denom = _plane.normal.dot(_raycaster.ray.direction)
69+
if (denom === 0) return null
70+
const t =
71+
-(_raycaster.ray.origin.dot(_plane.normal) + _plane.constant) / denom
72+
return _raycaster.ray.at(t, target)
73+
}
74+
6275
export function onPointerDown(event: PointerLikeEvent): void {
6376
if (!surface || !ctx.camera) return
6477
// Don't re-raycast here: the pointer-down ray kept landing on other
@@ -79,7 +92,7 @@ export function onPointerDown(event: PointerLikeEvent): void {
7992
ctx.camera.getWorldDirection(_plane.normal),
8093
_worldPosition.setFromMatrixPosition(selected.matrixWorld)
8194
)
82-
if (_raycaster.ray.intersectPlane(_plane, _intersection)) {
95+
if (rayPlanePoint(_intersection)) {
8396
_inverseMatrix.copy(selected.parent!.matrixWorld).invert()
8497
_offset
8598
.copy(_intersection)
@@ -100,7 +113,7 @@ export function onPointerMove(event: PointerLikeEvent): void {
100113
ctx.scene?.leftClickPressed &&
101114
findIndexByUuid(layerPlanes(), selected.uuid) !== -1
102115
) {
103-
if (_raycaster.ray.intersectPlane(_plane, _intersection)) {
116+
if (rayPlanePoint(_intersection)) {
104117
ctx.renderLayerLabelsFlag = true
105118
ctx.renderNodeLabelsFlag = true
106119
ctx.renderInterLayerEdgesFlag = true // edges follow the dragged layer

0 commit comments

Comments
 (0)