diff --git a/src/App.tsx b/src/App.tsx index 42fbbb6..3798cd4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,7 +5,6 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { BrowserRouter, Routes, Route } from "react-router-dom"; import Index from "./pages/Index"; import Viewer from "./pages/Viewer"; -import XRSession from "./pages/XRSession"; import Watch from "./pages/Watch"; import NotFound from "./pages/NotFound"; import "@/lib/remoteLogger"; @@ -21,7 +20,6 @@ const App = () => ( } /> } /> - } /> } /> } /> diff --git a/src/components/three/ModelViewer.tsx b/src/components/three/ModelViewer.tsx index 26db547..fb536b6 100644 --- a/src/components/three/ModelViewer.tsx +++ b/src/components/three/ModelViewer.tsx @@ -9,6 +9,30 @@ import { registerModelLocalGroup, unregisterModelLocalGroup } from '@/lib/modelT import { parseIFCtoThree } from '@/lib/convertIFC'; import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; +// WebXR imersivo +import { XR, useXR, XROrigin } from '@react-three/xr'; +import { xrStore } from '@/lib/xrStore'; +import { toast } from 'sonner'; +import { + XRBackgroundModels, + XRActiveModel, + XRMeasurementOverlay, + XRSnapHandler, + XRGrid, + XRGridAutoFollower, +} from './XRSceneComponents'; +import { XRGrabbable } from './XRGrabbable'; +import { XRHitTestPlacement } from './XRHitTestPlacement'; +import { XRControllerMeasure } from './XRControllerMeasure'; +import { ControllerLocomotion } from './ControllerLocomotion'; +import { XRHudInWorld } from './XRHudInWorld'; +import { XRBroadcastMirror } from './XRBroadcastMirror'; +import { XRHud } from '@/components/XRHud'; +import { ShareButton } from '@/components/ShareButton'; +import { useDevKit } from '@/devkit/useDevKit'; +import { DevPanel } from '@/devkit/DevPanel'; +import { FakeControllers } from '@/devkit/FakeControllers'; + interface ModelViewerProps { url?: string; // legacy, ignored — uses store.models } @@ -1006,8 +1030,8 @@ function PositionDragHandler() { const ft = active.fineTuning; const factor = st.scaleRatio?.factor ?? 1; - if (button === 2) { - // Right button: rotate + if (button === 0) { + // Left button (Trigger in Quest emulated mouse): rotate (unifies with AR / VR standard) const sens = 0.4; // deg per pixel if (shiftKey) { useModelStore.getState().setFineTuning({ rotZ: ft.rotZ + dx * sens }); @@ -1034,8 +1058,8 @@ function PositionDragHandler() { } else { useModelStore.getState().setFineTuning({ rotZ: ft.rotZ + delta }); } - } else if (button === 0 && shiftKey) { - // Shift+left: depth (Z in camera space) + } else if (button === 2 && shiftKey) { + // Shift+Right button: depth (Z in camera space) const worldDelta = dy * pixelsPerWorldUnit; const camDir = new THREE.Vector3(); camera.getWorldDirection(camDir); @@ -1046,7 +1070,7 @@ function PositionDragHandler() { posZ: ft.posZ + move.z, }); } else { - // Left: translate in camera plane + // Right button (Grip in Quest emulated mouse): translate in camera plane (matches AR pan-only grip) const right = new THREE.Vector3(); const up = new THREE.Vector3(); camera.matrixWorld.extractBasis(right, up, new THREE.Vector3()); @@ -1088,10 +1112,193 @@ function PositionDragHandler() { -export function ModelViewerCanvas({ url }: ModelViewerProps) { +function SceneInsideXR() { + const isPresenting = useXR((state) => state.isPresenting); const positionMode = useModelStore((s) => s.positionMode); - const measureMode = useModelStore((s) => s.measureMode); - const selectionMode = useModelStore((s) => s.selectionMode); + const activeId = useModelStore((s) => s.activeModelId); + const models = useModelStore((s) => s.models); + const activeModel = models.find(m => m.id === activeId); + const isActiveLocked = !!activeModel?.locked; + + // Estados locais para a sessão de XR + const [freeMove, setFreeMove] = useState(true); + const [placementMode, setPlacementMode] = useState(true); + const [snapToPlanes, setSnapToPlanes] = useState(true); + const [allowScale, setAllowScale] = useState(true); + const [liveCode, setLiveCode] = useState(null); + const [liveViewers, setLiveViewers] = useState(0); + + const rigRef = useRef(null); + + // DEVKIT: simXR forces in-XR rendering on desktop without a real WebXR session + const devkit = useDevKit(); + const [simXR, setSimXR] = useState(false); + const effectiveInXR = isPresenting || simXR; + + // Monitorar início e fim da sessão + useEffect(() => { + if (isPresenting) { + toast.success('Sessão AR iniciada!'); + } + }, [isPresenting]); + + if (!effectiveInXR) { + // Modo 2D normal (não imersivo) + return ( + <> + + + + + }> + + + + + + + + + + + + + + + + + + + ); + } + + // Modo AR imersivo (ou SimXR) + return ( + <> + + + + + + + + + {simXR ? ( + + + { if (placementMode) setPlacementMode(false); }} + > + + + + ) : ( + { + setPlacementMode(false); + toast.success('Modelo posicionado na superfície!'); + }} + > + + { if (placementMode) setPlacementMode(false); }} + > + + + + )} + + + + + + + + setFreeMove(!freeMove)} + placementMode={placementMode} + onTogglePlacement={() => setPlacementMode(!placementMode)} + snapToPlanes={snapToPlanes} + onToggleSnap={() => setSnapToPlanes(!snapToPlanes)} + allowScale={allowScale} + onToggleAllowScale={() => setAllowScale(!allowScale)} + liveCode={liveCode} + liveViewers={liveViewers} + onStartLive={() => (window as unknown as { __trackSteelStartLive?: () => void }).__trackSteelStartLive?.()} + onStopLive={() => (window as unknown as { __trackSteelStopLive?: () => void }).__trackSteelStopLive?.()} + /> + + + + {devkit && simXR && } + {simXR && ( + + )} + + + + + + {/* Floating HUD overlay - visível apenas fora do headset passthrough */} + +
+ setFreeMove(!freeMove)} + placementMode={placementMode} + onTogglePlacement={() => setPlacementMode(!placementMode)} + snapToPlanes={snapToPlanes} + onToggleSnap={() => setSnapToPlanes(!snapToPlanes)} + allowScale={allowScale} + onToggleAllowScale={() => setAllowScale(!allowScale)} + /> +
+ + + {/* Compartilhar botões ocultos para WebRTC e HUD de fora */} + +
+ setLiveCode(h?.code ?? null)} + onViewerCountChange={setLiveViewers} + /> +
+ + + {/* DevPanel se devkit habilitado */} + {devkit && ( + +
+ setSimXR((v) => !v)} /> +
+ + )} + + ); +} + +export function ModelViewerCanvas({ url }: ModelViewerProps) { return ( - - - - - }> - - - - - - - - - - - - - - - - - - + + + ); } diff --git a/src/pages/XRSession.tsx b/src/components/three/XRSceneComponents.tsx similarity index 54% rename from src/pages/XRSession.tsx rename to src/components/three/XRSceneComponents.tsx index e885d3c..0ff069b 100644 --- a/src/pages/XRSession.tsx +++ b/src/components/three/XRSceneComponents.tsx @@ -1,74 +1,15 @@ -import { useEffect, useRef, useMemo, useCallback, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { Canvas, useFrame, useThree } from '@react-three/fiber'; -import { useGLTF, Grid, Html, Line, OrbitControls, Text } from '@react-three/drei'; -import { XR, createXRStore, useXR, XROrigin } from '@react-three/xr'; +import { useEffect, useRef, useMemo, useState, useCallback } from 'react'; +import { useFrame, useThree } from '@react-three/fiber'; +import { Grid } from '@react-three/drei'; +import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; import * as THREE from 'three'; -import { useModelStore } from '@/stores/useModelStore'; -import { toast } from 'sonner'; -import { ArrowLeft, Download, QrCode, Crosshair, Home } from 'lucide-react'; -import { Button } from '@/components/ui/button'; -import { generateMarkerDownloadURL } from '@/lib/trackingMarker'; -import { XRHud } from '@/components/XRHud'; -import { ShareButton } from '@/components/ShareButton'; -import { XRHudInWorld } from '@/components/three/XRHudInWorld'; -import { XRBroadcastMirror } from '@/components/three/XRBroadcastMirror'; -import { findNearestVertex } from '@/components/three/SmartMeasure'; -import { XRHitTestPlacement } from '@/components/three/XRHitTestPlacement'; -import { XRGrabbable } from '@/components/three/XRGrabbable'; -import { XRControllerMeasure } from '@/components/three/XRControllerMeasure'; -import { ControllerLocomotion } from '@/components/three/ControllerLocomotion'; -// DEVKIT: remove these imports + all `// DEVKIT:` blocks below to strip devkit -import { useDevKit } from '@/devkit/useDevKit'; -import { DevPanel } from '@/devkit/DevPanel'; -import { FakeControllers } from '@/devkit/FakeControllers'; -import { VisibilityApplier } from '@/components/three/ModelViewer'; +import { useModelStore, type SceneModel } from '@/stores/useModelStore'; +import { findNearestVertex } from './SmartMeasure'; import { registerModelLocalGroup, unregisterModelLocalGroup } from '@/lib/modelTransforms'; import { parseIFCtoThree } from '@/lib/convertIFC'; -import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; - -// --- Diagnóstico XR --- -console.log('[XR] Inicializando store...'); -if (navigator.xr) { - navigator.xr.isSessionSupported('immersive-ar').then((supported) => { - console.log('[XR] immersive-ar suportado:', supported); - }); -} - -// Lê flags de feature WebXR do localStorage (default: todas ON). -// O usuário liga/desliga pelo HUD AR (aba WebXR) para diagnosticar qual -// feature ativa o grid de segurança do Quest. Mudanças exigem sair e re-entrar do AR. -function loadXRFeatures() { - const defaults = { - handTracking: true, planeDetection: true, hitTest: true, domOverlay: true, - anchors: true, meshDetection: true, depthSensing: true, layers: true, - bodyTracking: true, lightEstimation: true, - }; - try { - const raw = localStorage.getItem('xrFeatures'); - if (raw) return { ...defaults, ...JSON.parse(raw) }; - } catch {} - return defaults; -} -const _xrf = loadXRFeatures(); -console.log('[XR] Features:', _xrf); - -const store = createXRStore({ - hand: { left: true, right: true }, - controller: { left: true, right: true }, - handTracking: _xrf.handTracking, - planeDetection: _xrf.planeDetection, - hitTest: _xrf.hitTest, - domOverlay: _xrf.domOverlay, - anchors: _xrf.anchors, - meshDetection: _xrf.meshDetection, - depthSensing: _xrf.depthSensing, - layers: _xrf.layers, - bodyTracking: _xrf.bodyTracking, -}); // ─── XRModel ─────────────────────────────────────────── -function XRModel({ sceneModel }: { sceneModel: import('@/stores/useModelStore').SceneModel }) { +export function XRModel({ sceneModel }: { sceneModel: SceneModel }) { const [rawScene, setRawScene] = useState(null); const scene = useMemo(() => rawScene ? rawScene.clone(true) : null, [rawScene]); @@ -215,8 +156,7 @@ function XRModel({ sceneModel }: { sceneModel: import('@/stores/useModelStore'). ); } -/** Renders all NON-active models as static background (placement + grab apply only to active). */ -function XRBackgroundModels() { +export function XRBackgroundModels() { const models = useModelStore((s) => s.models); const activeId = useModelStore((s) => s.activeModelId); return ( @@ -228,8 +168,7 @@ function XRBackgroundModels() { ); } -/** Renders the currently-active model (the one wrapped by grab/placement). */ -function XRActiveModel() { +export function XRActiveModel() { const models = useModelStore((s) => s.models); const activeId = useModelStore((s) => s.activeModelId); const active = models.find(m => m.id === activeId); @@ -238,12 +177,9 @@ function XRActiveModel() { } // ─── ControllerFineTuning ────────────────────────────── -// Joystick for slow numeric adjustments. Disabled while any grip is held -// (grab takes priority). Trigger pressure modulates speed: light = fine, -// strong = fast. Standard VR mapping: left=lateral/forward, right=rotY/height. -function ControllerFineTuning({ freeMove }: { freeMove: boolean }) { +export function ControllerFineTuning({ freeMove }: { freeMove: boolean }) { const { setFineTuning } = useModelStore(); - const session = useXR((s) => s.session); + const session = useThree((s) => s.gl.xr.getSession()); const isActiveLocked = useModelStore((s) => { const a = s.models.find(m => m.id === s.activeModelId); return !!a?.locked; @@ -268,7 +204,7 @@ function ControllerFineTuning({ freeMove }: { freeMove: boolean }) { const trigBtn = gp.buttons[1]; const gripVal = gripBtn ? (gripBtn.value || (gripBtn.pressed ? 1 : 0)) : 0; if (gripBtn?.pressed) gripHeld = true; - if (gripVal > 0.3) anyGripHeld = true; // matches grab hysteresis OFF + if (gripVal > 0.3) anyGripHeld = true; const trigVal = trigBtn ? (trigBtn.value || (trigBtn.pressed ? 1 : 0)) : 0; if (source.handedness === 'left' && gp.axes.length >= 4) { leftAxes = [gp.axes[2], gp.axes[3]]; @@ -280,31 +216,24 @@ function ControllerFineTuning({ freeMove }: { freeMove: boolean }) { } } - // Grab takes priority — joystick is disabled while grabbing if (anyGripHeld) return; - // In freeMove mode, joystick always moves. Otherwise requires grip — but - // grip is reserved for grab now, so freeMove=false essentially disables. if (!freeMove && !gripHeld) return; const baseStep = 0.001; const baseRot = 0.1; const deadzone = 0.15; - // Trigger modulation: 0 → 0.3×, 1 → 3× const speedL = 0.3 + leftTrig * 2.7; const speedR = 0.3 + rightTrig * 2.7; - // Quadratic acceleration on thumbstick value const curve = (v: number) => Math.sign(v) * v * v; const modelStore = useModelStore.getState(); const ft = { ...modelStore.fineTuning }; if (leftAxes) { - // Left: X (horizontal) / Z forward-back (vertical, up = forward = -Z) if (Math.abs(leftAxes[0]) > deadzone) ft.posX += curve(leftAxes[0]) * baseStep * speedL; if (Math.abs(leftAxes[1]) > deadzone) ft.posZ += curve(leftAxes[1]) * baseStep * speedL; } if (rightAxes) { - // Right: rotY (horizontal) / Y height (vertical, up = up) if (Math.abs(rightAxes[0]) > deadzone) ft.rotY += curve(rightAxes[0]) * baseRot * speedR; if (Math.abs(rightAxes[1]) > deadzone) ft.posY -= curve(rightAxes[1]) * baseStep * speedR; } @@ -315,7 +244,6 @@ function ControllerFineTuning({ freeMove }: { freeMove: boolean }) { return null; } -// Componente de renderização de texto compatível e seguro para WebXR function XRMeasurementText({ text, color }: { text: string; color: string }) { const texture = useMemo(() => { const canvas = document.createElement('canvas'); @@ -323,10 +251,8 @@ function XRMeasurementText({ text, color }: { text: string; color: string }) { canvas.height = 64; const ctx = canvas.getContext('2d'); if (ctx) { - // Limpa e desenha fundo semi-transparente ctx.fillStyle = 'rgba(11, 18, 32, 0.85)'; ctx.beginPath(); - // Desenha retângulo arredondado manualmente const x = 0, y = 0, width = 256, height = 64, radius = 12; ctx.moveTo(x + radius, y); ctx.lineTo(x + width - radius, y); @@ -340,12 +266,10 @@ function XRMeasurementText({ text, color }: { text: string; color: string }) { ctx.closePath(); ctx.fill(); - // Desenha borda sutil ctx.strokeStyle = color; ctx.lineWidth = 3; ctx.stroke(); - // Texto ctx.fillStyle = color; ctx.font = 'bold 24px monospace'; ctx.textAlign = 'center'; @@ -370,7 +294,6 @@ function XRMeasurementText({ text, color }: { text: string; color: string }) { ); } -// Componente de linha nativa seguro para WebXR export function XRSafeLine({ a, b }: { a: [number, number, number]; b: [number, number, number] }) { const points = useMemo(() => [ new THREE.Vector3(a[0], a[1], a[2]), @@ -388,7 +311,6 @@ export function XRSafeLine({ a, b }: { a: [number, number, number]; b: [number, ); } -// Componente para renderizar medições que pertencem a um modelo específico export function XRLocalModelMeasurements({ modelId }: { modelId: string }) { const measurements = useModelStore((s) => s.measurements); const modelMeasurements = useMemo(() => { @@ -430,8 +352,7 @@ export function XRLocalModelMeasurements({ modelId }: { modelId: string }) { ); } -// ─── Measurement overlay (reused from ModelViewer) ───── -function XRMeasurementOverlay() { +export function XRMeasurementOverlay() { const measurements = useModelStore((s) => s.measurements); const measurePoints = useModelStore((s) => s.measurePoints); const snapPoint = useModelStore((s) => s.snapPoint); @@ -488,8 +409,7 @@ function XRMeasurementOverlay() { ); } -// ─── Snap handler for XR raycasting ──────────────────── -function XRSnapHandler() { +export function XRSnapHandler() { const { camera, scene, gl } = useThree(); const measureMode = useModelStore((s) => s.measureMode); const setSnapPoint = useModelStore((s) => s.setSnapPoint); @@ -510,8 +430,6 @@ function XRSnapHandler() { }, [gl, mouse, measureMode]); useFrame(() => { - // Em AR imersivo, a medição é feita pelo XRControllerMeasure (gatilho do controle). - // Evita raycast pesado por frame que travava o Quest ao ativar "Medir". if (gl.xr.isPresenting) { setSnapPoint(null); return; } if (!measureMode) { setSnapPoint(null); return; } raycaster.setFromCamera(mouse, camera); @@ -532,11 +450,9 @@ function XRSnapHandler() { } }); - - // Click to measure useEffect(() => { if (!measureMode) return; - if (gl.xr.isPresenting) return; // Ignora cliques simulados do DOM quando o WebXR está ativo + if (gl.xr.isPresenting) return; const onClick = (e: MouseEvent) => { if (snapPoint) { addMeasurePoint({ x: snapPoint.x, y: snapPoint.y, z: snapPoint.z }); @@ -562,8 +478,7 @@ function XRSnapHandler() { return null; } -// ─── XR Grid ─────────────────────────────────────────── -function XRGrid() { +export function XRGrid() { const showGrid = useModelStore((s) => s.showGrid); const gridY = useModelStore((s) => s.gridY); if (!showGrid) return null; @@ -583,8 +498,7 @@ function XRGrid() { ); } -/** Auto-follow grid to model bottom when enabled. */ -function XRGridAutoFollower() { +export function XRGridAutoFollower() { const models = useModelStore((s) => s.models); const { scene } = useThree(); useEffect(() => { @@ -612,258 +526,3 @@ function XRGridAutoFollower() { }, [models, scene]); return null; } - -// ImageTrackingAnchor removed — replaced by XRHitTestPlacement - -// ─── XRSession Page ──────────────────────────────────── -const XRSession = () => { - const navigate = useNavigate(); - const { model, anchorMode, setAnchorMode } = useModelStore(); - const isActiveLocked = useModelStore((s) => { - const a = s.models.find(m => m.id === s.activeModelId); - return !!a?.locked; - }); - const [inXR, setInXR] = useState(false); - const [freeMove, setFreeMove] = useState(true); - const [placementMode, setPlacementMode] = useState(true); // start in placement mode - const [snapToPlanes, setSnapToPlanes] = useState(true); - // Zoom por grip duplo — toggle real, padrão ligado. - const [allowScale, setAllowScale] = useState(true); - const [liveCode, setLiveCode] = useState(null); - const [liveViewers, setLiveViewers] = useState(0); - // Rig que envolve — locomoção por joystick escreve aqui. - const rigRef = useRef(null); - // DEVKIT: simXR forces in-XR rendering on desktop without a real WebXR session - const devkit = useDevKit(); - const [simXR, setSimXR] = useState(false); - const effectiveInXR = inXR || simXR; - - useEffect(() => { - if (!model) navigate('/'); - }, [model, navigate]); - - useEffect(() => { - const unsubscribe = store.subscribe((state) => { - const session = state.session; - if (session && !inXR) { - console.log('[XR] ✅ Sessão AR ativa!'); - setInXR(true); - setAnchorMode('manual'); - toast.success('Sessão AR iniciada!'); - session.addEventListener('end', () => { - console.log('[XR] ❌ Sessão AR encerrada'); - setInXR(false); - }); - } - }); - return unsubscribe; - }, [inXR, setAnchorMode]); - - const handleDownloadMarker = useCallback(async () => { - const url = await generateMarkerDownloadURL(); - const a = document.createElement('a'); - a.href = url; - a.download = 'TrackSteelXR_Marker.png'; - a.click(); - URL.revokeObjectURL(url); - toast.success('Marcador baixado — Imprima em 15×15cm'); - }, []); - - if (!model) return null; - - return ( -
- {/* Header */} -
-
- - -

- Modo XR Imersivo -

-
- -
- {!inXR && ( - <> - - - - )} - {inXR && ( - - - XR Ativo - - )} -
-
- - {/* 3D Canvas */} -
- { - gl.setPixelRatio(Math.min(window.devicePixelRatio, 1.5)); - gl.setClearColor(0x000000, 0); - }} - > - - - - - - - - - - {effectiveInXR ? ( - <> - {/* In XR (or SimXR): all models share the same placement origin so - switching active doesn't make others appear to "disappear". - Only the ACTIVE model receives grab transforms. */} - {simXR ? ( - - - { if (placementMode) setPlacementMode(false); }} - > - - - - ) : ( - { - setPlacementMode(false); - toast.success('Modelo posicionado na superfície!'); - }} - > - - { if (placementMode) setPlacementMode(false); }} - > - - - - )} - - - - - - - - {/* In-world HUD — visible inside passthrough where DOM overlays cannot reach */} - setFreeMove(!freeMove)} - placementMode={placementMode} - onTogglePlacement={() => setPlacementMode(!placementMode)} - snapToPlanes={snapToPlanes} - onToggleSnap={() => setSnapToPlanes(!snapToPlanes)} - allowScale={allowScale} - onToggleAllowScale={() => setAllowScale(!allowScale)} - liveCode={liveCode} - liveViewers={liveViewers} - onStartLive={() => (window as unknown as { __trackSteelStartLive?: () => void }).__trackSteelStartLive?.()} - onStopLive={() => (window as unknown as { __trackSteelStopLive?: () => void }).__trackSteelStopLive?.()} - /> - - {/* Opaque mirror canvas used as the WebRTC source while in AR */} - - - {/* DEVKIT: fake controllers visible in scene + keyboard driver */} - {devkit && simXR && } - {/* DEVKIT: orbit camera in SimXR so you can navigate around the model */} - {simXR && ( - - )} - - ) : ( - <> - {/* Desktop preview (no headset): show all models with OrbitControls */} - - - - - - - - )} - - - - - - - - {/* Floating DOM HUD overlay — only visible OUTSIDE passthrough. - Inside AR, the DOM is occluded by the headset compositor; the - in-world XRHudInWorld replaces it. */} -
- setFreeMove(!freeMove)} - placementMode={placementMode} - onTogglePlacement={() => setPlacementMode(!placementMode)} - snapToPlanes={snapToPlanes} - onToggleSnap={() => setSnapToPlanes(!snapToPlanes)} - allowScale={allowScale} - onToggleAllowScale={() => setAllowScale(!allowScale)} - /> -
- - {/* Hidden ShareButton instance — registers window.__trackSteelStartLive - so the in-world Share tab can trigger broadcasts. Also keeps live - state in sync with the in-XR HUD. */} -
- setLiveCode(h?.code ?? null)} - onViewerCountChange={setLiveViewers} - /> -
- - {/* DEVKIT: floating diagnostic panel + SimXR toggle */} - {devkit && ( - setSimXR((v) => !v)} /> - )} -
-
- ); -}; - -export default XRSession; diff --git a/src/lib/xrStore.ts b/src/lib/xrStore.ts new file mode 100644 index 0000000..d605498 --- /dev/null +++ b/src/lib/xrStore.ts @@ -0,0 +1,37 @@ +import { createXRStore } from '@react-three/xr'; + +function loadXRFeatures() { + const defaults = { + handTracking: true, + planeDetection: true, + hitTest: true, + domOverlay: true, + anchors: true, + meshDetection: true, + depthSensing: true, + layers: true, + bodyTracking: true, + lightEstimation: true, + }; + try { + const raw = localStorage.getItem('xrFeatures'); + if (raw) return { ...defaults, ...JSON.parse(raw) }; + } catch {} + return defaults; +} + +const _xrf = loadXRFeatures(); + +export const xrStore = createXRStore({ + hand: { left: true, right: true }, + controller: { left: true, right: true }, + handTracking: _xrf.handTracking, + planeDetection: _xrf.planeDetection, + hitTest: _xrf.hitTest, + domOverlay: _xrf.domOverlay, + anchors: _xrf.anchors, + meshDetection: _xrf.meshDetection, + depthSensing: _xrf.depthSensing, + layers: _xrf.layers, + bodyTracking: _xrf.bodyTracking, +}); diff --git a/src/pages/Viewer.tsx b/src/pages/Viewer.tsx index 3c3f616..b33da93 100644 --- a/src/pages/Viewer.tsx +++ b/src/pages/Viewer.tsx @@ -3,6 +3,7 @@ import { ArrowLeft, Glasses, Box, Ruler, FileText, Upload, Loader2 } from "lucid import { Button } from "@/components/ui/button"; import { useModelStore } from "@/stores/useModelStore"; import { ModelViewerCanvas } from "@/components/three/ModelViewer"; +import { xrStore } from "@/lib/xrStore"; import { ViewerControls } from "@/components/ViewerControls"; import { InspectionChecklist } from "@/components/InspectionChecklist"; import { FineTuningControls } from "@/components/FineTuningControls"; @@ -92,11 +93,12 @@ const Viewer = () => { if (!model) return null; const handleEnterXR = async () => { - if (!xrSupported) { - toast.error("WebXR não é suportado neste navegador. Use o navegador do Meta Quest 3."); - return; + try { + await xrStore.enterAR(); + } catch (e) { + console.error('[XR] Falha ao entrar no AR:', e); + toast.error("Não foi possível iniciar a sessão AR. Certifique-se de que está usando um dispositivo compatível (como Meta Quest 3)."); } - navigate("/xr"); }; const canAddMore = models.length < maxModels; @@ -129,7 +131,7 @@ const Viewer = () => {