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 { Button } from '@/components/ui/button';
|
||||||
import { generateMarkerDownloadURL } from '@/lib/trackingMarker';
|
import { generateMarkerDownloadURL } from '@/lib/trackingMarker';
|
||||||
|
|
||||||
// --- XR Store singleton ---
|
// --- XR Store at MODULE level (required by Quest 3 browser) ---
|
||||||
let xrStoreInstance: ReturnType<typeof createXRStore> | null = null;
|
const store = createXRStore({
|
||||||
|
hand: { left: true, right: true },
|
||||||
function getOrCreateXRStore() {
|
controller: { left: true, right: true },
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── XRModel ───────────────────────────────────────────
|
// ─── XRModel ───────────────────────────────────────────
|
||||||
function XRModel({ url }: { url: string }) {
|
function XRModel({ url }: { url: string }) {
|
||||||
@@ -129,8 +114,8 @@ function ControllerFineTuning() {
|
|||||||
const posStep = 0.001;
|
const posStep = 0.001;
|
||||||
const rotStep = 0.1;
|
const rotStep = 0.1;
|
||||||
const deadzone = 0.15;
|
const deadzone = 0.15;
|
||||||
const store = useModelStore.getState();
|
const modelStore = useModelStore.getState();
|
||||||
const ft = { ...store.fineTuning };
|
const ft = { ...modelStore.fineTuning };
|
||||||
|
|
||||||
if (leftAxes) {
|
if (leftAxes) {
|
||||||
if (Math.abs(leftAxes[0]) > deadzone) ft.posX += leftAxes[0] * posStep;
|
if (Math.abs(leftAxes[0]) > deadzone) ft.posX += leftAxes[0] * posStep;
|
||||||
@@ -150,47 +135,10 @@ function ControllerFineTuning() {
|
|||||||
// ─── ImageTrackingAnchor ───────────────────────────────
|
// ─── ImageTrackingAnchor ───────────────────────────────
|
||||||
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 { gl } = useThree();
|
|
||||||
const { setAnchorMode } = useModelStore();
|
|
||||||
const trackingActive = useRef(false);
|
|
||||||
|
|
||||||
useFrame((_, __, frame: any) => {
|
useFrame(() => {
|
||||||
if (!frame || !session || !groupRef.current) return;
|
if (!groupRef.current) return;
|
||||||
|
// Place model 1.5m in front of the user
|
||||||
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
|
|
||||||
groupRef.current.position.set(0, 0, -1.5);
|
groupRef.current.position.set(0, 0, -1.5);
|
||||||
groupRef.current.quaternion.identity();
|
groupRef.current.quaternion.identity();
|
||||||
});
|
});
|
||||||
@@ -203,7 +151,6 @@ const XRSession = () => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { model, anchorMode, setAnchorMode } = useModelStore();
|
const { model, anchorMode, setAnchorMode } = useModelStore();
|
||||||
const [inXR, setInXR] = useState(false);
|
const [inXR, setInXR] = useState(false);
|
||||||
const [store] = useState(() => getOrCreateXRStore());
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!model) {
|
if (!model) {
|
||||||
@@ -211,23 +158,22 @@ const XRSession = () => {
|
|||||||
}
|
}
|
||||||
}, [model, navigate]);
|
}, [model, navigate]);
|
||||||
|
|
||||||
const handleEnterAR = useCallback(async () => {
|
// Listen for XR session state changes
|
||||||
try {
|
useEffect(() => {
|
||||||
const session = await store.enterAR();
|
const unsubscribe = store.subscribe((state) => {
|
||||||
if (session) {
|
const session = state.session;
|
||||||
|
if (session && !inXR) {
|
||||||
|
setInXR(true);
|
||||||
|
setAnchorMode('manual');
|
||||||
|
toast.success('Sessão AR iniciada!');
|
||||||
session.addEventListener('end', () => {
|
session.addEventListener('end', () => {
|
||||||
setInXR(false);
|
setInXR(false);
|
||||||
setAnchorMode('manual');
|
setAnchorMode('manual');
|
||||||
});
|
});
|
||||||
setInXR(true);
|
|
||||||
setAnchorMode('manual');
|
|
||||||
toast.success('Sessão XR iniciada!');
|
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
});
|
||||||
console.error('XR error:', err);
|
return unsubscribe;
|
||||||
toast.error(`Falha ao iniciar XR: ${err.message}`);
|
}, [inXR, setAnchorMode]);
|
||||||
}
|
|
||||||
}, [store, setAnchorMode]);
|
|
||||||
|
|
||||||
const handleDownloadMarker = useCallback(async () => {
|
const handleDownloadMarker = useCallback(async () => {
|
||||||
const url = await generateMarkerDownloadURL();
|
const url = await generateMarkerDownloadURL();
|
||||||
@@ -261,7 +207,8 @@ const XRSession = () => {
|
|||||||
<Download className="h-3.5 w-3.5" />
|
<Download className="h-3.5 w-3.5" />
|
||||||
<span className="font-mono text-xs">Marcador QR</span>
|
<span className="font-mono text-xs">Marcador QR</span>
|
||||||
</Button>
|
</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
|
Iniciar Sessão AR
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
@@ -318,12 +265,6 @@ const XRSession = () => {
|
|||||||
{/* Floating HUD overlay */}
|
{/* Floating HUD overlay */}
|
||||||
<div className="absolute bottom-4 left-4 right-4 z-10 pointer-events-none">
|
<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="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">
|
<div className="flex items-center gap-2">
|
||||||
<Move className="h-3.5 w-3.5 text-primary" />
|
<Move className="h-3.5 w-3.5 text-primary" />
|
||||||
<span className="font-mono text-[10px] text-muted-foreground">
|
<span className="font-mono text-[10px] text-muted-foreground">
|
||||||
|
|||||||
Reference in New Issue
Block a user