This commit is contained in:
gpt-engineer-app[bot]
2026-02-27 19:44:52 +00:00
parent 973e7219a8
commit a85fba9866
+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} />