Changes
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