Files
BrainWind/app/src/components/three/Bridge3D.tsx
T

148 lines
4.9 KiB
TypeScript

import { useMemo } from 'react';
import { Grid, Environment, Text } from '@react-three/drei';
import { ViewerOrbitControls as OrbitControls } from './ViewerOrbitControls';
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) */
lp: number;
/** Largura do tabuleiro B (m) */
width: number;
/** Altura do tabuleiro z (m) */
deckHeight: number;
/** Altura equivalente H_eq (m) — soma de áreas expostas por metro */
heg: number;
/** Coeficiente de arrasto Cx (adimensional) */
cx: number;
/** Coeficiente de sustentação Cz (adimensional) */
cz: number;
/** Força de arrasto por unidade de comprimento Fx (kN/m) */
fxPerLength: number;
/** Força de sustentação por unidade de comprimento Fz (kN/m) */
fzPerLength: number;
/** Ângulo de ataque do vento (graus) */
alpha: number;
}
function BridgeModel({
lp,
width,
deckHeight,
heg,
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);
const deckY = deckHeight;
// Cor do tabuleiro baseada em Cx
const deckColor = useMemo(() => {
const intensity = Math.min(1, Math.abs(cx) / 3);
const hue = 200 - intensity * 60;
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 (
<group>
{/* Tabuleiro (deck) */}
<mesh position={[0, deckY, 0]} castShadow receiveShadow>
<boxGeometry args={[lp, deckThickness, width]} />
<meshStandardMaterial color={deckColor} roughness={0.5} />
</mesh>
{/* Guarda-rodas/barreira lateral */}
<mesh position={[0, deckY + deckThickness / 2 + 0.3, halfW - 0.15]} castShadow>
<boxGeometry args={[lp, 0.5, 0.1]} />
<meshStandardMaterial color={barrierColor} roughness={0.7} />
</mesh>
<mesh position={[0, deckY + deckThickness / 2 + 0.3, -halfW + 0.15]} castShadow>
<boxGeometry args={[lp, 0.5, 0.1]} />
<meshStandardMaterial color={barrierColor} roughness={0.7} />
</mesh>
{/* Pilares (3 ao longo do comprimento) */}
{pillarHeights.map((h, i) => {
const x = i === 0 ? -halfL + halfL * 0.3 : i === 1 ? 0 : halfL - halfL * 0.3;
return (
<mesh key={`pillar-${i}`} position={[x, h / 2, 0]} castShadow receiveShadow>
<boxGeometry args={[1.5, h, 1.5]} />
<meshStandardMaterial color={pillarColor} roughness={0.7} />
</mesh>
);
})}
{/* Solo / água */}
<mesh position={[0, -0.5, 0]} rotation={[-Math.PI / 2, 0, 0]} receiveShadow>
<planeGeometry args={[lp * 1.6, width * 3]} />
<meshStandardMaterial color="#60a5fa" opacity={0.4} transparent roughness={0.3} />
</mesh>
{/* Seta de Vento (bate no meio do tabuleiro) */}
<WindArrow
target={[0, deckY + deckThickness / 2, 0]}
direction={[0, Math.sin((alpha * Math.PI) / 180), Math.cos((alpha * Math.PI) / 180)]}
scale={4}
/>
<Text
position={[0, deckY + deckThickness + 1.5, 0]}
fontSize={1.2}
color={isDark ? "#93c5fd" : "#1e40af"}
anchorX="center"
anchorY="bottom"
>
Lp={lp}m | B={width}m | Cx={cx.toFixed(2)}
</Text>
</group>
);
}
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);
const fallback = (
<FallbackDiagram
type="bridge"
props={input}
/>
);
return (
<SceneCanvas moduleId="ponte"
shadows
gl={{ preserveDrawingBuffer: true, antialias: true }}
camera={{ position: [dist * 0.8, deckHeight + width, dist], fov: 45 }}
fallback={fallback}
>
<ambientLight intensity={0.6} />
<directionalLight position={[lp, deckHeight * 3, width * 3]} intensity={1.2} castShadow shadow-mapSize-width={1024} shadow-mapSize-height={1024} />
<BridgeModel {...input} />
<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} />
<Environment preset="city" />
</SceneCanvas>
);
}