Reverted to commit ceb9b8c1a1

This commit is contained in:
gpt-engineer-app[bot]
2026-05-13 21:12:16 +00:00
parent 96e806042f
commit fe2d6a3ae0
16 changed files with 128 additions and 931 deletions
+7 -61
View File
@@ -1,8 +1,7 @@
import { useRef, ReactNode } from 'react';
import { useFrame, useThree } from '@react-three/fiber';
import { useFrame } 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) */
@@ -43,57 +42,18 @@ const _tmpQuat = new THREE.Quaternion();
* • Two-hand grab: midpoint = pivot, axis between hands = rotation,
* distance ratio = uniform scale (if allowScale).
*
* 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.
* 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).
*/
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;
@@ -103,20 +63,14 @@ 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 = isLocked ? 0 : Math.max(L.gripValue, R.gripValue);
const maxGrip = 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) {
@@ -129,9 +83,7 @@ export function XRGrabbable({ allowScale = false, onGrabStart, children }: XRGra
axisNow.divideScalar(distNow);
if (!dual.current) {
// Capture — reparent BEFORE snapshotting matrixWorld so subsequent
// applyWorldMatrixToLocal math is consistent.
beginGrab('dual', 'dual');
// Capture
group.updateMatrixWorld();
dual.current = {
startGroupWorld: group.matrixWorld.clone(),
@@ -141,6 +93,7 @@ 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');
}
@@ -170,7 +123,6 @@ 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 ───────────────────────────────────────────
@@ -178,19 +130,18 @@ 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();
}
});
@@ -201,11 +152,6 @@ 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>
);