Changes
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
import { useRef, useState, useCallback, useEffect } from 'react';
|
||||
import { useFrame } from '@react-three/fiber';
|
||||
import { useXRHitTest, useXRRequestHitTest } from '@react-three/xr';
|
||||
import * as THREE from 'three';
|
||||
|
||||
const matrixHelper = new THREE.Matrix4();
|
||||
const positionHelper = new THREE.Vector3();
|
||||
const quaternionHelper = new THREE.Quaternion();
|
||||
|
||||
interface XRHitTestPlacementProps {
|
||||
placementMode: boolean;
|
||||
onPlace: (position: THREE.Vector3, quaternion: THREE.Quaternion) => void;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses @react-three/xr useXRHitTest to show a reticle on real-world surfaces.
|
||||
* Tap (select) to place the model at the reticle position.
|
||||
*/
|
||||
export function XRHitTestPlacement({ placementMode, onPlace, children }: XRHitTestPlacementProps) {
|
||||
const reticleRef = useRef<THREE.Mesh>(null);
|
||||
const [reticlePos, setReticlePos] = useState<THREE.Vector3 | null>(null);
|
||||
const [reticleQuat, setReticleQuat] = useState<THREE.Quaternion | null>(null);
|
||||
const [placed, setPlaced] = useState(false);
|
||||
const prevPlacementMode = useRef(placementMode);
|
||||
const placedPosition = useRef(new THREE.Vector3(0, 0, -1.5));
|
||||
const placedQuaternion = useRef(new THREE.Quaternion());
|
||||
|
||||
// Continuous hit-test from the viewer (camera center)
|
||||
useXRHitTest(
|
||||
placementMode && !placed
|
||||
? (results, getWorldMatrix) => {
|
||||
if (results.length === 0) {
|
||||
setReticlePos(null);
|
||||
return;
|
||||
}
|
||||
getWorldMatrix(matrixHelper, results[0]);
|
||||
positionHelper.setFromMatrixPosition(matrixHelper);
|
||||
quaternionHelper.setFromRotationMatrix(matrixHelper);
|
||||
setReticlePos(positionHelper.clone());
|
||||
setReticleQuat(quaternionHelper.clone());
|
||||
}
|
||||
: undefined,
|
||||
'viewer'
|
||||
);
|
||||
|
||||
// Request-based hit test for tap-to-place
|
||||
const requestHitTest = useXRRequestHitTest();
|
||||
|
||||
const handleSelect = useCallback(async () => {
|
||||
if (!placementMode) return;
|
||||
|
||||
// Use current reticle position if available
|
||||
if (reticlePos) {
|
||||
placedPosition.current.copy(reticlePos);
|
||||
if (reticleQuat) placedQuaternion.current.copy(reticleQuat);
|
||||
setPlaced(true);
|
||||
onPlace(reticlePos.clone(), reticleQuat?.clone() ?? new THREE.Quaternion());
|
||||
console.log('[HitTest] 📍 Modelo posicionado via reticle:', reticlePos.toArray());
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback: on-demand hit test
|
||||
if (!requestHitTest) return;
|
||||
const result = await requestHitTest('viewer', ['plane', 'mesh']);
|
||||
if (!result || result.results.length === 0) return;
|
||||
result.getWorldMatrix(matrixHelper, result.results[0]);
|
||||
const pos = new THREE.Vector3().setFromMatrixPosition(matrixHelper);
|
||||
const quat = new THREE.Quaternion().setFromRotationMatrix(matrixHelper);
|
||||
placedPosition.current.copy(pos);
|
||||
placedQuaternion.current.copy(quat);
|
||||
setPlaced(true);
|
||||
onPlace(pos, quat);
|
||||
console.log('[HitTest] 📍 Modelo posicionado via requestHitTest:', pos.toArray());
|
||||
}, [placementMode, reticlePos, reticleQuat, onPlace, requestHitTest]);
|
||||
|
||||
// Update reticle mesh position each frame
|
||||
useFrame(() => {
|
||||
if (reticleRef.current) {
|
||||
if (reticlePos && placementMode && !placed) {
|
||||
reticleRef.current.visible = true;
|
||||
reticleRef.current.position.copy(reticlePos);
|
||||
if (reticleQuat) reticleRef.current.quaternion.copy(reticleQuat);
|
||||
} else {
|
||||
reticleRef.current.visible = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Reset placement when user re-enters placement mode
|
||||
useEffect(() => {
|
||||
if (placementMode && !prevPlacementMode.current) {
|
||||
setPlaced(false);
|
||||
}
|
||||
prevPlacementMode.current = placementMode;
|
||||
}, [placementMode]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Reticle on detected surface */}
|
||||
<mesh
|
||||
ref={reticleRef}
|
||||
visible={false}
|
||||
onClick={handleSelect}
|
||||
pointerEventsType={{ deny: 'grab' as any }}
|
||||
>
|
||||
<ringGeometry args={[0.08, 0.1, 32]} />
|
||||
<meshBasicMaterial color="#22c55e" side={THREE.DoubleSide} transparent opacity={0.8} />
|
||||
</mesh>
|
||||
|
||||
{/* Invisible tap plane to catch select events */}
|
||||
{placementMode && !placed && (
|
||||
<mesh
|
||||
position={[0, 0, -2]}
|
||||
onClick={handleSelect}
|
||||
visible={false}
|
||||
pointerEventsType={{ deny: 'grab' as any }}
|
||||
>
|
||||
<planeGeometry args={[10, 10]} />
|
||||
<meshBasicMaterial transparent opacity={0} />
|
||||
</mesh>
|
||||
)}
|
||||
|
||||
{/* Placed model or default position */}
|
||||
<group
|
||||
position={placed ? placedPosition.current : [0, 0, -1.5]}
|
||||
quaternion={placed ? placedQuaternion.current : undefined}
|
||||
>
|
||||
{children}
|
||||
</group>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user