feat: improve dark mode contrast in Tower3D and IsolatedRoof3D components

This commit is contained in:
2026-07-10 15:25:38 +00:00
parent df52cecc64
commit bcca3738d0
4 changed files with 66 additions and 21 deletions
+18 -10
View File
@@ -4,6 +4,7 @@ import * as THREE from 'three';
import SceneCanvas from '../SceneCanvas';
import FallbackDiagram from '../FallbackDiagram';
import { WindArrow } from './WindArrow';
import { useCanvasTheme } from '@/lib/theme';
export interface IsolatedRoof3DInput {
/** Tipo de cobertura: 'shed' (uma água) ou 'gable' (duas águas) */
@@ -43,6 +44,13 @@ function IsolatedRoofModel({
cpeLeeward,
forceKN,
}: IsolatedRoof3DInput) {
const theme = useCanvasTheme();
const isDark = theme === 'dark';
const labelColor = isDark ? '#cbd5e1' : '#475569';
const textColor = isDark ? '#93c5fd' : '#1a202c';
const lineMaterialColor = isDark ? '#94a3b8' : '#64748b';
const pillarColor = isDark ? '#cbd5e1' : '#475569';
const thetaRad = (theta * Math.PI) / 180;
const halfDepth = depth / 2;
const halfWidth = width / 2;
@@ -142,7 +150,7 @@ function IsolatedRoofModel({
{pillars.map((p, i) => (
<mesh key={`pillar-${i}`} position={p.pos} castShadow>
<cylinderGeometry args={[0.06, 0.06, p.h, 16]} />
<meshStandardMaterial color="#475569" roughness={0.5} />
<meshStandardMaterial color={pillarColor} roughness={0.5} />
</mesh>
))}
@@ -157,21 +165,21 @@ function IsolatedRoofModel({
<group position={[-halfDepth - 0.4, 0, -halfDepth]}>
<mesh position={[0, height / 2, 0]}>
<boxGeometry args={[0.015, height, 0.015]} />
<meshStandardMaterial color="#64748b" />
<meshStandardMaterial color={lineMaterialColor} />
</mesh>
<mesh position={[0, height, 0]}>
<boxGeometry args={[0.1, 0.015, 0.015]} />
<meshStandardMaterial color="#64748b" />
<meshStandardMaterial color={lineMaterialColor} />
</mesh>
<mesh position={[0, 0, 0]}>
<boxGeometry args={[0.1, 0.015, 0.015]} />
<meshStandardMaterial color="#64748b" />
<meshStandardMaterial color={lineMaterialColor} />
</mesh>
<Text
position={[-0.15, height / 2, 0]}
rotation={[0, -Math.PI / 2, 0]}
fontSize={0.25}
color="#475569"
color={labelColor}
anchorX="center"
anchorY="middle"
>
@@ -183,21 +191,21 @@ function IsolatedRoofModel({
<group position={[halfDepth + 0.4, height / 2, 0]}>
<mesh position={[0, 0, 0]}>
<boxGeometry args={[0.015, 0.015, depth]} />
<meshStandardMaterial color="#64748b" />
<meshStandardMaterial color={lineMaterialColor} />
</mesh>
<mesh position={[0, 0, halfDepth]}>
<boxGeometry args={[0.1, 0.015, 0.015]} />
<meshStandardMaterial color="#64748b" />
<meshStandardMaterial color={lineMaterialColor} />
</mesh>
<mesh position={[0, 0, -halfDepth]}>
<boxGeometry args={[0.1, 0.015, 0.015]} />
<meshStandardMaterial color="#64748b" />
<meshStandardMaterial color={lineMaterialColor} />
</mesh>
<Text
position={[0.15, 0, 0]}
rotation={[0, Math.PI / 2, 0]}
fontSize={0.25}
color="#475569"
color={labelColor}
anchorX="center"
anchorY="middle"
>
@@ -208,7 +216,7 @@ function IsolatedRoofModel({
{/* Rótulo Superior */}
<Text
position={[0, centerY + 3, 0]}
color="#1a202c"
color={textColor}
anchorX="center"
anchorY="bottom"
>
+26 -9
View File
@@ -114,10 +114,16 @@ function buildTowerGeometry(
return { nodes, members };
}
function pressureColor(phi: number, forceKN: number): THREE.Color {
import { useCanvasTheme } from '@/lib/theme';
function pressureColor(phi: number, forceKN: number, isDark: boolean): THREE.Color {
const intensity = Math.min(1, (phi * forceKN) / 30);
const hue = 220 - intensity * 220;
return new THREE.Color(`hsl(${hue}, ${60 + intensity * 30}%, ${50 - intensity * 10}%)`);
// Ajusta a luminosidade no HSL para contrastar melhor em fundos escuros
const lightness = isDark
? (60 + intensity * 10)
: (50 - intensity * 10);
return new THREE.Color(`hsl(${hue}, ${60 + intensity * 30}%, ${lightness}%)`);
}
function TowerModel({
@@ -130,13 +136,16 @@ function TowerModel({
alphaWind,
forceKN,
}: Tower3DInput) {
const theme = useCanvasTheme();
const isDark = theme === 'dark';
const geometry = useMemo(
() => buildTowerGeometry(section, panels, baseWidth, height),
[section, panels, baseWidth, height],
);
const barRadius = barType === 'circular' ? 0.04 : 0.03;
const barColor = pressureColor(phi, forceKN);
const barColor = pressureColor(phi, forceKN, isDark);
// Direção do vetor de força
const alphaRad = (alphaWind * Math.PI) / 180;
@@ -150,14 +159,14 @@ function TowerModel({
{/* Solo */}
<mesh position={[0, 0, 0]} rotation={[-Math.PI / 2, 0, 0]} receiveShadow>
<planeGeometry args={[baseWidth * 4, baseWidth * 4]} />
<meshStandardMaterial color="#94a3b8" opacity={0.2} transparent />
<meshStandardMaterial color={isDark ? "#334155" : "#94a3b8"} opacity={0.2} transparent />
</mesh>
{/* Nós (esferas pequenas) */}
{geometry.nodes.map((node, idx) => (
<mesh key={`node-${idx}`} position={node.toArray()} castShadow>
<sphereGeometry args={[barRadius * 1.4, 8, 8]} />
<meshStandardMaterial color="#475569" />
<meshStandardMaterial color={isDark ? "#94a3b8" : "#475569"} />
</mesh>
))}
@@ -178,9 +187,9 @@ function TowerModel({
const color =
m.type === 'leg'
? '#1e293b'
? (isDark ? '#f1f5f9' : '#1e293b')
: m.type === 'horizontal'
? '#64748b'
? (isDark ? '#cbd5e1' : '#64748b')
: barColor;
return (
@@ -205,7 +214,7 @@ function TowerModel({
<Text
position={[0, height + 1.0, 0]}
fontSize={0.5}
color="#1e40af"
color={isDark ? "#93c5fd" : "#1e40af"}
anchorX="center"
anchorY="bottom"
>
@@ -216,6 +225,8 @@ function TowerModel({
}
export default function Tower3DViewer(input: Tower3DInput) {
const theme = useCanvasTheme();
const isDark = theme === 'dark';
const { baseWidth, height } = input;
const dist = Math.max(baseWidth * 3, height * 1.2);
@@ -236,7 +247,13 @@ export default function Tower3DViewer(input: Tower3DInput) {
<ambientLight intensity={0.6} />
<directionalLight position={[dist, height, dist]} intensity={1.2} castShadow shadow-mapSize-width={1024} shadow-mapSize-height={1024} />
<TowerModel {...input} />
<Grid infiniteGrid fadeDistance={height * 2} sectionColor="#94a3b8" cellColor="#cbd5e1" position={[0, -0.01, 0]} />
<Grid
infiniteGrid
fadeDistance={height * 2}
sectionColor={isDark ? "#475569" : "#94a3b8"}
cellColor={isDark ? "#1e293b" : "#cbd5e1"}
position={[0, -0.01, 0]}
/>
<OrbitControls makeDefault minPolarAngle={0} maxPolarAngle={Math.PI / 2 - 0.05} />
<Environment preset="city" />
</SceneCanvas>
+20
View File
@@ -61,4 +61,24 @@ export function useTheme(): ThemeContextValue {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error('useTheme deve ser usado dentro de ThemeProvider');
return ctx;
}
export function useCanvasTheme(): 'light' | 'dark' {
const [isDark, setIsDark] = useState(() => {
if (typeof document === 'undefined') return false;
return document.documentElement.classList.contains('dark');
});
useEffect(() => {
const observer = new MutationObserver(() => {
setIsDark(document.documentElement.classList.contains('dark'));
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class'],
});
return () => observer.disconnect();
}, []);
return isDark ? 'dark' : 'light';
}
+2 -2
View File
@@ -162,8 +162,8 @@ const IsolatedRoofModule: React.FC = () => {
{/* Coluna Central: 3D */}
<div className="flex-1 flex flex-col min-h-[500px] lg:min-h-0 bg-background rounded-xl border border-border shadow-sm overflow-hidden relative">
<div className="absolute top-4 left-4 z-10 flex gap-2">
<Badge variant="secondary" className="bg-background/80 backdrop-blur-sm">θ = {theta}°</Badge>
<Badge variant="secondary" className="bg-background/80 backdrop-blur-sm">{type === 'shed' ? 'Uma água' : 'Duas águas'}</Badge>
<Badge variant="secondary" className="bg-background/80 text-foreground border border-border/80 backdrop-blur-sm shadow-sm">θ = {theta}°</Badge>
<Badge variant="secondary" className="bg-background/80 text-foreground border border-border/80 backdrop-blur-sm shadow-sm">{type === 'shed' ? 'Uma água' : 'Duas águas'}</Badge>
</div>
<div className="absolute top-4 right-4 z-10">
<EducationalManual type="isolated-roof" params={{ theta, type }} />