148 lines
4.9 KiB
TypeScript
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>
|
|
);
|
|
} |