Corrigir entrada XR Quest 3

Refactor XRSession.tsx to use lazy-initialized XR store with image-tracking via customSessionInit, remove manual navigator.xr.requestSession, and conditionally emulate Quest3 only on localhost. Adds singleton marker bitmap loading and initial XR store fallback, updates to render with updated store and AR entry flow.

X-Lovable-Edit-ID: edt-b777747a-a859-4071-93ad-831f6c0fc336
This commit is contained in:
gpt-engineer-app[bot]
2026-02-27 19:44:53 +00:00
+78 -80
View File
@@ -10,7 +10,7 @@ import { ArrowLeft, Move, RotateCw, QrCode, Download, Crosshair } from 'lucide-r
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { generateTrackingMarker, generateMarkerDownloadURL } from '@/lib/trackingMarker'; import { generateTrackingMarker, generateMarkerDownloadURL } from '@/lib/trackingMarker';
// Store tracking marker bitmap globally so it persists across renders // --- Marker bitmap singleton ---
let markerBitmapPromise: Promise<ImageBitmap> | null = null; let markerBitmapPromise: Promise<ImageBitmap> | null = null;
function getMarkerBitmap(): Promise<ImageBitmap> { function getMarkerBitmap(): Promise<ImageBitmap> {
if (!markerBitmapPromise) { if (!markerBitmapPromise) {
@@ -19,15 +19,59 @@ function getMarkerBitmap(): Promise<ImageBitmap> {
return markerBitmapPromise; return markerBitmapPromise;
} }
const xrStore = createXRStore({ // --- XR Store singleton (lazy-initialized with marker) ---
emulate: 'metaQuest3', let xrStoreInstance: ReturnType<typeof createXRStore> | null = null;
hand: { left: true, right: true },
controller: { left: true, right: true },
});
/** async function getXRStore() {
* The 3D model rendered inside the XR scene. 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 }) { function XRModel({ url }: { url: string }) {
const { scene } = useGLTF(url); const { scene } = useGLTF(url);
const ref = useRef<THREE.Group>(null); const ref = useRef<THREE.Group>(null);
@@ -93,9 +137,7 @@ function XRModel({ url }: { url: string }) {
); );
} }
/** // ─── ControllerFineTuning ──────────────────────────────
* Controller-based fine tuning input handler.
*/
function ControllerFineTuning() { function ControllerFineTuning() {
const { setFineTuning } = useModelStore(); const { setFineTuning } = useModelStore();
const session = useXR((s) => s.session); const session = useXR((s) => s.session);
@@ -145,23 +187,17 @@ function ControllerFineTuning() {
return null; return null;
} }
/** // ─── ImageTrackingAnchor ───────────────────────────────
* 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.
*/
function ImageTrackingAnchor({ children }: { children: React.ReactNode }) { function ImageTrackingAnchor({ children }: { children: React.ReactNode }) {
const groupRef = useRef<THREE.Group>(null); const groupRef = useRef<THREE.Group>(null);
const session = useXR((s) => s.session); const session = useXR((s) => s.session);
const { gl } = useThree(); const { gl } = useThree();
const { setAnchorMode } = useModelStore(); const { setAnchorMode } = useModelStore();
const trackingActive = useRef(false); const trackingActive = useRef(false);
const lastPoseMatrix = useRef(new THREE.Matrix4());
useFrame((_, __, frame: any) => { useFrame((_, __, frame: any) => {
if (!frame || !session || !groupRef.current) return; if (!frame || !session || !groupRef.current) return;
// Try to get image tracking results from the XR frame
try { try {
const results = frame.getImageTrackingResults?.(); const results = frame.getImageTrackingResults?.();
if (results && results.length > 0) { if (results && results.length > 0) {
@@ -189,89 +225,51 @@ function ImageTrackingAnchor({ children }: { children: React.ReactNode }) {
} }
} }
} catch { } catch {
// getImageTrackingResults not available — fall back to manual // getImageTrackingResults not available
} }
// If tracking was active but lost if (trackingActive.current) return;
if (trackingActive.current) {
// Keep last known position (don't reset)
return;
}
// Manual fallback — place 1.5m in front // Manual fallback — place 1.5m in front
if (!trackingActive.current) { groupRef.current.position.set(0, 0, -1.5);
groupRef.current.position.set(0, 0, -1.5); groupRef.current.quaternion.identity();
groupRef.current.quaternion.identity();
}
}); });
return <group ref={groupRef}>{children}</group>; return <group ref={groupRef}>{children}</group>;
} }
// ─── XRSession Page ────────────────────────────────────
const XRSession = () => { const XRSession = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { model, xrSupported, anchorMode, setAnchorMode } = useModelStore(); const { model, anchorMode, setAnchorMode } = useModelStore();
const [inXR, setInXR] = useState(false); const [inXR, setInXR] = useState(false);
const [markerReady, setMarkerReady] = useState(false); const [store, setStore] = useState(() => getInitialXRStore());
// Pre-load marker and rebuild store with customSessionInit
useEffect(() => { useEffect(() => {
if (!model) { if (!model) {
navigate('/'); navigate('/');
return; return;
} }
// Pre-load marker bitmap getXRStore().then((s) => {
getMarkerBitmap().then(() => setMarkerReady(true)); // Update store ref if it was replaced with the marker-enabled one
if (s !== store) setStore(s);
});
}, [model, navigate]); }, [model, navigate]);
const handleEnterAR = useCallback(async () => { const handleEnterAR = useCallback(async () => {
try { try {
// We attempt to request with image-tracking as optional feature. const s = await getXRStore();
// The createXRStore.enterAR will handle the session request. const session = await s.enterAR();
// We need to configure the session with trackedImages. if (session) {
// Since @react-three/xr doesn't directly support trackedImages, session.addEventListener('end', () => {
// we'll try to patch it through the native API. setInXR(false);
setAnchorMode('manual');
const bitmap = await getMarkerBitmap(); });
setInXR(true);
// Try entering AR with image tracking support setAnchorMode('manual');
// The @react-three/xr store manages the session, but we can toast.success('Sessão XR iniciada — Buscando QR Code…');
// 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');
} }
await xrStore.enterAR();
setInXR(true);
setAnchorMode('manual');
toast.success('Sessão XR iniciada — Buscando QR Code…');
} catch (err: any) { } catch (err: any) {
console.error('XR error:', err); console.error('XR error:', err);
toast.error(`Falha ao iniciar XR: ${err.message}`); toast.error(`Falha ao iniciar XR: ${err.message}`);
@@ -349,7 +347,7 @@ const XRSession = () => {
gl.setClearColor(0x000000, 0); gl.setClearColor(0x000000, 0);
}} }}
> >
<XR store={xrStore}> <XR store={store}>
<ambientLight intensity={0.8} /> <ambientLight intensity={0.8} />
<directionalLight position={[3, 5, 3]} intensity={1.2} /> <directionalLight position={[3, 5, 3]} intensity={1.2} />
<directionalLight position={[-3, 3, -3]} intensity={0.4} /> <directionalLight position={[-3, 3, -3]} intensity={0.4} />