This commit is contained in:
gpt-engineer-app[bot]
2026-02-28 00:15:34 +00:00
parent 1d38e0f474
commit 113f961ed4
+168 -118
View File
@@ -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> </>
); );
} }