From 5641bbe2659d0936046338f421289cc0028a6ab9 Mon Sep 17 00:00:00 2001 From: ypshim Date: Thu, 22 Oct 2026 11:37:08 +0900 Subject: [PATCH] =?UTF-8?q?feat(core):=20lengths=20for=20coping,=20column?= =?UTF-8?q?=20and=20the=20B2=20split=20(=EC=B6=94=EC=B2=9C=EC=95=88)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Coping: C9/C10 lower U = 1740 + 2h with h linear along the slope and 2300 on the Level (3522 / 6098 / 6249 / 6340 from the document); C12 (3717–10183), C13 11746, C14 3447.5, C15 3852, C16 4070. - Column: P3 = C_t + 1.3·l_d(H19) lap (추천안 for Q-C6, lap 1146.6), P5 = C_t. - Footing: B2 13646 is split into two pieces at a splice with 1.3·l_d(S25, SD500) (추천안 for Q-B6), piece ≈ 7776 each. - Not done: C9/C11 cap (삿갓) lengths (Q-K5 open), P1/P2 per-line pieces. - Tests against the document values; P1 summary updated. - Not verified in a browser. Co-Authored-By: Claude Haiku 5.5 --- src/core/rebar/column.ts | 10 +++++- src/core/rebar/common.ts | 10 ++++++ src/core/rebar/coping.ts | 24 +++++++++++++++ src/core/rebar/summary.ts | 49 ++++++++++++++++-------------- tests/rebar-coping-lengths.test.ts | 24 +++++++++++++++ tests/rebar-summary.test.ts | 12 +++++++- 6 files changed, 105 insertions(+), 24 deletions(-) create mode 100644 tests/rebar-coping-lengths.test.ts diff --git a/src/core/rebar/column.ts b/src/core/rebar/column.ts index 4423456..e503dfd 100644 --- a/src/core/rebar/column.ts +++ b/src/core/rebar/column.ts @@ -1,4 +1,4 @@ -import { developmentLength, hookLength, NOMINAL_DIAMETER } from './common'; +import { developmentLength, hookLength, NOMINAL_DIAMETER, spliceLength } from './common'; /** * 기둥 철근 C4 (plan C4). Source: RAG/기둥철근 (1).md, 2026-10-22 최종 정리. Type-1 원형 중실 기둥. @@ -93,7 +93,15 @@ export function columnRebar(x: ColumnInput = COLUMN_P1) { const p6Hook = hookLength('tie90', dH16); const p6Length = p6Straight + 2 * p6Hook; + // P3 lap: Q-C6 is open; recommended = 1.3·l_d of H19 (fck 27, fy 400). + const lapP3 = spliceLength(dH19, 400, 27); + const p3Length = 2 * Math.PI * tieRadius + lapP3; + const p5Length = 2 * Math.PI * tieRadius; + return { + p3Length: round(p3Length, 1), + p5Length: round(p5Length, 1), + lapP3: round(lapP3, 1), r1: round(r1), r2: round(r2), angleStepDeg: 360 / x.nDiv, diff --git a/src/core/rebar/common.ts b/src/core/rebar/common.ts index cddb6bc..f27ba16 100644 --- a/src/core/rebar/common.ts +++ b/src/core/rebar/common.ts @@ -62,3 +62,13 @@ export function hookLength(kind: HookKind, db: number): number { } return 2.5 * db * ((3 * Math.PI) / 4) + 6 * db; } + +/** + * Splice of a long bar into two pieces (추천안 for Q-B6): each piece is half the total plus half the lap, + * and the lap is l_s = 1.3·l_d. Used only when the bar is longer than the standard bar length. + */ +export function splitPieces(total: number, db: number, fy: number, fck: number, standard: number): { pieces: number; pieceLength: number; lap: number } | null { + if (total <= standard) return null; + const lap = spliceLength(db, fy, fck); + return { pieces: 2, pieceLength: total / 2 + lap / 2, lap }; +} diff --git a/src/core/rebar/coping.ts b/src/core/rebar/coping.ts index 8d23ac1..b4e523d 100644 --- a/src/core/rebar/coping.ts +++ b/src/core/rebar/coping.ts @@ -71,3 +71,27 @@ export function c14Total(): number { /** C15 (거꾸로 된 U, 가장 높은 계단) and C16 (L형, 2구간) counts (코핑철근.md §7). */ export const STEP_BAR_COUNTS = { C15: 11, C16: 22 } as const; + +/** + * C9·C10 lower U length (코핑철근.md §4.3): 1740 + 2h. h rises linearly along the slope between the document's + * ends (x 300 → h 890.9, x 4550 → h 2178.8) and stays at the Level height 2300 from the Level edge (4950) on. + * The bar's upper cap (삿갓) length depends on l_cap, which is still open (Q-K5), so only the U is given here. + */ +export const SHEAR_U = { x0: 300, h0: 890.9, x1: 4550, h1: 2178.8, levelH: 2300, levelEdgeFromCentre: 1050, flat: 1740 } as const; + +export function shearUHeight(x: number, coping: number): number { + const xs = Math.min(x, coping - x); // mirror to the left half + const slope = (SHEAR_U.h1 - SHEAR_U.h0) / (SHEAR_U.x1 - SHEAR_U.x0); + const h = SHEAR_U.h0 + (xs - SHEAR_U.x0) * slope; + return Math.min(h, SHEAR_U.levelH); +} + +export function shearULength(x: number, coping: number): number { + return SHEAR_U.flat + 2 * shearUHeight(x, coping); +} + +/** C12 straight lengths per row (코핑철근.md §6 table, mm) and C13. */ +export const SIDE_LENGTHS = { C12: [3717, 5333, 6950, 8567, 10183], C13: 11746 } as const; + +/** C15 (거꾸로 된 U, 3852) and C16 (L형, 4070 / 4069) lengths, mm (코핑철근.md §7). */ +export const STEP_LENGTHS = { C15: 3852, C16: [4070, 4069] as const, C14: 3447.5 } as const; diff --git a/src/core/rebar/summary.ts b/src/core/rebar/summary.ts index 65417cd..e9f5b18 100644 --- a/src/core/rebar/summary.ts +++ b/src/core/rebar/summary.ts @@ -1,9 +1,10 @@ import type { PierModel } from '../geometry'; import { bearingBlockRebar, totals } from './bearing-block'; import { bottomPiece, topBarLength, widthLayout } from './coping-main'; -import { shearBars, sideRows, C14_COUNTS, c14Total, STEP_BAR_COUNTS } from './coping'; +import { shearBars, sideRows, C14_COUNTS, c14Total, STEP_BAR_COUNTS, shearULength, SIDE_LENGTHS, STEP_LENGTHS } from './coping'; import { columnRebar, COLUMN_P1 } from './column'; import { bottomBars, crossBars, sideBars, assemblyBars, shearB9 } from './footing'; +import { splitPieces } from './common'; /** One line of a 철근표. length is null where the document gives no length yet. */ export interface RebarRow { @@ -49,23 +50,27 @@ export function rebarSummary(model: PierModel): { rows: RebarRow[]; warnings: Re rows.push({ member: 'coping', name: 'C7', size: 'H29', count: perRow, length: bottom.piece, note: '하면, 2조각 겹침이음' }); rows.push({ member: 'coping', name: 'C8', size: 'H29', count: perRow, length: bottom.piece, note: '하면, 2조각 겹침이음' }); const shear = shearBars(L); - rows.push({ member: 'coping', name: 'C9', size: 'H25', count: shear.c9.length, length: null, note: '전단 좌 @250' }); - rows.push({ member: 'coping', name: 'C10', size: 'H25', count: shear.c10.length, length: null, note: '전단 Level @300' }); - rows.push({ member: 'coping', name: 'C11', size: 'H25', count: shear.c11.length, length: null, note: '전단 우 @250' }); + const uRange = (xs: number[]) => { + const u = xs.map((x) => Math.round(shearULength(x, L))); + return `${Math.min(...u)}–${Math.max(...u)}`; + }; + rows.push({ member: 'coping', name: 'C9', size: 'H25', count: shear.c9.length, length: null, note: `전단 좌 @250, 하부 U ${uRange(shear.c9)} (삿갓 l_cap 미정)` }); + rows.push({ member: 'coping', name: 'C10', size: 'H25', count: shear.c10.length, length: null, note: `전단 Level @300, 하부 U ${uRange(shear.c10)} (삿갓 l_cap 미정)` }); + rows.push({ member: 'coping', name: 'C11', size: 'H25', count: shear.c11.length, length: null, note: `전단 우 @250, 하부 U ${uRange(shear.c11)} (삿갓 l_cap 미정)` }); const side = sideRows(41.288, 42.538); - rows.push({ member: 'coping', name: 'C12', size: 'H25', count: side.c12.length, length: null, note: '측면, 경사 하면에서 끊김' }); - rows.push({ member: 'coping', name: 'C13', size: 'H25', count: side.c13.length, length: null, note: '측면, 끝면까지' }); - rows.push({ member: 'coping', name: 'C14', size: 'H19', count: c14Total(), length: null, note: `직각 수평 ${C14_COUNTS.upperLines * C14_COUNTS.upperPerLine}+${C14_COUNTS.lower.join('+')}` }); - rows.push({ member: 'coping', name: 'C15', size: 'H19', count: STEP_BAR_COUNTS.C15, length: null, note: '계단부, 가장 높은 구간' }); - rows.push({ member: 'coping', name: 'C16', size: 'H19', count: STEP_BAR_COUNTS.C16, length: null, note: '계단부, 3·4구간' }); + rows.push({ member: 'coping', name: 'C12', size: 'H25', count: side.c12.length, length: null, note: `측면, 경사 하면에서 끊김, 일자 ${SIDE_LENGTHS.C12[0]}–${SIDE_LENGTHS.C12[4]}` }); + rows.push({ member: 'coping', name: 'C13', size: 'H25', count: side.c13.length, length: SIDE_LENGTHS.C13, note: '측면, 끝면까지' }); + rows.push({ member: 'coping', name: 'C14', size: 'H19', count: c14Total(), length: STEP_LENGTHS.C14, note: `직각 수평 ${C14_COUNTS.upperLines * C14_COUNTS.upperPerLine}+${C14_COUNTS.lower.join('+')}` }); + rows.push({ member: 'coping', name: 'C15', size: 'H19', count: STEP_BAR_COUNTS.C15, length: STEP_LENGTHS.C15, note: '계단부, 가장 높은 구간 (거꾸로 U)' }); + rows.push({ member: 'coping', name: 'C16', size: 'H19', count: STEP_BAR_COUNTS.C16, length: STEP_LENGTHS.C16[0], note: `계단부, 3·4구간 (${STEP_LENGTHS.C16[0]} / ${STEP_LENGTHS.C16[1]})` }); // Column (기둥철근.md · plan C4), P1 defaults const col = columnRebar(COLUMN_P1); rows.push({ member: 'column', name: 'P1', size: 'S32', count: col.mainP1, length: null, note: '외측 r 875, 커플러로 이음' }); rows.push({ member: 'column', name: 'P2', size: 'S32', count: col.mainP2, length: null, note: '내측 r 843.2, 커플러로 이음' }); rows.push({ member: 'column', name: '커플러', size: 'L150 Ø60', count: col.couplers, length: 150, note: '@3000, P2 +150' }); - rows.push({ member: 'column', name: 'P3', size: 'H19', count: col.p3Count, length: null, note: '일반 구간 띠철근 @300' }); - rows.push({ member: 'column', name: 'P5', size: 'H19', count: col.p5Count, length: null, note: '단부 띠철근 @120' }); + rows.push({ member: 'column', name: 'P3', size: 'H19', count: col.p3Count, length: col.p3Length, note: `일반 구간 띠철근 @300, 겹침 ${col.lapP3} (추천안, Q-C6)` }); + rows.push({ member: 'column', name: 'P5', size: 'H19', count: col.p5Count, length: col.p5Length, note: '단부 띠철근 @120, 커플러 1개' }); rows.push({ member: 'column', name: 'P4', size: 'H19', count: col.p4Count, length: col.p4SemicircleLength * 2, note: '확대기초 안, 반원 2개 (길이는 반원 2개 합) · B11 간섭 무시' }); rows.push({ member: 'column', name: 'P6', size: 'H16', count: col.p6Count, length: col.p6Length, note: '크로스타이, 8단 × 4' }); @@ -75,18 +80,18 @@ export function rebarSummary(model: PierModel): { rows: RebarRow[]; warnings: Re const Lf = f.length * 1000; const Wf = f.width * 1000; const Hf = (f.top - f.bottom) * 1000; - for (const b of bottomBars(Lf, Wf, Hf)) { - rows.push({ member: 'footing', name: b.name, size: b.size, count: b.count, length: b.length, note: '교각축' }); - if (b.length > STANDARD_BAR) { - warnings.push({ code: 'RB-F08', message: `${b.name} 길이 ${Math.round(b.length)} mm가 정척 ${STANDARD_BAR} mm를 넘어 이음이 필요합니다 (Q-B6).` }); + // Bars longer than the standard length are split in two (추천안, Q-B6). The count is still the bar count. + const addBar = (b: { name: string; size: string; count: number; length: number }, note: string) => { + const split = splitPieces(b.length, 25.4, 500, 27, STANDARD_BAR); + if (split) { + rows.push({ member: 'footing', name: b.name, size: b.size, count: b.count, length: split.pieceLength, note: `${note}, 2조각 이음 (추천안, 겹침 ${Math.round(split.lap)})` }); + warnings.push({ code: 'RB-F08', message: `${b.name} 길이 ${Math.round(b.length)} mm > 정척 ${STANDARD_BAR} mm: 2조각 이음을 추천안으로 적용 (Q-B6).` }); + } else { + rows.push({ member: 'footing', name: b.name, size: b.size, count: b.count, length: b.length, note }); } - } - for (const b of crossBars(Lf, Wf, Hf)) { - rows.push({ member: 'footing', name: b.name, size: b.size, count: b.count, length: b.length, note: '교각축 직각' }); - if (b.length > STANDARD_BAR) { - warnings.push({ code: 'RB-F08', message: `${b.name} 길이 ${Math.round(b.length)} mm가 정척 ${STANDARD_BAR} mm를 넘어 이음이 필요합니다 (Q-B6).` }); - } - } + }; + for (const b of bottomBars(Lf, Wf, Hf)) addBar(b, '교각축'); + for (const b of crossBars(Lf, Wf, Hf)) addBar(b, '교각축 직각'); for (const b of sideBars(Lf, Wf)) rows.push({ member: 'footing', name: b.name, size: b.size, count: b.count, length: b.length, note: '측면 수평' }); for (const b of assemblyBars(Lf, Wf)) rows.push({ member: 'footing', name: b.name, size: b.size, count: b.count, length: b.length, note: '조립 수평' }); const b9 = shearB9(Lf, Wf, Hf); diff --git a/tests/rebar-coping-lengths.test.ts b/tests/rebar-coping-lengths.test.ts new file mode 100644 index 0000000..6ac7265 --- /dev/null +++ b/tests/rebar-coping-lengths.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from 'vitest'; +import { shearULength, shearUHeight } from '../src/core/rebar/coping'; + +// Coping 12000 (코핑철근.md §4.3 · §12): lower U = 1740 + 2h. +const L = 12000; + +describe('C9·C10 하부 U 길이 (코핑철근.md §4.3)', () => { + it('C9 first (x 300) 3522, C9 last (x 4550) 6098, C10 outer (x 4800) 6249, Level (x 6000) 6340', () => { + expect(shearULength(300, L)).toBeCloseTo(3522, 0); + expect(shearULength(4550, L)).toBeCloseTo(6098, 0); + expect(shearULength(4800, L)).toBeCloseTo(6249, 0); + expect(shearULength(6000, L)).toBeCloseTo(6340, 0); + }); + + it('heights h: 890.9 at the end, 2178.8 at the last C9, 2300 on the Level', () => { + expect(shearUHeight(300, L)).toBeCloseTo(890.9, 1); + expect(shearUHeight(4550, L)).toBeCloseTo(2178.8, 1); + expect(shearUHeight(6000, L)).toBe(2300); + }); + + it('mirrors on the right half', () => { + expect(shearULength(11700, L)).toBeCloseTo(3522, 0); + }); +}); diff --git a/tests/rebar-summary.test.ts b/tests/rebar-summary.test.ts index 9fcc4fc..20d316c 100644 --- a/tests/rebar-summary.test.ts +++ b/tests/rebar-summary.test.ts @@ -46,6 +46,16 @@ describe('③ 철근표 요약 (P1)', () => { it('B9: 320 bars; warnings: B2 needs a splice (13646 > 12000)', () => { expect(count('B9')).toBe(320); expect(warnings.map((w) => w.code)).toEqual(['RB-F08']); - expect(rows.find((r) => r.name === 'B2')!.length!).toBeGreaterThan(12000); + // B2 is split into two pieces (추천안): each piece is about half the 13646 bar plus half the lap. + expect(rows.find((r) => r.name === 'B2')!.length!).toBeCloseTo(7776, 0); + }); + + it('lengths from the documents: C13 11746, C14 3447.5, C15 3852, C16 4070, P3 ≈ C_t + 1.3·l_d, P5 ≈ C_t', () => { + expect(rows.find((r) => r.name === 'C13')!.length).toBe(11746); + expect(rows.find((r) => r.name === 'C14')!.length).toBe(3447.5); + expect(rows.find((r) => r.name === 'C15')!.length).toBe(3852); + expect(rows.find((r) => r.name === 'C16')!.length).toBe(4070); + expect(rows.find((r) => r.name === 'P5')!.length).toBeCloseTo(5657.7, 0); + expect(rows.find((r) => r.name === 'P3')!.length!).toBeCloseTo(5657.7 + 1146.6, 0); }); });