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,
|
AlertTriangle,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
HelpCircle,
|
HelpCircle,
|
||||||
Cpu
|
Cpu,
|
||||||
|
Focus,
|
||||||
|
SunMedium,
|
||||||
|
Compass
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { supabase } from '@/integrations/supabase/client';
|
import { supabase } from '@/integrations/supabase/client';
|
||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
@@ -66,11 +69,21 @@ export const AnalyzerViewer3D: React.FC<AnalyzerViewer3DProps> = ({
|
|||||||
const canvasContainerRef = 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 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 controlsRef = useRef<OrbitControls | null>(null);
|
||||||
const modelGroupRef = useRef<THREE.Group | null>(null);
|
const modelGroupRef = useRef<THREE.Group | null>(null);
|
||||||
const gridRef = useRef<THREE.GridHelper | 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 [wireframe, setWireframe] = useState<boolean>(false);
|
||||||
const [hoveredDefect, setHoveredDefect] = useState<DefectItem | null>(null);
|
const [hoveredDefect, setHoveredDefect] = useState<DefectItem | null>(null);
|
||||||
const [hoverPos, setHoverPos] = useState<{ x: number; y: number } | 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();
|
const scene = new THREE.Scene();
|
||||||
sceneRef.current = scene;
|
sceneRef.current = scene;
|
||||||
|
|
||||||
const camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 2000);
|
const perspCamera = new THREE.PerspectiveCamera(45, width / height, 0.1, 2000);
|
||||||
camera.position.set(40, 35, 50);
|
perspCamera.position.set(40, 35, 50);
|
||||||
cameraRef.current = camera;
|
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({
|
const renderer = new THREE.WebGLRenderer({
|
||||||
antialias: true,
|
antialias: true,
|
||||||
@@ -116,24 +144,32 @@ export const AnalyzerViewer3D: React.FC<AnalyzerViewer3DProps> = ({
|
|||||||
canvasContainerRef.current.innerHTML = '';
|
canvasContainerRef.current.innerHTML = '';
|
||||||
canvasContainerRef.current.appendChild(renderer.domElement);
|
canvasContainerRef.current.appendChild(renderer.domElement);
|
||||||
|
|
||||||
const controls = new OrbitControls(camera, renderer.domElement);
|
const controls = new OrbitControls(perspCamera, renderer.domElement);
|
||||||
controls.enableDamping = true;
|
controls.enableDamping = true;
|
||||||
controls.dampingFactor = 0.05;
|
controls.dampingFactor = 0.05;
|
||||||
controls.maxDistance = 1500;
|
controls.maxDistance = 1500;
|
||||||
controls.minDistance = 0.2;
|
controls.minDistance = 0.2;
|
||||||
controlsRef.current = controls;
|
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);
|
const ambientLight = new THREE.AmbientLight(0xffffff, 0.85);
|
||||||
scene.add(ambientLight);
|
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);
|
const dirLight1 = new THREE.DirectionalLight(0xffffff, 1.2);
|
||||||
dirLight1.position.set(80, 120, 80);
|
dirLight1.position.set(80, 120, 80);
|
||||||
scene.add(dirLight1);
|
scene.add(dirLight1);
|
||||||
|
dirLight1Ref.current = dirLight1;
|
||||||
|
|
||||||
const dirLight2 = new THREE.DirectionalLight(0x94a3b8, 0.6);
|
const dirLight2 = new THREE.DirectionalLight(0x94a3b8, 0.6);
|
||||||
dirLight2.position.set(-80, -40, -80);
|
dirLight2.position.set(-80, -40, -80);
|
||||||
scene.add(dirLight2);
|
scene.add(dirLight2);
|
||||||
|
dirLight2Ref.current = dirLight2;
|
||||||
|
|
||||||
// Grid no piso com cores refinadas para modo claro e escuro
|
// Grid no piso com cores refinadas para modo claro e escuro
|
||||||
const isDarkMode = document.documentElement.classList.contains('dark');
|
const isDarkMode = document.documentElement.classList.contains('dark');
|
||||||
@@ -178,16 +214,31 @@ export const AnalyzerViewer3D: React.FC<AnalyzerViewer3DProps> = ({
|
|||||||
const animate = () => {
|
const animate = () => {
|
||||||
animId = requestAnimationFrame(animate);
|
animId = requestAnimationFrame(animate);
|
||||||
controls.update();
|
controls.update();
|
||||||
renderer.render(scene, camera);
|
if (renderer && scene && cameraRef.current) {
|
||||||
|
renderer.render(scene, cameraRef.current);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
animate();
|
animate();
|
||||||
|
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
if (!canvasContainerRef.current || !renderer || !camera) return;
|
if (!canvasContainerRef.current || !renderer) return;
|
||||||
const w = canvasContainerRef.current.clientWidth;
|
const w = canvasContainerRef.current.clientWidth;
|
||||||
const h = canvasContainerRef.current.clientHeight;
|
const h = canvasContainerRef.current.clientHeight;
|
||||||
camera.aspect = w / h;
|
if (w <= 0 || h <= 0) return;
|
||||||
camera.updateProjectionMatrix();
|
|
||||||
|
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);
|
renderer.setSize(w, h);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -205,7 +256,45 @@ export const AnalyzerViewer3D: React.FC<AnalyzerViewer3DProps> = ({
|
|||||||
};
|
};
|
||||||
}, [theme]);
|
}, [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 fitModelToView = useCallback((targetObj?: THREE.Object3D) => {
|
||||||
const obj = targetObj || modelGroupRef.current;
|
const obj = targetObj || modelGroupRef.current;
|
||||||
if (!obj || !cameraRef.current || !controlsRef.current) return;
|
if (!obj || !cameraRef.current || !controlsRef.current) return;
|
||||||
@@ -224,11 +313,27 @@ export const AnalyzerViewer3D: React.FC<AnalyzerViewer3DProps> = ({
|
|||||||
cameraDistance = Math.max(cameraDistance, 5);
|
cameraDistance = Math.max(cameraDistance, 5);
|
||||||
|
|
||||||
const camera = cameraRef.current;
|
const camera = cameraRef.current;
|
||||||
camera.near = Math.max(0.01, maxDim / 500);
|
if ((camera as THREE.PerspectiveCamera).isPerspectiveCamera) {
|
||||||
camera.far = Math.max(2000, maxDim * 20);
|
const pCam = camera as THREE.PerspectiveCamera;
|
||||||
camera.updateProjectionMatrix();
|
pCam.near = Math.max(0.01, maxDim / 500);
|
||||||
camera.position.set(center.x + cameraDistance * 0.7, center.y + cameraDistance * 0.5, center.z + cameraDistance * 0.7);
|
pCam.far = Math.max(2000, maxDim * 20);
|
||||||
camera.lookAt(center);
|
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.target.copy(center);
|
||||||
controlsRef.current.maxDistance = Math.max(1500, maxDim * 10);
|
controlsRef.current.maxDistance = Math.max(1500, maxDim * 10);
|
||||||
@@ -418,19 +523,54 @@ export const AnalyzerViewer3D: React.FC<AnalyzerViewer3DProps> = ({
|
|||||||
emissiveIntensity = 0.35;
|
emissiveIntensity = 0.35;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aplica o material atualizado
|
// 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({
|
obj.material = new THREE.MeshStandardMaterial({
|
||||||
color: new THREE.Color(color),
|
color: new THREE.Color(color),
|
||||||
emissive: new THREE.Color(emissiveColor),
|
emissive: new THREE.Color(emissiveColor),
|
||||||
emissiveIntensity,
|
emissiveIntensity,
|
||||||
roughness: 0.35,
|
roughness: 0.35,
|
||||||
metalness: 0.2,
|
metalness: 0.2,
|
||||||
wireframe,
|
wireframe: false,
|
||||||
transparent,
|
transparent,
|
||||||
opacity,
|
opacity,
|
||||||
side: THREE.DoubleSide,
|
side: THREE.DoubleSide,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}, [colorizationMode, selectedDefect, selectedPieceMark, isGhostMode, wireframe, modelData]);
|
}, [colorizationMode, selectedDefect, selectedPieceMark, isGhostMode, wireframe, modelData]);
|
||||||
|
|
||||||
@@ -598,51 +738,108 @@ export const AnalyzerViewer3D: React.FC<AnalyzerViewer3DProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Barra de Ferramentas Flutuante Superior */}
|
{/* 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
|
<button
|
||||||
onClick={handleResetCamera}
|
onClick={handleResetCamera}
|
||||||
title="Resetar Câmera"
|
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" />
|
<RotateCcw className="w-4 h-4" />
|
||||||
</button>
|
</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
|
<button
|
||||||
onClick={onToggleGhostMode}
|
onClick={onToggleGhostMode}
|
||||||
title={isGhostMode ? 'Desativar Modo Raio-X / Foco' : 'Ativar Modo Raio-X / Foco'}
|
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
|
isGhostMode
|
||||||
? 'bg-indigo-600 text-white shadow'
|
? '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'
|
: '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" />
|
<Eye className="w-3.5 h-3.5" />
|
||||||
<span>Foco / Raio-X</span>
|
<span className="hidden sm:inline">Foco / Raio-X</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* Aramado Limpo (EdgesGeometry) */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setWireframe(!wireframe)}
|
onClick={() => setWireframe(!wireframe)}
|
||||||
title="Alternar Estrutura de Arame (Wireframe)"
|
title={wireframe ? 'Mudar para Modelo Sólido' : 'Mudar para Aramado Limpo de Engenharia (sem linhas internas)'}
|
||||||
className={`p-1.5 rounded transition ${
|
className={`p-1.5 rounded-lg transition ${
|
||||||
wireframe
|
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'
|
: '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>
|
</button>
|
||||||
|
|
||||||
{onToggleFullscreen && (
|
{onToggleFullscreen && (
|
||||||
<button
|
<button
|
||||||
onClick={onToggleFullscreen}
|
onClick={onToggleFullscreen}
|
||||||
title={isFullscreen ? 'Sair da Tela Cheia' : 'Tela Cheia'}
|
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" />}
|
{isFullscreen ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
|
||||||
</button>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* Legenda de Cores Flutuante Inferior */}
|
{/* Legenda de Cores Flutuante Inferior */}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { ViewCube } from './ViewCube';
|
|||||||
import { Viewer3DSidebar } from './Viewer3DSidebar';
|
import { Viewer3DSidebar } from './Viewer3DSidebar';
|
||||||
import { PieceHoverTooltip } from './PieceHoverTooltip';
|
import { PieceHoverTooltip } from './PieceHoverTooltip';
|
||||||
import { SelectionListModal, SelectedPieceItem } from './SelectionListModal';
|
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 { supabase } from '@/integrations/supabase/client';
|
||||||
import { useToast } from '@/components/ui/use-toast';
|
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 [colorMode, setColorMode] = useState<'description' | 'production'>('production');
|
||||||
const [selectedProcess, setSelectedProcess] = useState<string>('all');
|
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)
|
// Hover Tooltip States (1.0 second delay)
|
||||||
const hoverTimerRef = useRef<NodeJS.Timeout | null>(null);
|
const hoverTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
const [hoveredPiece, setHoveredPiece] = useState<PieceInfo | null>(null);
|
const [hoveredPiece, setHoveredPiece] = useState<PieceInfo | null>(null);
|
||||||
@@ -161,18 +168,22 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
|
|||||||
// 5. Lights
|
// 5. Lights
|
||||||
const ambientLight = new THREE.AmbientLight(0xffffff, 0.75);
|
const ambientLight = new THREE.AmbientLight(0xffffff, 0.75);
|
||||||
scene.add(ambientLight);
|
scene.add(ambientLight);
|
||||||
|
ambientLightRef.current = ambientLight;
|
||||||
|
|
||||||
const dirLight1 = new THREE.DirectionalLight(0xffffff, 1.2);
|
const dirLight1 = new THREE.DirectionalLight(0xffffff, 1.2);
|
||||||
dirLight1.position.set(100, 200, 100);
|
dirLight1.position.set(100, 200, 100);
|
||||||
dirLight1.castShadow = true;
|
dirLight1.castShadow = true;
|
||||||
scene.add(dirLight1);
|
scene.add(dirLight1);
|
||||||
|
dirLight1Ref.current = dirLight1;
|
||||||
|
|
||||||
const dirLight2 = new THREE.DirectionalLight(0x94a3b8, 0.6);
|
const dirLight2 = new THREE.DirectionalLight(0x94a3b8, 0.6);
|
||||||
dirLight2.position.set(-100, -50, -100);
|
dirLight2.position.set(-100, -50, -100);
|
||||||
scene.add(dirLight2);
|
scene.add(dirLight2);
|
||||||
|
dirLight2Ref.current = dirLight2;
|
||||||
|
|
||||||
const hemiLight = new THREE.HemisphereLight(0xffffff, 0x1e293b, 0.5);
|
const hemiLight = new THREE.HemisphereLight(0xffffff, 0x1e293b, 0.5);
|
||||||
scene.add(hemiLight);
|
scene.add(hemiLight);
|
||||||
|
hemiLightRef.current = hemiLight;
|
||||||
|
|
||||||
// 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) => {
|
||||||
@@ -278,6 +289,16 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
|
|||||||
const [isRotationLocked, setIsRotationLocked] = useState<boolean>(false);
|
const [isRotationLocked, setIsRotationLocked] = useState<boolean>(false);
|
||||||
const { toast } = useToast();
|
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
|
// Function: Fit Model to View
|
||||||
const fitModelToView = useCallback((group?: THREE.Group | null) => {
|
const fitModelToView = useCallback((group?: THREE.Group | null) => {
|
||||||
const targetGroup = group || modelGroupRef.current;
|
const targetGroup = group || modelGroupRef.current;
|
||||||
@@ -1142,6 +1163,8 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
|
|||||||
onSelectProcess={setSelectedProcess}
|
onSelectProcess={setSelectedProcess}
|
||||||
onFitView={() => fitModelToView()}
|
onFitView={() => fitModelToView()}
|
||||||
onToggleFullscreen={handleToggleFullscreen}
|
onToggleFullscreen={handleToggleFullscreen}
|
||||||
|
brightness={brightness}
|
||||||
|
onBrightnessChange={setBrightness}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Top Right: ViewCube + Painel de Ações de Seleção de Peças */}
|
{/* Top Right: ViewCube + Painel de Ações de Seleção de Peças */}
|
||||||
@@ -1202,6 +1225,49 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 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) */}
|
{/* Card de Ajuste e Travamento de Rotação 3D (Tombamento 90° + Cadeado Memorizável) */}
|
||||||
<div
|
<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"
|
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"
|
||||||
@@ -1250,6 +1316,7 @@ export const ModelViewer3D: React.FC<ModelViewer3DProps> = ({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Hover Tooltip (1.0s delay trigger) */}
|
{/* Hover Tooltip (1.0s delay trigger) */}
|
||||||
<PieceHoverTooltip
|
<PieceHoverTooltip
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ import {
|
|||||||
Wrench,
|
Wrench,
|
||||||
Paintbrush,
|
Paintbrush,
|
||||||
Truck,
|
Truck,
|
||||||
Scissors
|
Scissors,
|
||||||
|
SunMedium
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
interface Viewer3DSidebarProps {
|
interface Viewer3DSidebarProps {
|
||||||
@@ -32,6 +33,8 @@ interface Viewer3DSidebarProps {
|
|||||||
onSelectProcess?: (proc: string) => void;
|
onSelectProcess?: (proc: string) => void;
|
||||||
onFitView: () => void;
|
onFitView: () => void;
|
||||||
onToggleFullscreen: () => void;
|
onToggleFullscreen: () => void;
|
||||||
|
brightness?: number;
|
||||||
|
onBrightnessChange?: (val: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FABRICATION_PROCESSES = [
|
const FABRICATION_PROCESSES = [
|
||||||
@@ -56,6 +59,8 @@ export const Viewer3DSidebar: React.FC<Viewer3DSidebarProps> = ({
|
|||||||
onSelectProcess,
|
onSelectProcess,
|
||||||
onFitView,
|
onFitView,
|
||||||
onToggleFullscreen,
|
onToggleFullscreen,
|
||||||
|
brightness = 100,
|
||||||
|
onBrightnessChange,
|
||||||
}) => {
|
}) => {
|
||||||
const [isOpen, setIsOpen] = useState<boolean>(true);
|
const [isOpen, setIsOpen] = useState<boolean>(true);
|
||||||
|
|
||||||
@@ -64,36 +69,36 @@ 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-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 */}
|
{/* Cabeçalho do Sub-Menu Enxuto */}
|
||||||
<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 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-2">
|
<div className="flex items-center gap-1.5">
|
||||||
<Sliders className="w-4 h-4 text-cyan-600 dark:text-cyan-400" />
|
<Sliders className="w-3.5 h-3.5 text-cyan-600 dark:text-cyan-400" />
|
||||||
<span className="text-xs font-bold uppercase tracking-wider text-slate-800 dark:text-slate-200">
|
<span className="text-[11px] font-bold uppercase tracking-wider text-slate-800 dark:text-slate-200">
|
||||||
Controles 3D
|
Controles 3D
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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
|
SteelXR
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Corpo com Rolagem Suave */}
|
{/* Corpo Compactado 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-2 space-y-2 custom-scrollbar">
|
||||||
{/* Grupo 1: Destaque por Processo Industrial com Cores Correspondentes */}
|
{/* Grupo 1: Destaque por Processo Industrial */}
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1">
|
||||||
<div className="flex items-center justify-between">
|
<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">
|
<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.5 h-3.5 text-emerald-600 dark:text-emerald-400" />
|
<CheckCircle2 className="w-3 h-3 text-emerald-600 dark:text-emerald-400" />
|
||||||
Destaque por Processo
|
Processo Fabril
|
||||||
</label>
|
</label>
|
||||||
<span className="text-[9px] font-mono text-emerald-600 dark:text-emerald-400 font-bold">
|
<span className="text-[8px] font-mono text-emerald-600 dark:text-emerald-400 font-bold">
|
||||||
BIM + Fábrica
|
BIM
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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) => {
|
{FABRICATION_PROCESSES.map((proc) => {
|
||||||
const Icon = proc.icon;
|
const Icon = proc.icon;
|
||||||
const isSelected = selectedProcess === proc.id;
|
const isSelected = selectedProcess === proc.id;
|
||||||
@@ -106,24 +111,22 @@ export const Viewer3DSidebar: React.FC<Viewer3DSidebarProps> = ({
|
|||||||
? {
|
? {
|
||||||
backgroundColor: proc.color,
|
backgroundColor: proc.color,
|
||||||
color: '#ffffff',
|
color: '#ffffff',
|
||||||
boxShadow: `0 4px 12px ${proc.color}50`,
|
boxShadow: `0 2px 8px ${proc.color}40`,
|
||||||
borderColor: proc.color,
|
borderColor: proc.color,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
backgroundColor: `${proc.color}18`,
|
backgroundColor: `${proc.color}14`,
|
||||||
color: proc.color,
|
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] ${
|
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
|
isSelected ? 'border-transparent font-extrabold' : 'hover:brightness-110'
|
||||||
? 'shadow-md 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]" />
|
<Icon className="w-3 h-3 stroke-[2.2]" />
|
||||||
<span className="tracking-tight">{proc.label}</span>
|
<span className="tracking-tight leading-none">{proc.label}</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -131,99 +134,93 @@ export const Viewer3DSidebar: React.FC<Viewer3DSidebarProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Grupo 2: Modo de Cores Geral */}
|
{/* Grupo 2: Modo de Cores Geral */}
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1">
|
||||||
<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-[10px] font-bold uppercase tracking-wider text-slate-600 dark:text-slate-400 flex items-center gap-1">
|
||||||
<Palette className="w-3.5 h-3.5 text-cyan-600 dark:text-cyan-400" />
|
<Palette className="w-3 h-3 text-cyan-600 dark:text-cyan-400" />
|
||||||
Modo 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 bg-slate-100 dark:bg-slate-950/60 p-0.5 rounded-lg 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 px-1.5 py-1 rounded-md text-[11px] font-bold transition-all ${
|
||||||
colorMode === 'production'
|
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'
|
: '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>
|
<span>Produção</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => onColorModeChange('description')}
|
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'
|
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'
|
: '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>
|
<span>Padrão</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Grupo 3: Ambiente & Grade */}
|
{/* Grupo 3: Ambiente & Grade */}
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1">
|
||||||
<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-[10px] font-bold uppercase tracking-wider text-slate-600 dark:text-slate-400 flex items-center gap-1">
|
||||||
<GridIcon className="w-3.5 h-3.5 text-cyan-600 dark:text-cyan-400" />
|
<GridIcon className="w-3 h-3 text-cyan-600 dark:text-cyan-400" />
|
||||||
Ambiente & Piso
|
Ambiente & Piso
|
||||||
</label>
|
</label>
|
||||||
<div>
|
|
||||||
{/* Toggle Grid */}
|
|
||||||
<button
|
<button
|
||||||
onClick={onToggleGrid}
|
onClick={onToggleGrid}
|
||||||
className={`w-full flex items-center justify-center gap-2 py-2 rounded-xl text-xs font-semibold border transition-all ${
|
className={`w-full flex items-center justify-center gap-1.5 py-1 rounded-lg text-[11px] font-semibold border transition-all ${
|
||||||
showGrid
|
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-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'
|
: '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" />
|
<GridIcon className="w-3 h-3" />
|
||||||
<span>Grade de Piso: {showGrid ? 'LIGADO' : 'DESL'}</span>
|
<span>Grade de Piso: {showGrid ? 'LIGADO' : 'DESL'}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Grupo 4: Câmera & Projeção */}
|
{/* Grupo 4: Câmera & Visualização */}
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1">
|
||||||
<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-[10px] font-bold uppercase tracking-wider text-slate-600 dark:text-slate-400 flex items-center gap-1">
|
||||||
<Compass className="w-3.5 h-3.5 text-cyan-600 dark:text-cyan-400" />
|
<Compass className="w-3 h-3 text-cyan-600 dark:text-cyan-400" />
|
||||||
Câmera & Visualização
|
Câmera & Visualização
|
||||||
</label>
|
</label>
|
||||||
<div className="grid grid-cols-2 gap-1.5">
|
<div className="grid grid-cols-2 gap-1">
|
||||||
{/* Câmera Toggle */}
|
|
||||||
<button
|
<button
|
||||||
onClick={onToggleCamera}
|
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
|
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'
|
: '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>
|
<span>{isOrthographic ? 'Ortogonal' : 'Perspectiva'}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Enquadrar / Reset View */}
|
|
||||||
<button
|
<button
|
||||||
onClick={onFitView}
|
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"
|
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>
|
<span>Enquadrar</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Grupo 5: Opacidade da Estrutura */}
|
{/* Grupo 5: Opacidade da Estrutura Compacta */}
|
||||||
<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 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-xs">
|
<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.5">
|
<span className="font-semibold text-slate-700 dark:text-slate-300 flex items-center gap-1">
|
||||||
<Eye className="w-3.5 h-3.5 text-amber-500" />
|
<Eye className="w-3 h-3 text-amber-500" />
|
||||||
Opacidade
|
Opacidade
|
||||||
</span>
|
</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>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
@@ -231,9 +228,9 @@ export const Viewer3DSidebar: React.FC<Viewer3DSidebarProps> = ({
|
|||||||
max={100}
|
max={100}
|
||||||
value={opacity}
|
value={opacity}
|
||||||
onChange={(e) => onOpacityChange(Number(e.target.value))}
|
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(25)} className="hover:text-amber-600">25%</button>
|
||||||
<button onClick={() => onOpacityChange(50)} className="hover:text-amber-600">50%</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(75)} className="hover:text-amber-600">75%</button>
|
||||||
@@ -241,12 +238,12 @@ export const Viewer3DSidebar: React.FC<Viewer3DSidebarProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Botão Tela Cheia */}
|
{/* Botão Tela Cheia Compacto */}
|
||||||
<button
|
<button
|
||||||
onClick={onToggleFullscreen}
|
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>
|
<span>Expandir Tela Cheia</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -695,7 +695,7 @@ export default function Visualizador3D() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main 3D Viewport */}
|
{/* 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 && (
|
{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="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">
|
<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