feat: improve dark mode contrast in Tower3D and IsolatedRoof3D components
This commit is contained in:
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
@@ -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 }} />
|
||||
|
||||
Reference in New Issue
Block a user