Move XR store to module level
Refactor XRSession to instantiate a module-level XR store, remove legacy per-call store creation, ensure AR entry is triggered directly from user gesture, and adjust minor XR-related UI and state handling to align with Quest 3 browser expectations. X-Lovable-Edit-ID: edt-ef87ec06-a98f-4ade-a9b7-e50d2d513233
This commit is contained in:
+23
-82
@@ -10,26 +10,11 @@ import { ArrowLeft, Move, RotateCw, QrCode, Download, Crosshair } from 'lucide-r
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { generateMarkerDownloadURL } from '@/lib/trackingMarker';
|
||||
|
||||
// --- XR Store singleton ---
|
||||
let xrStoreInstance: ReturnType<typeof createXRStore> | null = null;
|
||||
|
||||
function getOrCreateXRStore() {
|
||||
if (xrStoreInstance) return xrStoreInstance;
|
||||
|
||||
// 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 },
|
||||
});
|
||||
|
||||
return xrStoreInstance;
|
||||
}
|
||||
// --- XR Store at MODULE level (required by Quest 3 browser) ---
|
||||
const store = createXRStore({
|
||||
hand: { left: true, right: true },
|
||||
controller: { left: true, right: true },
|
||||
});
|
||||
|
||||
// ─── XRModel ───────────────────────────────────────────
|
||||
function XRModel({ url }: { url: string }) {
|
||||
@@ -129,8 +114,8 @@ function ControllerFineTuning() {
|
||||
const posStep = 0.001;
|
||||
const rotStep = 0.1;
|
||||
const deadzone = 0.15;
|
||||
const store = useModelStore.getState();
|
||||
const ft = { ...store.fineTuning };
|
||||
const modelStore = useModelStore.getState();
|
||||
const ft = { ...modelStore.fineTuning };
|
||||
|
||||
if (leftAxes) {
|
||||
if (Math.abs(leftAxes[0]) > deadzone) ft.posX += leftAxes[0] * posStep;
|
||||
@@ -150,47 +135,10 @@ function ControllerFineTuning() {
|
||||
// ─── 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);
|
||||
|
||||
useFrame((_, __, frame: any) => {
|
||||
if (!frame || !session || !groupRef.current) return;
|
||||
|
||||
try {
|
||||
const results = frame.getImageTrackingResults?.();
|
||||
if (results && results.length > 0) {
|
||||
for (const result of results) {
|
||||
if (result.trackingState === 'tracked' || result.trackingState === 'emulated') {
|
||||
const refSpace = gl.xr.getReferenceSpace();
|
||||
if (!refSpace) continue;
|
||||
|
||||
const pose = frame.getPose(result.imageSpace, refSpace);
|
||||
if (pose) {
|
||||
const pos = pose.transform.position;
|
||||
const ori = pose.transform.orientation;
|
||||
|
||||
groupRef.current.position.set(pos.x, pos.y, pos.z);
|
||||
groupRef.current.quaternion.set(ori.x, ori.y, ori.z, ori.w);
|
||||
|
||||
if (!trackingActive.current) {
|
||||
trackingActive.current = true;
|
||||
setAnchorMode('tracking');
|
||||
toast.success('QR Code detectado — Modelo ancorado!');
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// getImageTrackingResults not available
|
||||
}
|
||||
|
||||
if (trackingActive.current) return;
|
||||
|
||||
// Manual fallback — place 1.5m in front
|
||||
useFrame(() => {
|
||||
if (!groupRef.current) return;
|
||||
// Place model 1.5m in front of the user
|
||||
groupRef.current.position.set(0, 0, -1.5);
|
||||
groupRef.current.quaternion.identity();
|
||||
});
|
||||
@@ -203,7 +151,6 @@ const XRSession = () => {
|
||||
const navigate = useNavigate();
|
||||
const { model, anchorMode, setAnchorMode } = useModelStore();
|
||||
const [inXR, setInXR] = useState(false);
|
||||
const [store] = useState(() => getOrCreateXRStore());
|
||||
|
||||
useEffect(() => {
|
||||
if (!model) {
|
||||
@@ -211,23 +158,22 @@ const XRSession = () => {
|
||||
}
|
||||
}, [model, navigate]);
|
||||
|
||||
const handleEnterAR = useCallback(async () => {
|
||||
try {
|
||||
const session = await store.enterAR();
|
||||
if (session) {
|
||||
// Listen for XR session state changes
|
||||
useEffect(() => {
|
||||
const unsubscribe = store.subscribe((state) => {
|
||||
const session = state.session;
|
||||
if (session && !inXR) {
|
||||
setInXR(true);
|
||||
setAnchorMode('manual');
|
||||
toast.success('Sessão AR iniciada!');
|
||||
session.addEventListener('end', () => {
|
||||
setInXR(false);
|
||||
setAnchorMode('manual');
|
||||
});
|
||||
setInXR(true);
|
||||
setAnchorMode('manual');
|
||||
toast.success('Sessão XR iniciada!');
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('XR error:', err);
|
||||
toast.error(`Falha ao iniciar XR: ${err.message}`);
|
||||
}
|
||||
}, [store, setAnchorMode]);
|
||||
});
|
||||
return unsubscribe;
|
||||
}, [inXR, setAnchorMode]);
|
||||
|
||||
const handleDownloadMarker = useCallback(async () => {
|
||||
const url = await generateMarkerDownloadURL();
|
||||
@@ -261,7 +207,8 @@ const XRSession = () => {
|
||||
<Download className="h-3.5 w-3.5" />
|
||||
<span className="font-mono text-xs">Marcador QR</span>
|
||||
</Button>
|
||||
<Button className="gap-2 glow-primary" onClick={handleEnterAR}>
|
||||
{/* CRITICAL: Direct call without async wrapper to preserve user gesture */}
|
||||
<Button className="gap-2 glow-primary" onClick={() => store.enterAR()}>
|
||||
Iniciar Sessão AR
|
||||
</Button>
|
||||
</>
|
||||
@@ -318,12 +265,6 @@ const XRSession = () => {
|
||||
{/* Floating HUD overlay */}
|
||||
<div className="absolute bottom-4 left-4 right-4 z-10 pointer-events-none">
|
||||
<div className="pointer-events-auto inline-flex flex-wrap gap-3 rounded-lg border bg-card/90 backdrop-blur-sm p-3 shadow-lg">
|
||||
<div className="flex items-center gap-2">
|
||||
<QrCode className="h-3.5 w-3.5 text-primary" />
|
||||
<span className="font-mono text-[10px] text-muted-foreground">
|
||||
{anchorMode === 'tracking' ? '● QR Detectado' : '○ Buscando QR…'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Move className="h-3.5 w-3.5 text-primary" />
|
||||
<span className="font-mono text-[10px] text-muted-foreground">
|
||||
|
||||
Reference in New Issue
Block a user