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 { useFrame } from '@react-three/fiber';
|
||||
import { useFrame, useThree } from '@react-three/fiber';
|
||||
import * as THREE from 'three';
|
||||
import { useControllerGrab, ControllerGrabSnapshot } from '@/hooks/useControllerGrab';
|
||||
import { useModelStore } from '@/stores/useModelStore';
|
||||
|
||||
interface XRGrabbableProps {
|
||||
/** 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,
|
||||
* distance ratio = uniform scale (if allowScale).
|
||||
*
|
||||
* On release the group keeps its last pose (we don't write to the store —
|
||||
* the manual fine-tuning sliders remain available as additional offsets).
|
||||
* Parent re-attach strategy:
|
||||
* 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) {
|
||||
const groupRef = useRef<THREE.Group>(null);
|
||||
const grab = useControllerGrab();
|
||||
const { scene } = useThree();
|
||||
|
||||
const single = useRef<SingleGrabRecord | 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 lockHaloRef = useRef<THREE.Mesh>(null);
|
||||
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(() => {
|
||||
const group = groupRef.current;
|
||||
if (!group) return;
|
||||
@@ -63,14 +103,20 @@ export function XRGrabbable({ allowScale = false, onGrabStart, children }: XRGra
|
||||
|
||||
const lActive = L.isGrabbing && L.hasPose;
|
||||
const rActive = R.isGrabbing && R.hasPose;
|
||||
const isLocked = useModelStore.getState().isLocked;
|
||||
|
||||
// ─── Halo intensity (visual feedback for analog grip) ────────
|
||||
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;
|
||||
mat.opacity = maxGrip * 0.25;
|
||||
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 ───────────────────────────────────────────
|
||||
if (lActive && rActive) {
|
||||
@@ -83,7 +129,9 @@ export function XRGrabbable({ allowScale = false, onGrabStart, children }: XRGra
|
||||
axisNow.divideScalar(distNow);
|
||||
|
||||
if (!dual.current) {
|
||||
// Capture
|
||||
// Capture — reparent BEFORE snapshotting matrixWorld so subsequent
|
||||
// applyWorldMatrixToLocal math is consistent.
|
||||
beginGrab('dual', 'dual');
|
||||
group.updateMatrixWorld();
|
||||
dual.current = {
|
||||
startGroupWorld: group.matrixWorld.clone(),
|
||||
@@ -93,7 +141,6 @@ export function XRGrabbable({ allowScale = false, onGrabStart, children }: XRGra
|
||||
startScale: group.scale.x,
|
||||
};
|
||||
single.current = null; // dual takes over
|
||||
if (!everGrabbed.current) { everGrabbed.current = true; onGrabStart?.(); }
|
||||
console.log('[XR][grab] ◆ TWO-HAND start');
|
||||
}
|
||||
|
||||
@@ -123,6 +170,7 @@ export function XRGrabbable({ allowScale = false, onGrabStart, children }: XRGra
|
||||
} else if (dual.current) {
|
||||
console.log('[XR][grab] ◆ TWO-HAND end');
|
||||
dual.current = null;
|
||||
if (!lActive && !rActive) endGrab();
|
||||
}
|
||||
|
||||
// ─── One-hand mode ───────────────────────────────────────────
|
||||
@@ -130,18 +178,19 @@ export function XRGrabbable({ allowScale = false, onGrabStart, children }: XRGra
|
||||
if (activeHand) {
|
||||
const slot = activeHand === 'left' ? L : R;
|
||||
if (!single.current || single.current.hand !== activeHand) {
|
||||
beginGrab('single', activeHand);
|
||||
group.updateMatrixWorld();
|
||||
const offset = new THREE.Matrix4()
|
||||
.copy(slot.gripWorld).invert()
|
||||
.multiply(group.matrixWorld);
|
||||
single.current = { hand: activeHand, offset };
|
||||
if (!everGrabbed.current) { everGrabbed.current = true; onGrabStart?.(); }
|
||||
console.log(`[XR][grab] ● ONE-HAND start (${activeHand})`);
|
||||
}
|
||||
_tmpMat.multiplyMatrices(slot.gripWorld, single.current.offset);
|
||||
applyWorldMatrixToLocal(group, _tmpMat);
|
||||
} else if (single.current) {
|
||||
single.current = null;
|
||||
endGrab();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -152,6 +201,11 @@ export function XRGrabbable({ allowScale = false, onGrabStart, children }: XRGra
|
||||
<sphereGeometry args={[0.5, 16, 12]} />
|
||||
<meshBasicMaterial color="#22c55e" transparent opacity={0} depthWrite={false} />
|
||||
</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}
|
||||
</group>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user