281 lines
9.3 KiB
TypeScript
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>
|
|
);
|
|
}
|