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

221 lines
6.3 KiB
TypeScript

import { useRef } from 'react';
import { useFrame } from '@react-three/fiber';
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) */
height: number;
/** Frequência natural f₁ (Hz) */
freq: number;
/** Velocidade do vento (m/s) */
windSpeed: number;
/** Número de Scruton */
scruton: number;
/** Tipo de seção */
sectionShape: string;
/** Tamanho da seção (m) */
sectionSize: number;
/** Mostrar rua de vórtices */
showVortexStreet: boolean;
/** Mostrar modo de oscilação */
showModeShape: boolean;
}
const SCALE = 0.15;
function OscillatingBuilding({
height,
freq,
scruton,
sectionShape,
sectionSize,
showModeShape,
}: {
height: number;
freq: number;
scruton: number;
sectionShape: string;
sectionSize: number;
showModeShape: boolean;
}) {
const groupRef = useRef<THREE.Group>(null);
const timeRef = useRef(0);
const hScaled = height * SCALE;
const wScaled = sectionSize * SCALE;
useFrame((_, delta) => {
timeRef.current += delta;
if (groupRef.current && showModeShape) {
const amplitude = Math.min(0.3, 0.1 / Math.max(scruton, 0.1));
const displacement = amplitude * Math.sin(2 * Math.PI * freq * timeRef.current);
groupRef.current.position.x = displacement;
groupRef.current.rotation.z = displacement * 0.02;
}
});
const sectionColor = '#3b82f6';
return (
<group ref={groupRef}>
{sectionShape === 'circle' ? (
<mesh position={[0, hScaled / 2, 0]} castShadow>
<cylinderGeometry args={[wScaled / 2, wScaled / 2, hScaled, 16]} />
<meshStandardMaterial color={sectionColor} transparent opacity={0.7} />
</mesh>
) : (
<mesh position={[0, hScaled / 2, 0]} castShadow>
<boxGeometry args={[wScaled, hScaled, wScaled]} />
<meshStandardMaterial color={sectionColor} transparent opacity={0.7} />
</mesh>
)}
{showModeShape && (
<group>
{[0, 0.25, 0.5, 0.75, 1].map((frac, i, arr) => {
if (i === arr.length - 1) return null;
const y0 = frac * hScaled;
const y1 = arr[i + 1] * hScaled;
const amp = 0.03;
return (
<mesh key={`mode-${i}`} position={[amp * Math.sin(frac * Math.PI), (y0 + y1) / 2, 0]}>
<cylinderGeometry args={[0.01, 0.01, y1 - y0, 4]} />
<meshStandardMaterial color="#ef4444" />
</mesh>
);
})}
</group>
)}
<mesh position={[-wScaled - 0.3, hScaled / 2, 0]}>
<boxGeometry args={[0.02, hScaled, 0.02]} />
<meshStandardMaterial color="#94a3b8" />
</mesh>
</group>
);
}
function VortexStreet({
windSpeed,
height,
sectionSize,
}: {
windSpeed: number;
height: number;
sectionSize: number;
}) {
const hScaled = height * SCALE;
const wScaled = sectionSize * SCALE;
return (
<group>
{Array.from({ length: 12 }).map((_, i) => {
const x = wScaled / 2 + 0.5 + i * 0.5;
const sign = i % 2 === 0 ? 1 : -1;
const y = hScaled / 2 + sign * wScaled * 0.4 * (1 + i * 0.05);
const opacity = Math.max(0.1, 0.8 - i * 0.06);
return (
<mesh key={`vortex-${i}`} position={[x, y, 0]}>
<sphereGeometry args={[0.06, 8, 8]} />
<meshStandardMaterial color="#a855f7" transparent opacity={opacity} />
</mesh>
);
})}
<mesh position={[windSpeed * SCALE * 0.5 + 1.5, hScaled / 2, 0]} rotation={[0, 0, -Math.PI / 2]}>
<cylinderGeometry args={[0.03, 0.03, 2, 8]} />
<meshStandardMaterial color="#22c55e" />
</mesh>
<mesh position={[windSpeed * SCALE * 0.5 + 2.5, hScaled / 2, 0]} rotation={[0, 0, -Math.PI / 2]}>
<coneGeometry args={[0.08, 0.2, 8]} />
<meshStandardMaterial color="#22c55e" />
</mesh>
</group>
);
}
function DynamicsModel(props: Dynamics3DInput) {
const theme = useCanvasTheme();
const isDark = theme === 'dark';
return (
<group>
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, -0.01, 0]} receiveShadow>
<planeGeometry args={[20, 20]} />
<meshStandardMaterial color={isDark ? "#1e293b" : "#94a3b8"} transparent opacity={0.15} />
</mesh>
<OscillatingBuilding
height={props.height}
freq={props.freq}
scruton={props.scruton}
sectionShape={props.sectionShape}
sectionSize={props.sectionSize}
showModeShape={props.showModeShape}
/>
{props.showVortexStreet && (
<VortexStreet
windSpeed={props.windSpeed}
height={props.height}
sectionSize={props.sectionSize}
/>
)}
<Text
position={[0, props.height * SCALE + 0.8, 0]}
fontSize={0.5}
color={isDark ? "#93c5fd" : "#1e40af"}
anchorX="center"
anchorY="bottom"
>
h={props.height}m | f={props.freq}Hz | Sc={props.scruton.toFixed(1)}
</Text>
</group>
);
}
export default function Dynamics3DViewer(props: Dynamics3DInput) {
const theme = useCanvasTheme();
const isDark = theme === 'dark';
const cameraDistance = Math.max(props.height * SCALE * 2, 6);
const fallback = (
<FallbackDiagram
type="dynamics"
props={props}
/>
);
return (
<SceneCanvas
frameloop="always"
shadows
gl={{ preserveDrawingBuffer: true, antialias: true }}
camera={{ position: [cameraDistance, cameraDistance * 0.5, cameraDistance], fov: 45 }}
fallback={fallback}
>
<ambientLight intensity={0.6} />
<directionalLight
position={[10, 15, 10]}
intensity={1.2}
castShadow
shadow-mapSize-width={1024}
shadow-mapSize-height={1024}
/>
<DynamicsModel {...props} />
<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} />
<Environment preset="city" />
</SceneCanvas>
);
}