From b43d5314fbc4b22cb22c66e389b7a99421e41ad4 Mon Sep 17 00:00:00 2001 From: Marcos Reifonas Date: Mon, 21 Sep 2026 01:31:25 +0000 Subject: [PATCH] feat(viewer3d): clean EdgesGeometry wireframe, SteelXR 3D ViewCube, dark mode ultra-fine grid, and simplified sidebar controls --- src/components/viewer3d/ModelViewer3D.tsx | 103 +++++++++--- src/components/viewer3d/ViewCube.tsx | 168 +++++++++++++++++--- src/components/viewer3d/Viewer3DSidebar.tsx | 90 +---------- 3 files changed, 236 insertions(+), 125 deletions(-) diff --git a/src/components/viewer3d/ModelViewer3D.tsx b/src/components/viewer3d/ModelViewer3D.tsx index 61fd9ee..e706a68 100644 --- a/src/components/viewer3d/ModelViewer3D.tsx +++ b/src/components/viewer3d/ModelViewer3D.tsx @@ -144,19 +144,55 @@ export const ModelViewer3D: React.FC = ({ scene.add(hemiLight); // 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 centerLineColor = isDarkMode ? 0x06b6d4 : 0x94a3b8; - const gridLineColor = isDarkMode ? 0x334155 : 0xe2e8f0; + const centerLineColor = isDarkMode ? 0x334155 : 0x94a3b8; + const gridLineColor = isDarkMode ? 0x0f172a : 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.6 : 0.5; + (grid.material as THREE.Material).opacity = isDarkMode ? 0.35 : 0.45; } scene.add(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 let animationFrameId: number; const animate = () => { @@ -191,6 +227,7 @@ export const ModelViewer3D: React.FC = ({ return () => { cancelAnimationFrame(animationFrameId); window.removeEventListener('resize', handleResize); + themeObserver.disconnect(); renderer.dispose(); }; }, []); @@ -366,17 +403,49 @@ export const ModelViewer3D: React.FC = ({ }); } - // Apply Opacity & Wireframe - if (Array.isArray(mesh.material)) { - mesh.material.forEach((m) => { - m.transparent = opacity < 100; - m.opacity = opacity / 100; - if ('wireframe' in m) (m as any).wireframe = isWireframe; - }); - } else if (mesh.material) { - mesh.material.transparent = opacity < 100; - mesh.material.opacity = opacity / 100; - if ('wireframe' in mesh.material) (mesh.material as any).wireframe = isWireframe; + // 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)) { + 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 = 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; + } } } }); @@ -566,16 +635,10 @@ export const ModelViewer3D: React.FC = ({ onToggleCamera={handleToggleCamera} isWireframe={isWireframe} onToggleWireframe={() => setIsWireframe(!isWireframe)} - navMode={navMode} - onNavModeChange={setNavMode} colorMode={colorMode} onColorModeChange={setColorMode} onFitView={() => fitModelToView()} onToggleFullscreen={handleToggleFullscreen} - onToggleSectionPlanes={() => setHasSectionPlanes(!hasSectionPlanes)} - onToggleMeasure={() => setIsMeasuring(!isMeasuring)} - hasSectionPlanes={hasSectionPlanes} - isMeasuring={isMeasuring} /> {/* Top Right: Interactive ViewCube */} diff --git a/src/components/viewer3d/ViewCube.tsx b/src/components/viewer3d/ViewCube.tsx index 90f843a..d17d35a 100644 --- a/src/components/viewer3d/ViewCube.tsx +++ b/src/components/viewer3d/ViewCube.tsx @@ -1,57 +1,185 @@ -import React from 'react'; +import React, { useState } from 'react'; +import { RotateCcw } from 'lucide-react'; interface ViewCubeProps { onSelectView: (view: 'top' | 'bottom' | 'front' | 'back' | 'left' | 'right' | 'iso') => void; } export const ViewCube: React.FC = ({ onSelectView }) => { + const [hoveredFace, setHoveredFace] = useState(null); + return ( -
-
-
- {/* Central Cube Simulation with Interactive Buttons */} -
+
+ {/* Container Principal translúcido idêntico ao SteelXR */} +
+ {/* Glow de Fundo */} +
+ + {/* Viewport 3D Isométrica do Cubo */} +
+ {/* Cubo 3D Rotacionado em Vista Isométrica Natural */} +
+ {/* FACE TOPO */} - + + {/* FACE FRENTE */} + + {/* FACE DIREITA (DIR) */} + + {/* FACE ESQUERDA (ESQ) */} + + + {/* FACE ATRÁS */} + + {/* FACE BASE (INFERIOR) */} +
+ + {/* Botão de Vista Isométrica Padrão (ISO) */} +
-
- Orientação 3D -
+ + {/* Rótulo Inferior Elegante */} + + Cubo de Orientação +
); }; diff --git a/src/components/viewer3d/Viewer3DSidebar.tsx b/src/components/viewer3d/Viewer3DSidebar.tsx index b5d4ed1..2ad9b47 100644 --- a/src/components/viewer3d/Viewer3DSidebar.tsx +++ b/src/components/viewer3d/Viewer3DSidebar.tsx @@ -2,21 +2,16 @@ import React, { useState } from 'react'; import { Eye, Grid as GridIcon, - Scissors, Maximize2, Box, Layers, Compass, - Footprints, - Ruler, Palette, CheckCircle2, ChevronLeft, ChevronRight, Focus, - Sliders, - Sparkles, - RotateCcw + Sliders } from 'lucide-react'; interface Viewer3DSidebarProps { @@ -28,16 +23,10 @@ interface Viewer3DSidebarProps { onToggleCamera: () => void; isWireframe: boolean; onToggleWireframe: () => void; - navMode: 'orbit' | 'walk'; - onNavModeChange: (mode: 'orbit' | 'walk') => void; colorMode: 'description' | 'production'; onColorModeChange: (mode: 'description' | 'production') => void; onFitView: () => void; onToggleFullscreen: () => void; - onToggleSectionPlanes?: () => void; - onToggleMeasure?: () => void; - hasSectionPlanes?: boolean; - isMeasuring?: boolean; } export const Viewer3DSidebar: React.FC = ({ @@ -49,16 +38,10 @@ export const Viewer3DSidebar: React.FC = ({ onToggleCamera, isWireframe, onToggleWireframe, - navMode, - onNavModeChange, colorMode, onColorModeChange, onFitView, onToggleFullscreen, - onToggleSectionPlanes, - onToggleMeasure, - hasSectionPlanes, - isMeasuring, }) => { const [isOpen, setIsOpen] = useState(true); @@ -67,7 +50,7 @@ export const Viewer3DSidebar: React.FC = ({ {/* Container Principal da Barra Lateral */}
{/* Cabeçalho do Sub-Menu */} @@ -142,11 +125,11 @@ export const Viewer3DSidebar: React.FC = ({ 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 ${ 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' }`} > - {isWireframe ? : } + {isWireframe ? : } {isWireframe ? 'Aramado' : 'Sólido'}
@@ -156,7 +139,7 @@ export const Viewer3DSidebar: React.FC = ({
{/* Câmera Toggle */} @@ -182,34 +165,6 @@ export const Viewer3DSidebar: React.FC = ({ Enquadrar
- -
- {/* Orbitar */} - - - {/* Caminhar */} - -
{/* Grupo 4: Opacidade da Estrutura */} @@ -237,41 +192,6 @@ export const Viewer3DSidebar: React.FC = ({
- {/* Grupo 5: Ferramentas Especiais */} -
- -
- {/* Cortes */} - - - {/* Medir */} - -
-
- {/* Botão Tela Cheia */}