feat: unified 0 and 90 degree PDF envelope and category descriptors
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
import { useMemo } from 'react';
|
||||
import { OrbitControls, Grid, Environment, Text } from '@react-three/drei';
|
||||
import * as THREE from 'three';
|
||||
import SceneCanvas from '../SceneCanvas';
|
||||
import FallbackDiagram from '../FallbackDiagram';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converte kN para um comprimento visual proporcional no eixo 3D.
|
||||
*/
|
||||
const forceToLength = (kN: number): number => Math.min(Math.max(Math.abs(kN) * 0.3, 0.3), 4);
|
||||
|
||||
function BarModel({
|
||||
barType,
|
||||
section,
|
||||
diameter,
|
||||
width,
|
||||
length,
|
||||
alpha,
|
||||
fxKN,
|
||||
fyKN,
|
||||
cx,
|
||||
}: Bar3DInput) {
|
||||
const barRadius = barType === 'circular' ? (diameter ?? 0.05) / 2 : Math.min(width ?? 0.1, 0.08) / 2;
|
||||
const barThickness = barType === 'circular' ? barRadius : barRadius * 0.5;
|
||||
|
||||
// Cor baseada em Cx
|
||||
const barColor = useMemo(() => {
|
||||
const intensity = Math.min(1, Math.abs(cx) / 2.5);
|
||||
const hue = 215 - intensity * 215;
|
||||
return new THREE.Color(`hsl(${hue}, ${65 + intensity * 25}%, ${45 - intensity * 10}%)`);
|
||||
}, [cx]);
|
||||
|
||||
// Rotação da barra em torno do eixo Y (alinhada com eixo X inicialmente)
|
||||
// Direção do vento é +X; α é o ângulo da face da barra em relação ao vento
|
||||
const alphaRad = (alpha * Math.PI) / 180;
|
||||
const barRotation = -alphaRad; // rotação em torno do eixo Y para alinhar a face
|
||||
|
||||
// Direção do vetor de força resultante (na direção da força calculada)
|
||||
const forceMag = Math.sqrt(fxKN * fxKN + fyKN * fyKN);
|
||||
const forceAngle = Math.atan2(fyKN, fxKN);
|
||||
const arrowLen = forceToLength(forceMag);
|
||||
|
||||
// Centro da barra (origem)
|
||||
const center = new THREE.Vector3(0, 0, 0);
|
||||
|
||||
// Posição da ponta da seta
|
||||
const arrowEnd = useMemo(
|
||||
() => new THREE.Vector3(
|
||||
Math.cos(forceAngle) * arrowLen,
|
||||
Math.sin(forceAngle) * arrowLen,
|
||||
0,
|
||||
),
|
||||
[forceAngle, arrowLen],
|
||||
);
|
||||
const arrowMid = useMemo(
|
||||
() => new THREE.Vector3(arrowEnd.x / 2, arrowEnd.y / 2, 0),
|
||||
[arrowEnd],
|
||||
);
|
||||
const quat = useMemo(() => {
|
||||
const dir = arrowEnd.clone().normalize();
|
||||
const q = new THREE.Quaternion();
|
||||
q.setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir);
|
||||
return new THREE.Euler().setFromQuaternion(q);
|
||||
}, [arrowEnd]);
|
||||
|
||||
const headLen = 0.25;
|
||||
|
||||
return (
|
||||
<group rotation={[0, barRotation, 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 */}
|
||||
<axesHelper args={[length * 0.5]} />
|
||||
|
||||
{/* Vetor de força (resultante) */}
|
||||
{forceMag > 0.01 && (
|
||||
<group>
|
||||
{arrowLen - headLen > 0.01 && (
|
||||
<mesh position={arrowMid.toArray()} rotation={[quat.x, quat.y, quat.z]} castShadow>
|
||||
<cylinderGeometry args={[0.04, 0.04, arrowLen - headLen, 10]} />
|
||||
<meshStandardMaterial color="#ef4444" />
|
||||
</mesh>
|
||||
)}
|
||||
<mesh position={arrowEnd.toArray()} rotation={[quat.x, quat.y, quat.z]} castShadow>
|
||||
<coneGeometry args={[0.1, headLen, 10]} />
|
||||
<meshStandardMaterial color="#ef4444" />
|
||||
</mesh>
|
||||
</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>
|
||||
<axesHelper args={[length * 0.3]} />
|
||||
<Text
|
||||
position={[0, -barRadius * 2 - 0.3, 0]}
|
||||
fontSize={0.3}
|
||||
color="#1e40af"
|
||||
anchorX="center"
|
||||
anchorY="top"
|
||||
>
|
||||
α={alpha}° | Cx={cx.toFixed(2)}
|
||||
</Text>
|
||||
{center && null}
|
||||
</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 { 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
|
||||
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="#94a3b8" cellColor="#cbd5e1" position={[0, -size * 0.3, 0]} />
|
||||
<OrbitControls makeDefault minPolarAngle={0} maxPolarAngle={Math.PI - 0.05} />
|
||||
<Environment preset="city" />
|
||||
</SceneCanvas>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user