From 94d41c0e8510fd91bbc44e3d94b22d378b96ac87 Mon Sep 17 00:00:00 2001 From: Marcos Reifonas Date: Mon, 21 Sep 2026 01:42:58 +0000 Subject: [PATCH] feat(viewer3d): hybrid process visualization (green solid + grey ghost frame), 70% synced WebGL ViewCube, dark grid fine-tuning, and industrial grey wireframe --- src/components/viewer3d/ModelViewer3D.tsx | 233 ++++++++----- src/components/viewer3d/ViewCube.tsx | 346 +++++++++++--------- src/components/viewer3d/Viewer3DSidebar.tsx | 72 +++- 3 files changed, 396 insertions(+), 255 deletions(-) diff --git a/src/components/viewer3d/ModelViewer3D.tsx b/src/components/viewer3d/ModelViewer3D.tsx index e706a68..9d18f6f 100644 --- a/src/components/viewer3d/ModelViewer3D.tsx +++ b/src/components/viewer3d/ModelViewer3D.tsx @@ -51,10 +51,8 @@ export const ModelViewer3D: React.FC = ({ const [showGrid, setShowGrid] = useState(true); const [isOrthographic, setIsOrthographic] = useState(false); const [isWireframe, setIsWireframe] = useState(false); - const [navMode, setNavMode] = useState<'orbit' | 'walk'>('orbit'); const [colorMode, setColorMode] = useState<'description' | 'production'>('production'); - const [hasSectionPlanes, setHasSectionPlanes] = useState(false); - const [isMeasuring, setIsMeasuring] = useState(false); + const [selectedProcess, setSelectedProcess] = useState('all'); // Hover Tooltip States (1.5 seconds) const hoverTimerRef = useRef(null); @@ -146,14 +144,14 @@ export const ModelViewer3D: React.FC = ({ // 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); + // No modo escuro: tom ligeiramente mais claro (slate-800 #1e293b e centro #475569) + const centerColor = isDark ? new THREE.Color(0x475569) : new THREE.Color(0x94a3b8); + const gridColor = isDark ? new THREE.Color(0x1e293b) : 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; @@ -168,19 +166,19 @@ export const ModelViewer3D: React.FC = ({ 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; + (gridHelper.material as THREE.Material).opacity = isDark ? 0.45 : 0.45; } }; const isDarkMode = document.documentElement.classList.contains('dark'); - const centerLineColor = isDarkMode ? 0x334155 : 0x94a3b8; - const gridLineColor = isDarkMode ? 0x0f172a : 0xe2e8f0; + const centerLineColor = isDarkMode ? 0x475569 : 0x94a3b8; + const gridLineColor = isDarkMode ? 0x1e293b : 0xe2e8f0; const grid = new THREE.GridHelper(300, 150, centerLineColor, gridLineColor); grid.position.y = 0; if (grid.material && (grid.material as THREE.Material)) { (grid.material as THREE.Material).transparent = true; - (grid.material as THREE.Material).opacity = isDarkMode ? 0.35 : 0.45; + (grid.material as THREE.Material).opacity = isDarkMode ? 0.45 : 0.45; } scene.add(grid); gridHelperRef.current = grid; @@ -354,34 +352,111 @@ export const ModelViewer3D: React.FC = ({ // Se o elemento não estiver visível na fase atual, pula estilização detalhada if (!isVisibleByPhase) return; - // 4. Aplicação de Cores / Materiais - if (colorMode === 'production' && productionData && productionData.size > 0) { - if (prod) { - matchedCount++; - if (!sampleMatch && prod.pointedQtd > 0) { - sampleMatch = { pmark: assMark || pmark, dbMarca: prod.marca, proc: prod.currentProcessName, cor: prod.processColor }; - } - } + // 4. Mapeamento de Processos Industriais para Inovação Híbrida + const PROCESS_ORDER_MAP: Record = { + 'corte': 1, + 'montagem': 2, + 'solda': 3, + 'pintura': 4, + 'pintura/galv': 4, + 'expedicao': 5, + 'expedição': 5, + }; - if (prod && prod.pointedQtd > 0) { - pointedCount++; - // Cores baseadas no progresso: verde claro -> verde escuro - const qtyPercent = Math.min(prod.pointedQtd / prod.totalQtd, 1.0); - const processPercent = Math.min((prod.processOrdem || 1) / 5, 1.0); - const percent = qtyPercent * processPercent; - - const colorLight = new THREE.Color('#4ade80'); // Verde claro - const colorDark = new THREE.Color('#14532d'); // Verde escuro - const finalColor = new THREE.Color().lerpColors(colorLight, colorDark, percent); + const targetProcKey = selectedProcess.toLowerCase(); + const targetOrder = PROCESS_ORDER_MAP[targetProcKey] || 0; + let isPointedInTargetProcess = false; + if (selectedProcess !== 'all' && prod && prod.pointedQtd > 0) { + const pieceProcOrder = prod.processOrdem || 1; + const pieceProcName = (prod.currentProcessName || '').toLowerCase(); + isPointedInTargetProcess = + pieceProcOrder >= targetOrder || + pieceProcName.includes(targetProcKey); + } + + // 5. Aplicação de Cores / Materiais (Sólido vs Aramado Híbrido) + if (selectedProcess !== 'all') { + // --- MODO INOVAÇÃO: Destaque por Processo --- + if (isPointedInTargetProcess) { + // Peça apontada no processo: SÓLIDO VERDE BRILHANTE mesh.material = new THREE.MeshStandardMaterial({ - color: finalColor, + color: new THREE.Color('#22c55e'), // Verde Sólido metalness: 0.35, roughness: 0.45, + transparent: opacity < 100, + opacity: opacity / 100, side: THREE.DoubleSide, }); + if (mesh.userData.edgesLine) { + mesh.userData.edgesLine.visible = false; + } } else { - // Não apontada / Pendente ou Não vinculada: 100% CINZA CLARO INDUSTRIAL (#a1a1aa) + // Peça NÃO apontada no processo: ARAMADO CINZA CLARO FANTASMA (Ghost Frame) + if (!mesh.userData.edgesLine) { + const edgesGeo = new THREE.EdgesGeometry(mesh.geometry, 24); + const lineMat = new THREE.LineBasicMaterial({ + color: 0x94a3b8, // Cinza claro industrial + transparent: true, + opacity: 0.65, + }); + const edgesLine = new THREE.LineSegments(edgesGeo, lineMat); + mesh.userData.edgesLine = edgesLine; + mesh.add(edgesLine); + } else { + if (mesh.userData.edgesLine.material) { + mesh.userData.edgesLine.material.color.set(0x94a3b8); + mesh.userData.edgesLine.material.opacity = 0.65; + } + } + mesh.userData.edgesLine.visible = true; + + // Oculta sólido + if (Array.isArray(mesh.material)) { + mesh.material.forEach((m) => { m.visible = false; }); + } else if (mesh.material) { + mesh.material.visible = false; + } + } + } else { + // --- MODO GERAL (Todas as peças) --- + if (colorMode === 'production' && productionData && productionData.size > 0) { + if (prod) { + matchedCount++; + if (!sampleMatch && prod.pointedQtd > 0) { + sampleMatch = { pmark: assMark || pmark, dbMarca: prod.marca, proc: prod.currentProcessName, cor: prod.processColor }; + } + } + + if (prod && prod.pointedQtd > 0) { + pointedCount++; + const qtyPercent = Math.min(prod.pointedQtd / prod.totalQtd, 1.0); + const processPercent = Math.min((prod.processOrdem || 1) / 5, 1.0); + const percent = qtyPercent * processPercent; + + const colorLight = new THREE.Color('#4ade80'); + const colorDark = new THREE.Color('#14532d'); + const finalColor = new THREE.Color().lerpColors(colorLight, colorDark, percent); + + mesh.material = new THREE.MeshStandardMaterial({ + color: finalColor, + metalness: 0.35, + roughness: 0.45, + side: THREE.DoubleSide, + }); + } else { + // Cinza Claro Industrial (#a1a1aa) + mesh.material = new THREE.MeshStandardMaterial({ + color: new THREE.Color(0xa1a1aa), + metalness: 0.25, + roughness: 0.65, + transparent: opacity < 100, + opacity: opacity / 100, + side: THREE.DoubleSide, + }); + } + } else { + // Modo Padrão / Descrição mesh.material = new THREE.MeshStandardMaterial({ color: new THREE.Color(0xa1a1aa), metalness: 0.25, @@ -391,60 +466,50 @@ export const ModelViewer3D: React.FC = ({ side: THREE.DoubleSide, }); } - } else { - // Todas as peças iniciam / permanecem em Cinza Claro Industrial (#a1a1aa) - mesh.material = new THREE.MeshStandardMaterial({ - color: new THREE.Color(0xa1a1aa), - metalness: 0.25, - roughness: 0.65, - transparent: opacity < 100, - opacity: opacity / 100, - side: THREE.DoubleSide, - }); - } - // 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; + // Controle de Aramado Geral (Botão Sólido / Aramado) + if (isWireframe) { + if (!mesh.userData.edgesLine) { + const edgesGeo = new THREE.EdgesGeometry(mesh.geometry, 24); + const lineMat = new THREE.LineBasicMaterial({ + color: 0x94a3b8, // Cinza claro industrial idêntico às peças não apontadas + transparent: true, + opacity: 0.85, + }); + const edgesLine = new THREE.LineSegments(edgesGeo, lineMat); + mesh.userData.edgesLine = edgesLine; + mesh.add(edgesLine); + } else { + if (mesh.userData.edgesLine.material) { + mesh.userData.edgesLine.material.color.set(0x94a3b8); + mesh.userData.edgesLine.material.opacity = 0.85; + } + } + mesh.userData.edgesLine.visible = true; - // Oculta a malha sólida - if (Array.isArray(mesh.material)) { - mesh.material.forEach((m) => { - m.visible = false; - }); - } 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 = false; }); + } else if (mesh.material) { + mesh.material.visible = false; + } + } else { + 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.opacity = opacity / 100; - if ('wireframe' in mesh.material) (mesh.material as any).wireframe = 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.opacity = opacity / 100; + if ('wireframe' in mesh.material) (mesh.material as any).wireframe = false; + } } } } @@ -637,13 +702,15 @@ export const ModelViewer3D: React.FC = ({ onToggleWireframe={() => setIsWireframe(!isWireframe)} colorMode={colorMode} onColorModeChange={setColorMode} + selectedProcess={selectedProcess} + onSelectProcess={setSelectedProcess} onFitView={() => fitModelToView()} onToggleFullscreen={handleToggleFullscreen} /> - {/* Top Right: Interactive ViewCube */} + {/* Top Right: Interactive ViewCube WebGL sincronizado */}
- +
{/* Hover Tooltip (1.5s delay trigger) */} diff --git a/src/components/viewer3d/ViewCube.tsx b/src/components/viewer3d/ViewCube.tsx index d17d35a..f4dacc1 100644 --- a/src/components/viewer3d/ViewCube.tsx +++ b/src/components/viewer3d/ViewCube.tsx @@ -1,174 +1,196 @@ -import React, { useState } from 'react'; +import React, { useEffect, useRef } from 'react'; +import * as THREE from 'three'; import { RotateCcw } from 'lucide-react'; interface ViewCubeProps { onSelectView: (view: 'top' | 'bottom' | 'front' | 'back' | 'left' | 'right' | 'iso') => void; + mainCameraRef?: React.MutableRefObject; } -export const ViewCube: React.FC = ({ onSelectView }) => { - const [hoveredFace, setHoveredFace] = useState(null); +export const ViewCube: React.FC = ({ onSelectView, mainCameraRef }) => { + const containerRef = useRef(null); + const rendererRef = useRef(null); + const sceneRef = useRef(null); + const cameraRef = useRef(null); + const cubeRef = useRef(null); + const raycasterRef = useRef(new THREE.Raycaster()); + const mouseRef = useRef(new THREE.Vector2()); + + useEffect(() => { + if (!containerRef.current) return; + + const width = 84; + const height = 84; + + // 1. Scene & Camera + const scene = new THREE.Scene(); + sceneRef.current = scene; + + const frustum = 55; + const camera = new THREE.OrthographicCamera( + frustum / -2, + frustum / 2, + frustum / 2, + frustum / -2, + 1, + 500 + ); + camera.position.set(0, 0, 100); + camera.lookAt(0, 0, 0); + cameraRef.current = camera; + + // 2. Renderer + const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); + renderer.setSize(width, height); + renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); + renderer.outputColorSpace = THREE.SRGBColorSpace; + rendererRef.current = renderer; + + containerRef.current.innerHTML = ''; + containerRef.current.appendChild(renderer.domElement); + + // 3. Helper to create high-res Canvas Texture for each face (SteelXR Style) + const createFaceTexture = (text: string) => { + const canvas = document.createElement('canvas'); + canvas.width = 256; + canvas.height = 256; + const ctx = canvas.getContext('2d'); + if (ctx) { + // Background: Deep Dark Navy Blue (#0f172a / #1e293b) + ctx.fillStyle = '#0f172a'; + ctx.fillRect(0, 0, 256, 256); + + // Neon Cyan Border (#22d3ee) + ctx.strokeStyle = '#22d3ee'; + ctx.lineWidth = 14; + ctx.strokeRect(7, 7, 242, 242); + + // Inner soft cyan glow + ctx.strokeStyle = 'rgba(34, 211, 238, 0.4)'; + ctx.lineWidth = 6; + ctx.strokeRect(18, 18, 220, 220); + + // Text: Bold Sans-Serif (#ffffff with Cyan Glow) + ctx.fillStyle = '#ffffff'; + ctx.font = 'bold 54px system-ui, -apple-system, sans-serif'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.shadowColor = '#06b6d4'; + ctx.shadowBlur = 12; + ctx.fillText(text, 128, 128); + } + const texture = new THREE.CanvasTexture(canvas); + texture.colorSpace = THREE.SRGBColorSpace; + return texture; + }; + + // Faces Order in Three.js BoxGeometry: + // 0: Right (DIR), 1: Left (ESQ), 2: Top (TOPO), 3: Bottom (BASE), 4: Front (FRENTE), 5: Back (ATRÁS) + const materials = [ + new THREE.MeshBasicMaterial({ map: createFaceTexture('DIR') }), + new THREE.MeshBasicMaterial({ map: createFaceTexture('ESQ') }), + new THREE.MeshBasicMaterial({ map: createFaceTexture('TOPO') }), + new THREE.MeshBasicMaterial({ map: createFaceTexture('BASE') }), + new THREE.MeshBasicMaterial({ map: createFaceTexture('FRENTE') }), + new THREE.MeshBasicMaterial({ map: createFaceTexture('ATRÁS') }), + ]; + + const geometry = new THREE.BoxGeometry(32, 32, 32); + const cube = new THREE.Mesh(geometry, materials); + scene.add(cube); + cubeRef.current = cube; + + // Glowing Neon Edges on the Cube + const edges = new THREE.EdgesGeometry(geometry); + const lineMaterial = new THREE.LineBasicMaterial({ color: 0x22d3ee, linewidth: 2 }); + const wireframe = new THREE.LineSegments(edges, lineMaterial); + cube.add(wireframe); + + // 4. Animation Loop: Sincroniza com a câmera principal em tempo real + let animId: number; + const animate = () => { + animId = requestAnimationFrame(animate); + + if (mainCameraRef && mainCameraRef.current && cubeRef.current) { + // Copia a orientação da câmera principal invertida para o cubo navegar sincronizado + cubeRef.current.quaternion.copy(mainCameraRef.current.quaternion).invert(); + } + + if (rendererRef.current && sceneRef.current && cameraRef.current) { + rendererRef.current.render(sceneRef.current, cameraRef.current); + } + }; + animate(); + + // 5. Interactivity: Click face to switch view + const handleCanvasClick = (event: MouseEvent) => { + if (!renderer.domElement || !cubeRef.current) return; + const rect = renderer.domElement.getBoundingClientRect(); + mouseRef.current.x = ((event.clientX - rect.left) / rect.width) * 2 - 1; + mouseRef.current.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; + + raycasterRef.current.setFromCamera(mouseRef.current, camera); + const intersects = raycasterRef.current.intersectObject(cubeRef.current); + + if (intersects.length > 0) { + const faceIndex = intersects[0].face?.materialIndex; + switch (faceIndex) { + case 0: + onSelectView('right'); + break; + case 1: + onSelectView('left'); + break; + case 2: + onSelectView('top'); + break; + case 3: + onSelectView('bottom'); + break; + case 4: + onSelectView('front'); + break; + case 5: + onSelectView('back'); + break; + default: + onSelectView('iso'); + } + } + }; + + const dom = renderer.domElement; + dom.addEventListener('click', handleCanvasClick); + + return () => { + cancelAnimationFrame(animId); + dom.removeEventListener('click', handleCanvasClick); + materials.forEach((m) => { + if (m.map) m.map.dispose(); + m.dispose(); + }); + geometry.dispose(); + renderer.dispose(); + }; + }, [onSelectView, mainCameraRef]); return (
- {/* Container Principal translúcido idêntico ao SteelXR */} -
- {/* Glow de Fundo */} -
+ {/* Container Reduzido (~70% do anterior) com estilo SteelXR */} +
+ {/* Glow sutil */} +
- {/* Viewport 3D Isométrica do Cubo */} -
- {/* Cubo 3D Rotacionado em Vista Isométrica Natural */} -
- {/* FACE TOPO */} - + {/* Canvas WebGL 3D do Cubo Sincronizado */} +
- {/* FACE FRENTE */} - - - {/* FACE DIREITA (DIR) */} - - - {/* FACE ESQUERDA (ESQ) */} - - - {/* FACE ATRÁS */} - - - {/* FACE BASE (INFERIOR) */} - -
-
- - {/* Botão de Vista Isométrica Padrão (ISO) */} + {/* Botão ISO no canto inferior */}
- {/* Rótulo Inferior Elegante */} - - Cubo de Orientação + {/* Rótulo inferior */} + + Cubo 3D
); diff --git a/src/components/viewer3d/Viewer3DSidebar.tsx b/src/components/viewer3d/Viewer3DSidebar.tsx index 2ad9b47..a0bcba2 100644 --- a/src/components/viewer3d/Viewer3DSidebar.tsx +++ b/src/components/viewer3d/Viewer3DSidebar.tsx @@ -11,7 +11,12 @@ import { ChevronLeft, ChevronRight, Focus, - Sliders + Sliders, + Flame, + Wrench, + Paintbrush, + Truck, + Scissors } from 'lucide-react'; interface Viewer3DSidebarProps { @@ -25,10 +30,21 @@ interface Viewer3DSidebarProps { onToggleWireframe: () => void; colorMode: 'description' | 'production'; onColorModeChange: (mode: 'description' | 'production') => void; + selectedProcess?: string; + onSelectProcess?: (proc: string) => void; onFitView: () => void; onToggleFullscreen: () => void; } +const FABRICATION_PROCESSES = [ + { id: 'all', label: 'Todos', icon: Layers }, + { id: 'Corte', label: 'Corte', icon: Scissors }, + { id: 'Montagem', label: 'Montagem', icon: Wrench }, + { id: 'Solda', label: 'Solda', icon: Flame }, + { id: 'Pintura', label: 'Pintura', icon: Paintbrush }, + { id: 'Expedicao', label: 'Expedição', icon: Truck }, +]; + export const Viewer3DSidebar: React.FC = ({ opacity, onOpacityChange, @@ -40,6 +56,8 @@ export const Viewer3DSidebar: React.FC = ({ onToggleWireframe, colorMode, onColorModeChange, + selectedProcess = 'all', + onSelectProcess, onFitView, onToggleFullscreen, }) => { @@ -50,7 +68,7 @@ export const Viewer3DSidebar: React.FC = ({ {/* Container Principal da Barra Lateral */}
{/* Cabeçalho do Sub-Menu */} @@ -68,18 +86,52 @@ export const Viewer3DSidebar: React.FC = ({ {/* Corpo com Rolagem Suave */}
- {/* Grupo 1: Modo de Cores / Produção */} + {/* Grupo 1: Destaque por Processo Industrial (Inovação Híbrida: Verde Sólido + Aramado Cinza Fantasma) */} +
+
+ + + BIM + Fábrica + +
+
+ {FABRICATION_PROCESSES.map((proc) => { + const Icon = proc.icon; + const isSelected = selectedProcess === proc.id; + return ( + + ); + })} +
+
+ + {/* Grupo 2: Modo de Cores Geral */}
- {/* Grupo 2: Visibilidade & Grade */} + {/* Grupo 3: Visibilidade & Grade */}
- {/* Grupo 3: Câmera & Projeção */} + {/* Grupo 4: Câmera & Projeção */}
- {/* Grupo 4: Opacidade da Estrutura */} + {/* Grupo 5: Opacidade da Estrutura */}