fix: Calibragem do fundo no modo claro do visualizador 3D e prevenção de circular JSON
This commit is contained in:
@@ -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"
|
||||
>
|
||||
×
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user