feat(viewer3d): add collapsible sidebar submenu, ultra-fine light grid and high-contrast light mode typography
This commit is contained in:
@@ -3,7 +3,7 @@ import * as THREE from 'three';
|
|||||||
import { OrbitControls } from 'three-stdlib';
|
import { OrbitControls } from 'three-stdlib';
|
||||||
import { PieceInfo, LoadedIFCResult, getColorForMaterialName } from '@/lib/ifc/ifcLoaderService';
|
import { PieceInfo, LoadedIFCResult, getColorForMaterialName } from '@/lib/ifc/ifcLoaderService';
|
||||||
import { ViewCube } from './ViewCube';
|
import { ViewCube } from './ViewCube';
|
||||||
import { Viewer3DToolbar } from './Viewer3DToolbar';
|
import { Viewer3DSidebar } from './Viewer3DSidebar';
|
||||||
import { PieceHoverTooltip } from './PieceHoverTooltip';
|
import { PieceHoverTooltip } from './PieceHoverTooltip';
|
||||||
|
|
||||||
interface ProductionPieceStatus {
|
interface ProductionPieceStatus {
|
||||||
@@ -34,6 +34,7 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
|
|||||||
onSelectPiece,
|
onSelectPiece,
|
||||||
}) => {
|
}) => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const canvasContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const rendererRef = useRef<THREE.WebGLRenderer | null>(null);
|
const rendererRef = useRef<THREE.WebGLRenderer | null>(null);
|
||||||
const sceneRef = useRef<THREE.Scene | null>(null);
|
const sceneRef = useRef<THREE.Scene | null>(null);
|
||||||
const perspCameraRef = useRef<THREE.PerspectiveCamera | null>(null);
|
const perspCameraRef = useRef<THREE.PerspectiveCamera | null>(null);
|
||||||
@@ -74,10 +75,10 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
|
|||||||
|
|
||||||
// Initialize Scene
|
// Initialize Scene
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!containerRef.current) return;
|
if (!canvasContainerRef.current) return;
|
||||||
|
|
||||||
const width = containerRef.current.clientWidth || 800;
|
const width = canvasContainerRef.current.clientWidth || 800;
|
||||||
const height = containerRef.current.clientHeight || 600;
|
const height = canvasContainerRef.current.clientHeight || 600;
|
||||||
|
|
||||||
// 1. Scene
|
// 1. Scene
|
||||||
const scene = new THREE.Scene();
|
const scene = new THREE.Scene();
|
||||||
@@ -115,8 +116,8 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
|
|||||||
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
||||||
rendererRef.current = renderer;
|
rendererRef.current = renderer;
|
||||||
|
|
||||||
containerRef.current.innerHTML = '';
|
canvasContainerRef.current.innerHTML = '';
|
||||||
containerRef.current.appendChild(renderer.domElement);
|
canvasContainerRef.current.appendChild(renderer.domElement);
|
||||||
|
|
||||||
// 4. OrbitControls
|
// 4. OrbitControls
|
||||||
const controls = new OrbitControls(perspCamera, renderer.domElement);
|
const controls = new OrbitControls(perspCamera, renderer.domElement);
|
||||||
@@ -142,9 +143,17 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
|
|||||||
const hemiLight = new THREE.HemisphereLight(0xffffff, 0x1e293b, 0.5);
|
const hemiLight = new THREE.HemisphereLight(0xffffff, 0x1e293b, 0.5);
|
||||||
scene.add(hemiLight);
|
scene.add(hemiLight);
|
||||||
|
|
||||||
// 6. Floor Grid
|
// 6. Floor Grid (Cores refinadas e ultrafinas para modo claro e escuro)
|
||||||
const grid = new THREE.GridHelper(200, 100, 0x06b6d4, 0x1e293b);
|
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;
|
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);
|
scene.add(grid);
|
||||||
gridHelperRef.current = grid;
|
gridHelperRef.current = grid;
|
||||||
|
|
||||||
@@ -161,9 +170,9 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
|
|||||||
|
|
||||||
// 8. Resize Handler
|
// 8. Resize Handler
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
if (!containerRef.current || !rendererRef.current) return;
|
if (!canvasContainerRef.current || !rendererRef.current) return;
|
||||||
const w = containerRef.current.clientWidth;
|
const w = canvasContainerRef.current.clientWidth;
|
||||||
const h = containerRef.current.clientHeight;
|
const h = canvasContainerRef.current.clientHeight;
|
||||||
|
|
||||||
perspCamera.aspect = w / h;
|
perspCamera.aspect = w / h;
|
||||||
perspCamera.updateProjectionMatrix();
|
perspCamera.updateProjectionMatrix();
|
||||||
@@ -522,7 +531,8 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
|
|||||||
const mouseX = ((e.clientX - rect.left) / rect.width) * 2 - 1;
|
const mouseX = ((e.clientX - rect.left) / rect.width) * 2 - 1;
|
||||||
const mouseY = -((e.clientY - rect.top) / rect.height) * 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);
|
const intersects = raycasterRef.current.intersectObjects(modelGroupRef.current.children, true);
|
||||||
|
|
||||||
if (intersects.length > 0) {
|
if (intersects.length > 0) {
|
||||||
@@ -538,8 +548,36 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
|
|||||||
onPointerMove={handlePointerMove}
|
onPointerMove={handlePointerMove}
|
||||||
onPointerLeave={handlePointerLeave}
|
onPointerLeave={handlePointerLeave}
|
||||||
onDoubleClick={handleDoubleClick}
|
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 */}
|
||||||
|
<div ref={canvasContainerRef} className="absolute inset-0 w-full h-full z-0" />
|
||||||
|
|
||||||
|
{/* Top Left: Sub-Menu / Barra Lateral Retrátil de Ferramentas SteelXR */}
|
||||||
|
<Viewer3DSidebar
|
||||||
|
opacity={opacity}
|
||||||
|
onOpacityChange={setOpacity}
|
||||||
|
showGrid={showGrid}
|
||||||
|
onToggleGrid={() => {
|
||||||
|
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 */}
|
{/* Top Right: Interactive ViewCube */}
|
||||||
<div className="absolute top-4 right-4 z-20">
|
<div className="absolute top-4 right-4 z-20">
|
||||||
<ViewCube onSelectView={handleViewCubeSelect} />
|
<ViewCube onSelectView={handleViewCubeSelect} />
|
||||||
@@ -560,33 +598,6 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
|
|||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Bottom Floating Toolbar */}
|
|
||||||
<div className="absolute bottom-4 left-4 right-4 z-20 max-w-5xl mx-auto">
|
|
||||||
<Viewer3DToolbar
|
|
||||||
opacity={opacity}
|
|
||||||
onOpacityChange={setOpacity}
|
|
||||||
showGrid={showGrid}
|
|
||||||
onToggleGrid={() => {
|
|
||||||
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}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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<Viewer3DSidebarProps> = ({
|
||||||
|
opacity,
|
||||||
|
onOpacityChange,
|
||||||
|
showGrid,
|
||||||
|
onToggleGrid,
|
||||||
|
isOrthographic,
|
||||||
|
onToggleCamera,
|
||||||
|
isWireframe,
|
||||||
|
onToggleWireframe,
|
||||||
|
navMode,
|
||||||
|
onNavModeChange,
|
||||||
|
colorMode,
|
||||||
|
onColorModeChange,
|
||||||
|
onFitView,
|
||||||
|
onToggleFullscreen,
|
||||||
|
onToggleSectionPlanes,
|
||||||
|
onToggleMeasure,
|
||||||
|
hasSectionPlanes,
|
||||||
|
isMeasuring,
|
||||||
|
}) => {
|
||||||
|
const [isOpen, setIsOpen] = useState<boolean>(true);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="absolute top-4 left-4 z-30 flex items-start select-none">
|
||||||
|
{/* 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-16rem)]' : '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">
|
||||||
|
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">
|
||||||
|
SteelXR
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Corpo com Rolagem Suave */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-3 space-y-4 custom-scrollbar">
|
||||||
|
{/* Grupo 1: Modo de Cores / Produçã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">
|
||||||
|
<Palette className="w-3.5 h-3.5 text-cyan-600 dark:text-cyan-400" />
|
||||||
|
Exibição 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">
|
||||||
|
<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 ${
|
||||||
|
colorMode === 'production'
|
||||||
|
? 'bg-emerald-600 text-white shadow-md shadow-emerald-600/30'
|
||||||
|
: 'text-slate-700 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<CheckCircle2 className="w-3.5 h-3.5" />
|
||||||
|
<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 ${
|
||||||
|
colorMode === 'description'
|
||||||
|
? 'bg-amber-600 text-white shadow-md shadow-amber-600/30'
|
||||||
|
: 'text-slate-700 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Box className="w-3.5 h-3.5" />
|
||||||
|
<span>Padrão</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Grupo 2: Visibilidade & 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" />
|
||||||
|
Ambiente & Estrutura
|
||||||
|
</label>
|
||||||
|
<div className="grid grid-cols-2 gap-1.5">
|
||||||
|
{/* Toggle Grid */}
|
||||||
|
<button
|
||||||
|
onClick={onToggleGrid}
|
||||||
|
className={`flex items-center justify-center gap-1.5 px-2.5 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>Grid: {showGrid ? 'LIGADO' : 'DESL'}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Sólido / Aramado */}
|
||||||
|
<button
|
||||||
|
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-white dark:bg-slate-900 text-slate-700 dark:text-slate-400 border-slate-200 dark:border-slate-800 hover:border-slate-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isWireframe ? <Layers className="w-3.5 h-3.5" /> : <Box className="w-3.5 h-3.5" />}
|
||||||
|
<span>{isWireframe ? 'Aramado' : 'Sólido'}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Grupo 3: 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" />
|
||||||
|
Câmera & Navegação
|
||||||
|
</label>
|
||||||
|
<div className="grid grid-cols-2 gap-1.5">
|
||||||
|
{/* Câmera Toggle */}
|
||||||
|
<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 ${
|
||||||
|
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-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" />
|
||||||
|
<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"
|
||||||
|
title="Enquadrar / Centralizar Estrutura"
|
||||||
|
>
|
||||||
|
<Focus className="w-3.5 h-3.5 text-cyan-600 dark:text-cyan-400" />
|
||||||
|
<span>Enquadrar</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-1.5">
|
||||||
|
{/* Orbitar */}
|
||||||
|
<button
|
||||||
|
onClick={() => onNavModeChange('orbit')}
|
||||||
|
className={`flex items-center justify-center gap-1.5 px-2 py-1.5 rounded-lg text-xs font-semibold border transition-all ${
|
||||||
|
navMode === 'orbit'
|
||||||
|
? 'bg-cyan-50 dark:bg-cyan-950/40 text-cyan-800 dark:text-cyan-300 border-cyan-300 dark:border-cyan-800'
|
||||||
|
: 'bg-white dark:bg-slate-900 text-slate-700 dark:text-slate-400 border-slate-200 dark:border-slate-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Compass className="w-3.5 h-3.5" />
|
||||||
|
<span>Orbitar</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Caminhar */}
|
||||||
|
<button
|
||||||
|
onClick={() => onNavModeChange('walk')}
|
||||||
|
className={`flex items-center justify-center gap-1.5 px-2 py-1.5 rounded-lg text-xs font-semibold border transition-all ${
|
||||||
|
navMode === 'walk'
|
||||||
|
? 'bg-cyan-50 dark:bg-cyan-950/40 text-cyan-800 dark:text-cyan-300 border-cyan-300 dark:border-cyan-800'
|
||||||
|
: 'bg-white dark:bg-slate-900 text-slate-700 dark:text-slate-400 border-slate-200 dark:border-slate-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Footprints className="w-3.5 h-3.5" />
|
||||||
|
<span>Caminhar</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Grupo 4: 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" />
|
||||||
|
Opacidade
|
||||||
|
</span>
|
||||||
|
<span className="font-mono font-bold text-amber-700 dark:text-amber-400">{opacity}%</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min={10}
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
<div className="flex justify-between text-[10px] font-mono text-slate-400">
|
||||||
|
<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>
|
||||||
|
<button onClick={() => onOpacityChange(100)} className="hover:text-amber-600">100%</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Grupo 5: Ferramentas Especiais */}
|
||||||
|
<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">
|
||||||
|
<Sparkles className="w-3.5 h-3.5 text-cyan-600 dark:text-cyan-400" />
|
||||||
|
Ferramentas SteelXR
|
||||||
|
</label>
|
||||||
|
<div className="grid grid-cols-2 gap-1.5">
|
||||||
|
{/* Cortes */}
|
||||||
|
<button
|
||||||
|
onClick={onToggleSectionPlanes}
|
||||||
|
className={`flex items-center justify-center gap-1.5 px-2.5 py-2 rounded-xl text-xs font-semibold border transition-all ${
|
||||||
|
hasSectionPlanes
|
||||||
|
? 'bg-rose-50 dark:bg-rose-950/40 text-rose-800 dark:text-rose-300 border-rose-300 dark:border-rose-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'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Scissors className="w-3.5 h-3.5" />
|
||||||
|
<span>Cortes</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Medir */}
|
||||||
|
<button
|
||||||
|
onClick={onToggleMeasure}
|
||||||
|
className={`flex items-center justify-center gap-1.5 px-2.5 py-2 rounded-xl text-xs font-semibold border transition-all ${
|
||||||
|
isMeasuring
|
||||||
|
? 'bg-amber-50 dark:bg-amber-950/40 text-amber-800 dark:text-amber-300 border-amber-300 dark:border-amber-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'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Ruler className="w-3.5 h-3.5" />
|
||||||
|
<span>Medir</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Botão Tela Cheia */}
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
<Maximize2 className="w-3.5 h-3.5 text-cyan-600 dark:text-cyan-400" />
|
||||||
|
<span>Expandir Tela Cheia</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Botão de Abrir / Fechar Sub-Menu Lateral */}
|
||||||
|
<button
|
||||||
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
|
className="ml-2 p-2 bg-white/95 dark:bg-slate-900/95 hover:bg-slate-100 dark:hover:bg-slate-800 text-slate-700 dark:text-slate-200 border border-slate-200 dark:border-slate-800 rounded-xl shadow-lg transition-all"
|
||||||
|
title={isOpen ? 'Recolher Menu Lateral' : 'Expandir Menu Lateral 3D'}
|
||||||
|
>
|
||||||
|
{isOpen ? <ChevronLeft className="w-4 h-4" /> : <ChevronRight className="w-4 h-4" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -486,15 +486,15 @@ export default function Visualizador3D() {
|
|||||||
|
|
||||||
{/* OF Selector Dropdown */}
|
{/* OF Selector Dropdown */}
|
||||||
<div className="flex items-center gap-2 bg-slate-100 dark:bg-slate-950/80 px-3 py-1.5 rounded-xl border border-slate-200 dark:border-slate-800">
|
<div className="flex items-center gap-2 bg-slate-100 dark:bg-slate-950/80 px-3 py-1.5 rounded-xl border border-slate-200 dark:border-slate-800">
|
||||||
<span className="text-xs font-semibold text-slate-700 font-bold dark:text-slate-300 dark:font-medium">OF:</span>
|
<span className="text-xs font-semibold text-slate-700 dark:text-slate-300">OF:</span>
|
||||||
<select
|
<select
|
||||||
value={selectedOF}
|
value={selectedOF}
|
||||||
onChange={(e) => setSelectedOF(e.target.value)}
|
onChange={(e) => setSelectedOF(e.target.value)}
|
||||||
className="bg-transparent text-sm font-bold text-cyan-300 focus:outline-none cursor-pointer"
|
className="bg-transparent text-sm font-extrabold text-cyan-900 dark:text-cyan-300 focus:outline-none cursor-pointer"
|
||||||
>
|
>
|
||||||
<option value="" disabled className="text-slate-500">Selecione a OF...</option>
|
<option value="" disabled className="text-slate-500">Selecione a OF...</option>
|
||||||
{ofs.map((ofItem) => (
|
{ofs.map((ofItem) => (
|
||||||
<option key={ofItem.id} value={ofItem.of_number} className="bg-white dark:bg-slate-900 text-slate-900 dark:text-white">
|
<option key={ofItem.id} value={ofItem.of_number} className="bg-white dark:bg-slate-900 text-slate-900 dark:text-white font-medium">
|
||||||
{ofItem.of_number} {ofItem.descritivo ? `— ${ofItem.descritivo}` : ''}
|
{ofItem.of_number} {ofItem.descritivo ? `— ${ofItem.descritivo}` : ''}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
@@ -503,10 +503,10 @@ export default function Visualizador3D() {
|
|||||||
|
|
||||||
{/* Cloud Stored Model Badge */}
|
{/* Cloud Stored Model Badge */}
|
||||||
{hasSavedModel && (
|
{hasSavedModel && (
|
||||||
<div className="flex items-center gap-2 px-3 py-1.5 bg-emerald-500/10 border border-emerald-500/30 rounded-xl text-xs">
|
<div className="flex items-center gap-2 px-3 py-1.5 bg-emerald-100/80 dark:bg-emerald-500/10 border border-emerald-300 dark:border-emerald-500/30 rounded-xl text-xs shadow-sm">
|
||||||
<Cloud className="w-3.5 h-3.5 text-emerald-400" />
|
<Cloud className="w-3.5 h-3.5 text-emerald-700 dark:text-emerald-400" />
|
||||||
<span
|
<span
|
||||||
className="text-emerald-300 font-mono font-medium max-w-[170px] truncate"
|
className="text-emerald-900 dark:text-emerald-300 font-mono font-bold max-w-[170px] truncate"
|
||||||
title={currentOFInfo?.ifc_filename || 'Modelo IFC Vinculado'}
|
title={currentOFInfo?.ifc_filename || 'Modelo IFC Vinculado'}
|
||||||
>
|
>
|
||||||
{currentOFInfo?.ifc_filename || 'Modelo Salvo'}
|
{currentOFInfo?.ifc_filename || 'Modelo Salvo'}
|
||||||
@@ -514,7 +514,7 @@ export default function Visualizador3D() {
|
|||||||
<button
|
<button
|
||||||
onClick={handleRemoveSavedModel}
|
onClick={handleRemoveSavedModel}
|
||||||
disabled={isLoadingModel}
|
disabled={isLoadingModel}
|
||||||
className="text-slate-400 hover:text-red-400 transition-colors ml-1 p-0.5 rounded"
|
className="text-slate-500 dark:text-slate-400 hover:text-red-600 dark:hover:text-red-400 transition-colors ml-1 p-0.5 rounded cursor-pointer"
|
||||||
title="Desvincular modelo desta OF"
|
title="Desvincular modelo desta OF"
|
||||||
>
|
>
|
||||||
<Trash2 className="w-3.5 h-3.5" />
|
<Trash2 className="w-3.5 h-3.5" />
|
||||||
@@ -529,7 +529,7 @@ export default function Visualizador3D() {
|
|||||||
onClick={() => setSelectedPhase('all')}
|
onClick={() => setSelectedPhase('all')}
|
||||||
className={`px-2.5 py-1 rounded-lg text-xs font-medium transition-all ${
|
className={`px-2.5 py-1 rounded-lg text-xs font-medium transition-all ${
|
||||||
selectedPhase === 'all'
|
selectedPhase === 'all'
|
||||||
? 'bg-cyan-500 text-white dark:text-slate-950 font-bold shadow-md shadow-cyan-500/30'
|
? 'bg-cyan-600 text-white dark:bg-cyan-500 dark:text-slate-950 font-bold shadow-md shadow-cyan-600/30'
|
||||||
: 'text-slate-800 font-medium dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-200'
|
: 'text-slate-800 font-medium dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-200'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -541,7 +541,7 @@ export default function Visualizador3D() {
|
|||||||
onClick={() => setSelectedPhase(f)}
|
onClick={() => setSelectedPhase(f)}
|
||||||
className={`px-2.5 py-1 rounded-lg text-xs font-medium transition-all ${
|
className={`px-2.5 py-1 rounded-lg text-xs font-medium transition-all ${
|
||||||
selectedPhase === f
|
selectedPhase === f
|
||||||
? 'bg-cyan-500 text-white dark:text-slate-950 font-bold shadow-md shadow-cyan-500/30'
|
? 'bg-cyan-600 text-white dark:bg-cyan-500 dark:text-slate-950 font-bold shadow-md shadow-cyan-600/30'
|
||||||
: 'text-slate-800 font-medium dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-200'
|
: 'text-slate-800 font-medium dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-200'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -557,10 +557,10 @@ export default function Visualizador3D() {
|
|||||||
{auditData && (
|
{auditData && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsAuditModalOpen(true)}
|
onClick={() => setIsAuditModalOpen(true)}
|
||||||
className="flex items-center gap-2 px-3.5 py-2 bg-emerald-500/10 hover:bg-emerald-500/20 text-emerald-400 border border-emerald-500/30 rounded-xl text-xs font-semibold transition-all shadow"
|
className="flex items-center gap-2 px-3.5 py-2 bg-emerald-100/70 hover:bg-emerald-100 dark:bg-emerald-500/10 dark:hover:bg-emerald-500/20 text-emerald-800 dark:text-emerald-400 border border-emerald-300 dark:border-emerald-500/30 rounded-xl text-xs font-bold transition-all shadow"
|
||||||
title="Ver detalhes da auditoria do IFC"
|
title="Ver detalhes da auditoria do IFC"
|
||||||
>
|
>
|
||||||
<ShieldCheck className="w-4 h-4" />
|
<ShieldCheck className="w-4 h-4 text-emerald-700 dark:text-emerald-400" />
|
||||||
<span>Auditoria: {auditData.qualityScore}</span>
|
<span>Auditoria: {auditData.qualityScore}</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
@@ -576,7 +576,7 @@ export default function Visualizador3D() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => fileInputRef.current?.click()}
|
onClick={() => fileInputRef.current?.click()}
|
||||||
disabled={isLoadingModel}
|
disabled={isLoadingModel}
|
||||||
className="flex items-center gap-2 px-4 py-2 bg-gradient-to-r from-cyan-500 to-blue-600 hover:from-cyan-400 hover:to-blue-500 text-white dark:text-slate-950 font-bold text-xs rounded-xl shadow-lg shadow-cyan-500/20 transition-all disabled:opacity-50 cursor-pointer"
|
className="flex items-center gap-2 px-4 py-2 bg-gradient-to-r from-cyan-600 to-blue-600 hover:from-cyan-500 hover:to-blue-500 text-white font-bold text-xs rounded-xl shadow-lg shadow-cyan-600/20 transition-all disabled:opacity-50 cursor-pointer"
|
||||||
>
|
>
|
||||||
{isLoadingModel ? (
|
{isLoadingModel ? (
|
||||||
<>
|
<>
|
||||||
@@ -597,50 +597,50 @@ export default function Visualizador3D() {
|
|||||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
||||||
<div className="bg-white dark:bg-slate-900/80 border border-slate-200 dark:border-slate-800 p-3 rounded-xl flex items-center justify-between shadow-sm dark:shadow-none">
|
<div className="bg-white dark:bg-slate-900/80 border border-slate-200 dark:border-slate-800 p-3 rounded-xl flex items-center justify-between shadow-sm dark:shadow-none">
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[10px] font-medium uppercase tracking-wider text-slate-700 font-bold dark:text-slate-300 dark:font-medium block">
|
<span className="text-[10px] font-bold uppercase tracking-wider text-slate-600 dark:text-slate-400 block">
|
||||||
Total de Peças {activeMetrics.phaseLabel ? `(${activeMetrics.phaseLabel})` : ''}
|
Total de Peças {activeMetrics.phaseLabel ? `(${activeMetrics.phaseLabel})` : ''}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-lg font-mono font-extrabold text-slate-900 dark:text-white">
|
<span className="text-lg font-mono font-extrabold text-slate-900 dark:text-white">
|
||||||
{activeMetrics.totalPecas} peças
|
{activeMetrics.totalPecas} peças
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Layers className="w-5 h-5 text-cyan-400" />
|
<Layers className="w-5 h-5 text-cyan-600 dark:text-cyan-400" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white dark:bg-slate-900/80 border border-slate-200 dark:border-slate-800 p-3 rounded-xl flex items-center justify-between shadow-sm dark:shadow-none">
|
<div className="bg-white dark:bg-slate-900/80 border border-slate-200 dark:border-slate-800 p-3 rounded-xl flex items-center justify-between shadow-sm dark:shadow-none">
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[10px] font-medium uppercase tracking-wider text-slate-700 font-bold dark:text-slate-300 dark:font-medium block">
|
<span className="text-[10px] font-bold uppercase tracking-wider text-slate-600 dark:text-slate-400 block">
|
||||||
Apontamentos Realizados {activeMetrics.phaseLabel ? `(${activeMetrics.phaseLabel})` : ''}
|
Apontamentos Realizados {activeMetrics.phaseLabel ? `(${activeMetrics.phaseLabel})` : ''}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-lg font-mono font-extrabold text-emerald-400">
|
<span className="text-lg font-mono font-extrabold text-emerald-700 dark:text-emerald-400">
|
||||||
{activeMetrics.apontadas} apontadas
|
{activeMetrics.apontadas} apontadas
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<CheckCircle2 className="w-5 h-5 text-emerald-400" />
|
<CheckCircle2 className="w-5 h-5 text-emerald-600 dark:text-emerald-400" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white dark:bg-slate-900/80 border border-slate-200 dark:border-slate-800 p-3 rounded-xl flex items-center justify-between shadow-sm dark:shadow-none">
|
<div className="bg-white dark:bg-slate-900/80 border border-slate-200 dark:border-slate-800 p-3 rounded-xl flex items-center justify-between shadow-sm dark:shadow-none">
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[10px] font-medium uppercase tracking-wider text-slate-700 font-bold dark:text-slate-300 dark:font-medium block">
|
<span className="text-[10px] font-bold uppercase tracking-wider text-slate-600 dark:text-slate-400 block">
|
||||||
Evolução {activeMetrics.phaseLabel ? `(${activeMetrics.phaseLabel})` : 'da OF'}
|
Evolução {activeMetrics.phaseLabel ? `(${activeMetrics.phaseLabel})` : 'da OF'}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-lg font-mono font-extrabold text-amber-400">
|
<span className="text-lg font-mono font-extrabold text-amber-600 dark:text-amber-400">
|
||||||
{activeMetrics.progresso}% concluído
|
{activeMetrics.progresso}% concluído
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<TrendingUp className="w-5 h-5 text-amber-400" />
|
<TrendingUp className="w-5 h-5 text-amber-600 dark:text-amber-400" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white dark:bg-slate-900/80 border border-slate-200 dark:border-slate-800 p-3 rounded-xl flex items-center justify-between shadow-sm dark:shadow-none">
|
<div className="bg-white dark:bg-slate-900/80 border border-slate-200 dark:border-slate-800 p-3 rounded-xl flex items-center justify-between shadow-sm dark:shadow-none">
|
||||||
<div>
|
<div>
|
||||||
<span className="text-[10px] font-medium uppercase tracking-wider text-slate-700 font-bold dark:text-slate-300 dark:font-medium block">
|
<span className="text-[10px] font-bold uppercase tracking-wider text-slate-600 dark:text-slate-400 block">
|
||||||
Peso {activeMetrics.phaseLabel ? `(${activeMetrics.phaseLabel})` : 'da Estrutura'}
|
Peso {activeMetrics.phaseLabel ? `(${activeMetrics.phaseLabel})` : 'da Estrutura'}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-lg font-mono font-extrabold text-cyan-300">
|
<span className="text-lg font-mono font-extrabold text-cyan-800 dark:text-cyan-300">
|
||||||
{activeMetrics.peso > 0 ? `${(activeMetrics.peso / 1000).toFixed(1)} ton` : '0.0 ton'}
|
{activeMetrics.peso > 0 ? `${(activeMetrics.peso / 1000).toFixed(1)} ton` : '0.0 ton'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Weight className="w-5 h-5 text-cyan-400" />
|
<Weight className="w-5 h-5 text-cyan-600 dark:text-cyan-400" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user