Add side-by-side compare mode
Enable a comparison view between 3D model and real photo, including upload, toggle, and panel integration. Added ComparePanel component, integrated into Viewer with compareMode toggle, and extended store with compareMode/compareImage support; Viewer layout now splits when comparing. X-Lovable-Edit-ID: edt-5d8eb329-0a4c-4fdd-8480-0e4c83e2ec50
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
import { useRef } from 'react';
|
||||
import { ImagePlus, X, ZoomIn, ZoomOut, RotateCcw } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useModelStore } from '@/stores/useModelStore';
|
||||
import { useState } from 'react';
|
||||
|
||||
export function ComparePanel() {
|
||||
const { compareImage, setCompareImage } = useModelStore();
|
||||
const fileRef = useRef<HTMLInputElement>(null);
|
||||
const [zoom, setZoom] = useState(1);
|
||||
|
||||
const handleFile = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
const url = URL.createObjectURL(file);
|
||||
setCompareImage(url);
|
||||
setZoom(1);
|
||||
};
|
||||
|
||||
if (!compareImage) {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-4 bg-muted/30 p-6">
|
||||
<div className="rounded-xl border-2 border-dashed border-muted-foreground/30 p-8 text-center">
|
||||
<ImagePlus className="mx-auto mb-3 h-10 w-10 text-muted-foreground/50" />
|
||||
<p className="mb-1 font-mono text-sm font-semibold text-foreground">
|
||||
Foto Real da Peça
|
||||
</p>
|
||||
<p className="mb-4 font-mono text-xs text-muted-foreground">
|
||||
Carregue uma foto para comparar com o modelo 3D
|
||||
</p>
|
||||
<Button size="sm" className="gap-2" onClick={() => fileRef.current?.click()}>
|
||||
<ImagePlus className="h-3.5 w-3.5" />
|
||||
Selecionar Foto
|
||||
</Button>
|
||||
</div>
|
||||
<input
|
||||
ref={fileRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={handleFile}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full flex-col bg-muted/20">
|
||||
{/* Toolbar */}
|
||||
<div className="flex items-center justify-between border-b bg-card/90 px-3 py-2 backdrop-blur-sm">
|
||||
<span className="font-mono text-xs font-semibold text-muted-foreground uppercase tracking-widest">
|
||||
Foto Real
|
||||
</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
onClick={() => setZoom((z) => Math.max(0.25, z - 0.25))}
|
||||
title="Zoom out"
|
||||
>
|
||||
<ZoomOut className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<span className="font-mono text-xs text-muted-foreground w-10 text-center">
|
||||
{Math.round(zoom * 100)}%
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
onClick={() => setZoom((z) => Math.min(4, z + 0.25))}
|
||||
title="Zoom in"
|
||||
>
|
||||
<ZoomIn className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
onClick={() => setZoom(1)}
|
||||
title="Reset zoom"
|
||||
>
|
||||
<RotateCcw className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
onClick={() => fileRef.current?.click()}
|
||||
title="Trocar foto"
|
||||
>
|
||||
<ImagePlus className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
onClick={() => { setCompareImage(null); setZoom(1); }}
|
||||
title="Remover foto"
|
||||
>
|
||||
<X className="h-3.5 w-3.5 text-destructive" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Image */}
|
||||
<div className="flex-1 overflow-auto flex items-center justify-center">
|
||||
<img
|
||||
src={compareImage}
|
||||
alt="Foto real da peça"
|
||||
className="max-w-none transition-transform duration-150"
|
||||
style={{ transform: `scale(${zoom})` }}
|
||||
draggable={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<input
|
||||
ref={fileRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={handleFile}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+22
-3
@@ -1,5 +1,5 @@
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { ArrowLeft, Glasses, Box, Ruler, FileText } from 'lucide-react';
|
||||
import { ArrowLeft, Glasses, Box, Ruler, FileText, Columns2 } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useModelStore } from '@/stores/useModelStore';
|
||||
import { ModelViewerCanvas } from '@/components/three/ModelViewer';
|
||||
@@ -8,6 +8,7 @@ import { InspectionChecklist } from '@/components/InspectionChecklist';
|
||||
import { FineTuningControls } from '@/components/FineTuningControls';
|
||||
import { MeasurementsList } from '@/components/MeasurementsList';
|
||||
import { ScreenshotGallery } from '@/components/ScreenshotGallery';
|
||||
import { ComparePanel } from '@/components/ComparePanel';
|
||||
import { useEffect } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
@@ -15,7 +16,7 @@ import { Separator } from '@/components/ui/separator';
|
||||
|
||||
const Viewer = () => {
|
||||
const navigate = useNavigate();
|
||||
const { model, xrSupported } = useModelStore();
|
||||
const { model, xrSupported, compareMode, setCompareMode } = useModelStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (!model) {
|
||||
@@ -49,6 +50,16 @@ const Viewer = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant={compareMode ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
className="gap-2"
|
||||
onClick={() => setCompareMode(!compareMode)}
|
||||
>
|
||||
<Columns2 className="h-4 w-4" />
|
||||
Comparar
|
||||
</Button>
|
||||
<Button
|
||||
className="gap-2 glow-primary"
|
||||
disabled={!xrSupported}
|
||||
@@ -57,15 +68,23 @@ const Viewer = () => {
|
||||
<Glasses className="h-4 w-4" />
|
||||
Entrar em Modo XR
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
{/* 3D Canvas with floating controls */}
|
||||
<div className="relative flex-1">
|
||||
<div className={`relative ${compareMode ? 'w-1/2' : 'flex-1'}`}>
|
||||
<ModelViewerCanvas url={model.url} />
|
||||
<ViewerControls />
|
||||
</div>
|
||||
|
||||
{/* Compare panel */}
|
||||
{compareMode && (
|
||||
<div className="w-1/2 border-l">
|
||||
<ComparePanel />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Side panel */}
|
||||
<aside className="w-80 shrink-0 border-l bg-card">
|
||||
<ScrollArea className="h-full">
|
||||
|
||||
@@ -91,6 +91,11 @@ interface ModelStore {
|
||||
clearMeasurements: () => void;
|
||||
removeMeasurement: (id: string) => void;
|
||||
|
||||
compareMode: boolean;
|
||||
setCompareMode: (on: boolean) => void;
|
||||
compareImage: string | null;
|
||||
setCompareImage: (url: string | null) => void;
|
||||
|
||||
screenshots: string[];
|
||||
addScreenshot: (dataUrl: string) => void;
|
||||
removeScreenshot: (index: number) => void;
|
||||
@@ -176,6 +181,11 @@ export const useModelStore = create<ModelStore>((set) => ({
|
||||
measurements: state.measurements.filter(m => m.id !== id),
|
||||
})),
|
||||
|
||||
compareMode: false,
|
||||
setCompareMode: (compareMode) => set({ compareMode }),
|
||||
compareImage: null,
|
||||
setCompareImage: (compareImage) => set({ compareImage }),
|
||||
|
||||
screenshots: [],
|
||||
addScreenshot: (dataUrl) => set((state) => ({ screenshots: [...state.screenshots, dataUrl] })),
|
||||
removeScreenshot: (index) => set((state) => ({
|
||||
|
||||
Reference in New Issue
Block a user