From b962cf1a1c33cc9b9a17370feed7eaba86bf5d39 Mon Sep 17 00:00:00 2001 From: ypshim Date: Thu, 22 Oct 2026 11:08:27 +0900 Subject: [PATCH] =?UTF-8?q?feat(core):=20bearing-block=20drawing=20check?= =?UTF-8?q?=20and=20coping=20rebar=20C3=20(plan=20C2=C2=B7C3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Drawing check (spec §6.3): an integrated 2500 × 1200 block gives 18@125 = 2250 along (ends 50 + 75) and 7@125 = 875 across (ends 162.5). - C3 coping rebar, from RAG/코핑철근.md (2026-10-22 final): - C10: 9 bars @300 at 4800–7200 (k_max 4, beyond the Level edge). - C9 / C11: 18 bars each @250 outward, ends at 300 and 11700 (end gap 300). - Refuses a coping too short for C9 (RB-K01). - Side bars C12 5 rows / C13 2 rows at the 42.538 break; C14 38 bars; C15 11 bars, C16 22 bars. - spec §6.4 is older than 코핑철근.md (C10/C9/C11 layout, C12 row count, C14 dimensions). This follows the final document; spec §6.4 needs an update. - Not yet: C1–C8 main bars (shape and lengths), the C12/C13 bar lengths and shapes, the drawing check of the coping sheet, and the ③ screen. Co-Authored-By: Claude Haiku 5.5 --- src/core/rebar/bearing-block.ts | 4 +- src/core/rebar/coping.ts | 73 +++++++++++++++++++++++++++++++++ tests/rebar-bearing.test.ts | 15 +++++++ tests/rebar-coping.test.ts | 53 ++++++++++++++++++++++++ 4 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 src/core/rebar/coping.ts create mode 100644 tests/rebar-coping.test.ts diff --git a/src/core/rebar/bearing-block.ts b/src/core/rebar/bearing-block.ts index 80f5002..790db49 100644 --- a/src/core/rebar/bearing-block.ts +++ b/src/core/rebar/bearing-block.ts @@ -32,7 +32,7 @@ export interface BarRow { } /** 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 } { +export function barsAcross(dim: number = 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; @@ -41,7 +41,7 @@ export function barsAcross(dim = BLOCK_REBAR.dim): { count: number; spacing: num } /** 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[] { +export function blockBars(blockHeight: number, dim: number = BLOCK_REBAR.dim): BarRow[] { const across = barsAcross(dim); const length = BLOCK_REBAR.straight + 2 * BLOCK_REBAR.legLength; return [ diff --git a/src/core/rebar/coping.ts b/src/core/rebar/coping.ts new file mode 100644 index 0000000..8d23ac1 --- /dev/null +++ b/src/core/rebar/coping.ts @@ -0,0 +1,73 @@ +/** + * 코핑 철근 (plan C3). Source: RAG/코핑철근.md, 2026-10-22 최종 정리 (spec §6.4 is older and is not used here). + * Positions are along the coping length from its start, in mm. + */ +export const COPING_REBAR = { + /** 피복 (최외측 철근 중심까지). */ + cover: 100, + /** 첫 전단철근 최소 여유 e_add. 끝 여유 e_min = cover + e_add = 300. */ + eAdd: 200, + /** Level 폭 W_L. */ + levelWidth: 2100, + /** C10 간격 s_L (기둥 중심부터). */ + spacingL: 300, + /** C9·C11 간격 s_s (C10에서 바깥으로). */ + spacingS: 250, + /** C12·C13 측면 수평 간격. */ + spacingSide: 250, + /** C14 위 2줄 간격 / 아래 줄 수평 간격 / 아래 줄 연직 간격 / 끝 여유. */ + c14: { upperSpacing: 1000, lowerSpacing: 2000, verticalSpacing: 500, endGap: 500 }, +} as const; + +export const COPING_END_GAP = COPING_REBAR.cover + COPING_REBAR.eAdd; + +export interface ShearBars { + /** C10 positions (mm), from the first to the last. */ + c10: number[]; + /** C9 positions (mm), from the left end toward the column. */ + c9: number[]; + /** C11 positions (mm), from the column toward the right end. */ + c11: number[]; + /** Distance from the coping end to the outermost C9 / C11 bar (mm). */ + endGap: number; +} + +/** + * Shear bars C9–C11 along the axis (코핑철근.md §3). A single column at the coping centre. + * C10: @300 from the column centre, out to the first position beyond the Level edge (k_max). + * C9 / C11: @250 outward from the outermost C10, as many as fit with the end gap of at least 300. + */ +export function shearBars(coping: number, xc = coping / 2): ShearBars { + const { spacingL, spacingS, levelWidth } = COPING_REBAR; + const kMax = Math.floor(levelWidth / 2 / spacingL) + 1; + const c10 = Array.from({ length: 2 * kMax + 1 }, (_, i) => Math.round(xc + (i - kMax) * spacingL)); + const left = c10[0]; + const right = c10[c10.length - 1]; + const n9 = Math.floor((left - COPING_END_GAP) / spacingS); + if (n9 < 0) { + throw new Error('RB-K01: C10 바깥 구간이 끝 여유보다 짧아 C9를 놓을 수 없습니다.'); + } + const c9 = Array.from({ length: n9 }, (_, j) => Math.round(left - (n9 - j) * spacingS)); + const c11 = Array.from({ length: n9 }, (_, j) => Math.round(right + (j + 1) * spacingS)); + const endGap = Math.min(left - c9[0], coping - c11[c11.length - 1]); + return { c10, c9, c11, endGap: endGap }; +} + +/** + * Side horizontal bars (코핑철근.md §6): seven rows from the bottom at 250 mm. + * Rows below the sloped-bottom break (z_7,end) are C12, the rows at and above it are C13. + */ +export function sideRows(zBottom: number, zBreak: number, rows = 7): { c12: number[]; c13: number[] } { + const z = Array.from({ length: rows }, (_, i) => Math.round((zBottom + i * COPING_REBAR.spacingSide / 1000) * 1000) / 1000); + return { c12: z.filter((v) => v < zBreak - 1e-9), c13: z.filter((v) => v >= zBreak - 1e-9) }; +} + +/** C14 counts (코핑철근.md §8): 2 upper lines of 11 plus the lower rows 5 / 6 / 3 / 2 = 38. */ +export const C14_COUNTS = { upperLines: 2, upperPerLine: 11, lower: [5, 6, 3, 2] as const }; + +export function c14Total(): number { + return C14_COUNTS.upperLines * C14_COUNTS.upperPerLine + C14_COUNTS.lower.reduce((a, b) => a + b, 0); +} + +/** C15 (거꾸로 된 U, 가장 높은 계단) and C16 (L형, 2구간) counts (코핑철근.md §7). */ +export const STEP_BAR_COUNTS = { C15: 11, C16: 22 } as const; diff --git a/tests/rebar-bearing.test.ts b/tests/rebar-bearing.test.ts index 276e256..21f3a2c 100644 --- a/tests/rebar-bearing.test.ts +++ b/tests/rebar-bearing.test.ts @@ -32,3 +32,18 @@ describe('받침블럭 철근 (spec §6.3, plan C2)', () => { expect(totals(rebar)).toEqual({ S1: 80, S2: 80 }); }); }); + +describe('받침블럭 도면 재현 (spec §6.3)', () => { + it('integrated block 2500 × 1200: 18@125 = 2250 along (ends 50 + 75), 7@125 = 875 across (ends 50 + 113 ≈ 162.5)', () => { + const along = barsAcross(2500); + expect(along.spacing).toBe(125); + expect(along.count - 1).toBe(18); + expect((along.count - 1) * along.spacing).toBe(2250); + expect(along.firstEnd).toBeCloseTo(125, 6); + + const across = barsAcross(1200); + expect(across.count - 1).toBe(7); + expect((across.count - 1) * across.spacing).toBe(875); + expect(across.firstEnd).toBeCloseTo(162.5, 6); // 50 + 75 + 37.5, printed as 50 + 113 in the drawing + }); +}); diff --git a/tests/rebar-coping.test.ts b/tests/rebar-coping.test.ts new file mode 100644 index 0000000..e4d8ac0 --- /dev/null +++ b/tests/rebar-coping.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from 'vitest'; +import { c14Total, COPING_END_GAP, sideRows, shearBars, STEP_BAR_COUNTS } from '../src/core/rebar/coping'; + +// P1 coping: L 12000, one column at the centre (T형). +const L = 12000; + +describe('코핑 철근 (코핑철근.md, plan C3)', () => { + it('end gap is cover + e_add = 300', () => { + expect(COPING_END_GAP).toBe(300); + }); + + it('C10: 9 bars @300 from 4800 to 7200 (k_max 4, beyond the Level edge at 4950 / 7050)', () => { + const { c10 } = shearBars(L); + expect(c10).toHaveLength(9); + expect(c10[0]).toBe(4800); + expect(c10[c10.length - 1]).toBe(7200); + expect(c10[1] - c10[0]).toBe(300); + }); + + it('C9 and C11: 18 bars each @250 outward, first at 4550 / 7450, last at 300 / 11700', () => { + const b = shearBars(L); + expect(b.c9).toHaveLength(18); + expect(b.c11).toHaveLength(18); + expect(b.c9[b.c9.length - 1]).toBe(4550); + expect(b.c9[0]).toBe(300); + expect(b.c11[0]).toBe(7450); + expect(b.c11[b.c11.length - 1]).toBe(11700); + expect(b.endGap).toBe(300); + }); + + it('total shear bars per face: 9 + 18 + 18 = 45 positions', () => { + const b = shearBars(L); + expect(b.c10.length + b.c9.length + b.c11.length).toBe(45); + }); + + it('a coping too short for C9 is refused (RB-K01)', () => { + expect(() => shearBars(2200)).toThrow('RB-K01'); + }); + + it('C12 has 5 rows below the break at 42.538, C13 has 2 rows at and above it', () => { + const r = sideRows(41.288, 42.538); + expect(r.c12).toEqual([41.288, 41.538, 41.788, 42.038, 42.288]); + expect(r.c13).toEqual([42.538, 42.788]); + }); + + it('C14: 38 bars = 2 × 11 upper + 5 + 6 + 3 + 2 lower', () => { + expect(c14Total()).toBe(38); + }); + + it('C15 11 bars, C16 22 bars', () => { + expect(STEP_BAR_COUNTS).toEqual({ C15: 11, C16: 22 }); + }); +});