feat(viewer3d): clean EdgesGeometry wireframe, SteelXR 3D ViewCube, dark mode ultra-fine grid, and simplified sidebar controls

This commit is contained in:
2026-09-21 01:31:25 +00:00
parent d003309481
commit b43d5314fb
3 changed files with 236 additions and 125 deletions
+77 -14
View File
@@ -144,19 +144,55 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
scene.add(hemiLight); scene.add(hemiLight);
// 6. Floor Grid (Cores refinadas e ultrafinas para modo claro e escuro) // 6. Floor Grid (Cores refinadas e ultrafinas para modo claro e escuro)
const updateGridTheme = (gridHelper: THREE.GridHelper) => {
const isDark = document.documentElement.classList.contains('dark');
const centerColor = isDark ? new THREE.Color(0x334155) : new THREE.Color(0x94a3b8);
const gridColor = isDark ? new THREE.Color(0x0f172a) : new THREE.Color(0xe2e8f0);
const colors = (gridHelper.geometry.attributes.color as THREE.BufferAttribute);
if (colors) {
const colorArray = colors.array as Float32Array;
for (let i = 0; i < colorArray.length; i += 6) {
// Atualiza as cores dos vértices da grade dinamicamente
const isCenter = i === Math.floor(colorArray.length / 2);
const c = isCenter ? centerColor : gridColor;
colorArray[i] = c.r;
colorArray[i + 1] = c.g;
colorArray[i + 2] = c.b;
colorArray[i + 3] = c.r;
colorArray[i + 4] = c.g;
colorArray[i + 5] = c.b;
}
colors.needsUpdate = true;
}
if (gridHelper.material && (gridHelper.material as THREE.Material)) {
(gridHelper.material as THREE.Material).transparent = true;
(gridHelper.material as THREE.Material).opacity = isDark ? 0.35 : 0.45;
}
};
const isDarkMode = document.documentElement.classList.contains('dark'); const isDarkMode = document.documentElement.classList.contains('dark');
const centerLineColor = isDarkMode ? 0x06b6d4 : 0x94a3b8; const centerLineColor = isDarkMode ? 0x334155 : 0x94a3b8;
const gridLineColor = isDarkMode ? 0x334155 : 0xe2e8f0; const gridLineColor = isDarkMode ? 0x0f172a : 0xe2e8f0;
const grid = new THREE.GridHelper(300, 150, centerLineColor, gridLineColor); const grid = new THREE.GridHelper(300, 150, centerLineColor, gridLineColor);
grid.position.y = 0; grid.position.y = 0;
if (grid.material && (grid.material as THREE.Material)) { if (grid.material && (grid.material as THREE.Material)) {
(grid.material as THREE.Material).transparent = true; (grid.material as THREE.Material).transparent = true;
(grid.material as THREE.Material).opacity = isDarkMode ? 0.6 : 0.5; (grid.material as THREE.Material).opacity = isDarkMode ? 0.35 : 0.45;
} }
scene.add(grid); scene.add(grid);
gridHelperRef.current = grid; gridHelperRef.current = grid;
// Observador para atualizar cores do grid automaticamente quando o tema mudar
const themeObserver = new MutationObserver(() => {
if (gridHelperRef.current) {
updateGridTheme(gridHelperRef.current);
}
});
themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
// 7. Render Loop // 7. Render Loop
let animationFrameId: number; let animationFrameId: number;
const animate = () => { const animate = () => {
@@ -191,6 +227,7 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
return () => { return () => {
cancelAnimationFrame(animationFrameId); cancelAnimationFrame(animationFrameId);
window.removeEventListener('resize', handleResize); window.removeEventListener('resize', handleResize);
themeObserver.disconnect();
renderer.dispose(); renderer.dispose();
}; };
}, []); }, []);
@@ -366,17 +403,49 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
}); });
} }
// Apply Opacity & Wireframe // Apply Opacity & Clean Architectural Wireframe (EdgesGeometry - SteelXR Style)
if (isWireframe) {
// Cria arestas de contorno se ainda não existirem
if (!mesh.userData.edgesLine) {
const edgesGeo = new THREE.EdgesGeometry(mesh.geometry, 24);
const lineMat = new THREE.LineBasicMaterial({
color: 0x00ffff,
transparent: true,
opacity: 0.95,
});
const edgesLine = new THREE.LineSegments(edgesGeo, lineMat);
mesh.userData.edgesLine = edgesLine;
mesh.add(edgesLine);
}
mesh.userData.edgesLine.visible = true;
// Oculta a malha sólida
if (Array.isArray(mesh.material)) { if (Array.isArray(mesh.material)) {
mesh.material.forEach((m) => { mesh.material.forEach((m) => {
m.transparent = opacity < 100; m.visible = false;
m.opacity = opacity / 100;
if ('wireframe' in m) (m as any).wireframe = isWireframe;
}); });
} else if (mesh.material) { } else if (mesh.material) {
mesh.material.visible = false;
}
} else {
// Modo Sólido: desativa as linhas de contorno e exibe a malha sólida
if (mesh.userData.edgesLine) {
mesh.userData.edgesLine.visible = false;
}
if (Array.isArray(mesh.material)) {
mesh.material.forEach((m) => {
m.visible = true;
m.transparent = opacity < 100;
m.opacity = opacity / 100;
if ('wireframe' in m) (m as any).wireframe = false;
});
} else if (mesh.material) {
mesh.material.visible = true;
mesh.material.transparent = opacity < 100; mesh.material.transparent = opacity < 100;
mesh.material.opacity = opacity / 100; mesh.material.opacity = opacity / 100;
if ('wireframe' in mesh.material) (mesh.material as any).wireframe = isWireframe; if ('wireframe' in mesh.material) (mesh.material as any).wireframe = false;
}
} }
} }
}); });
@@ -566,16 +635,10 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
onToggleCamera={handleToggleCamera} onToggleCamera={handleToggleCamera}
isWireframe={isWireframe} isWireframe={isWireframe}
onToggleWireframe={() => setIsWireframe(!isWireframe)} onToggleWireframe={() => setIsWireframe(!isWireframe)}
navMode={navMode}
onNavModeChange={setNavMode}
colorMode={colorMode} colorMode={colorMode}
onColorModeChange={setColorMode} onColorModeChange={setColorMode}
onFitView={() => fitModelToView()} onFitView={() => fitModelToView()}
onToggleFullscreen={handleToggleFullscreen} onToggleFullscreen={handleToggleFullscreen}
onToggleSectionPlanes={() => setHasSectionPlanes(!hasSectionPlanes)}
onToggleMeasure={() => setIsMeasuring(!isMeasuring)}
hasSectionPlanes={hasSectionPlanes}
isMeasuring={isMeasuring}
/> />
{/* Top Right: Interactive ViewCube */} {/* Top Right: Interactive ViewCube */}
+148 -20
View File
@@ -1,57 +1,185 @@
import React from 'react'; import React, { useState } from 'react';
import { RotateCcw } from 'lucide-react';
interface ViewCubeProps { interface ViewCubeProps {
onSelectView: (view: 'top' | 'bottom' | 'front' | 'back' | 'left' | 'right' | 'iso') => void; onSelectView: (view: 'top' | 'bottom' | 'front' | 'back' | 'left' | 'right' | 'iso') => void;
} }
export const ViewCube: React.FC<ViewCubeProps> = ({ onSelectView }) => { export const ViewCube: React.FC<ViewCubeProps> = ({ onSelectView }) => {
const [hoveredFace, setHoveredFace] = useState<string | null>(null);
return ( return (
<div className="relative w-28 h-28 select-none"> <div className="relative select-none flex flex-col items-center">
<div className="w-full h-full flex items-center justify-center p-2 rounded-xl bg-slate-900/80 backdrop-blur-md border border-cyan-500/30 shadow-2xl shadow-cyan-950/40"> {/* Container Principal translúcido idêntico ao SteelXR */}
<div className="relative w-20 h-20 perspective-[600px] flex items-center justify-center"> <div className="relative w-32 h-32 p-2 rounded-2xl bg-slate-950/85 backdrop-blur-xl border border-cyan-500/40 shadow-2xl shadow-cyan-950/60 flex items-center justify-center overflow-hidden">
{/* Central Cube Simulation with Interactive Buttons */} {/* Glow de Fundo */}
<div className="grid grid-cols-3 grid-rows-3 gap-1 w-full h-full text-[10px] font-bold tracking-wider"> <div className="absolute inset-0 bg-gradient-to-tr from-cyan-500/10 via-transparent to-blue-500/10 pointer-events-none" />
{/* Viewport 3D Isométrica do Cubo */}
<div
className="relative w-20 h-20"
style={{
perspective: '600px',
perspectiveOrigin: '50% 50%',
}}
>
{/* Cubo 3D Rotacionado em Vista Isométrica Natural */}
<div
className="w-full h-full relative transition-transform duration-300 ease-out"
style={{
transformStyle: 'preserve-3d',
transform: 'rotateX(-28deg) rotateY(42deg)',
}}
>
{/* FACE TOPO */}
<button <button
onClick={() => onSelectView('top')} onClick={() => onSelectView('top')}
className="col-span-3 py-1 bg-cyan-950/70 hover:bg-cyan-500 hover:text-slate-950 text-cyan-300 rounded border border-cyan-500/40 transition-all flex items-center justify-center shadow" onMouseEnter={() => setHoveredFace('TOPO')}
onMouseLeave={() => setHoveredFace(null)}
className="absolute inset-0 flex items-center justify-center font-extrabold text-xs tracking-wider transition-all duration-200 cursor-pointer"
style={{
transform: 'rotateX(90deg) translateZ(40px)',
backgroundColor: hoveredFace === 'TOPO' ? '#0891b2' : '#0f172a',
color: hoveredFace === 'TOPO' ? '#ffffff' : '#a5f3fc',
border: '2px solid #22d3ee',
boxShadow:
hoveredFace === 'TOPO'
? 'inset 0 0 15px rgba(34, 211, 238, 0.9), 0 0 15px #06b6d4'
: 'inset 0 0 10px rgba(34, 211, 238, 0.35)',
textShadow: '0 0 8px #22d3ee',
}}
title="Vista Superior (Planta)" title="Vista Superior (Planta)"
> >
TOPO TOPO
</button> </button>
<button
onClick={() => onSelectView('left')} {/* FACE FRENTE */}
className="py-2 bg-cyan-950/70 hover:bg-cyan-500 hover:text-slate-950 text-cyan-300 rounded border border-cyan-500/40 transition-all flex items-center justify-center shadow"
title="Vista Esquerda"
>
ESQ
</button>
<button <button
onClick={() => onSelectView('front')} onClick={() => onSelectView('front')}
className="py-2 bg-cyan-600/80 hover:bg-cyan-400 hover:text-slate-950 text-white rounded border border-cyan-400 transition-all flex items-center justify-center shadow font-extrabold" onMouseEnter={() => setHoveredFace('FRENTE')}
onMouseLeave={() => setHoveredFace(null)}
className="absolute inset-0 flex items-center justify-center font-extrabold text-xs tracking-wider transition-all duration-200 cursor-pointer"
style={{
transform: 'translateZ(40px)',
backgroundColor: hoveredFace === 'FRENTE' ? '#0891b2' : '#0f172a',
color: hoveredFace === 'FRENTE' ? '#ffffff' : '#a5f3fc',
border: '2px solid #22d3ee',
boxShadow:
hoveredFace === 'FRENTE'
? 'inset 0 0 15px rgba(34, 211, 238, 0.9), 0 0 15px #06b6d4'
: 'inset 0 0 10px rgba(34, 211, 238, 0.35)',
textShadow: '0 0 8px #22d3ee',
}}
title="Vista Frontal" title="Vista Frontal"
> >
FRENTE FRENTE
</button> </button>
{/* FACE DIREITA (DIR) */}
<button <button
onClick={() => onSelectView('right')} onClick={() => onSelectView('right')}
className="py-2 bg-cyan-950/70 hover:bg-cyan-500 hover:text-slate-950 text-cyan-300 rounded border border-cyan-500/40 transition-all flex items-center justify-center shadow" onMouseEnter={() => setHoveredFace('DIR')}
onMouseLeave={() => setHoveredFace(null)}
className="absolute inset-0 flex items-center justify-center font-extrabold text-xs tracking-wider transition-all duration-200 cursor-pointer"
style={{
transform: 'rotateY(90deg) translateZ(40px)',
backgroundColor: hoveredFace === 'DIR' ? '#0891b2' : '#0f172a',
color: hoveredFace === 'DIR' ? '#ffffff' : '#a5f3fc',
border: '2px solid #22d3ee',
boxShadow:
hoveredFace === 'DIR'
? 'inset 0 0 15px rgba(34, 211, 238, 0.9), 0 0 15px #06b6d4'
: 'inset 0 0 10px rgba(34, 211, 238, 0.35)',
textShadow: '0 0 8px #22d3ee',
}}
title="Vista Direita" title="Vista Direita"
> >
DIR DIR
</button> </button>
{/* FACE ESQUERDA (ESQ) */}
<button
onClick={() => onSelectView('left')}
onMouseEnter={() => setHoveredFace('ESQ')}
onMouseLeave={() => setHoveredFace(null)}
className="absolute inset-0 flex items-center justify-center font-extrabold text-xs tracking-wider transition-all duration-200 cursor-pointer"
style={{
transform: 'rotateY(-90deg) translateZ(40px)',
backgroundColor: hoveredFace === 'ESQ' ? '#0891b2' : '#0f172a',
color: hoveredFace === 'ESQ' ? '#ffffff' : '#a5f3fc',
border: '2px solid #22d3ee',
boxShadow:
hoveredFace === 'ESQ'
? 'inset 0 0 15px rgba(34, 211, 238, 0.9), 0 0 15px #06b6d4'
: 'inset 0 0 10px rgba(34, 211, 238, 0.35)',
textShadow: '0 0 8px #22d3ee',
}}
title="Vista Esquerda"
>
ESQ
</button>
{/* FACE ATRÁS */}
<button <button
onClick={() => onSelectView('back')} onClick={() => onSelectView('back')}
className="col-span-3 py-1 bg-cyan-950/70 hover:bg-cyan-500 hover:text-slate-950 text-cyan-300 rounded border border-cyan-500/40 transition-all flex items-center justify-center shadow" onMouseEnter={() => setHoveredFace('ATRÁS')}
onMouseLeave={() => setHoveredFace(null)}
className="absolute inset-0 flex items-center justify-center font-extrabold text-xs tracking-wider transition-all duration-200 cursor-pointer"
style={{
transform: 'rotateY(180deg) translateZ(40px)',
backgroundColor: hoveredFace === 'ATRÁS' ? '#0891b2' : '#0f172a',
color: hoveredFace === 'ATRÁS' ? '#ffffff' : '#a5f3fc',
border: '2px solid #22d3ee',
boxShadow:
hoveredFace === 'ATRÁS'
? 'inset 0 0 15px rgba(34, 211, 238, 0.9), 0 0 15px #06b6d4'
: 'inset 0 0 10px rgba(34, 211, 238, 0.35)',
textShadow: '0 0 8px #22d3ee',
}}
title="Vista Posterior (Atrás)" title="Vista Posterior (Atrás)"
> >
ATRÁS ATRÁS
</button> </button>
{/* FACE BASE (INFERIOR) */}
<button
onClick={() => onSelectView('bottom')}
onMouseEnter={() => setHoveredFace('BASE')}
onMouseLeave={() => setHoveredFace(null)}
className="absolute inset-0 flex items-center justify-center font-extrabold text-xs tracking-wider transition-all duration-200 cursor-pointer"
style={{
transform: 'rotateX(-90deg) translateZ(40px)',
backgroundColor: hoveredFace === 'BASE' ? '#0891b2' : '#0f172a',
color: hoveredFace === 'BASE' ? '#ffffff' : '#a5f3fc',
border: '2px solid #22d3ee',
boxShadow:
hoveredFace === 'BASE'
? 'inset 0 0 15px rgba(34, 211, 238, 0.9), 0 0 15px #06b6d4'
: 'inset 0 0 10px rgba(34, 211, 238, 0.35)',
textShadow: '0 0 8px #22d3ee',
}}
title="Vista Inferior (Base)"
>
BASE
</button>
</div> </div>
</div> </div>
{/* Botão de Vista Isométrica Padrão (ISO) */}
<button
onClick={() => onSelectView('iso')}
className="absolute bottom-1.5 right-1.5 p-1 rounded-lg bg-slate-900/90 hover:bg-cyan-600 text-cyan-300 hover:text-white border border-cyan-500/30 transition-all text-[10px] font-mono font-bold flex items-center gap-1 shadow cursor-pointer"
title="Restaurar Vista Isométrica 3D"
>
<RotateCcw className="w-2.5 h-2.5" />
<span>ISO</span>
</button>
</div> </div>
<div className="text-center mt-1 text-[9px] font-mono uppercase text-cyan-400/80 tracking-widest">
Orientação 3D {/* Rótulo Inferior Elegante */}
</div> <span className="text-[9px] font-mono font-bold tracking-widest uppercase text-cyan-700 dark:text-cyan-400 mt-1">
Cubo de Orientação
</span>
</div> </div>
); );
}; };
+5 -85
View File
@@ -2,21 +2,16 @@ import React, { useState } from 'react';
import { import {
Eye, Eye,
Grid as GridIcon, Grid as GridIcon,
Scissors,
Maximize2, Maximize2,
Box, Box,
Layers, Layers,
Compass, Compass,
Footprints,
Ruler,
Palette, Palette,
CheckCircle2, CheckCircle2,
ChevronLeft, ChevronLeft,
ChevronRight, ChevronRight,
Focus, Focus,
Sliders, Sliders
Sparkles,
RotateCcw
} from 'lucide-react'; } from 'lucide-react';
interface Viewer3DSidebarProps { interface Viewer3DSidebarProps {
@@ -28,16 +23,10 @@ interface Viewer3DSidebarProps {
onToggleCamera: () => void; onToggleCamera: () => void;
isWireframe: boolean; isWireframe: boolean;
onToggleWireframe: () => void; onToggleWireframe: () => void;
navMode: 'orbit' | 'walk';
onNavModeChange: (mode: 'orbit' | 'walk') => void;
colorMode: 'description' | 'production'; colorMode: 'description' | 'production';
onColorModeChange: (mode: 'description' | 'production') => void; onColorModeChange: (mode: 'description' | 'production') => void;
onFitView: () => void; onFitView: () => void;
onToggleFullscreen: () => void; onToggleFullscreen: () => void;
onToggleSectionPlanes?: () => void;
onToggleMeasure?: () => void;
hasSectionPlanes?: boolean;
isMeasuring?: boolean;
} }
export const Viewer3DSidebar: React.FC<Viewer3DSidebarProps> = ({ export const Viewer3DSidebar: React.FC<Viewer3DSidebarProps> = ({
@@ -49,16 +38,10 @@ export const Viewer3DSidebar: React.FC<Viewer3DSidebarProps> = ({
onToggleCamera, onToggleCamera,
isWireframe, isWireframe,
onToggleWireframe, onToggleWireframe,
navMode,
onNavModeChange,
colorMode, colorMode,
onColorModeChange, onColorModeChange,
onFitView, onFitView,
onToggleFullscreen, onToggleFullscreen,
onToggleSectionPlanes,
onToggleMeasure,
hasSectionPlanes,
isMeasuring,
}) => { }) => {
const [isOpen, setIsOpen] = useState<boolean>(true); const [isOpen, setIsOpen] = useState<boolean>(true);
@@ -67,7 +50,7 @@ export const Viewer3DSidebar: React.FC<Viewer3DSidebarProps> = ({
{/* Container Principal da Barra Lateral */} {/* Container Principal da Barra Lateral */}
<div <div
className={`transition-all duration-300 ease-in-out flex flex-col bg-white/95 dark:bg-slate-900/95 backdrop-blur-md border border-slate-200 dark:border-slate-800 rounded-2xl shadow-2xl overflow-hidden ${ className={`transition-all duration-300 ease-in-out flex flex-col bg-white/95 dark:bg-slate-900/95 backdrop-blur-md border border-slate-200 dark:border-slate-800 rounded-2xl shadow-2xl overflow-hidden ${
isOpen ? 'w-64 max-h-[calc(100vh-16rem)]' : 'w-0 border-0 opacity-0 pointer-events-none' isOpen ? 'w-60 max-h-[calc(100vh-16rem)]' : 'w-0 border-0 opacity-0 pointer-events-none'
}`} }`}
> >
{/* Cabeçalho do Sub-Menu */} {/* Cabeçalho do Sub-Menu */}
@@ -142,11 +125,11 @@ export const Viewer3DSidebar: React.FC<Viewer3DSidebarProps> = ({
onClick={onToggleWireframe} onClick={onToggleWireframe}
className={`flex items-center justify-center gap-1.5 px-2.5 py-2 rounded-xl text-xs font-semibold border transition-all ${ className={`flex items-center justify-center gap-1.5 px-2.5 py-2 rounded-xl text-xs font-semibold border transition-all ${
isWireframe isWireframe
? 'bg-blue-50 dark:bg-blue-950/40 text-blue-800 dark:text-blue-300 border-blue-300 dark:border-blue-800 shadow-sm' ? 'bg-cyan-50 dark:bg-cyan-950/40 text-cyan-800 dark:text-cyan-300 border-cyan-300 dark:border-cyan-800 shadow-sm'
: 'bg-white dark:bg-slate-900 text-slate-700 dark:text-slate-400 border-slate-200 dark:border-slate-800 hover:border-slate-300' : 'bg-white dark:bg-slate-900 text-slate-700 dark:text-slate-400 border-slate-200 dark:border-slate-800 hover:border-slate-300'
}`} }`}
> >
{isWireframe ? <Layers className="w-3.5 h-3.5" /> : <Box className="w-3.5 h-3.5" />} {isWireframe ? <Layers className="w-3.5 h-3.5 text-cyan-600" /> : <Box className="w-3.5 h-3.5" />}
<span>{isWireframe ? 'Aramado' : 'Sólido'}</span> <span>{isWireframe ? 'Aramado' : 'Sólido'}</span>
</button> </button>
</div> </div>
@@ -156,7 +139,7 @@ export const Viewer3DSidebar: React.FC<Viewer3DSidebarProps> = ({
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className="text-[11px] font-bold uppercase tracking-wider text-slate-600 dark:text-slate-400 flex items-center gap-1.5"> <label className="text-[11px] font-bold uppercase tracking-wider text-slate-600 dark:text-slate-400 flex items-center gap-1.5">
<Compass className="w-3.5 h-3.5 text-cyan-600 dark:text-cyan-400" /> <Compass className="w-3.5 h-3.5 text-cyan-600 dark:text-cyan-400" />
Câmera & Navegação Câmera & Visualização
</label> </label>
<div className="grid grid-cols-2 gap-1.5"> <div className="grid grid-cols-2 gap-1.5">
{/* Câmera Toggle */} {/* Câmera Toggle */}
@@ -182,34 +165,6 @@ export const Viewer3DSidebar: React.FC<Viewer3DSidebarProps> = ({
<span>Enquadrar</span> <span>Enquadrar</span>
</button> </button>
</div> </div>
<div className="grid grid-cols-2 gap-1.5">
{/* Orbitar */}
<button
onClick={() => onNavModeChange('orbit')}
className={`flex items-center justify-center gap-1.5 px-2 py-1.5 rounded-lg text-xs font-semibold border transition-all ${
navMode === 'orbit'
? 'bg-cyan-50 dark:bg-cyan-950/40 text-cyan-800 dark:text-cyan-300 border-cyan-300 dark:border-cyan-800'
: 'bg-white dark:bg-slate-900 text-slate-700 dark:text-slate-400 border-slate-200 dark:border-slate-800'
}`}
>
<Compass className="w-3.5 h-3.5" />
<span>Orbitar</span>
</button>
{/* Caminhar */}
<button
onClick={() => onNavModeChange('walk')}
className={`flex items-center justify-center gap-1.5 px-2 py-1.5 rounded-lg text-xs font-semibold border transition-all ${
navMode === 'walk'
? 'bg-cyan-50 dark:bg-cyan-950/40 text-cyan-800 dark:text-cyan-300 border-cyan-300 dark:border-cyan-800'
: 'bg-white dark:bg-slate-900 text-slate-700 dark:text-slate-400 border-slate-200 dark:border-slate-800'
}`}
>
<Footprints className="w-3.5 h-3.5" />
<span>Caminhar</span>
</button>
</div>
</div> </div>
{/* Grupo 4: Opacidade da Estrutura */} {/* Grupo 4: Opacidade da Estrutura */}
@@ -237,41 +192,6 @@ export const Viewer3DSidebar: React.FC<Viewer3DSidebarProps> = ({
</div> </div>
</div> </div>
{/* Grupo 5: Ferramentas Especiais */}
<div className="space-y-1.5">
<label className="text-[11px] font-bold uppercase tracking-wider text-slate-600 dark:text-slate-400 flex items-center gap-1.5">
<Sparkles className="w-3.5 h-3.5 text-cyan-600 dark:text-cyan-400" />
Ferramentas SteelXR
</label>
<div className="grid grid-cols-2 gap-1.5">
{/* Cortes */}
<button
onClick={onToggleSectionPlanes}
className={`flex items-center justify-center gap-1.5 px-2.5 py-2 rounded-xl text-xs font-semibold border transition-all ${
hasSectionPlanes
? 'bg-rose-50 dark:bg-rose-950/40 text-rose-800 dark:text-rose-300 border-rose-300 dark:border-rose-800 shadow-sm'
: 'bg-white dark:bg-slate-900 text-slate-700 dark:text-slate-400 border-slate-200 dark:border-slate-800 hover:border-slate-300'
}`}
>
<Scissors className="w-3.5 h-3.5" />
<span>Cortes</span>
</button>
{/* Medir */}
<button
onClick={onToggleMeasure}
className={`flex items-center justify-center gap-1.5 px-2.5 py-2 rounded-xl text-xs font-semibold border transition-all ${
isMeasuring
? 'bg-amber-50 dark:bg-amber-950/40 text-amber-800 dark:text-amber-300 border-amber-300 dark:border-amber-800 shadow-sm'
: 'bg-white dark:bg-slate-900 text-slate-700 dark:text-slate-400 border-slate-200 dark:border-slate-800 hover:border-slate-300'
}`}
>
<Ruler className="w-3.5 h-3.5" />
<span>Medir</span>
</button>
</div>
</div>
{/* Botão Tela Cheia */} {/* Botão Tela Cheia */}
<button <button
onClick={onToggleFullscreen} onClick={onToggleFullscreen}