This commit is contained in:
gpt-engineer-app[bot]
2026-02-28 00:49:38 +00:00
parent fcc6856f4d
commit 89fbd4636f
3 changed files with 167 additions and 16 deletions
+133
View File
@@ -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>
</>
);
}