import { useMemo } from 'react'; import { Grid, Environment, Text } from '@react-three/drei'; import { ViewerOrbitControls as OrbitControls } from './ViewerOrbitControls'; import * as THREE from 'three'; import SceneCanvas from '../SceneCanvas'; import FallbackDiagram from '../FallbackDiagram'; import { useCanvasTheme } from '@/lib/theme'; export interface Cylinder3DInput { diameter: number; height: number; /** Cpe profile ao longo da circunferência (0° a 180°) */ cpeProfile: { angle: number; cpe: number }[]; cpi: number; } function cylinderColor(cpe: number, cpi: number): THREE.Color { const p = cpe - cpi; const intensity = Math.min(1, Math.abs(p) / 1.2); if (p > 0) { return new THREE.Color(`hsl(${215 - intensity * 10}, ${70 + intensity * 25}%, ${Math.max(35, 65 - intensity * 25)}%)`); } return new THREE.Color(`hsl(0, ${70 + intensity * 25}%, ${Math.max(40, 65 - intensity * 20)}%)`); } function CylinderModel({ diameter, height, cpeProfile, cpi }: Cylinder3DInput) { const theme = useCanvasTheme(); const isDark = theme === 'dark'; const segments = 64; const radius = diameter / 2; // Espelha o cpeProfile para cobrir de 0° a 360° const fullCpeProfile = useMemo(() => { if (cpeProfile.length === 0) return []; const arr = [...cpeProfile]; const step = cpeProfile.length > 1 ? cpeProfile[1].angle - cpeProfile[0].angle : 10; // Espelha de 180° a 360° for (let angle = 180 + step; angle < 360; angle += step) { const mirroredAngle = 360 - angle; const closest = cpeProfile.find(p => Math.abs(p.angle - mirroredAngle) < 0.1) || cpeProfile[cpeProfile.length - 1]; arr.push({ angle, cpe: closest.cpe }); } // Fecha o ciclo em 360° (igual a 0°) arr.push({ angle: 360, cpe: cpeProfile[0].cpe }); return arr; }, [cpeProfile]); // Cria faces individuais com cor independente por ângulo const faces = useMemo(() => { const arr: { angle: number; cpe: number; color: THREE.Color }[] = []; for (let i = 0; i < fullCpeProfile.length - 1; i++) { const a = fullCpeProfile[i]; const b = fullCpeProfile[i + 1]; const angleMid = (a.angle + b.angle) / 2; const cpeMid = (a.cpe + b.cpe) / 2; arr.push({ angle: angleMid, cpe: cpeMid, color: cylinderColor(cpeMid, cpi) }); } return arr; }, [fullCpeProfile, cpi]); return ( {/* Paredes Verticais do Cilindro */} {faces.map((face, idx) => { if (fullCpeProfile.length <= idx + 1) return null; const stepAngle = fullCpeProfile[1].angle - fullCpeProfile[0].angle; const a0 = (face.angle - stepAngle / 2) * Math.PI / 180; const a1 = (face.angle + stepAngle / 2) * Math.PI / 180; const x0 = Math.cos(a0) * radius; const z0 = Math.sin(a0) * radius; const x1 = Math.cos(a1) * radius; const z1 = Math.sin(a1) * radius; // Normais dos vértices const nx0 = Math.cos(a0); const nz0 = Math.sin(a0); const nx1 = Math.cos(a1); const nz1 = Math.sin(a1); // Array com os 6 vértices para formar dois triângulos (um quad completo) const vertices = new Float32Array([ x0, 0, z0, x1, 0, z1, x1, height, z1, x0, 0, z0, x1, height, z1, x0, height, z0, ]); const normals = new Float32Array([ nx0, 0, nz0, nx1, 0, nz1, nx1, 0, nz1, nx0, 0, nz0, nx1, 0, nz1, nx0, 0, nz0, ]); return ( ); })} {/* Tampa superior sólida */} {/* Anéis de detalhe (bordas do cilindro) */} {/* Seta indicativa de direção do vento */} Vento {/* Texto de Informação */} Alt = {height}m | Diâm = {diameter}m ); } export default function Cylinder3DViewer({ diameter, height, cpeProfile, cpi }: Cylinder3DInput) { const theme = useCanvasTheme(); const isDark = theme === 'dark'; const fallback = ( ); const maxDim = Math.max(diameter, height); return ( ); }