Changes
Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
import { useRef, ReactNode } from 'react';
|
import { useRef, ReactNode } from 'react';
|
||||||
import { useFrame } from '@react-three/fiber';
|
import { useFrame, useThree } from '@react-three/fiber';
|
||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import { useControllerGrab, ControllerGrabSnapshot } from '@/hooks/useControllerGrab';
|
import { useControllerGrab, ControllerGrabSnapshot } from '@/hooks/useControllerGrab';
|
||||||
|
import { useModelStore } from '@/stores/useModelStore';
|
||||||
|
|
||||||
interface XRGrabbableProps {
|
interface XRGrabbableProps {
|
||||||
/** Allow uniform scaling via two-hand grab (distance between hands) */
|
/** Allow uniform scaling via two-hand grab (distance between hands) */
|
||||||
@@ -42,18 +43,57 @@ const _tmpQuat = new THREE.Quaternion();
|
|||||||
* • Two-hand grab: midpoint = pivot, axis between hands = rotation,
|
* • Two-hand grab: midpoint = pivot, axis between hands = rotation,
|
||||||
* distance ratio = uniform scale (if allowScale).
|
* distance ratio = uniform scale (if allowScale).
|
||||||
*
|
*
|
||||||
* On release the group keeps its last pose (we don't write to the store —
|
* Parent re-attach strategy:
|
||||||
* the manual fine-tuning sliders remain available as additional offsets).
|
* At grab start the group is attached to the Scene root (preserving
|
||||||
|
* world-transform). At grab end it is re-attached to its original parent
|
||||||
|
* (typically XRHitTestPlacement), unless `isLocked` is true — in which case
|
||||||
|
* it stays anchored in world space and the original parent can no longer
|
||||||
|
* influence its matrix.
|
||||||
*/
|
*/
|
||||||
export function XRGrabbable({ allowScale = false, onGrabStart, children }: XRGrabbableProps) {
|
export function XRGrabbable({ allowScale = false, onGrabStart, children }: XRGrabbableProps) {
|
||||||
const groupRef = useRef<THREE.Group>(null);
|
const groupRef = useRef<THREE.Group>(null);
|
||||||
const grab = useControllerGrab();
|
const grab = useControllerGrab();
|
||||||
|
const { scene } = useThree();
|
||||||
|
|
||||||
const single = useRef<SingleGrabRecord | null>(null);
|
const single = useRef<SingleGrabRecord | null>(null);
|
||||||
const dual = useRef<DualGrabRecord | null>(null);
|
const dual = useRef<DualGrabRecord | null>(null);
|
||||||
|
/** Parent the group belonged to before the active grab started. */
|
||||||
|
const originalParent = useRef<THREE.Object3D | null>(null);
|
||||||
const haloRef = useRef<THREE.Mesh>(null);
|
const haloRef = useRef<THREE.Mesh>(null);
|
||||||
|
const lockHaloRef = useRef<THREE.Mesh>(null);
|
||||||
const everGrabbed = useRef(false);
|
const everGrabbed = useRef(false);
|
||||||
|
|
||||||
|
const beginGrab = (mode: 'single' | 'dual', hand: 'left' | 'right' | 'dual') => {
|
||||||
|
const group = groupRef.current;
|
||||||
|
if (!group) return;
|
||||||
|
// Reparent to scene root preserving world-transform.
|
||||||
|
if (group.parent && group.parent !== scene) {
|
||||||
|
originalParent.current = group.parent;
|
||||||
|
scene.attach(group);
|
||||||
|
console.log(`[XR][grab] attach→scene (mode=${mode} hand=${hand})`);
|
||||||
|
} else if (!originalParent.current) {
|
||||||
|
originalParent.current = group.parent ?? scene;
|
||||||
|
}
|
||||||
|
if (!everGrabbed.current) { everGrabbed.current = true; onGrabStart?.(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
const endGrab = () => {
|
||||||
|
const group = groupRef.current;
|
||||||
|
if (!group) return;
|
||||||
|
const locked = useModelStore.getState().isLocked;
|
||||||
|
if (locked) {
|
||||||
|
console.log('[XR][grab] release→scene (locked, staying anchored)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const target = originalParent.current;
|
||||||
|
originalParent.current = null;
|
||||||
|
if (target && target !== group.parent) {
|
||||||
|
const name = target === scene ? 'scene' : (target.name || target.type);
|
||||||
|
target.attach(group);
|
||||||
|
console.log(`[XR][grab] release→${name}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useFrame(() => {
|
useFrame(() => {
|
||||||
const group = groupRef.current;
|
const group = groupRef.current;
|
||||||
if (!group) return;
|
if (!group) return;
|
||||||
@@ -63,14 +103,20 @@ export function XRGrabbable({ allowScale = false, onGrabStart, children }: XRGra
|
|||||||
|
|
||||||
const lActive = L.isGrabbing && L.hasPose;
|
const lActive = L.isGrabbing && L.hasPose;
|
||||||
const rActive = R.isGrabbing && R.hasPose;
|
const rActive = R.isGrabbing && R.hasPose;
|
||||||
|
const isLocked = useModelStore.getState().isLocked;
|
||||||
|
|
||||||
// ─── Halo intensity (visual feedback for analog grip) ────────
|
// ─── Halo intensity (visual feedback for analog grip) ────────
|
||||||
if (haloRef.current) {
|
if (haloRef.current) {
|
||||||
const maxGrip = Math.max(L.gripValue, R.gripValue);
|
const maxGrip = isLocked ? 0 : Math.max(L.gripValue, R.gripValue);
|
||||||
const mat = haloRef.current.material as THREE.MeshBasicMaterial;
|
const mat = haloRef.current.material as THREE.MeshBasicMaterial;
|
||||||
mat.opacity = maxGrip * 0.25;
|
mat.opacity = maxGrip * 0.25;
|
||||||
haloRef.current.visible = maxGrip > 0.05;
|
haloRef.current.visible = maxGrip > 0.05;
|
||||||
}
|
}
|
||||||
|
if (lockHaloRef.current) {
|
||||||
|
const mat = lockHaloRef.current.material as THREE.MeshBasicMaterial;
|
||||||
|
mat.opacity = 0.18;
|
||||||
|
lockHaloRef.current.visible = isLocked;
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Two-hand mode ───────────────────────────────────────────
|
// ─── Two-hand mode ───────────────────────────────────────────
|
||||||
if (lActive && rActive) {
|
if (lActive && rActive) {
|
||||||
@@ -83,7 +129,9 @@ export function XRGrabbable({ allowScale = false, onGrabStart, children }: XRGra
|
|||||||
axisNow.divideScalar(distNow);
|
axisNow.divideScalar(distNow);
|
||||||
|
|
||||||
if (!dual.current) {
|
if (!dual.current) {
|
||||||
// Capture
|
// Capture — reparent BEFORE snapshotting matrixWorld so subsequent
|
||||||
|
// applyWorldMatrixToLocal math is consistent.
|
||||||
|
beginGrab('dual', 'dual');
|
||||||
group.updateMatrixWorld();
|
group.updateMatrixWorld();
|
||||||
dual.current = {
|
dual.current = {
|
||||||
startGroupWorld: group.matrixWorld.clone(),
|
startGroupWorld: group.matrixWorld.clone(),
|
||||||
@@ -93,7 +141,6 @@ export function XRGrabbable({ allowScale = false, onGrabStart, children }: XRGra
|
|||||||
startScale: group.scale.x,
|
startScale: group.scale.x,
|
||||||
};
|
};
|
||||||
single.current = null; // dual takes over
|
single.current = null; // dual takes over
|
||||||
if (!everGrabbed.current) { everGrabbed.current = true; onGrabStart?.(); }
|
|
||||||
console.log('[XR][grab] ◆ TWO-HAND start');
|
console.log('[XR][grab] ◆ TWO-HAND start');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,6 +170,7 @@ export function XRGrabbable({ allowScale = false, onGrabStart, children }: XRGra
|
|||||||
} else if (dual.current) {
|
} else if (dual.current) {
|
||||||
console.log('[XR][grab] ◆ TWO-HAND end');
|
console.log('[XR][grab] ◆ TWO-HAND end');
|
||||||
dual.current = null;
|
dual.current = null;
|
||||||
|
if (!lActive && !rActive) endGrab();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── One-hand mode ───────────────────────────────────────────
|
// ─── One-hand mode ───────────────────────────────────────────
|
||||||
@@ -130,18 +178,19 @@ export function XRGrabbable({ allowScale = false, onGrabStart, children }: XRGra
|
|||||||
if (activeHand) {
|
if (activeHand) {
|
||||||
const slot = activeHand === 'left' ? L : R;
|
const slot = activeHand === 'left' ? L : R;
|
||||||
if (!single.current || single.current.hand !== activeHand) {
|
if (!single.current || single.current.hand !== activeHand) {
|
||||||
|
beginGrab('single', activeHand);
|
||||||
group.updateMatrixWorld();
|
group.updateMatrixWorld();
|
||||||
const offset = new THREE.Matrix4()
|
const offset = new THREE.Matrix4()
|
||||||
.copy(slot.gripWorld).invert()
|
.copy(slot.gripWorld).invert()
|
||||||
.multiply(group.matrixWorld);
|
.multiply(group.matrixWorld);
|
||||||
single.current = { hand: activeHand, offset };
|
single.current = { hand: activeHand, offset };
|
||||||
if (!everGrabbed.current) { everGrabbed.current = true; onGrabStart?.(); }
|
|
||||||
console.log(`[XR][grab] ● ONE-HAND start (${activeHand})`);
|
console.log(`[XR][grab] ● ONE-HAND start (${activeHand})`);
|
||||||
}
|
}
|
||||||
_tmpMat.multiplyMatrices(slot.gripWorld, single.current.offset);
|
_tmpMat.multiplyMatrices(slot.gripWorld, single.current.offset);
|
||||||
applyWorldMatrixToLocal(group, _tmpMat);
|
applyWorldMatrixToLocal(group, _tmpMat);
|
||||||
} else if (single.current) {
|
} else if (single.current) {
|
||||||
single.current = null;
|
single.current = null;
|
||||||
|
endGrab();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -152,6 +201,11 @@ export function XRGrabbable({ allowScale = false, onGrabStart, children }: XRGra
|
|||||||
<sphereGeometry args={[0.5, 16, 12]} />
|
<sphereGeometry args={[0.5, 16, 12]} />
|
||||||
<meshBasicMaterial color="#22c55e" transparent opacity={0} depthWrite={false} />
|
<meshBasicMaterial color="#22c55e" transparent opacity={0} depthWrite={false} />
|
||||||
</mesh>
|
</mesh>
|
||||||
|
{/* Lock halo — golden, visible while isLocked is true */}
|
||||||
|
<mesh ref={lockHaloRef} visible={false}>
|
||||||
|
<sphereGeometry args={[0.55, 16, 12]} />
|
||||||
|
<meshBasicMaterial color="#f59e0b" transparent opacity={0} depthWrite={false} wireframe />
|
||||||
|
</mesh>
|
||||||
{children}
|
{children}
|
||||||
</group>
|
</group>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user