From c83f6ba9aff068c51b88049ce3b228107b93132e Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 01:07:02 +0000 Subject: [PATCH] Changes --- src/pages/XRSession.tsx | 308 +++++++++++++++++++++++++++++----------- 1 file changed, 229 insertions(+), 79 deletions(-) diff --git a/src/pages/XRSession.tsx b/src/pages/XRSession.tsx index 64ed387..b6547ef 100644 --- a/src/pages/XRSession.tsx +++ b/src/pages/XRSession.tsx @@ -1,65 +1,180 @@ -import { useEffect, useCallback, useState } from 'react'; +import { useEffect, useRef, useMemo, useCallback, useState } from 'react'; import { useNavigate } from 'react-router-dom'; +import { Canvas, useFrame, useThree } from '@react-three/fiber'; +import { useGLTF } from '@react-three/drei'; +import { XR, createXRStore, useXR, XROrigin } from '@react-three/xr'; +import * as THREE from 'three'; import { useModelStore } from '@/stores/useModelStore'; import { toast } from 'sonner'; +import { ArrowLeft, Move, RotateCw } from 'lucide-react'; +import { Button } from '@/components/ui/button'; + +const xrStore = createXRStore({ + emulate: 'metaQuest3', + hand: { left: true, right: true }, + controller: { left: true, right: true }, +}); /** - * XR Immersive Session Page - * Uses native WebXR API for passthrough AR on Quest 3. - * - Loads GLB model - * - Attempts image tracking for QR anchor - * - Falls back to manual placement - * - Fine tuning with grip + joysticks + * The 3D model rendered inside the XR scene. + * Applies fine tuning offsets from the store. */ +function XRModel({ url }: { url: string }) { + const { scene } = useGLTF(url); + const ref = useRef(null); + const fineTuning = useModelStore((s) => s.fineTuning); + const opacity = useModelStore((s) => s.opacity); + const renderMode = useModelStore((s) => s.renderMode); + const checklist = useModelStore((s) => s.checklist); + + const modelInfo = useMemo(() => { + const box = new THREE.Box3().setFromObject(scene); + const size = new THREE.Vector3(); + const center = new THREE.Vector3(); + box.getSize(size); + box.getCenter(center); + return { size, center }; + }, [scene]); + + // Apply visual properties + useEffect(() => { + const hasRejected = checklist.some(i => i.status === 'rejected'); + const allApproved = checklist.every(i => i.status === 'approved'); + + scene.traverse((child) => { + if (child instanceof THREE.Mesh) { + if (Array.isArray(child.material)) { + child.material = child.material.map(m => m.clone()); + } else { + child.material = child.material.clone(); + } + const materials = Array.isArray(child.material) ? child.material : [child.material]; + materials.forEach((mat: THREE.Material) => { + if (mat instanceof THREE.MeshStandardMaterial) { + mat.transparent = opacity < 1; + mat.opacity = opacity; + mat.wireframe = renderMode === 'wireframe'; + mat.needsUpdate = true; + if (hasRejected) { + mat.color.setHSL(0, 0.7, 0.5); + } else if (allApproved) { + mat.color.setHSL(0.38, 0.7, 0.45); + } else { + mat.color.set(0x8899aa); + } + } + }); + } + }); + }, [scene, opacity, renderMode, checklist]); + + const rotYRad = (fineTuning.rotY * Math.PI) / 180; + + return ( + + + + ); +} + +/** + * Controller-based fine tuning input handler. + * Left joystick = X/Y position, Right joystick = Z position + Y rotation. + * Active only when any grip button is held. + */ +function ControllerFineTuning() { + const { setFineTuning } = useModelStore(); + const session = useXR((s) => s.session); + + useFrame(() => { + if (!session) return; + const inputSources = session.inputSources; + if (!inputSources) return; + + let leftAxes: number[] | null = null; + let rightAxes: number[] | null = null; + let gripHeld = false; + + for (const source of inputSources) { + const gp = source.gamepad; + if (!gp) continue; + + // Check grip button (usually index 2) + const gripButton = gp.buttons[2]; + if (gripButton && gripButton.pressed) { + gripHeld = true; + } + + if (source.handedness === 'left' && gp.axes.length >= 4) { + leftAxes = [gp.axes[2], gp.axes[3]]; + } + if (source.handedness === 'right' && gp.axes.length >= 4) { + rightAxes = [gp.axes[2], gp.axes[3]]; + } + } + + if (!gripHeld) return; + + const posStep = 0.001; // 1mm per frame at full deflection + const rotStep = 0.1; // 0.1° per frame at full deflection + const deadzone = 0.15; + + const store = useModelStore.getState(); + const ft = { ...store.fineTuning }; + + if (leftAxes) { + if (Math.abs(leftAxes[0]) > deadzone) ft.posX += leftAxes[0] * posStep; + if (Math.abs(leftAxes[1]) > deadzone) ft.posY -= leftAxes[1] * posStep; + } + if (rightAxes) { + if (Math.abs(rightAxes[1]) > deadzone) ft.posZ += rightAxes[1] * posStep; + if (Math.abs(rightAxes[0]) > deadzone) ft.rotY += rightAxes[0] * rotStep; + } + + setFineTuning(ft); + }); + + return null; +} + +/** + * Manual placement: places model 1.5m in front of user at floor level. + */ +function ManualAnchor({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} + const XRSession = () => { const navigate = useNavigate(); - const { model, xrSupported } = useModelStore(); - const [sessionActive, setSessionActive] = useState(false); - const [statusMessage, setStatusMessage] = useState('Preparando sessão XR…'); + const { model, xrSupported, anchorMode } = useModelStore(); + const [inXR, setInXR] = useState(false); useEffect(() => { if (!model) { navigate('/'); return; } - if (!xrSupported) { - toast.error('WebXR não disponível'); - navigate('/viewer'); - return; - } - }, [model, xrSupported, navigate]); - - const startXRSession = useCallback(async () => { - if (!navigator.xr) return; + }, [model, navigate]); + const handleEnterAR = useCallback(async () => { try { - setStatusMessage('Iniciando sessão imersiva…'); - - // Check for image tracking support - const features: string[] = ['local-floor', 'hand-tracking']; - const optionalFeatures: string[] = ['image-tracking', 'anchors', 'plane-detection']; - - const session = await navigator.xr.requestSession('immersive-ar', { - requiredFeatures: features, - optionalFeatures: optionalFeatures, - domOverlay: { root: document.getElementById('xr-overlay')! }, - }); - - setSessionActive(true); - setStatusMessage('Sessão XR ativa — Modo Passthrough'); - - session.addEventListener('end', () => { - setSessionActive(false); - setStatusMessage('Sessão XR encerrada'); - toast.info('Sessão XR encerrada'); - }); - - // The actual 3D rendering in XR would require a full WebXR render loop - // with Three.js. For now we show the overlay UI. - toast.success('Sessão XR iniciada com passthrough!'); + await xrStore.enterAR(); + setInXR(true); + toast.success('Sessão XR iniciada — Passthrough ativo'); } catch (err: any) { - console.error('XR Session error:', err); - setStatusMessage('Erro ao iniciar XR'); + console.error('XR error:', err); toast.error(`Falha ao iniciar XR: ${err.message}`); } }, []); @@ -67,48 +182,83 @@ const XRSession = () => { if (!model) return null; return ( -
-
- -
-

- Modo XR Imersivo -

- -

{statusMessage}

- -
-

Modelo:

-

{model.fileName}

+
+ {/* Header */} +
+
+ +

+ Modo XR Imersivo +

- {!sessionActive && ( - + {!inXR && ( + )} - {sessionActive && ( -
-
-

● Sessão XR Ativa

-
-

- Coloque o Quest 3 para visualizar o modelo em passthrough. - Use Grip + Joysticks para ajuste fino. -

+ {inXR && ( +
+ + + XR Ativo +
)} +
- + + + + + + + + + + + + + + + + {/* Floating HUD overlay */} +
+
+
+ + + Grip + Joy Esq: X/Y + +
+
+ + + Grip + Joy Dir: Z/Rot + +
+
+ + Modo: {anchorMode === 'tracking' ? 'Image Tracking' : 'Manual'} + +
+
+
);