Add grid and wireframe controls
Implemented UI and state updates to toggle grid visibility and customize wireframe: - Added Grid toggle button in ViewerControls - Added wireframe color and thickness controls via a popover - Integrated grid visibility and wireframe styling into ModelViewer rendering - Updated store to manage showGrid, wireframeColor, and wireframeThickness - Wiring ensures grid and wireframe reflect in real-time during inspection X-Lovable-Edit-ID: edt-65af62ff-0e67-4129-96b6-0e281bfcdf91
This commit is contained in:
@@ -1,12 +1,30 @@
|
|||||||
import { Eye, Grid3X3, Ruler, Trash2, Camera } from 'lucide-react';
|
import { Eye, Grid3X3, Ruler, Trash2, Camera, LayoutGrid, Palette } from 'lucide-react';
|
||||||
import { Slider } from '@/components/ui/slider';
|
import { Slider } from '@/components/ui/slider';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||||
import { useModelStore } from '@/stores/useModelStore';
|
import { useModelStore } from '@/stores/useModelStore';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { toast } from 'sonner';
|
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() {
|
export function ViewerControls() {
|
||||||
const { opacity, setOpacity, renderMode, setRenderMode, measureMode, setMeasureMode, measurements, clearMeasurements, measurePoints, addScreenshot } = useModelStore();
|
const {
|
||||||
|
opacity, setOpacity, renderMode, setRenderMode,
|
||||||
|
measureMode, setMeasureMode, measurements, clearMeasurements,
|
||||||
|
measurePoints, addScreenshot,
|
||||||
|
showGrid, setShowGrid,
|
||||||
|
wireframeColor, setWireframeColor,
|
||||||
|
wireframeThickness, setWireframeThickness,
|
||||||
|
} = useModelStore();
|
||||||
|
|
||||||
const handleScreenshot = useCallback(() => {
|
const handleScreenshot = useCallback(() => {
|
||||||
const canvas = document.querySelector('canvas');
|
const canvas = document.querySelector('canvas');
|
||||||
@@ -20,7 +38,7 @@ export function ViewerControls() {
|
|||||||
}, [addScreenshot]);
|
}, [addScreenshot]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute bottom-4 left-4 z-10 flex items-end gap-3">
|
<div className="absolute bottom-4 left-4 z-10 flex items-end gap-3 flex-wrap">
|
||||||
{/* Opacity slider */}
|
{/* Opacity slider */}
|
||||||
<div className="rounded-lg border bg-card/90 backdrop-blur-sm p-3 shadow-lg w-56">
|
<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 justify-between mb-2">
|
||||||
@@ -39,6 +57,17 @@ export function ViewerControls() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
{/* Render mode toggle */}
|
{/* Render mode toggle */}
|
||||||
<Button
|
<Button
|
||||||
variant={renderMode === 'wireframe' ? 'default' : 'outline'}
|
variant={renderMode === 'wireframe' ? 'default' : 'outline'}
|
||||||
@@ -52,6 +81,47 @@ export function ViewerControls() {
|
|||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
{/* Wireframe settings popover */}
|
||||||
|
{renderMode === 'wireframe' && (
|
||||||
|
<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 do Wireframe</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>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Measure tool */}
|
{/* Measure tool */}
|
||||||
<div className="flex items-end gap-1.5">
|
<div className="flex items-end gap-1.5">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ function GLBModel({ url }: { url: string }) {
|
|||||||
const ref = useRef<THREE.Group>(null);
|
const ref = useRef<THREE.Group>(null);
|
||||||
const opacity = useModelStore((s) => s.opacity);
|
const opacity = useModelStore((s) => s.opacity);
|
||||||
const renderMode = useModelStore((s) => s.renderMode);
|
const renderMode = useModelStore((s) => s.renderMode);
|
||||||
|
const wireframeColor = useModelStore((s) => s.wireframeColor);
|
||||||
|
const wireframeThickness = useModelStore((s) => s.wireframeThickness);
|
||||||
const checklist = useModelStore((s) => s.checklist);
|
const checklist = useModelStore((s) => s.checklist);
|
||||||
const fineTuning = useModelStore((s) => s.fineTuning);
|
const fineTuning = useModelStore((s) => s.fineTuning);
|
||||||
|
|
||||||
@@ -54,8 +56,14 @@ function GLBModel({ url }: { url: string }) {
|
|||||||
mat.transparent = opacity < 1;
|
mat.transparent = opacity < 1;
|
||||||
mat.opacity = opacity;
|
mat.opacity = opacity;
|
||||||
mat.wireframe = renderMode === 'wireframe';
|
mat.wireframe = renderMode === 'wireframe';
|
||||||
|
if (renderMode === 'wireframe') {
|
||||||
|
mat.wireframeLinewidth = wireframeThickness;
|
||||||
|
}
|
||||||
mat.needsUpdate = true;
|
mat.needsUpdate = true;
|
||||||
if (hasRejected) {
|
mat.needsUpdate = true;
|
||||||
|
if (renderMode === 'wireframe') {
|
||||||
|
mat.color.set(wireframeColor);
|
||||||
|
} else if (hasRejected) {
|
||||||
mat.color.setHSL(0, 0.7, 0.5);
|
mat.color.setHSL(0, 0.7, 0.5);
|
||||||
} else if (allApproved) {
|
} else if (allApproved) {
|
||||||
mat.color.setHSL(0.38, 0.7, 0.45);
|
mat.color.setHSL(0.38, 0.7, 0.45);
|
||||||
@@ -66,7 +74,7 @@ function GLBModel({ url }: { url: string }) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [scene, opacity, renderMode, checklist]);
|
}, [scene, opacity, renderMode, checklist, wireframeColor, wireframeThickness]);
|
||||||
|
|
||||||
const rotYRad = (fineTuning.rotY * Math.PI) / 180;
|
const rotYRad = (fineTuning.rotY * Math.PI) / 180;
|
||||||
|
|
||||||
@@ -188,6 +196,24 @@ function MeasureClickHandler() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function GridLayer() {
|
||||||
|
const showGrid = useModelStore((s) => s.showGrid);
|
||||||
|
if (!showGrid) return null;
|
||||||
|
return (
|
||||||
|
<Grid
|
||||||
|
infiniteGrid
|
||||||
|
cellSize={0.01}
|
||||||
|
sectionSize={0.1}
|
||||||
|
cellThickness={0.5}
|
||||||
|
sectionThickness={1}
|
||||||
|
cellColor="#334155"
|
||||||
|
sectionColor="#475569"
|
||||||
|
fadeDistance={5}
|
||||||
|
fadeStrength={1}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function ModelViewerCanvas({ url }: ModelViewerProps) {
|
export function ModelViewerCanvas({ url }: ModelViewerProps) {
|
||||||
return (
|
return (
|
||||||
<Canvas
|
<Canvas
|
||||||
@@ -212,17 +238,7 @@ export function ModelViewerCanvas({ url }: ModelViewerProps) {
|
|||||||
<MeasurementOverlay />
|
<MeasurementOverlay />
|
||||||
<MeasureClickHandler />
|
<MeasureClickHandler />
|
||||||
|
|
||||||
<Grid
|
<GridLayer />
|
||||||
infiniteGrid
|
|
||||||
cellSize={0.01}
|
|
||||||
sectionSize={0.1}
|
|
||||||
cellThickness={0.5}
|
|
||||||
sectionThickness={1}
|
|
||||||
cellColor="#334155"
|
|
||||||
sectionColor="#475569"
|
|
||||||
fadeDistance={5}
|
|
||||||
fadeStrength={1}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<OrbitControls
|
<OrbitControls
|
||||||
makeDefault
|
makeDefault
|
||||||
|
|||||||
@@ -68,6 +68,15 @@ interface ModelStore {
|
|||||||
renderMode: RenderMode;
|
renderMode: RenderMode;
|
||||||
setRenderMode: (mode: RenderMode) => void;
|
setRenderMode: (mode: RenderMode) => void;
|
||||||
|
|
||||||
|
showGrid: boolean;
|
||||||
|
setShowGrid: (show: boolean) => void;
|
||||||
|
|
||||||
|
wireframeColor: string;
|
||||||
|
setWireframeColor: (color: string) => void;
|
||||||
|
|
||||||
|
wireframeThickness: number;
|
||||||
|
setWireframeThickness: (t: number) => void;
|
||||||
|
|
||||||
checklist: ChecklistItem[];
|
checklist: ChecklistItem[];
|
||||||
setChecklistItemStatus: (id: string, status: 'approved' | 'rejected') => void;
|
setChecklistItemStatus: (id: string, status: 'approved' | 'rejected') => void;
|
||||||
setChecklistItemNote: (id: string, note: string) => void;
|
setChecklistItemNote: (id: string, note: string) => void;
|
||||||
@@ -113,6 +122,15 @@ export const useModelStore = create<ModelStore>((set) => ({
|
|||||||
renderMode: 'solid',
|
renderMode: 'solid',
|
||||||
setRenderMode: (renderMode) => set({ renderMode }),
|
setRenderMode: (renderMode) => set({ renderMode }),
|
||||||
|
|
||||||
|
showGrid: true,
|
||||||
|
setShowGrid: (showGrid) => set({ showGrid }),
|
||||||
|
|
||||||
|
wireframeColor: '#8899aa',
|
||||||
|
setWireframeColor: (wireframeColor) => set({ wireframeColor }),
|
||||||
|
|
||||||
|
wireframeThickness: 1,
|
||||||
|
setWireframeThickness: (wireframeThickness) => set({ wireframeThickness }),
|
||||||
|
|
||||||
checklist: defaultChecklist.map(i => ({ ...i })),
|
checklist: defaultChecklist.map(i => ({ ...i })),
|
||||||
setChecklistItemStatus: (id, status) => set((state) => ({
|
setChecklistItemStatus: (id, status) => set((state) => ({
|
||||||
checklist: state.checklist.map(item =>
|
checklist: state.checklist.map(item =>
|
||||||
|
|||||||
Reference in New Issue
Block a user