Changes
This commit is contained in:
@@ -2,7 +2,7 @@ import { useState } from 'react';
|
|||||||
import {
|
import {
|
||||||
Eye, LayoutGrid, Grid3X3, Box, Ruler, Trash2, Camera,
|
Eye, LayoutGrid, Grid3X3, Box, Ruler, Trash2, Camera,
|
||||||
Move, RotateCw, RefreshCw, ChevronUp, ChevronDown, Grip,
|
Move, RotateCw, RefreshCw, ChevronUp, ChevronDown, Grip,
|
||||||
ClipboardCheck, X,
|
ClipboardCheck, X, Crosshair,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Slider } from '@/components/ui/slider';
|
import { Slider } from '@/components/ui/slider';
|
||||||
@@ -17,9 +17,11 @@ const ROTATION_STEP = 0.1;
|
|||||||
interface XRHudProps {
|
interface XRHudProps {
|
||||||
freeMove: boolean;
|
freeMove: boolean;
|
||||||
onToggleFreeMove: () => void;
|
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 [expanded, setExpanded] = useState(false);
|
||||||
const [checklistOpen, setChecklistOpen] = 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" />}
|
{expanded ? <ChevronDown className="h-4 w-4" /> : <ChevronUp className="h-4 w-4" />}
|
||||||
</Button>
|
</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
|
<Button
|
||||||
variant={freeMove ? 'default' : 'outline'}
|
variant={freeMove ? 'default' : 'outline'}
|
||||||
size="sm" className="h-8 gap-1.5 text-[10px] font-mono"
|
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 { generateMarkerDownloadURL } from '@/lib/trackingMarker';
|
||||||
import { XRHud } from '@/components/XRHud';
|
import { XRHud } from '@/components/XRHud';
|
||||||
import { findNearestVertex } from '@/components/three/SmartMeasure';
|
import { findNearestVertex } from '@/components/three/SmartMeasure';
|
||||||
|
import { XRHitTestPlacement } from '@/components/three/XRHitTestPlacement';
|
||||||
|
|
||||||
// --- Diagnóstico XR ---
|
// --- Diagnóstico XR ---
|
||||||
console.log('[XR] Inicializando store...');
|
console.log('[XR] Inicializando store...');
|
||||||
@@ -307,23 +308,15 @@ function XRGrid() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── ImageTrackingAnchor ───────────────────────────────
|
// ImageTrackingAnchor removed — replaced by XRHitTestPlacement
|
||||||
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>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── XRSession Page ────────────────────────────────────
|
// ─── XRSession Page ────────────────────────────────────
|
||||||
const XRSession = () => {
|
const XRSession = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { model, anchorMode, setAnchorMode } = useModelStore();
|
const { model, anchorMode, setAnchorMode } = useModelStore();
|
||||||
const [inXR, setInXR] = useState(false);
|
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(() => {
|
useEffect(() => {
|
||||||
if (!model) navigate('/');
|
if (!model) navigate('/');
|
||||||
@@ -415,9 +408,15 @@ const XRSession = () => {
|
|||||||
|
|
||||||
<XROrigin />
|
<XROrigin />
|
||||||
|
|
||||||
<ImageTrackingAnchor>
|
<XRHitTestPlacement
|
||||||
|
placementMode={placementMode}
|
||||||
|
onPlace={() => {
|
||||||
|
setPlacementMode(false);
|
||||||
|
toast.success('Modelo posicionado na superfície!');
|
||||||
|
}}
|
||||||
|
>
|
||||||
<XRModel url={model.url} />
|
<XRModel url={model.url} />
|
||||||
</ImageTrackingAnchor>
|
</XRHitTestPlacement>
|
||||||
|
|
||||||
<XRMeasurementOverlay />
|
<XRMeasurementOverlay />
|
||||||
<XRSnapHandler />
|
<XRSnapHandler />
|
||||||
@@ -427,7 +426,12 @@ const XRSession = () => {
|
|||||||
</Canvas>
|
</Canvas>
|
||||||
|
|
||||||
{/* Floating HUD overlay with all controls */}
|
{/* 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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user