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);
|
dirLight2.position.set(-80, -40, -80);
|
||||||
scene.add(dirLight2);
|
scene.add(dirLight2);
|
||||||
|
|
||||||
// Grid no piso
|
// Grid no piso com cores refinadas para modo claro e escuro
|
||||||
const isDark = theme === 'dark' || document.documentElement.classList.contains('dark');
|
const isDarkMode = document.documentElement.classList.contains('dark');
|
||||||
const gridColor1 = isDark ? 0x475569 : 0x94a3b8;
|
const gridColor1 = isDarkMode ? 0x475569 : 0x94a3b8;
|
||||||
const gridColor2 = isDark ? 0x1e293b : 0xe2e8f0;
|
const gridColor2 = isDarkMode ? 0x1e293b : 0xe2e8f0;
|
||||||
const grid = new THREE.GridHelper(100, 50, gridColor1, gridColor2);
|
const grid = new THREE.GridHelper(100, 50, gridColor1, gridColor2);
|
||||||
grid.position.y = -0.01;
|
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);
|
scene.add(grid);
|
||||||
gridRef.current = 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;
|
let animId: number;
|
||||||
const animate = () => {
|
const animate = () => {
|
||||||
animId = requestAnimationFrame(animate);
|
animId = requestAnimationFrame(animate);
|
||||||
@@ -156,12 +186,13 @@ export const AnalyzerViewer3D: React.FC<AnalyzerViewer3DProps> = ({
|
|||||||
cancelAnimationFrame(animId);
|
cancelAnimationFrame(animId);
|
||||||
window.removeEventListener('resize', handleResize);
|
window.removeEventListener('resize', handleResize);
|
||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
|
themeObserver.disconnect();
|
||||||
renderer.dispose();
|
renderer.dispose();
|
||||||
controls.dispose();
|
controls.dispose();
|
||||||
};
|
};
|
||||||
}, [theme]);
|
}, [theme]);
|
||||||
|
|
||||||
// 2. Carrega o modelo na cena
|
// 2. Carrega o modelo na cena diretamente sem deep clone circular
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const scene = sceneRef.current;
|
const scene = sceneRef.current;
|
||||||
if (!scene) return;
|
if (!scene) return;
|
||||||
@@ -173,7 +204,8 @@ export const AnalyzerViewer3D: React.FC<AnalyzerViewer3DProps> = ({
|
|||||||
|
|
||||||
if (!modelData || !modelData.sceneGroup) return;
|
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;
|
modelGroupRef.current = group;
|
||||||
scene.add(group);
|
scene.add(group);
|
||||||
|
|
||||||
@@ -339,14 +371,50 @@ export const AnalyzerViewer3D: React.FC<AnalyzerViewer3DProps> = ({
|
|||||||
controlsRef.current.update();
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className={`relative w-full h-full select-none overflow-hidden rounded-xl border ${
|
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"
|
||||||
theme === 'dark'
|
|
||||||
? 'bg-slate-950/80 border-slate-800'
|
|
||||||
: 'bg-slate-900 border-slate-700'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{/* Container Three.js */}
|
{/* Container Three.js */}
|
||||||
<div
|
<div
|
||||||
@@ -357,20 +425,23 @@ export const AnalyzerViewer3D: React.FC<AnalyzerViewer3DProps> = ({
|
|||||||
|
|
||||||
{/* ViewCube Superior Direito */}
|
{/* ViewCube Superior Direito */}
|
||||||
{cameraRef.current && controlsRef.current && (
|
{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 */}
|
{/* 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
|
<button
|
||||||
onClick={handleResetCamera}
|
onClick={handleResetCamera}
|
||||||
title="Resetar Câmera"
|
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" />
|
<RotateCcw className="w-4 h-4" />
|
||||||
</button>
|
</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
|
<button
|
||||||
onClick={onToggleGhostMode}
|
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 ${
|
className={`flex items-center gap-1.5 px-2 py-1 text-xs font-medium rounded transition ${
|
||||||
isGhostMode
|
isGhostMode
|
||||||
? 'bg-indigo-600 text-white shadow'
|
? '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" />
|
<Eye className="w-3.5 h-3.5" />
|
||||||
@@ -391,7 +462,7 @@ export const AnalyzerViewer3D: React.FC<AnalyzerViewer3DProps> = ({
|
|||||||
className={`p-1.5 rounded transition ${
|
className={`p-1.5 rounded transition ${
|
||||||
wireframe
|
wireframe
|
||||||
? 'bg-indigo-600 text-white'
|
? '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" />
|
<Box className="w-4 h-4" />
|
||||||
@@ -401,7 +472,7 @@ export const AnalyzerViewer3D: React.FC<AnalyzerViewer3DProps> = ({
|
|||||||
<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-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" />}
|
{isFullscreen ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
|
||||||
</button>
|
</button>
|
||||||
@@ -409,21 +480,21 @@ export const AnalyzerViewer3D: React.FC<AnalyzerViewer3DProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Legenda de Cores Flutuante Inferior */}
|
{/* 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">
|
<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-400 font-medium">Legenda:</span>
|
<span className="text-slate-500 dark:text-slate-400 font-medium">Legenda:</span>
|
||||||
{colorizationMode === 'status_defeito' && (
|
{colorizationMode === 'status_defeito' && (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center gap-1.5">
|
<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="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>
|
||||||
<div className="flex items-center gap-1.5">
|
<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="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>
|
||||||
<div className="flex items-center gap-1.5">
|
<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="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>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -431,19 +502,19 @@ export const AnalyzerViewer3D: React.FC<AnalyzerViewer3DProps> = ({
|
|||||||
<>
|
<>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<span className="w-2.5 h-2.5 rounded-full bg-emerald-500" />
|
<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>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<span className="w-2.5 h-2.5 rounded-full bg-amber-500" />
|
<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>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<span className="w-2.5 h-2.5 rounded-full bg-blue-500" />
|
<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>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<span className="w-2.5 h-2.5 rounded-full bg-red-500" />
|
<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>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -451,15 +522,15 @@ export const AnalyzerViewer3D: React.FC<AnalyzerViewer3DProps> = ({
|
|||||||
<>
|
<>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<span className="w-2.5 h-2.5 rounded-full bg-emerald-500" />
|
<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>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<span className="w-2.5 h-2.5 rounded-full bg-amber-500" />
|
<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>
|
||||||
<div className="flex items-center gap-1.5">
|
<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="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>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -467,17 +538,17 @@ export const AnalyzerViewer3D: React.FC<AnalyzerViewer3DProps> = ({
|
|||||||
|
|
||||||
{/* Alerta de elemento selecionado flutuante */}
|
{/* Alerta de elemento selecionado flutuante */}
|
||||||
{selectedPieceMark && (
|
{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">
|
<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-400 animate-spin" style={{ animationDuration: '6s' }} />
|
<Crosshair className="w-3.5 h-3.5 text-indigo-600 dark:text-indigo-400 animate-spin" style={{ animationDuration: '6s' }} />
|
||||||
<span className="text-indigo-200">
|
<span className="text-indigo-900 dark:text-indigo-200">
|
||||||
Foco Ativo: <strong className="text-white font-mono">{selectedPieceMark}</strong>
|
Foco Ativo: <strong className="text-indigo-700 dark:text-white font-mono">{selectedPieceMark}</strong>
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onSelectPiece?.(null);
|
onSelectPiece?.(null);
|
||||||
onSelectDefect?.(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"
|
title="Limpar seleção"
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
|
|||||||
@@ -59,6 +59,17 @@ export default function Analisador3D() {
|
|||||||
const [colorizationMode, setColorizationMode] = useState<ColorizationMode>('status_defeito');
|
const [colorizationMode, setColorizationMode] = useState<ColorizationMode>('status_defeito');
|
||||||
const [isGhostMode, setIsGhostMode] = useState<boolean>(true);
|
const [isGhostMode, setIsGhostMode] = useState<boolean>(true);
|
||||||
const [isFullscreen, setIsFullscreen] = useState<boolean>(false);
|
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
|
// Modelos 3D Carregados
|
||||||
const [modelBase, setModelBase] = useState<LoadedModel3DResult | null>(null);
|
const [modelBase, setModelBase] = useState<LoadedModel3DResult | null>(null);
|
||||||
@@ -785,7 +796,7 @@ export default function Analisador3D() {
|
|||||||
onToggleGhostMode={() => setIsGhostMode(!isGhostMode)}
|
onToggleGhostMode={() => setIsGhostMode(!isGhostMode)}
|
||||||
isFullscreen={isFullscreen}
|
isFullscreen={isFullscreen}
|
||||||
onToggleFullscreen={() => setIsFullscreen(!isFullscreen)}
|
onToggleFullscreen={() => setIsFullscreen(!isFullscreen)}
|
||||||
theme={document.documentElement.classList.contains('dark') ? 'dark' : 'light'}
|
theme={isDarkTheme ? 'dark' : 'light'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user