Changes
This commit is contained in:
@@ -2,7 +2,7 @@ import { useState } from 'react';
|
||||
import {
|
||||
Eye, LayoutGrid, Grid3X3, Box, Ruler, Trash2, Camera,
|
||||
Move, RotateCw, RefreshCw, ChevronUp, ChevronDown, Grip,
|
||||
ClipboardCheck, X,
|
||||
ClipboardCheck, X, Crosshair,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Slider } from '@/components/ui/slider';
|
||||
@@ -17,9 +17,11 @@ const ROTATION_STEP = 0.1;
|
||||
interface XRHudProps {
|
||||
freeMove: boolean;
|
||||
onToggleFreeMove: () => void;
|
||||
placementMode?: boolean;
|
||||
onTogglePlacement?: () => void;
|
||||
}
|
||||
|
||||
export function XRHud({ freeMove, onToggleFreeMove }: XRHudProps) {
|
||||
export function XRHud({ freeMove, onToggleFreeMove, placementMode = false, onTogglePlacement }: XRHudProps) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [checklistOpen, setChecklistOpen] = useState(false);
|
||||
|
||||
@@ -147,6 +149,18 @@ export function XRHud({ freeMove, onToggleFreeMove }: XRHudProps) {
|
||||
{expanded ? <ChevronDown className="h-4 w-4" /> : <ChevronUp className="h-4 w-4" />}
|
||||
</Button>
|
||||
|
||||
{/* Hit-test placement toggle */}
|
||||
{onTogglePlacement && (
|
||||
<Button
|
||||
variant={placementMode ? 'default' : 'outline'}
|
||||
size="sm" className="h-8 gap-1.5 text-[10px] font-mono"
|
||||
onClick={onTogglePlacement}
|
||||
>
|
||||
<Crosshair className="h-3.5 w-3.5" />
|
||||
{placementMode ? 'Posicionar…' : 'Reposicionar'}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant={freeMove ? 'default' : 'outline'}
|
||||
size="sm" className="h-8 gap-1.5 text-[10px] font-mono"
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+18
-14
@@ -11,6 +11,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { generateMarkerDownloadURL } from '@/lib/trackingMarker';
|
||||
import { XRHud } from '@/components/XRHud';
|
||||
import { findNearestVertex } from '@/components/three/SmartMeasure';
|
||||
import { XRHitTestPlacement } from '@/components/three/XRHitTestPlacement';
|
||||
|
||||
// --- Diagnóstico XR ---
|
||||
console.log('[XR] Inicializando store...');
|
||||
@@ -307,23 +308,15 @@ function XRGrid() {
|
||||
);
|
||||
}
|
||||
|
||||
// ─── ImageTrackingAnchor ───────────────────────────────
|
||||
function ImageTrackingAnchor({ children }: { children: React.ReactNode }) {
|
||||
const groupRef = useRef<THREE.Group>(null);
|
||||
useFrame(() => {
|
||||
if (!groupRef.current) return;
|
||||
groupRef.current.position.set(0, 0, -1.5);
|
||||
groupRef.current.quaternion.identity();
|
||||
});
|
||||
return <group ref={groupRef}>{children}</group>;
|
||||
}
|
||||
// ImageTrackingAnchor removed — replaced by XRHitTestPlacement
|
||||
|
||||
// ─── XRSession Page ────────────────────────────────────
|
||||
const XRSession = () => {
|
||||
const navigate = useNavigate();
|
||||
const { model, anchorMode, setAnchorMode } = useModelStore();
|
||||
const [inXR, setInXR] = useState(false);
|
||||
const [freeMove, setFreeMove] = useState(true); // default ON for easier positioning
|
||||
const [freeMove, setFreeMove] = useState(true);
|
||||
const [placementMode, setPlacementMode] = useState(true); // start in placement mode
|
||||
|
||||
useEffect(() => {
|
||||
if (!model) navigate('/');
|
||||
@@ -415,9 +408,15 @@ const XRSession = () => {
|
||||
|
||||
<XROrigin />
|
||||
|
||||
<ImageTrackingAnchor>
|
||||
<XRHitTestPlacement
|
||||
placementMode={placementMode}
|
||||
onPlace={() => {
|
||||
setPlacementMode(false);
|
||||
toast.success('Modelo posicionado na superfície!');
|
||||
}}
|
||||
>
|
||||
<XRModel url={model.url} />
|
||||
</ImageTrackingAnchor>
|
||||
</XRHitTestPlacement>
|
||||
|
||||
<XRMeasurementOverlay />
|
||||
<XRSnapHandler />
|
||||
@@ -427,7 +426,12 @@ const XRSession = () => {
|
||||
</Canvas>
|
||||
|
||||
{/* Floating HUD overlay with all controls */}
|
||||
<XRHud freeMove={freeMove} onToggleFreeMove={() => setFreeMove(!freeMove)} />
|
||||
<XRHud
|
||||
freeMove={freeMove}
|
||||
onToggleFreeMove={() => setFreeMove(!freeMove)}
|
||||
placementMode={placementMode}
|
||||
onTogglePlacement={() => setPlacementMode(!placementMode)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user