Files
SteelXR/src/components/ViewerControls.tsx
T
gpt-engineer-app[bot] aca89d6b28 Changes
Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
2026-05-21 12:56:53 +00:00

210 lines
7.5 KiB
TypeScript

import { Eye, Grid3X3, Ruler, Trash2, Camera, LayoutGrid, Palette, Box, Move3D, Undo2 } from 'lucide-react';
import { Slider } from '@/components/ui/slider';
import { Button } from '@/components/ui/button';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { useModelStore } from '@/stores/useModelStore';
import { ScaleSelector } from '@/components/ScaleSelector';
import { useCallback } from 'react';
import { toast } from 'sonner';
const WIRE_COLORS = [
{ label: 'Cinza', value: '#8899aa' },
{ label: 'Vermelho', value: '#ef4444' },
{ label: 'Verde', value: '#22c55e' },
{ label: 'Azul', value: '#3b82f6' },
{ label: 'Amarelo', value: '#eab308' },
{ label: 'Laranja', value: '#f97316' },
{ label: 'Branco', value: '#e2e8f0' },
];
export function ViewerControls() {
const {
opacity, setOpacity, renderMode, setRenderMode,
measureMode, setMeasureMode, measurements, clearMeasurements, undoLastMeasurement,
measurePoints, addScreenshot,
showGrid, setShowGrid,
wireframeColor, setWireframeColor,
wireframeThickness, setWireframeThickness,
edgeThresholdAngle, setEdgeThresholdAngle,
positionMode, setPositionMode,
activeModelId, models,
} = useModelStore();
const activeModel = models.find(m => m.id === activeModelId);
const handleScreenshot = useCallback(() => {
const canvas = document.querySelector('canvas');
if (!canvas) {
toast.error('Canvas não encontrado');
return;
}
const dataUrl = canvas.toDataURL('image/png');
addScreenshot(dataUrl);
toast.success('Screenshot capturado!');
}, [addScreenshot]);
return (
<div className="absolute bottom-4 left-4 z-10 flex items-end gap-3 flex-wrap">
{/* Opacity slider */}
<div className="rounded-lg border bg-card/90 backdrop-blur-sm p-3 shadow-lg w-56">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<Eye className="h-3.5 w-3.5 text-primary" />
<span className="font-mono text-xs text-muted-foreground">Opacidade</span>
</div>
<span className="font-mono text-xs text-foreground">{Math.round(opacity * 100)}%</span>
</div>
<Slider
value={[opacity * 100]}
min={0}
max={100}
step={1}
onValueChange={([v]) => setOpacity(v / 100)}
/>
</div>
{/* Grid toggle */}
<Button
variant={showGrid ? 'default' : 'outline'}
size="sm"
className="gap-2 h-9"
onClick={() => setShowGrid(!showGrid)}
>
<LayoutGrid className="h-3.5 w-3.5" />
<span className="font-mono text-xs">Grid</span>
</Button>
{/* Scale presets */}
<ScaleSelector />
<Button
variant={renderMode !== 'solid' ? 'default' : 'outline'}
size="sm"
className="gap-2 h-9"
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" />}
<span className="font-mono text-xs">
{renderMode === 'solid' ? 'Sólido' : renderMode === 'wireframe' ? 'Wireframe' : 'Bordas'}
</span>
</Button>
{/* Wireframe/Edges settings popover */}
{renderMode !== 'solid' && (
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" size="sm" className="gap-2 h-9">
<Palette className="h-3.5 w-3.5" />
<div className="h-3 w-3 rounded-full border border-muted-foreground/30" style={{ backgroundColor: wireframeColor }} />
</Button>
</PopoverTrigger>
<PopoverContent className="w-56 space-y-3" side="top" align="start">
<div>
<span className="font-mono text-xs text-muted-foreground">Cor das Linhas</span>
<div className="mt-2 flex flex-wrap gap-2">
{WIRE_COLORS.map((c) => (
<button
key={c.value}
className={`h-6 w-6 rounded-full border-2 transition-all ${wireframeColor === c.value ? 'border-primary scale-110' : 'border-muted-foreground/30'}`}
style={{ backgroundColor: c.value }}
onClick={() => setWireframeColor(c.value)}
title={c.label}
/>
))}
</div>
</div>
<div>
<div className="flex items-center justify-between mb-1">
<span className="font-mono text-xs text-muted-foreground">Espessura</span>
<span className="font-mono text-xs text-foreground">{wireframeThickness.toFixed(1)}</span>
</div>
<Slider
value={[wireframeThickness]}
min={0.5}
max={5}
step={0.5}
onValueChange={([v]) => setWireframeThickness(v)}
/>
</div>
{renderMode === 'edges' && (
<div>
<div className="flex items-center justify-between mb-1">
<span className="font-mono text-xs text-muted-foreground">Ângulo Limite</span>
<span className="font-mono text-xs text-foreground">{edgeThresholdAngle}°</span>
</div>
<Slider
value={[edgeThresholdAngle]}
min={1}
max={45}
step={1}
onValueChange={([v]) => setEdgeThresholdAngle(v)}
/>
</div>
)}
</PopoverContent>
</Popover>
)}
{/* Posicionar (manual mouse placement) */}
<Button
variant={positionMode ? 'default' : 'outline'}
size="sm"
className="gap-2 h-9"
onClick={() => setPositionMode(!positionMode)}
disabled={!activeModelId}
title="Esquerdo: mover · Shift+Esq: profundidade · Meio (roda): girar no próprio eixo · Direito: rotação livre · Shift+Dir: rotZ"
>
<Move3D className="h-3.5 w-3.5" />
<span className="font-mono text-xs">
{positionMode
? `Posicionando${activeModel ? ` · ${activeModel.fileName.slice(0, 14)}` : ''}`
: 'Posicionar'}
</span>
</Button>
{/* Measure tool */}
<div className="flex items-end gap-1.5">
<Button
variant={measureMode ? 'default' : 'outline'}
size="sm"
className="gap-2 h-9"
onClick={() => setMeasureMode(!measureMode)}
>
<Ruler className="h-3.5 w-3.5" />
<span className="font-mono text-xs">
{measureMode
? measurePoints.length === 0
? 'Ponto A…'
: 'Ponto B…'
: 'Medir'}
</span>
</Button>
{measurements.length > 0 && (
<Button
variant="ghost"
size="icon"
className="h-9 w-9"
onClick={clearMeasurements}
title="Limpar medições"
>
<Trash2 className="h-3.5 w-3.5 text-destructive" />
</Button>
)}
</div>
{/* Screenshot */}
<Button
variant="outline"
size="sm"
className="gap-2 h-9"
onClick={handleScreenshot}
>
<Camera className="h-3.5 w-3.5" />
<span className="font-mono text-xs">Capturar</span>
</Button>
</div>
);
}