222 lines
6.4 KiB
TypeScript
222 lines
6.4 KiB
TypeScript
import { useRef } from 'react';
|
|
import { useFrame } from '@react-three/fiber';
|
|
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 { 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 moduleId="dinamica"
|
|
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>
|
|
);
|
|
}
|