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 { 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} />
|
||||||
|
|||||||
Reference in New Issue
Block a user