diff --git a/src/pages/XRSession.tsx b/src/pages/XRSession.tsx index 08c143b..1df6fb7 100644 --- a/src/pages/XRSession.tsx +++ b/src/pages/XRSession.tsx @@ -10,7 +10,7 @@ import { ArrowLeft, Move, RotateCw, QrCode, Download, Crosshair } from 'lucide-r import { Button } from '@/components/ui/button'; import { generateTrackingMarker, generateMarkerDownloadURL } from '@/lib/trackingMarker'; -// Store tracking marker bitmap globally so it persists across renders +// --- Marker bitmap singleton --- let markerBitmapPromise: Promise | null = null; function getMarkerBitmap(): Promise { if (!markerBitmapPromise) { @@ -19,15 +19,59 @@ function getMarkerBitmap(): Promise { return markerBitmapPromise; } -const xrStore = createXRStore({ - emulate: 'metaQuest3', - hand: { left: true, right: true }, - controller: { left: true, right: true }, -}); +// --- XR Store singleton (lazy-initialized with marker) --- +let xrStoreInstance: ReturnType | null = null; -/** - * The 3D model rendered inside the XR scene. - */ +async function getXRStore() { + if (xrStoreInstance) return xrStoreInstance; + + const bitmap = await getMarkerBitmap(); + + // Only emulate when on localhost AND no native XR support + const shouldEmulate = + typeof window !== 'undefined' && + window.location.hostname === 'localhost' && + !navigator.xr; + + xrStoreInstance = createXRStore({ + emulate: shouldEmulate ? 'metaQuest3' : false, + hand: { left: true, right: true }, + controller: { left: true, right: true }, + customSessionInit: { + optionalFeatures: ['image-tracking'], + // @ts-ignore — trackedImages is a WebXR extension not yet in TS types + trackedImages: [ + { + image: bitmap, + widthInMeters: 0.15, // 15cm printed marker + }, + ], + }, + }); + + return xrStoreInstance; +} + +// Create a synchronous fallback store for initial render +// (will be replaced once the async one is ready) +function getInitialXRStore() { + if (xrStoreInstance) return xrStoreInstance; + + const shouldEmulate = + typeof window !== 'undefined' && + window.location.hostname === 'localhost' && + !navigator.xr; + + xrStoreInstance = createXRStore({ + emulate: shouldEmulate ? 'metaQuest3' : false, + hand: { left: true, right: true }, + controller: { left: true, right: true }, + }); + + return xrStoreInstance; +} + +// ─── XRModel ─────────────────────────────────────────── function XRModel({ url }: { url: string }) { const { scene } = useGLTF(url); const ref = useRef(null); @@ -93,9 +137,7 @@ function XRModel({ url }: { url: string }) { ); } -/** - * Controller-based fine tuning input handler. - */ +// ─── ControllerFineTuning ────────────────────────────── function ControllerFineTuning() { const { setFineTuning } = useModelStore(); const session = useXR((s) => s.session); @@ -145,23 +187,17 @@ function ControllerFineTuning() { return null; } -/** - * Image Tracking Anchor — uses WebXR's getImageTrackingResults() - * to position the model at a detected QR marker. - * Falls back to manual placement when tracking is lost or unavailable. - */ +// ─── ImageTrackingAnchor ─────────────────────────────── function ImageTrackingAnchor({ children }: { children: React.ReactNode }) { const groupRef = useRef(null); const session = useXR((s) => s.session); const { gl } = useThree(); const { setAnchorMode } = useModelStore(); const trackingActive = useRef(false); - const lastPoseMatrix = useRef(new THREE.Matrix4()); useFrame((_, __, frame: any) => { if (!frame || !session || !groupRef.current) return; - // Try to get image tracking results from the XR frame try { const results = frame.getImageTrackingResults?.(); if (results && results.length > 0) { @@ -189,89 +225,51 @@ function ImageTrackingAnchor({ children }: { children: React.ReactNode }) { } } } catch { - // getImageTrackingResults not available — fall back to manual + // getImageTrackingResults not available } - // If tracking was active but lost - if (trackingActive.current) { - // Keep last known position (don't reset) - return; - } + if (trackingActive.current) return; // Manual fallback — place 1.5m in front - if (!trackingActive.current) { - groupRef.current.position.set(0, 0, -1.5); - groupRef.current.quaternion.identity(); - } + groupRef.current.position.set(0, 0, -1.5); + groupRef.current.quaternion.identity(); }); return {children}; } +// ─── XRSession Page ──────────────────────────────────── const XRSession = () => { const navigate = useNavigate(); - const { model, xrSupported, anchorMode, setAnchorMode } = useModelStore(); + const { model, anchorMode, setAnchorMode } = useModelStore(); const [inXR, setInXR] = useState(false); - const [markerReady, setMarkerReady] = useState(false); + const [store, setStore] = useState(() => getInitialXRStore()); + // Pre-load marker and rebuild store with customSessionInit useEffect(() => { if (!model) { navigate('/'); return; } - // Pre-load marker bitmap - getMarkerBitmap().then(() => setMarkerReady(true)); + getXRStore().then((s) => { + // Update store ref if it was replaced with the marker-enabled one + if (s !== store) setStore(s); + }); }, [model, navigate]); const handleEnterAR = useCallback(async () => { try { - // We attempt to request with image-tracking as optional feature. - // The createXRStore.enterAR will handle the session request. - // We need to configure the session with trackedImages. - // Since @react-three/xr doesn't directly support trackedImages, - // we'll try to patch it through the native API. - - const bitmap = await getMarkerBitmap(); - - // Try entering AR with image tracking support - // The @react-three/xr store manages the session, but we can - // request image-tracking as an optional feature - try { - // Override: request session directly with trackedImages - if (navigator.xr) { - const session = await navigator.xr.requestSession('immersive-ar', { - requiredFeatures: ['local-floor'], - optionalFeatures: [ - 'hand-tracking', - 'image-tracking', - 'anchors', - 'plane-detection', - ], - // @ts-ignore — trackedImages is a WebXR extension - trackedImages: [ - { - image: bitmap, - widthInMeters: 0.15, // 15cm QR code - }, - ], - }); - - // Let @react-three/xr know about this session - // by entering through the store after manual session creation - session.addEventListener('end', () => { - setInXR(false); - setAnchorMode('manual'); - }); - } - } catch { - // Fallback: enter AR without image tracking - console.warn('Image tracking not available, using manual placement'); + const s = await getXRStore(); + const session = await s.enterAR(); + if (session) { + session.addEventListener('end', () => { + setInXR(false); + setAnchorMode('manual'); + }); + setInXR(true); + setAnchorMode('manual'); + toast.success('Sessão XR iniciada — Buscando QR Code…'); } - - await xrStore.enterAR(); - setInXR(true); - setAnchorMode('manual'); - toast.success('Sessão XR iniciada — Buscando QR Code…'); } catch (err: any) { console.error('XR error:', err); toast.error(`Falha ao iniciar XR: ${err.message}`); @@ -349,7 +347,7 @@ const XRSession = () => { gl.setClearColor(0x000000, 0); }} > - +