feat: adicionar super manual didatico com fluxos 2d e sheet panel
This commit is contained in:
@@ -25,11 +25,6 @@ export interface Bar3DInput {
|
||||
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,
|
||||
@@ -37,94 +32,60 @@ function BarModel({
|
||||
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
|
||||
// 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;
|
||||
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;
|
||||
// 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 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} />
|
||||
<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>
|
||||
) : (
|
||||
<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 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>
|
||||
</group>
|
||||
)}
|
||||
) : (
|
||||
<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>
|
||||
<axesHelper args={[length * 0.3]} />
|
||||
<Text
|
||||
position={[0, -barRadius * 2 - 0.3, 0]}
|
||||
position={[0, -barRadius * 2 - 0.4, 0]}
|
||||
fontSize={0.3}
|
||||
color="#1e40af"
|
||||
anchorX="center"
|
||||
@@ -132,7 +93,6 @@ function BarModel({
|
||||
>
|
||||
α={alpha}° | Cx={cx.toFixed(2)}
|
||||
</Text>
|
||||
{center && null}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { OrbitControls, Grid, Environment, Text } from '@react-three/drei';
|
||||
import * as THREE from 'three';
|
||||
import SceneCanvas from '../SceneCanvas';
|
||||
import FallbackDiagram from '../FallbackDiagram';
|
||||
import { WindArrow } from './WindArrow';
|
||||
|
||||
export interface Bridge3DInput {
|
||||
/** Maior vão Lₚ (m) */
|
||||
@@ -21,6 +22,8 @@ export interface Bridge3DInput {
|
||||
fxPerLength: number;
|
||||
/** Força de sustentação por unidade de comprimento Fz (kN/m) */
|
||||
fzPerLength: number;
|
||||
/** Ângulo de ataque do vento (graus) */
|
||||
alpha: number;
|
||||
}
|
||||
|
||||
function BridgeModel({
|
||||
@@ -29,8 +32,7 @@ function BridgeModel({
|
||||
deckHeight,
|
||||
heg,
|
||||
cx,
|
||||
fxPerLength,
|
||||
fzPerLength,
|
||||
alpha,
|
||||
}: Bridge3DInput) {
|
||||
const halfL = lp / 2;
|
||||
const halfW = width / 2;
|
||||
@@ -47,13 +49,6 @@ function BridgeModel({
|
||||
// Pilar heights: posicionar 3 pilares ao longo do vão
|
||||
const pillarHeights = useMemo(() => [deckY - 0.5, deckY - 0.5, deckY - 0.5], [deckY]);
|
||||
|
||||
// Vetor de força (Fx horizontal)
|
||||
const fxLen = Math.min(Math.max(Math.abs(fxPerLength) * 0.5, 0.3), 4);
|
||||
const fxDir = fxPerLength >= 0 ? 1 : -1;
|
||||
// Vetor de força (Fz vertical)
|
||||
const fzLen = Math.min(Math.max(Math.abs(fzPerLength) * 0.5, 0.3), 4);
|
||||
const fzDir = fzPerLength >= 0 ? 1 : -1;
|
||||
|
||||
return (
|
||||
<group>
|
||||
{/* Tabuleiro (deck) */}
|
||||
@@ -89,32 +84,15 @@ function BridgeModel({
|
||||
<meshStandardMaterial color="#60a5fa" opacity={0.4} transparent roughness={0.3} />
|
||||
</mesh>
|
||||
|
||||
{/* Vetor Cx (horizontal) */}
|
||||
<ForceArrow
|
||||
start={[-halfL * 0.6, deckY + deckThickness + 0.3, halfW + 0.5]}
|
||||
direction={[fxDir, 0, 0]}
|
||||
length={fxLen}
|
||||
color="#ef4444"
|
||||
/>
|
||||
|
||||
{/* Vetor Cz (vertical) */}
|
||||
<ForceArrow
|
||||
start={[halfL * 0.6, deckY + deckThickness + 0.3, halfW + 0.5]}
|
||||
direction={[0, fzDir, 0]}
|
||||
length={fzLen}
|
||||
color="#3b82f6"
|
||||
/>
|
||||
|
||||
{/* Vetor no centro também para destacar */}
|
||||
<ForceArrow
|
||||
start={[0, deckY + deckThickness + 0.3, 0]}
|
||||
direction={[fxDir, 0, 0]}
|
||||
length={fxLen * 0.7}
|
||||
color="#ef4444"
|
||||
{/* Seta de Vento (bate no meio do tabuleiro) */}
|
||||
<WindArrow
|
||||
target={[0, deckY + deckThickness / 2, 0]}
|
||||
direction={[0, Math.sin((alpha * Math.PI) / 180), Math.cos((alpha * Math.PI) / 180)]}
|
||||
scale={4}
|
||||
/>
|
||||
<Text
|
||||
position={[0, deckY + deckThickness + 1.0, 0]}
|
||||
fontSize={0.6}
|
||||
position={[0, deckY + deckThickness + 1.5, 0]}
|
||||
fontSize={1.2}
|
||||
color="#1e40af"
|
||||
anchorX="center"
|
||||
anchorY="bottom"
|
||||
@@ -125,54 +103,7 @@ function BridgeModel({
|
||||
);
|
||||
}
|
||||
|
||||
function ForceArrow({
|
||||
start,
|
||||
direction,
|
||||
length,
|
||||
color,
|
||||
}: {
|
||||
start: [number, number, number];
|
||||
direction: [number, number, number];
|
||||
length: number;
|
||||
color: string;
|
||||
}) {
|
||||
const startVec = useMemo(() => new THREE.Vector3(...start), [start]);
|
||||
const dirVec = useMemo(() => new THREE.Vector3(...direction), [direction]);
|
||||
const end = useMemo(
|
||||
() => new THREE.Vector3(
|
||||
startVec.x + dirVec.x * length,
|
||||
startVec.y + dirVec.y * length,
|
||||
startVec.z + dirVec.z * length,
|
||||
),
|
||||
[startVec, dirVec, length],
|
||||
);
|
||||
const mid = useMemo(
|
||||
() => new THREE.Vector3().addVectors(startVec, end).multiplyScalar(0.5),
|
||||
[startVec, end],
|
||||
);
|
||||
const quat = useMemo(() => {
|
||||
const dir = new THREE.Vector3().subVectors(end, startVec).normalize();
|
||||
const q = new THREE.Quaternion();
|
||||
q.setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir);
|
||||
return new THREE.Euler().setFromQuaternion(q);
|
||||
}, [startVec, end]);
|
||||
const headLen = 0.3;
|
||||
|
||||
return (
|
||||
<group>
|
||||
{length - headLen > 0.01 && (
|
||||
<mesh position={mid.toArray()} rotation={[quat.x, quat.y, quat.z]} castShadow>
|
||||
<cylinderGeometry args={[0.06, 0.06, length - headLen, 10]} />
|
||||
<meshStandardMaterial color={color} />
|
||||
</mesh>
|
||||
)}
|
||||
<mesh position={end.toArray()} rotation={[quat.x, quat.y, quat.z]} castShadow>
|
||||
<coneGeometry args={[0.15, headLen, 10]} />
|
||||
<meshStandardMaterial color={color} />
|
||||
</mesh>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Bridge3DViewer(input: Bridge3DInput) {
|
||||
const { lp, deckHeight, width } = input;
|
||||
|
||||
@@ -186,6 +186,7 @@ export default function Dynamics3DViewer(props: Dynamics3DInput) {
|
||||
|
||||
return (
|
||||
<SceneCanvas
|
||||
frameloop="always"
|
||||
shadows
|
||||
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
||||
camera={{ position: [cameraDistance, cameraDistance * 0.5, cameraDistance], fov: 45 }}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { OrbitControls, Grid, Environment, Text } from '@react-three/drei';
|
||||
import * as THREE from 'three';
|
||||
import SceneCanvas from '../SceneCanvas';
|
||||
import FallbackDiagram from '../FallbackDiagram';
|
||||
import { WindArrow } from './WindArrow';
|
||||
|
||||
export interface IsolatedRoof3DInput {
|
||||
/** Tipo de cobertura: 'shed' (uma água) ou 'gable' (duas águas) */
|
||||
@@ -11,7 +12,9 @@ export interface IsolatedRoof3DInput {
|
||||
theta: number;
|
||||
/** Altura livre dos suportes (m) */
|
||||
height: number;
|
||||
/** Profundidade da cobertura (m) — dimensão perpendicular à seção */
|
||||
/** Largura da cobertura (m) — dimensão transversal (b) */
|
||||
width: number;
|
||||
/** Profundidade da cobertura (m) — dimensão longitudinal (l) */
|
||||
depth: number;
|
||||
/** Cpe barlavento (sobre a face exposta ao vento) */
|
||||
cpeWindward: number;
|
||||
@@ -23,8 +26,6 @@ export interface IsolatedRoof3DInput {
|
||||
forceKN: number;
|
||||
}
|
||||
|
||||
const forceToLength = (kN: number): number => Math.min(Math.max(Math.abs(kN) * 0.15, 0.5), 6);
|
||||
|
||||
function pressureColor(cpe: number): THREE.Color {
|
||||
const clamped = Math.max(-2.5, Math.min(1.5, cpe));
|
||||
const t = (clamped + 2.5) / 4.0;
|
||||
@@ -36,6 +37,7 @@ function IsolatedRoofModel({
|
||||
type,
|
||||
theta,
|
||||
height,
|
||||
width,
|
||||
depth,
|
||||
cpeWindward,
|
||||
cpeLeeward,
|
||||
@@ -43,12 +45,11 @@ function IsolatedRoofModel({
|
||||
}: IsolatedRoof3DInput) {
|
||||
const thetaRad = (theta * Math.PI) / 180;
|
||||
const halfDepth = depth / 2;
|
||||
const halfWidth = width / 2;
|
||||
|
||||
const windwardColor = useMemo(() => pressureColor(cpeWindward), [cpeWindward]);
|
||||
const leewardColor = useMemo(() => pressureColor(cpeLeeward), [cpeLeeward]);
|
||||
|
||||
const arrowLen = forceToLength(forceKN);
|
||||
|
||||
const h_diff = depth * Math.tan(thetaRad);
|
||||
const h_half = (depth / 2) * Math.tan(thetaRad);
|
||||
|
||||
@@ -60,29 +61,29 @@ function IsolatedRoofModel({
|
||||
const list: { pos: [number, number, number]; h: number }[] = [];
|
||||
if (type === 'shed') {
|
||||
list.push(
|
||||
{ pos: [-halfDepth, height / 2, -halfDepth], h: height },
|
||||
{ pos: [halfDepth, height / 2, -halfDepth], h: height },
|
||||
{ pos: [-halfDepth, (height + h_diff) / 2, halfDepth], h: height + h_diff },
|
||||
{ pos: [halfDepth, (height + h_diff) / 2, halfDepth], h: height + h_diff },
|
||||
{ pos: [-halfWidth, height / 2, -halfDepth], h: height },
|
||||
{ pos: [halfWidth, height / 2, -halfDepth], h: height },
|
||||
{ pos: [-halfWidth, (height + h_diff) / 2, halfDepth], h: height + h_diff },
|
||||
{ pos: [halfWidth, (height + h_diff) / 2, halfDepth], h: height + h_diff },
|
||||
);
|
||||
} else {
|
||||
list.push(
|
||||
{ pos: [-halfDepth, height / 2, -halfDepth], h: height },
|
||||
{ pos: [halfDepth, height / 2, -halfDepth], h: height },
|
||||
{ pos: [-halfDepth, height / 2, halfDepth], h: height },
|
||||
{ pos: [halfDepth, height / 2, halfDepth], h: height },
|
||||
{ pos: [-halfDepth, (height + h_half) / 2, 0], h: height + h_half },
|
||||
{ pos: [halfDepth, (height + h_half) / 2, 0], h: height + h_half },
|
||||
{ pos: [-halfWidth, height / 2, -halfDepth], h: height },
|
||||
{ pos: [halfWidth, height / 2, -halfDepth], h: height },
|
||||
{ pos: [-halfWidth, height / 2, halfDepth], h: height },
|
||||
{ pos: [halfWidth, height / 2, halfDepth], h: height },
|
||||
{ pos: [-halfWidth, (height + h_half) / 2, 0], h: height + h_half },
|
||||
{ pos: [halfWidth, (height + h_half) / 2, 0], h: height + h_half },
|
||||
);
|
||||
}
|
||||
return list;
|
||||
}, [type, depth, height, h_diff, h_half, halfDepth]);
|
||||
}, [type, depth, width, height, h_diff, h_half, halfDepth, halfWidth]);
|
||||
|
||||
return (
|
||||
<group>
|
||||
{/* Solo translúcido */}
|
||||
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, -0.01, 0]} receiveShadow>
|
||||
<planeGeometry args={[depth * 2, depth * 2]} />
|
||||
<planeGeometry args={[width * 1.5, depth * 1.5]} />
|
||||
<meshStandardMaterial color="#94a3b8" transparent opacity={0.15} />
|
||||
</mesh>
|
||||
|
||||
@@ -97,7 +98,7 @@ function IsolatedRoofModel({
|
||||
castShadow
|
||||
receiveShadow
|
||||
>
|
||||
<boxGeometry args={[depth, 0.08, depth / (2 * Math.cos(thetaRad))]} />
|
||||
<boxGeometry args={[width, 0.08, depth / (2 * Math.cos(thetaRad))]} />
|
||||
<meshStandardMaterial color={windwardColor} opacity={0.9} transparent roughness={0.4} />
|
||||
</mesh>
|
||||
{/* Metade Sotavento (Z > 0) */}
|
||||
@@ -107,7 +108,7 @@ function IsolatedRoofModel({
|
||||
castShadow
|
||||
receiveShadow
|
||||
>
|
||||
<boxGeometry args={[depth, 0.08, depth / (2 * Math.cos(thetaRad))]} />
|
||||
<boxGeometry args={[width, 0.08, depth / (2 * Math.cos(thetaRad))]} />
|
||||
<meshStandardMaterial color={leewardColor} opacity={0.9} transparent roughness={0.4} />
|
||||
</mesh>
|
||||
</group>
|
||||
@@ -121,7 +122,7 @@ function IsolatedRoofModel({
|
||||
castShadow
|
||||
receiveShadow
|
||||
>
|
||||
<boxGeometry args={[depth, 0.08, depth / (2 * Math.cos(thetaRad))]} />
|
||||
<boxGeometry args={[width, 0.08, depth / (2 * Math.cos(thetaRad))]} />
|
||||
<meshStandardMaterial color={windwardColor} opacity={0.9} transparent roughness={0.4} />
|
||||
</mesh>
|
||||
{/* Água Direita / Sotavento (Z > 0) */}
|
||||
@@ -131,7 +132,7 @@ function IsolatedRoofModel({
|
||||
castShadow
|
||||
receiveShadow
|
||||
>
|
||||
<boxGeometry args={[depth, 0.08, depth / (2 * Math.cos(thetaRad))]} />
|
||||
<boxGeometry args={[width, 0.08, depth / (2 * Math.cos(thetaRad))]} />
|
||||
<meshStandardMaterial color={leewardColor} opacity={0.9} transparent roughness={0.4} />
|
||||
</mesh>
|
||||
</group>
|
||||
@@ -145,12 +146,10 @@ function IsolatedRoofModel({
|
||||
</mesh>
|
||||
))}
|
||||
|
||||
{/* Seta de força resultante (sucção para cima) */}
|
||||
<ForceArrow
|
||||
start={new THREE.Vector3(0, centerY, 0)}
|
||||
direction={new THREE.Vector3(0, 1, 0)}
|
||||
length={arrowLen}
|
||||
color="#ef4444"
|
||||
{/* Vetor de vento (Seta verde/azul batendo no elemento) */}
|
||||
<WindArrow
|
||||
target={new THREE.Vector3(0, centerY, 0)}
|
||||
direction={new THREE.Vector3(0, 0, 1)}
|
||||
/>
|
||||
|
||||
{/* === LINHAS DE COTA (CAD-Style) === */}
|
||||
@@ -208,8 +207,7 @@ function IsolatedRoofModel({
|
||||
|
||||
{/* Rótulo Superior */}
|
||||
<Text
|
||||
position={[0, centerY + arrowLen + 0.8, 0]}
|
||||
fontSize={0.4}
|
||||
position={[0, centerY + 3, 0]}
|
||||
color="#1a202c"
|
||||
anchorX="center"
|
||||
anchorY="bottom"
|
||||
@@ -220,75 +218,23 @@ function IsolatedRoofModel({
|
||||
);
|
||||
}
|
||||
|
||||
function ForceArrow({
|
||||
start,
|
||||
direction,
|
||||
length,
|
||||
color,
|
||||
}: {
|
||||
start: THREE.Vector3;
|
||||
direction: THREE.Vector3;
|
||||
length: number;
|
||||
color: string;
|
||||
}) {
|
||||
const end = useMemo(
|
||||
() => new THREE.Vector3(start.x + direction.x * length, start.y + direction.y * length, start.z + direction.z * length),
|
||||
[start, direction, length],
|
||||
);
|
||||
const headLen = 0.3;
|
||||
const headRadius = 0.1;
|
||||
const shaftRadius = 0.04;
|
||||
|
||||
const midPoint = useMemo(
|
||||
() => new THREE.Vector3((start.x + end.x) / 2, (start.y + end.y) / 2, (start.z + end.z) / 2),
|
||||
[start, end],
|
||||
);
|
||||
const shaftLength = length - headLen;
|
||||
|
||||
const rotation = useMemo(() => {
|
||||
const dir = new THREE.Vector3().subVectors(end, start).normalize();
|
||||
const quat = new THREE.Quaternion();
|
||||
quat.setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir);
|
||||
const euler = new THREE.Euler().setFromQuaternion(quat);
|
||||
return [euler.x, euler.y, euler.z] as [number, number, number];
|
||||
}, [start, end]);
|
||||
|
||||
return (
|
||||
<group>
|
||||
{shaftLength > 0 && (
|
||||
<mesh position={midPoint.toArray()} rotation={rotation} castShadow>
|
||||
<cylinderGeometry args={[shaftRadius, shaftRadius, shaftLength, 12]} />
|
||||
<meshStandardMaterial color={color} />
|
||||
</mesh>
|
||||
)}
|
||||
<mesh
|
||||
position={[end.x, end.y, end.z]}
|
||||
rotation={rotation}
|
||||
castShadow
|
||||
>
|
||||
<coneGeometry args={[headRadius, headLen, 12]} />
|
||||
<meshStandardMaterial color={color} />
|
||||
</mesh>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
export default function IsolatedRoof3DViewer({
|
||||
type,
|
||||
theta,
|
||||
height,
|
||||
width,
|
||||
depth,
|
||||
cpeWindward,
|
||||
cpeLeeward,
|
||||
cpeTop,
|
||||
forceKN,
|
||||
}: IsolatedRoof3DInput) {
|
||||
const cameraDistance = Math.max(depth * 1.3, height * 1.5, 8);
|
||||
const cameraDistance = Math.max(Math.max(width, depth) * 1.3, height * 1.5, 8);
|
||||
|
||||
const fallback = (
|
||||
<FallbackDiagram
|
||||
type="isolatedRoof"
|
||||
props={{ type, theta, height, depth, cpeWindward, cpeLeeward, cpeTop, forceKN }}
|
||||
props={{ type, theta, height, width, depth, cpeWindward, cpeLeeward, cpeTop, forceKN }}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -311,6 +257,7 @@ export default function IsolatedRoof3DViewer({
|
||||
type={type}
|
||||
theta={theta}
|
||||
height={height}
|
||||
width={width}
|
||||
depth={depth}
|
||||
cpeWindward={cpeWindward}
|
||||
cpeLeeward={cpeLeeward}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { OrbitControls, Grid, Environment, Text } from '@react-three/drei';
|
||||
import * as THREE from 'three';
|
||||
import SceneCanvas from '../SceneCanvas';
|
||||
import FallbackDiagram from '../FallbackDiagram';
|
||||
import { WindArrow } from './WindArrow';
|
||||
|
||||
export interface Sign3DInput {
|
||||
/** Comprimento ℓ (m) */
|
||||
@@ -22,10 +23,8 @@ export interface Sign3DInput {
|
||||
}
|
||||
|
||||
/**
|
||||
* Converte kN para um comprimento visual proporcional no eixo 3D.
|
||||
* 1 kN = 0.25 m de seta (escala calibrada para visualização).
|
||||
* Módulo 3D para visualização de Muros e Placas Isoladas.
|
||||
*/
|
||||
const forceToLength = (kN: number): number => Math.min(Math.max(kN * 0.25, 0.5), 8);
|
||||
|
||||
function SignModel({
|
||||
length,
|
||||
@@ -33,13 +32,11 @@ function SignModel({
|
||||
groundClearance,
|
||||
alpha,
|
||||
cf,
|
||||
forceKN,
|
||||
applicationPoint,
|
||||
}: Sign3DInput) {
|
||||
const baseY = groundClearance;
|
||||
const topY = baseY + height;
|
||||
const halfL = length / 2;
|
||||
const arrowLen = forceToLength(forceKN);
|
||||
|
||||
// Direção da seta no plano XZ (α é o ângulo de incidência do vento relativo à superfície)
|
||||
// O ângulo em relação à normal da placa (eixo X) é 90 - α
|
||||
@@ -114,12 +111,10 @@ function SignModel({
|
||||
Resultante (e = {applicationPoint.toFixed(2)}m)
|
||||
</Text>
|
||||
|
||||
{/* Vetor de força resultante */}
|
||||
<ForceArrow
|
||||
start={arrowStart}
|
||||
{/* Vetor de vento (Seta verde/azul batendo no elemento) */}
|
||||
<WindArrow
|
||||
target={arrowStart}
|
||||
direction={arrowDir}
|
||||
length={arrowLen}
|
||||
color={forceKN >= 0 ? '#ef4444' : '#3b82f6'}
|
||||
/>
|
||||
|
||||
{/* === LINHAS DE COTA (CAD-Style Dimensions) === */}
|
||||
@@ -154,7 +149,7 @@ function SignModel({
|
||||
</group>
|
||||
|
||||
{/* Cota de Comprimento (l) */}
|
||||
<group position={[0.4, baseY + height / 2, 0]}>
|
||||
<group position={[-0.4, baseY + height / 2, 0]}>
|
||||
{/* Linha horizontal longitudinal */}
|
||||
<mesh position={[0, 0, 0]}>
|
||||
<boxGeometry args={[0.015, 0.015, length]} />
|
||||
@@ -172,8 +167,8 @@ function SignModel({
|
||||
</mesh>
|
||||
{/* Texto do comprimento */}
|
||||
<Text
|
||||
position={[0.15, 0, 0]}
|
||||
rotation={[0, Math.PI / 2, 0]}
|
||||
position={[-0.15, 0, 0]}
|
||||
rotation={[0, -Math.PI / 2, 0]}
|
||||
fontSize={0.25}
|
||||
color="#475569"
|
||||
anchorX="center"
|
||||
@@ -197,61 +192,7 @@ function SignModel({
|
||||
);
|
||||
}
|
||||
|
||||
function ForceArrow({
|
||||
start,
|
||||
direction,
|
||||
length,
|
||||
color,
|
||||
}: {
|
||||
start: THREE.Vector3;
|
||||
direction: THREE.Vector3;
|
||||
length: number;
|
||||
color: string;
|
||||
}) {
|
||||
const end = useMemo(
|
||||
() => new THREE.Vector3(start.x + direction.x * length, start.y, start.z + direction.z * length),
|
||||
[start, direction, length],
|
||||
);
|
||||
const headLen = 0.3;
|
||||
const headRadius = 0.1;
|
||||
const shaftRadius = 0.04;
|
||||
|
||||
// Cilindro principal (haste)
|
||||
const midPoint = useMemo(
|
||||
() => new THREE.Vector3((start.x + end.x) / 2, (start.y + end.y) / 2, (start.z + end.z) / 2),
|
||||
[start, end],
|
||||
);
|
||||
const shaftLength = length - headLen;
|
||||
|
||||
// Rotação do cilindro (apontar de start para end)
|
||||
const rotation = useMemo(() => {
|
||||
const dir = new THREE.Vector3().subVectors(end, start).normalize();
|
||||
const quat = new THREE.Quaternion();
|
||||
quat.setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir);
|
||||
const euler = new THREE.Euler().setFromQuaternion(quat);
|
||||
return [euler.x, euler.y, euler.z] as [number, number, number];
|
||||
}, [start, end]);
|
||||
|
||||
return (
|
||||
<group>
|
||||
{shaftLength > 0 && (
|
||||
<mesh position={midPoint.toArray()} rotation={rotation} castShadow>
|
||||
<cylinderGeometry args={[shaftRadius, shaftRadius, shaftLength, 12]} />
|
||||
<meshStandardMaterial color={color} />
|
||||
</mesh>
|
||||
)}
|
||||
{/* Ponta da seta (cone) */}
|
||||
<mesh
|
||||
position={[end.x, end.y, end.z]}
|
||||
rotation={rotation}
|
||||
castShadow
|
||||
>
|
||||
<coneGeometry args={[headRadius, headLen, 12]} />
|
||||
<meshStandardMaterial color={color} />
|
||||
</mesh>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Sign3DViewer({
|
||||
length,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useMemo } from 'react';
|
||||
import { OrbitControls, Grid, Environment, Text } from '@react-three/drei';
|
||||
import * as THREE from 'three';
|
||||
import SceneCanvas from '../SceneCanvas';
|
||||
import { WindArrow } from './WindArrow';
|
||||
import FallbackDiagram from '../FallbackDiagram';
|
||||
|
||||
export interface Tower3DInput {
|
||||
@@ -190,31 +191,11 @@ function TowerModel({
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Vetores de força distribuídos pelos tramos (meio de cada tramo) */}
|
||||
{Array.from({ length: panels }).map((_, i) => {
|
||||
const pHeight = height / panels;
|
||||
const startY = (i + 0.5) * pHeight; // Altura no meio do tramo
|
||||
const pForce = forceKN / panels; // Força por tramo
|
||||
|
||||
// Escala da seta menor para ficar visualmente agradável
|
||||
const pArrowLen = Math.min(Math.max(pForce * 0.1, 0.5), 2.5);
|
||||
|
||||
// Calcular o ponto inicial para que a ponta da seta encoste na face (baseWidth / 2)
|
||||
const endX = -forceDir.x * (baseWidth / 2);
|
||||
const endZ = -forceDir.z * (baseWidth / 2);
|
||||
const startX = endX - forceDir.x * pArrowLen;
|
||||
const startZ = endZ - forceDir.z * pArrowLen;
|
||||
|
||||
return (
|
||||
<ForceArrow
|
||||
key={`force-${i}`}
|
||||
start={[startX, startY, startZ]}
|
||||
direction={forceDir}
|
||||
length={pArrowLen}
|
||||
color="#ef4444"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{/* Seta de Vento única atingindo o meio da torre */}
|
||||
<WindArrow
|
||||
target={[0, height / 2, 0]}
|
||||
direction={forceDir}
|
||||
/>
|
||||
|
||||
{/* Labels indicativos */}
|
||||
<mesh position={[baseWidth / 2 + 0.5, 0.5, 0]}>
|
||||
@@ -234,50 +215,6 @@ function TowerModel({
|
||||
);
|
||||
}
|
||||
|
||||
function ForceArrow({
|
||||
start,
|
||||
direction,
|
||||
length,
|
||||
color,
|
||||
}: {
|
||||
start: [number, number, number];
|
||||
direction: THREE.Vector3;
|
||||
length: number;
|
||||
color: string;
|
||||
}) {
|
||||
const startVec = useMemo(() => new THREE.Vector3(...start), [start]);
|
||||
const end = useMemo(
|
||||
() => new THREE.Vector3(startVec.x + direction.x * length, startVec.y, startVec.z + direction.z * length),
|
||||
[startVec, direction, length],
|
||||
);
|
||||
const mid = useMemo(
|
||||
() => new THREE.Vector3((startVec.x + end.x) / 2, (startVec.y + end.y) / 2, (startVec.z + end.z) / 2),
|
||||
[startVec, end],
|
||||
);
|
||||
const quat = useMemo(() => {
|
||||
const dir = new THREE.Vector3().subVectors(end, startVec).normalize();
|
||||
const q = new THREE.Quaternion();
|
||||
q.setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir);
|
||||
return new THREE.Euler().setFromQuaternion(q);
|
||||
}, [startVec, end]);
|
||||
const headLen = 0.3;
|
||||
|
||||
return (
|
||||
<group>
|
||||
{length - headLen > 0 && (
|
||||
<mesh position={mid.toArray()} rotation={[quat.x, quat.y, quat.z]} castShadow>
|
||||
<cylinderGeometry args={[0.05, 0.05, length - headLen, 10]} />
|
||||
<meshStandardMaterial color={color} />
|
||||
</mesh>
|
||||
)}
|
||||
<mesh position={end.toArray()} rotation={[quat.x, quat.y, quat.z]} castShadow>
|
||||
<coneGeometry args={[0.12, headLen, 10]} />
|
||||
<meshStandardMaterial color={color} />
|
||||
</mesh>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Tower3DViewer(input: Tower3DInput) {
|
||||
const { baseWidth, height } = input;
|
||||
const dist = Math.max(baseWidth * 3, height * 1.2);
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { useMemo } from 'react';
|
||||
import * as THREE from 'three';
|
||||
|
||||
export interface WindArrowProps {
|
||||
/** Ponto onde a ponta da seta do vento toca (ex: centro geométrico da estrutura) */
|
||||
target: THREE.Vector3 | [number, number, number];
|
||||
/** Direção de onde o vento vem (e para onde ele vai). Vetor normalizado. */
|
||||
direction: THREE.Vector3 | [number, number, number];
|
||||
/** Fator de escala da seta (default: 1) */
|
||||
scale?: number;
|
||||
}
|
||||
|
||||
export function WindArrow({ target, direction, scale = 1 }: WindArrowProps) {
|
||||
const length = 2.5 * scale; // Tamanho visual fixo
|
||||
const color = '#3b82f6'; // Azul padrão do vento
|
||||
|
||||
const targetVec = target instanceof THREE.Vector3 ? target : new THREE.Vector3(...target);
|
||||
const dirVec = direction instanceof THREE.Vector3 ? direction : new THREE.Vector3(...direction);
|
||||
|
||||
const end = targetVec;
|
||||
const base = useMemo(
|
||||
() => new THREE.Vector3(end.x - dirVec.x * length, end.y - dirVec.y * length, end.z - dirVec.z * length),
|
||||
[end, dirVec, length],
|
||||
);
|
||||
|
||||
const headLen = 0.3 * scale;
|
||||
const headRadius = 0.1 * scale;
|
||||
const shaftRadius = 0.04 * scale;
|
||||
|
||||
const midPoint = useMemo(
|
||||
() => new THREE.Vector3((base.x + end.x) / 2, (base.y + end.y) / 2, (base.z + end.z) / 2),
|
||||
[base, end],
|
||||
);
|
||||
const shaftLength = length - headLen;
|
||||
|
||||
const rotation = useMemo(() => {
|
||||
const dir = new THREE.Vector3().subVectors(end, base).normalize();
|
||||
const quat = new THREE.Quaternion();
|
||||
quat.setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir);
|
||||
const euler = new THREE.Euler().setFromQuaternion(quat);
|
||||
return [euler.x, euler.y, euler.z] as [number, number, number];
|
||||
}, [base, end]);
|
||||
|
||||
return (
|
||||
<group>
|
||||
{shaftLength > 0 && (
|
||||
<mesh position={midPoint.toArray()} rotation={rotation} castShadow>
|
||||
<cylinderGeometry args={[shaftRadius, shaftRadius, shaftLength, 12]} />
|
||||
<meshStandardMaterial color={color} />
|
||||
</mesh>
|
||||
)}
|
||||
{/* Ponta da seta (cone) */}
|
||||
<mesh position={[end.x, end.y, end.z]} rotation={rotation} castShadow>
|
||||
<coneGeometry args={[headRadius, headLen, 12]} />
|
||||
<meshStandardMaterial color={color} />
|
||||
</mesh>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user