From cebfb08980ca774a73331d58642102271cb6b677 Mon Sep 17 00:00:00 2001 From: Marcos Date: Thu, 23 Jul 2026 20:38:36 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=80=20Auto-deploy:=20BrainWind=20atual?= =?UTF-8?q?izado=20em=2023/07/2026=2020:38:36?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/App.tsx | 6 +- app/src/components/DragCoefficientChart.tsx | 115 +++++++++++++++ app/src/components/WindFlowGrid2D.tsx | 23 +++ app/src/components/three/ResizableBox3D.tsx | 120 +++++++++++++++ app/src/data/manuals/calc_ca.ts | 16 ++ app/src/data/manuals/index.ts | 2 + app/src/lib/i18n.ts | 1 + app/src/pages/CalcCaModule.tsx | 153 ++++++++++++++++++++ 8 files changed, 435 insertions(+), 1 deletion(-) create mode 100644 app/src/components/DragCoefficientChart.tsx create mode 100644 app/src/components/three/ResizableBox3D.tsx create mode 100644 app/src/data/manuals/calc_ca.ts create mode 100644 app/src/pages/CalcCaModule.tsx diff --git a/app/src/App.tsx b/app/src/App.tsx index 5434d55..e6c705a 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -8,6 +8,7 @@ import IsolatedRoofModule from './pages/IsolatedRoofModule'; import BarSelectorModule from './pages/BarSelectorModule'; import BridgeModule from './pages/BridgeModule'; import DynamicsModule from './pages/DynamicsModule'; +import CalcCaModule from './pages/CalcCaModule'; import SettingsModule from './pages/SettingsModule'; import TowerModule from './pages/TowerModule'; import PiperackModule from './pages/PiperackModule'; @@ -15,7 +16,7 @@ import CertificatePage from './pages/CertificatePage'; import { Home, Settings, Menu, Cylinder, Church, CircleDot, Square, Layers, BarChart3, Activity, Warehouse, - Settings2, Sun, Moon, Frame, FolderOpen, BookOpen, MoreVertical, Info, ShieldCheck + Settings2, Sun, Moon, Frame, FolderOpen, BookOpen, MoreVertical, Info, ShieldCheck, Calculator } from 'lucide-react'; import { DropdownMenu, @@ -95,6 +96,7 @@ function AppLayout({ children }: { children: React.ReactNode }) { { path: '/torre', icon: , labelKey: 'nav_tower' as const }, { path: '/piperack', icon: , labelKey: 'nav_piperack' as const }, { path: '/dinamica', icon: , labelKey: 'nav_dynamics' as const }, + { path: '/calc-ca', icon: , labelKey: 'nav_calc_ca' as const }, ]; return ( @@ -356,6 +358,7 @@ function HomeMock() { { to: '/pontes', icon: BridgeIcon, label: 'Pontes', desc: 'Pse, Cx/Cz do tabuleiro, flutter, galope (sec. 11).' }, { to: '/torre', icon: TowerIcon, label: 'Torres', desc: 'Torres treliçadas e mastros (sec. 8.4).' }, { to: '/piperack', icon: Frame, label: 'Pipe-Racks Industriais', desc: 'Estruturas reticuladas múltiplas, tubulações, fator η (Tab. 28).' }, + { to: '/calc-ca', icon: Calculator, label: 'Calculadora de C_a', desc: 'Coeficiente de arrasto (Fig. 4 e 5).' }, ]; return ( @@ -421,6 +424,7 @@ function App() { } /> } /> } /> + } /> } /> } /> diff --git a/app/src/components/DragCoefficientChart.tsx b/app/src/components/DragCoefficientChart.tsx new file mode 100644 index 0000000..7df0a8a --- /dev/null +++ b/app/src/components/DragCoefficientChart.tsx @@ -0,0 +1,115 @@ +import { useMemo } from 'react'; + +interface DragCoefficientChartProps { + l1: number; + l2: number; + h: number; + caLow: number; + caHigh: number; +} + +export function DragCoefficientChart({ l1, l2, h, caLow, caHigh }: DragCoefficientChartProps) { + const hL1 = h / l1; + const l1L2 = l1 / l2; + + // The chart in the user's image is a log-log scale. + // X axis: L1/L2 from 4.0 to 0.2 (inverted in the image, usually right to left or log scale). + // Y axis: H/L1 from 0.25 to 10 (log scale). + // For a simplified visual representation, we will map these to linear or semi-log pixel coordinates. + + const width = 400; + const height = 400; + const padding = 40; + + const getCoordinates = (xVal: number, yVal: number) => { + // Map l1/l2 (0.2 to 4) to X axis (padding to width - padding) + // using log scale + const minX = 0.2; + const maxX = 4.0; + const logMinX = Math.log10(minX); + const logMaxX = Math.log10(maxX); + const logX = Math.log10(Math.max(minX, Math.min(xVal, maxX))); + // the image has 4 on the left and 0.2 on the right, let's follow the standard 0.2 -> 4.0 (left to right) + const px = padding + ((logX - logMinX) / (logMaxX - logMinX)) * (width - 2 * padding); + + // Map h/l1 (0.25 to 8) to Y axis (height - padding to padding) + const minY = 0.25; + const maxY = 8.0; + const logMinY = Math.log10(minY); + const logMaxY = Math.log10(maxY); + const logY = Math.log10(Math.max(minY, Math.min(yVal, maxY))); + const py = height - padding - ((logY - logMinY) / (logMaxY - logMinY)) * (height - 2 * padding); + + return { px, py }; + }; + + const point = getCoordinates(l1L2, hL1); + + // Generate some background curves just for aesthetic resemblance to the NBR abacus + const curves = useMemo(() => { + const paths = []; + for (let i = 0.5; i <= 2.2; i += 0.2) { + // Fake isoline generation + const points = []; + for (let x = 0.2; x <= 4.0; x += 0.2) { + // y = (i * x) + something non-linear just to make curves + const y = Math.max(0.25, Math.min(8, (i * 2) / (x + 0.5))); + points.push(getCoordinates(x, y)); + } + + const d = points.reduce((acc, pt, idx) => { + return idx === 0 ? `M ${pt.px} ${pt.py}` : `${acc} L ${pt.px} ${pt.py}`; + }, ''); + paths.push(d); + } + return paths; + }, []); + + return ( +
+

Posição no Ábaco (Visualização Simplificada)

+ + {/* Grid lines */} + {[0.2, 0.5, 1, 2, 4].map((x) => { + const pt = getCoordinates(x, 1); + return ( + + ); + })} + {[0.25, 0.5, 1, 2, 4, 8].map((y) => { + const pt = getCoordinates(1, y); + return ( + + ); + })} + + {/* Fake Curves */} + {curves.map((d, idx) => ( + + ))} + + {/* Axes */} + + + + {/* Labels */} + L₁ / L₂ (Eixo X) + H / L₁ (Eixo Y) + + {/* User Point */} + + + {/* Crosshair */} + + + + + Ca Baixa: {caLow} + + + Ca Alta: {caHigh} + + +
+ ); +} diff --git a/app/src/components/WindFlowGrid2D.tsx b/app/src/components/WindFlowGrid2D.tsx index d8699c7..bc07b13 100644 --- a/app/src/components/WindFlowGrid2D.tsx +++ b/app/src/components/WindFlowGrid2D.tsx @@ -769,6 +769,29 @@ export function WindFlowGrid2D({ type }: WindFlowGrid2DProps) { ); + case 'calc_ca': + return ( +
+ Escoamento sobre edificação retangular + + {styleBlock} + + + + + + {/* Esteira turbulenta a sotavento */} + + + + + +

+ O vento incide perpendicularmente à face L1, separando-se nas quinas e criando zonas de esteira nas laterais (L2) e a sotavento. A relação entre essas faces define o arrasto global. +

+
+ ); + default: // Caso padrão dinâmico geral (Módulos dinâmicos simples) return ( diff --git a/app/src/components/three/ResizableBox3D.tsx b/app/src/components/three/ResizableBox3D.tsx new file mode 100644 index 0000000..d93d63c --- /dev/null +++ b/app/src/components/three/ResizableBox3D.tsx @@ -0,0 +1,120 @@ +import { useRef, useMemo } from 'react'; +import { useFrame } from '@react-three/fiber'; +import { Edges } from '@react-three/drei'; +import * as THREE from 'three'; + +interface ResizableBox3DProps { + l1: number; + l2: number; + h: number; +} + +function WindParticles({ l1, l2, h }: { l1: number, l2: number, h: number }) { + const count = 40; + const meshRef = useRef(null); + + // Create dummy object to compute matrix for each instance + const dummy = useMemo(() => new THREE.Object3D(), []); + + // Generate random starting positions + const particles = useMemo(() => { + const temp = []; + for (let i = 0; i < count; i++) { + temp.push({ + x: (Math.random() - 0.5) * (l1 * 3), // Spread wider than the building + y: Math.random() * (h * 1.5), // Spread higher than the building + z: (Math.random() * 40) + l2 / 2 + 5, // Start in front of the building + speed: 10 + Math.random() * 15, // Random speeds + offset: Math.random() * 100 // Phase offset + }); + } + return temp; + }, [l1, l2, h, count]); + + useFrame((_state, delta) => { + if (!meshRef.current) return; + + particles.forEach((particle, i) => { + // Move particle along Z axis (wind direction) + particle.z -= particle.speed * delta; + + // If particle passed the building, reset it to the front + if (particle.z < -l2 - 10) { + particle.z = (Math.random() * 20) + l2 / 2 + 10; + particle.x = (Math.random() - 0.5) * (l1 * 3); + particle.y = Math.random() * (h * 1.5); + } + + // Simple avoidance / turbulence effect around the building + let currentX = particle.x; + let currentY = particle.y; + + // If particle is close to the front face, push it out + if (particle.z > -l2/2 && particle.z < l2/2 + 2) { + const isHittingFront = Math.abs(particle.x) < l1/2 + 0.5 && particle.y < h + 0.5; + if (isHittingFront) { + // Push to the sides or up + if (Math.abs(particle.x) > (l1/2) * 0.7) { + currentX += Math.sign(particle.x) * (20 * delta); + } else { + currentY += 20 * delta; + } + } + } + + dummy.position.set(currentX, currentY, particle.z); + dummy.scale.set(0.05, 0.05, Math.max(0.5, particle.speed * 0.1)); // Stretch by speed + dummy.updateMatrix(); + meshRef.current!.setMatrixAt(i, dummy.matrix); + }); + + meshRef.current.instanceMatrix.needsUpdate = true; + }); + + return ( + + + + + ); +} + +export function ResizableBox3D({ l1, l2, h }: ResizableBox3DProps) { + const meshRef = useRef(null); + + // Create target scale and position based on inputs + const targetScale = useMemo(() => new THREE.Vector3(l1, h, l2), [l1, l2, h]); + + useFrame((_state, delta) => { + if (meshRef.current) { + // Smoothly animate the scale + meshRef.current.scale.lerp(targetScale, delta * 5); + // Smoothly animate the position to keep the base on the ground + meshRef.current.position.lerp( + new THREE.Vector3(0, meshRef.current.scale.y / 2, 0), + delta * 5 + ); + } + }); + + return ( + + {/* Ground plane for reference */} + + + + + + + {/* The Animated Box */} + + + + + + + {/* Wind Flow Animation */} + + + ); +} diff --git a/app/src/data/manuals/calc_ca.ts b/app/src/data/manuals/calc_ca.ts new file mode 100644 index 0000000..c44bb4b --- /dev/null +++ b/app/src/data/manuals/calc_ca.ts @@ -0,0 +1,16 @@ +import React from 'react'; + +export const calcCaManual = { + title: 'Calculadora de Coeficiente de Arrasto (Ca)', + intro: 'Entenda como o vento incide sobre uma edificação retangular e gera a força de arrasto global.', + sections: [ + { + title: 'Coeficiente de Arrasto (Ca)', + content: React.createElement('p', null, 'O coeficiente de arrasto é utilizado para calcular a força global do vento sobre a estrutura inteira. Ele leva em conta as proporções da edificação (L1/L2 e H/L1) e a turbulência do vento.'), + }, + { + title: 'Baixa vs. Alta Turbulência', + content: React.createElement('p', null, 'Edificações em locais planos (baixa turbulência) sofrem um padrão de escoamento diferente de edificações em centros urbanos (alta turbulência), alterando significativamente o valor de Ca.'), + } + ], +}; diff --git a/app/src/data/manuals/index.ts b/app/src/data/manuals/index.ts index 4f9fc4a..cc56a3d 100644 --- a/app/src/data/manuals/index.ts +++ b/app/src/data/manuals/index.ts @@ -9,6 +9,7 @@ import { barManual } from './bar'; import { dynamicsManual } from './dynamics'; import { isolatedRoofManual } from './isolated-roof'; import { piperackManual } from './piperack'; +import { calcCaManual } from './calc_ca'; export const manuals: Record = { bridge: bridgeManual, @@ -22,4 +23,5 @@ export const manuals: Record = { dynamics: dynamicsManual, 'isolated-roof': isolatedRoofManual, piperack: piperackManual, + calc_ca: calcCaManual, }; diff --git a/app/src/lib/i18n.ts b/app/src/lib/i18n.ts index b397e62..48223b9 100644 --- a/app/src/lib/i18n.ts +++ b/app/src/lib/i18n.ts @@ -60,6 +60,7 @@ const translations: Dict = { nav_tower: { 'pt-BR': 'Torres', 'en-US': 'Towers' }, nav_piperack: { 'pt-BR': 'Pipe-rack', 'en-US': 'Pipe-rack' }, nav_dynamics: { 'pt-BR': 'Dinâmica + Vórtices', 'en-US': 'Dynamics + Vortex' }, + nav_calc_ca: { 'pt-BR': 'Calc_Ca', 'en-US': 'Calc_Ca' }, nav_settings: { 'pt-BR': 'Preferências', 'en-US': 'Preferences' }, nav_collapse: { 'pt-BR': 'Recolher sidebar', 'en-US': 'Collapse sidebar' }, nav_expand: { 'pt-BR': 'Expandir sidebar', 'en-US': 'Expand sidebar' }, diff --git a/app/src/pages/CalcCaModule.tsx b/app/src/pages/CalcCaModule.tsx new file mode 100644 index 0000000..ed3d78c --- /dev/null +++ b/app/src/pages/CalcCaModule.tsx @@ -0,0 +1,153 @@ +import { useState } from 'react'; +import { getDragCoefficient } from '@/lib/drag'; +import { useI18n } from '@/store/i18nStore'; +import { DragCoefficientChart } from '@/components/DragCoefficientChart'; +import { ResizableBox3D } from '@/components/three/ResizableBox3D'; +import SceneCanvas from '@/components/SceneCanvas'; +import { Environment } from '@react-three/drei'; +import { ViewerOrbitControls as OrbitControls } from '@/components/three/ViewerOrbitControls'; +import { EducationalManual } from '@/components/EducationalManual'; +import { Calculator, Info } from 'lucide-react'; +import { Input } from '@/components/ui/input'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; + +export default function CalcCaModule() { + const { t } = useI18n(); + const [l1, setL1] = useState(20); + const [l2, setL2] = useState(40); + const [h, setH] = useState(10); + + // Clamp inputs to avoid Infinity or negative values breaking the UI + const safeL1 = Math.max(0.1, l1 || 1); + const safeL2 = Math.max(0.1, l2 || 1); + const safeH = Math.max(0.1, h || 1); + + const caLow = getDragCoefficient(safeL1, safeL2, safeH, 'low'); + const caHigh = getDragCoefficient(safeL1, safeL2, safeH, 'high'); + + return ( +
+
+
+ +
+
+

{t('nav_calc_ca')}

+

+ Cálculo rápido de Coeficiente de Arrasto (Ca) - NBR 6123:2023, Fig. 4 e 5. +

+
+
+ +
+ {/* Left Column: Inputs & Results */} +
+ + + Entrada de Dados + Dimensões da edificação retangular + + +
+ + setL1(parseFloat(e.target.value))} + /> +
+
+ + setL2(parseFloat(e.target.value))} + /> +
+
+ + setH(parseFloat(e.target.value))} + /> +
+
+
+ + + + + + Resultados (Ca) + + + +
+
+ Baixa Turbulência + {caLow.toFixed(2)} +
+
+ Alta Turbulência + {caHigh.toFixed(2)} +
+
+ H/L₁ = {(safeH / safeL1).toFixed(2)} + L₁/L₂ = {(safeL1 / safeL2).toFixed(2)} +
+
+
+
+
+ + {/* Center Column: 3D Visualization */} +
+ + + Visualização 3D + Proporções dinâmicas + + +
+ +
+ } frameloop="always"> + + + + + + +
+ Arraste para rotacionar, role para aproximar +
+
+
+
+ + {/* Right Column: Chart */} +
+ + + Ábaco NBR 6123 + Curvas isotermas aproximadas + + + + + +
+
+
+ ); +}