feat: unified 0 and 90 degree PDF envelope and category descriptors
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
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';
|
||||
|
||||
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) {
|
||||
return (
|
||||
<group>
|
||||
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, -0.01, 0]} receiveShadow>
|
||||
<planeGeometry args={[20, 20]} />
|
||||
<meshStandardMaterial color="#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="#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 cameraDistance = Math.max(props.height * SCALE * 2, 6);
|
||||
|
||||
const fallback = (
|
||||
<FallbackDiagram
|
||||
type="dynamics"
|
||||
props={props}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<SceneCanvas
|
||||
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="#94a3b8" cellColor="#cbd5e1" position={[0, -0.01, 0]} />
|
||||
<OrbitControls makeDefault minPolarAngle={0} maxPolarAngle={Math.PI / 2 - 0.05} />
|
||||
<Environment preset="city" />
|
||||
</SceneCanvas>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user