fix(viewer3d): harmonize ViewCube container background and ISO button for light mode

This commit is contained in:
2026-09-21 01:48:01 +00:00
parent 94d41c0e85
commit 3882e8c338
+52 -26
View File
@@ -13,6 +13,7 @@ export const ViewCube: React.FC<ViewCubeProps> = ({ onSelectView, mainCameraRef
const sceneRef = useRef<THREE.Scene | null>(null); const sceneRef = useRef<THREE.Scene | null>(null);
const cameraRef = useRef<THREE.OrthographicCamera | null>(null); const cameraRef = useRef<THREE.OrthographicCamera | null>(null);
const cubeRef = useRef<THREE.Mesh | null>(null); const cubeRef = useRef<THREE.Mesh | null>(null);
const wireframeRef = useRef<THREE.LineSegments | null>(null);
const raycasterRef = useRef<THREE.Raycaster>(new THREE.Raycaster()); const raycasterRef = useRef<THREE.Raycaster>(new THREE.Raycaster());
const mouseRef = useRef<THREE.Vector2>(new THREE.Vector2()); const mouseRef = useRef<THREE.Vector2>(new THREE.Vector2());
@@ -39,7 +40,7 @@ export const ViewCube: React.FC<ViewCubeProps> = ({ onSelectView, mainCameraRef
camera.lookAt(0, 0, 0); camera.lookAt(0, 0, 0);
cameraRef.current = camera; cameraRef.current = camera;
// 2. Renderer // 2. Renderer Transparente
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(width, height); renderer.setSize(width, height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
@@ -49,24 +50,24 @@ export const ViewCube: React.FC<ViewCubeProps> = ({ onSelectView, mainCameraRef
containerRef.current.innerHTML = ''; containerRef.current.innerHTML = '';
containerRef.current.appendChild(renderer.domElement); containerRef.current.appendChild(renderer.domElement);
// 3. Helper to create high-res Canvas Texture for each face (SteelXR Style) // 3. Helper to create high-res Canvas Texture for each face
const createFaceTexture = (text: string) => { const createFaceTexture = (text: string, isDark: boolean) => {
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
canvas.width = 256; canvas.width = 256;
canvas.height = 256; canvas.height = 256;
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
if (ctx) { if (ctx) {
// Background: Deep Dark Navy Blue (#0f172a / #1e293b) // Face Background: Azul petróleo equilibrado no claro (#1e293b) e escuro (#0f172a)
ctx.fillStyle = '#0f172a'; ctx.fillStyle = isDark ? '#0f172a' : '#1e293b';
ctx.fillRect(0, 0, 256, 256); ctx.fillRect(0, 0, 256, 256);
// Neon Cyan Border (#22d3ee) // Neon Cyan Border (#0891b2 no claro / #22d3ee no escuro)
ctx.strokeStyle = '#22d3ee'; ctx.strokeStyle = isDark ? '#22d3ee' : '#06b6d4';
ctx.lineWidth = 14; ctx.lineWidth = 14;
ctx.strokeRect(7, 7, 242, 242); ctx.strokeRect(7, 7, 242, 242);
// Inner soft cyan glow // Inner soft cyan glow
ctx.strokeStyle = 'rgba(34, 211, 238, 0.4)'; ctx.strokeStyle = isDark ? 'rgba(34, 211, 238, 0.4)' : 'rgba(6, 182, 212, 0.3)';
ctx.lineWidth = 6; ctx.lineWidth = 6;
ctx.strokeRect(18, 18, 220, 220); ctx.strokeRect(18, 18, 220, 220);
@@ -75,8 +76,8 @@ export const ViewCube: React.FC<ViewCubeProps> = ({ onSelectView, mainCameraRef
ctx.font = 'bold 54px system-ui, -apple-system, sans-serif'; ctx.font = 'bold 54px system-ui, -apple-system, sans-serif';
ctx.textAlign = 'center'; ctx.textAlign = 'center';
ctx.textBaseline = 'middle'; ctx.textBaseline = 'middle';
ctx.shadowColor = '#06b6d4'; ctx.shadowColor = isDark ? '#06b6d4' : '#0891b2';
ctx.shadowBlur = 12; ctx.shadowBlur = isDark ? 12 : 8;
ctx.fillText(text, 128, 128); ctx.fillText(text, 128, 128);
} }
const texture = new THREE.CanvasTexture(canvas); const texture = new THREE.CanvasTexture(canvas);
@@ -84,15 +85,16 @@ export const ViewCube: React.FC<ViewCubeProps> = ({ onSelectView, mainCameraRef
return texture; return texture;
}; };
// Faces Order in Three.js BoxGeometry: const isDarkMode = document.documentElement.classList.contains('dark');
// 0: Right (DIR), 1: Left (ESQ), 2: Top (TOPO), 3: Bottom (BASE), 4: Front (FRENTE), 5: Back (ATRÁS)
// Faces: 0: Right (DIR), 1: Left (ESQ), 2: Top (TOPO), 3: Bottom (BASE), 4: Front (FRENTE), 5: Back (ATRÁS)
const materials = [ const materials = [
new THREE.MeshBasicMaterial({ map: createFaceTexture('DIR') }), new THREE.MeshBasicMaterial({ map: createFaceTexture('DIR', isDarkMode) }),
new THREE.MeshBasicMaterial({ map: createFaceTexture('ESQ') }), new THREE.MeshBasicMaterial({ map: createFaceTexture('ESQ', isDarkMode) }),
new THREE.MeshBasicMaterial({ map: createFaceTexture('TOPO') }), new THREE.MeshBasicMaterial({ map: createFaceTexture('TOPO', isDarkMode) }),
new THREE.MeshBasicMaterial({ map: createFaceTexture('BASE') }), new THREE.MeshBasicMaterial({ map: createFaceTexture('BASE', isDarkMode) }),
new THREE.MeshBasicMaterial({ map: createFaceTexture('FRENTE') }), new THREE.MeshBasicMaterial({ map: createFaceTexture('FRENTE', isDarkMode) }),
new THREE.MeshBasicMaterial({ map: createFaceTexture('ATRÁS') }), new THREE.MeshBasicMaterial({ map: createFaceTexture('ATRÁS', isDarkMode) }),
]; ];
const geometry = new THREE.BoxGeometry(32, 32, 32); const geometry = new THREE.BoxGeometry(32, 32, 32);
@@ -102,9 +104,32 @@ export const ViewCube: React.FC<ViewCubeProps> = ({ onSelectView, mainCameraRef
// Glowing Neon Edges on the Cube // Glowing Neon Edges on the Cube
const edges = new THREE.EdgesGeometry(geometry); const edges = new THREE.EdgesGeometry(geometry);
const lineMaterial = new THREE.LineBasicMaterial({ color: 0x22d3ee, linewidth: 2 }); const lineMaterial = new THREE.LineBasicMaterial({
color: isDarkMode ? 0x22d3ee : 0x06b6d4,
linewidth: 2,
});
const wireframe = new THREE.LineSegments(edges, lineMaterial); const wireframe = new THREE.LineSegments(edges, lineMaterial);
cube.add(wireframe); cube.add(wireframe);
wireframeRef.current = wireframe;
// Função de atualização quando o tema mudar
const updateThemeMaterials = () => {
const dark = document.documentElement.classList.contains('dark');
const labels = ['DIR', 'ESQ', 'TOPO', 'BASE', 'FRENTE', 'ATRÁS'];
materials.forEach((mat, idx) => {
if (mat.map) mat.map.dispose();
mat.map = createFaceTexture(labels[idx], dark);
mat.needsUpdate = true;
});
if (wireframeRef.current && wireframeRef.current.material) {
(wireframeRef.current.material as THREE.LineBasicMaterial).color.set(dark ? 0x22d3ee : 0x06b6d4);
}
};
const themeObserver = new MutationObserver(() => {
updateThemeMaterials();
});
themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
// 4. Animation Loop: Sincroniza com a câmera principal em tempo real // 4. Animation Loop: Sincroniza com a câmera principal em tempo real
let animId: number; let animId: number;
@@ -165,6 +190,7 @@ export const ViewCube: React.FC<ViewCubeProps> = ({ onSelectView, mainCameraRef
return () => { return () => {
cancelAnimationFrame(animId); cancelAnimationFrame(animId);
dom.removeEventListener('click', handleCanvasClick); dom.removeEventListener('click', handleCanvasClick);
themeObserver.disconnect();
materials.forEach((m) => { materials.forEach((m) => {
if (m.map) m.map.dispose(); if (m.map) m.map.dispose();
m.dispose(); m.dispose();
@@ -176,21 +202,21 @@ export const ViewCube: React.FC<ViewCubeProps> = ({ onSelectView, mainCameraRef
return ( return (
<div className="relative select-none flex flex-col items-center"> <div className="relative select-none flex flex-col items-center">
{/* Container Reduzido (~70% do anterior) com estilo SteelXR */} {/* Container Reduzido (~70%) Harmonizado para Modo Claro e Escuro */}
<div className="relative w-24 h-24 p-1 rounded-2xl bg-slate-950/85 backdrop-blur-xl border border-cyan-500/40 shadow-xl shadow-cyan-950/60 flex items-center justify-center cursor-pointer group"> <div className="relative w-24 h-24 p-1 rounded-2xl bg-white/90 dark:bg-slate-950/85 backdrop-blur-xl border border-slate-200 dark:border-cyan-500/40 shadow-lg dark:shadow-xl dark:shadow-cyan-950/60 flex items-center justify-center cursor-pointer group transition-all">
{/* Glow sutil */} {/* Glow sutil harmonizado */}
<div className="absolute inset-0 bg-gradient-to-tr from-cyan-500/10 via-transparent to-blue-500/10 rounded-2xl pointer-events-none" /> <div className="absolute inset-0 bg-gradient-to-tr from-cyan-500/5 dark:from-cyan-500/10 via-transparent to-blue-500/5 dark:to-blue-500/10 rounded-2xl pointer-events-none" />
{/* Canvas WebGL 3D do Cubo Sincronizado */} {/* Canvas WebGL 3D do Cubo Sincronizado */}
<div ref={containerRef} className="w-[84px] h-[84px] flex items-center justify-center" /> <div ref={containerRef} className="w-[84px] h-[84px] flex items-center justify-center" />
{/* Botão ISO no canto inferior */} {/* Botão ISO no canto inferior harmonizado com o tema */}
<button <button
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onSelectView('iso'); onSelectView('iso');
}} }}
className="absolute bottom-1 right-1 px-1 py-0.5 rounded bg-slate-900/90 hover:bg-cyan-600 text-cyan-300 hover:text-white border border-cyan-500/30 transition-all text-[9px] font-mono font-bold flex items-center gap-0.5 shadow cursor-pointer" className="absolute bottom-1 right-1 px-1 py-0.5 rounded bg-slate-100 hover:bg-cyan-600 text-slate-700 hover:text-white border border-slate-300 hover:border-cyan-500 dark:bg-slate-900/90 dark:text-cyan-300 dark:border-cyan-500/30 transition-all text-[9px] font-mono font-bold flex items-center gap-0.5 shadow-sm cursor-pointer"
title="Restaurar Vista Isométrica 3D" title="Restaurar Vista Isométrica 3D"
> >
<RotateCcw className="w-2.5 h-2.5" /> <RotateCcw className="w-2.5 h-2.5" />
@@ -199,7 +225,7 @@ export const ViewCube: React.FC<ViewCubeProps> = ({ onSelectView, mainCameraRef
</div> </div>
{/* Rótulo inferior */} {/* Rótulo inferior */}
<span className="text-[8px] font-mono font-bold tracking-widest uppercase text-cyan-700 dark:text-cyan-400 mt-0.5"> <span className="text-[8px] font-mono font-bold tracking-widest uppercase text-cyan-800 dark:text-cyan-400 mt-0.5">
Cubo 3D Cubo 3D
</span> </span>
</div> </div>