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:
+78
-80
@@ -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<ImageBitmap> | null = null;
|
||||
function getMarkerBitmap(): Promise<ImageBitmap> {
|
||||
if (!markerBitmapPromise) {
|
||||
@@ -19,15 +19,59 @@ function getMarkerBitmap(): Promise<ImageBitmap> {
|
||||
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<typeof createXRStore> | 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<THREE.Group>(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<THREE.Group>(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 <group ref={groupRef}>{children}</group>;
|
||||
}
|
||||
|
||||
// ─── 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);
|
||||
}}
|
||||
>
|
||||
<XR store={xrStore}>
|
||||
<XR store={store}>
|
||||
<ambientLight intensity={0.8} />
|
||||
<directionalLight position={[3, 5, 3]} intensity={1.2} />
|
||||
<directionalLight position={[-3, 3, -3]} intensity={0.4} />
|
||||
|
||||
Reference in New Issue
Block a user