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:
+117
-8
@@ -2,7 +2,7 @@ import { useState } from 'react';
|
|||||||
import {
|
import {
|
||||||
Eye, LayoutGrid, Grid3X3, Box, Ruler, Trash2, Camera,
|
Eye, LayoutGrid, Grid3X3, Box, Ruler, Trash2, Camera,
|
||||||
Move, RotateCw, RefreshCw, ChevronUp, ChevronDown, Grip,
|
Move, RotateCw, RefreshCw, ChevronUp, ChevronDown, Grip,
|
||||||
ClipboardCheck, X, Crosshair, Magnet,
|
ClipboardCheck, X, Crosshair, Magnet, HelpCircle,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Slider } from '@/components/ui/slider';
|
import { Slider } from '@/components/ui/slider';
|
||||||
@@ -23,9 +23,73 @@ interface XRHudProps {
|
|||||||
onToggleSnap?: () => void;
|
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) {
|
export function XRHud({ freeMove, onToggleFreeMove, placementMode = false, onTogglePlacement, snapToPlanes = true, onToggleSnap }: XRHudProps) {
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
const [checklistOpen, setChecklistOpen] = useState(false);
|
const [checklistOpen, setChecklistOpen] = useState(false);
|
||||||
|
const [helpOpen, setHelpOpen] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
opacity, setOpacity,
|
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>
|
<span className="font-mono text-xs font-semibold text-foreground">Inspeção</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{/* Progress badge */}
|
|
||||||
<span className="font-mono text-[10px] text-muted-foreground">
|
<span className="font-mono text-[10px] text-muted-foreground">
|
||||||
{approved + rejected}/{total}
|
{approved + rejected}/{total}
|
||||||
</span>
|
</span>
|
||||||
@@ -82,6 +145,38 @@ export function XRHud({ freeMove, onToggleFreeMove, placementMode = false, onTog
|
|||||||
</div>
|
</div>
|
||||||
</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 ── */}
|
{/* ── Bottom HUD ── */}
|
||||||
<div className="absolute bottom-0 left-0 right-0 z-20 pointer-events-none p-3">
|
<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">
|
<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>
|
</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" />
|
{/* Rotation X, Y, Z */}
|
||||||
<span className="font-mono text-[9px] text-muted-foreground w-4">Y°</span>
|
<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]"
|
<Button variant="ghost" size="icon" className="h-5 w-5 text-[10px]"
|
||||||
onClick={() => setFineTuning({ rotY: fineTuning.rotY - ROTATION_STEP })}>−</Button>
|
onClick={() => setFineTuning({ [axis]: fineTuning[axis] - ROTATION_STEP })}>−</Button>
|
||||||
<span className="font-mono text-[9px] text-foreground flex-1 text-center">
|
<span className="font-mono text-[9px] text-foreground flex-1 text-center">
|
||||||
{fineTuning.rotY.toFixed(1)}
|
{fineTuning[axis].toFixed(1)}
|
||||||
</span>
|
</span>
|
||||||
<Button variant="ghost" size="icon" className="h-5 w-5 text-[10px]"
|
<Button variant="ghost" size="icon" className="h-5 w-5 text-[10px]"
|
||||||
onClick={() => setFineTuning({ rotY: fineTuning.rotY + ROTATION_STEP })}>+</Button>
|
onClick={() => setFineTuning({ [axis]: fineTuning[axis] + ROTATION_STEP })}>+</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -147,6 +251,11 @@ export function XRHud({ freeMove, onToggleFreeMove, placementMode = false, onTog
|
|||||||
{/* Main toolbar */}
|
{/* 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 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">
|
<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)}>
|
<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" />}
|
{expanded ? <ChevronDown className="h-4 w-4" /> : <ChevronUp className="h-4 w-4" />}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -114,7 +114,9 @@ function GLBModel({ url }: { url: string }) {
|
|||||||
});
|
});
|
||||||
}, [scene, opacity, renderMode, checklist, wireframeColor, wireframeThickness, edgeThresholdAngle]);
|
}, [scene, opacity, renderMode, checklist, wireframeColor, wireframeThickness, edgeThresholdAngle]);
|
||||||
|
|
||||||
|
const rotXRad = (fineTuning.rotX * Math.PI) / 180;
|
||||||
const rotYRad = (fineTuning.rotY * Math.PI) / 180;
|
const rotYRad = (fineTuning.rotY * Math.PI) / 180;
|
||||||
|
const rotZRad = (fineTuning.rotZ * Math.PI) / 180;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group
|
<group
|
||||||
@@ -124,7 +126,7 @@ function GLBModel({ url }: { url: string }) {
|
|||||||
-modelInfo.center.y + fineTuning.posY,
|
-modelInfo.center.y + fineTuning.posY,
|
||||||
-modelInfo.center.z + fineTuning.posZ,
|
-modelInfo.center.z + fineTuning.posZ,
|
||||||
]}
|
]}
|
||||||
rotation={[0, rotYRad, 0]}
|
rotation={[rotXRad, rotYRad, rotZRad]}
|
||||||
>
|
>
|
||||||
<primitive object={scene} />
|
<primitive object={scene} />
|
||||||
</group>
|
</group>
|
||||||
|
|||||||
@@ -108,7 +108,9 @@ function XRModel({ url }: { url: string }) {
|
|||||||
});
|
});
|
||||||
}, [scene, opacity, renderMode, checklist, wireframeColor, wireframeThickness, edgeThresholdAngle]);
|
}, [scene, opacity, renderMode, checklist, wireframeColor, wireframeThickness, edgeThresholdAngle]);
|
||||||
|
|
||||||
|
const rotXRad = (fineTuning.rotX * Math.PI) / 180;
|
||||||
const rotYRad = (fineTuning.rotY * Math.PI) / 180;
|
const rotYRad = (fineTuning.rotY * Math.PI) / 180;
|
||||||
|
const rotZRad = (fineTuning.rotZ * Math.PI) / 180;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group
|
<group
|
||||||
@@ -118,7 +120,7 @@ function XRModel({ url }: { url: string }) {
|
|||||||
-modelInfo.center.y + fineTuning.posY,
|
-modelInfo.center.y + fineTuning.posY,
|
||||||
-modelInfo.center.z + fineTuning.posZ,
|
-modelInfo.center.z + fineTuning.posZ,
|
||||||
]}
|
]}
|
||||||
rotation={[0, rotYRad, 0]}
|
rotation={[rotXRad, rotYRad, rotZRad]}
|
||||||
>
|
>
|
||||||
<primitive object={scene} />
|
<primitive object={scene} />
|
||||||
</group>
|
</group>
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ interface FineTuning {
|
|||||||
posX: number;
|
posX: number;
|
||||||
posY: number;
|
posY: number;
|
||||||
posZ: number;
|
posZ: number;
|
||||||
|
rotX: number;
|
||||||
rotY: number;
|
rotY: number;
|
||||||
|
rotZ: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
type AnchorMode = 'tracking' | 'manual' | 'fine-tuning';
|
type AnchorMode = 'tracking' | 'manual' | 'fine-tuning';
|
||||||
@@ -117,7 +119,7 @@ interface ModelStore {
|
|||||||
clearScreenshots: () => void;
|
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) => ({
|
export const useModelStore = create<ModelStore>((set) => ({
|
||||||
model: null,
|
model: null,
|
||||||
|
|||||||
Reference in New Issue
Block a user