feat(viewer3d): clean EdgesGeometry wireframe, SteelXR 3D ViewCube, dark mode ultra-fine grid, and simplified sidebar controls
This commit is contained in:
@@ -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 */}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user