feat: Aramado limpo sem linhas internas (EdgesGeometry) no Analisador 3D e compactação de layout da sidebar 3D
This commit is contained in:
@@ -20,7 +20,10 @@ import {
|
||||
AlertTriangle,
|
||||
CheckCircle,
|
||||
HelpCircle,
|
||||
Cpu
|
||||
Cpu,
|
||||
Focus,
|
||||
SunMedium,
|
||||
Compass
|
||||
} from 'lucide-react';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
@@ -66,11 +69,21 @@ export const AnalyzerViewer3D: React.FC<AnalyzerViewer3DProps> = ({
|
||||
const canvasContainerRef = useRef<HTMLDivElement>(null);
|
||||
const rendererRef = useRef<THREE.WebGLRenderer | null>(null);
|
||||
const sceneRef = useRef<THREE.Scene | null>(null);
|
||||
const cameraRef = useRef<THREE.PerspectiveCamera | null>(null);
|
||||
const cameraRef = useRef<THREE.Camera | null>(null);
|
||||
const perspCameraRef = useRef<THREE.PerspectiveCamera | null>(null);
|
||||
const orthoCameraRef = useRef<THREE.OrthographicCamera | null>(null);
|
||||
const [isOrthographic, setIsOrthographic] = useState<boolean>(false);
|
||||
const controlsRef = useRef<OrbitControls | null>(null);
|
||||
const modelGroupRef = useRef<THREE.Group | null>(null);
|
||||
const gridRef = useRef<THREE.GridHelper | null>(null);
|
||||
|
||||
// Iluminação ajustável em tempo real
|
||||
const ambientLightRef = useRef<THREE.AmbientLight | null>(null);
|
||||
const hemiLightRef = useRef<THREE.HemisphereLight | null>(null);
|
||||
const dirLight1Ref = useRef<THREE.DirectionalLight | null>(null);
|
||||
const dirLight2Ref = useRef<THREE.DirectionalLight | null>(null);
|
||||
const [brightness, setBrightness] = useState<number>(100);
|
||||
|
||||
const [wireframe, setWireframe] = useState<boolean>(false);
|
||||
const [hoveredDefect, setHoveredDefect] = useState<DefectItem | null>(null);
|
||||
const [hoverPos, setHoverPos] = useState<{ x: number; y: number } | null>(null);
|
||||
@@ -98,9 +111,24 @@ export const AnalyzerViewer3D: React.FC<AnalyzerViewer3DProps> = ({
|
||||
const scene = new THREE.Scene();
|
||||
sceneRef.current = scene;
|
||||
|
||||
const camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 2000);
|
||||
camera.position.set(40, 35, 50);
|
||||
cameraRef.current = camera;
|
||||
const perspCamera = new THREE.PerspectiveCamera(45, width / height, 0.1, 2000);
|
||||
perspCamera.position.set(40, 35, 50);
|
||||
perspCameraRef.current = perspCamera;
|
||||
|
||||
const frustumSize = 80;
|
||||
const aspect = width / height;
|
||||
const orthoCamera = new THREE.OrthographicCamera(
|
||||
(frustumSize * aspect) / -2,
|
||||
(frustumSize * aspect) / 2,
|
||||
frustumSize / 2,
|
||||
frustumSize / -2,
|
||||
0.1,
|
||||
2000
|
||||
);
|
||||
orthoCamera.position.copy(perspCamera.position);
|
||||
orthoCameraRef.current = orthoCamera;
|
||||
|
||||
cameraRef.current = perspCamera;
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({
|
||||
antialias: true,
|
||||
@@ -116,24 +144,32 @@ export const AnalyzerViewer3D: React.FC<AnalyzerViewer3DProps> = ({
|
||||
canvasContainerRef.current.innerHTML = '';
|
||||
canvasContainerRef.current.appendChild(renderer.domElement);
|
||||
|
||||
const controls = new OrbitControls(camera, renderer.domElement);
|
||||
const controls = new OrbitControls(perspCamera, renderer.domElement);
|
||||
controls.enableDamping = true;
|
||||
controls.dampingFactor = 0.05;
|
||||
controls.maxDistance = 1500;
|
||||
controls.minDistance = 0.2;
|
||||
controlsRef.current = controls;
|
||||
|
||||
// Iluminação balanceada industrial
|
||||
// Iluminação balanceada industrial com suporte a controle de luminosidade
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 0.85);
|
||||
scene.add(ambientLight);
|
||||
ambientLightRef.current = ambientLight;
|
||||
|
||||
// Luz hemisférica uniforme: ilumina simultaneamente de cima e baixo para eliminar sombras duras de normais invertidas
|
||||
const hemiLight = new THREE.HemisphereLight(0xffffff, 0x475569, 0.6);
|
||||
scene.add(hemiLight);
|
||||
hemiLightRef.current = hemiLight;
|
||||
|
||||
const dirLight1 = new THREE.DirectionalLight(0xffffff, 1.2);
|
||||
dirLight1.position.set(80, 120, 80);
|
||||
scene.add(dirLight1);
|
||||
dirLight1Ref.current = dirLight1;
|
||||
|
||||
const dirLight2 = new THREE.DirectionalLight(0x94a3b8, 0.6);
|
||||
dirLight2.position.set(-80, -40, -80);
|
||||
scene.add(dirLight2);
|
||||
dirLight2Ref.current = dirLight2;
|
||||
|
||||
// Grid no piso com cores refinadas para modo claro e escuro
|
||||
const isDarkMode = document.documentElement.classList.contains('dark');
|
||||
@@ -178,16 +214,31 @@ export const AnalyzerViewer3D: React.FC<AnalyzerViewer3DProps> = ({
|
||||
const animate = () => {
|
||||
animId = requestAnimationFrame(animate);
|
||||
controls.update();
|
||||
renderer.render(scene, camera);
|
||||
if (renderer && scene && cameraRef.current) {
|
||||
renderer.render(scene, cameraRef.current);
|
||||
}
|
||||
};
|
||||
animate();
|
||||
|
||||
const handleResize = () => {
|
||||
if (!canvasContainerRef.current || !renderer || !camera) return;
|
||||
if (!canvasContainerRef.current || !renderer) return;
|
||||
const w = canvasContainerRef.current.clientWidth;
|
||||
const h = canvasContainerRef.current.clientHeight;
|
||||
camera.aspect = w / h;
|
||||
camera.updateProjectionMatrix();
|
||||
if (w <= 0 || h <= 0) return;
|
||||
|
||||
const newAspect = w / h;
|
||||
if (perspCameraRef.current) {
|
||||
perspCameraRef.current.aspect = newAspect;
|
||||
perspCameraRef.current.updateProjectionMatrix();
|
||||
}
|
||||
if (orthoCameraRef.current) {
|
||||
const frustum = 80;
|
||||
orthoCameraRef.current.left = (frustum * newAspect) / -2;
|
||||
orthoCameraRef.current.right = (frustum * newAspect) / 2;
|
||||
orthoCameraRef.current.top = frustum / 2;
|
||||
orthoCameraRef.current.bottom = frustum / -2;
|
||||
orthoCameraRef.current.updateProjectionMatrix();
|
||||
}
|
||||
renderer.setSize(w, h);
|
||||
};
|
||||
|
||||
@@ -205,7 +256,45 @@ export const AnalyzerViewer3D: React.FC<AnalyzerViewer3DProps> = ({
|
||||
};
|
||||
}, [theme]);
|
||||
|
||||
// Centraliza e enquadra perfeitamente o modelo na câmera (Fit View)
|
||||
// Efeito dinâmico de ajuste de Luminosidade / Iluminação
|
||||
useEffect(() => {
|
||||
const factor = brightness / 100;
|
||||
if (ambientLightRef.current) {
|
||||
ambientLightRef.current.intensity = 0.85 * factor;
|
||||
}
|
||||
if (hemiLightRef.current) {
|
||||
hemiLightRef.current.intensity = 0.6 * factor;
|
||||
}
|
||||
if (dirLight1Ref.current) {
|
||||
dirLight1Ref.current.intensity = 1.2 * Math.min(1.4, factor);
|
||||
}
|
||||
if (dirLight2Ref.current) {
|
||||
dirLight2Ref.current.intensity = 0.6 * factor;
|
||||
}
|
||||
if (rendererRef.current) {
|
||||
rendererRef.current.toneMappingExposure = 1.1 * (0.85 + 0.15 * factor);
|
||||
}
|
||||
}, [brightness]);
|
||||
|
||||
// Alterna entre Câmera Perspectiva e Ortográfica
|
||||
const handleToggleCamera = useCallback(() => {
|
||||
if (!perspCameraRef.current || !orthoCameraRef.current || !controlsRef.current) return;
|
||||
|
||||
const nextOrtho = !isOrthographic;
|
||||
setIsOrthographic(nextOrtho);
|
||||
|
||||
const prevCamera = cameraRef.current!;
|
||||
const nextCamera = nextOrtho ? orthoCameraRef.current : perspCameraRef.current;
|
||||
|
||||
nextCamera.position.copy(prevCamera.position);
|
||||
nextCamera.quaternion.copy(prevCamera.quaternion);
|
||||
|
||||
cameraRef.current = nextCamera;
|
||||
controlsRef.current.object = nextCamera;
|
||||
controlsRef.current.update();
|
||||
}, [isOrthographic]);
|
||||
|
||||
// Centraliza e enquadra perfeitamente o modelo na câmera (Fit View / Fit to Screen)
|
||||
const fitModelToView = useCallback((targetObj?: THREE.Object3D) => {
|
||||
const obj = targetObj || modelGroupRef.current;
|
||||
if (!obj || !cameraRef.current || !controlsRef.current) return;
|
||||
@@ -224,11 +313,27 @@ export const AnalyzerViewer3D: React.FC<AnalyzerViewer3DProps> = ({
|
||||
cameraDistance = Math.max(cameraDistance, 5);
|
||||
|
||||
const camera = cameraRef.current;
|
||||
camera.near = Math.max(0.01, maxDim / 500);
|
||||
camera.far = Math.max(2000, maxDim * 20);
|
||||
camera.updateProjectionMatrix();
|
||||
camera.position.set(center.x + cameraDistance * 0.7, center.y + cameraDistance * 0.5, center.z + cameraDistance * 0.7);
|
||||
camera.lookAt(center);
|
||||
if ((camera as THREE.PerspectiveCamera).isPerspectiveCamera) {
|
||||
const pCam = camera as THREE.PerspectiveCamera;
|
||||
pCam.near = Math.max(0.01, maxDim / 500);
|
||||
pCam.far = Math.max(2000, maxDim * 20);
|
||||
pCam.updateProjectionMatrix();
|
||||
pCam.position.set(center.x + cameraDistance * 0.7, center.y + cameraDistance * 0.5, center.z + cameraDistance * 0.7);
|
||||
pCam.lookAt(center);
|
||||
} else if ((camera as THREE.OrthographicCamera).isOrthographicCamera) {
|
||||
const oCam = camera as THREE.OrthographicCamera;
|
||||
const aspect = oCam.right / oCam.top;
|
||||
const frustum = maxDim * 1.5;
|
||||
oCam.left = (frustum * aspect) / -2;
|
||||
oCam.right = (frustum * aspect) / 2;
|
||||
oCam.top = frustum / 2;
|
||||
oCam.bottom = frustum / -2;
|
||||
oCam.near = Math.max(0.01, maxDim / 500);
|
||||
oCam.far = Math.max(2000, maxDim * 20);
|
||||
oCam.position.set(center.x + maxDim, center.y + maxDim * 0.8, center.z + maxDim);
|
||||
oCam.lookAt(center);
|
||||
oCam.updateProjectionMatrix();
|
||||
}
|
||||
|
||||
controlsRef.current.target.copy(center);
|
||||
controlsRef.current.maxDistance = Math.max(1500, maxDim * 10);
|
||||
@@ -418,18 +523,53 @@ export const AnalyzerViewer3D: React.FC<AnalyzerViewer3DProps> = ({
|
||||
emissiveIntensity = 0.35;
|
||||
}
|
||||
|
||||
// Aplica o material atualizado
|
||||
obj.material = new THREE.MeshStandardMaterial({
|
||||
color: new THREE.Color(color),
|
||||
emissive: new THREE.Color(emissiveColor),
|
||||
emissiveIntensity,
|
||||
roughness: 0.35,
|
||||
metalness: 0.2,
|
||||
wireframe,
|
||||
transparent,
|
||||
opacity,
|
||||
side: THREE.DoubleSide,
|
||||
});
|
||||
// Aramado Arquitetural Limpo (EdgesGeometry - Estilo SteelXR / Tekla sem diagonais internas)
|
||||
if (wireframe) {
|
||||
// Cria as arestas de contorno se ainda não existirem para esta malha
|
||||
if (!obj.userData.edgesLine) {
|
||||
const edgesGeo = new THREE.EdgesGeometry(obj.geometry, 24);
|
||||
const lineMat = new THREE.LineBasicMaterial({
|
||||
color: new THREE.Color(color),
|
||||
transparent: true,
|
||||
opacity: isGhostMode && targetMark && !isSelected ? 0.15 : 0.95,
|
||||
});
|
||||
const edgesLine = new THREE.LineSegments(edgesGeo, lineMat);
|
||||
edgesLine.name = 'edgesLine';
|
||||
obj.userData.edgesLine = edgesLine;
|
||||
obj.add(edgesLine);
|
||||
} else {
|
||||
// Atualiza a cor e opacidade das arestas existentes
|
||||
const lineMat = obj.userData.edgesLine.material as THREE.LineBasicMaterial;
|
||||
if (lineMat) {
|
||||
lineMat.color.set(color);
|
||||
lineMat.opacity = isGhostMode && targetMark && !isSelected ? 0.15 : 0.95;
|
||||
}
|
||||
}
|
||||
obj.userData.edgesLine.visible = true;
|
||||
|
||||
// Oculta a malha sólida para exibir apenas o aramado limpo
|
||||
obj.material = new THREE.MeshBasicMaterial({
|
||||
visible: false,
|
||||
wireframe: false,
|
||||
});
|
||||
} else {
|
||||
// Modo Sólido: desativa as linhas de contorno e restaura o sólido PBR
|
||||
if (obj.userData.edgesLine) {
|
||||
obj.userData.edgesLine.visible = false;
|
||||
}
|
||||
|
||||
obj.material = new THREE.MeshStandardMaterial({
|
||||
color: new THREE.Color(color),
|
||||
emissive: new THREE.Color(emissiveColor),
|
||||
emissiveIntensity,
|
||||
roughness: 0.35,
|
||||
metalness: 0.2,
|
||||
wireframe: false,
|
||||
transparent,
|
||||
opacity,
|
||||
side: THREE.DoubleSide,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}, [colorizationMode, selectedDefect, selectedPieceMark, isGhostMode, wireframe, modelData]);
|
||||
@@ -598,51 +738,108 @@ export const AnalyzerViewer3D: React.FC<AnalyzerViewer3DProps> = ({
|
||||
</div>
|
||||
|
||||
{/* Barra de Ferramentas Flutuante Superior */}
|
||||
<div className="absolute top-3 left-3 flex items-center gap-1.5 p-1.5 bg-white/95 dark:bg-slate-900/90 backdrop-blur-md border border-slate-200 dark:border-slate-700/80 rounded-lg shadow-md z-20">
|
||||
<div className="absolute top-3 left-3 flex flex-wrap items-center gap-1.5 p-1.5 bg-white/95 dark:bg-slate-900/90 backdrop-blur-md border border-slate-200 dark:border-slate-700/80 rounded-xl shadow-md z-20">
|
||||
{/* Reset Câmera */}
|
||||
<button
|
||||
onClick={handleResetCamera}
|
||||
title="Resetar Câmera"
|
||||
className="p-1.5 text-slate-600 hover:text-slate-900 hover:bg-slate-100 dark:text-slate-300 dark:hover:text-white dark:hover:bg-slate-800 rounded transition"
|
||||
className="p-1.5 text-slate-600 hover:text-slate-900 hover:bg-slate-100 dark:text-slate-300 dark:hover:text-white dark:hover:bg-slate-800 rounded-lg transition"
|
||||
>
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
<div className="w-[1px] h-4 bg-slate-200 dark:bg-slate-700 mx-1" />
|
||||
{/* Centralizar e Enquadrar (Fit to Screen) */}
|
||||
<button
|
||||
onClick={() => fitModelToView()}
|
||||
title="Centralizar e Enquadrar Estrutura na Tela (Fit to Screen)"
|
||||
className="p-1.5 text-slate-600 hover:text-cyan-600 hover:bg-cyan-50 dark:text-slate-300 dark:hover:text-cyan-400 dark:hover:bg-cyan-950/40 rounded-lg transition"
|
||||
>
|
||||
<Focus className="w-4 h-4 text-cyan-600 dark:text-cyan-400" />
|
||||
</button>
|
||||
|
||||
<div className="w-[1px] h-4 bg-slate-200 dark:bg-slate-700 mx-0.5" />
|
||||
|
||||
{/* Comutar Perspectiva / Ortogonal */}
|
||||
<button
|
||||
onClick={handleToggleCamera}
|
||||
title={isOrthographic ? 'Mudar para Vista em Perspectiva (3D Cônica)' : 'Mudar para Vista Ortogonal (Paralela Técnica)'}
|
||||
className={`flex items-center gap-1.5 px-2 py-1 text-xs font-semibold rounded-lg transition ${
|
||||
isOrthographic
|
||||
? 'bg-emerald-600 text-white shadow-sm'
|
||||
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-300 dark:hover:bg-slate-800 dark:hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<Compass className="w-3.5 h-3.5" />
|
||||
<span className="hidden sm:inline">{isOrthographic ? 'Ortogonal' : 'Perspectiva'}</span>
|
||||
</button>
|
||||
|
||||
<div className="w-[1px] h-4 bg-slate-200 dark:bg-slate-700 mx-0.5" />
|
||||
|
||||
{/* Modo Raio-X / Foco */}
|
||||
<button
|
||||
onClick={onToggleGhostMode}
|
||||
title={isGhostMode ? 'Desativar Modo Raio-X / Foco' : 'Ativar Modo Raio-X / Foco'}
|
||||
className={`flex items-center gap-1.5 px-2 py-1 text-xs font-medium rounded transition ${
|
||||
className={`flex items-center gap-1.5 px-2 py-1 text-xs font-medium rounded-lg transition ${
|
||||
isGhostMode
|
||||
? 'bg-indigo-600 text-white shadow'
|
||||
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-300 dark:hover:bg-slate-800 dark:hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<Eye className="w-3.5 h-3.5" />
|
||||
<span>Foco / Raio-X</span>
|
||||
<span className="hidden sm:inline">Foco / Raio-X</span>
|
||||
</button>
|
||||
|
||||
{/* Aramado Limpo (EdgesGeometry) */}
|
||||
<button
|
||||
onClick={() => setWireframe(!wireframe)}
|
||||
title="Alternar Estrutura de Arame (Wireframe)"
|
||||
className={`p-1.5 rounded transition ${
|
||||
title={wireframe ? 'Mudar para Modelo Sólido' : 'Mudar para Aramado Limpo de Engenharia (sem linhas internas)'}
|
||||
className={`p-1.5 rounded-lg transition ${
|
||||
wireframe
|
||||
? 'bg-indigo-600 text-white'
|
||||
? 'bg-indigo-600 text-white shadow-xs'
|
||||
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-300 dark:hover:bg-slate-800 dark:hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<Box className="w-4 h-4" />
|
||||
{wireframe ? <Layers className="w-4 h-4" /> : <Box className="w-4 h-4" />}
|
||||
</button>
|
||||
|
||||
{onToggleFullscreen && (
|
||||
<button
|
||||
onClick={onToggleFullscreen}
|
||||
title={isFullscreen ? 'Sair da Tela Cheia' : 'Tela Cheia'}
|
||||
className="p-1.5 text-slate-600 hover:text-slate-900 hover:bg-slate-100 dark:text-slate-300 dark:hover:text-white dark:hover:bg-slate-800 rounded transition"
|
||||
className="p-1.5 text-slate-600 hover:text-slate-900 hover:bg-slate-100 dark:text-slate-300 dark:hover:text-white dark:hover:bg-slate-800 rounded-lg transition"
|
||||
>
|
||||
{isFullscreen ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className="w-[1px] h-4 bg-slate-200 dark:bg-slate-700 mx-0.5" />
|
||||
|
||||
{/* Barra de Luminosidade (Slider Interativo) */}
|
||||
<div
|
||||
className="flex items-center gap-1.5 px-2 py-1 bg-slate-100/90 dark:bg-slate-800/90 rounded-lg border border-slate-200 dark:border-slate-700"
|
||||
title="Ajuste de Luminosidade da Estrutura (arraste para clarear ou suavizar sombras das peças)"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setBrightness(100)}
|
||||
title="Clique para resetar luminosidade em 100%"
|
||||
className="hover:scale-110 active:scale-95 transition-transform"
|
||||
>
|
||||
<SunMedium className="w-3.5 h-3.5 text-amber-500 shrink-0" />
|
||||
</button>
|
||||
<input
|
||||
type="range"
|
||||
min="40"
|
||||
max="220"
|
||||
step="5"
|
||||
value={brightness}
|
||||
onChange={(e) => setBrightness(Number(e.target.value))}
|
||||
className="w-16 sm:w-20 h-1.5 bg-slate-300 dark:bg-slate-700 rounded-lg appearance-none cursor-pointer accent-amber-500"
|
||||
/>
|
||||
<span className="text-[10px] font-mono font-bold text-slate-600 dark:text-slate-300 min-w-[28px] text-right">
|
||||
{brightness}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Legenda de Cores Flutuante Inferior */}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { ViewCube } from './ViewCube';
|
||||
import { Viewer3DSidebar } from './Viewer3DSidebar';
|
||||
import { PieceHoverTooltip } from './PieceHoverTooltip';
|
||||
import { SelectionListModal, SelectedPieceItem } from './SelectionListModal';
|
||||
import { Trash2, FileSpreadsheet, RotateCcw, RotateCw, Lock, Unlock } from 'lucide-react';
|
||||
import { Trash2, FileSpreadsheet, RotateCcw, RotateCw, Lock, Unlock, Focus, SunMedium } from 'lucide-react';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import { useToast } from '@/components/ui/use-toast';
|
||||
|
||||
@@ -59,6 +59,13 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
|
||||
const [colorMode, setColorMode] = useState<'description' | 'production'>('production');
|
||||
const [selectedProcess, setSelectedProcess] = useState<string>('all');
|
||||
|
||||
// Iluminação ajustável em tempo real
|
||||
const ambientLightRef = useRef<THREE.AmbientLight | null>(null);
|
||||
const hemiLightRef = useRef<THREE.HemisphereLight | null>(null);
|
||||
const dirLight1Ref = useRef<THREE.DirectionalLight | null>(null);
|
||||
const dirLight2Ref = useRef<THREE.DirectionalLight | null>(null);
|
||||
const [brightness, setBrightness] = useState<number>(100);
|
||||
|
||||
// Hover Tooltip States (1.0 second delay)
|
||||
const hoverTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const [hoveredPiece, setHoveredPiece] = useState<PieceInfo | null>(null);
|
||||
@@ -161,18 +168,22 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
|
||||
// 5. Lights
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 0.75);
|
||||
scene.add(ambientLight);
|
||||
ambientLightRef.current = ambientLight;
|
||||
|
||||
const dirLight1 = new THREE.DirectionalLight(0xffffff, 1.2);
|
||||
dirLight1.position.set(100, 200, 100);
|
||||
dirLight1.castShadow = true;
|
||||
scene.add(dirLight1);
|
||||
dirLight1Ref.current = dirLight1;
|
||||
|
||||
const dirLight2 = new THREE.DirectionalLight(0x94a3b8, 0.6);
|
||||
dirLight2.position.set(-100, -50, -100);
|
||||
scene.add(dirLight2);
|
||||
dirLight2Ref.current = dirLight2;
|
||||
|
||||
const hemiLight = new THREE.HemisphereLight(0xffffff, 0x1e293b, 0.5);
|
||||
scene.add(hemiLight);
|
||||
hemiLightRef.current = hemiLight;
|
||||
|
||||
// 6. Floor Grid (Cores refinadas e ultrafinas para modo claro e escuro)
|
||||
const updateGridTheme = (gridHelper: THREE.GridHelper) => {
|
||||
@@ -278,6 +289,16 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
|
||||
const [isRotationLocked, setIsRotationLocked] = useState<boolean>(false);
|
||||
const { toast } = useToast();
|
||||
|
||||
// Efeito dinâmico de ajuste de Luminosidade / Iluminação no ModelViewer3D
|
||||
useEffect(() => {
|
||||
const factor = brightness / 100;
|
||||
if (ambientLightRef.current) ambientLightRef.current.intensity = 0.75 * factor;
|
||||
if (hemiLightRef.current) hemiLightRef.current.intensity = 0.5 * factor;
|
||||
if (dirLight1Ref.current) dirLight1Ref.current.intensity = 1.2 * Math.min(1.4, factor);
|
||||
if (dirLight2Ref.current) dirLight2Ref.current.intensity = 0.6 * factor;
|
||||
if (rendererRef.current) rendererRef.current.toneMappingExposure = 1.0 * (0.85 + 0.15 * factor);
|
||||
}, [brightness]);
|
||||
|
||||
// Function: Fit Model to View
|
||||
const fitModelToView = useCallback((group?: THREE.Group | null) => {
|
||||
const targetGroup = group || modelGroupRef.current;
|
||||
@@ -1142,6 +1163,8 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
|
||||
onSelectProcess={setSelectedProcess}
|
||||
onFitView={() => fitModelToView()}
|
||||
onToggleFullscreen={handleToggleFullscreen}
|
||||
brightness={brightness}
|
||||
onBrightnessChange={setBrightness}
|
||||
/>
|
||||
|
||||
{/* Top Right: ViewCube + Painel de Ações de Seleção de Peças */}
|
||||
@@ -1202,11 +1225,54 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Card de Ajuste e Travamento de Rotação 3D (Tombamento 90° + Cadeado Memorizável) */}
|
||||
<div
|
||||
className="flex items-center gap-1.5 p-1.5 rounded-2xl bg-white/95 dark:bg-slate-900/95 backdrop-blur-md border border-slate-300 dark:border-slate-700 shadow-lg shadow-slate-300/30 dark:shadow-slate-950/50 pointer-events-auto"
|
||||
style={{ minWidth: '130px' }}
|
||||
>
|
||||
{/* Barra de Ferramentas Flutuante Rápida: Luminosidade + Enquadrar + Rotação 90° & Cadeado */}
|
||||
<div className="flex items-center gap-2 pointer-events-auto">
|
||||
{/* Card de Luminosidade */}
|
||||
<div
|
||||
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-2xl bg-white/95 dark:bg-slate-900/95 backdrop-blur-md border border-slate-300 dark:border-slate-700 shadow-lg shadow-slate-300/30 dark:shadow-slate-950/50"
|
||||
title="Ajuste de Luminosidade da Estrutura (arraste para clarear ou suavizar sombras das peças)"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setBrightness(100)}
|
||||
title="Clique para resetar luminosidade em 100%"
|
||||
className="hover:scale-110 active:scale-95 transition-transform"
|
||||
>
|
||||
<SunMedium className="w-3.5 h-3.5 text-amber-500 shrink-0" />
|
||||
</button>
|
||||
<input
|
||||
type="range"
|
||||
min="40"
|
||||
max="220"
|
||||
step="5"
|
||||
value={brightness}
|
||||
onChange={(e) => setBrightness(Number(e.target.value))}
|
||||
className="w-16 sm:w-20 h-1.5 bg-slate-300 dark:bg-slate-700 rounded-lg appearance-none cursor-pointer accent-amber-500"
|
||||
/>
|
||||
<span className="text-[10px] font-mono font-bold text-slate-600 dark:text-slate-300 min-w-[28px] text-right">
|
||||
{brightness}%
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Botão Enquadrar / Centralizar Estrutura (Fit to Screen) */}
|
||||
<button
|
||||
onClick={() => fitModelToView(modelGroupRef.current)}
|
||||
disabled={!modelData}
|
||||
className={`flex items-center justify-center p-2 rounded-2xl bg-white/95 dark:bg-slate-900/95 backdrop-blur-md border border-slate-300 dark:border-slate-700 shadow-lg shadow-slate-300/30 dark:shadow-slate-950/50 transition-all duration-150 active:scale-95 ${
|
||||
modelData
|
||||
? 'text-slate-700 hover:text-cyan-600 dark:text-slate-200 dark:hover:text-cyan-400 hover:bg-slate-100 dark:hover:bg-slate-800 cursor-pointer'
|
||||
: 'text-slate-400 dark:text-slate-600 opacity-40 cursor-not-allowed'
|
||||
}`}
|
||||
title="Centralizar e Enquadrar Estrutura na Janela (Fit to Screen)"
|
||||
>
|
||||
<Focus className="w-4 h-4 text-cyan-600 dark:text-cyan-400" />
|
||||
</button>
|
||||
|
||||
{/* Card de Ajuste e Travamento de Rotação 3D (Tombamento 90° + Cadeado Memorizável) */}
|
||||
<div
|
||||
className="flex items-center gap-1.5 p-1.5 rounded-2xl bg-white/95 dark:bg-slate-900/95 backdrop-blur-md border border-slate-300 dark:border-slate-700 shadow-lg shadow-slate-300/30 dark:shadow-slate-950/50 pointer-events-auto"
|
||||
style={{ minWidth: '130px' }}
|
||||
>
|
||||
{/* Botão Rotacionar / Tombar 90° */}
|
||||
<button
|
||||
onClick={handleRotate90X}
|
||||
@@ -1248,6 +1314,7 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
|
||||
<Unlock className="w-4 h-4 text-amber-600 dark:text-amber-400" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -16,7 +16,8 @@ import {
|
||||
Wrench,
|
||||
Paintbrush,
|
||||
Truck,
|
||||
Scissors
|
||||
Scissors,
|
||||
SunMedium
|
||||
} from 'lucide-react';
|
||||
|
||||
interface Viewer3DSidebarProps {
|
||||
@@ -32,6 +33,8 @@ interface Viewer3DSidebarProps {
|
||||
onSelectProcess?: (proc: string) => void;
|
||||
onFitView: () => void;
|
||||
onToggleFullscreen: () => void;
|
||||
brightness?: number;
|
||||
onBrightnessChange?: (val: number) => void;
|
||||
}
|
||||
|
||||
const FABRICATION_PROCESSES = [
|
||||
@@ -56,6 +59,8 @@ export const Viewer3DSidebar: React.FC<Viewer3DSidebarProps> = ({
|
||||
onSelectProcess,
|
||||
onFitView,
|
||||
onToggleFullscreen,
|
||||
brightness = 100,
|
||||
onBrightnessChange,
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState<boolean>(true);
|
||||
|
||||
@@ -64,36 +69,36 @@ export const Viewer3DSidebar: React.FC<Viewer3DSidebarProps> = ({
|
||||
{/* 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-14rem)]' : 'w-0 border-0 opacity-0 pointer-events-none'
|
||||
isOpen ? 'w-60 max-h-[calc(100%-2rem)]' : '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">
|
||||
{/* Cabeçalho do Sub-Menu Enxuto */}
|
||||
<div className="flex items-center justify-between px-3 py-1.5 bg-slate-100/90 dark:bg-slate-950/80 border-b border-slate-200 dark:border-slate-800">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Sliders className="w-3.5 h-3.5 text-cyan-600 dark:text-cyan-400" />
|
||||
<span className="text-[11px] 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">
|
||||
<span className="text-[9px] font-mono px-1 py-0.2 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: Destaque por Processo Industrial com Cores Correspondentes */}
|
||||
<div className="space-y-1.5">
|
||||
{/* Corpo Compactado com Rolagem Suave */}
|
||||
<div className="flex-1 overflow-y-auto p-2 space-y-2 custom-scrollbar">
|
||||
{/* Grupo 1: Destaque por Processo Industrial */}
|
||||
<div className="space-y-1">
|
||||
<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 className="text-[10px] font-bold uppercase tracking-wider text-slate-600 dark:text-slate-400 flex items-center gap-1">
|
||||
<CheckCircle2 className="w-3 h-3 text-emerald-600 dark:text-emerald-400" />
|
||||
Processo Fabril
|
||||
</label>
|
||||
<span className="text-[9px] font-mono text-emerald-600 dark:text-emerald-400 font-bold">
|
||||
BIM + Fábrica
|
||||
<span className="text-[8px] font-mono text-emerald-600 dark:text-emerald-400 font-bold">
|
||||
BIM
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-1.5 bg-slate-100 dark:bg-slate-950/60 p-1.5 rounded-xl border border-slate-200 dark:border-slate-800">
|
||||
<div className="grid grid-cols-3 gap-1 bg-slate-100 dark:bg-slate-950/60 p-1 rounded-lg border border-slate-200 dark:border-slate-800">
|
||||
{FABRICATION_PROCESSES.map((proc) => {
|
||||
const Icon = proc.icon;
|
||||
const isSelected = selectedProcess === proc.id;
|
||||
@@ -106,24 +111,22 @@ export const Viewer3DSidebar: React.FC<Viewer3DSidebarProps> = ({
|
||||
? {
|
||||
backgroundColor: proc.color,
|
||||
color: '#ffffff',
|
||||
boxShadow: `0 4px 12px ${proc.color}50`,
|
||||
boxShadow: `0 2px 8px ${proc.color}40`,
|
||||
borderColor: proc.color,
|
||||
}
|
||||
: {
|
||||
backgroundColor: `${proc.color}18`,
|
||||
backgroundColor: `${proc.color}14`,
|
||||
color: proc.color,
|
||||
borderColor: `${proc.color}35`,
|
||||
borderColor: `${proc.color}25`,
|
||||
}
|
||||
}
|
||||
className={`flex flex-col items-center justify-center gap-1 py-2 px-1 rounded-lg text-[10px] font-bold border transition-all duration-200 hover:scale-[1.03] active:scale-[0.98] ${
|
||||
isSelected
|
||||
? 'shadow-md border-transparent font-extrabold'
|
||||
: 'hover:brightness-110'
|
||||
className={`flex flex-col items-center justify-center gap-0.5 py-1 px-1 rounded-md text-[9.5px] font-bold border transition-all duration-150 hover:scale-[1.02] active:scale-[0.98] ${
|
||||
isSelected ? 'border-transparent font-extrabold' : 'hover:brightness-110'
|
||||
}`}
|
||||
title={`Destacar peças em ${proc.label} (${proc.color}) e não apontadas em aramado cinza`}
|
||||
title={`Destacar peças em ${proc.label}`}
|
||||
>
|
||||
<Icon className="w-3.5 h-3.5 stroke-[2.2]" />
|
||||
<span className="tracking-tight">{proc.label}</span>
|
||||
<Icon className="w-3 h-3 stroke-[2.2]" />
|
||||
<span className="tracking-tight leading-none">{proc.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
@@ -131,99 +134,93 @@ export const Viewer3DSidebar: React.FC<Viewer3DSidebarProps> = ({
|
||||
</div>
|
||||
|
||||
{/* Grupo 2: Modo de Cores Geral */}
|
||||
<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" />
|
||||
<div className="space-y-1">
|
||||
<label className="text-[10px] font-bold uppercase tracking-wider text-slate-600 dark:text-slate-400 flex items-center gap-1">
|
||||
<Palette className="w-3 h-3 text-cyan-600 dark:text-cyan-400" />
|
||||
Modo 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">
|
||||
<div className="grid grid-cols-2 gap-1 bg-slate-100 dark:bg-slate-950/60 p-0.5 rounded-lg 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 ${
|
||||
className={`flex items-center justify-center gap-1 px-1.5 py-1 rounded-md text-[11px] font-bold transition-all ${
|
||||
colorMode === 'production'
|
||||
? 'bg-cyan-600 text-white shadow-md shadow-cyan-600/30'
|
||||
? 'bg-cyan-600 text-white shadow-sm'
|
||||
: 'text-slate-700 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<CheckCircle2 className="w-3.5 h-3.5" />
|
||||
<CheckCircle2 className="w-3 h-3" />
|
||||
<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 ${
|
||||
className={`flex items-center justify-center gap-1 px-1.5 py-1 rounded-md text-[11px] font-bold transition-all ${
|
||||
colorMode === 'description'
|
||||
? 'bg-amber-600 text-white shadow-md shadow-amber-600/30'
|
||||
? 'bg-amber-600 text-white shadow-sm'
|
||||
: 'text-slate-700 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<Box className="w-3.5 h-3.5" />
|
||||
<Box className="w-3 h-3" />
|
||||
<span>Padrão</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Grupo 3: Ambiente & 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" />
|
||||
<div className="space-y-1">
|
||||
<label className="text-[10px] font-bold uppercase tracking-wider text-slate-600 dark:text-slate-400 flex items-center gap-1">
|
||||
<GridIcon className="w-3 h-3 text-cyan-600 dark:text-cyan-400" />
|
||||
Ambiente & Piso
|
||||
</label>
|
||||
<div>
|
||||
{/* Toggle Grid */}
|
||||
<button
|
||||
onClick={onToggleGrid}
|
||||
className={`w-full flex items-center justify-center gap-2 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>Grade de Piso: {showGrid ? 'LIGADO' : 'DESL'}</span>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={onToggleGrid}
|
||||
className={`w-full flex items-center justify-center gap-1.5 py-1 rounded-lg text-[11px] 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-xs'
|
||||
: '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 h-3" />
|
||||
<span>Grade de Piso: {showGrid ? 'LIGADO' : 'DESL'}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Grupo 4: 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" />
|
||||
{/* Grupo 4: Câmera & Visualização */}
|
||||
<div className="space-y-1">
|
||||
<label className="text-[10px] font-bold uppercase tracking-wider text-slate-600 dark:text-slate-400 flex items-center gap-1">
|
||||
<Compass className="w-3 h-3 text-cyan-600 dark:text-cyan-400" />
|
||||
Câmera & Visualização
|
||||
</label>
|
||||
<div className="grid grid-cols-2 gap-1.5">
|
||||
{/* Câmera Toggle */}
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
<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 ${
|
||||
className={`flex items-center justify-center gap-1 px-1.5 py-1 rounded-lg text-[11px] 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-emerald-50 dark:bg-emerald-950/40 text-emerald-800 dark:text-emerald-300 border-emerald-300 dark:border-emerald-800 shadow-xs'
|
||||
: '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" />
|
||||
<Box className="w-3 h-3" />
|
||||
<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"
|
||||
className="flex items-center justify-center gap-1 px-1.5 py-1 rounded-lg text-[11px] 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" />
|
||||
<Focus className="w-3 h-3 text-cyan-600 dark:text-cyan-400" />
|
||||
<span>Enquadrar</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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="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" />
|
||||
{/* Grupo 5: Opacidade da Estrutura Compacta */}
|
||||
<div className="space-y-1 bg-slate-50 dark:bg-slate-950/60 p-2 rounded-lg border border-slate-200 dark:border-slate-800">
|
||||
<div className="flex items-center justify-between text-[11px]">
|
||||
<span className="font-semibold text-slate-700 dark:text-slate-300 flex items-center gap-1">
|
||||
<Eye className="w-3 h-3 text-amber-500" />
|
||||
Opacidade
|
||||
</span>
|
||||
<span className="font-mono font-bold text-amber-700 dark:text-amber-400">{opacity}%</span>
|
||||
<span className="font-mono font-bold text-amber-700 dark:text-amber-400 text-[10px]">{opacity}%</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
@@ -231,9 +228,9 @@ export const Viewer3DSidebar: React.FC<Viewer3DSidebarProps> = ({
|
||||
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"
|
||||
className="w-full h-1 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">
|
||||
<div className="flex justify-between text-[9px] font-mono text-slate-400 pt-0.5">
|
||||
<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>
|
||||
@@ -241,12 +238,12 @@ export const Viewer3DSidebar: React.FC<Viewer3DSidebarProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Botão Tela Cheia */}
|
||||
{/* Botão Tela Cheia Compacto */}
|
||||
<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"
|
||||
className="w-full flex items-center justify-center gap-1.5 py-1.5 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-lg text-[11px] font-bold transition-all shadow-xs"
|
||||
>
|
||||
<Maximize2 className="w-3.5 h-3.5 text-cyan-600 dark:text-cyan-400" />
|
||||
<Maximize2 className="w-3 h-3 text-cyan-600 dark:text-cyan-400" />
|
||||
<span>Expandir Tela Cheia</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -695,7 +695,7 @@ export default function Visualizador3D() {
|
||||
</div>
|
||||
|
||||
{/* Main 3D Viewport */}
|
||||
<div className="relative flex-1 min-h-[480px] w-full bg-slate-100 dark:bg-slate-950 rounded-2xl overflow-hidden border border-slate-200 dark:border-slate-800 shadow-xl dark:shadow-2xl">
|
||||
<div className="relative flex-1 min-h-[560px] w-full bg-slate-100 dark:bg-slate-950 rounded-2xl overflow-hidden border border-slate-200 dark:border-slate-800 shadow-xl dark:shadow-2xl">
|
||||
{isLoadingModel && (
|
||||
<div className="absolute inset-0 z-30 flex flex-col items-center justify-center p-6 bg-white dark:bg-slate-950/90 backdrop-blur-md text-slate-900 dark:text-white space-y-4">
|
||||
<div className="p-4 bg-cyan-100 text-cyan-600 dark:bg-cyan-500/20 dark:text-cyan-400 rounded-2xl border border-cyan-300 dark:border-cyan-500/40 animate-pulse">
|
||||
|
||||
Reference in New Issue
Block a user