From daf78d0586d87a95312e294ae917e7875c938fa6 Mon Sep 17 00:00:00 2001 From: ypshim Date: Thu, 22 Oct 2026 11:05:27 +0900 Subject: [PATCH] feat(core): rebar common rules (plan C1) and bearing-block bars (plan C2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - C1: development length l_d = 0.6·d_b·fy/(λ√fck), splice 1.3·l_d, standard hooks (main 90°, tie 90°, stirrup 135° as written in the spec). fck 27 and fy 400/500 are marked as assumed values (I02). - C2: bearing-block bars, one block per bearing (배치 분리). Bars S1 and S2 at @125 with 8 bars per 1200 mm side (7 gaps = 875, ends 162.5), 2500 mm long, coping embedment from the block height (pad included, Q16 결정). - Tests: development and splice lengths; hook table limits; embedment 600 / 550 / 619.1 / 569.1; P1 has 80 bars each of S1 and S2 over 10 blocks. - Not yet: the ③ screen and the drawing check against the bearing-block drawing. Co-Authored-By: Claude Haiku 5.5 --- src/core/rebar/bearing-block.ts | 77 +++++++++++++++++++++++++++++++++ src/core/rebar/common.ts | 45 +++++++++++++++++++ tests/rebar-bearing.test.ts | 34 +++++++++++++++ tests/rebar-common.test.ts | 33 ++++++++++++++ 4 files changed, 189 insertions(+) create mode 100644 src/core/rebar/bearing-block.ts create mode 100644 src/core/rebar/common.ts create mode 100644 tests/rebar-bearing.test.ts create mode 100644 tests/rebar-common.test.ts diff --git a/src/core/rebar/bearing-block.ts b/src/core/rebar/bearing-block.ts new file mode 100644 index 0000000..80f5002 --- /dev/null +++ b/src/core/rebar/bearing-block.ts @@ -0,0 +1,77 @@ +import type { PierModel } from '../geometry'; + +/** + * 받침블럭 철근 (spec §6.3 · plan C2), 배치 분리: one block per bearing, 1200 × 1200. + * S2 is the upper bar (cover 50 from the block top); S1 is the lower bar (69.1 below the top). + * Both have the same layout: 7@125 = 875 with ends of 162.5 in a 1200 mm block, and legs of 700 below the bar. + */ +export const BLOCK_REBAR = { + dim: 1200, + cover: 50, + firstBarGap: 75, + spacing: 125, + /** Straight part of the bar for the 1200 block (spec table: 1100 + 700 × 2). */ + straight: 1100, + legLength: 700, + /** Depth of each bar centre below the block top (mm). */ + depthS2: 50, + depthS1: 69.1, +} as const; + +export interface BarRow { + name: 'S1' | 'S2'; + /** Bars along one block side. */ + count: number; + spacing: number; + /** Position of the first bar from the block edge. */ + firstEnd: number; + /** Straight length plus two legs (mm). */ + length: number; + /** Coping embedment of the legs (mm): 700 − (block height − bar depth). */ + embedment: number; +} + +/** Bars along one 1200 mm side: available = dim − 2·(cover + first gap); gaps = floor(available / spacing). */ +export function barsAcross(dim = BLOCK_REBAR.dim): { count: number; spacing: number; firstEnd: number } { + const available = dim - 2 * (BLOCK_REBAR.cover + BLOCK_REBAR.firstBarGap); + const gaps = Math.floor(available / BLOCK_REBAR.spacing); + const span = gaps * BLOCK_REBAR.spacing; + const leftover = available - span; + return { count: gaps + 1, spacing: BLOCK_REBAR.spacing, firstEnd: BLOCK_REBAR.cover + BLOCK_REBAR.firstBarGap + leftover / 2 }; +} + +/** Rebar of one block; blockHeight is the bearing block height in mm (h1 front or h2 back, pad included). */ +export function blockBars(blockHeight: number, dim = BLOCK_REBAR.dim): BarRow[] { + const across = barsAcross(dim); + const length = BLOCK_REBAR.straight + 2 * BLOCK_REBAR.legLength; + return [ + { name: 'S1', ...across, length, embedment: BLOCK_REBAR.legLength - (blockHeight - BLOCK_REBAR.depthS1) }, + { name: 'S2', ...across, length, embedment: BLOCK_REBAR.legLength - (blockHeight - BLOCK_REBAR.depthS2) }, + ]; +} + +export interface BlockRebar { + no: number; + row: '전열' | '후열'; + bars: BarRow[]; +} + +/** Rebar of every block of a pier, using the model's block heights (h1 front, h2 back; pad included, Q16 결정). */ +export function bearingBlockRebar(model: PierModel): BlockRebar[] { + return model.blocks.map((b) => ({ + no: b.no, + row: b.row, + bars: blockBars(b.row === '전열' ? model.h1 * 1000 : model.h2 * 1000), + })); +} + +/** Total bars of one pier: bars per block × blocks, for S1 and S2 separately. */ +export function totals(rebar: BlockRebar[]): { S1: number; S2: number } { + return rebar.reduce( + (sum, blk) => { + for (const bar of blk.bars) sum[bar.name] += bar.count; + return sum; + }, + { S1: 0, S2: 0 }, + ); +} diff --git a/src/core/rebar/common.ts b/src/core/rebar/common.ts new file mode 100644 index 0000000..a249017 --- /dev/null +++ b/src/core/rebar/common.ts @@ -0,0 +1,45 @@ +/** + * ③ 철근배근 공통 규칙 (spec §6.2 · plan C1). Lengths in mm, stresses in MPa. + * fck and fy are inputs. The defaults below are assumed values and must be shown as such (I02). + */ +export const MATERIAL_DEFAULTS = { + fck: 27, + /** H = SD400, S = SD500. */ + fy: { H: 400, S: 500 } as const, + /** 경량 콘크리트 계수 λ (보통 콘크리트 1.0). */ + lambda: 1.0, + /** Marks that these are assumed values (정보 I02). */ + assumed: true, +} as const; + +/** Development length l_d = 0.6·d_b·f_y / (λ·√f_ck) (spec §4 공통 설계 규약, 보정계수 1.0). */ +export function developmentLength(db: number, fy: number, fck: number, lambda = MATERIAL_DEFAULTS.lambda): number { + if (db <= 0 || fy <= 0 || fck <= 0 || lambda <= 0) throw new Error('정착길이 계산 값은 모두 0보다 커야 합니다.'); + return (0.6 * db * fy) / (lambda * Math.sqrt(fck)); +} + +/** Lap splice length l_s = 1.3·l_d (spec §4 공통 설계 규약). */ +export function spliceLength(db: number, fy: number, fck: number, lambda = MATERIAL_DEFAULTS.lambda): number { + return 1.3 * developmentLength(db, fy, fck, lambda); +} + +export type HookKind = 'main90' | 'tie90' | 'stirrup135'; + +/** + * Standard hook lengths (KDS 14 20 52, spec §4). Refuses bar sizes outside the table rather than guessing. + * - main90: 주철근 90°, D25 이하 15.5 d_b, D29–D35 16.5 d_b + * - tie90: 띠철근 90°, D16 이하 8.5 d_b + * - stirrup135: 스터럽 135°, 2.5·d_b·(3π/4) + 6·d_b (spec formula, as written) + */ +export function hookLength(kind: HookKind, db: number): number { + if (kind === 'main90') { + if (db <= 25) return 15.5 * db; + if (db <= 35) return 16.5 * db; + throw new Error(`주철근 90° 갈고리 표에 없는 지름입니다 (D${db}).`); + } + if (kind === 'tie90') { + if (db <= 16) return 8.5 * db; + throw new Error(`띠철근 90° 갈고리 표에 없는 지름입니다 (D${db}).`); + } + return 2.5 * db * ((3 * Math.PI) / 4) + 6 * db; +} diff --git a/tests/rebar-bearing.test.ts b/tests/rebar-bearing.test.ts new file mode 100644 index 0000000..276e256 --- /dev/null +++ b/tests/rebar-bearing.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from 'vitest'; +import { buildPier } from '../src/core/geometry'; +import { barsAcross, bearingBlockRebar, blockBars, totals } from '../src/core/rebar/bearing-block'; +import { loadP1 } from './helpers'; + +describe('받침블럭 철근 (spec §6.3, plan C2)', () => { + it('a 1200 block takes 8 bars at 125 mm: 7 gaps = 875, ends 162.5 each', () => { + const a = barsAcross(1200); + expect(a.count).toBe(8); + expect(a.spacing).toBe(125); + expect((a.count - 1) * a.spacing).toBe(875); + expect(a.firstEnd).toBeCloseTo(162.5, 6); + }); + + it('each bar is 1100 + 700 × 2 = 2500 mm long', () => { + for (const bar of blockBars(150)) expect(bar.length).toBe(2500); + }); + + it('coping embedment: S2 600 (front) / 550 (back), S1 619.1 (front) / 569.1 (back), as in the spec', () => { + const front = blockBars(150); + const back = blockBars(200); + expect(front.find((b) => b.name === 'S2')!.embedment).toBeCloseTo(600, 6); + expect(back.find((b) => b.name === 'S2')!.embedment).toBeCloseTo(550, 6); + expect(front.find((b) => b.name === 'S1')!.embedment).toBeCloseTo(619.1, 6); + expect(back.find((b) => b.name === 'S1')!.embedment).toBeCloseTo(569.1, 6); + }); + + it('P1: 10 blocks, 80 bars each of S1 and S2 (spec table: 8 本 per block)', () => { + const m = buildPier(loadP1()); + const rebar = bearingBlockRebar(m); + expect(rebar).toHaveLength(10); + expect(totals(rebar)).toEqual({ S1: 80, S2: 80 }); + }); +}); diff --git a/tests/rebar-common.test.ts b/tests/rebar-common.test.ts new file mode 100644 index 0000000..f8aa2c1 --- /dev/null +++ b/tests/rebar-common.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from 'vitest'; +import { developmentLength, hookLength, MATERIAL_DEFAULTS, spliceLength } from '../src/core/rebar/common'; + +describe('③ 공통 규칙 (spec §6.2, plan C1)', () => { + it('fck and fy defaults are assumed values (I02)', () => { + expect(MATERIAL_DEFAULTS.fck).toBe(27); + expect(MATERIAL_DEFAULTS.fy.H).toBe(400); + expect(MATERIAL_DEFAULTS.fy.S).toBe(500); + expect(MATERIAL_DEFAULTS.assumed).toBe(true); + }); + + it('development length follows l_d = 0.6·d_b·fy/(λ·√fck)', () => { + // H19, SD400, fck 27: 0.6 × 19 × 400 / √27 + expect(developmentLength(19, 400, 27)).toBeCloseTo((0.6 * 19 * 400) / Math.sqrt(27), 9); + expect(developmentLength(19, 400, 27)).toBeCloseTo(877.6, 0); + }); + + it('splice length is 1.3 × l_d', () => { + expect(spliceLength(19, 400, 27)).toBeCloseTo(1.3 * developmentLength(19, 400, 27), 9); + }); + + it('hook lengths: main bars D25 or less 15.5 d_b, D29–D35 16.5 d_b; ties D16 or less 8.5 d_b', () => { + expect(hookLength('main90', 25)).toBeCloseTo(15.5 * 25, 9); + expect(hookLength('main90', 29)).toBeCloseTo(16.5 * 29, 9); + expect(hookLength('tie90', 16)).toBeCloseTo(8.5 * 16, 9); + expect(() => hookLength('main90', 40)).toThrow('없는 지름'); + expect(() => hookLength('tie90', 19)).toThrow('없는 지름'); + }); + + it('135° stirrup hook as written in the spec: 2.5·d_b·(3π/4) + 6·d_b', () => { + expect(hookLength('stirrup135', 16)).toBeCloseTo(2.5 * 16 * ((3 * Math.PI) / 4) + 6 * 16, 9); + }); +});