feat: unified 0 and 90 degree PDF envelope and category descriptors

This commit is contained in:
2026-07-08 19:52:34 +00:00
commit 9fece3f174
170 changed files with 27177 additions and 0 deletions
+240
View File
@@ -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>
);
}