From d003309481d57bb1905da408e93f761a34127603 Mon Sep 17 00:00:00 2001 From: Marcos Reifonas Date: Mon, 21 Sep 2026 01:13:46 +0000 Subject: [PATCH] feat(viewer3d): add collapsible sidebar submenu, ultra-fine light grid and high-contrast light mode typography --- src/components/viewer3d/ModelViewer3D.tsx | 91 +++--- src/components/viewer3d/Viewer3DSidebar.tsx | 296 ++++++++++++++++++++ src/pages/Visualizador3D.tsx | 46 +-- 3 files changed, 370 insertions(+), 63 deletions(-) create mode 100644 src/components/viewer3d/Viewer3DSidebar.tsx diff --git a/src/components/viewer3d/ModelViewer3D.tsx b/src/components/viewer3d/ModelViewer3D.tsx index 0501808..61fd9ee 100644 --- a/src/components/viewer3d/ModelViewer3D.tsx +++ b/src/components/viewer3d/ModelViewer3D.tsx @@ -3,7 +3,7 @@ import * as THREE from 'three'; import { OrbitControls } from 'three-stdlib'; import { PieceInfo, LoadedIFCResult, getColorForMaterialName } from '@/lib/ifc/ifcLoaderService'; import { ViewCube } from './ViewCube'; -import { Viewer3DToolbar } from './Viewer3DToolbar'; +import { Viewer3DSidebar } from './Viewer3DSidebar'; import { PieceHoverTooltip } from './PieceHoverTooltip'; interface ProductionPieceStatus { @@ -34,6 +34,7 @@ export const ModelViewer3D: React.FC = ({ onSelectPiece, }) => { const containerRef = useRef(null); + const canvasContainerRef = useRef(null); const rendererRef = useRef(null); const sceneRef = useRef(null); const perspCameraRef = useRef(null); @@ -74,10 +75,10 @@ export const ModelViewer3D: React.FC = ({ // Initialize Scene useEffect(() => { - if (!containerRef.current) return; + if (!canvasContainerRef.current) return; - const width = containerRef.current.clientWidth || 800; - const height = containerRef.current.clientHeight || 600; + const width = canvasContainerRef.current.clientWidth || 800; + const height = canvasContainerRef.current.clientHeight || 600; // 1. Scene const scene = new THREE.Scene(); @@ -115,8 +116,8 @@ export const ModelViewer3D: React.FC = ({ renderer.outputColorSpace = THREE.SRGBColorSpace; rendererRef.current = renderer; - containerRef.current.innerHTML = ''; - containerRef.current.appendChild(renderer.domElement); + canvasContainerRef.current.innerHTML = ''; + canvasContainerRef.current.appendChild(renderer.domElement); // 4. OrbitControls const controls = new OrbitControls(perspCamera, renderer.domElement); @@ -142,9 +143,17 @@ export const ModelViewer3D: React.FC = ({ const hemiLight = new THREE.HemisphereLight(0xffffff, 0x1e293b, 0.5); scene.add(hemiLight); - // 6. Floor Grid - const grid = new THREE.GridHelper(200, 100, 0x06b6d4, 0x1e293b); + // 6. Floor Grid (Cores refinadas e ultrafinas para modo claro e escuro) + const isDarkMode = document.documentElement.classList.contains('dark'); + const centerLineColor = isDarkMode ? 0x06b6d4 : 0x94a3b8; + const gridLineColor = isDarkMode ? 0x334155 : 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; + } scene.add(grid); gridHelperRef.current = grid; @@ -161,9 +170,9 @@ export const ModelViewer3D: React.FC = ({ // 8. Resize Handler const handleResize = () => { - if (!containerRef.current || !rendererRef.current) return; - const w = containerRef.current.clientWidth; - const h = containerRef.current.clientHeight; + if (!canvasContainerRef.current || !rendererRef.current) return; + const w = canvasContainerRef.current.clientWidth; + const h = canvasContainerRef.current.clientHeight; perspCamera.aspect = w / h; perspCamera.updateProjectionMatrix(); @@ -522,7 +531,8 @@ export const ModelViewer3D: React.FC = ({ const mouseX = ((e.clientX - rect.left) / rect.width) * 2 - 1; const mouseY = -((e.clientY - rect.top) / rect.height) * 2 + 1; - raycasterRef.current.setFromCamera({ x: mouseX, y: mouseY }, activeCameraRef.current); + const v = new THREE.Vector2(mouseX, mouseY); + raycasterRef.current.setFromCamera(v, activeCameraRef.current); const intersects = raycasterRef.current.intersectObjects(modelGroupRef.current.children, true); if (intersects.length > 0) { @@ -538,8 +548,36 @@ export const ModelViewer3D: React.FC = ({ onPointerMove={handlePointerMove} onPointerLeave={handlePointerLeave} onDoubleClick={handleDoubleClick} - className="relative w-full h-full bg-slate-50 dark:bg-slate-950 rounded-2xl overflow-hidden border border-slate-200 dark:border-slate-800 shadow-xl dark:shadow-2xl select-none" + className="relative w-full h-full bg-slate-50 dark:bg-slate-950 rounded-2xl overflow-hidden border border-slate-200 dark:border-slate-800 shadow-xl dark:shadow-2xl select-none" > + {/* Three.js Canvas Container Dedicado */} +
+ + {/* Top Left: Sub-Menu / Barra Lateral Retrátil de Ferramentas SteelXR */} + { + setShowGrid(!showGrid); + if (gridHelperRef.current) gridHelperRef.current.visible = !showGrid; + }} + isOrthographic={isOrthographic} + 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 */}
@@ -560,33 +598,6 @@ export const ModelViewer3D: React.FC = ({ : undefined } /> - - {/* Bottom Floating Toolbar */} -
- { - setShowGrid(!showGrid); - if (gridHelperRef.current) gridHelperRef.current.visible = !showGrid; - }} - isOrthographic={isOrthographic} - 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} - /> -
); }; diff --git a/src/components/viewer3d/Viewer3DSidebar.tsx b/src/components/viewer3d/Viewer3DSidebar.tsx new file mode 100644 index 0000000..b5d4ed1 --- /dev/null +++ b/src/components/viewer3d/Viewer3DSidebar.tsx @@ -0,0 +1,296 @@ +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 +} from 'lucide-react'; + +interface Viewer3DSidebarProps { + opacity: number; + onOpacityChange: (val: number) => void; + showGrid: boolean; + onToggleGrid: () => void; + isOrthographic: boolean; + 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 = ({ + opacity, + onOpacityChange, + showGrid, + onToggleGrid, + isOrthographic, + onToggleCamera, + isWireframe, + onToggleWireframe, + navMode, + onNavModeChange, + colorMode, + onColorModeChange, + onFitView, + onToggleFullscreen, + onToggleSectionPlanes, + onToggleMeasure, + hasSectionPlanes, + isMeasuring, +}) => { + const [isOpen, setIsOpen] = useState(true); + + return ( +
+ {/* Container Principal da Barra Lateral */} +
+ {/* Cabeçalho do Sub-Menu */} +
+
+ + + Controles 3D + +
+ + SteelXR + +
+ + {/* Corpo com Rolagem Suave */} +
+ {/* Grupo 1: Modo de Cores / Produção */} +
+ +
+ + +
+
+ + {/* Grupo 2: Visibilidade & Grade */} +
+ +
+ {/* Toggle Grid */} + + + {/* Sólido / Aramado */} + +
+
+ + {/* Grupo 3: Câmera & Projeção */} +
+ +
+ {/* Câmera Toggle */} + + + {/* Enquadrar / Reset View */} + +
+ +
+ {/* Orbitar */} + + + {/* Caminhar */} + +
+
+ + {/* Grupo 4: Opacidade da Estrutura */} +
+
+ + + Opacidade + + {opacity}% +
+ 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" + /> +
+ + + + +
+
+ + {/* Grupo 5: Ferramentas Especiais */} +
+ +
+ {/* Cortes */} + + + {/* Medir */} + +
+
+ + {/* Botão Tela Cheia */} + +
+
+ + {/* Botão de Abrir / Fechar Sub-Menu Lateral */} + +
+ ); +}; diff --git a/src/pages/Visualizador3D.tsx b/src/pages/Visualizador3D.tsx index 5c1449f..f447d60 100644 --- a/src/pages/Visualizador3D.tsx +++ b/src/pages/Visualizador3D.tsx @@ -486,15 +486,15 @@ export default function Visualizador3D() { {/* OF Selector Dropdown */}
- OF: + OF: