fix: Calibragem do fundo no modo claro do visualizador 3D e prevenção de circular JSON

This commit is contained in:
2026-10-08 10:27:48 -03:00
parent f42976a09d
commit 7f4afc5e61
2 changed files with 118 additions and 36 deletions
+106 -35
View File
@@ -122,15 +122,45 @@ export const AnalyzerViewer3D: React.FC<AnalyzerViewer3DProps> = ({
dirLight2.position.set(-80, -40, -80);
scene.add(dirLight2);
// Grid no piso
const isDark = theme === 'dark' || document.documentElement.classList.contains('dark');
const gridColor1 = isDark ? 0x475569 : 0x94a3b8;
const gridColor2 = isDark ? 0x1e293b : 0xe2e8f0;
// Grid no piso com cores refinadas para modo claro e escuro
const isDarkMode = document.documentElement.classList.contains('dark');
const gridColor1 = isDarkMode ? 0x475569 : 0x94a3b8;
const gridColor2 = isDarkMode ? 0x1e293b : 0xe2e8f0;
const grid = new THREE.GridHelper(100, 50, gridColor1, gridColor2);
grid.position.y = -0.01;
if (grid.material && (grid.material as THREE.Material)) {
(grid.material as THREE.Material).transparent = true;
(grid.material as THREE.Material).opacity = 0.55;
}
scene.add(grid);
gridRef.current = grid;
// Observador para atualizar cores do grid automaticamente quando o usuário alternar tema
const themeObserver = new MutationObserver(() => {
const isDarkNow = document.documentElement.classList.contains('dark');
const centerC = isDarkNow ? new THREE.Color(0x475569) : new THREE.Color(0x94a3b8);
const lineC = isDarkNow ? new THREE.Color(0x1e293b) : new THREE.Color(0xe2e8f0);
if (gridRef.current) {
const colors = gridRef.current.geometry.attributes.color as THREE.BufferAttribute;
if (colors) {
const arr = colors.array as Float32Array;
for (let i = 0; i < arr.length; i += 6) {
const isCenter = i === Math.floor(arr.length / 2);
const c = isCenter ? centerC : lineC;
arr[i] = c.r;
arr[i + 1] = c.g;
arr[i + 2] = c.b;
arr[i + 3] = c.r;
arr[i + 4] = c.g;
arr[i + 5] = c.b;
}
colors.needsUpdate = true;
}
}
});
themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
let animId: number;
const animate = () => {
animId = requestAnimationFrame(animate);
@@ -156,12 +186,13 @@ export const AnalyzerViewer3D: React.FC<AnalyzerViewer3DProps> = ({
cancelAnimationFrame(animId);
window.removeEventListener('resize', handleResize);
observer.disconnect();
themeObserver.disconnect();
renderer.dispose();
controls.dispose();
};
}, [theme]);
// 2. Carrega o modelo na cena
// 2. Carrega o modelo na cena diretamente sem deep clone circular
useEffect(() => {
const scene = sceneRef.current;
if (!scene) return;
@@ -173,7 +204,8 @@ export const AnalyzerViewer3D: React.FC<AnalyzerViewer3DProps> = ({
if (!modelData || !modelData.sceneGroup) return;
const group = modelData.sceneGroup.clone();
// Usa diretamente a sceneGroup da carga sem clone para evitar erro de circular JSON
const group = modelData.sceneGroup;
modelGroupRef.current = group;
scene.add(group);
@@ -339,14 +371,50 @@ export const AnalyzerViewer3D: React.FC<AnalyzerViewer3DProps> = ({
controlsRef.current.update();
};
// ViewCube Switcher de Direções
const handleViewCubeSelect = useCallback(
(view: 'top' | 'bottom' | 'front' | 'back' | 'left' | 'right' | 'iso') => {
if (!controlsRef.current || !cameraRef.current) return;
const target = controlsRef.current.target;
const camera = cameraRef.current;
const distance = camera.position.distanceTo(target);
const newPos = target.clone();
switch (view) {
case 'top':
newPos.y += distance;
break;
case 'bottom':
newPos.y -= distance;
break;
case 'front':
newPos.z += distance;
break;
case 'back':
newPos.z -= distance;
break;
case 'left':
newPos.x -= distance;
break;
case 'right':
newPos.x += distance;
break;
case 'iso':
newPos.x += distance * 0.7;
newPos.y += distance * 0.7;
newPos.z += distance * 0.7;
break;
}
camera.position.copy(newPos);
controlsRef.current.update();
},
[]
);
return (
<div
ref={containerRef}
className={`relative w-full h-full select-none overflow-hidden rounded-xl border ${
theme === 'dark'
? 'bg-slate-950/80 border-slate-800'
: 'bg-slate-900 border-slate-700'
}`}
className="relative w-full h-full select-none overflow-hidden rounded-xl border bg-slate-50 dark:bg-slate-950 border-slate-200 dark:border-slate-800 shadow-sm"
>
{/* Container Three.js */}
<div
@@ -357,20 +425,23 @@ export const AnalyzerViewer3D: React.FC<AnalyzerViewer3DProps> = ({
{/* ViewCube Superior Direito */}
{cameraRef.current && controlsRef.current && (
<ViewCube camera={cameraRef.current} controls={controlsRef.current} />
<ViewCube
onSelectView={handleViewCubeSelect}
mainCameraRef={cameraRef as React.MutableRefObject<THREE.Camera | null>}
/>
)}
{/* Barra de Ferramentas Flutuante Superior */}
<div className="absolute top-3 left-3 flex items-center gap-1.5 p-1.5 bg-slate-900/90 backdrop-blur-md border border-slate-700/80 rounded-lg shadow-xl z-20">
<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">
<button
onClick={handleResetCamera}
title="Resetar Câmera"
className="p-1.5 text-slate-300 hover:text-white 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 transition"
>
<RotateCcw className="w-4 h-4" />
</button>
<div className="w-[1px] h-4 bg-slate-700 mx-1" />
<div className="w-[1px] h-4 bg-slate-200 dark:bg-slate-700 mx-1" />
<button
onClick={onToggleGhostMode}
@@ -378,7 +449,7 @@ export const AnalyzerViewer3D: React.FC<AnalyzerViewer3DProps> = ({
className={`flex items-center gap-1.5 px-2 py-1 text-xs font-medium rounded transition ${
isGhostMode
? 'bg-indigo-600 text-white shadow'
: 'text-slate-300 hover:bg-slate-800 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" />
@@ -391,7 +462,7 @@ export const AnalyzerViewer3D: React.FC<AnalyzerViewer3DProps> = ({
className={`p-1.5 rounded transition ${
wireframe
? 'bg-indigo-600 text-white'
: 'text-slate-300 hover:bg-slate-800 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" />
@@ -401,7 +472,7 @@ export const AnalyzerViewer3D: React.FC<AnalyzerViewer3DProps> = ({
<button
onClick={onToggleFullscreen}
title={isFullscreen ? 'Sair da Tela Cheia' : 'Tela Cheia'}
className="p-1.5 text-slate-300 hover:text-white 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 transition"
>
{isFullscreen ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
</button>
@@ -409,21 +480,21 @@ export const AnalyzerViewer3D: React.FC<AnalyzerViewer3DProps> = ({
</div>
{/* Legenda de Cores Flutuante Inferior */}
<div className="absolute bottom-3 left-3 flex flex-wrap items-center gap-3 px-3 py-2 bg-slate-900/90 backdrop-blur-md border border-slate-700/80 rounded-lg shadow-xl text-xs z-20">
<span className="text-slate-400 font-medium">Legenda:</span>
<div className="absolute bottom-3 left-3 flex flex-wrap items-center gap-3 px-3 py-2 bg-white/95 dark:bg-slate-900/90 backdrop-blur-md border border-slate-200 dark:border-slate-700/80 rounded-lg shadow-md text-xs z-20">
<span className="text-slate-500 dark:text-slate-400 font-medium">Legenda:</span>
{colorizationMode === 'status_defeito' && (
<>
<div className="flex items-center gap-1.5">
<span className="w-2.5 h-2.5 rounded-full bg-emerald-500 shadow-sm" />
<span className="text-slate-300">Conforme</span>
<span className="text-slate-700 dark:text-slate-300">Conforme</span>
</div>
<div className="flex items-center gap-1.5">
<span className="w-2.5 h-2.5 rounded-full bg-amber-500 shadow-sm" />
<span className="text-slate-300">Alerta (Deslocado)</span>
<span className="text-slate-700 dark:text-slate-300">Alerta (Deslocado)</span>
</div>
<div className="flex items-center gap-1.5">
<span className="w-2.5 h-2.5 rounded-full bg-red-500 shadow-sm" />
<span className="text-slate-300">Crítico (Furo Cego/Colisão)</span>
<span className="text-slate-700 dark:text-slate-300">Crítico (Furo Cego/Colisão)</span>
</div>
</>
)}
@@ -431,19 +502,19 @@ export const AnalyzerViewer3D: React.FC<AnalyzerViewer3DProps> = ({
<>
<div className="flex items-center gap-1.5">
<span className="w-2.5 h-2.5 rounded-full bg-emerald-500" />
<span className="text-slate-300">Inalterada</span>
<span className="text-slate-700 dark:text-slate-300">Inalterada</span>
</div>
<div className="flex items-center gap-1.5">
<span className="w-2.5 h-2.5 rounded-full bg-amber-500" />
<span className="text-slate-300">Modificada</span>
<span className="text-slate-700 dark:text-slate-300">Modificada</span>
</div>
<div className="flex items-center gap-1.5">
<span className="w-2.5 h-2.5 rounded-full bg-blue-500" />
<span className="text-slate-300">Nova</span>
<span className="text-slate-700 dark:text-slate-300">Nova</span>
</div>
<div className="flex items-center gap-1.5">
<span className="w-2.5 h-2.5 rounded-full bg-red-500" />
<span className="text-slate-300">Excluída</span>
<span className="text-slate-700 dark:text-slate-300">Excluída</span>
</div>
</>
)}
@@ -451,15 +522,15 @@ export const AnalyzerViewer3D: React.FC<AnalyzerViewer3DProps> = ({
<>
<div className="flex items-center gap-1.5">
<span className="w-2.5 h-2.5 rounded-full bg-emerald-500" />
<span className="text-slate-300">Baixo Risco (Sem Corte)</span>
<span className="text-slate-700 dark:text-slate-300">Baixo Risco (Sem Corte)</span>
</div>
<div className="flex items-center gap-1.5">
<span className="w-2.5 h-2.5 rounded-full bg-amber-500" />
<span className="text-slate-300">Médio (Em Corte)</span>
<span className="text-slate-700 dark:text-slate-300">Médio (Em Corte)</span>
</div>
<div className="flex items-center gap-1.5">
<span className="w-2.5 h-2.5 rounded-full bg-red-600 animate-pulse" />
<span className="text-slate-300">Crítico (Soldada/Expedida)</span>
<span className="text-slate-700 dark:text-slate-300">Crítico (Soldada/Expedida)</span>
</div>
</>
)}
@@ -467,17 +538,17 @@ export const AnalyzerViewer3D: React.FC<AnalyzerViewer3DProps> = ({
{/* Alerta de elemento selecionado flutuante */}
{selectedPieceMark && (
<div className="absolute top-3 right-16 px-3 py-1.5 bg-indigo-950/90 border border-indigo-700/80 rounded-lg shadow-xl text-xs flex items-center gap-2 z-20">
<Crosshair className="w-3.5 h-3.5 text-indigo-400 animate-spin" style={{ animationDuration: '6s' }} />
<span className="text-indigo-200">
Foco Ativo: <strong className="text-white font-mono">{selectedPieceMark}</strong>
<div className="absolute top-3 right-16 px-3 py-1.5 bg-indigo-50 dark:bg-indigo-950/90 border border-indigo-200 dark:border-indigo-700/80 rounded-lg shadow-md text-xs flex items-center gap-2 z-20">
<Crosshair className="w-3.5 h-3.5 text-indigo-600 dark:text-indigo-400 animate-spin" style={{ animationDuration: '6s' }} />
<span className="text-indigo-900 dark:text-indigo-200">
Foco Ativo: <strong className="text-indigo-700 dark:text-white font-mono">{selectedPieceMark}</strong>
</span>
<button
onClick={() => {
onSelectPiece?.(null);
onSelectDefect?.(null);
}}
className="ml-1 text-indigo-400 hover:text-white"
className="ml-1 text-indigo-500 hover:text-indigo-800 dark:hover:text-white"
title="Limpar seleção"
>
×
+12 -1
View File
@@ -59,6 +59,17 @@ export default function Analisador3D() {
const [colorizationMode, setColorizationMode] = useState<ColorizationMode>('status_defeito');
const [isGhostMode, setIsGhostMode] = useState<boolean>(true);
const [isFullscreen, setIsFullscreen] = useState<boolean>(false);
const [isDarkTheme, setIsDarkTheme] = useState<boolean>(() =>
typeof document !== 'undefined' ? document.documentElement.classList.contains('dark') : false
);
useEffect(() => {
const observer = new MutationObserver(() => {
setIsDarkTheme(document.documentElement.classList.contains('dark'));
});
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
return () => observer.disconnect();
}, []);
// Modelos 3D Carregados
const [modelBase, setModelBase] = useState<LoadedModel3DResult | null>(null);
@@ -785,7 +796,7 @@ export default function Analisador3D() {
onToggleGhostMode={() => setIsGhostMode(!isGhostMode)}
isFullscreen={isFullscreen}
onToggleFullscreen={() => setIsFullscreen(!isFullscreen)}
theme={document.documentElement.classList.contains('dark') ? 'dark' : 'light'}
theme={isDarkTheme ? 'dark' : 'light'}
/>
</div>
</div>