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 SceneCanvas from '../SceneCanvas';
|
||||||
import FallbackDiagram from '../FallbackDiagram';
|
import FallbackDiagram from '../FallbackDiagram';
|
||||||
import { WindArrow } from './WindArrow';
|
import { WindArrow } from './WindArrow';
|
||||||
|
import { useCanvasTheme } from '@/lib/theme';
|
||||||
|
|
||||||
export interface IsolatedRoof3DInput {
|
export interface IsolatedRoof3DInput {
|
||||||
/** Tipo de cobertura: 'shed' (uma água) ou 'gable' (duas águas) */
|
/** Tipo de cobertura: 'shed' (uma água) ou 'gable' (duas águas) */
|
||||||
@@ -43,6 +44,13 @@ function IsolatedRoofModel({
|
|||||||
cpeLeeward,
|
cpeLeeward,
|
||||||
forceKN,
|
forceKN,
|
||||||
}: IsolatedRoof3DInput) {
|
}: 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 thetaRad = (theta * Math.PI) / 180;
|
||||||
const halfDepth = depth / 2;
|
const halfDepth = depth / 2;
|
||||||
const halfWidth = width / 2;
|
const halfWidth = width / 2;
|
||||||
@@ -142,7 +150,7 @@ function IsolatedRoofModel({
|
|||||||
{pillars.map((p, i) => (
|
{pillars.map((p, i) => (
|
||||||
<mesh key={`pillar-${i}`} position={p.pos} castShadow>
|
<mesh key={`pillar-${i}`} position={p.pos} castShadow>
|
||||||
<cylinderGeometry args={[0.06, 0.06, p.h, 16]} />
|
<cylinderGeometry args={[0.06, 0.06, p.h, 16]} />
|
||||||
<meshStandardMaterial color="#475569" roughness={0.5} />
|
<meshStandardMaterial color={pillarColor} roughness={0.5} />
|
||||||
</mesh>
|
</mesh>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
@@ -157,21 +165,21 @@ function IsolatedRoofModel({
|
|||||||
<group position={[-halfDepth - 0.4, 0, -halfDepth]}>
|
<group position={[-halfDepth - 0.4, 0, -halfDepth]}>
|
||||||
<mesh position={[0, height / 2, 0]}>
|
<mesh position={[0, height / 2, 0]}>
|
||||||
<boxGeometry args={[0.015, height, 0.015]} />
|
<boxGeometry args={[0.015, height, 0.015]} />
|
||||||
<meshStandardMaterial color="#64748b" />
|
<meshStandardMaterial color={lineMaterialColor} />
|
||||||
</mesh>
|
</mesh>
|
||||||
<mesh position={[0, height, 0]}>
|
<mesh position={[0, height, 0]}>
|
||||||
<boxGeometry args={[0.1, 0.015, 0.015]} />
|
<boxGeometry args={[0.1, 0.015, 0.015]} />
|
||||||
<meshStandardMaterial color="#64748b" />
|
<meshStandardMaterial color={lineMaterialColor} />
|
||||||
</mesh>
|
</mesh>
|
||||||
<mesh position={[0, 0, 0]}>
|
<mesh position={[0, 0, 0]}>
|
||||||
<boxGeometry args={[0.1, 0.015, 0.015]} />
|
<boxGeometry args={[0.1, 0.015, 0.015]} />
|
||||||
<meshStandardMaterial color="#64748b" />
|
<meshStandardMaterial color={lineMaterialColor} />
|
||||||
</mesh>
|
</mesh>
|
||||||
<Text
|
<Text
|
||||||
position={[-0.15, height / 2, 0]}
|
position={[-0.15, height / 2, 0]}
|
||||||
rotation={[0, -Math.PI / 2, 0]}
|
rotation={[0, -Math.PI / 2, 0]}
|
||||||
fontSize={0.25}
|
fontSize={0.25}
|
||||||
color="#475569"
|
color={labelColor}
|
||||||
anchorX="center"
|
anchorX="center"
|
||||||
anchorY="middle"
|
anchorY="middle"
|
||||||
>
|
>
|
||||||
@@ -183,21 +191,21 @@ function IsolatedRoofModel({
|
|||||||
<group position={[halfDepth + 0.4, height / 2, 0]}>
|
<group position={[halfDepth + 0.4, height / 2, 0]}>
|
||||||
<mesh position={[0, 0, 0]}>
|
<mesh position={[0, 0, 0]}>
|
||||||
<boxGeometry args={[0.015, 0.015, depth]} />
|
<boxGeometry args={[0.015, 0.015, depth]} />
|
||||||
<meshStandardMaterial color="#64748b" />
|
<meshStandardMaterial color={lineMaterialColor} />
|
||||||
</mesh>
|
</mesh>
|
||||||
<mesh position={[0, 0, halfDepth]}>
|
<mesh position={[0, 0, halfDepth]}>
|
||||||
<boxGeometry args={[0.1, 0.015, 0.015]} />
|
<boxGeometry args={[0.1, 0.015, 0.015]} />
|
||||||
<meshStandardMaterial color="#64748b" />
|
<meshStandardMaterial color={lineMaterialColor} />
|
||||||
</mesh>
|
</mesh>
|
||||||
<mesh position={[0, 0, -halfDepth]}>
|
<mesh position={[0, 0, -halfDepth]}>
|
||||||
<boxGeometry args={[0.1, 0.015, 0.015]} />
|
<boxGeometry args={[0.1, 0.015, 0.015]} />
|
||||||
<meshStandardMaterial color="#64748b" />
|
<meshStandardMaterial color={lineMaterialColor} />
|
||||||
</mesh>
|
</mesh>
|
||||||
<Text
|
<Text
|
||||||
position={[0.15, 0, 0]}
|
position={[0.15, 0, 0]}
|
||||||
rotation={[0, Math.PI / 2, 0]}
|
rotation={[0, Math.PI / 2, 0]}
|
||||||
fontSize={0.25}
|
fontSize={0.25}
|
||||||
color="#475569"
|
color={labelColor}
|
||||||
anchorX="center"
|
anchorX="center"
|
||||||
anchorY="middle"
|
anchorY="middle"
|
||||||
>
|
>
|
||||||
@@ -208,7 +216,7 @@ function IsolatedRoofModel({
|
|||||||
{/* Rótulo Superior */}
|
{/* Rótulo Superior */}
|
||||||
<Text
|
<Text
|
||||||
position={[0, centerY + 3, 0]}
|
position={[0, centerY + 3, 0]}
|
||||||
color="#1a202c"
|
color={textColor}
|
||||||
anchorX="center"
|
anchorX="center"
|
||||||
anchorY="bottom"
|
anchorY="bottom"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -114,10 +114,16 @@ function buildTowerGeometry(
|
|||||||
return { nodes, members };
|
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 intensity = Math.min(1, (phi * forceKN) / 30);
|
||||||
const hue = 220 - intensity * 220;
|
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({
|
function TowerModel({
|
||||||
@@ -130,13 +136,16 @@ function TowerModel({
|
|||||||
alphaWind,
|
alphaWind,
|
||||||
forceKN,
|
forceKN,
|
||||||
}: Tower3DInput) {
|
}: Tower3DInput) {
|
||||||
|
const theme = useCanvasTheme();
|
||||||
|
const isDark = theme === 'dark';
|
||||||
|
|
||||||
const geometry = useMemo(
|
const geometry = useMemo(
|
||||||
() => buildTowerGeometry(section, panels, baseWidth, height),
|
() => buildTowerGeometry(section, panels, baseWidth, height),
|
||||||
[section, panels, baseWidth, height],
|
[section, panels, baseWidth, height],
|
||||||
);
|
);
|
||||||
|
|
||||||
const barRadius = barType === 'circular' ? 0.04 : 0.03;
|
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
|
// Direção do vetor de força
|
||||||
const alphaRad = (alphaWind * Math.PI) / 180;
|
const alphaRad = (alphaWind * Math.PI) / 180;
|
||||||
@@ -150,14 +159,14 @@ function TowerModel({
|
|||||||
{/* Solo */}
|
{/* Solo */}
|
||||||
<mesh position={[0, 0, 0]} rotation={[-Math.PI / 2, 0, 0]} receiveShadow>
|
<mesh position={[0, 0, 0]} rotation={[-Math.PI / 2, 0, 0]} receiveShadow>
|
||||||
<planeGeometry args={[baseWidth * 4, baseWidth * 4]} />
|
<planeGeometry args={[baseWidth * 4, baseWidth * 4]} />
|
||||||
<meshStandardMaterial color="#94a3b8" opacity={0.2} transparent />
|
<meshStandardMaterial color={isDark ? "#334155" : "#94a3b8"} opacity={0.2} transparent />
|
||||||
</mesh>
|
</mesh>
|
||||||
|
|
||||||
{/* Nós (esferas pequenas) */}
|
{/* Nós (esferas pequenas) */}
|
||||||
{geometry.nodes.map((node, idx) => (
|
{geometry.nodes.map((node, idx) => (
|
||||||
<mesh key={`node-${idx}`} position={node.toArray()} castShadow>
|
<mesh key={`node-${idx}`} position={node.toArray()} castShadow>
|
||||||
<sphereGeometry args={[barRadius * 1.4, 8, 8]} />
|
<sphereGeometry args={[barRadius * 1.4, 8, 8]} />
|
||||||
<meshStandardMaterial color="#475569" />
|
<meshStandardMaterial color={isDark ? "#94a3b8" : "#475569"} />
|
||||||
</mesh>
|
</mesh>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
@@ -178,9 +187,9 @@ function TowerModel({
|
|||||||
|
|
||||||
const color =
|
const color =
|
||||||
m.type === 'leg'
|
m.type === 'leg'
|
||||||
? '#1e293b'
|
? (isDark ? '#f1f5f9' : '#1e293b')
|
||||||
: m.type === 'horizontal'
|
: m.type === 'horizontal'
|
||||||
? '#64748b'
|
? (isDark ? '#cbd5e1' : '#64748b')
|
||||||
: barColor;
|
: barColor;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -205,7 +214,7 @@ function TowerModel({
|
|||||||
<Text
|
<Text
|
||||||
position={[0, height + 1.0, 0]}
|
position={[0, height + 1.0, 0]}
|
||||||
fontSize={0.5}
|
fontSize={0.5}
|
||||||
color="#1e40af"
|
color={isDark ? "#93c5fd" : "#1e40af"}
|
||||||
anchorX="center"
|
anchorX="center"
|
||||||
anchorY="bottom"
|
anchorY="bottom"
|
||||||
>
|
>
|
||||||
@@ -216,6 +225,8 @@ function TowerModel({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Tower3DViewer(input: Tower3DInput) {
|
export default function Tower3DViewer(input: Tower3DInput) {
|
||||||
|
const theme = useCanvasTheme();
|
||||||
|
const isDark = theme === 'dark';
|
||||||
const { baseWidth, height } = input;
|
const { baseWidth, height } = input;
|
||||||
const dist = Math.max(baseWidth * 3, height * 1.2);
|
const dist = Math.max(baseWidth * 3, height * 1.2);
|
||||||
|
|
||||||
@@ -236,7 +247,13 @@ export default function Tower3DViewer(input: Tower3DInput) {
|
|||||||
<ambientLight intensity={0.6} />
|
<ambientLight intensity={0.6} />
|
||||||
<directionalLight position={[dist, height, dist]} intensity={1.2} castShadow shadow-mapSize-width={1024} shadow-mapSize-height={1024} />
|
<directionalLight position={[dist, height, dist]} intensity={1.2} castShadow shadow-mapSize-width={1024} shadow-mapSize-height={1024} />
|
||||||
<TowerModel {...input} />
|
<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} />
|
<OrbitControls makeDefault minPolarAngle={0} maxPolarAngle={Math.PI / 2 - 0.05} />
|
||||||
<Environment preset="city" />
|
<Environment preset="city" />
|
||||||
</SceneCanvas>
|
</SceneCanvas>
|
||||||
|
|||||||
@@ -62,3 +62,23 @@ export function useTheme(): ThemeContextValue {
|
|||||||
if (!ctx) throw new Error('useTheme deve ser usado dentro de ThemeProvider');
|
if (!ctx) throw new Error('useTheme deve ser usado dentro de ThemeProvider');
|
||||||
return ctx;
|
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 */}
|
{/* 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="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">
|
<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 text-foreground border border-border/80 backdrop-blur-sm shadow-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">{type === 'shed' ? 'Uma água' : 'Duas águas'}</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute top-4 right-4 z-10">
|
<div className="absolute top-4 right-4 z-10">
|
||||||
<EducationalManual type="isolated-roof" params={{ theta, type }} />
|
<EducationalManual type="isolated-roof" params={{ theta, type }} />
|
||||||
|
|||||||
Reference in New Issue
Block a user