From ed847614494138e96630afe0cddac03adb146f32 Mon Sep 17 00:00:00 2001 From: ypshim Date: Tue, 10 Nov 2026 17:28:18 +0900 Subject: [PATCH] =?UTF-8?q?fix(rebar):=20=ED=99=95=EB=8C=80=EA=B8=B0?= =?UTF-8?q?=EC=B4=88=20=EC=B2=A0=EA=B7=BC=20=E2=80=94=20=EA=B5=AC=EA=B0=84?= =?UTF-8?q?=EB=B3=84=203D=20=EC=9C=84=EC=B9=98,=20=EA=B8=B0=EB=91=A5=20?= =?UTF-8?q?=EC=86=8C=EC=86=8D,=20=EA=B8=B8=EC=9D=B4=EB=B3=84=20=ED=96=89,?= =?UTF-8?q?=202=EC=A1=B0=EA=B0=81=20=EC=88=98=EB=9F=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 3D: 확대기초 철근을 늘 구간 0에 그려, 꺾인 교각의 구간 1 기초(산외교 P11 ③)는 철근이 없고 구간 0 기초에 겹쳐 그려졌다 - 기초에 속한 기둥을 같은 구간 안에서만 찾는다 (B9 기둥 구역이 다른 구간 기둥으로 잘못 빠지던 것) - 길이가 다른 기초의 철근을 가장 긴 길이 하나로 합치지 않고 길이별 조각으로 둔다 - 정척 초과 2조각 이음 행이 철근 한 개당 한 조각만 중량에 넣던 것을 두 조각으로 (P1 B6 31개 → 62조각) Co-Authored-By: Claude Haiku 5.5 --- src/core/rebar/summary.ts | 37 +++++++++++++++++++-------------- src/view/rebar3d.ts | 12 +++++------ tests/kink-steps.test.ts | 43 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 21 deletions(-) diff --git a/src/core/rebar/summary.ts b/src/core/rebar/summary.ts index 0399c94..edd953b 100644 --- a/src/core/rebar/summary.ts +++ b/src/core/rebar/summary.ts @@ -391,27 +391,34 @@ export function rebarSummary( if (model.footings.length > 0) { // Each footing (분리 or 일체) gives the same bar set. Several footings add their counts per bar name; // the length is the longest of them (the largest footing governs the bar). - const agg = new Map(); + // Per footing (2026-11-10): the columns of its own stretch only, and every bar length kept (a bent pier has footings + // of different lengths; the longest no longer stands for all). + const byName = new Map }>(); for (const f of model.footings) { // Columns on this footing, as x mm from the footing centre (for the B9 column-zone rule). - const columnX = model.columns.filter((c) => Math.abs(c.s - f.s) <= f.length / 2).map((c) => (c.s - f.s) * 1000); + const columnX = model.columns.filter((c) => c.k === f.k && Math.abs(c.s - f.s) <= f.length / 2).map((c) => (c.s - f.s) * 1000); for (const b of footingBarList(f.length * 1000, f.width * 1000, (f.top - f.bottom) * 1000, columnX, model.footingRebar)) { - const cur = agg.get(b.name); - if (cur) { - cur.count += b.count; - cur.length = Math.max(cur.length, b.length); - } else { - agg.set(b.name, { ...b }); - } + const cur = byName.get(b.name) ?? { first: b, lengths: new Map() }; + cur.lengths.set(b.length, (cur.lengths.get(b.length) ?? 0) + b.count); + byName.set(b.name, cur); } } - for (const b of agg.values()) { - const split = splitPieces(b.length, 25.4, fyOf(mFooting, b.size), mFooting.fck, STANDARD_BAR); - if (split && b.length > STANDARD_BAR && b.name !== 'B9') { - rows.push({ member: 'footing', name: b.name, size: b.size, count: b.count, length: split.pieceLength, note: `${b.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).`, target: 'footing', bar: b.name }); + for (const { first: b, lengths } of byName.values()) { + // Over 정척: two pieces lapped (추천안, Q-B6); both pieces count (the row used to weigh only one per bar). + const pieces: { length: number; count: number }[] = []; + let lap: number | null = null; + for (const [length, count] of lengths) { + const split = b.name === 'B9' ? null : splitPieces(length, 25.4, fyOf(mFooting, b.size), mFooting.fck, STANDARD_BAR); + if (split) { pieces.push({ length: split.pieceLength, count: 2 * count }); lap = split.lap; } else pieces.push({ length, count }); + } + const count = [...lengths.values()].reduce((sum, n) => sum + n, 0); + const longest = Math.max(...lengths.keys()); + if (lap !== null) { + rows.push({ member: 'footing', name: b.name, size: b.size, count, length: pieces.length === 1 ? pieces[0].length : null, pieces, + note: `${b.note}, 2조각 이음 (추천안, 겹침 ${Math.round(lap)})` }); + warnings.push({ code: 'RB-F08', message: `${b.name} 길이 ${Math.round(longest)} mm > 정척 ${STANDARD_BAR} mm: 2조각 이음을 추천안으로 적용 (Q-B6).`, target: 'footing', bar: b.name }); } else { - rows.push({ member: 'footing', name: b.name, size: b.size, count: b.count, length: b.length, note: b.note }); + rows.push({ member: 'footing', name: b.name, size: b.size, count, length: pieces.length === 1 ? pieces[0].length : null, ...(pieces.length > 1 ? { pieces } : {}), note: b.note }); } } } else { diff --git a/src/view/rebar3d.ts b/src/view/rebar3d.ts index 323ec5e..33f5d6a 100644 --- a/src/view/rebar3d.ts +++ b/src/view/rebar3d.ts @@ -452,7 +452,7 @@ export function buildRebar3D(m: PierModel, common: Material = DEFAULT_MATERIAL, const off = legOffsets(); // One name, several cylinders: each segment of the path is one bar piece. const path = (name: string, pts: [number, number, number][], dMm: number) => { - addPath(name, 0, pts, dMm, COLOR.footing); + addPath(name, f.k, pts, dMm, COLOR.footing); }; // B1–B4: along s, at W position n. legTo (absolute EL, m) is where both ends turn up or down (§5.1); undefined = straight. const along = (name: string, n: number, y: number, legTo: number | undefined, dMm: number) => { @@ -492,21 +492,21 @@ export function buildRebar3D(m: PierModel, common: Material = DEFAULT_MATERIAL, // Each hook tail (6·d) turns toward the footing centre along the pier axis (Q-B19). const dB9 = NOMINAL_DIAMETER.H16; const tail9 = (6 * dB9) / 1000; - const colX = m.columns.filter((c) => Math.abs(c.s - f.s) <= Lf / 2).map((c) => (c.s - f.s) * 1000); + const colX = m.columns.filter((c) => c.k === f.k && Math.abs(c.s - f.s) <= Lf / 2).map((c) => (c.s - f.s) * 1000); const yB9Bot = bottom + mm(z.B24); const yB9Top = bottom + mm(z.B5); for (const b of b9Bars(Lf * 1000, Wf * 1000, colX, 2000, fs).bars) { const x = mm(b.x); const n = mm(b.n); const tailDir = x >= 0 ? -1 : 1; - addBar('B9', 0, [f.s + x, yB9Bot, n], [f.s + x, yB9Top, n], dB9, COLOR.footing); - addBar('B9', 0, [f.s + x, yB9Top, n], [f.s + x + tailDir * tail9, yB9Top, n], dB9, COLOR.footing); - addBar('B9', 0, [f.s + x, yB9Bot, n], [f.s + x + tailDir * tail9, yB9Bot, n], dB9, COLOR.footing); + addBar('B9', f.k, [f.s + x, yB9Bot, n], [f.s + x, yB9Top, n], dB9, COLOR.footing); + addBar('B9', f.k, [f.s + x, yB9Top, n], [f.s + x + tailDir * tail9, yB9Top, n], dB9, COLOR.footing); + addBar('B9', f.k, [f.s + x, yB9Bot, n], [f.s + x + tailDir * tail9, yB9Bot, n], dB9, COLOR.footing); } // B10-1 / B10-2 (§7) at each B10 level, B11 / B12 (§8) between them. Ends turn 90° inward (B10) or down (B11, B12). const polyline = (name: string, pts: [number, number, number][], dMm: number) => { - addPath(name, 0, pts, dMm, COLOR.footing); + addPath(name, f.k, pts, dMm, COLOR.footing); }; const hookB10 = mm(hookLength('main90', NOMINAL_DIAMETER.S19)); // 296 const hookAsm = mm(Math.round(hookLength('main90', NOMINAL_DIAMETER.S16))); // 246 diff --git a/tests/kink-steps.test.ts b/tests/kink-steps.test.ts index 109d882..9496e68 100644 --- a/tests/kink-steps.test.ts +++ b/tests/kink-steps.test.ts @@ -9,6 +9,8 @@ import { rebarSummary } from '../src/core/rebar/summary'; import { multicolCopingInfo } from '../src/core/rebar/multicol-coping'; import { copingFold, inWedge, isTransverse } from '../src/core/rebar/coping-fold'; import { buildRebar3D } from '../src/view/rebar3d'; +import { shearB9 } from '../src/core/rebar/footing'; +import { loadP1 } from './helpers'; // 산외교 P11 (샘플01) with the coping bent 8.767° at IP4 (2026-11-10 검토): back ⑥ g 11.64, ⑦ g 18.49, kink g 15.14. const SAMPLE = fileURLToPath(new URL('../project_data/01_엑셀/샘플01_산외교_교각입력데이터.xlsx', import.meta.url)); @@ -207,3 +209,44 @@ describe('꺾인 코핑 3D 철근 (E, 2026-11-10)', () => { expect(count(bent, 'C1')).toBeGreaterThan(count(straight, 'C1')); }, 60000); }); + +describe('꺾인 교각의 확대기초 철근 (2026-11-10)', () => { + // 3주형 as in the project (산외교 P11): columns ① ② share one 18.05 m footing in stretch 0, ③ has a 10.4 m footing in stretch 1. + const m = buildPier({ ...p11(), type: '3주형', columnCount: 3 }); + + it('each footing draws its rebar in its own stretch', () => { + const rb = buildRebar3D(m); + const footingNames = /^B\d/; + const ks = new Set(rb.records.filter((r) => footingNames.test(r.name)).map((r) => r.k)); + expect(m.footings.map((f) => f.k)).toEqual([0, 1]); + expect(ks).toEqual(new Set([0, 1])); + }, 60000); + + it('B9 leaves out only the column zones of the same stretch', () => { + const rows = rebarSummary(m).rows; + const expected = m.footings.reduce((sum, f) => { + const cols = m.columns.filter((c) => c.k === f.k && Math.abs(c.s - f.s) <= f.length / 2).map((c) => (c.s - f.s) * 1000); + return sum + shearB9(f.length * 1000, f.width * 1000, (f.top - f.bottom) * 1000, cols, 2000, m.footingRebar).count; + }, 0); + expect(rows.find((r) => r.name === 'B9')!.count).toBe(expected); + }); + + it('footings of different lengths keep each length (no longest-for-all), spliced bars count both pieces', () => { + const rows = rebarSummary(m).rows.filter((r) => r.member === 'footing'); + expect(m.footings.map((f) => f.length)).toEqual([18.05, 10.4]); + const b5 = rows.find((r) => r.name === 'B5')!; + expect(b5.pieces!.length).toBe(2); + const total = b5.pieces!.reduce((s, p) => s + p.count, 0); + // The 18.05 m footing's bars are two pieces each; the 10.4 m footing's one piece. + expect(total).toBeGreaterThan(b5.count); + }); +}); + +describe('정척 초과 확대기초 철근의 2조각 (2026-11-10)', () => { + it('P1 B6 (13646 > 12000): 31 bars of 2 pieces 7776 each, so the weight counts 62 pieces', () => { + const b6 = rebarSummary(buildPier(loadP1())).rows.find((r) => r.name === 'B6')!; + expect(b6.count).toBe(31); + expect(b6.length!).toBeCloseTo(7776, 0); + expect(b6.pieces).toEqual([{ length: b6.length, count: 62 }]); + }); +});