214 lines
7.1 KiB
TypeScript
214 lines
7.1 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 { 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>
|
||
);
|
||
} |