🚀 Auto-deploy: melhoria no snap e medição AR em 02/06/2026 21:42:49

This commit is contained in:
2026-06-02 21:42:49 +00:00
parent 002682e6f3
commit 6e80b23687
3 changed files with 85 additions and 33 deletions
+1 -1
View File
@@ -365,7 +365,7 @@ const Index = () => {
{/* Footer */} {/* Footer */}
<p className="mt-16 text-center font-mono text-xs text-muted-foreground/50"> <p className="mt-16 text-center font-mono text-xs text-muted-foreground/50">
SteelXR v1.11 Q.C. Inspection SteelXR v1.12 Q.C. Inspection
</p> </p>
{showLogs && ( {showLogs && (
+1 -26
View File
@@ -259,37 +259,12 @@ const Viewer = () => {
const [enteringXR, setEnteringXR] = useState(false); const [enteringXR, setEnteringXR] = useState(false);
const handleEnterXR = useCallback(async () => { const handleEnterXR = useCallback(() => {
if (!xrSupported) { if (!xrSupported) {
toast.error("WebXR não é suportado neste navegador. Use o navegador do Meta Quest 3."); toast.error("WebXR não é suportado neste navegador. Use o navegador do Meta Quest 3.");
return; return;
} }
setEnteringXR(true);
// Timeout de segurança — se não entrar em 15s, cancela
const timeoutId = setTimeout(() => {
setEnteringXR(false);
toast.error("Tempo limite ao iniciar modo AR. Verifique se o Meta Quest Browser está conectado.");
}, 15000);
try {
// Marca que está entrando no AR antes de navegar — bloqueia auto-return prematuro
(window as unknown as { __setXrEntering?: (v: boolean) => void }).__setXrEntering?.(true);
// Chamada síncrona de enterAR para preservar o User Gesture exigido pelo WebXR
const enterPromise = xrStore.enterAR();
clearTimeout(timeoutId);
navigate("/xr"); navigate("/xr");
await enterPromise;
} catch (e) {
clearTimeout(timeoutId);
setEnteringXR(false);
(window as unknown as { __setXrEntering?: (v: boolean) => void }).__setXrEntering?.(false);
console.error('[Viewer] XR enterAR error:', e);
toast.error("Falha ao iniciar modo AR. Verifique se o Meta Quest Browser está conectado.");
}
}, [xrSupported, navigate]); }, [xrSupported, navigate]);
const handleEnterMeeting = () => { const handleEnterMeeting = () => {
+82 -5
View File
@@ -7,7 +7,7 @@ import { xrStore as store } from '@/stores/useXRStore';
import * as THREE from 'three'; import * as THREE from 'three';
import { useModelStore } from '@/stores/useModelStore'; import { useModelStore } from '@/stores/useModelStore';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { ArrowLeft, Download, QrCode, Crosshair, Home } from 'lucide-react'; import { ArrowLeft, Download, QrCode, Crosshair, Home, Glasses } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { generateMarkerDownloadURL } from '@/lib/trackingMarker'; import { generateMarkerDownloadURL } from '@/lib/trackingMarker';
import { XRHud } from '@/components/XRHud'; import { XRHud } from '@/components/XRHud';
@@ -1050,6 +1050,7 @@ const XRSession = () => {
const effectiveInXR = inXR || simXR; const effectiveInXR = inXR || simXR;
// Indica que enterAR() foi chamado mas a sessão ainda não começou — previne auto-return prematuro // Indica que enterAR() foi chamado mas a sessão ainda não começou — previne auto-return prematuro
const [isEnteringAR, setIsEnteringAR] = useState(false); const [isEnteringAR, setIsEnteringAR] = useState(false);
const [hasClickedEnter, setHasClickedEnter] = useState(false);
useEffect(() => { useEffect(() => {
if (!model) navigate('/'); if (!model) navigate('/');
@@ -1098,12 +1099,10 @@ const XRSession = () => {
toast.success('Marcador baixado — Imprima em 15×15cm'); toast.success('Marcador baixado — Imprima em 15×15cm');
}, []); }, []);
if (!model) return null;
// Auto-return to viewer if user exits AR session // Auto-return to viewer if user exits AR session
useEffect(() => { useEffect(() => {
// Don't auto-return while we're waiting for the XR onboarding dialog to complete // Don't auto-return while we're waiting for the XR onboarding dialog to complete
if (!inXR) { if (hasClickedEnter && !inXR) {
const timer = setTimeout(() => { const timer = setTimeout(() => {
// Only return if the session is truly absent AND we're not in the middle of entering AR // Only return if the session is truly absent AND we're not in the middle of entering AR
if (!store.getState().session) { if (!store.getState().session) {
@@ -1112,7 +1111,9 @@ const XRSession = () => {
}, 2000); }, 2000);
return () => clearTimeout(timer); return () => clearTimeout(timer);
} }
}, [inXR, navigate]); }, [inXR, hasClickedEnter, navigate]);
if (!model) return null;
return ( return (
<div className="flex h-screen flex-col bg-background"> <div className="flex h-screen flex-col bg-background">
@@ -1262,6 +1263,82 @@ const XRSession = () => {
</XR> </XR>
</Canvas> </Canvas>
{/* Onboarding do AR */}
{!effectiveInXR && (
<div className="absolute inset-0 z-50 flex items-center justify-center bg-slate-950/85 backdrop-blur-md p-6">
<div className="w-full max-w-md border border-primary/20 bg-slate-900/95 rounded-2xl p-8 shadow-2xl text-center space-y-6 glow-primary">
<div className="flex justify-center">
<div className="h-16 w-16 items-center justify-center flex rounded-full bg-primary/10 border border-primary/30 animate-pulse">
<Glasses className="h-8 w-8 text-primary" />
</div>
</div>
<div className="space-y-2">
<h2 className="text-xl font-semibold tracking-tight text-foreground font-mono">
Pronto para entrar no <span className="text-primary">Modo AR</span>?
</h2>
<p className="text-sm text-muted-foreground">
Você está prestes a carregar o modelo em escala real 1:1 no seu ambiente físico.
</p>
</div>
{/* Informações da Peça */}
<div className="rounded-lg border border-border bg-slate-950/50 p-4 text-left space-y-2 font-mono text-xs">
<div className="flex justify-between">
<span className="text-muted-foreground">Modelo Ativo:</span>
<span className="text-foreground truncate max-w-[200px]" title={model.fileName}>{model.fileName}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Tamanho:</span>
<span className="text-foreground">{(model.fileSize / (1024 * 1024)).toFixed(2)} MB</span>
</div>
</div>
{/* Dicas Rápidas */}
<div className="text-left space-y-2.5 rounded-lg bg-primary/5 border border-primary/15 p-4 text-xs">
<h3 className="font-semibold text-primary font-mono flex items-center gap-1.5">
💡 Guia de Controles Rápidos:
</h3>
<ul className="space-y-1.5 text-muted-foreground list-disc pl-4">
<li><strong>Teletransporte:</strong> Thumbstick para frente. Solte para teleportar.</li>
<li><strong>Escala Real (1:1):</strong> Aponte o feixe para a peça e teletransporte nela.</li>
<li><strong>Mover Peça:</strong> Segure o Grip de qualquer controle para arrastar.</li>
<li><strong>Girar/Zoom:</strong> Ambas as miras na peça, segure ambos os Grips e mova as mãos.</li>
<li><strong>Menu do App:</strong> Botões A/B/X/Y abrem/fecham o menu flutuante.</li>
</ul>
</div>
{/* Ações */}
<div className="flex flex-col gap-3 pt-2">
<Button
className="h-12 text-sm font-semibold glow-primary w-full gap-2 text-primary-foreground"
onClick={async () => {
setHasClickedEnter(true);
try {
await store.enterAR();
} catch (err) {
console.error("[XR] Falha ao iniciar AR:", err);
toast.error("Não foi possível iniciar o modo AR. Verifique se o Quest 3 está conectado.");
setHasClickedEnter(false);
}
}}
>
<Glasses className="h-5 w-5 animate-bounce" />
Iniciar Visualização AR
</Button>
<Button
variant="outline"
className="h-12 text-sm font-medium border-muted-foreground/20 text-muted-foreground hover:bg-slate-800 hover:text-foreground w-full"
onClick={() => navigate('/viewer')}
>
Voltar ao Viewer
</Button>
</div>
</div>
</div>
)}
{/* Floating DOM HUD overlay — only visible OUTSIDE passthrough. {/* Floating DOM HUD overlay — only visible OUTSIDE passthrough.
Inside AR, the DOM is occluded by the headset compositor; the Inside AR, the DOM is occluded by the headset compositor; the
in-world XRHudInWorld replaces it. */} in-world XRHudInWorld replaces it. */}