feat: apply dark mode contrast corrections to all 3D modules

This commit is contained in:
2026-07-10 16:34:03 +00:00
parent bcca3738d0
commit 9d01915704
8 changed files with 158 additions and 66 deletions
+19 -8
View File
@@ -5,6 +5,7 @@ import { useGalpaoStore } from '../store/galpaoStore';
import { useWindStore } from '../store/appStore'; import { useWindStore } from '../store/appStore';
import SceneCanvas from './SceneCanvas'; import SceneCanvas from './SceneCanvas';
import FallbackDiagram from './FallbackDiagram'; import FallbackDiagram from './FallbackDiagram';
import { useCanvasTheme } from '../lib/theme';
function pressureColor(cpe: number, cpi: number): THREE.Color { function pressureColor(cpe: number, cpi: number): THREE.Color {
const p = cpe - cpi; const p = cpe - cpi;
@@ -72,6 +73,8 @@ function PressureArrow({
} }
export function WarehouseModel() { export function WarehouseModel() {
const theme = useCanvasTheme();
const isDark = theme === 'dark';
const { width, length, height, roofPitch, wallCpe, roofCpe } = useGalpaoStore(); const { width, length, height, roofPitch, wallCpe, roofCpe } = useGalpaoStore();
const { windAngle, cpi } = useWindStore(); const { windAngle, cpi } = useWindStore();
@@ -286,17 +289,17 @@ export function WarehouseModel() {
{/* Cumeeira */} {/* Cumeeira */}
<mesh position={[0, height + roofHeight + 0.02, 0]}> <mesh position={[0, height + roofHeight + 0.02, 0]}>
<boxGeometry args={[0.08, 0.04, length + 0.1]} /> <boxGeometry args={[0.08, 0.04, length + 0.1]} />
<meshStandardMaterial color="#2d3748" roughness={0.5} /> <meshStandardMaterial color={isDark ? "#475569" : "#2d3748"} roughness={0.5} />
</mesh> </mesh>
{/* Bordas Laterais do Telhado (Beirais) */} {/* Bordas Laterais do Telhado (Beirais) */}
<mesh position={[width / 2 + 0.02, height, 0]}> <mesh position={[width / 2 + 0.02, height, 0]}>
<boxGeometry args={[0.04, 0.08, length]} /> <boxGeometry args={[0.04, 0.08, length]} />
<meshStandardMaterial color="#2d3748" /> <meshStandardMaterial color={isDark ? "#475569" : "#2d3748"} />
</mesh> </mesh>
<mesh position={[-width / 2 - 0.02, height, 0]}> <mesh position={[-width / 2 - 0.02, height, 0]}>
<boxGeometry args={[0.04, 0.08, length]} /> <boxGeometry args={[0.04, 0.08, length]} />
<meshStandardMaterial color="#2d3748" /> <meshStandardMaterial color={isDark ? "#475569" : "#2d3748"} />
</mesh> </mesh>
{/* === RÓTULOS 3D === */} {/* === RÓTULOS 3D === */}
@@ -304,7 +307,7 @@ export function WarehouseModel() {
<Text <Text
position={[0, height / 2, length / 2 + 1.0]} position={[0, height / 2, length / 2 + 1.0]}
fontSize={Math.max(0.3, Math.min(0.6, width / 20))} fontSize={Math.max(0.3, Math.min(0.6, width / 20))}
color="#1a202c" color={isDark ? "#cbd5e1" : "#1a202c"}
anchorX="center" anchorX="center"
anchorY="middle" anchorY="middle"
> >
@@ -316,7 +319,7 @@ export function WarehouseModel() {
position={[0, height / 2, -length / 2 - 1.0]} position={[0, height / 2, -length / 2 - 1.0]}
rotation={[0, Math.PI, 0]} rotation={[0, Math.PI, 0]}
fontSize={Math.max(0.3, Math.min(0.6, width / 20))} fontSize={Math.max(0.3, Math.min(0.6, width / 20))}
color="#1a202c" color={isDark ? "#cbd5e1" : "#1a202c"}
anchorX="center" anchorX="center"
anchorY="middle" anchorY="middle"
> >
@@ -328,7 +331,7 @@ export function WarehouseModel() {
position={[-width / 2 - 1.0, height / 2, 0]} position={[-width / 2 - 1.0, height / 2, 0]}
rotation={[0, -Math.PI / 2, 0]} rotation={[0, -Math.PI / 2, 0]}
fontSize={Math.max(0.3, Math.min(0.6, length / 20))} fontSize={Math.max(0.3, Math.min(0.6, length / 20))}
color="#1a202c" color={isDark ? "#cbd5e1" : "#1a202c"}
anchorX="center" anchorX="center"
anchorY="middle" anchorY="middle"
> >
@@ -340,7 +343,7 @@ export function WarehouseModel() {
position={[width / 2 + 1.0, height / 2, 0]} position={[width / 2 + 1.0, height / 2, 0]}
rotation={[0, Math.PI / 2, 0]} rotation={[0, Math.PI / 2, 0]}
fontSize={Math.max(0.3, Math.min(0.6, length / 20))} fontSize={Math.max(0.3, Math.min(0.6, length / 20))}
color="#1a202c" color={isDark ? "#cbd5e1" : "#1a202c"}
anchorX="center" anchorX="center"
anchorY="middle" anchorY="middle"
> >
@@ -351,6 +354,8 @@ export function WarehouseModel() {
} }
export default function Warehouse3DViewer() { export default function Warehouse3DViewer() {
const theme = useCanvasTheme();
const isDark = theme === 'dark';
const { width, length, height, roofPitch, wallCpe, roofCpe } = useGalpaoStore(); const { width, length, height, roofPitch, wallCpe, roofCpe } = useGalpaoStore();
const { windAngle, cpi } = useWindStore(); const { windAngle, cpi } = useWindStore();
@@ -384,7 +389,13 @@ export default function Warehouse3DViewer() {
shadow-camera-bottom={-maxDimension} shadow-camera-bottom={-maxDimension}
/> />
<WarehouseModel /> <WarehouseModel />
<Grid infiniteGrid fadeDistance={maxDimension * 5} sectionColor="#94a3b8" cellColor="#cbd5e1" position={[0, -0.01, 0]} /> <Grid
infiniteGrid
fadeDistance={maxDimension * 5}
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>
+17 -4
View File
@@ -3,6 +3,7 @@ import { OrbitControls, Grid, Environment, Text } from '@react-three/drei';
import * as THREE from 'three'; import * as THREE from 'three';
import SceneCanvas from '../SceneCanvas'; import SceneCanvas from '../SceneCanvas';
import FallbackDiagram from '../FallbackDiagram'; import FallbackDiagram from '../FallbackDiagram';
import { useCanvasTheme } from '@/lib/theme';
export interface Bar3DInput { export interface Bar3DInput {
/** Tipo de seção */ /** Tipo de seção */
@@ -34,14 +35,18 @@ function BarModel({
alpha, alpha,
cx, cx,
}: Bar3DInput) { }: 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 barRadius = barType === 'circular' ? (diameter ?? 0.05) / 2 : Math.min(width ?? 0.1, 0.08) / 2;
const barThickness = barType === 'circular' ? barRadius : barRadius * 0.5; const barThickness = barType === 'circular' ? barRadius : barRadius * 0.5;
const barColor = useMemo(() => { const barColor = useMemo(() => {
const intensity = Math.min(1, Math.abs(cx) / 2.5); const intensity = Math.min(1, Math.abs(cx) / 2.5);
const hue = 215 - intensity * 215; const hue = 215 - intensity * 215;
return new THREE.Color(`hsl(${hue}, ${65 + intensity * 25}%, ${45 - intensity * 10}%)`); const l = isDark ? (55 - intensity * 10) : (45 - intensity * 10);
}, [cx]); 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). // Ação do vento deve rotacionar a barra em seu próprio eixo longitudinal (roll).
// O eixo longitudinal na nossa geometria é o X. // O eixo longitudinal na nossa geometria é o X.
@@ -87,7 +92,7 @@ function BarModel({
<Text <Text
position={[0, -barRadius * 2 - 0.4, 0]} position={[0, -barRadius * 2 - 0.4, 0]}
fontSize={0.3} fontSize={0.3}
color="#1e40af" color={isDark ? "#93c5fd" : "#1e40af"}
anchorX="center" anchorX="center"
anchorY="top" anchorY="top"
> >
@@ -172,6 +177,8 @@ function SectionShape({
} }
export default function Bar3DViewer(input: Bar3DInput) { export default function Bar3DViewer(input: Bar3DInput) {
const theme = useCanvasTheme();
const isDark = theme === 'dark';
const { length, width, diameter } = input; const { length, width, diameter } = input;
const size = Math.max(length * 0.6, (width ?? diameter ?? 0.1) * 8); const size = Math.max(length * 0.6, (width ?? diameter ?? 0.1) * 8);
@@ -192,7 +199,13 @@ export default function Bar3DViewer(input: Bar3DInput) {
<ambientLight intensity={0.6} /> <ambientLight intensity={0.6} />
<directionalLight position={[size, size, size]} intensity={1.2} castShadow shadow-mapSize-width={1024} shadow-mapSize-height={1024} /> <directionalLight position={[size, size, size]} intensity={1.2} castShadow shadow-mapSize-width={1024} shadow-mapSize-height={1024} />
<BarModel {...input} /> <BarModel {...input} />
<Grid infiniteGrid fadeDistance={size * 2} sectionColor="#94a3b8" cellColor="#cbd5e1" position={[0, -size * 0.3, 0]} /> <Grid
infiniteGrid
fadeDistance={size * 2}
sectionColor={isDark ? "#475569" : "#94a3b8"}
cellColor={isDark ? "#1e293b" : "#cbd5e1"}
position={[0, -size * 0.3, 0]}
/>
<OrbitControls makeDefault minPolarAngle={0} maxPolarAngle={Math.PI - 0.05} /> <OrbitControls makeDefault minPolarAngle={0} maxPolarAngle={Math.PI - 0.05} />
<Environment preset="city" /> <Environment preset="city" />
</SceneCanvas> </SceneCanvas>
+22 -9
View File
@@ -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 Bridge3DInput { export interface Bridge3DInput {
/** Maior vão Lₚ (m) */ /** Maior vão Lₚ (m) */
@@ -34,6 +35,9 @@ function BridgeModel({
cx, cx,
alpha, alpha,
}: Bridge3DInput) { }: Bridge3DInput) {
const theme = useCanvasTheme();
const isDark = theme === 'dark';
const halfL = lp / 2; const halfL = lp / 2;
const halfW = width / 2; const halfW = width / 2;
const deckThickness = Math.max(heg, 0.8); const deckThickness = Math.max(heg, 0.8);
@@ -43,11 +47,14 @@ function BridgeModel({
const deckColor = useMemo(() => { const deckColor = useMemo(() => {
const intensity = Math.min(1, Math.abs(cx) / 3); const intensity = Math.min(1, Math.abs(cx) / 3);
const hue = 200 - intensity * 60; const hue = 200 - intensity * 60;
return new THREE.Color(`hsl(${hue}, ${55 + intensity * 30}%, ${50 - intensity * 8}%)`); const l = isDark ? (60 - intensity * 8) : (50 - intensity * 8);
}, [cx]); return new THREE.Color(`hsl(${hue}, ${55 + intensity * 30}%, ${l}%)`);
}, [cx, isDark]);
// Pilar heights: posicionar 3 pilares ao longo do vão // Pilar heights: posicionar 3 pilares ao longo do vão
const pillarHeights = useMemo(() => [deckY - 0.5, deckY - 0.5, deckY - 0.5], [deckY]); const pillarHeights = useMemo(() => [deckY - 0.5, deckY - 0.5, deckY - 0.5], [deckY]);
const barrierColor = isDark ? '#cbd5e1' : '#94a3b8';
const pillarColor = isDark ? '#94a3b8' : '#64748b';
return ( return (
<group> <group>
@@ -60,11 +67,11 @@ function BridgeModel({
{/* Guarda-rodas/barreira lateral */} {/* Guarda-rodas/barreira lateral */}
<mesh position={[0, deckY + deckThickness / 2 + 0.3, halfW - 0.15]} castShadow> <mesh position={[0, deckY + deckThickness / 2 + 0.3, halfW - 0.15]} castShadow>
<boxGeometry args={[lp, 0.5, 0.1]} /> <boxGeometry args={[lp, 0.5, 0.1]} />
<meshStandardMaterial color="#94a3b8" roughness={0.7} /> <meshStandardMaterial color={barrierColor} roughness={0.7} />
</mesh> </mesh>
<mesh position={[0, deckY + deckThickness / 2 + 0.3, -halfW + 0.15]} castShadow> <mesh position={[0, deckY + deckThickness / 2 + 0.3, -halfW + 0.15]} castShadow>
<boxGeometry args={[lp, 0.5, 0.1]} /> <boxGeometry args={[lp, 0.5, 0.1]} />
<meshStandardMaterial color="#94a3b8" roughness={0.7} /> <meshStandardMaterial color={barrierColor} roughness={0.7} />
</mesh> </mesh>
{/* Pilares (3 ao longo do comprimento) */} {/* Pilares (3 ao longo do comprimento) */}
@@ -73,7 +80,7 @@ function BridgeModel({
return ( return (
<mesh key={`pillar-${i}`} position={[x, h / 2, 0]} castShadow receiveShadow> <mesh key={`pillar-${i}`} position={[x, h / 2, 0]} castShadow receiveShadow>
<boxGeometry args={[1.5, h, 1.5]} /> <boxGeometry args={[1.5, h, 1.5]} />
<meshStandardMaterial color="#64748b" roughness={0.7} /> <meshStandardMaterial color={pillarColor} roughness={0.7} />
</mesh> </mesh>
); );
})} })}
@@ -93,7 +100,7 @@ function BridgeModel({
<Text <Text
position={[0, deckY + deckThickness + 1.5, 0]} position={[0, deckY + deckThickness + 1.5, 0]}
fontSize={1.2} fontSize={1.2}
color="#1e40af" color={isDark ? "#93c5fd" : "#1e40af"}
anchorX="center" anchorX="center"
anchorY="bottom" anchorY="bottom"
> >
@@ -103,9 +110,9 @@ function BridgeModel({
); );
} }
export default function Bridge3DViewer(input: Bridge3DInput) { export default function Bridge3DViewer(input: Bridge3DInput) {
const theme = useCanvasTheme();
const isDark = theme === 'dark';
const { lp, deckHeight, width } = input; const { lp, deckHeight, width } = input;
const dist = Math.max(lp * 0.6, deckHeight * 2); const dist = Math.max(lp * 0.6, deckHeight * 2);
@@ -126,7 +133,13 @@ export default function Bridge3DViewer(input: Bridge3DInput) {
<ambientLight intensity={0.6} /> <ambientLight intensity={0.6} />
<directionalLight position={[lp, deckHeight * 3, width * 3]} intensity={1.2} castShadow shadow-mapSize-width={1024} shadow-mapSize-height={1024} /> <directionalLight position={[lp, deckHeight * 3, width * 3]} intensity={1.2} castShadow shadow-mapSize-width={1024} shadow-mapSize-height={1024} />
<BridgeModel {...input} /> <BridgeModel {...input} />
<Grid infiniteGrid fadeDistance={lp * 0.5} sectionColor="#94a3b8" cellColor="#cbd5e1" position={[0, -0.01, 0]} /> <Grid
infiniteGrid
fadeDistance={lp * 0.5}
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>
+16 -4
View File
@@ -3,6 +3,7 @@ import { OrbitControls, Grid, Environment, Text } from '@react-three/drei';
import * as THREE from 'three'; import * as THREE from 'three';
import SceneCanvas from '../SceneCanvas'; import SceneCanvas from '../SceneCanvas';
import FallbackDiagram from '../FallbackDiagram'; import FallbackDiagram from '../FallbackDiagram';
import { useCanvasTheme } from '@/lib/theme';
export interface Cylinder3DInput { export interface Cylinder3DInput {
diameter: number; diameter: number;
@@ -22,6 +23,8 @@ function cylinderColor(cpe: number, cpi: number): THREE.Color {
} }
function CylinderModel({ diameter, height, cpeProfile, cpi }: Cylinder3DInput) { function CylinderModel({ diameter, height, cpeProfile, cpi }: Cylinder3DInput) {
const theme = useCanvasTheme();
const isDark = theme === 'dark';
const segments = 64; const segments = 64;
const radius = diameter / 2; const radius = diameter / 2;
@@ -121,11 +124,11 @@ function CylinderModel({ diameter, height, cpeProfile, cpi }: Cylinder3DInput) {
{/* Anéis de detalhe (bordas do cilindro) */} {/* Anéis de detalhe (bordas do cilindro) */}
<mesh position={[0, height + 0.01, 0]} rotation={[-Math.PI / 2, 0, 0]}> <mesh position={[0, height + 0.01, 0]} rotation={[-Math.PI / 2, 0, 0]}>
<ringGeometry args={[radius - 0.03, radius + 0.03, segments]} /> <ringGeometry args={[radius - 0.03, radius + 0.03, segments]} />
<meshStandardMaterial color="#2d3748" roughness={0.5} /> <meshStandardMaterial color={isDark ? "#475569" : "#2d3748"} roughness={0.5} />
</mesh> </mesh>
<mesh position={[0, 0.01, 0]} rotation={[-Math.PI / 2, 0, 0]}> <mesh position={[0, 0.01, 0]} rotation={[-Math.PI / 2, 0, 0]}>
<ringGeometry args={[radius - 0.03, radius + 0.03, segments]} /> <ringGeometry args={[radius - 0.03, radius + 0.03, segments]} />
<meshStandardMaterial color="#2d3748" roughness={0.5} /> <meshStandardMaterial color={isDark ? "#475569" : "#2d3748"} roughness={0.5} />
</mesh> </mesh>
{/* Seta indicativa de direção do vento */} {/* Seta indicativa de direção do vento */}
@@ -154,7 +157,7 @@ function CylinderModel({ diameter, height, cpeProfile, cpi }: Cylinder3DInput) {
<Text <Text
position={[0, height + 0.8, 0]} position={[0, height + 0.8, 0]}
fontSize={Math.max(0.3, Math.min(0.6, diameter / 10))} fontSize={Math.max(0.3, Math.min(0.6, diameter / 10))}
color="#1a202c" color={isDark ? "#cbd5e1" : "#1a202c"}
anchorX="center" anchorX="center"
anchorY="bottom" anchorY="bottom"
> >
@@ -165,6 +168,9 @@ function CylinderModel({ diameter, height, cpeProfile, cpi }: Cylinder3DInput) {
} }
export default function Cylinder3DViewer({ diameter, height, cpeProfile, cpi }: Cylinder3DInput) { export default function Cylinder3DViewer({ diameter, height, cpeProfile, cpi }: Cylinder3DInput) {
const theme = useCanvasTheme();
const isDark = theme === 'dark';
const fallback = ( const fallback = (
<FallbackDiagram <FallbackDiagram
type="cylinder" type="cylinder"
@@ -195,7 +201,13 @@ export default function Cylinder3DViewer({ diameter, height, cpeProfile, cpi }:
shadow-camera-bottom={-maxDim} shadow-camera-bottom={-maxDim}
/> />
<CylinderModel diameter={diameter} height={height} cpeProfile={cpeProfile} cpi={cpi} /> <CylinderModel diameter={diameter} height={height} cpeProfile={cpeProfile} cpi={cpi} />
<Grid infiniteGrid fadeDistance={maxDim * 5} sectionColor="#94a3b8" cellColor="#cbd5e1" position={[0, -0.01, 0]} /> <Grid
infiniteGrid
fadeDistance={maxDim * 5}
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>
+22 -15
View File
@@ -3,6 +3,7 @@ import { OrbitControls, Grid, Environment, Text } from '@react-three/drei';
import * as THREE from 'three'; import * as THREE from 'three';
import SceneCanvas from '../SceneCanvas'; import SceneCanvas from '../SceneCanvas';
import FallbackDiagram from '../FallbackDiagram'; import FallbackDiagram from '../FallbackDiagram';
import { useCanvasTheme } from '@/lib/theme';
export interface Dome3DInput { export interface Dome3DInput {
diameter: number; diameter: number;
@@ -14,16 +15,19 @@ export interface Dome3DInput {
cpeLateral: number; 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 p = cpe - cpi;
const intensity = Math.min(1, Math.abs(p) / 1.5); const intensity = Math.min(1, Math.abs(p) / 1.5);
const l = isDark ? (65 - intensity * 15) : (60 - intensity * 25);
if (p > 0) { 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) { function DomeModel({ diameter, rise, wallHeight, cpi, cpeBarlavento, cpeTopo, cpeLateral }: Dome3DInput) {
const theme = useCanvasTheme();
const isDark = theme === 'dark';
const radius = diameter / 2; const radius = diameter / 2;
const segments = 64; const segments = 64;
@@ -31,12 +35,6 @@ function DomeModel({ diameter, rise, wallHeight, cpi, cpeBarlavento, cpeTopo, cp
const domeGeoms = useMemo(() => { const domeGeoms = useMemo(() => {
const arr: { startTheta: number; endTheta: number; color: THREE.Color }[] = []; const arr: { startTheta: number; endTheta: number; color: THREE.Color }[] = [];
// Divide a circunferência completa (360°) em 6 zonas (simétricas) // 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 = [ const zones = [
{ fromDeg: 0, toDeg: 60, cpe: cpeBarlavento }, { fromDeg: 0, toDeg: 60, cpe: cpeBarlavento },
{ fromDeg: 60, toDeg: 120, cpe: cpeTopo }, { fromDeg: 60, toDeg: 120, cpe: cpeTopo },
@@ -49,11 +47,11 @@ function DomeModel({ diameter, rise, wallHeight, cpi, cpeBarlavento, cpeTopo, cp
arr.push({ arr.push({
startTheta: (z.fromDeg * Math.PI) / 180, startTheta: (z.fromDeg * Math.PI) / 180,
endTheta: (z.toDeg * Math.PI) / 180, endTheta: (z.toDeg * Math.PI) / 180,
color: domeColor(z.cpe, cpi), color: domeColor(z.cpe, cpi, isDark),
}); });
} }
return arr; 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) // Raio da esfera da calota esférica baseada na flecha (rise) e raio da base (radius)
const rSphere = useMemo(() => { const rSphere = useMemo(() => {
@@ -71,11 +69,11 @@ function DomeModel({ diameter, rise, wallHeight, cpi, cpeBarlavento, cpeTopo, cp
{/* Detalhes de anéis metálicos nas bordas */} {/* Detalhes de anéis metálicos nas bordas */}
<mesh position={[0, 0.01, 0]} rotation={[-Math.PI / 2, 0, 0]}> <mesh position={[0, 0.01, 0]} rotation={[-Math.PI / 2, 0, 0]}>
<ringGeometry args={[radius - 0.03, radius + 0.03, segments]} /> <ringGeometry args={[radius - 0.03, radius + 0.03, segments]} />
<meshStandardMaterial color="#2d3748" roughness={0.5} /> <meshStandardMaterial color={isDark ? "#475569" : "#2d3748"} roughness={0.5} />
</mesh> </mesh>
<mesh position={[0, wallHeight, 0]} rotation={[-Math.PI / 2, 0, 0]}> <mesh position={[0, wallHeight, 0]} rotation={[-Math.PI / 2, 0, 0]}>
<ringGeometry args={[radius - 0.03, radius + 0.03, segments]} /> <ringGeometry args={[radius - 0.03, radius + 0.03, segments]} />
<meshStandardMaterial color="#2d3748" roughness={0.5} /> <meshStandardMaterial color={isDark ? "#475569" : "#2d3748"} roughness={0.5} />
</mesh> </mesh>
{/* Cúpula de cobertura (Spherical Cap) segmentada */} {/* Cúpula de cobertura (Spherical Cap) segmentada */}
@@ -156,7 +154,7 @@ function DomeModel({ diameter, rise, wallHeight, cpi, cpeBarlavento, cpeTopo, cp
<Text <Text
position={[0, wallHeight + rise + 0.8, 0]} position={[0, wallHeight + rise + 0.8, 0]}
fontSize={Math.max(0.3, Math.min(0.6, diameter / 12))} fontSize={Math.max(0.3, Math.min(0.6, diameter / 12))}
color="#1a202c" color={isDark ? "#cbd5e1" : "#1a202c"}
anchorX="center" anchorX="center"
anchorY="bottom" anchorY="bottom"
> >
@@ -167,6 +165,9 @@ function DomeModel({ diameter, rise, wallHeight, cpi, cpeBarlavento, cpeTopo, cp
} }
export default function Dome3DViewer(props: Dome3DInput) { export default function Dome3DViewer(props: Dome3DInput) {
const theme = useCanvasTheme();
const isDark = theme === 'dark';
const fallback = ( const fallback = (
<FallbackDiagram <FallbackDiagram
type="dome" type="dome"
@@ -197,7 +198,13 @@ export default function Dome3DViewer(props: Dome3DInput) {
shadow-camera-bottom={-maxDim} shadow-camera-bottom={-maxDim}
/> />
<DomeModel {...props} /> <DomeModel {...props} />
<Grid infiniteGrid fadeDistance={maxDim * 5} sectionColor="#94a3b8" cellColor="#cbd5e1" position={[0, -0.01, 0]} /> <Grid
infiniteGrid
fadeDistance={maxDim * 5}
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>
+14 -3
View File
@@ -4,6 +4,7 @@ import { OrbitControls, Grid, Environment, Text } from '@react-three/drei';
import * as THREE from 'three'; import * as THREE from 'three';
import SceneCanvas from '../SceneCanvas'; import SceneCanvas from '../SceneCanvas';
import FallbackDiagram from '../FallbackDiagram'; import FallbackDiagram from '../FallbackDiagram';
import { useCanvasTheme } from '@/lib/theme';
export interface Dynamics3DInput { export interface Dynamics3DInput {
/** Altura da estrutura (m) */ /** Altura da estrutura (m) */
@@ -138,11 +139,13 @@ function VortexStreet({
} }
function DynamicsModel(props: Dynamics3DInput) { function DynamicsModel(props: Dynamics3DInput) {
const theme = useCanvasTheme();
const isDark = theme === 'dark';
return ( return (
<group> <group>
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, -0.01, 0]} receiveShadow> <mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, -0.01, 0]} receiveShadow>
<planeGeometry args={[20, 20]} /> <planeGeometry args={[20, 20]} />
<meshStandardMaterial color="#94a3b8" transparent opacity={0.15} /> <meshStandardMaterial color={isDark ? "#1e293b" : "#94a3b8"} transparent opacity={0.15} />
</mesh> </mesh>
<OscillatingBuilding <OscillatingBuilding
@@ -164,7 +167,7 @@ function DynamicsModel(props: Dynamics3DInput) {
<Text <Text
position={[0, props.height * SCALE + 0.8, 0]} position={[0, props.height * SCALE + 0.8, 0]}
fontSize={0.5} fontSize={0.5}
color="#1e40af" color={isDark ? "#93c5fd" : "#1e40af"}
anchorX="center" anchorX="center"
anchorY="bottom" anchorY="bottom"
> >
@@ -175,6 +178,8 @@ function DynamicsModel(props: Dynamics3DInput) {
} }
export default function Dynamics3DViewer(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 cameraDistance = Math.max(props.height * SCALE * 2, 6);
const fallback = ( const fallback = (
@@ -201,7 +206,13 @@ export default function Dynamics3DViewer(props: Dynamics3DInput) {
shadow-mapSize-height={1024} shadow-mapSize-height={1024}
/> />
<DynamicsModel {...props} /> <DynamicsModel {...props} />
<Grid infiniteGrid fadeDistance={50} sectionColor="#94a3b8" cellColor="#cbd5e1" position={[0, -0.01, 0]} /> <Grid
infiniteGrid
fadeDistance={50}
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>
+27 -15
View File
@@ -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 Sign3DInput { export interface Sign3DInput {
/** Comprimento (m) */ /** Comprimento (m) */
@@ -34,6 +35,8 @@ function SignModel({
cf, cf,
applicationPoint, applicationPoint,
}: Sign3DInput) { }: Sign3DInput) {
const theme = useCanvasTheme();
const isDark = theme === 'dark';
const baseY = groundClearance; const baseY = groundClearance;
const topY = baseY + height; const topY = baseY + height;
const halfL = length / 2; const halfL = length / 2;
@@ -72,11 +75,11 @@ function SignModel({
<> <>
<mesh position={[0, (baseY + topY) / 2, halfL]} castShadow> <mesh position={[0, (baseY + topY) / 2, halfL]} castShadow>
<boxGeometry args={[0.3, height * 0.95, 0.04]} /> <boxGeometry args={[0.3, height * 0.95, 0.04]} />
<meshStandardMaterial color="#64748b" opacity={0.6} transparent side={THREE.DoubleSide} /> <meshStandardMaterial color={isDark ? "#475569" : "#64748b"} opacity={0.6} transparent side={THREE.DoubleSide} />
</mesh> </mesh>
<mesh position={[0, (baseY + topY) / 2, -halfL]} castShadow> <mesh position={[0, (baseY + topY) / 2, -halfL]} castShadow>
<boxGeometry args={[0.3, height * 0.95, 0.04]} /> <boxGeometry args={[0.3, height * 0.95, 0.04]} />
<meshStandardMaterial color="#64748b" opacity={0.6} transparent side={THREE.DoubleSide} /> <meshStandardMaterial color={isDark ? "#475569" : "#64748b"} opacity={0.6} transparent side={THREE.DoubleSide} />
</mesh> </mesh>
</> </>
)} )}
@@ -84,13 +87,13 @@ function SignModel({
{/* Linha do solo (base translúcida) */} {/* Linha do solo (base translúcida) */}
<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={[length * 1.5, length * 1.5]} /> <planeGeometry args={[length * 1.5, length * 1.5]} />
<meshStandardMaterial color="#94a3b8" opacity={0.15} transparent /> <meshStandardMaterial color={isDark ? "#1e293b" : "#94a3b8"} opacity={0.15} transparent />
</mesh> </mesh>
{/* Eixo horizontal de referência de direção do vento */} {/* Eixo horizontal de referência de direção do vento */}
<mesh position={[0, 0.005, 0]} rotation={[-Math.PI / 2, 0, 0]}> <mesh position={[0, 0.005, 0]} rotation={[-Math.PI / 2, 0, 0]}>
<planeGeometry args={[length * 1.5, 0.03]} /> <planeGeometry args={[length * 1.5, 0.03]} />
<meshStandardMaterial color="#475569" opacity={0.5} transparent /> <meshStandardMaterial color={isDark ? "#334155" : "#475569"} opacity={0.5} transparent />
</mesh> </mesh>
{/* Marca da excentricidade (Ponto de Aplicação da Resultante) */} {/* Marca da excentricidade (Ponto de Aplicação da Resultante) */}
@@ -104,7 +107,7 @@ function SignModel({
position={[0.2, (baseY + topY) / 2, applicationPoint]} position={[0.2, (baseY + topY) / 2, applicationPoint]}
rotation={[0, Math.PI / 2, 0]} rotation={[0, Math.PI / 2, 0]}
fontSize={0.15} fontSize={0.15}
color="#d97706" color={isDark ? "#fcd34d" : "#d97706"}
anchorX="left" anchorX="left"
anchorY="middle" anchorY="middle"
> >
@@ -123,24 +126,24 @@ function SignModel({
{/* Linha vertical */} {/* Linha vertical */}
<mesh position={[0, (baseY + topY) / 2, 0]}> <mesh position={[0, (baseY + topY) / 2, 0]}>
<boxGeometry args={[0.015, height, 0.015]} /> <boxGeometry args={[0.015, height, 0.015]} />
<meshStandardMaterial color="#64748b" /> <meshStandardMaterial color={isDark ? "#475569" : "#64748b"} />
</mesh> </mesh>
{/* Traço superior */} {/* Traço superior */}
<mesh position={[0, topY, 0]}> <mesh position={[0, topY, 0]}>
<boxGeometry args={[0.1, 0.015, 0.015]} /> <boxGeometry args={[0.1, 0.015, 0.015]} />
<meshStandardMaterial color="#64748b" /> <meshStandardMaterial color={isDark ? "#475569" : "#64748b"} />
</mesh> </mesh>
{/* Traço inferior */} {/* Traço inferior */}
<mesh position={[0, baseY, 0]}> <mesh position={[0, baseY, 0]}>
<boxGeometry args={[0.1, 0.015, 0.015]} /> <boxGeometry args={[0.1, 0.015, 0.015]} />
<meshStandardMaterial color="#64748b" /> <meshStandardMaterial color={isDark ? "#475569" : "#64748b"} />
</mesh> </mesh>
{/* Texto da altura */} {/* Texto da altura */}
<Text <Text
position={[-0.15, (baseY + topY) / 2, 0]} position={[-0.15, (baseY + topY) / 2, 0]}
rotation={[0, -Math.PI / 2, 0]} rotation={[0, -Math.PI / 2, 0]}
fontSize={0.25} fontSize={0.25}
color="#475569" color={isDark ? "#94a3b8" : "#475569"}
anchorX="center" anchorX="center"
anchorY="middle" anchorY="middle"
> >
@@ -153,24 +156,24 @@ function SignModel({
{/* Linha horizontal longitudinal */} {/* Linha horizontal longitudinal */}
<mesh position={[0, 0, 0]}> <mesh position={[0, 0, 0]}>
<boxGeometry args={[0.015, 0.015, length]} /> <boxGeometry args={[0.015, 0.015, length]} />
<meshStandardMaterial color="#64748b" /> <meshStandardMaterial color={isDark ? "#475569" : "#64748b"} />
</mesh> </mesh>
{/* Traço frontal */} {/* Traço frontal */}
<mesh position={[0, 0, halfL]}> <mesh position={[0, 0, halfL]}>
<boxGeometry args={[0.1, 0.015, 0.015]} /> <boxGeometry args={[0.1, 0.015, 0.015]} />
<meshStandardMaterial color="#64748b" /> <meshStandardMaterial color={isDark ? "#475569" : "#64748b"} />
</mesh> </mesh>
{/* Traço traseiro */} {/* Traço traseiro */}
<mesh position={[0, 0, -halfL]}> <mesh position={[0, 0, -halfL]}>
<boxGeometry args={[0.1, 0.015, 0.015]} /> <boxGeometry args={[0.1, 0.015, 0.015]} />
<meshStandardMaterial color="#64748b" /> <meshStandardMaterial color={isDark ? "#475569" : "#64748b"} />
</mesh> </mesh>
{/* Texto do comprimento */} {/* Texto do comprimento */}
<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={isDark ? "#94a3b8" : "#475569"}
anchorX="center" anchorX="center"
anchorY="middle" anchorY="middle"
> >
@@ -182,7 +185,7 @@ function SignModel({
<Text <Text
position={[0, topY + 0.6, 0]} position={[0, topY + 0.6, 0]}
fontSize={0.4} fontSize={0.4}
color="#1a202c" color={isDark ? "#cbd5e1" : "#1a202c"}
anchorX="center" anchorX="center"
anchorY="bottom" anchorY="bottom"
> >
@@ -203,6 +206,9 @@ export default function Sign3DViewer({
forceKN, forceKN,
applicationPoint, applicationPoint,
}: Sign3DInput) { }: Sign3DInput) {
const theme = useCanvasTheme();
const isDark = theme === 'dark';
const fallback = ( const fallback = (
<FallbackDiagram <FallbackDiagram
type="sign" type="sign"
@@ -230,7 +236,13 @@ export default function Sign3DViewer({
forceKN={forceKN} forceKN={forceKN}
applicationPoint={applicationPoint} applicationPoint={applicationPoint}
/> />
<Grid infiniteGrid fadeDistance={maxDim * 5} sectionColor="#94a3b8" cellColor="#cbd5e1" position={[0, -0.01, 0]} /> <Grid
infiniteGrid
fadeDistance={maxDim * 5}
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>
+21 -8
View File
@@ -3,6 +3,7 @@ import { OrbitControls, Grid, Environment, Text } from '@react-three/drei';
import * as THREE from 'three'; import * as THREE from 'three';
import SceneCanvas from '../SceneCanvas'; import SceneCanvas from '../SceneCanvas';
import FallbackDiagram from '../FallbackDiagram'; import FallbackDiagram from '../FallbackDiagram';
import { useCanvasTheme } from '@/lib/theme';
export interface Vault3DInput { export interface Vault3DInput {
span: number; span: number;
@@ -12,16 +13,19 @@ export interface Vault3DInput {
cpeProfile: Record<string, number>; cpeProfile: Record<string, number>;
} }
function vaultColor(cpe: number, cpi: number): THREE.Color { function vaultColor(cpe: number, cpi: number, isDark: boolean): THREE.Color {
const p = cpe - cpi; const p = cpe - cpi;
const intensity = Math.min(1, Math.abs(p) / 1.5); const intensity = Math.min(1, Math.abs(p) / 1.5);
const l = isDark ? (65 - intensity * 15) : (60 - intensity * 25);
if (p > 0) { 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) { function VaultModel({ span, length, rise, cpi, cpeProfile }: Vault3DInput) {
const theme = useCanvasTheme();
const isDark = theme === 'dark';
const segments = 64; const segments = 64;
const points = useMemo(() => { const points = useMemo(() => {
const pts: THREE.Vector3[] = []; const pts: THREE.Vector3[] = [];
@@ -66,7 +70,7 @@ function VaultModel({ span, length, rise, cpi, cpeProfile }: Vault3DInput) {
<group> <group>
{/* Casca da Abóbada */} {/* Casca da Abóbada */}
{zones.map((zone, idx) => { {zones.map((zone, idx) => {
const color = vaultColor(zone.cpe, cpi); const color = vaultColor(zone.cpe, cpi, isDark);
const verts: number[] = []; const verts: number[] = [];
for (let i = zone.startIdx; i <= zone.endIdx; i++) { for (let i = zone.startIdx; i <= zone.endIdx; i++) {
verts.push(points[i].x, points[i].y, points[i].z); 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 Traseiro (Z = 0) */}
<mesh position={[0, 0, 0]} castShadow receiveShadow> <mesh position={[0, 0, 0]} castShadow receiveShadow>
<shapeGeometry args={[archShape]} /> <shapeGeometry args={[archShape]} />
<meshStandardMaterial color="#cbd5e1" opacity={0.7} transparent side={THREE.DoubleSide} roughness={0.5} /> <meshStandardMaterial color={isDark ? "#334155" : "#cbd5e1"} opacity={0.7} transparent side={THREE.DoubleSide} roughness={0.5} />
</mesh> </mesh>
{/* Tímpano Frontal (Z = length) */} {/* Tímpano Frontal (Z = length) */}
<mesh position={[0, 0, length]} castShadow receiveShadow> <mesh position={[0, 0, length]} castShadow receiveShadow>
<shapeGeometry args={[archShape]} /> <shapeGeometry args={[archShape]} />
<meshStandardMaterial color="#cbd5e1" opacity={0.7} transparent side={THREE.DoubleSide} roughness={0.5} /> <meshStandardMaterial color={isDark ? "#334155" : "#cbd5e1"} opacity={0.7} transparent side={THREE.DoubleSide} roughness={0.5} />
</mesh> </mesh>
{/* Rótulo de dimensões */} {/* Rótulo de dimensões */}
<Text <Text
position={[0, rise + 0.6, length / 2]} position={[0, rise + 0.6, length / 2]}
fontSize={Math.max(0.3, Math.min(0.6, span / 20))} fontSize={Math.max(0.3, Math.min(0.6, span / 20))}
color="#1a202c" color={isDark ? "#cbd5e1" : "#1a202c"}
anchorX="center" anchorX="center"
anchorY="bottom" anchorY="bottom"
> >
@@ -118,6 +122,9 @@ function VaultModel({ span, length, rise, cpi, cpeProfile }: Vault3DInput) {
} }
export default function Vault3DViewer({ 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 = ( const fallback = (
<FallbackDiagram <FallbackDiagram
type="vault" type="vault"
@@ -143,7 +150,13 @@ export default function Vault3DViewer({ span, length, rise, cpi, cpeProfile }: V
shadow-mapSize-height={1024} shadow-mapSize-height={1024}
/> />
<VaultModel span={span} length={length} rise={rise} cpi={cpi} cpeProfile={cpeProfile} /> <VaultModel span={span} length={length} rise={rise} cpi={cpi} cpeProfile={cpeProfile} />
<Grid infiniteGrid fadeDistance={maxDimension * 5} sectionColor="#94a3b8" cellColor="#cbd5e1" position={[0, -0.01, 0]} /> <Grid
infiniteGrid
fadeDistance={maxDimension * 5}
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>