Files
BrainWind/app/src/components/three/IsolatedRoof3D.tsx
T

281 lines
9.3 KiB
TypeScript

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';
import { WindArrow } from './WindArrow';
import { useCanvasTheme } from '@/lib/theme';
export interface IsolatedRoof3DInput {
/** Tipo de cobertura: 'shed' (uma água) ou 'gable' (duas águas) */
type: 'shed' | 'gable';
/** Inclinação θ (graus) */
theta: number;
/** Altura livre dos suportes (m) */
height: number;
/** 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;
/** Cpe sotavento (face oposta) */
cpeLeeward: number;
/** Cpe sob a face superior (sucção) */
cpeTop: number;
/** Força resultante na cobertura (kN) */
forceKN: number;
}
function pressureColor(cpe: number): THREE.Color {
const clamped = Math.max(-2.5, Math.min(1.5, cpe));
const t = (clamped + 2.5) / 4.0;
const h = 240 - t * 240; // azul -> vermelho
return new THREE.Color(`hsl(${h}, 75%, 50%)`);
}
function IsolatedRoofModel({
type,
theta,
height,
width,
depth,
cpeWindward,
cpeLeeward,
forceKN,
}: IsolatedRoof3DInput) {
const theme = useCanvasTheme();
const isDark = theme === 'dark';
const labelColor = isDark ? '#cbd5e1' : '#475569';
const textColor = isDark ? '#93c5fd' : '#1a202c';
const lineMaterialColor = isDark ? '#94a3b8' : '#64748b';
const pillarColor = isDark ? '#cbd5e1' : '#475569';
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 h_diff = depth * Math.tan(thetaRad);
const h_half = (depth / 2) * Math.tan(thetaRad);
// Altura média da cobertura no centro geométrico
const centerY = type === 'shed' ? height + h_diff / 2 : height + h_half / 2;
// Definição das colunas de suporte (pilares)
const pillars = useMemo(() => {
const list: { pos: [number, number, number]; h: number }[] = [];
if (type === 'shed') {
list.push(
{ 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: [-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, 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={[width * 1.5, depth * 1.5]} />
<meshStandardMaterial color="#94a3b8" transparent opacity={0.15} />
</mesh>
{/* === COBERTURA (PAINÉIS 3D SÓLIDOS) === */}
{type === 'shed' ? (
// Uma água (Shed): dividida em metade barlavento e metade sotavento
<group>
{/* Metade Barlavento (Z < 0) */}
<mesh
position={[0, height + h_diff / 4, -depth / 4]}
rotation={[-thetaRad, 0, 0]}
castShadow
receiveShadow
>
<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) */}
<mesh
position={[0, height + (3 * h_diff) / 4, depth / 4]}
rotation={[-thetaRad, 0, 0]}
castShadow
receiveShadow
>
<boxGeometry args={[width, 0.08, depth / (2 * Math.cos(thetaRad))]} />
<meshStandardMaterial color={leewardColor} opacity={0.9} transparent roughness={0.4} />
</mesh>
</group>
) : (
// Duas águas (Gable)
<group>
{/* Água Esquerda / Barlavento (Z < 0) */}
<mesh
position={[0, height + h_half / 2, -depth / 4]}
rotation={[-thetaRad, 0, 0]}
castShadow
receiveShadow
>
<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) */}
<mesh
position={[0, height + h_half / 2, depth / 4]}
rotation={[thetaRad, 0, 0]}
castShadow
receiveShadow
>
<boxGeometry args={[width, 0.08, depth / (2 * Math.cos(thetaRad))]} />
<meshStandardMaterial color={leewardColor} opacity={0.9} transparent roughness={0.4} />
</mesh>
</group>
)}
{/* Pilares de Suporte */}
{pillars.map((p, i) => (
<mesh key={`pillar-${i}`} position={p.pos} castShadow>
<cylinderGeometry args={[0.06, 0.06, p.h, 16]} />
<meshStandardMaterial color={pillarColor} roughness={0.5} />
</mesh>
))}
{/* 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) === */}
{/* Cota de Altura (h) */}
<group position={[-halfDepth - 0.4, 0, -halfDepth]}>
<mesh position={[0, height / 2, 0]}>
<boxGeometry args={[0.015, height, 0.015]} />
<meshStandardMaterial color={lineMaterialColor} />
</mesh>
<mesh position={[0, height, 0]}>
<boxGeometry args={[0.1, 0.015, 0.015]} />
<meshStandardMaterial color={lineMaterialColor} />
</mesh>
<mesh position={[0, 0, 0]}>
<boxGeometry args={[0.1, 0.015, 0.015]} />
<meshStandardMaterial color={lineMaterialColor} />
</mesh>
<Text
position={[-0.15, height / 2, 0]}
rotation={[0, -Math.PI / 2, 0]}
fontSize={0.25}
color={labelColor}
anchorX="center"
anchorY="middle"
>
h = {height}m
</Text>
</group>
{/* Cota de Profundidade/Span (d) */}
<group position={[halfDepth + 0.4, height / 2, 0]}>
<mesh position={[0, 0, 0]}>
<boxGeometry args={[0.015, 0.015, depth]} />
<meshStandardMaterial color={lineMaterialColor} />
</mesh>
<mesh position={[0, 0, halfDepth]}>
<boxGeometry args={[0.1, 0.015, 0.015]} />
<meshStandardMaterial color={lineMaterialColor} />
</mesh>
<mesh position={[0, 0, -halfDepth]}>
<boxGeometry args={[0.1, 0.015, 0.015]} />
<meshStandardMaterial color={lineMaterialColor} />
</mesh>
<Text
position={[0.15, 0, 0]}
rotation={[0, Math.PI / 2, 0]}
fontSize={0.25}
color={labelColor}
anchorX="center"
anchorY="middle"
>
d = {depth}m
</Text>
</group>
{/* Rótulo Superior */}
<Text
position={[0, centerY + 3, 0]}
color={textColor}
anchorX="center"
anchorY="bottom"
>
θ={theta}° | F = {forceKN.toFixed(1)} kN
</Text>
</group>
);
}
export default function IsolatedRoof3DViewer({
type,
theta,
height,
width,
depth,
cpeWindward,
cpeLeeward,
cpeTop,
forceKN,
}: IsolatedRoof3DInput) {
const cameraDistance = Math.max(Math.max(width, depth) * 1.3, height * 1.5, 8);
const fallback = (
<FallbackDiagram
type="isolatedRoof"
props={{ type, theta, height, width, depth, cpeWindward, cpeLeeward, cpeTop, forceKN }}
/>
);
return (
<SceneCanvas
shadows
gl={{ preserveDrawingBuffer: true, antialias: true }}
camera={{ position: [cameraDistance, cameraDistance * 0.8, cameraDistance], fov: 40 }}
fallback={fallback}
>
<ambientLight intensity={0.7} />
<directionalLight
position={[depth * 1.5, height * 3, depth * 1.5]}
intensity={1.2}
castShadow
shadow-mapSize-width={1024}
shadow-mapSize-height={1024}
/>
<IsolatedRoofModel
type={type}
theta={theta}
height={height}
width={width}
depth={depth}
cpeWindward={cpeWindward}
cpeLeeward={cpeLeeward}
cpeTop={cpeTop}
forceKN={forceKN}
/>
<Grid infiniteGrid fadeDistance={cameraDistance * 5} sectionColor="#94a3b8" cellColor="#cbd5e1" position={[0, -0.01, 0]} />
<OrbitControls makeDefault minPolarAngle={0} maxPolarAngle={Math.PI / 2 - 0.05} />
<Environment preset="city" />
</SceneCanvas>
);
}