import { hookLength, NOMINAL_DIAMETER, type BarSize } from './common'; import { DEFAULT_FOOTING_REBAR, type FootingGridKey, type FootingRebarSettings } from './footing-settings'; /** * 확대기초 철근 B1–B8, B10–B12 (plan C5). Source: RAG/확대기초철근 (1).md, 2026-10-22 최종 정리. * B9 (전단철근) is not computed yet. Lengths in mm, heights in mm from the 확대기초 하면. */ /** 확대기초 고정 규칙 (사양): Δ₂ 하면 1·2단 간격 = 100 (고정). The other values are the editable defaults. */ export const DELTA2 = 100; /** Kept for reference; the calculations read the settings passed in (승인 2026-10-26). */ export const FOOTING_DEFAULTS = { ...DEFAULT_FOOTING_REBAR, delta2: DELTA2 } as const; export interface GridResult { /** 격자 칸 수 n = floor((A − 2·e_min)/s); 위치 수 = n + 1. */ n: number; /** 격자 길이 G = n·s. */ G: number; /** 양끝 여유 e = (A − G)/2. */ e: number; /** 위치 수 n + 1. */ positions: number; } /** Grid along one side of the footing (§3.1). side = W for B1–B4, L for B5–B8. */ export function grid(side: number, f: FootingRebarSettings = DEFAULT_FOOTING_REBAR, key: FootingGridKey = 'sAxis'): GridResult { const s = f[key]; const A = side - 2 * f.cSide; const n = Math.floor((A - 2 * f.eMin) / s); const G = n * s; const e = (A - G) / 2; return { n, G, e, positions: n + 1 }; } const d = (size: BarSize) => NOMINAL_DIAMETER[size]; /** Leg of B1·B3·B5·B7: 12·d_b on each end (proposed rule, spec §1.2 · Q-B5). */ export const legLength = (size: BarSize) => 12 * d(size); /** Layer heights (§4.2), mm from the 하면. H = footing height. */ export interface Layers { B5: number; B1: number; B24: number; B68: number; B3: number; B7: number; } export function layers(H: number, f: FootingRebarSettings = DEFAULT_FOOTING_REBAR): Layers { const zB5 = H - f.cTop; const zB1 = zB5 - (d('S29') + d('S25')) / 2; const zB24 = f.cBottom; const zB68 = f.cBottom + (d('S25') + d('S29')) / 2; return { B5: zB5, B1: zB1, B24: zB24, B68: zB68, B3: zB24 + DELTA2, B7: zB68 + DELTA2 }; } /** Lengths of the B1–B4 family along the length L (§5.2). */ export function legB2(H: number, f: FootingRebarSettings = DEFAULT_FOOTING_REBAR): number { const z = layers(H, f); return z.B1 - z.B24; // 1723 at H 2000 } export interface FootingBar { name: 'B1' | 'B2' | 'B3' | 'B4' | 'B5' | 'B6' | 'B7' | 'B8' | 'B10-1' | 'B10-2' | 'B11' | 'B12'; size: BarSize; /** Bars in all (over the footing). */ count: number; /** Length of one bar, mm (straight part + legs). */ length: number; } /** B1–B4 (§5): the grid along W, with lengths from L. */ export function bottomBars(L: number, W: number, H: number, f: FootingRebarSettings = DEFAULT_FOOTING_REBAR): FootingBar[] { const g = grid(W, f); const n = g.positions; const N4 = Math.ceil(n / 2); // B4 at even i const N2 = Math.floor(n / 2); // B2 at odd i const l0 = L - 2 * f.cSide; const leg2 = legB2(H, f); return [ { name: 'B1', size: 'S25', count: N2, length: l0 + 2 * legLength('S25') }, { name: 'B2', size: 'S25', count: N2, length: l0 + 2 * leg2 }, { name: 'B3', size: 'S19', count: N2, length: l0 + 2 * legLength('S19') }, { name: 'B4', size: 'S25', count: N4, length: l0 }, ]; } /** B5–B8 (§6): the grid along L, with lengths from W. */ export function crossBars(L: number, W: number, H: number, f: FootingRebarSettings = DEFAULT_FOOTING_REBAR): FootingBar[] { const g = grid(L, f, 'sPerp'); const n = g.positions; const N8 = Math.ceil(n / 2); // B8 at even i const N6 = Math.floor(n / 2); // B6, B5, B7 at odd i const l0 = W - 2 * f.cSide; const z = layers(H, f); const leg6 = z.B5 - z.B68; return [ { name: 'B5', size: 'S29', count: N6, length: l0 + 2 * legLength('S29') }, { name: 'B6', size: 'S29', count: N6, length: l0 + 2 * leg6 }, { name: 'B7', size: 'S19', count: N6, length: l0 + 2 * legLength('S19') }, { name: 'B8', size: 'S29', count: N8, length: l0 }, ]; } /** B10 levels: m = floor((z_B5 − e_top − c_b)/s_v) (§7.1), so the count is m + 1 (9 for P1, H 2000). */ export function b10Levels(H: number, f: FootingRebarSettings = DEFAULT_FOOTING_REBAR): number { const m = Math.floor((layers(H, f).B5 - f.eTop - f.cBottom) / f.sV); return m + 1; } /** Depth of a side bar from the side face: c_s + (d_a + d_b)/2 (§7, §8.3). B10-1 hangs on B6, B10-2 on B2. */ export const sideDepth = (dOuter: number, dB10: number, f: FootingRebarSettings = DEFAULT_FOOTING_REBAR) => f.cSide + (dOuter + dB10) / 2; /** B10-1 (on W sides) and B10-2 (on L sides): m + 1 levels at s_v, two faces each (§7). */ export function sideBars(L: number, W: number, H: number, f: FootingRebarSettings = DEFAULT_FOOTING_REBAR): FootingBar[] { const depth1 = sideDepth(NOMINAL_DIAMETER.S29, NOMINAL_DIAMETER.S19, f); // B10-1: 123.85 const depth2 = sideDepth(NOMINAL_DIAMETER.S25, NOMINAL_DIAMETER.S19, f); // B10-2: 122.25 const levels = b10Levels(H, f); const hook = hookLength('main90', d('S19')); return [ { name: 'B10-1', size: 'S19', count: levels * 2, length: L - 2 * depth2 + 2 * hook }, { name: 'B10-2', size: 'S19', count: levels * 2, length: W - 2 * depth1 + 2 * hook }, ]; } /** Assembly levels j = n, 2n, … strictly inside the B10 levels, 0 < j < m (§8.1). */ export function assemblyJs(H: number, f: FootingRebarSettings = DEFAULT_FOOTING_REBAR): number[] { const m = b10Levels(H, f) - 1; const js: number[] = []; for (let j = f.n; j < m; j += f.n) js.push(j); return js; } /** Ends of B11 / B12 lie (d_B10 + d_assembly)/2 further inside than the B10 depth, so the two bars just touch (§8.3). */ export function assemblyEndOffset(): number { return (d('S19') + d('S16')) / 2; // 17.5 } /** B11 (along W, between the B10-2 levels) and B12 (along L), 4s apart with a 2s stagger per level (§8). */ /** * First grid index of the even group of the assembly bars (B11 or B12), chosen on EVEN indices (확대기초철근 (1).md §9.1: the * B5·B1 and B9 positions take the odd grid, so B11·B12 sit on the even grid and never overlap them). Among the even choices, * the one that centres the union of both 2-step-staggered groups best (left and right gaps as equal as possible) is used. */ export function assemblyEvenStart(n: number, k: number): number { let best = 0; let bestGap = Infinity; for (let e0 = 0; e0 + 2 <= n; e0 += 2) { const idx: number[] = []; for (const s of [e0, e0 + 2]) for (let i = s; i <= n; i += k) idx.push(i); const gap = Math.abs(Math.min(...idx) - (n - Math.max(...idx))); if (gap < bestGap) { best = e0; bestGap = gap; } } return best; } /** * Grid indices of the assembly bars (B11 or B12) on one level (승인 2026-10-26): every k grid steps, on even indices, with the * 2-step stagger between levels (even levels at e0, odd levels at e0 + 2). n = positions − 1; idx = level index (0, 1, …). */ export function assemblyGridIndices(n: number, k: number, idx: number): number[] { const start = idx % 2 === 0 ? assemblyEvenStart(n, k) : assemblyEvenStart(n, k) + 2; const count = start <= n ? Math.floor((n - start) / k) + 1 : 0; return Array.from({ length: count }, (_, m) => start + m * k); } export function assemblyBars(L: number, W: number, H: number, f: FootingRebarSettings = DEFAULT_FOOTING_REBAR): FootingBar[] { const levelsJ = assemblyJs(H, f); // B11 is spread along L (교각축 직각 grid) and B12 along W (교각축 grid), per 확대기초철근 (1).md §8. const nW = grid(W, f, 'sAxis').positions - 1; const nL = grid(L, f, 'sPerp').positions - 1; const b11 = levelsJ.reduce((a, _, idx) => a + assemblyGridIndices(nL, f.kB11, idx).length, 0); const b12 = levelsJ.reduce((a, _, idx) => a + assemblyGridIndices(nW, f.kB12, idx).length, 0); const hook = Math.round(hookLength('main90', d('S16'))); // 246 mm, rounded as in the document (§8.3) const depth1 = sideDepth(NOMINAL_DIAMETER.S29, NOMINAL_DIAMETER.S19, f); const depth2 = sideDepth(NOMINAL_DIAMETER.S25, NOMINAL_DIAMETER.S19, f); return [ { name: 'B11', size: 'S16', count: b11, length: W - 2 * (depth1 + assemblyEndOffset()) + 2 * hook }, { name: 'B12', size: 'S16', count: b12, length: L - 2 * (depth2 + assemblyEndOffset()) + 2 * hook }, ]; } /** Plane positions of the grid along one side, mm from the footing centre: x_i = −side/2 + c_s + e + i·s (§3.1). */ export function gridPositions(side: number, f: FootingRebarSettings = DEFAULT_FOOTING_REBAR, key: FootingGridKey = 'sAxis'): number[] { const g = grid(side, f, key); return Array.from({ length: g.positions }, (_, i) => -side / 2 + f.cSide + g.e + i * f[key]); } /** B9 bar of the rule (§9.1, Q-B18): a vertical bar at (x, n), mm from the footing centre. */ export interface B9Bar { x: number; n: number; } /** * B9 positions (확대기초철근 (1).md §9.1, rule applied): rows at W index 1, 5, 9 … (step 4 on the grid); in row r the L index * starts at 1 (even r) or 3 (odd r) and steps by 4. A bar is left out when it falls in a column zone: its centre is closer * than the column radius plus half a bar to a column axis (Q-B20, applied: the 기둥 구역 is removed). */ export function b9Bars(L: number, W: number, columnX: number[], columnD: number = 2000, f: FootingRebarSettings = DEFAULT_FOOTING_REBAR): { rows: number; perRow: number[]; bars: B9Bar[] } { const xs = gridPositions(L, f); const ns = gridPositions(W, f); const rowIndices: number[] = []; for (let i = 1; i < ns.length; i += 4) rowIndices.push(i); const halfBar = NOMINAL_DIAMETER.H16 / 2; const bars: B9Bar[] = []; const perRow = rowIndices.map((iW, r) => { const start = r % 2 === 0 ? 1 : 3; let kept = 0; for (let iL = start; iL < xs.length; iL += 4) { const x = xs[iL]; const n = ns[iW]; if (columnX.some((cx) => Math.hypot(x - cx, n) < columnD / 2 + halfBar)) continue; bars.push({ x, n }); kept++; } return kept; }); return { rows: rowIndices.length, perRow, bars }; } /** * B9 전단철근 (확대기초철근 (1).md §9). Vertical bars from the B2·B4 level to the B5 level, H16, with 135° hooks at both ends. * Length = l_v + 2·l_h (l_v = z_B5 − z_B24). The hook tail (6·d) turns toward the footing centre (Q-B19, applied). */ export function shearB9(L: number, W: number, H: number, columnX: number[] = [], columnD: number = 2000, f: FootingRebarSettings = DEFAULT_FOOTING_REBAR): { rows: number; perRow: number[]; count: number; length: number; bars: B9Bar[] } { const { rows, perRow, bars } = b9Bars(L, W, columnX, columnD, f); const lv = layers(H, f).B5 - layers(H, f).B24; // 1750 const lh = hookLength('stirrup135', d('H16')); return { rows, perRow, count: bars.length, length: lv + 2 * lh, bars }; } /** Leg offsets from the grid line (§5.3, §6.3): the B1/B3 and B5/B7 legs sit beside the outer leg. */ export function legOffsets(): { B1: number; B3: number; B5: number; B7: number } { const dB2 = NOMINAL_DIAMETER.S25; const dB1 = NOMINAL_DIAMETER.S25; const dB3 = NOMINAL_DIAMETER.S19; const dB6 = NOMINAL_DIAMETER.S29; const dB5 = NOMINAL_DIAMETER.S29; const dB7 = NOMINAL_DIAMETER.S19; return { B1: (dB2 + dB1) / 2, B3: (dB2 + dB3) / 2, B5: (dB6 + dB5) / 2, B7: (dB6 + dB7) / 2, }; } /** B11/B12 heights (§8.1): B10 levels at 150 + k·s_v; the assembly levels are j = n, 2n, 3n (n = 2), and B12 sits d_B10 above B11. */ export function assemblyHeights(H: number, f: FootingRebarSettings = DEFAULT_FOOTING_REBAR): { B11: number[]; B12: number[] } { const js = assemblyJs(H, f); const B11 = js.map((j) => f.cBottom + j * f.sV); const B12 = B11.map((z) => z + NOMINAL_DIAMETER.S19); return { B11, B12 }; } /** * Values shown on the 확대기초 panel as rules and derived values (승인 2026-10-26). Each is read from the same functions the * rebar table and the 3D view use, so the panel cannot drift from the calculation. H is the footing height (mm). */ export function footingRuleValues(H: number, f: FootingRebarSettings = DEFAULT_FOOTING_REBAR): { upper2sAxis: number; upper2sPerp: number; delta2: number; b2Leg: number; b6Leg: number; b9Pitch: number; b9Stagger: number; b11Pitch: number; b12Pitch: number; assemblyVertical: number; assemblyLevels: number; } { const z = layers(H, f); return { upper2sAxis: 2 * f.sAxis, upper2sPerp: 2 * f.sPerp, delta2: DELTA2, b2Leg: legB2(H, f), b6Leg: z.B5 - z.B68, b9Pitch: 4 * f.sAxis, b9Stagger: 2 * f.sAxis, b11Pitch: f.kB11 * f.sPerp, b12Pitch: f.kB12 * f.sAxis, assemblyVertical: f.n * f.sV, assemblyLevels: assemblyJs(H, f).length, }; }