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

214 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 { useCanvasTheme } from '@/lib/theme';
export interface Bar3DInput {
/** Tipo de seção */
barType: 'flat' | 'circular';
/** Forma (apenas flat): 'placa' | 'l' | 't' | 'i' | 'rectangle' */
section?: 'placa' | 'l' | 't' | 'i' | 'rectangle';
/** Diâmetro (apenas circular, m) */
diameter?: number;
/** Largura da seção (flat, m) */
width?: number;
/** Comprimento da barra (m) */
length: number;
/** Ângulo de incidência (graus) — 0° = face plana contra o vento */
alpha: number;
/** Força Fx (kN) */
fxKN: number;
/** Força Fy (kN) */
fyKN: number;
/** Coeficiente Cx (apenas visualização) */
cx: number;
}
function BarModel({
barType,
section,
diameter,
width,
length,
alpha,
cx,
}: Bar3DInput) {
const theme = useCanvasTheme();
const isDark = theme === 'dark';
const barRadius = barType === 'circular' ? (diameter ?? 0.05) / 2 : Math.min(width ?? 0.1, 0.08) / 2;
const barThickness = barType === 'circular' ? barRadius : barRadius * 0.5;
const barColor = useMemo(() => {
const intensity = Math.min(1, Math.abs(cx) / 2.5);
const hue = 215 - intensity * 215;
const l = isDark ? (55 - intensity * 10) : (45 - intensity * 10);
return new THREE.Color(`hsl(${hue}, ${65 + intensity * 25}%, ${l}%)`);
}, [cx, isDark]);
// Ação do vento deve rotacionar a barra em seu próprio eixo longitudinal (roll).
// O eixo longitudinal na nossa geometria é o X.
const alphaRad = (alpha * Math.PI) / 180;
// A rotação deve ser negativa para que o vento (vindo de -Z) atinja as faces corretas conforme a NBR.
const barRotationX = -alphaRad;
return (
<group>
{/* Seta do Vento (Fixa Globalmente, soprando em +Z) */}
<group position={[0, 0, barRadius + 0.5]}>
<mesh position={[0, 0, 0.4]} rotation={[Math.PI / 2, 0, 0]}>
<cylinderGeometry args={[0.02, 0.02, 0.8, 8]} />
<meshStandardMaterial color="#22c55e" />
</mesh>
<mesh position={[0, 0, 0]} rotation={[-Math.PI / 2, 0, 0]}>
<coneGeometry args={[0.08, 0.2, 8]} />
<meshStandardMaterial color="#22c55e" />
</mesh>
</group>
{/* Barra Rotacionada (Roll) */}
<group rotation={[barRotationX, 0, 0]}>
{/* Eixo principal da barra ao longo do eixo X */}
{barType === 'circular' ? (
<mesh position={[0, 0, 0]} rotation={[0, 0, Math.PI / 2]} castShadow>
<cylinderGeometry args={[barRadius, barRadius, length, 16]} />
<meshStandardMaterial color={barColor} roughness={0.4} metalness={0.3} />
</mesh>
) : (
<SectionShape section={section ?? 'placa'} width={width ?? 0.1} length={length} color={barColor} thickness={barThickness} />
)}
{/* Eixos de referência locais da barra */}
<axesHelper args={[length * 0.5]} />
</group>
{/* Marca de origem */}
<mesh position={[0, 0, 0]} castShadow>
<sphereGeometry args={[0.06, 12, 12]} />
<meshStandardMaterial color="#fbbf24" emissive="#fbbf24" emissiveIntensity={0.4} />
</mesh>
<Text
position={[0, -barRadius * 2 - 0.4, 0]}
fontSize={0.3}
color={isDark ? "#93c5fd" : "#1e40af"}
anchorX="center"
anchorY="top"
>
α={alpha}° | Cx={cx.toFixed(2)}
</Text>
</group>
);
}
function SectionShape({
section,
width,
length,
color,
thickness,
}: {
section: 'placa' | 'l' | 't' | 'i' | 'rectangle';
width: number;
length: number;
color: THREE.Color;
thickness: number;
}) {
switch (section) {
case 'placa':
return (
<mesh position={[0, 0, 0]} castShadow>
<boxGeometry args={[length, width, thickness]} />
<meshStandardMaterial color={color} roughness={0.5} />
</mesh>
);
case 'l':
return (
<group>
<mesh position={[0, width / 2 - thickness / 2, width / 2 - thickness / 2]} castShadow>
<boxGeometry args={[length, thickness, width]} />
<meshStandardMaterial color={color} roughness={0.5} />
</mesh>
<mesh position={[0, 0, 0]} castShadow>
<boxGeometry args={[length, width, thickness]} />
<meshStandardMaterial color={color} roughness={0.5} />
</mesh>
</group>
);
case 't':
return (
<group>
<mesh position={[0, width / 2 - thickness / 2, 0]} castShadow>
<boxGeometry args={[length, thickness, width]} />
<meshStandardMaterial color={color} roughness={0.5} />
</mesh>
<mesh position={[0, 0, 0]} castShadow>
<boxGeometry args={[length, width, thickness]} />
<meshStandardMaterial color={color} roughness={0.5} />
</mesh>
</group>
);
case 'i':
return (
<group>
<mesh position={[0, width / 2 - thickness / 2, 0]} castShadow>
<boxGeometry args={[length, thickness, width]} />
<meshStandardMaterial color={color} roughness={0.5} />
</mesh>
<mesh position={[0, 0, 0]} castShadow>
<boxGeometry args={[length, width - thickness, thickness]} />
<meshStandardMaterial color={color} roughness={0.5} />
</mesh>
<mesh position={[0, -width / 2 + thickness / 2, 0]} castShadow>
<boxGeometry args={[length, thickness, width]} />
<meshStandardMaterial color={color} roughness={0.5} />
</mesh>
</group>
);
case 'rectangle':
return (
<mesh position={[0, 0, 0]} castShadow>
<boxGeometry args={[length, width, thickness]} />
<meshStandardMaterial color={color} roughness={0.5} />
</mesh>
);
}
}
export default function Bar3DViewer(input: Bar3DInput) {
const theme = useCanvasTheme();
const isDark = theme === 'dark';
const { length, width, diameter } = input;
const size = Math.max(length * 0.6, (width ?? diameter ?? 0.1) * 8);
const fallback = (
<FallbackDiagram
type="bar"
props={input}
/>
);
return (
<SceneCanvas moduleId="barras"
shadows
gl={{ preserveDrawingBuffer: true, antialias: true }}
camera={{ position: [size, size * 0.6, size], fov: 45 }}
fallback={fallback}
>
<ambientLight intensity={0.6} />
<directionalLight position={[size, size, size]} intensity={1.2} castShadow shadow-mapSize-width={1024} shadow-mapSize-height={1024} />
<BarModel {...input} />
<Grid
infiniteGrid
fadeDistance={size * 2}
sectionColor={isDark ? "#475569" : "#94a3b8"}
cellColor={isDark ? "#1e293b" : "#cbd5e1"}
position={[0, -size * 0.3, 0]}
/>
<OrbitControls makeDefault minPolarAngle={0} maxPolarAngle={Math.PI - 0.05} />
<Environment preset="city" />
</SceneCanvas>
);
}