From 29a4800ff5a9821e93b0d2c3b452c665cd23517a Mon Sep 17 00:00:00 2001 From: admbrainsteel Date: Thu, 8 Oct 2026 11:52:48 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20Aramado=20limpo=20sem=20linhas=20intern?= =?UTF-8?q?as=20(EdgesGeometry)=20no=20Analisador=203D=20e=20compacta?= =?UTF-8?q?=C3=A7=C3=A3o=20de=20layout=20da=20sidebar=203D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/viewer3d/AnalyzerViewer3D.tsx | 275 ++++++++++++++++--- src/components/viewer3d/ModelViewer3D.tsx | 79 +++++- src/components/viewer3d/Viewer3DSidebar.tsx | 155 +++++------ src/pages/Visualizador3D.tsx | 2 +- 4 files changed, 386 insertions(+), 125 deletions(-) diff --git a/src/components/viewer3d/AnalyzerViewer3D.tsx b/src/components/viewer3d/AnalyzerViewer3D.tsx index 7503f6d..ee78e16 100644 --- a/src/components/viewer3d/AnalyzerViewer3D.tsx +++ b/src/components/viewer3d/AnalyzerViewer3D.tsx @@ -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 = ({ const canvasContainerRef = useRef(null); const rendererRef = useRef(null); const sceneRef = useRef(null); - const cameraRef = useRef(null); + const cameraRef = useRef(null); + const perspCameraRef = useRef(null); + const orthoCameraRef = useRef(null); + const [isOrthographic, setIsOrthographic] = useState(false); const controlsRef = useRef(null); const modelGroupRef = useRef(null); const gridRef = useRef(null); + // Iluminação ajustável em tempo real + const ambientLightRef = useRef(null); + const hemiLightRef = useRef(null); + const dirLight1Ref = useRef(null); + const dirLight2Ref = useRef(null); + const [brightness, setBrightness] = useState(100); + const [wireframe, setWireframe] = useState(false); const [hoveredDefect, setHoveredDefect] = useState(null); const [hoverPos, setHoverPos] = useState<{ x: number; y: number } | null>(null); @@ -98,9 +111,24 @@ export const AnalyzerViewer3D: React.FC = ({ 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 = ({ 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 = ({ 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 = ({ }; }, [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 = ({ 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 = ({ 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 = ({ {/* Barra de Ferramentas Flutuante Superior */} -
+
+ {/* Reset Câmera */} -
+ {/* Centralizar e Enquadrar (Fit to Screen) */} + +
+ + {/* Comutar Perspectiva / Ortogonal */} + + +
+ + {/* Modo Raio-X / Foco */} + {/* Aramado Limpo (EdgesGeometry) */} {onToggleFullscreen && ( )} + +
+ + {/* Barra de Luminosidade (Slider Interativo) */} +
+ + 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" + /> + + {brightness}% + +
{/* Legenda de Cores Flutuante Inferior */} diff --git a/src/components/viewer3d/ModelViewer3D.tsx b/src/components/viewer3d/ModelViewer3D.tsx index d83e901..dcd85c1 100644 --- a/src/components/viewer3d/ModelViewer3D.tsx +++ b/src/components/viewer3d/ModelViewer3D.tsx @@ -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 = ({ const [colorMode, setColorMode] = useState<'description' | 'production'>('production'); const [selectedProcess, setSelectedProcess] = useState('all'); + // Iluminação ajustável em tempo real + const ambientLightRef = useRef(null); + const hemiLightRef = useRef(null); + const dirLight1Ref = useRef(null); + const dirLight2Ref = useRef(null); + const [brightness, setBrightness] = useState(100); + // Hover Tooltip States (1.0 second delay) const hoverTimerRef = useRef(null); const [hoveredPiece, setHoveredPiece] = useState(null); @@ -161,18 +168,22 @@ export const ModelViewer3D: React.FC = ({ // 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 = ({ const [isRotationLocked, setIsRotationLocked] = useState(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 = ({ 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 = ({
- {/* Card de Ajuste e Travamento de Rotação 3D (Tombamento 90° + Cadeado Memorizável) */} -
+ {/* Barra de Ferramentas Flutuante Rápida: Luminosidade + Enquadrar + Rotação 90° & Cadeado */} +
+ {/* Card de Luminosidade */} +
+ + 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" + /> + + {brightness}% + +
+ + {/* Botão Enquadrar / Centralizar Estrutura (Fit to Screen) */} + + + {/* Card de Ajuste e Travamento de Rotação 3D (Tombamento 90° + Cadeado Memorizável) */} +
{/* Botão Rotacionar / Tombar 90° */} +
diff --git a/src/components/viewer3d/Viewer3DSidebar.tsx b/src/components/viewer3d/Viewer3DSidebar.tsx index a42b775..02fd389 100644 --- a/src/components/viewer3d/Viewer3DSidebar.tsx +++ b/src/components/viewer3d/Viewer3DSidebar.tsx @@ -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 = ({ onSelectProcess, onFitView, onToggleFullscreen, + brightness = 100, + onBrightnessChange, }) => { const [isOpen, setIsOpen] = useState(true); @@ -64,36 +69,36 @@ export const Viewer3DSidebar: React.FC = ({ {/* Container Principal da Barra Lateral */}
- {/* Cabeçalho do Sub-Menu */} -
-
- - + {/* Cabeçalho do Sub-Menu Enxuto */} +
+
+ + Controles 3D
- + SteelXR
- {/* Corpo com Rolagem Suave */} -
- {/* Grupo 1: Destaque por Processo Industrial com Cores Correspondentes */} -
+ {/* Corpo Compactado com Rolagem Suave */} +
+ {/* Grupo 1: Destaque por Processo Industrial */} +
-
-
+
{FABRICATION_PROCESSES.map((proc) => { const Icon = proc.icon; const isSelected = selectedProcess === proc.id; @@ -106,24 +111,22 @@ export const Viewer3DSidebar: React.FC = ({ ? { 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}`} > - - {proc.label} + + {proc.label} ); })} @@ -131,99 +134,93 @@ export const Viewer3DSidebar: React.FC = ({
{/* Grupo 2: Modo de Cores Geral */} -
-