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:
gpt-engineer-app[bot]
2026-02-28 18:49:34 +00:00
4 changed files with 131 additions and 16 deletions
+122 -13
View File
@@ -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>
</>
);
}
}
+3 -1
View File
@@ -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>
+3 -1
View File
@@ -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>
+3 -1
View File
@@ -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,