feat(viewer3d): hybrid process visualization (green solid + grey ghost frame), 70% synced WebGL ViewCube, dark grid fine-tuning, and industrial grey wireframe
This commit is contained in:
@@ -51,10 +51,8 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
|
|||||||
const [showGrid, setShowGrid] = useState<boolean>(true);
|
const [showGrid, setShowGrid] = useState<boolean>(true);
|
||||||
const [isOrthographic, setIsOrthographic] = useState<boolean>(false);
|
const [isOrthographic, setIsOrthographic] = useState<boolean>(false);
|
||||||
const [isWireframe, setIsWireframe] = useState<boolean>(false);
|
const [isWireframe, setIsWireframe] = useState<boolean>(false);
|
||||||
const [navMode, setNavMode] = useState<'orbit' | 'walk'>('orbit');
|
|
||||||
const [colorMode, setColorMode] = useState<'description' | 'production'>('production');
|
const [colorMode, setColorMode] = useState<'description' | 'production'>('production');
|
||||||
const [hasSectionPlanes, setHasSectionPlanes] = useState<boolean>(false);
|
const [selectedProcess, setSelectedProcess] = useState<string>('all');
|
||||||
const [isMeasuring, setIsMeasuring] = useState<boolean>(false);
|
|
||||||
|
|
||||||
// Hover Tooltip States (1.5 seconds)
|
// Hover Tooltip States (1.5 seconds)
|
||||||
const hoverTimerRef = useRef<NodeJS.Timeout | null>(null);
|
const hoverTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
@@ -146,14 +144,14 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
|
|||||||
// 6. Floor Grid (Cores refinadas e ultrafinas para modo claro e escuro)
|
// 6. Floor Grid (Cores refinadas e ultrafinas para modo claro e escuro)
|
||||||
const updateGridTheme = (gridHelper: THREE.GridHelper) => {
|
const updateGridTheme = (gridHelper: THREE.GridHelper) => {
|
||||||
const isDark = document.documentElement.classList.contains('dark');
|
const isDark = document.documentElement.classList.contains('dark');
|
||||||
const centerColor = isDark ? new THREE.Color(0x334155) : new THREE.Color(0x94a3b8);
|
// No modo escuro: tom ligeiramente mais claro (slate-800 #1e293b e centro #475569)
|
||||||
const gridColor = isDark ? new THREE.Color(0x0f172a) : new THREE.Color(0xe2e8f0);
|
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);
|
const colors = (gridHelper.geometry.attributes.color as THREE.BufferAttribute);
|
||||||
if (colors) {
|
if (colors) {
|
||||||
const colorArray = colors.array as Float32Array;
|
const colorArray = colors.array as Float32Array;
|
||||||
for (let i = 0; i < colorArray.length; i += 6) {
|
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 isCenter = i === Math.floor(colorArray.length / 2);
|
||||||
const c = isCenter ? centerColor : gridColor;
|
const c = isCenter ? centerColor : gridColor;
|
||||||
colorArray[i] = c.r;
|
colorArray[i] = c.r;
|
||||||
@@ -168,19 +166,19 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
|
|||||||
|
|
||||||
if (gridHelper.material && (gridHelper.material as THREE.Material)) {
|
if (gridHelper.material && (gridHelper.material as THREE.Material)) {
|
||||||
(gridHelper.material as THREE.Material).transparent = true;
|
(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 isDarkMode = document.documentElement.classList.contains('dark');
|
||||||
const centerLineColor = isDarkMode ? 0x334155 : 0x94a3b8;
|
const centerLineColor = isDarkMode ? 0x475569 : 0x94a3b8;
|
||||||
const gridLineColor = isDarkMode ? 0x0f172a : 0xe2e8f0;
|
const gridLineColor = isDarkMode ? 0x1e293b : 0xe2e8f0;
|
||||||
|
|
||||||
const grid = new THREE.GridHelper(300, 150, centerLineColor, gridLineColor);
|
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)) {
|
if (grid.material && (grid.material as THREE.Material)) {
|
||||||
(grid.material as THREE.Material).transparent = true;
|
(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);
|
scene.add(grid);
|
||||||
gridHelperRef.current = grid;
|
gridHelperRef.current = grid;
|
||||||
@@ -354,34 +352,111 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
|
|||||||
// Se o elemento não estiver visível na fase atual, pula estilização detalhada
|
// Se o elemento não estiver visível na fase atual, pula estilização detalhada
|
||||||
if (!isVisibleByPhase) return;
|
if (!isVisibleByPhase) return;
|
||||||
|
|
||||||
// 4. Aplicação de Cores / Materiais
|
// 4. Mapeamento de Processos Industriais para Inovação Híbrida
|
||||||
if (colorMode === 'production' && productionData && productionData.size > 0) {
|
const PROCESS_ORDER_MAP: Record<string, number> = {
|
||||||
if (prod) {
|
'corte': 1,
|
||||||
matchedCount++;
|
'montagem': 2,
|
||||||
if (!sampleMatch && prod.pointedQtd > 0) {
|
'solda': 3,
|
||||||
sampleMatch = { pmark: assMark || pmark, dbMarca: prod.marca, proc: prod.currentProcessName, cor: prod.processColor };
|
'pintura': 4,
|
||||||
}
|
'pintura/galv': 4,
|
||||||
}
|
'expedicao': 5,
|
||||||
|
'expedição': 5,
|
||||||
|
};
|
||||||
|
|
||||||
if (prod && prod.pointedQtd > 0) {
|
const targetProcKey = selectedProcess.toLowerCase();
|
||||||
pointedCount++;
|
const targetOrder = PROCESS_ORDER_MAP[targetProcKey] || 0;
|
||||||
// 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
|
let isPointedInTargetProcess = false;
|
||||||
const colorDark = new THREE.Color('#14532d'); // Verde escuro
|
if (selectedProcess !== 'all' && prod && prod.pointedQtd > 0) {
|
||||||
const finalColor = new THREE.Color().lerpColors(colorLight, colorDark, percent);
|
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({
|
mesh.material = new THREE.MeshStandardMaterial({
|
||||||
color: finalColor,
|
color: new THREE.Color('#22c55e'), // Verde Sólido
|
||||||
metalness: 0.35,
|
metalness: 0.35,
|
||||||
roughness: 0.45,
|
roughness: 0.45,
|
||||||
|
transparent: opacity < 100,
|
||||||
|
opacity: opacity / 100,
|
||||||
side: THREE.DoubleSide,
|
side: THREE.DoubleSide,
|
||||||
});
|
});
|
||||||
|
if (mesh.userData.edgesLine) {
|
||||||
|
mesh.userData.edgesLine.visible = false;
|
||||||
|
}
|
||||||
} else {
|
} 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({
|
mesh.material = new THREE.MeshStandardMaterial({
|
||||||
color: new THREE.Color(0xa1a1aa),
|
color: new THREE.Color(0xa1a1aa),
|
||||||
metalness: 0.25,
|
metalness: 0.25,
|
||||||
@@ -391,60 +466,50 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
|
|||||||
side: THREE.DoubleSide,
|
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)
|
// Controle de Aramado Geral (Botão Sólido / Aramado)
|
||||||
if (isWireframe) {
|
if (isWireframe) {
|
||||||
// Cria arestas de contorno se ainda não existirem
|
if (!mesh.userData.edgesLine) {
|
||||||
if (!mesh.userData.edgesLine) {
|
const edgesGeo = new THREE.EdgesGeometry(mesh.geometry, 24);
|
||||||
const edgesGeo = new THREE.EdgesGeometry(mesh.geometry, 24);
|
const lineMat = new THREE.LineBasicMaterial({
|
||||||
const lineMat = new THREE.LineBasicMaterial({
|
color: 0x94a3b8, // Cinza claro industrial idêntico às peças não apontadas
|
||||||
color: 0x00ffff,
|
transparent: true,
|
||||||
transparent: true,
|
opacity: 0.85,
|
||||||
opacity: 0.95,
|
});
|
||||||
});
|
const edgesLine = new THREE.LineSegments(edgesGeo, lineMat);
|
||||||
const edgesLine = new THREE.LineSegments(edgesGeo, lineMat);
|
mesh.userData.edgesLine = edgesLine;
|
||||||
mesh.userData.edgesLine = edgesLine;
|
mesh.add(edgesLine);
|
||||||
mesh.add(edgesLine);
|
} else {
|
||||||
}
|
if (mesh.userData.edgesLine.material) {
|
||||||
mesh.userData.edgesLine.visible = true;
|
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)) {
|
||||||
if (Array.isArray(mesh.material)) {
|
mesh.material.forEach((m) => { m.visible = false; });
|
||||||
mesh.material.forEach((m) => {
|
} else if (mesh.material) {
|
||||||
m.visible = false;
|
mesh.material.visible = false;
|
||||||
});
|
}
|
||||||
} else if (mesh.material) {
|
} else {
|
||||||
mesh.material.visible = false;
|
if (mesh.userData.edgesLine) {
|
||||||
}
|
mesh.userData.edgesLine.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)) {
|
if (Array.isArray(mesh.material)) {
|
||||||
mesh.material.forEach((m) => {
|
mesh.material.forEach((m) => {
|
||||||
m.visible = true;
|
m.visible = true;
|
||||||
m.transparent = opacity < 100;
|
m.transparent = opacity < 100;
|
||||||
m.opacity = opacity / 100;
|
m.opacity = opacity / 100;
|
||||||
if ('wireframe' in m) (m as any).wireframe = false;
|
if ('wireframe' in m) (m as any).wireframe = false;
|
||||||
});
|
});
|
||||||
} else if (mesh.material) {
|
} else if (mesh.material) {
|
||||||
mesh.material.visible = true;
|
mesh.material.visible = true;
|
||||||
mesh.material.transparent = opacity < 100;
|
mesh.material.transparent = opacity < 100;
|
||||||
mesh.material.opacity = opacity / 100;
|
mesh.material.opacity = opacity / 100;
|
||||||
if ('wireframe' in mesh.material) (mesh.material as any).wireframe = false;
|
if ('wireframe' in mesh.material) (mesh.material as any).wireframe = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -637,13 +702,15 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
|
|||||||
onToggleWireframe={() => setIsWireframe(!isWireframe)}
|
onToggleWireframe={() => setIsWireframe(!isWireframe)}
|
||||||
colorMode={colorMode}
|
colorMode={colorMode}
|
||||||
onColorModeChange={setColorMode}
|
onColorModeChange={setColorMode}
|
||||||
|
selectedProcess={selectedProcess}
|
||||||
|
onSelectProcess={setSelectedProcess}
|
||||||
onFitView={() => fitModelToView()}
|
onFitView={() => fitModelToView()}
|
||||||
onToggleFullscreen={handleToggleFullscreen}
|
onToggleFullscreen={handleToggleFullscreen}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Top Right: Interactive ViewCube */}
|
{/* Top Right: Interactive ViewCube WebGL sincronizado */}
|
||||||
<div className="absolute top-4 right-4 z-20">
|
<div className="absolute top-4 right-4 z-20">
|
||||||
<ViewCube onSelectView={handleViewCubeSelect} />
|
<ViewCube onSelectView={handleViewCubeSelect} mainCameraRef={activeCameraRef} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Hover Tooltip (1.5s delay trigger) */}
|
{/* Hover Tooltip (1.5s delay trigger) */}
|
||||||
|
|||||||
@@ -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';
|
import { RotateCcw } from 'lucide-react';
|
||||||
|
|
||||||
interface ViewCubeProps {
|
interface ViewCubeProps {
|
||||||
onSelectView: (view: 'top' | 'bottom' | 'front' | 'back' | 'left' | 'right' | 'iso') => void;
|
onSelectView: (view: 'top' | 'bottom' | 'front' | 'back' | 'left' | 'right' | 'iso') => void;
|
||||||
|
mainCameraRef?: React.MutableRefObject<THREE.Camera | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ViewCube: React.FC<ViewCubeProps> = ({ onSelectView }) => {
|
export const ViewCube: React.FC<ViewCubeProps> = ({ onSelectView, mainCameraRef }) => {
|
||||||
const [hoveredFace, setHoveredFace] = useState<string | null>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const rendererRef = useRef<THREE.WebGLRenderer | null>(null);
|
||||||
|
const sceneRef = useRef<THREE.Scene | null>(null);
|
||||||
|
const cameraRef = useRef<THREE.OrthographicCamera | null>(null);
|
||||||
|
const cubeRef = useRef<THREE.Mesh | null>(null);
|
||||||
|
const raycasterRef = useRef<THREE.Raycaster>(new THREE.Raycaster());
|
||||||
|
const mouseRef = useRef<THREE.Vector2>(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 (
|
return (
|
||||||
<div className="relative select-none flex flex-col items-center">
|
<div className="relative select-none flex flex-col items-center">
|
||||||
{/* Container Principal translúcido idêntico ao SteelXR */}
|
{/* Container Reduzido (~70% do anterior) com estilo SteelXR */}
|
||||||
<div className="relative w-32 h-32 p-2 rounded-2xl bg-slate-950/85 backdrop-blur-xl border border-cyan-500/40 shadow-2xl shadow-cyan-950/60 flex items-center justify-center overflow-hidden">
|
<div className="relative w-24 h-24 p-1 rounded-2xl bg-slate-950/85 backdrop-blur-xl border border-cyan-500/40 shadow-xl shadow-cyan-950/60 flex items-center justify-center cursor-pointer group">
|
||||||
{/* Glow de Fundo */}
|
{/* Glow sutil */}
|
||||||
<div className="absolute inset-0 bg-gradient-to-tr from-cyan-500/10 via-transparent to-blue-500/10 pointer-events-none" />
|
<div className="absolute inset-0 bg-gradient-to-tr from-cyan-500/10 via-transparent to-blue-500/10 rounded-2xl pointer-events-none" />
|
||||||
|
|
||||||
{/* Viewport 3D Isométrica do Cubo */}
|
{/* Canvas WebGL 3D do Cubo Sincronizado */}
|
||||||
<div
|
<div ref={containerRef} className="w-[84px] h-[84px] flex items-center justify-center" />
|
||||||
className="relative w-20 h-20"
|
|
||||||
style={{
|
|
||||||
perspective: '600px',
|
|
||||||
perspectiveOrigin: '50% 50%',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* Cubo 3D Rotacionado em Vista Isométrica Natural */}
|
|
||||||
<div
|
|
||||||
className="w-full h-full relative transition-transform duration-300 ease-out"
|
|
||||||
style={{
|
|
||||||
transformStyle: 'preserve-3d',
|
|
||||||
transform: 'rotateX(-28deg) rotateY(42deg)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* FACE TOPO */}
|
|
||||||
<button
|
|
||||||
onClick={() => onSelectView('top')}
|
|
||||||
onMouseEnter={() => setHoveredFace('TOPO')}
|
|
||||||
onMouseLeave={() => setHoveredFace(null)}
|
|
||||||
className="absolute inset-0 flex items-center justify-center font-extrabold text-xs tracking-wider transition-all duration-200 cursor-pointer"
|
|
||||||
style={{
|
|
||||||
transform: 'rotateX(90deg) translateZ(40px)',
|
|
||||||
backgroundColor: hoveredFace === 'TOPO' ? '#0891b2' : '#0f172a',
|
|
||||||
color: hoveredFace === 'TOPO' ? '#ffffff' : '#a5f3fc',
|
|
||||||
border: '2px solid #22d3ee',
|
|
||||||
boxShadow:
|
|
||||||
hoveredFace === 'TOPO'
|
|
||||||
? 'inset 0 0 15px rgba(34, 211, 238, 0.9), 0 0 15px #06b6d4'
|
|
||||||
: 'inset 0 0 10px rgba(34, 211, 238, 0.35)',
|
|
||||||
textShadow: '0 0 8px #22d3ee',
|
|
||||||
}}
|
|
||||||
title="Vista Superior (Planta)"
|
|
||||||
>
|
|
||||||
TOPO
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* FACE FRENTE */}
|
{/* Botão ISO no canto inferior */}
|
||||||
<button
|
|
||||||
onClick={() => onSelectView('front')}
|
|
||||||
onMouseEnter={() => setHoveredFace('FRENTE')}
|
|
||||||
onMouseLeave={() => setHoveredFace(null)}
|
|
||||||
className="absolute inset-0 flex items-center justify-center font-extrabold text-xs tracking-wider transition-all duration-200 cursor-pointer"
|
|
||||||
style={{
|
|
||||||
transform: 'translateZ(40px)',
|
|
||||||
backgroundColor: hoveredFace === 'FRENTE' ? '#0891b2' : '#0f172a',
|
|
||||||
color: hoveredFace === 'FRENTE' ? '#ffffff' : '#a5f3fc',
|
|
||||||
border: '2px solid #22d3ee',
|
|
||||||
boxShadow:
|
|
||||||
hoveredFace === 'FRENTE'
|
|
||||||
? 'inset 0 0 15px rgba(34, 211, 238, 0.9), 0 0 15px #06b6d4'
|
|
||||||
: 'inset 0 0 10px rgba(34, 211, 238, 0.35)',
|
|
||||||
textShadow: '0 0 8px #22d3ee',
|
|
||||||
}}
|
|
||||||
title="Vista Frontal"
|
|
||||||
>
|
|
||||||
FRENTE
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* FACE DIREITA (DIR) */}
|
|
||||||
<button
|
|
||||||
onClick={() => onSelectView('right')}
|
|
||||||
onMouseEnter={() => setHoveredFace('DIR')}
|
|
||||||
onMouseLeave={() => setHoveredFace(null)}
|
|
||||||
className="absolute inset-0 flex items-center justify-center font-extrabold text-xs tracking-wider transition-all duration-200 cursor-pointer"
|
|
||||||
style={{
|
|
||||||
transform: 'rotateY(90deg) translateZ(40px)',
|
|
||||||
backgroundColor: hoveredFace === 'DIR' ? '#0891b2' : '#0f172a',
|
|
||||||
color: hoveredFace === 'DIR' ? '#ffffff' : '#a5f3fc',
|
|
||||||
border: '2px solid #22d3ee',
|
|
||||||
boxShadow:
|
|
||||||
hoveredFace === 'DIR'
|
|
||||||
? 'inset 0 0 15px rgba(34, 211, 238, 0.9), 0 0 15px #06b6d4'
|
|
||||||
: 'inset 0 0 10px rgba(34, 211, 238, 0.35)',
|
|
||||||
textShadow: '0 0 8px #22d3ee',
|
|
||||||
}}
|
|
||||||
title="Vista Direita"
|
|
||||||
>
|
|
||||||
DIR
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* FACE ESQUERDA (ESQ) */}
|
|
||||||
<button
|
|
||||||
onClick={() => onSelectView('left')}
|
|
||||||
onMouseEnter={() => setHoveredFace('ESQ')}
|
|
||||||
onMouseLeave={() => setHoveredFace(null)}
|
|
||||||
className="absolute inset-0 flex items-center justify-center font-extrabold text-xs tracking-wider transition-all duration-200 cursor-pointer"
|
|
||||||
style={{
|
|
||||||
transform: 'rotateY(-90deg) translateZ(40px)',
|
|
||||||
backgroundColor: hoveredFace === 'ESQ' ? '#0891b2' : '#0f172a',
|
|
||||||
color: hoveredFace === 'ESQ' ? '#ffffff' : '#a5f3fc',
|
|
||||||
border: '2px solid #22d3ee',
|
|
||||||
boxShadow:
|
|
||||||
hoveredFace === 'ESQ'
|
|
||||||
? 'inset 0 0 15px rgba(34, 211, 238, 0.9), 0 0 15px #06b6d4'
|
|
||||||
: 'inset 0 0 10px rgba(34, 211, 238, 0.35)',
|
|
||||||
textShadow: '0 0 8px #22d3ee',
|
|
||||||
}}
|
|
||||||
title="Vista Esquerda"
|
|
||||||
>
|
|
||||||
ESQ
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* FACE ATRÁS */}
|
|
||||||
<button
|
|
||||||
onClick={() => onSelectView('back')}
|
|
||||||
onMouseEnter={() => setHoveredFace('ATRÁS')}
|
|
||||||
onMouseLeave={() => setHoveredFace(null)}
|
|
||||||
className="absolute inset-0 flex items-center justify-center font-extrabold text-xs tracking-wider transition-all duration-200 cursor-pointer"
|
|
||||||
style={{
|
|
||||||
transform: 'rotateY(180deg) translateZ(40px)',
|
|
||||||
backgroundColor: hoveredFace === 'ATRÁS' ? '#0891b2' : '#0f172a',
|
|
||||||
color: hoveredFace === 'ATRÁS' ? '#ffffff' : '#a5f3fc',
|
|
||||||
border: '2px solid #22d3ee',
|
|
||||||
boxShadow:
|
|
||||||
hoveredFace === 'ATRÁS'
|
|
||||||
? 'inset 0 0 15px rgba(34, 211, 238, 0.9), 0 0 15px #06b6d4'
|
|
||||||
: 'inset 0 0 10px rgba(34, 211, 238, 0.35)',
|
|
||||||
textShadow: '0 0 8px #22d3ee',
|
|
||||||
}}
|
|
||||||
title="Vista Posterior (Atrás)"
|
|
||||||
>
|
|
||||||
ATRÁS
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* FACE BASE (INFERIOR) */}
|
|
||||||
<button
|
|
||||||
onClick={() => onSelectView('bottom')}
|
|
||||||
onMouseEnter={() => setHoveredFace('BASE')}
|
|
||||||
onMouseLeave={() => setHoveredFace(null)}
|
|
||||||
className="absolute inset-0 flex items-center justify-center font-extrabold text-xs tracking-wider transition-all duration-200 cursor-pointer"
|
|
||||||
style={{
|
|
||||||
transform: 'rotateX(-90deg) translateZ(40px)',
|
|
||||||
backgroundColor: hoveredFace === 'BASE' ? '#0891b2' : '#0f172a',
|
|
||||||
color: hoveredFace === 'BASE' ? '#ffffff' : '#a5f3fc',
|
|
||||||
border: '2px solid #22d3ee',
|
|
||||||
boxShadow:
|
|
||||||
hoveredFace === 'BASE'
|
|
||||||
? 'inset 0 0 15px rgba(34, 211, 238, 0.9), 0 0 15px #06b6d4'
|
|
||||||
: 'inset 0 0 10px rgba(34, 211, 238, 0.35)',
|
|
||||||
textShadow: '0 0 8px #22d3ee',
|
|
||||||
}}
|
|
||||||
title="Vista Inferior (Base)"
|
|
||||||
>
|
|
||||||
BASE
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Botão de Vista Isométrica Padrão (ISO) */}
|
|
||||||
<button
|
<button
|
||||||
onClick={() => onSelectView('iso')}
|
onClick={(e) => {
|
||||||
className="absolute bottom-1.5 right-1.5 p-1 rounded-lg bg-slate-900/90 hover:bg-cyan-600 text-cyan-300 hover:text-white border border-cyan-500/30 transition-all text-[10px] font-mono font-bold flex items-center gap-1 shadow cursor-pointer"
|
e.stopPropagation();
|
||||||
|
onSelectView('iso');
|
||||||
|
}}
|
||||||
|
className="absolute bottom-1 right-1 px-1 py-0.5 rounded bg-slate-900/90 hover:bg-cyan-600 text-cyan-300 hover:text-white border border-cyan-500/30 transition-all text-[9px] font-mono font-bold flex items-center gap-0.5 shadow cursor-pointer"
|
||||||
title="Restaurar Vista Isométrica 3D"
|
title="Restaurar Vista Isométrica 3D"
|
||||||
>
|
>
|
||||||
<RotateCcw className="w-2.5 h-2.5" />
|
<RotateCcw className="w-2.5 h-2.5" />
|
||||||
@@ -176,9 +198,9 @@ export const ViewCube: React.FC<ViewCubeProps> = ({ onSelectView }) => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Rótulo Inferior Elegante */}
|
{/* Rótulo inferior */}
|
||||||
<span className="text-[9px] font-mono font-bold tracking-widest uppercase text-cyan-700 dark:text-cyan-400 mt-1">
|
<span className="text-[8px] font-mono font-bold tracking-widest uppercase text-cyan-700 dark:text-cyan-400 mt-0.5">
|
||||||
Cubo de Orientação
|
Cubo 3D
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,7 +11,12 @@ import {
|
|||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Focus,
|
Focus,
|
||||||
Sliders
|
Sliders,
|
||||||
|
Flame,
|
||||||
|
Wrench,
|
||||||
|
Paintbrush,
|
||||||
|
Truck,
|
||||||
|
Scissors
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
interface Viewer3DSidebarProps {
|
interface Viewer3DSidebarProps {
|
||||||
@@ -25,10 +30,21 @@ interface Viewer3DSidebarProps {
|
|||||||
onToggleWireframe: () => void;
|
onToggleWireframe: () => void;
|
||||||
colorMode: 'description' | 'production';
|
colorMode: 'description' | 'production';
|
||||||
onColorModeChange: (mode: 'description' | 'production') => void;
|
onColorModeChange: (mode: 'description' | 'production') => void;
|
||||||
|
selectedProcess?: string;
|
||||||
|
onSelectProcess?: (proc: string) => void;
|
||||||
onFitView: () => void;
|
onFitView: () => void;
|
||||||
onToggleFullscreen: () => 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<Viewer3DSidebarProps> = ({
|
export const Viewer3DSidebar: React.FC<Viewer3DSidebarProps> = ({
|
||||||
opacity,
|
opacity,
|
||||||
onOpacityChange,
|
onOpacityChange,
|
||||||
@@ -40,6 +56,8 @@ export const Viewer3DSidebar: React.FC<Viewer3DSidebarProps> = ({
|
|||||||
onToggleWireframe,
|
onToggleWireframe,
|
||||||
colorMode,
|
colorMode,
|
||||||
onColorModeChange,
|
onColorModeChange,
|
||||||
|
selectedProcess = 'all',
|
||||||
|
onSelectProcess,
|
||||||
onFitView,
|
onFitView,
|
||||||
onToggleFullscreen,
|
onToggleFullscreen,
|
||||||
}) => {
|
}) => {
|
||||||
@@ -50,7 +68,7 @@ export const Viewer3DSidebar: React.FC<Viewer3DSidebarProps> = ({
|
|||||||
{/* Container Principal da Barra Lateral */}
|
{/* Container Principal da Barra Lateral */}
|
||||||
<div
|
<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 ${
|
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-60 max-h-[calc(100vh-16rem)]' : 'w-0 border-0 opacity-0 pointer-events-none'
|
isOpen ? 'w-64 max-h-[calc(100vh-14rem)]' : 'w-0 border-0 opacity-0 pointer-events-none'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{/* Cabeçalho do Sub-Menu */}
|
{/* Cabeçalho do Sub-Menu */}
|
||||||
@@ -68,18 +86,52 @@ export const Viewer3DSidebar: React.FC<Viewer3DSidebarProps> = ({
|
|||||||
|
|
||||||
{/* Corpo com Rolagem Suave */}
|
{/* Corpo com Rolagem Suave */}
|
||||||
<div className="flex-1 overflow-y-auto p-3 space-y-4 custom-scrollbar">
|
<div className="flex-1 overflow-y-auto p-3 space-y-4 custom-scrollbar">
|
||||||
{/* Grupo 1: Modo de Cores / Produção */}
|
{/* Grupo 1: Destaque por Processo Industrial (Inovação Híbrida: Verde Sólido + Aramado Cinza Fantasma) */}
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="text-[11px] font-bold uppercase tracking-wider text-slate-600 dark:text-slate-400 flex items-center gap-1.5">
|
||||||
|
<CheckCircle2 className="w-3.5 h-3.5 text-emerald-600 dark:text-emerald-400" />
|
||||||
|
Destaque por Processo
|
||||||
|
</label>
|
||||||
|
<span className="text-[9px] font-mono text-emerald-600 dark:text-emerald-400 font-bold">
|
||||||
|
BIM + Fábrica
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-3 gap-1 bg-slate-100 dark:bg-slate-950/60 p-1 rounded-xl border border-slate-200 dark:border-slate-800">
|
||||||
|
{FABRICATION_PROCESSES.map((proc) => {
|
||||||
|
const Icon = proc.icon;
|
||||||
|
const isSelected = selectedProcess === proc.id;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={proc.id}
|
||||||
|
onClick={() => onSelectProcess && onSelectProcess(proc.id)}
|
||||||
|
className={`flex flex-col items-center justify-center gap-1 py-1.5 px-1 rounded-lg text-[10px] font-bold transition-all ${
|
||||||
|
isSelected
|
||||||
|
? '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 hover:bg-slate-200/60 dark:hover:bg-slate-800/60'
|
||||||
|
}`}
|
||||||
|
title={`Destacar peças em ${proc.label} (Sólido Verde) e não apontadas em aramado cinza`}
|
||||||
|
>
|
||||||
|
<Icon className="w-3.5 h-3.5" />
|
||||||
|
<span>{proc.label}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Grupo 2: Modo de Cores Geral */}
|
||||||
<div className="space-y-1.5">
|
<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">
|
<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" />
|
<Palette className="w-3.5 h-3.5 text-cyan-600 dark:text-cyan-400" />
|
||||||
Exibição de Cores
|
Modo de Cores
|
||||||
</label>
|
</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">
|
<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
|
<button
|
||||||
onClick={() => onColorModeChange('production')}
|
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 ${
|
className={`flex items-center justify-center gap-1.5 px-2 py-1.5 rounded-lg text-xs font-bold transition-all ${
|
||||||
colorMode === 'production'
|
colorMode === 'production'
|
||||||
? 'bg-emerald-600 text-white shadow-md shadow-emerald-600/30'
|
? 'bg-cyan-600 text-white shadow-md shadow-cyan-600/30'
|
||||||
: 'text-slate-700 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white'
|
: 'text-slate-700 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -100,7 +152,7 @@ export const Viewer3DSidebar: React.FC<Viewer3DSidebarProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Grupo 2: Visibilidade & Grade */}
|
{/* Grupo 3: Visibilidade & Grade */}
|
||||||
<div className="space-y-1.5">
|
<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">
|
<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" />
|
<GridIcon className="w-3.5 h-3.5 text-cyan-600 dark:text-cyan-400" />
|
||||||
@@ -125,17 +177,17 @@ export const Viewer3DSidebar: React.FC<Viewer3DSidebarProps> = ({
|
|||||||
onClick={onToggleWireframe}
|
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 ${
|
className={`flex items-center justify-center gap-1.5 px-2.5 py-2 rounded-xl text-xs font-semibold border transition-all ${
|
||||||
isWireframe
|
isWireframe
|
||||||
? '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-slate-100 dark:bg-slate-800 text-slate-900 dark:text-slate-100 border-slate-300 dark:border-slate-700 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'
|
: '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 text-cyan-600" /> : <Box className="w-3.5 h-3.5" />}
|
{isWireframe ? <Layers className="w-3.5 h-3.5 text-slate-600 dark:text-slate-300" /> : <Box className="w-3.5 h-3.5" />}
|
||||||
<span>{isWireframe ? 'Aramado' : 'Sólido'}</span>
|
<span>{isWireframe ? 'Aramado' : 'Sólido'}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Grupo 3: Câmera & Projeção */}
|
{/* Grupo 4: Câmera & Projeção */}
|
||||||
<div className="space-y-1.5">
|
<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">
|
<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" />
|
<Compass className="w-3.5 h-3.5 text-cyan-600 dark:text-cyan-400" />
|
||||||
@@ -167,7 +219,7 @@ export const Viewer3DSidebar: React.FC<Viewer3DSidebarProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Grupo 4: Opacidade da Estrutura */}
|
{/* Grupo 5: 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="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">
|
<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">
|
<span className="font-semibold text-slate-700 dark:text-slate-300 flex items-center gap-1.5">
|
||||||
|
|||||||
Reference in New Issue
Block a user