Add rotX/rotZ support
Implemented 3-axis rotation (X, Y, Z) for XR model, updated model store, applied in XRSession and ModelViewer, and added a Help panel with AR tool instructions. Also wired a new slide-out help UI and integrated rotation controls into HUD. X-Lovable-Edit-ID: edt-136f1e77-9880-4984-8ca2-cf01457dc714
This commit is contained in:
+122
-13
@@ -2,7 +2,7 @@ import { useState } from 'react';
|
||||
import {
|
||||
Eye, LayoutGrid, Grid3X3, Box, Ruler, Trash2, Camera,
|
||||
Move, RotateCw, RefreshCw, ChevronUp, ChevronDown, Grip,
|
||||
ClipboardCheck, X, Crosshair, Magnet,
|
||||
ClipboardCheck, X, Crosshair, Magnet, HelpCircle,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Slider } from '@/components/ui/slider';
|
||||
@@ -23,9 +23,73 @@ interface XRHudProps {
|
||||
onToggleSnap?: () => void;
|
||||
}
|
||||
|
||||
const helpItems = [
|
||||
{
|
||||
title: 'Posicionar (Hit-Test)',
|
||||
icon: '🎯',
|
||||
desc: 'Aponte para uma superfície real e toque para posicionar o modelo. O retículo verde indica onde o modelo será colocado.',
|
||||
},
|
||||
{
|
||||
title: 'Snap (Planos)',
|
||||
icon: '🧲',
|
||||
desc: 'Quando ativado, o modelo se alinha automaticamente a planos detectados (chão, parede, mesa). O retículo fica azul quando snap está ativo.',
|
||||
},
|
||||
{
|
||||
title: 'Mover (Free Move)',
|
||||
icon: '🕹️',
|
||||
desc: 'Joystick esquerdo: move o modelo nos eixos X/Y. Joystick direito (vertical): move no eixo Z (profundidade). Quando desativado, é necessário segurar o grip para mover.',
|
||||
},
|
||||
{
|
||||
title: 'Grid',
|
||||
icon: '📐',
|
||||
desc: 'Exibe/oculta a grade de referência para auxiliar no alinhamento visual do modelo com o ambiente.',
|
||||
},
|
||||
{
|
||||
title: 'Sólido / Wire / Bordas',
|
||||
icon: '🔲',
|
||||
desc: 'Alterna entre modos de renderização: Sólido (preenchido), Wireframe (malha de arame), Bordas (apenas contornos).',
|
||||
},
|
||||
{
|
||||
title: 'Medir',
|
||||
icon: '📏',
|
||||
desc: 'Ative e toque em dois pontos no modelo para medir a distância entre eles em milímetros. O snap de vértice auxilia na precisão.',
|
||||
},
|
||||
{
|
||||
title: 'Screenshot',
|
||||
icon: '📸',
|
||||
desc: 'Captura uma imagem da visão AR atual. As capturas ficam salvas na galeria do viewer.',
|
||||
},
|
||||
{
|
||||
title: 'Inspeção',
|
||||
icon: '📋',
|
||||
desc: 'Abre o checklist de inspeção para aprovar ou rejeitar itens como dimensões, furos, soldas e posição das placas.',
|
||||
},
|
||||
{
|
||||
title: 'Opacidade',
|
||||
icon: '👁️',
|
||||
desc: 'Ajusta a transparência do modelo de 0% (invisível) a 100% (opaco) para visualizar através da peça.',
|
||||
},
|
||||
{
|
||||
title: 'Posição (mm)',
|
||||
icon: '↔️',
|
||||
desc: 'Ajuste fino da posição do modelo nos eixos X, Y e Z com incrementos de 1mm usando os botões +/−.',
|
||||
},
|
||||
{
|
||||
title: 'Rotação X° / Y° / Z°',
|
||||
icon: '🔄',
|
||||
desc: 'Rotaciona o modelo ao redor de cada eixo com incrementos de 0.1°. X = inclinação frontal, Y = giro lateral, Z = tombamento.',
|
||||
},
|
||||
{
|
||||
title: 'Controladores Quest 3',
|
||||
icon: '🎮',
|
||||
desc: 'Joystick Esq: mover X/Y • Joystick Dir: mover Z + rotação Y • Grip (segurar): habilita movimento quando Free Move está desativado.',
|
||||
},
|
||||
];
|
||||
|
||||
export function XRHud({ freeMove, onToggleFreeMove, placementMode = false, onTogglePlacement, snapToPlanes = true, onToggleSnap }: XRHudProps) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [checklistOpen, setChecklistOpen] = useState(false);
|
||||
const [helpOpen, setHelpOpen] = useState(false);
|
||||
|
||||
const {
|
||||
opacity, setOpacity,
|
||||
@@ -65,7 +129,6 @@ export function XRHud({ freeMove, onToggleFreeMove, placementMode = false, onTog
|
||||
<span className="font-mono text-xs font-semibold text-foreground">Inspeção</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Progress badge */}
|
||||
<span className="font-mono text-[10px] text-muted-foreground">
|
||||
{approved + rejected}/{total}
|
||||
</span>
|
||||
@@ -82,6 +145,38 @@ export function XRHud({ freeMove, onToggleFreeMove, placementMode = false, onTog
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Slide-out help panel ── */}
|
||||
<div
|
||||
className={`absolute top-0 left-0 z-30 h-full w-80 max-w-[85vw] transition-transform duration-300 ease-in-out ${
|
||||
helpOpen ? 'translate-x-0' : '-translate-x-full'
|
||||
}`}
|
||||
>
|
||||
<div className="h-full border-r bg-card/95 backdrop-blur-md shadow-2xl flex flex-col">
|
||||
<div className="flex items-center justify-between border-b px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<HelpCircle className="h-4 w-4 text-primary" />
|
||||
<span className="font-mono text-xs font-semibold text-foreground">Ajuda — Ferramentas AR</span>
|
||||
</div>
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => setHelpOpen(false)}>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<ScrollArea className="flex-1">
|
||||
<div className="p-3 space-y-2">
|
||||
{helpItems.map((item) => (
|
||||
<div key={item.title} className="rounded-lg border bg-muted/30 p-3 space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm">{item.icon}</span>
|
||||
<span className="font-mono text-[11px] font-semibold text-foreground">{item.title}</span>
|
||||
</div>
|
||||
<p className="text-[10px] leading-relaxed text-muted-foreground">{item.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Bottom HUD ── */}
|
||||
<div className="absolute bottom-0 left-0 right-0 z-20 pointer-events-none p-3">
|
||||
<div className="pointer-events-auto mx-auto max-w-lg space-y-2">
|
||||
@@ -129,16 +224,25 @@ export function XRHud({ freeMove, onToggleFreeMove, placementMode = false, onTog
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center gap-1 rounded bg-muted/50 px-1.5 py-1 max-w-[33%]">
|
||||
<RotateCw className="h-2.5 w-2.5 text-primary" />
|
||||
<span className="font-mono text-[9px] text-muted-foreground w-4">Y°</span>
|
||||
<Button variant="ghost" size="icon" className="h-5 w-5 text-[10px]"
|
||||
onClick={() => setFineTuning({ rotY: fineTuning.rotY - ROTATION_STEP })}>−</Button>
|
||||
<span className="font-mono text-[9px] text-foreground flex-1 text-center">
|
||||
{fineTuning.rotY.toFixed(1)}
|
||||
</span>
|
||||
<Button variant="ghost" size="icon" className="h-5 w-5 text-[10px]"
|
||||
onClick={() => setFineTuning({ rotY: fineTuning.rotY + ROTATION_STEP })}>+</Button>
|
||||
|
||||
{/* Rotation X, Y, Z */}
|
||||
<div className="flex items-center gap-1.5 mt-1">
|
||||
<RotateCw className="h-3 w-3 text-primary" />
|
||||
<span className="font-mono text-[10px] text-muted-foreground">Rotação</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-1.5">
|
||||
{(['rotX', 'rotY', 'rotZ'] as const).map((axis) => (
|
||||
<div key={axis} className="flex items-center gap-1 rounded bg-muted/50 px-1.5 py-1">
|
||||
<span className="font-mono text-[9px] text-muted-foreground w-4">{axis.slice(-1).toUpperCase()}°</span>
|
||||
<Button variant="ghost" size="icon" className="h-5 w-5 text-[10px]"
|
||||
onClick={() => setFineTuning({ [axis]: fineTuning[axis] - ROTATION_STEP })}>−</Button>
|
||||
<span className="font-mono text-[9px] text-foreground flex-1 text-center">
|
||||
{fineTuning[axis].toFixed(1)}
|
||||
</span>
|
||||
<Button variant="ghost" size="icon" className="h-5 w-5 text-[10px]"
|
||||
onClick={() => setFineTuning({ [axis]: fineTuning[axis] + ROTATION_STEP })}>+</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -147,6 +251,11 @@ export function XRHud({ freeMove, onToggleFreeMove, placementMode = false, onTog
|
||||
{/* Main toolbar */}
|
||||
<div className="flex items-center justify-between rounded-xl border bg-card/90 backdrop-blur-md p-2 shadow-2xl">
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
{/* Help button */}
|
||||
<Button variant={helpOpen ? 'default' : 'outline'} size="icon" className="h-8 w-8" onClick={() => setHelpOpen(!helpOpen)} title="Ajuda">
|
||||
<HelpCircle className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => setExpanded(!expanded)}>
|
||||
{expanded ? <ChevronDown className="h-4 w-4" /> : <ChevronUp className="h-4 w-4" />}
|
||||
</Button>
|
||||
@@ -247,4 +356,4 @@ export function XRHud({ freeMove, onToggleFreeMove, placementMode = false, onTog
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -114,7 +114,9 @@ function GLBModel({ url }: { url: string }) {
|
||||
});
|
||||
}, [scene, opacity, renderMode, checklist, wireframeColor, wireframeThickness, edgeThresholdAngle]);
|
||||
|
||||
const rotXRad = (fineTuning.rotX * Math.PI) / 180;
|
||||
const rotYRad = (fineTuning.rotY * Math.PI) / 180;
|
||||
const rotZRad = (fineTuning.rotZ * Math.PI) / 180;
|
||||
|
||||
return (
|
||||
<group
|
||||
@@ -124,7 +126,7 @@ function GLBModel({ url }: { url: string }) {
|
||||
-modelInfo.center.y + fineTuning.posY,
|
||||
-modelInfo.center.z + fineTuning.posZ,
|
||||
]}
|
||||
rotation={[0, rotYRad, 0]}
|
||||
rotation={[rotXRad, rotYRad, rotZRad]}
|
||||
>
|
||||
<primitive object={scene} />
|
||||
</group>
|
||||
|
||||
@@ -108,7 +108,9 @@ function XRModel({ url }: { url: string }) {
|
||||
});
|
||||
}, [scene, opacity, renderMode, checklist, wireframeColor, wireframeThickness, edgeThresholdAngle]);
|
||||
|
||||
const rotXRad = (fineTuning.rotX * Math.PI) / 180;
|
||||
const rotYRad = (fineTuning.rotY * Math.PI) / 180;
|
||||
const rotZRad = (fineTuning.rotZ * Math.PI) / 180;
|
||||
|
||||
return (
|
||||
<group
|
||||
@@ -118,7 +120,7 @@ function XRModel({ url }: { url: string }) {
|
||||
-modelInfo.center.y + fineTuning.posY,
|
||||
-modelInfo.center.z + fineTuning.posZ,
|
||||
]}
|
||||
rotation={[0, rotYRad, 0]}
|
||||
rotation={[rotXRad, rotYRad, rotZRad]}
|
||||
>
|
||||
<primitive object={scene} />
|
||||
</group>
|
||||
|
||||
@@ -10,7 +10,9 @@ interface FineTuning {
|
||||
posX: number;
|
||||
posY: number;
|
||||
posZ: number;
|
||||
rotX: number;
|
||||
rotY: number;
|
||||
rotZ: number;
|
||||
}
|
||||
|
||||
type AnchorMode = 'tracking' | 'manual' | 'fine-tuning';
|
||||
@@ -117,7 +119,7 @@ interface ModelStore {
|
||||
clearScreenshots: () => void;
|
||||
}
|
||||
|
||||
const defaultFineTuning: FineTuning = { posX: 0, posY: 0, posZ: 0, rotY: 0 };
|
||||
const defaultFineTuning: FineTuning = { posX: 0, posY: 0, posZ: 0, rotX: 0, rotY: 0, rotZ: 0 };
|
||||
|
||||
export const useModelStore = create<ModelStore>((set) => ({
|
||||
model: null,
|
||||
|
||||
Reference in New Issue
Block a user