feat: adicionar super manual didatico com fluxos 2d e sheet panel

This commit is contained in:
2026-07-09 23:40:06 +00:00
parent 9fece3f174
commit 40e73e734a
34 changed files with 1443 additions and 390 deletions
+31 -84
View File
@@ -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}