import { WindParameters, BuildingDimensions, CalculationResults } from '../types'; import { getGCpf, getCCGCp } from './coefficients'; export const calculateWindLoad = ( parameters: WindParameters, dimensions: BuildingDimensions ): CalculationResults => { // 1. Constants and Factors const Kd = 0.85; // Directionality Factor (Buildings) const { basicWindSpeed: V, kzt: Kzt, ke: Ke, exposureCategory } = parameters; // 2. Building Geometry const meanRoofHeight = (dimensions.ridgeHeight + dimensions.eaveHeight) / 2; // Roof Angle (Theta) // tan(theta) = (hr - he) / (B / 2) assuming Gable symmetric // If Monoslope, different. Assuming Gable for now based on inputs. const rise = dimensions.ridgeHeight - dimensions.eaveHeight; const run = dimensions.width / 2; const roofAngleRad = Math.atan(rise / run); const roofAngleDeg = roofAngleRad * (180 / Math.PI); // 3. Velocity Pressure Coefficient Kz (Table 26.10-1) // Simplified lookup for 15ft < h <= 60ft (Low Rise) // For h < 15, use h=15. // This is valid for exposure B, C, D. // We need a helper for Kz based on Height and Exposure. const Kz = calculateKz(meanRoofHeight, exposureCategory); const Kh = Kz; // For low-rise, Kh = Kz evaluated at mean roof height // 4. Velocity Pressure qh (Eq 26.10-1) // qz = 0.00256 * Kz * Kzt * Kd * Ke * V^2 const qh = 0.00256 * Kh * Kzt * Kd * Ke * Math.pow(V, 2); const qz = qh; // At mean roof height // 5. Internal Pressure Coefficient GCpi (Table 26.13-1) let gcpi_pos = 0.18; let gcpi_neg = -0.18; if (parameters.enclosureClassification === 'Partially Enclosed') { gcpi_pos = 0.55; gcpi_neg = -0.55; } // Open is 0.00 // 6. External Pressure Coefficients (GCpf) // MWFRS Envelope Procedure (Chapter 28) // Zones: 1, 2, 3, 4, 1E, 2E, 3E, 4E // 6. External Pressure Coefficients (GCpf) // MWFRS Envelope Procedure (Chapter 28) const zones = ['1', '2', '3', '4', '1E', '2E', '3E', '4E']; const loadCase1: any = {}; const loadCase2: any = {}; const loadCase3: any = {}; const loadCase4: any = {}; zones.forEach(zone => { // --- Load Case 1 --- const gcpf1 = getGCpf(zone, roofAngleDeg, '1'); const p1_pos = qh * (gcpf1 - gcpi_pos); const p1_neg = qh * (gcpf1 - gcpi_neg); loadCase1[zone] = { positive: parseFloat(p1_pos.toFixed(1)), negative: parseFloat(p1_neg.toFixed(1)), gcpf: gcpf1 }; // --- Load Case 2 --- const gcpf2 = getGCpf(zone, roofAngleDeg, '2'); const p2_pos = qh * (gcpf2 - gcpi_pos); const p2_neg = qh * (gcpf2 - gcpi_neg); loadCase2[zone] = { positive: parseFloat(p2_pos.toFixed(1)), negative: parseFloat(p2_neg.toFixed(1)), gcpf: gcpf2 }; // --- Load Case 3 (Torsion) --- // Case 3 = (Load Case 1 values) but reduced for Torsional zones T? // Actually usually involves Torsional Moment MT. // Simplified approach for calculator: // For Low-Rise, Case 3 is usually Case 1 with Torsional Zones receiving different pressures? // Note 5 usually says "Zone 1T, 2T, etc.". // We will just replicate logic if we had T logic, but for now we'll reuse Case 1 with 'T' logic if implemented. // Let's create dummy entries for T zones or just copy Case 1 for prototype if undefined. // The screenshot shows "Zone 1T, 2T...". So we need those zones. }); // Handle Torsional Zones for Case 3 & 4 const tZones = ['1T', '2T', '3T', '4T', '5T', '6T']; tZones.forEach(zone => { // Determine base zone (e.g. 1T -> 1) const baseZone = zone.replace('T', ''); // Case 3 (Based on Case 1) // Usually 25% reduction or similar? // Screenshot showing: 1T (0.10, 1.5, 3.9). Base 1 (0.40, 5.9, 15.5). // 0.10 is 25% of 0.40. // 1.5 is roughly 25% of 5.9. // So Case 3 is 25% of Case 1 for T zones? // "Torsional loading shall be 25% of full...". // Pressure p = qh * [(GCpf) - (GCpi)] // If we reduce pressure by 25%: p * 0.25 => 1.5 vs 5.9. 5.9 * 0.25 = 1.475. Close. // 15.5 * 0.25 = 3.875. Close to 3.9. // So logic: Load Case 3 Zone T = 0.25 * Load Case 1 Zone (Pressure). // However, note that T load cases apply to specific corners while others remain full? // Simpler implementation: Just calc T zones as 0.25 * Case 1/2. const lc1 = loadCase1[baseZone]; if (lc1) { loadCase3[zone] = { positive: parseFloat((lc1.positive * 0.25).toFixed(1)), negative: parseFloat((lc1.negative * 0.25).toFixed(1)), gcpf: parseFloat((lc1.gcpf * 0.25).toFixed(2)) } } // Case 4 (Based on Case 2) const lc2 = loadCase2[baseZone]; if (lc2) { loadCase4[zone] = { positive: parseFloat((lc2.positive * 0.25).toFixed(1)), negative: parseFloat((lc2.negative * 0.25).toFixed(1)), gcpf: parseFloat((lc2.gcpf * 0.25).toFixed(2)) } } }); // 7. C&C Pressures const ccRoof: any = {}; const ccWall: any = {}; // Roof Zones 1, 2, 3 (simplified) ['1', '2', '3'].forEach(z => { const { pos, neg } = getCCGCp('Roof', z); // p = qh * [ (GCp) - (GCpi) ] // Usually GCp is (+/-). // We have pos/neg GCp. // We have (+/-) GCpi. // Two cases for each GCp? // Usually find worst case. // (+GCp) - (+GCpi) // (+GCp) - (-GCpi) ... // Simplified: // Max Positive = (+GCp) - (-GCpi) usually? // Max Negative = (-GCp) - (+GCpi) usually? // Let's output set for (+GCpi) and set for (-GCpi) or just bounds. // For C&C usually we just want the design pressures. // Screenshot "Wind Pressures p at Different Roof Zones... 1,2,3(+) ... 1(-) ...". // 1,2,3(+) = 10.1 (-39.2) - wait. // Let's implement generic calc // Case A (Pos GCp): p = qh * (pos - gcpi_neg); // Max positive pressure // Case B (Neg GCp): p = qh * (neg - gcpi_pos); // Max negative pressure (suction) const p_max_pos = qh * (pos - (-0.18)); // Assuming Enclosed const p_max_neg = qh * (neg - (0.18)); ccRoof[z] = { positive: parseFloat(p_max_pos.toFixed(1)), negative: parseFloat(p_max_neg.toFixed(1)), gcpf: 0 // Not applicable }; }); // Wall Zones 4, 5 ['4', '5'].forEach(z => { const { pos, neg } = getCCGCp('Wall', z); const p_max_pos = qh * (pos - (-0.18)); const p_max_neg = qh * (neg - (0.18)); ccWall[z] = { positive: parseFloat(p_max_pos.toFixed(1)), negative: parseFloat(p_max_neg.toFixed(1)), gcpf: 0 }; }); // Calculate 'a' (Zone Width) // a = 10% of least horizontal dimension or 0.4h, whichever is smaller, // but not less than either 4% of least horizontal dimension or 3 ft. const leastDim = Math.min(dimensions.width, dimensions.length); const val1 = 0.1 * leastDim; const val2 = 0.4 * meanRoofHeight; const small = Math.min(val1, val2); const min_limit = 0.04 * leastDim; const a = Math.max(small, min_limit, 3); return { meanRoofHeight, roofSlope: roofAngleDeg, zoneWidth: parseFloat(a.toFixed(2)), qh, qz, kd: Kd, kh: Kh, gcpi: { positive: gcpi_pos, negative: gcpi_neg }, loadCase1, loadCase2, loadCase3, loadCase4, ccRoofPressures: ccRoof, ccWallPressures: ccWall }; }; function calculateKz(z: number, exposure: string): number { // Table 26.10-1 simplified (interpolated or formula) // Using formula from commentary or Table 26.11-1 alpha/zg // For now, approximate or implement formula: // Kz = 2.01 * (z / zg)^(2/alpha) for 15 <= z <= zg // For z < 15, use z=15 usually. const h = Math.max(z, 15); let alpha = 7.0; let zg = 1200; if (exposure === 'B') { alpha = 7.0; zg = 1200; } else if (exposure === 'C') { alpha = 9.5; zg = 900; } else if (exposure === 'D') { alpha = 11.5; zg = 700; } // Formula Eq C26.10-1? Actually Table 26.10-1 footnotes. // Exposure B: 2.01 * (h/1200)^(2/7) = 2.01 * (h/1200)^0.2857 // Exposure C: 2.01 * (h/900)^(2/9.5) // Exposure D: 2.01 * (h/700)^(2/11.5) const val = 2.01 * Math.pow(h / zg, 2 / alpha); return Math.min(Math.max(val, 0.5), 2.5); // Safety bounds }