feat: Aramado limpo sem linhas internas (EdgesGeometry) no Analisador 3D e compactação de layout da sidebar 3D

This commit is contained in:
2026-10-08 11:52:48 -03:00
parent fbdf7e379e
commit 29a4800ff5
4 changed files with 386 additions and 125 deletions
+236 -39
View File
@@ -20,7 +20,10 @@ import {
AlertTriangle,
CheckCircle,
HelpCircle,
Cpu
Cpu,
Focus,
SunMedium,
Compass
} from 'lucide-react';
import { supabase } from '@/integrations/supabase/client';
import { useToast } from '@/hooks/use-toast';
@@ -66,11 +69,21 @@ export const AnalyzerViewer3D: React.FC<AnalyzerViewer3DProps> = ({
const canvasContainerRef = useRef<HTMLDivElement>(null);
const rendererRef = useRef<THREE.WebGLRenderer | null>(null);
const sceneRef = useRef<THREE.Scene | null>(null);
const cameraRef = useRef<THREE.PerspectiveCamera | null>(null);
const cameraRef = useRef<THREE.Camera | null>(null);
const perspCameraRef = useRef<THREE.PerspectiveCamera | null>(null);
const orthoCameraRef = useRef<THREE.OrthographicCamera | null>(null);
const [isOrthographic, setIsOrthographic] = useState<boolean>(false);
const controlsRef = useRef<OrbitControls | null>(null);
const modelGroupRef = useRef<THREE.Group | null>(null);
const gridRef = useRef<THREE.GridHelper | null>(null);
// Iluminação ajustável em tempo real
const ambientLightRef = useRef<THREE.AmbientLight | null>(null);
const hemiLightRef = useRef<THREE.HemisphereLight | null>(null);
const dirLight1Ref = useRef<THREE.DirectionalLight | null>(null);
const dirLight2Ref = useRef<THREE.DirectionalLight | null>(null);
const [brightness, setBrightness] = useState<number>(100);
const [wireframe, setWireframe] = useState<boolean>(false);
const [hoveredDefect, setHoveredDefect] = useState<DefectItem | null>(null);
const [hoverPos, setHoverPos] = useState<{ x: number; y: number } | null>(null);
@@ -98,9 +111,24 @@ export const AnalyzerViewer3D: React.FC<AnalyzerViewer3DProps> = ({
const scene = new THREE.Scene();
sceneRef.current = scene;
const camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 2000);
camera.position.set(40, 35, 50);
cameraRef.current = camera;
const perspCamera = new THREE.PerspectiveCamera(45, width / height, 0.1, 2000);
perspCamera.position.set(40, 35, 50);
perspCameraRef.current = perspCamera;
const frustumSize = 80;
const aspect = width / height;
const orthoCamera = new THREE.OrthographicCamera(
(frustumSize * aspect) / -2,
(frustumSize * aspect) / 2,
frustumSize / 2,
frustumSize / -2,
0.1,
2000
);
orthoCamera.position.copy(perspCamera.position);
orthoCameraRef.current = orthoCamera;
cameraRef.current = perspCamera;
const renderer = new THREE.WebGLRenderer({
antialias: true,
@@ -116,24 +144,32 @@ export const AnalyzerViewer3D: React.FC<AnalyzerViewer3DProps> = ({
canvasContainerRef.current.innerHTML = '';
canvasContainerRef.current.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
const controls = new OrbitControls(perspCamera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.maxDistance = 1500;
controls.minDistance = 0.2;
controlsRef.current = controls;
// Iluminação balanceada industrial
// Iluminação balanceada industrial com suporte a controle de luminosidade
const ambientLight = new THREE.AmbientLight(0xffffff, 0.85);
scene.add(ambientLight);
ambientLightRef.current = ambientLight;
// Luz hemisférica uniforme: ilumina simultaneamente de cima e baixo para eliminar sombras duras de normais invertidas
const hemiLight = new THREE.HemisphereLight(0xffffff, 0x475569, 0.6);
scene.add(hemiLight);
hemiLightRef.current = hemiLight;
const dirLight1 = new THREE.DirectionalLight(0xffffff, 1.2);
dirLight1.position.set(80, 120, 80);
scene.add(dirLight1);
dirLight1Ref.current = dirLight1;
const dirLight2 = new THREE.DirectionalLight(0x94a3b8, 0.6);
dirLight2.position.set(-80, -40, -80);
scene.add(dirLight2);
dirLight2Ref.current = dirLight2;
// Grid no piso com cores refinadas para modo claro e escuro
const isDarkMode = document.documentElement.classList.contains('dark');
@@ -178,16 +214,31 @@ export const AnalyzerViewer3D: React.FC<AnalyzerViewer3DProps> = ({
const animate = () => {
animId = requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
if (renderer && scene && cameraRef.current) {
renderer.render(scene, cameraRef.current);
}
};
animate();
const handleResize = () => {
if (!canvasContainerRef.current || !renderer || !camera) return;
if (!canvasContainerRef.current || !renderer) return;
const w = canvasContainerRef.current.clientWidth;
const h = canvasContainerRef.current.clientHeight;
camera.aspect = w / h;
camera.updateProjectionMatrix();
if (w <= 0 || h <= 0) return;
const newAspect = w / h;
if (perspCameraRef.current) {
perspCameraRef.current.aspect = newAspect;
perspCameraRef.current.updateProjectionMatrix();
}
if (orthoCameraRef.current) {
const frustum = 80;
orthoCameraRef.current.left = (frustum * newAspect) / -2;
orthoCameraRef.current.right = (frustum * newAspect) / 2;
orthoCameraRef.current.top = frustum / 2;
orthoCameraRef.current.bottom = frustum / -2;
orthoCameraRef.current.updateProjectionMatrix();
}
renderer.setSize(w, h);
};
@@ -205,7 +256,45 @@ export const AnalyzerViewer3D: React.FC<AnalyzerViewer3DProps> = ({
};
}, [theme]);
// Centraliza e enquadra perfeitamente o modelo na câmera (Fit View)
// Efeito dinâmico de ajuste de Luminosidade / Iluminação
useEffect(() => {
const factor = brightness / 100;
if (ambientLightRef.current) {
ambientLightRef.current.intensity = 0.85 * factor;
}
if (hemiLightRef.current) {
hemiLightRef.current.intensity = 0.6 * factor;
}
if (dirLight1Ref.current) {
dirLight1Ref.current.intensity = 1.2 * Math.min(1.4, factor);
}
if (dirLight2Ref.current) {
dirLight2Ref.current.intensity = 0.6 * factor;
}
if (rendererRef.current) {
rendererRef.current.toneMappingExposure = 1.1 * (0.85 + 0.15 * factor);
}
}, [brightness]);
// Alterna entre Câmera Perspectiva e Ortográfica
const handleToggleCamera = useCallback(() => {
if (!perspCameraRef.current || !orthoCameraRef.current || !controlsRef.current) return;
const nextOrtho = !isOrthographic;
setIsOrthographic(nextOrtho);
const prevCamera = cameraRef.current!;
const nextCamera = nextOrtho ? orthoCameraRef.current : perspCameraRef.current;
nextCamera.position.copy(prevCamera.position);
nextCamera.quaternion.copy(prevCamera.quaternion);
cameraRef.current = nextCamera;
controlsRef.current.object = nextCamera;
controlsRef.current.update();
}, [isOrthographic]);
// Centraliza e enquadra perfeitamente o modelo na câmera (Fit View / Fit to Screen)
const fitModelToView = useCallback((targetObj?: THREE.Object3D) => {
const obj = targetObj || modelGroupRef.current;
if (!obj || !cameraRef.current || !controlsRef.current) return;
@@ -224,11 +313,27 @@ export const AnalyzerViewer3D: React.FC<AnalyzerViewer3DProps> = ({
cameraDistance = Math.max(cameraDistance, 5);
const camera = cameraRef.current;
camera.near = Math.max(0.01, maxDim / 500);
camera.far = Math.max(2000, maxDim * 20);
camera.updateProjectionMatrix();
camera.position.set(center.x + cameraDistance * 0.7, center.y + cameraDistance * 0.5, center.z + cameraDistance * 0.7);
camera.lookAt(center);
if ((camera as THREE.PerspectiveCamera).isPerspectiveCamera) {
const pCam = camera as THREE.PerspectiveCamera;
pCam.near = Math.max(0.01, maxDim / 500);
pCam.far = Math.max(2000, maxDim * 20);
pCam.updateProjectionMatrix();
pCam.position.set(center.x + cameraDistance * 0.7, center.y + cameraDistance * 0.5, center.z + cameraDistance * 0.7);
pCam.lookAt(center);
} else if ((camera as THREE.OrthographicCamera).isOrthographicCamera) {
const oCam = camera as THREE.OrthographicCamera;
const aspect = oCam.right / oCam.top;
const frustum = maxDim * 1.5;
oCam.left = (frustum * aspect) / -2;
oCam.right = (frustum * aspect) / 2;
oCam.top = frustum / 2;
oCam.bottom = frustum / -2;
oCam.near = Math.max(0.01, maxDim / 500);
oCam.far = Math.max(2000, maxDim * 20);
oCam.position.set(center.x + maxDim, center.y + maxDim * 0.8, center.z + maxDim);
oCam.lookAt(center);
oCam.updateProjectionMatrix();
}
controlsRef.current.target.copy(center);
controlsRef.current.maxDistance = Math.max(1500, maxDim * 10);
@@ -418,18 +523,53 @@ export const AnalyzerViewer3D: React.FC<AnalyzerViewer3DProps> = ({
emissiveIntensity = 0.35;
}
// Aplica o material atualizado
obj.material = new THREE.MeshStandardMaterial({
color: new THREE.Color(color),
emissive: new THREE.Color(emissiveColor),
emissiveIntensity,
roughness: 0.35,
metalness: 0.2,
wireframe,
transparent,
opacity,
side: THREE.DoubleSide,
});
// Aramado Arquitetural Limpo (EdgesGeometry - Estilo SteelXR / Tekla sem diagonais internas)
if (wireframe) {
// Cria as arestas de contorno se ainda não existirem para esta malha
if (!obj.userData.edgesLine) {
const edgesGeo = new THREE.EdgesGeometry(obj.geometry, 24);
const lineMat = new THREE.LineBasicMaterial({
color: new THREE.Color(color),
transparent: true,
opacity: isGhostMode && targetMark && !isSelected ? 0.15 : 0.95,
});
const edgesLine = new THREE.LineSegments(edgesGeo, lineMat);
edgesLine.name = 'edgesLine';
obj.userData.edgesLine = edgesLine;
obj.add(edgesLine);
} else {
// Atualiza a cor e opacidade das arestas existentes
const lineMat = obj.userData.edgesLine.material as THREE.LineBasicMaterial;
if (lineMat) {
lineMat.color.set(color);
lineMat.opacity = isGhostMode && targetMark && !isSelected ? 0.15 : 0.95;
}
}
obj.userData.edgesLine.visible = true;
// Oculta a malha sólida para exibir apenas o aramado limpo
obj.material = new THREE.MeshBasicMaterial({
visible: false,
wireframe: false,
});
} else {
// Modo Sólido: desativa as linhas de contorno e restaura o sólido PBR
if (obj.userData.edgesLine) {
obj.userData.edgesLine.visible = false;
}
obj.material = new THREE.MeshStandardMaterial({
color: new THREE.Color(color),
emissive: new THREE.Color(emissiveColor),
emissiveIntensity,
roughness: 0.35,
metalness: 0.2,
wireframe: false,
transparent,
opacity,
side: THREE.DoubleSide,
});
}
}
});
}, [colorizationMode, selectedDefect, selectedPieceMark, isGhostMode, wireframe, modelData]);
@@ -598,51 +738,108 @@ export const AnalyzerViewer3D: React.FC<AnalyzerViewer3DProps> = ({
</div>
{/* Barra de Ferramentas Flutuante Superior */}
<div className="absolute top-3 left-3 flex items-center gap-1.5 p-1.5 bg-white/95 dark:bg-slate-900/90 backdrop-blur-md border border-slate-200 dark:border-slate-700/80 rounded-lg shadow-md z-20">
<div className="absolute top-3 left-3 flex flex-wrap items-center gap-1.5 p-1.5 bg-white/95 dark:bg-slate-900/90 backdrop-blur-md border border-slate-200 dark:border-slate-700/80 rounded-xl shadow-md z-20">
{/* Reset Câmera */}
<button
onClick={handleResetCamera}
title="Resetar Câmera"
className="p-1.5 text-slate-600 hover:text-slate-900 hover:bg-slate-100 dark:text-slate-300 dark:hover:text-white dark:hover:bg-slate-800 rounded transition"
className="p-1.5 text-slate-600 hover:text-slate-900 hover:bg-slate-100 dark:text-slate-300 dark:hover:text-white dark:hover:bg-slate-800 rounded-lg transition"
>
<RotateCcw className="w-4 h-4" />
</button>
<div className="w-[1px] h-4 bg-slate-200 dark:bg-slate-700 mx-1" />
{/* Centralizar e Enquadrar (Fit to Screen) */}
<button
onClick={() => fitModelToView()}
title="Centralizar e Enquadrar Estrutura na Tela (Fit to Screen)"
className="p-1.5 text-slate-600 hover:text-cyan-600 hover:bg-cyan-50 dark:text-slate-300 dark:hover:text-cyan-400 dark:hover:bg-cyan-950/40 rounded-lg transition"
>
<Focus className="w-4 h-4 text-cyan-600 dark:text-cyan-400" />
</button>
<div className="w-[1px] h-4 bg-slate-200 dark:bg-slate-700 mx-0.5" />
{/* Comutar Perspectiva / Ortogonal */}
<button
onClick={handleToggleCamera}
title={isOrthographic ? 'Mudar para Vista em Perspectiva (3D Cônica)' : 'Mudar para Vista Ortogonal (Paralela Técnica)'}
className={`flex items-center gap-1.5 px-2 py-1 text-xs font-semibold rounded-lg transition ${
isOrthographic
? 'bg-emerald-600 text-white shadow-sm'
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-300 dark:hover:bg-slate-800 dark:hover:text-white'
}`}
>
<Compass className="w-3.5 h-3.5" />
<span className="hidden sm:inline">{isOrthographic ? 'Ortogonal' : 'Perspectiva'}</span>
</button>
<div className="w-[1px] h-4 bg-slate-200 dark:bg-slate-700 mx-0.5" />
{/* Modo Raio-X / Foco */}
<button
onClick={onToggleGhostMode}
title={isGhostMode ? 'Desativar Modo Raio-X / Foco' : 'Ativar Modo Raio-X / Foco'}
className={`flex items-center gap-1.5 px-2 py-1 text-xs font-medium rounded transition ${
className={`flex items-center gap-1.5 px-2 py-1 text-xs font-medium rounded-lg transition ${
isGhostMode
? 'bg-indigo-600 text-white shadow'
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-300 dark:hover:bg-slate-800 dark:hover:text-white'
}`}
>
<Eye className="w-3.5 h-3.5" />
<span>Foco / Raio-X</span>
<span className="hidden sm:inline">Foco / Raio-X</span>
</button>
{/* Aramado Limpo (EdgesGeometry) */}
<button
onClick={() => setWireframe(!wireframe)}
title="Alternar Estrutura de Arame (Wireframe)"
className={`p-1.5 rounded transition ${
title={wireframe ? 'Mudar para Modelo Sólido' : 'Mudar para Aramado Limpo de Engenharia (sem linhas internas)'}
className={`p-1.5 rounded-lg transition ${
wireframe
? 'bg-indigo-600 text-white'
? 'bg-indigo-600 text-white shadow-xs'
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-300 dark:hover:bg-slate-800 dark:hover:text-white'
}`}
>
<Box className="w-4 h-4" />
{wireframe ? <Layers className="w-4 h-4" /> : <Box className="w-4 h-4" />}
</button>
{onToggleFullscreen && (
<button
onClick={onToggleFullscreen}
title={isFullscreen ? 'Sair da Tela Cheia' : 'Tela Cheia'}
className="p-1.5 text-slate-600 hover:text-slate-900 hover:bg-slate-100 dark:text-slate-300 dark:hover:text-white dark:hover:bg-slate-800 rounded transition"
className="p-1.5 text-slate-600 hover:text-slate-900 hover:bg-slate-100 dark:text-slate-300 dark:hover:text-white dark:hover:bg-slate-800 rounded-lg transition"
>
{isFullscreen ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
</button>
)}
<div className="w-[1px] h-4 bg-slate-200 dark:bg-slate-700 mx-0.5" />
{/* Barra de Luminosidade (Slider Interativo) */}
<div
className="flex items-center gap-1.5 px-2 py-1 bg-slate-100/90 dark:bg-slate-800/90 rounded-lg border border-slate-200 dark:border-slate-700"
title="Ajuste de Luminosidade da Estrutura (arraste para clarear ou suavizar sombras das peças)"
>
<button
type="button"
onClick={() => setBrightness(100)}
title="Clique para resetar luminosidade em 100%"
className="hover:scale-110 active:scale-95 transition-transform"
>
<SunMedium className="w-3.5 h-3.5 text-amber-500 shrink-0" />
</button>
<input
type="range"
min="40"
max="220"
step="5"
value={brightness}
onChange={(e) => setBrightness(Number(e.target.value))}
className="w-16 sm:w-20 h-1.5 bg-slate-300 dark:bg-slate-700 rounded-lg appearance-none cursor-pointer accent-amber-500"
/>
<span className="text-[10px] font-mono font-bold text-slate-600 dark:text-slate-300 min-w-[28px] text-right">
{brightness}%
</span>
</div>
</div>
{/* Legenda de Cores Flutuante Inferior */}
+73 -6
View File
@@ -6,7 +6,7 @@ import { ViewCube } from './ViewCube';
import { Viewer3DSidebar } from './Viewer3DSidebar';
import { PieceHoverTooltip } from './PieceHoverTooltip';
import { SelectionListModal, SelectedPieceItem } from './SelectionListModal';
import { Trash2, FileSpreadsheet, RotateCcw, RotateCw, Lock, Unlock } from 'lucide-react';
import { Trash2, FileSpreadsheet, RotateCcw, RotateCw, Lock, Unlock, Focus, SunMedium } from 'lucide-react';
import { supabase } from '@/integrations/supabase/client';
import { useToast } from '@/components/ui/use-toast';
@@ -59,6 +59,13 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
const [colorMode, setColorMode] = useState<'description' | 'production'>('production');
const [selectedProcess, setSelectedProcess] = useState<string>('all');
// Iluminação ajustável em tempo real
const ambientLightRef = useRef<THREE.AmbientLight | null>(null);
const hemiLightRef = useRef<THREE.HemisphereLight | null>(null);
const dirLight1Ref = useRef<THREE.DirectionalLight | null>(null);
const dirLight2Ref = useRef<THREE.DirectionalLight | null>(null);
const [brightness, setBrightness] = useState<number>(100);
// Hover Tooltip States (1.0 second delay)
const hoverTimerRef = useRef<NodeJS.Timeout | null>(null);
const [hoveredPiece, setHoveredPiece] = useState<PieceInfo | null>(null);
@@ -161,18 +168,22 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
// 5. Lights
const ambientLight = new THREE.AmbientLight(0xffffff, 0.75);
scene.add(ambientLight);
ambientLightRef.current = ambientLight;
const dirLight1 = new THREE.DirectionalLight(0xffffff, 1.2);
dirLight1.position.set(100, 200, 100);
dirLight1.castShadow = true;
scene.add(dirLight1);
dirLight1Ref.current = dirLight1;
const dirLight2 = new THREE.DirectionalLight(0x94a3b8, 0.6);
dirLight2.position.set(-100, -50, -100);
scene.add(dirLight2);
dirLight2Ref.current = dirLight2;
const hemiLight = new THREE.HemisphereLight(0xffffff, 0x1e293b, 0.5);
scene.add(hemiLight);
hemiLightRef.current = hemiLight;
// 6. Floor Grid (Cores refinadas e ultrafinas para modo claro e escuro)
const updateGridTheme = (gridHelper: THREE.GridHelper) => {
@@ -278,6 +289,16 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
const [isRotationLocked, setIsRotationLocked] = useState<boolean>(false);
const { toast } = useToast();
// Efeito dinâmico de ajuste de Luminosidade / Iluminação no ModelViewer3D
useEffect(() => {
const factor = brightness / 100;
if (ambientLightRef.current) ambientLightRef.current.intensity = 0.75 * factor;
if (hemiLightRef.current) hemiLightRef.current.intensity = 0.5 * factor;
if (dirLight1Ref.current) dirLight1Ref.current.intensity = 1.2 * Math.min(1.4, factor);
if (dirLight2Ref.current) dirLight2Ref.current.intensity = 0.6 * factor;
if (rendererRef.current) rendererRef.current.toneMappingExposure = 1.0 * (0.85 + 0.15 * factor);
}, [brightness]);
// Function: Fit Model to View
const fitModelToView = useCallback((group?: THREE.Group | null) => {
const targetGroup = group || modelGroupRef.current;
@@ -1142,6 +1163,8 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
onSelectProcess={setSelectedProcess}
onFitView={() => fitModelToView()}
onToggleFullscreen={handleToggleFullscreen}
brightness={brightness}
onBrightnessChange={setBrightness}
/>
{/* Top Right: ViewCube + Painel de Ações de Seleção de Peças */}
@@ -1202,11 +1225,54 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
</button>
</div>
{/* Card de Ajuste e Travamento de Rotação 3D (Tombamento 90° + Cadeado Memorizável) */}
<div
className="flex items-center gap-1.5 p-1.5 rounded-2xl bg-white/95 dark:bg-slate-900/95 backdrop-blur-md border border-slate-300 dark:border-slate-700 shadow-lg shadow-slate-300/30 dark:shadow-slate-950/50 pointer-events-auto"
style={{ minWidth: '130px' }}
>
{/* Barra de Ferramentas Flutuante Rápida: Luminosidade + Enquadrar + Rotação 90° & Cadeado */}
<div className="flex items-center gap-2 pointer-events-auto">
{/* Card de Luminosidade */}
<div
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-2xl bg-white/95 dark:bg-slate-900/95 backdrop-blur-md border border-slate-300 dark:border-slate-700 shadow-lg shadow-slate-300/30 dark:shadow-slate-950/50"
title="Ajuste de Luminosidade da Estrutura (arraste para clarear ou suavizar sombras das peças)"
>
<button
type="button"
onClick={() => setBrightness(100)}
title="Clique para resetar luminosidade em 100%"
className="hover:scale-110 active:scale-95 transition-transform"
>
<SunMedium className="w-3.5 h-3.5 text-amber-500 shrink-0" />
</button>
<input
type="range"
min="40"
max="220"
step="5"
value={brightness}
onChange={(e) => setBrightness(Number(e.target.value))}
className="w-16 sm:w-20 h-1.5 bg-slate-300 dark:bg-slate-700 rounded-lg appearance-none cursor-pointer accent-amber-500"
/>
<span className="text-[10px] font-mono font-bold text-slate-600 dark:text-slate-300 min-w-[28px] text-right">
{brightness}%
</span>
</div>
{/* Botão Enquadrar / Centralizar Estrutura (Fit to Screen) */}
<button
onClick={() => fitModelToView(modelGroupRef.current)}
disabled={!modelData}
className={`flex items-center justify-center p-2 rounded-2xl bg-white/95 dark:bg-slate-900/95 backdrop-blur-md border border-slate-300 dark:border-slate-700 shadow-lg shadow-slate-300/30 dark:shadow-slate-950/50 transition-all duration-150 active:scale-95 ${
modelData
? 'text-slate-700 hover:text-cyan-600 dark:text-slate-200 dark:hover:text-cyan-400 hover:bg-slate-100 dark:hover:bg-slate-800 cursor-pointer'
: 'text-slate-400 dark:text-slate-600 opacity-40 cursor-not-allowed'
}`}
title="Centralizar e Enquadrar Estrutura na Janela (Fit to Screen)"
>
<Focus className="w-4 h-4 text-cyan-600 dark:text-cyan-400" />
</button>
{/* Card de Ajuste e Travamento de Rotação 3D (Tombamento 90° + Cadeado Memorizável) */}
<div
className="flex items-center gap-1.5 p-1.5 rounded-2xl bg-white/95 dark:bg-slate-900/95 backdrop-blur-md border border-slate-300 dark:border-slate-700 shadow-lg shadow-slate-300/30 dark:shadow-slate-950/50 pointer-events-auto"
style={{ minWidth: '130px' }}
>
{/* Botão Rotacionar / Tombar 90° */}
<button
onClick={handleRotate90X}
@@ -1248,6 +1314,7 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
<Unlock className="w-4 h-4 text-amber-600 dark:text-amber-400" />
)}
</button>
</div>
</div>
</div>
+76 -79
View File
@@ -16,7 +16,8 @@ import {
Wrench,
Paintbrush,
Truck,
Scissors
Scissors,
SunMedium
} from 'lucide-react';
interface Viewer3DSidebarProps {
@@ -32,6 +33,8 @@ interface Viewer3DSidebarProps {
onSelectProcess?: (proc: string) => void;
onFitView: () => void;
onToggleFullscreen: () => void;
brightness?: number;
onBrightnessChange?: (val: number) => void;
}
const FABRICATION_PROCESSES = [
@@ -56,6 +59,8 @@ export const Viewer3DSidebar: React.FC<Viewer3DSidebarProps> = ({
onSelectProcess,
onFitView,
onToggleFullscreen,
brightness = 100,
onBrightnessChange,
}) => {
const [isOpen, setIsOpen] = useState<boolean>(true);
@@ -64,36 +69,36 @@ export const Viewer3DSidebar: React.FC<Viewer3DSidebarProps> = ({
{/* Container Principal da Barra Lateral */}
<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 ${
isOpen ? 'w-64 max-h-[calc(100vh-14rem)]' : 'w-0 border-0 opacity-0 pointer-events-none'
isOpen ? 'w-60 max-h-[calc(100%-2rem)]' : 'w-0 border-0 opacity-0 pointer-events-none'
}`}
>
{/* Cabeçalho do Sub-Menu */}
<div className="flex items-center justify-between px-4 py-3 bg-slate-100/90 dark:bg-slate-950/80 border-b border-slate-200 dark:border-slate-800">
<div className="flex items-center gap-2">
<Sliders className="w-4 h-4 text-cyan-600 dark:text-cyan-400" />
<span className="text-xs font-bold uppercase tracking-wider text-slate-800 dark:text-slate-200">
{/* Cabeçalho do Sub-Menu Enxuto */}
<div className="flex items-center justify-between px-3 py-1.5 bg-slate-100/90 dark:bg-slate-950/80 border-b border-slate-200 dark:border-slate-800">
<div className="flex items-center gap-1.5">
<Sliders className="w-3.5 h-3.5 text-cyan-600 dark:text-cyan-400" />
<span className="text-[11px] font-bold uppercase tracking-wider text-slate-800 dark:text-slate-200">
Controles 3D
</span>
</div>
<span className="text-[10px] font-mono px-1.5 py-0.5 rounded bg-cyan-100 dark:bg-cyan-900/50 text-cyan-700 dark:text-cyan-300 font-bold">
<span className="text-[9px] font-mono px-1 py-0.2 rounded bg-cyan-100 dark:bg-cyan-900/50 text-cyan-700 dark:text-cyan-300 font-bold">
SteelXR
</span>
</div>
{/* Corpo com Rolagem Suave */}
<div className="flex-1 overflow-y-auto p-3 space-y-4 custom-scrollbar">
{/* Grupo 1: Destaque por Processo Industrial com Cores Correspondentes */}
<div className="space-y-1.5">
{/* Corpo Compactado com Rolagem Suave */}
<div className="flex-1 overflow-y-auto p-2 space-y-2 custom-scrollbar">
{/* Grupo 1: Destaque por Processo Industrial */}
<div className="space-y-1">
<div className="flex items-center justify-between">
<label className="text-[11px] font-bold uppercase tracking-wider text-slate-600 dark:text-slate-400 flex items-center gap-1.5">
<CheckCircle2 className="w-3.5 h-3.5 text-emerald-600 dark:text-emerald-400" />
Destaque por Processo
<label className="text-[10px] font-bold uppercase tracking-wider text-slate-600 dark:text-slate-400 flex items-center gap-1">
<CheckCircle2 className="w-3 h-3 text-emerald-600 dark:text-emerald-400" />
Processo Fabril
</label>
<span className="text-[9px] font-mono text-emerald-600 dark:text-emerald-400 font-bold">
BIM + Fábrica
<span className="text-[8px] font-mono text-emerald-600 dark:text-emerald-400 font-bold">
BIM
</span>
</div>
<div className="grid grid-cols-3 gap-1.5 bg-slate-100 dark:bg-slate-950/60 p-1.5 rounded-xl border border-slate-200 dark:border-slate-800">
<div className="grid grid-cols-3 gap-1 bg-slate-100 dark:bg-slate-950/60 p-1 rounded-lg border border-slate-200 dark:border-slate-800">
{FABRICATION_PROCESSES.map((proc) => {
const Icon = proc.icon;
const isSelected = selectedProcess === proc.id;
@@ -106,24 +111,22 @@ export const Viewer3DSidebar: React.FC<Viewer3DSidebarProps> = ({
? {
backgroundColor: proc.color,
color: '#ffffff',
boxShadow: `0 4px 12px ${proc.color}50`,
boxShadow: `0 2px 8px ${proc.color}40`,
borderColor: proc.color,
}
: {
backgroundColor: `${proc.color}18`,
backgroundColor: `${proc.color}14`,
color: proc.color,
borderColor: `${proc.color}35`,
borderColor: `${proc.color}25`,
}
}
className={`flex flex-col items-center justify-center gap-1 py-2 px-1 rounded-lg text-[10px] font-bold border transition-all duration-200 hover:scale-[1.03] active:scale-[0.98] ${
isSelected
? 'shadow-md border-transparent font-extrabold'
: 'hover:brightness-110'
className={`flex flex-col items-center justify-center gap-0.5 py-1 px-1 rounded-md text-[9.5px] font-bold border transition-all duration-150 hover:scale-[1.02] active:scale-[0.98] ${
isSelected ? 'border-transparent font-extrabold' : 'hover:brightness-110'
}`}
title={`Destacar peças em ${proc.label} (${proc.color}) e não apontadas em aramado cinza`}
title={`Destacar peças em ${proc.label}`}
>
<Icon className="w-3.5 h-3.5 stroke-[2.2]" />
<span className="tracking-tight">{proc.label}</span>
<Icon className="w-3 h-3 stroke-[2.2]" />
<span className="tracking-tight leading-none">{proc.label}</span>
</button>
);
})}
@@ -131,99 +134,93 @@ export const Viewer3DSidebar: React.FC<Viewer3DSidebarProps> = ({
</div>
{/* Grupo 2: Modo de Cores Geral */}
<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">
<Palette className="w-3.5 h-3.5 text-cyan-600 dark:text-cyan-400" />
<div className="space-y-1">
<label className="text-[10px] font-bold uppercase tracking-wider text-slate-600 dark:text-slate-400 flex items-center gap-1">
<Palette className="w-3 h-3 text-cyan-600 dark:text-cyan-400" />
Modo de Cores
</label>
<div className="grid grid-cols-2 gap-1.5 bg-slate-100 dark:bg-slate-950/60 p-1 rounded-xl border border-slate-200 dark:border-slate-800">
<div className="grid grid-cols-2 gap-1 bg-slate-100 dark:bg-slate-950/60 p-0.5 rounded-lg border border-slate-200 dark:border-slate-800">
<button
onClick={() => onColorModeChange('production')}
className={`flex items-center justify-center gap-1.5 px-2 py-1.5 rounded-lg text-xs font-bold transition-all ${
className={`flex items-center justify-center gap-1 px-1.5 py-1 rounded-md text-[11px] font-bold transition-all ${
colorMode === 'production'
? 'bg-cyan-600 text-white shadow-md shadow-cyan-600/30'
? 'bg-cyan-600 text-white shadow-sm'
: 'text-slate-700 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white'
}`}
>
<CheckCircle2 className="w-3.5 h-3.5" />
<CheckCircle2 className="w-3 h-3" />
<span>Produção</span>
</button>
<button
onClick={() => onColorModeChange('description')}
className={`flex items-center justify-center gap-1.5 px-2 py-1.5 rounded-lg text-xs font-bold transition-all ${
className={`flex items-center justify-center gap-1 px-1.5 py-1 rounded-md text-[11px] font-bold transition-all ${
colorMode === 'description'
? 'bg-amber-600 text-white shadow-md shadow-amber-600/30'
? 'bg-amber-600 text-white shadow-sm'
: 'text-slate-700 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white'
}`}
>
<Box className="w-3.5 h-3.5" />
<Box className="w-3 h-3" />
<span>Padrão</span>
</button>
</div>
</div>
{/* Grupo 3: Ambiente & Grade */}
<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">
<GridIcon className="w-3.5 h-3.5 text-cyan-600 dark:text-cyan-400" />
<div className="space-y-1">
<label className="text-[10px] font-bold uppercase tracking-wider text-slate-600 dark:text-slate-400 flex items-center gap-1">
<GridIcon className="w-3 h-3 text-cyan-600 dark:text-cyan-400" />
Ambiente & Piso
</label>
<div>
{/* Toggle Grid */}
<button
onClick={onToggleGrid}
className={`w-full flex items-center justify-center gap-2 py-2 rounded-xl text-xs font-semibold border transition-all ${
showGrid
? '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'
}`}
>
<GridIcon className="w-3.5 h-3.5" />
<span>Grade de Piso: {showGrid ? 'LIGADO' : 'DESL'}</span>
</button>
</div>
<button
onClick={onToggleGrid}
className={`w-full flex items-center justify-center gap-1.5 py-1 rounded-lg text-[11px] font-semibold border transition-all ${
showGrid
? 'bg-cyan-50 dark:bg-cyan-950/40 text-cyan-800 dark:text-cyan-300 border-cyan-300 dark:border-cyan-800 shadow-xs'
: 'bg-white dark:bg-slate-900 text-slate-700 dark:text-slate-400 border-slate-200 dark:border-slate-800 hover:border-slate-300'
}`}
>
<GridIcon className="w-3 h-3" />
<span>Grade de Piso: {showGrid ? 'LIGADO' : 'DESL'}</span>
</button>
</div>
{/* Grupo 4: Câmera & Projeção */}
<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">
<Compass className="w-3.5 h-3.5 text-cyan-600 dark:text-cyan-400" />
{/* Grupo 4: Câmera & Visualização */}
<div className="space-y-1">
<label className="text-[10px] font-bold uppercase tracking-wider text-slate-600 dark:text-slate-400 flex items-center gap-1">
<Compass className="w-3 h-3 text-cyan-600 dark:text-cyan-400" />
Câmera & Visualização
</label>
<div className="grid grid-cols-2 gap-1.5">
{/* Câmera Toggle */}
<div className="grid grid-cols-2 gap-1">
<button
onClick={onToggleCamera}
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 px-1.5 py-1 rounded-lg text-[11px] font-semibold border transition-all ${
isOrthographic
? 'bg-emerald-50 dark:bg-emerald-950/40 text-emerald-800 dark:text-emerald-300 border-emerald-300 dark:border-emerald-800 shadow-sm'
? 'bg-emerald-50 dark:bg-emerald-950/40 text-emerald-800 dark:text-emerald-300 border-emerald-300 dark:border-emerald-800 shadow-xs'
: 'bg-white dark:bg-slate-900 text-slate-700 dark:text-slate-400 border-slate-200 dark:border-slate-800 hover:border-slate-300'
}`}
>
<Box className="w-3.5 h-3.5" />
<Box className="w-3 h-3" />
<span>{isOrthographic ? 'Ortogonal' : 'Perspectiva'}</span>
</button>
{/* Enquadrar / Reset View */}
<button
onClick={onFitView}
className="flex items-center justify-center gap-1.5 px-2.5 py-2 rounded-xl text-xs font-semibold bg-white dark:bg-slate-900 text-slate-700 dark:text-slate-300 border border-slate-200 dark:border-slate-800 hover:bg-slate-50 dark:hover:bg-slate-800 transition-all"
className="flex items-center justify-center gap-1 px-1.5 py-1 rounded-lg text-[11px] font-semibold bg-white dark:bg-slate-900 text-slate-700 dark:text-slate-300 border border-slate-200 dark:border-slate-800 hover:bg-slate-50 dark:hover:bg-slate-800 transition-all"
title="Enquadrar / Centralizar Estrutura"
>
<Focus className="w-3.5 h-3.5 text-cyan-600 dark:text-cyan-400" />
<Focus className="w-3 h-3 text-cyan-600 dark:text-cyan-400" />
<span>Enquadrar</span>
</button>
</div>
</div>
{/* Grupo 5: Opacidade da Estrutura */}
<div className="space-y-1.5 bg-slate-50 dark:bg-slate-950/60 p-2.5 rounded-xl border border-slate-200 dark:border-slate-800">
<div className="flex items-center justify-between text-xs">
<span className="font-semibold text-slate-700 dark:text-slate-300 flex items-center gap-1.5">
<Eye className="w-3.5 h-3.5 text-amber-500" />
{/* Grupo 5: Opacidade da Estrutura Compacta */}
<div className="space-y-1 bg-slate-50 dark:bg-slate-950/60 p-2 rounded-lg border border-slate-200 dark:border-slate-800">
<div className="flex items-center justify-between text-[11px]">
<span className="font-semibold text-slate-700 dark:text-slate-300 flex items-center gap-1">
<Eye className="w-3 h-3 text-amber-500" />
Opacidade
</span>
<span className="font-mono font-bold text-amber-700 dark:text-amber-400">{opacity}%</span>
<span className="font-mono font-bold text-amber-700 dark:text-amber-400 text-[10px]">{opacity}%</span>
</div>
<input
type="range"
@@ -231,9 +228,9 @@ export const Viewer3DSidebar: React.FC<Viewer3DSidebarProps> = ({
max={100}
value={opacity}
onChange={(e) => onOpacityChange(Number(e.target.value))}
className="w-full h-1.5 bg-slate-200 dark:bg-slate-700 rounded-lg appearance-none cursor-pointer accent-amber-500"
className="w-full h-1 bg-slate-200 dark:bg-slate-700 rounded-lg appearance-none cursor-pointer accent-amber-500"
/>
<div className="flex justify-between text-[10px] font-mono text-slate-400">
<div className="flex justify-between text-[9px] font-mono text-slate-400 pt-0.5">
<button onClick={() => onOpacityChange(25)} className="hover:text-amber-600">25%</button>
<button onClick={() => onOpacityChange(50)} className="hover:text-amber-600">50%</button>
<button onClick={() => onOpacityChange(75)} className="hover:text-amber-600">75%</button>
@@ -241,12 +238,12 @@ export const Viewer3DSidebar: React.FC<Viewer3DSidebarProps> = ({
</div>
</div>
{/* Botão Tela Cheia */}
{/* Botão Tela Cheia Compacto */}
<button
onClick={onToggleFullscreen}
className="w-full flex items-center justify-center gap-2 py-2 bg-slate-100 hover:bg-cyan-50 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-800 dark:text-white border border-slate-200 dark:border-slate-700 rounded-xl text-xs font-bold transition-all shadow-sm"
className="w-full flex items-center justify-center gap-1.5 py-1.5 bg-slate-100 hover:bg-cyan-50 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-800 dark:text-white border border-slate-200 dark:border-slate-700 rounded-lg text-[11px] font-bold transition-all shadow-xs"
>
<Maximize2 className="w-3.5 h-3.5 text-cyan-600 dark:text-cyan-400" />
<Maximize2 className="w-3 h-3 text-cyan-600 dark:text-cyan-400" />
<span>Expandir Tela Cheia</span>
</button>
</div>
+1 -1
View File
@@ -695,7 +695,7 @@ export default function Visualizador3D() {
</div>
{/* Main 3D Viewport */}
<div className="relative flex-1 min-h-[480px] w-full bg-slate-100 dark:bg-slate-950 rounded-2xl overflow-hidden border border-slate-200 dark:border-slate-800 shadow-xl dark:shadow-2xl">
<div className="relative flex-1 min-h-[560px] w-full bg-slate-100 dark:bg-slate-950 rounded-2xl overflow-hidden border border-slate-200 dark:border-slate-800 shadow-xl dark:shadow-2xl">
{isLoadingModel && (
<div className="absolute inset-0 z-30 flex flex-col items-center justify-center p-6 bg-white dark:bg-slate-950/90 backdrop-blur-md text-slate-900 dark:text-white space-y-4">
<div className="p-4 bg-cyan-100 text-cyan-600 dark:bg-cyan-500/20 dark:text-cyan-400 rounded-2xl border border-cyan-300 dark:border-cyan-500/40 animate-pulse">