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 { 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