diff --git a/app/src/components/Warehouse3D.tsx b/app/src/components/Warehouse3D.tsx
index 6c65dca..9ef2156 100644
--- a/app/src/components/Warehouse3D.tsx
+++ b/app/src/components/Warehouse3D.tsx
@@ -5,6 +5,7 @@ import { useGalpaoStore } from '../store/galpaoStore';
import { useWindStore } from '../store/appStore';
import SceneCanvas from './SceneCanvas';
import FallbackDiagram from './FallbackDiagram';
+import { useCanvasTheme } from '../lib/theme';
function pressureColor(cpe: number, cpi: number): THREE.Color {
const p = cpe - cpi;
@@ -72,6 +73,8 @@ function PressureArrow({
}
export function WarehouseModel() {
+ const theme = useCanvasTheme();
+ const isDark = theme === 'dark';
const { width, length, height, roofPitch, wallCpe, roofCpe } = useGalpaoStore();
const { windAngle, cpi } = useWindStore();
@@ -286,17 +289,17 @@ export function WarehouseModel() {
{/* Cumeeira */}
-
+
{/* Bordas Laterais do Telhado (Beirais) */}
-
+
-
+
{/* === RÓTULOS 3D === */}
@@ -304,7 +307,7 @@ export function WarehouseModel() {
@@ -316,7 +319,7 @@ export function WarehouseModel() {
position={[0, height / 2, -length / 2 - 1.0]}
rotation={[0, Math.PI, 0]}
fontSize={Math.max(0.3, Math.min(0.6, width / 20))}
- color="#1a202c"
+ color={isDark ? "#cbd5e1" : "#1a202c"}
anchorX="center"
anchorY="middle"
>
@@ -328,7 +331,7 @@ export function WarehouseModel() {
position={[-width / 2 - 1.0, height / 2, 0]}
rotation={[0, -Math.PI / 2, 0]}
fontSize={Math.max(0.3, Math.min(0.6, length / 20))}
- color="#1a202c"
+ color={isDark ? "#cbd5e1" : "#1a202c"}
anchorX="center"
anchorY="middle"
>
@@ -340,7 +343,7 @@ export function WarehouseModel() {
position={[width / 2 + 1.0, height / 2, 0]}
rotation={[0, Math.PI / 2, 0]}
fontSize={Math.max(0.3, Math.min(0.6, length / 20))}
- color="#1a202c"
+ color={isDark ? "#cbd5e1" : "#1a202c"}
anchorX="center"
anchorY="middle"
>
@@ -351,6 +354,8 @@ export function WarehouseModel() {
}
export default function Warehouse3DViewer() {
+ const theme = useCanvasTheme();
+ const isDark = theme === 'dark';
const { width, length, height, roofPitch, wallCpe, roofCpe } = useGalpaoStore();
const { windAngle, cpi } = useWindStore();
@@ -384,7 +389,13 @@ export default function Warehouse3DViewer() {
shadow-camera-bottom={-maxDimension}
/>
-
+
diff --git a/app/src/components/three/Bar3D.tsx b/app/src/components/three/Bar3D.tsx
index 96bf9ff..930a67e 100644
--- a/app/src/components/three/Bar3D.tsx
+++ b/app/src/components/three/Bar3D.tsx
@@ -3,6 +3,7 @@ import { OrbitControls, Grid, Environment, Text } from '@react-three/drei';
import * as THREE from 'three';
import SceneCanvas from '../SceneCanvas';
import FallbackDiagram from '../FallbackDiagram';
+import { useCanvasTheme } from '@/lib/theme';
export interface Bar3DInput {
/** Tipo de seção */
@@ -34,14 +35,18 @@ function BarModel({
alpha,
cx,
}: Bar3DInput) {
+ const theme = useCanvasTheme();
+ const isDark = theme === 'dark';
+
const barRadius = barType === 'circular' ? (diameter ?? 0.05) / 2 : Math.min(width ?? 0.1, 0.08) / 2;
const barThickness = barType === 'circular' ? barRadius : barRadius * 0.5;
const barColor = useMemo(() => {
const intensity = Math.min(1, Math.abs(cx) / 2.5);
const hue = 215 - intensity * 215;
- return new THREE.Color(`hsl(${hue}, ${65 + intensity * 25}%, ${45 - intensity * 10}%)`);
- }, [cx]);
+ const l = isDark ? (55 - intensity * 10) : (45 - intensity * 10);
+ return new THREE.Color(`hsl(${hue}, ${65 + intensity * 25}%, ${l}%)`);
+ }, [cx, isDark]);
// Ação do vento deve rotacionar a barra em seu próprio eixo longitudinal (roll).
// O eixo longitudinal na nossa geometria é o X.
@@ -87,7 +92,7 @@ function BarModel({
@@ -172,6 +177,8 @@ function SectionShape({
}
export default function Bar3DViewer(input: Bar3DInput) {
+ const theme = useCanvasTheme();
+ const isDark = theme === 'dark';
const { length, width, diameter } = input;
const size = Math.max(length * 0.6, (width ?? diameter ?? 0.1) * 8);
@@ -192,7 +199,13 @@ export default function Bar3DViewer(input: Bar3DInput) {
-
+
diff --git a/app/src/components/three/Bridge3D.tsx b/app/src/components/three/Bridge3D.tsx
index a566a4b..08a4371 100644
--- a/app/src/components/three/Bridge3D.tsx
+++ b/app/src/components/three/Bridge3D.tsx
@@ -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 Bridge3DInput {
/** Maior vão Lₚ (m) */
@@ -34,6 +35,9 @@ function BridgeModel({
cx,
alpha,
}: Bridge3DInput) {
+ const theme = useCanvasTheme();
+ const isDark = theme === 'dark';
+
const halfL = lp / 2;
const halfW = width / 2;
const deckThickness = Math.max(heg, 0.8);
@@ -43,11 +47,14 @@ function BridgeModel({
const deckColor = useMemo(() => {
const intensity = Math.min(1, Math.abs(cx) / 3);
const hue = 200 - intensity * 60;
- return new THREE.Color(`hsl(${hue}, ${55 + intensity * 30}%, ${50 - intensity * 8}%)`);
- }, [cx]);
+ const l = isDark ? (60 - intensity * 8) : (50 - intensity * 8);
+ return new THREE.Color(`hsl(${hue}, ${55 + intensity * 30}%, ${l}%)`);
+ }, [cx, isDark]);
// Pilar heights: posicionar 3 pilares ao longo do vão
const pillarHeights = useMemo(() => [deckY - 0.5, deckY - 0.5, deckY - 0.5], [deckY]);
+ const barrierColor = isDark ? '#cbd5e1' : '#94a3b8';
+ const pillarColor = isDark ? '#94a3b8' : '#64748b';
return (
@@ -60,11 +67,11 @@ function BridgeModel({
{/* Guarda-rodas/barreira lateral */}
-
+
-
+
{/* Pilares (3 ao longo do comprimento) */}
@@ -73,7 +80,7 @@ function BridgeModel({
return (
-
+
);
})}
@@ -93,7 +100,7 @@ function BridgeModel({
@@ -103,9 +110,9 @@ function BridgeModel({
);
}
-
-
export default function Bridge3DViewer(input: Bridge3DInput) {
+ const theme = useCanvasTheme();
+ const isDark = theme === 'dark';
const { lp, deckHeight, width } = input;
const dist = Math.max(lp * 0.6, deckHeight * 2);
@@ -126,7 +133,13 @@ export default function Bridge3DViewer(input: Bridge3DInput) {
-
+
diff --git a/app/src/components/three/Cylinder3D.tsx b/app/src/components/three/Cylinder3D.tsx
index ce882c4..cc8b5b6 100644
--- a/app/src/components/three/Cylinder3D.tsx
+++ b/app/src/components/three/Cylinder3D.tsx
@@ -3,6 +3,7 @@ import { OrbitControls, Grid, Environment, Text } from '@react-three/drei';
import * as THREE from 'three';
import SceneCanvas from '../SceneCanvas';
import FallbackDiagram from '../FallbackDiagram';
+import { useCanvasTheme } from '@/lib/theme';
export interface Cylinder3DInput {
diameter: number;
@@ -22,6 +23,8 @@ function cylinderColor(cpe: number, cpi: number): THREE.Color {
}
function CylinderModel({ diameter, height, cpeProfile, cpi }: Cylinder3DInput) {
+ const theme = useCanvasTheme();
+ const isDark = theme === 'dark';
const segments = 64;
const radius = diameter / 2;
@@ -121,11 +124,11 @@ function CylinderModel({ diameter, height, cpeProfile, cpi }: Cylinder3DInput) {
{/* Anéis de detalhe (bordas do cilindro) */}
-
+
-
+
{/* Seta indicativa de direção do vento */}
@@ -154,7 +157,7 @@ function CylinderModel({ diameter, height, cpeProfile, cpi }: Cylinder3DInput) {
@@ -165,6 +168,9 @@ function CylinderModel({ diameter, height, cpeProfile, cpi }: Cylinder3DInput) {
}
export default function Cylinder3DViewer({ diameter, height, cpeProfile, cpi }: Cylinder3DInput) {
+ const theme = useCanvasTheme();
+ const isDark = theme === 'dark';
+
const fallback = (
-
+
diff --git a/app/src/components/three/Dome3D.tsx b/app/src/components/three/Dome3D.tsx
index 55cb9ef..e866ae1 100644
--- a/app/src/components/three/Dome3D.tsx
+++ b/app/src/components/three/Dome3D.tsx
@@ -3,6 +3,7 @@ import { OrbitControls, Grid, Environment, Text } from '@react-three/drei';
import * as THREE from 'three';
import SceneCanvas from '../SceneCanvas';
import FallbackDiagram from '../FallbackDiagram';
+import { useCanvasTheme } from '@/lib/theme';
export interface Dome3DInput {
diameter: number;
@@ -14,16 +15,19 @@ export interface Dome3DInput {
cpeLateral: number;
}
-function domeColor(cpe: number, cpi: number): THREE.Color {
+function domeColor(cpe: number, cpi: number, isDark: boolean): THREE.Color {
const p = cpe - cpi;
const intensity = Math.min(1, Math.abs(p) / 1.5);
+ const l = isDark ? (65 - intensity * 15) : (60 - intensity * 25);
if (p > 0) {
- return new THREE.Color(`hsl(${215 - intensity * 10}, ${70 + intensity * 25}%, ${Math.max(35, 60 - intensity * 25)}%)`);
+ return new THREE.Color(`hsl(${215 - intensity * 10}, ${70 + intensity * 25}%, ${Math.max(35, l)}%)`);
}
- return new THREE.Color(`hsl(0, ${70 + intensity * 25}%, ${Math.max(40, 60 - intensity * 20)}%)`);
+ return new THREE.Color(`hsl(0, ${70 + intensity * 25}%, ${Math.max(40, l + 5)}%)`);
}
function DomeModel({ diameter, rise, wallHeight, cpi, cpeBarlavento, cpeTopo, cpeLateral }: Dome3DInput) {
+ const theme = useCanvasTheme();
+ const isDark = theme === 'dark';
const radius = diameter / 2;
const segments = 64;
@@ -31,12 +35,6 @@ function DomeModel({ diameter, rise, wallHeight, cpi, cpeBarlavento, cpeTopo, cp
const domeGeoms = useMemo(() => {
const arr: { startTheta: number; endTheta: number; color: THREE.Color }[] = [];
// Divide a circunferência completa (360°) em 6 zonas (simétricas)
- // 0° a 60°: Barlavento
- // 60° a 120°: Topo
- // 120° a 180°: Lateral
- // 180° a 240°: Lateral (espelhado)
- // 240° a 300°: Topo (espelhado)
- // 300° a 360°: Barlavento (espelhado)
const zones = [
{ fromDeg: 0, toDeg: 60, cpe: cpeBarlavento },
{ fromDeg: 60, toDeg: 120, cpe: cpeTopo },
@@ -49,11 +47,11 @@ function DomeModel({ diameter, rise, wallHeight, cpi, cpeBarlavento, cpeTopo, cp
arr.push({
startTheta: (z.fromDeg * Math.PI) / 180,
endTheta: (z.toDeg * Math.PI) / 180,
- color: domeColor(z.cpe, cpi),
+ color: domeColor(z.cpe, cpi, isDark),
});
}
return arr;
- }, [cpeBarlavento, cpeTopo, cpeLateral, cpi]);
+ }, [cpeBarlavento, cpeTopo, cpeLateral, cpi, isDark]);
// Raio da esfera da calota esférica baseada na flecha (rise) e raio da base (radius)
const rSphere = useMemo(() => {
@@ -71,11 +69,11 @@ function DomeModel({ diameter, rise, wallHeight, cpi, cpeBarlavento, cpeTopo, cp
{/* Detalhes de anéis metálicos nas bordas */}
-
+
-
+
{/* Cúpula de cobertura (Spherical Cap) segmentada */}
@@ -156,7 +154,7 @@ function DomeModel({ diameter, rise, wallHeight, cpi, cpeBarlavento, cpeTopo, cp
@@ -167,6 +165,9 @@ function DomeModel({ diameter, rise, wallHeight, cpi, cpeBarlavento, cpeTopo, cp
}
export default function Dome3DViewer(props: Dome3DInput) {
+ const theme = useCanvasTheme();
+ const isDark = theme === 'dark';
+
const fallback = (
-
+
diff --git a/app/src/components/three/Dynamics3D.tsx b/app/src/components/three/Dynamics3D.tsx
index e2f75bc..52dc4e1 100644
--- a/app/src/components/three/Dynamics3D.tsx
+++ b/app/src/components/three/Dynamics3D.tsx
@@ -4,6 +4,7 @@ import { OrbitControls, Grid, Environment, Text } from '@react-three/drei';
import * as THREE from 'three';
import SceneCanvas from '../SceneCanvas';
import FallbackDiagram from '../FallbackDiagram';
+import { useCanvasTheme } from '@/lib/theme';
export interface Dynamics3DInput {
/** Altura da estrutura (m) */
@@ -138,11 +139,13 @@ function VortexStreet({
}
function DynamicsModel(props: Dynamics3DInput) {
+ const theme = useCanvasTheme();
+ const isDark = theme === 'dark';
return (
-
+
@@ -175,6 +178,8 @@ function DynamicsModel(props: Dynamics3DInput) {
}
export default function Dynamics3DViewer(props: Dynamics3DInput) {
+ const theme = useCanvasTheme();
+ const isDark = theme === 'dark';
const cameraDistance = Math.max(props.height * SCALE * 2, 6);
const fallback = (
@@ -201,7 +206,13 @@ export default function Dynamics3DViewer(props: Dynamics3DInput) {
shadow-mapSize-height={1024}
/>
-
+
diff --git a/app/src/components/three/Sign3D.tsx b/app/src/components/three/Sign3D.tsx
index 79bbb4c..6b449ce 100644
--- a/app/src/components/three/Sign3D.tsx
+++ b/app/src/components/three/Sign3D.tsx
@@ -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 Sign3DInput {
/** Comprimento ℓ (m) */
@@ -34,6 +35,8 @@ function SignModel({
cf,
applicationPoint,
}: Sign3DInput) {
+ const theme = useCanvasTheme();
+ const isDark = theme === 'dark';
const baseY = groundClearance;
const topY = baseY + height;
const halfL = length / 2;
@@ -72,11 +75,11 @@ function SignModel({
<>
-
+
-
+
>
)}
@@ -84,13 +87,13 @@ function SignModel({
{/* Linha do solo (base translúcida) */}
-
+
{/* Eixo horizontal de referência de direção do vento */}
-
+
{/* Marca da excentricidade (Ponto de Aplicação da Resultante) */}
@@ -104,7 +107,7 @@ function SignModel({
position={[0.2, (baseY + topY) / 2, applicationPoint]}
rotation={[0, Math.PI / 2, 0]}
fontSize={0.15}
- color="#d97706"
+ color={isDark ? "#fcd34d" : "#d97706"}
anchorX="left"
anchorY="middle"
>
@@ -123,24 +126,24 @@ function SignModel({
{/* Linha vertical */}
-
+
{/* Traço superior */}
-
+
{/* Traço inferior */}
-
+
{/* Texto da altura */}
@@ -153,24 +156,24 @@ function SignModel({
{/* Linha horizontal longitudinal */}
-
+
{/* Traço frontal */}
-
+
{/* Traço traseiro */}
-
+
{/* Texto do comprimento */}
@@ -182,7 +185,7 @@ function SignModel({
@@ -203,6 +206,9 @@ export default function Sign3DViewer({
forceKN,
applicationPoint,
}: Sign3DInput) {
+ const theme = useCanvasTheme();
+ const isDark = theme === 'dark';
+
const fallback = (
-
+
diff --git a/app/src/components/three/Vault3D.tsx b/app/src/components/three/Vault3D.tsx
index 14612b4..dab7a86 100644
--- a/app/src/components/three/Vault3D.tsx
+++ b/app/src/components/three/Vault3D.tsx
@@ -3,6 +3,7 @@ import { OrbitControls, Grid, Environment, Text } from '@react-three/drei';
import * as THREE from 'three';
import SceneCanvas from '../SceneCanvas';
import FallbackDiagram from '../FallbackDiagram';
+import { useCanvasTheme } from '@/lib/theme';
export interface Vault3DInput {
span: number;
@@ -12,16 +13,19 @@ export interface Vault3DInput {
cpeProfile: Record;
}
-function vaultColor(cpe: number, cpi: number): THREE.Color {
+function vaultColor(cpe: number, cpi: number, isDark: boolean): THREE.Color {
const p = cpe - cpi;
const intensity = Math.min(1, Math.abs(p) / 1.5);
+ const l = isDark ? (65 - intensity * 15) : (60 - intensity * 25);
if (p > 0) {
- return new THREE.Color(`hsl(${215 - intensity * 10}, ${70 + intensity * 25}%, ${Math.max(35, 60 - intensity * 25)}%)`);
+ return new THREE.Color(`hsl(${215 - intensity * 10}, ${70 + intensity * 25}%, ${Math.max(35, l)}%)`);
}
- return new THREE.Color(`hsl(0, ${70 + intensity * 25}%, ${Math.max(40, 60 - intensity * 20)}%)`);
+ return new THREE.Color(`hsl(0, ${70 + intensity * 25}%, ${Math.max(40, l + 5)}%)`);
}
function VaultModel({ span, length, rise, cpi, cpeProfile }: Vault3DInput) {
+ const theme = useCanvasTheme();
+ const isDark = theme === 'dark';
const segments = 64;
const points = useMemo(() => {
const pts: THREE.Vector3[] = [];
@@ -66,7 +70,7 @@ function VaultModel({ span, length, rise, cpi, cpeProfile }: Vault3DInput) {
{/* Casca da Abóbada */}
{zones.map((zone, idx) => {
- const color = vaultColor(zone.cpe, cpi);
+ const color = vaultColor(zone.cpe, cpi, isDark);
const verts: number[] = [];
for (let i = zone.startIdx; i <= zone.endIdx; i++) {
verts.push(points[i].x, points[i].y, points[i].z);
@@ -94,20 +98,20 @@ function VaultModel({ span, length, rise, cpi, cpeProfile }: Vault3DInput) {
{/* Tímpano Traseiro (Z = 0) */}
-
+
{/* Tímpano Frontal (Z = length) */}
-
+
{/* Rótulo de dimensões */}
@@ -118,6 +122,9 @@ function VaultModel({ span, length, rise, cpi, cpeProfile }: Vault3DInput) {
}
export default function Vault3DViewer({ span, length, rise, cpi, cpeProfile }: Vault3DInput) {
+ const theme = useCanvasTheme();
+ const isDark = theme === 'dark';
+
const fallback = (
-
+