Changes
This commit is contained in:
+168
-118
@@ -2,23 +2,26 @@ 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,
|
||||||
} 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';
|
||||||
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
import { useModelStore } from '@/stores/useModelStore';
|
import { useModelStore } from '@/stores/useModelStore';
|
||||||
|
import { InspectionChecklist } from '@/components/InspectionChecklist';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
const POSITION_STEP = 0.001;
|
const POSITION_STEP = 0.001;
|
||||||
const ROTATION_STEP = 0.1;
|
const ROTATION_STEP = 0.1;
|
||||||
|
|
||||||
interface XRHudProps {
|
interface XRHudProps {
|
||||||
/** Whether joystick moves model freely (without grip) */
|
|
||||||
freeMove: boolean;
|
freeMove: boolean;
|
||||||
onToggleFreeMove: () => void;
|
onToggleFreeMove: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function XRHud({ freeMove, onToggleFreeMove }: XRHudProps) {
|
export function XRHud({ freeMove, onToggleFreeMove }: XRHudProps) {
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
const [checklistOpen, setChecklistOpen] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
opacity, setOpacity,
|
opacity, setOpacity,
|
||||||
@@ -28,8 +31,13 @@ export function XRHud({ freeMove, onToggleFreeMove }: XRHudProps) {
|
|||||||
measurements, clearMeasurements, measurePoints,
|
measurements, clearMeasurements, measurePoints,
|
||||||
fineTuning, setFineTuning, resetFineTuning,
|
fineTuning, setFineTuning, resetFineTuning,
|
||||||
addScreenshot,
|
addScreenshot,
|
||||||
|
checklist,
|
||||||
} = useModelStore();
|
} = useModelStore();
|
||||||
|
|
||||||
|
const approved = checklist.filter(i => i.status === 'approved').length;
|
||||||
|
const rejected = checklist.filter(i => i.status === 'rejected').length;
|
||||||
|
const total = checklist.length;
|
||||||
|
|
||||||
const handleScreenshot = () => {
|
const handleScreenshot = () => {
|
||||||
const canvas = document.querySelector('canvas');
|
const canvas = document.querySelector('canvas');
|
||||||
if (!canvas) return;
|
if (!canvas) return;
|
||||||
@@ -39,133 +47,175 @@ export function XRHud({ freeMove, onToggleFreeMove }: XRHudProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<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">
|
{/* ── Slide-out checklist panel ── */}
|
||||||
{/* Expanded panel */}
|
<div
|
||||||
{expanded && (
|
className={`absolute top-0 right-0 z-30 h-full w-80 max-w-[85vw] transition-transform duration-300 ease-in-out ${
|
||||||
<div className="rounded-xl border bg-card/90 backdrop-blur-md p-3 shadow-2xl space-y-3 animate-in slide-in-from-bottom-2 duration-200">
|
checklistOpen ? 'translate-x-0' : 'translate-x-full'
|
||||||
{/* Opacity */}
|
}`}
|
||||||
<div>
|
>
|
||||||
<div className="flex items-center justify-between mb-1.5">
|
<div className="h-full border-l bg-card/95 backdrop-blur-md shadow-2xl flex flex-col">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center justify-between border-b px-4 py-3">
|
||||||
<Eye className="h-3 w-3 text-primary" />
|
<div className="flex items-center gap-2">
|
||||||
<span className="font-mono text-[10px] text-muted-foreground">Opacidade</span>
|
<ClipboardCheck className="h-4 w-4 text-primary" />
|
||||||
</div>
|
<span className="font-mono text-xs font-semibold text-foreground">Inspeção</span>
|
||||||
<span className="font-mono text-[10px] text-foreground">{Math.round(opacity * 100)}%</span>
|
|
||||||
</div>
|
|
||||||
<Slider
|
|
||||||
value={[opacity * 100]}
|
|
||||||
min={0} max={100} step={5}
|
|
||||||
onValueChange={([v]) => setOpacity(v / 100)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
{/* Fine Tuning */}
|
{/* Progress badge */}
|
||||||
<div className="space-y-1.5">
|
<span className="font-mono text-[10px] text-muted-foreground">
|
||||||
<div className="flex items-center justify-between">
|
{approved + rejected}/{total}
|
||||||
<div className="flex items-center gap-1.5">
|
</span>
|
||||||
<Move className="h-3 w-3 text-primary" />
|
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => setChecklistOpen(false)}>
|
||||||
<span className="font-mono text-[10px] text-muted-foreground">Posição (mm)</span>
|
<X className="h-4 w-4" />
|
||||||
</div>
|
</Button>
|
||||||
<Button variant="ghost" size="sm" className="h-5 text-[9px] px-1.5 gap-0.5" onClick={resetFineTuning}>
|
|
||||||
<RefreshCw className="h-2.5 w-2.5" /> Reset
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-3 gap-1.5">
|
|
||||||
{(['posX', 'posY', 'posZ'] 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] - POSITION_STEP })}>−</Button>
|
|
||||||
<span className="font-mono text-[9px] text-foreground flex-1 text-center">
|
|
||||||
{(fineTuning[axis] * 1000).toFixed(1)}
|
|
||||||
</span>
|
|
||||||
<Button variant="ghost" size="icon" className="h-5 w-5 text-[10px]"
|
|
||||||
onClick={() => setFineTuning({ [axis]: fineTuning[axis] + POSITION_STEP })}>+</Button>
|
|
||||||
</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>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
<ScrollArea className="flex-1">
|
||||||
|
<div className="p-4">
|
||||||
|
<InspectionChecklist />
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Main toolbar */}
|
{/* ── Bottom HUD ── */}
|
||||||
<div className="flex items-center justify-between rounded-xl border bg-card/90 backdrop-blur-md p-2 shadow-2xl">
|
<div className="absolute bottom-0 left-0 right-0 z-20 pointer-events-none p-3">
|
||||||
<div className="flex items-center gap-1.5 flex-wrap">
|
<div className="pointer-events-auto mx-auto max-w-lg space-y-2">
|
||||||
{/* Expand/collapse */}
|
{/* Expanded panel */}
|
||||||
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => setExpanded(!expanded)}>
|
{expanded && (
|
||||||
{expanded ? <ChevronDown className="h-4 w-4" /> : <ChevronUp className="h-4 w-4" />}
|
<div className="rounded-xl border bg-card/90 backdrop-blur-md p-3 shadow-2xl space-y-3 animate-in slide-in-from-bottom-2 duration-200">
|
||||||
</Button>
|
{/* Opacity */}
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-1.5">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<Eye className="h-3 w-3 text-primary" />
|
||||||
|
<span className="font-mono text-[10px] text-muted-foreground">Opacidade</span>
|
||||||
|
</div>
|
||||||
|
<span className="font-mono text-[10px] text-foreground">{Math.round(opacity * 100)}%</span>
|
||||||
|
</div>
|
||||||
|
<Slider
|
||||||
|
value={[opacity * 100]}
|
||||||
|
min={0} max={100} step={5}
|
||||||
|
onValueChange={([v]) => setOpacity(v / 100)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Free move toggle */}
|
{/* Fine Tuning */}
|
||||||
<Button
|
<div className="space-y-1.5">
|
||||||
variant={freeMove ? 'default' : 'outline'}
|
<div className="flex items-center justify-between">
|
||||||
size="sm" className="h-8 gap-1.5 text-[10px] font-mono"
|
<div className="flex items-center gap-1.5">
|
||||||
onClick={onToggleFreeMove}
|
<Move className="h-3 w-3 text-primary" />
|
||||||
>
|
<span className="font-mono text-[10px] text-muted-foreground">Posição (mm)</span>
|
||||||
<Grip className="h-3.5 w-3.5" />
|
</div>
|
||||||
{freeMove ? 'Mover ON' : 'Mover'}
|
<Button variant="ghost" size="sm" className="h-5 text-[9px] px-1.5 gap-0.5" onClick={resetFineTuning}>
|
||||||
</Button>
|
<RefreshCw className="h-2.5 w-2.5" /> Reset
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-3 gap-1.5">
|
||||||
|
{(['posX', 'posY', 'posZ'] 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] - POSITION_STEP })}>−</Button>
|
||||||
|
<span className="font-mono text-[9px] text-foreground flex-1 text-center">
|
||||||
|
{(fineTuning[axis] * 1000).toFixed(1)}
|
||||||
|
</span>
|
||||||
|
<Button variant="ghost" size="icon" className="h-5 w-5 text-[10px]"
|
||||||
|
onClick={() => setFineTuning({ [axis]: fineTuning[axis] + POSITION_STEP })}>+</Button>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Grid */}
|
{/* Main toolbar */}
|
||||||
<Button
|
<div className="flex items-center justify-between rounded-xl border bg-card/90 backdrop-blur-md p-2 shadow-2xl">
|
||||||
variant={showGrid ? 'default' : 'outline'}
|
<div className="flex items-center gap-1.5 flex-wrap">
|
||||||
size="icon" className="h-8 w-8"
|
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => setExpanded(!expanded)}>
|
||||||
onClick={() => setShowGrid(!showGrid)}
|
{expanded ? <ChevronDown className="h-4 w-4" /> : <ChevronUp className="h-4 w-4" />}
|
||||||
title="Grid"
|
|
||||||
>
|
|
||||||
<LayoutGrid className="h-3.5 w-3.5" />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{/* Render mode */}
|
|
||||||
<Button
|
|
||||||
variant={renderMode !== 'solid' ? 'default' : 'outline'}
|
|
||||||
size="sm" className="h-8 gap-1.5 text-[10px] font-mono"
|
|
||||||
onClick={() => {
|
|
||||||
const next = renderMode === 'solid' ? 'wireframe' : renderMode === 'wireframe' ? 'edges' : 'solid';
|
|
||||||
setRenderMode(next);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{renderMode === 'edges' ? <Box className="h-3.5 w-3.5" /> : <Grid3X3 className="h-3.5 w-3.5" />}
|
|
||||||
{renderMode === 'solid' ? 'Sólido' : renderMode === 'wireframe' ? 'Wire' : 'Bordas'}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{/* Measure */}
|
|
||||||
<Button
|
|
||||||
variant={measureMode ? 'default' : 'outline'}
|
|
||||||
size="sm" className="h-8 gap-1.5 text-[10px] font-mono"
|
|
||||||
onClick={() => setMeasureMode(!measureMode)}
|
|
||||||
>
|
|
||||||
<Ruler className="h-3.5 w-3.5" />
|
|
||||||
{measureMode
|
|
||||||
? measurePoints.length === 0 ? 'Pt A…' : 'Pt B…'
|
|
||||||
: 'Medir'}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{measurements.length > 0 && (
|
|
||||||
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={clearMeasurements} title="Limpar medições">
|
|
||||||
<Trash2 className="h-3.5 w-3.5 text-destructive" />
|
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Screenshot */}
|
<Button
|
||||||
<Button variant="outline" size="icon" className="h-8 w-8" onClick={handleScreenshot} title="Screenshot">
|
variant={freeMove ? 'default' : 'outline'}
|
||||||
<Camera className="h-3.5 w-3.5" />
|
size="sm" className="h-8 gap-1.5 text-[10px] font-mono"
|
||||||
</Button>
|
onClick={onToggleFreeMove}
|
||||||
|
>
|
||||||
|
<Grip className="h-3.5 w-3.5" />
|
||||||
|
{freeMove ? 'Mover ON' : 'Mover'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant={showGrid ? 'default' : 'outline'}
|
||||||
|
size="icon" className="h-8 w-8"
|
||||||
|
onClick={() => setShowGrid(!showGrid)}
|
||||||
|
title="Grid"
|
||||||
|
>
|
||||||
|
<LayoutGrid className="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant={renderMode !== 'solid' ? 'default' : 'outline'}
|
||||||
|
size="sm" className="h-8 gap-1.5 text-[10px] font-mono"
|
||||||
|
onClick={() => {
|
||||||
|
const next = renderMode === 'solid' ? 'wireframe' : renderMode === 'wireframe' ? 'edges' : 'solid';
|
||||||
|
setRenderMode(next);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{renderMode === 'edges' ? <Box className="h-3.5 w-3.5" /> : <Grid3X3 className="h-3.5 w-3.5" />}
|
||||||
|
{renderMode === 'solid' ? 'Sólido' : renderMode === 'wireframe' ? 'Wire' : 'Bordas'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant={measureMode ? 'default' : 'outline'}
|
||||||
|
size="sm" className="h-8 gap-1.5 text-[10px] font-mono"
|
||||||
|
onClick={() => setMeasureMode(!measureMode)}
|
||||||
|
>
|
||||||
|
<Ruler className="h-3.5 w-3.5" />
|
||||||
|
{measureMode
|
||||||
|
? measurePoints.length === 0 ? 'Pt A…' : 'Pt B…'
|
||||||
|
: 'Medir'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{measurements.length > 0 && (
|
||||||
|
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={clearMeasurements} title="Limpar medições">
|
||||||
|
<Trash2 className="h-3.5 w-3.5 text-destructive" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button variant="outline" size="icon" className="h-8 w-8" onClick={handleScreenshot} title="Screenshot">
|
||||||
|
<Camera className="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Checklist toggle */}
|
||||||
|
<Button
|
||||||
|
variant={checklistOpen ? 'default' : 'outline'}
|
||||||
|
size="sm" className="h-8 gap-1.5 text-[10px] font-mono relative"
|
||||||
|
onClick={() => setChecklistOpen(!checklistOpen)}
|
||||||
|
>
|
||||||
|
<ClipboardCheck className="h-3.5 w-3.5" />
|
||||||
|
Inspeção
|
||||||
|
{(approved + rejected) > 0 && (
|
||||||
|
<span className={`ml-1 font-mono text-[9px] ${rejected > 0 ? 'text-destructive' : 'text-primary'}`}>
|
||||||
|
{approved + rejected}/{total}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user