diff --git a/src/core/rebar/column.ts b/src/core/rebar/column.ts new file mode 100644 index 0000000..4423456 --- /dev/null +++ b/src/core/rebar/column.ts @@ -0,0 +1,123 @@ +import { developmentLength, hookLength, NOMINAL_DIAMETER } from './common'; + +/** + * 기둥 철근 C4 (plan C4). Source: RAG/기둥철근 (1).md, 2026-10-22 최종 정리. Type-1 원형 중실 기둥. + * Lengths in mm, heights in mm from the 확대기초 상면 (up +). + */ +export interface ColumnInput { + /** 기둥 지름 D. */ + D: number; + /** 기둥 높이 H_c (확대기초 상면 ~ 코핑 Level 하면). */ + Hc: number; + /** 기둥 중심 위치의 코핑 높이 h_cop. */ + hcop: number; + /** 기둥 면 ~ P1 중심 피복 c. */ + c: number; + /** P1 위 끝의 코핑 상면 아래 깊이 e_P1. */ + eP1: number; + /** 띠철근 구간 위 끝의 코핑 상면 아래 깊이 e_top. */ + eTop: number; + /** 확대기초 깊이 H_f (for P4). */ + Hf: number; + /** 주철근 원주 등분 수 n_div. */ + nDiv: number; + /** 주철근 커플러 간격 (확대기초 상면부터). */ + ctcCoupler: number; + /** P2 커플러를 P1보다 올리는 높이. */ + dzCp2: number; + ctcP3: number; + ctcP5: number; + ctcP4: number; + /** P4 구간 깊이 비율 r_P4. */ + rP4: number; + ctcP6: number; +} + +/** 예제 운곡교 P1 (기둥철근.md §1.2 기본값). */ +export const COLUMN_P1: ColumnInput = { + D: 2000, + Hc: 12703, + hcop: 2644, + c: 125, + eP1: 200, + eTop: 1623, + Hf: 2000, + nDiv: 46, + ctcCoupler: 3000, + dzCp2: 150, + ctcP3: 300, + ctcP5: 120, + ctcP4: 120, + rP4: 0.5, + ctcP6: 1800, +}; + +const dS32 = NOMINAL_DIAMETER.S32; +const dH19 = NOMINAL_DIAMETER.H19; +const dH16 = NOMINAL_DIAMETER.H16; +const round = (v: number, d = 1) => Math.round(v * 10 ** d) / 10 ** d; + +export function columnRebar(x: ColumnInput = COLUMN_P1) { + // P1 and P2 main bars (기둥철근.md §3) + const r1 = x.D / 2 - x.c; + const r2 = r1 - dS32; + const zTop1 = x.Hc + x.hcop - x.eP1; + const lD2 = developmentLength(dS32, 500, 27); + const zTop2 = x.Hc + lD2; + const couplerLevels = Array.from({ length: Math.max(0, Math.floor(zTop1 / x.ctcCoupler) - 1) }, (_, i) => (i + 1) * x.ctcCoupler); + const couplersP1 = couplerLevels.length; + const mainLines = x.nDiv; + const mainP1 = mainLines * (couplersP1 + 1); + const mainP2 = mainLines * (couplersP1 + 1); + const couplers = couplersP1 * x.nDiv * 2; + + // Ties (기둥철근.md §4–§6) + // Tie radius r_t = r_1 + (d_P1 + d_P3)/2 (기둥철근.md §4.1) + const tieRadius = r1 + (dS32 + dH19) / 2; + const zTopT = x.Hc + x.hcop - x.eTop; + const lEmin = Math.max(x.D, zTopT / 6, 450); + const n5 = Math.floor(lEmin / x.ctcP5); + const lE = Math.max(lEmin, n5 * x.ctcP5 + x.ctcP5 / 2); + const p3 = Array.from({ length: Math.floor((zTopT - lE) / x.ctcP3) + 1 }, (_, k) => lE + k * x.ctcP3); + const n5all = Math.floor(lE / x.ctcP5); + const p5 = Array.from({ length: n5all }, (_, j) => lE - (j + 1) * x.ctcP5); + const z5last = lE - n5all * x.ctcP5; + const m4 = Math.floor((z5last + x.rP4 * x.Hf) / x.ctcP4); + const p4 = Array.from({ length: m4 }, (_, j) => z5last - (j + 1) * x.ctcP4); + + // P6 crossties (기둥철근.md §7): 4 bars per 단, from the first P4 + const z4first = p4.length ? p4[0] : z5last; + const p6Levels: number[] = []; + for (let z = z4first; z <= x.Hc; z += x.ctcP6) p6Levels.push(z); + const p6Straight = 2 * tieRadius; + const p6Hook = hookLength('tie90', dH16); + const p6Length = p6Straight + 2 * p6Hook; + + return { + r1: round(r1), + r2: round(r2), + angleStepDeg: 360 / x.nDiv, + zTop1: round(zTop1), + zTop2: round(zTop2), + couplerP1: couplerLevels, + couplerP2: couplerLevels.map((z) => z + x.dzCp2), + mainP1, + mainP2, + couplers, + tieRadius: round(tieRadius, 2), + tieCircumference: round(2 * Math.PI * tieRadius, 1), + zTopT: round(zTopT), + lEmin: round(lEmin), + lE: round(lE), + p3, + p5, + p4, + p6Levels, + p6Count: p6Levels.length * 4, + p6Length: round(p6Length, 1), + p4SemicircleLength: round(Math.PI * tieRadius, 1), + p3Count: p3.length, + p5Count: p5.length, + p4Count: p4.length, + }; +} diff --git a/src/core/rebar/common.ts b/src/core/rebar/common.ts index a249017..5ec830f 100644 --- a/src/core/rebar/common.ts +++ b/src/core/rebar/common.ts @@ -23,6 +23,20 @@ export function spliceLength(db: number, fy: number, fck: number, lambda = MATER return 1.3 * developmentLength(db, fy, fck, lambda); } +/** Nominal diameters d (mm) of the bar sizes used in the documents (코핑철근.md, 기둥철근.md). */ +export const NOMINAL_DIAMETER = { + D13: 12.7, + H16: 15.9, + H19: 19.1, + D22: 22.2, + H25: 25.4, + H29: 28.6, + S32: 31.8, + D35: 34.9, +} as const; + +export type BarSize = keyof typeof NOMINAL_DIAMETER; + export type HookKind = 'main90' | 'tie90' | 'stirrup135'; /** diff --git a/src/core/rebar/coping-main.ts b/src/core/rebar/coping-main.ts new file mode 100644 index 0000000..977e479 --- /dev/null +++ b/src/core/rebar/coping-main.ts @@ -0,0 +1,80 @@ +import { hookLength, NOMINAL_DIAMETER, spliceLength } from './common'; + +/** + * C1–C8 상·하면 주철근 (plan C3 continued). Source: RAG/코핑철근.md §5, 2026-10-22 최종 정리. + * Lengths in mm, heights in m (EL). + */ +export const MAIN = { + /** 피복 (최외측 철근 중심까지). */ + cover: 100, + /** 기본 간격 s_m and minimum end gap g_min. */ + spacing: 125, + endGapMin: 80, + /** 상면 단 간격 s_lay (EL, m) and its horizontal step (mm). */ + layEl: 0.057, + layHorizontal: 57.2, + /** 90° 갈고리 l_h (mm, as in the document). */ + hook: 472, + /** 하면 경사 구간 수평 길이 (mm, each side) and Level width. */ + slopeEach: 5067.8, + level: 2100, +} as const; + +export interface WidthLayout { + /** Edge bar centre from the coping side face: c + (d_st + d_m)/2. */ + yE: number; + /** Clear length between the two edge bars: B − 2·y_e. */ + S: number; + /** Bars inside the edge bars: n (so n + 3 in all, including the two edges and the two 110.5 ends). */ + n: number; + /** Equal end gap g = (S − n·s_m)/2. */ + g: number; + /** Bars in all: n + 3. */ + count: number; + /** Bars per cycle (alternating): 12 / 12 for 24. */ + cyclePerKind: number; + /** Dimension string, e.g. "127 · 110.5 · 21@125 · 110.5 · 127". */ + text: string; +} + +/** 폭 방향 배치 (코핑철근.md §5.1). d_st = H25 stirrup, d_m = H29 main. */ +export function widthLayout(B: number, dSt = NOMINAL_DIAMETER.H25, dM = NOMINAL_DIAMETER.H29): WidthLayout { + const yE = MAIN.cover + (dSt + dM) / 2; + const S = B - 2 * yE; + const n = Math.floor((S - 2 * MAIN.endGapMin) / MAIN.spacing); + const g = (S - n * MAIN.spacing) / 2; + const count = n + 3; + const r = (v: number) => Math.round(v * 10) / 10; + return { + yE: r(yE), + S: r(S), + n, + g: r(g), + count, + cyclePerKind: count / 2, + text: `${r(yE)} · ${r(g)} · ${n}@${MAIN.spacing} · ${r(g)} · ${r(yE)}`, + }; +} + +/** Top-layer EL of the k-th row (k = 1, 2, 3): z_k = z_1 − (k − 1)·s_lay (m). */ +export function topRowEl(z1: number, k: number): number { + return Math.round((z1 - (k - 1) * MAIN.layEl) * 1000) / 1000; +} + +/** Length of one top bar on row k: path L − 2·(c + (k−1)·step), plus two 90° hooks. */ +export function topBarLength(L: number, k: number): number { + const path = L - 2 * (MAIN.cover + (k - 1) * MAIN.layHorizontal); + return path + 2 * MAIN.hook; +} + +/** + * Bottom bar C7·C8 (spliced at the column centre, 하면 겹침이음). Each piece: + * path/2 + l_s/2 + l_h, with path = 2·slope + Level and l_s = 1.3·l_d for H29 (nominal 28.6, fck 27, fy 400). + */ +export function bottomPiece(fck = 27): { path: number; ls: number; piece: number } { + const path = 2 * MAIN.slopeEach + MAIN.level; + const ls = spliceLength(NOMINAL_DIAMETER.H29, 400, fck); + const lh = hookLength('main90', NOMINAL_DIAMETER.H29); + return { path, ls, piece: path / 2 + ls / 2 + lh }; +} + diff --git a/tests/rebar-column.test.ts b/tests/rebar-column.test.ts new file mode 100644 index 0000000..cd1a553 --- /dev/null +++ b/tests/rebar-column.test.ts @@ -0,0 +1,63 @@ +import { describe, expect, it } from 'vitest'; +import { columnRebar } from '../src/core/rebar/column'; + +const c = columnRebar(); + +describe('기둥 철근 C4 (기둥철근.md, plan C4)', () => { + it('P1 radius 875, P2 radius 843.2 (touching; centre gap = d_P1)', () => { + expect(c.r1).toBe(875); + expect(c.r2).toBe(843.2); + expect(c.angleStepDeg).toBeCloseTo(360 / 46, 9); + }); + + it('heights: P1 top 15147, P2 top ≈ 14539 (l_d of S32 at fck 27, 1836.9 ≈ the document 1836)', () => { + expect(c.zTop1).toBe(15147); + expect(Math.abs(c.zTop2 - 14539)).toBeLessThan(1); + }); + + it('couplers P1 at +3 / 6 / 9 / 12 m, P2 at +150 above each', () => { + expect(c.couplerP1).toEqual([3000, 6000, 9000, 12000]); + expect(c.couplerP2).toEqual([3150, 6150, 9150, 12150]); + }); + + it('main bar pieces 230 + 230, couplers 4 × 46 × 2 = 368', () => { + expect(c.mainP1).toBe(230); + expect(c.mainP2).toBe(230); + expect(c.couplers).toBe(368); + }); + + it('tie radius 900.45, circumference ≈ 5657.7', () => { + expect(c.tieRadius).toBe(900.45); + expect(c.tieCircumference).toBeCloseTo(5657.7, 1); + }); + + it('P3: l_e = 2340, 38 ties from 2.340 at @300 (top 13.440)', () => { + expect(c.zTopT).toBe(13724); + expect(c.lE).toBe(2340); + expect(c.p3Count).toBe(38); + expect(c.p3[0]).toBe(2340); + expect(c.p3[c.p3.length - 1]).toBe(13440); + }); + + it('P5: 19 ties @120 from 2.220 down to 0.060', () => { + expect(c.p5Count).toBe(19); + expect(c.p5[0]).toBe(2220); + expect(c.p5[c.p5.length - 1]).toBe(60); + }); + + it('P4: 8 ties @120 from −0.060 to −0.900 (none removed in the example)', () => { + expect(c.p4Count).toBe(8); + expect(c.p4[0]).toBe(-60); + expect(c.p4[c.p4.length - 1]).toBe(-900); + }); + + it('P4 semicircle ≈ 2828.9', () => { + expect(c.p4SemicircleLength).toBeCloseTo(2828.9, 0); + }); + + it('P6: 8 levels from −0.060 to 12.540, 4 bars each = 32, length ≈ 2071', () => { + expect(c.p6Levels).toEqual([-60, 1740, 3540, 5340, 7140, 8940, 10740, 12540]); + expect(c.p6Count).toBe(32); + expect(c.p6Length).toBeCloseTo(2071, 0); + }); +}); diff --git a/tests/rebar-coping-main.test.ts b/tests/rebar-coping-main.test.ts new file mode 100644 index 0000000..3df66f2 --- /dev/null +++ b/tests/rebar-coping-main.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from 'vitest'; +import { developmentLength, hookLength, NOMINAL_DIAMETER, spliceLength } from '../src/core/rebar/common'; +import { bottomPiece, topBarLength, topRowEl, widthLayout } from '../src/core/rebar/coping-main'; + +describe('C1–C8 상·하면 주철근 (코핑철근.md §5, plan C3)', () => { + it('nominal diameters: H29 28.6, S32 31.8 (documents use these, not the round numbers)', () => { + expect(NOMINAL_DIAMETER.H29).toBe(28.6); + expect(NOMINAL_DIAMETER.S32).toBe(31.8); + }); + + it('width layout for B 3100: edge 127, ends 110.5, 21@125, 24 bars (12 + 12)', () => { + const w = widthLayout(3100); + expect(w.yE).toBe(127); + expect(w.S).toBe(2846); + expect(w.n).toBe(21); + expect(w.g).toBe(110.5); + expect(w.count).toBe(24); + expect(w.cyclePerKind).toBe(12); + expect(w.text).toBe('127 · 110.5 · 21@125 · 110.5 · 127'); + }); + + it('heights: 43.284 / 43.227 / 43.170 for the three top rows', () => { + expect(topRowEl(43.284, 1)).toBe(43.284); + expect(topRowEl(43.284, 2)).toBe(43.227); + expect(topRowEl(43.284, 3)).toBe(43.17); + }); + + it('top bar lengths 12744 / 12629.6 / 12515.2 (path + 2 × 472 hook)', () => { + expect(topBarLength(12000, 1)).toBeCloseTo(12744, 0); + expect(topBarLength(12000, 2)).toBeCloseTo(12629.6, 0); + expect(topBarLength(12000, 3)).toBeCloseTo(12515.2, 0); + }); + + it('bottom piece ≈ 7448 with l_s = 1717.6 (1.3 × l_d of H29 at fck 27) — the document shows 1717', () => { + const b = bottomPiece(); + expect(b.path).toBeCloseTo(12235.6, 6); + expect(b.ls).toBeCloseTo(1717.6, 0); + expect(b.piece).toBeCloseTo(7448, 0); + }); + + it('hook 472 (16.5 × 28.6) and splice from the same formula as C1', () => { + expect(hookLength('main90', NOMINAL_DIAMETER.H29)).toBeCloseTo(472, 0); + expect(spliceLength(NOMINAL_DIAMETER.H29, 400, 27)).toBeCloseTo(1.3 * developmentLength(NOMINAL_DIAMETER.H29, 400, 27), 9); + }); +});