From 2b26b7c095a3af4653c7f478cf0a60b47cfbb2b3 Mon Sep 17 00:00:00 2001 From: ypshim Date: Fri, 6 Nov 2026 14:29:50 +0900 Subject: [PATCH] =?UTF-8?q?fix(rebar):=20=EC=B8=A1=EB=A9=B4=20=EC=88=98?= =?UTF-8?q?=ED=8F=89=20C12=20=3D=20=EC=83=81=EB=8B=A8=20=C2=B7=20C13=20=3D?= =?UTF-8?q?=20=ED=95=98=EB=8B=A8=20=EB=B3=80=ED=99=94=EA=B5=AC=EA=B0=84=20?= =?UTF-8?q?(=EC=97=91=EC=85=80=20Pier=20B357=E2=80=93G358)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit T형과 다주형 모두 엑셀 Pier 시트 철근표(C12 상단, C13 하단(변화구간))에 맞춰 측면 수평철근 번호를 서로 바꾼다 (사용자 결정 2026-11-06). - 경사 하면에서 끊기는 아래 단 → C13, 끝면까지 가는 위 단 → C12 - 철근표 위치·비고 문구, 3D, 다주형 생성기가 같은 원천(sideRows)을 따른다 - C14 높이 순서를 유지하도록 측면 단을 아래(C13) → 위(C12) 순서로 넘김 - 테스트 기대값의 번호를 교환 (P1: C12 2단 8개, C13 5단 20개) Co-Authored-By: Claude Opus 5.5 --- src/core/coping-geom.ts | 4 ++-- src/core/rebar/coping.ts | 19 ++++++++++--------- src/core/rebar/multicol-coping.ts | 2 +- src/core/rebar/summary.ts | 12 ++++++------ src/view/rebar3d.ts | 2 +- tests/coping-geom.test.ts | 30 +++++++++++++++--------------- tests/rebar-coping.test.ts | 14 +++++++------- tests/rebar-stairs.test.ts | 2 +- tests/rebar-summary.test.ts | 12 ++++++------ tests/rebar3d.test.ts | 5 +++-- 10 files changed, 52 insertions(+), 50 deletions(-) diff --git a/src/core/coping-geom.ts b/src/core/coping-geom.ts index 89bb389..e57d3f8 100644 --- a/src/core/coping-geom.ts +++ b/src/core/coping-geom.ts @@ -20,8 +20,8 @@ export const SIDE_COVER_M = 0.127; /** * Side-row levels (EL m) of C12 / C13 (코핑철근.md §6.1). z_0 = Level underside + 127 (the lowest bottom bar's Level * height, 41.038 on P1), z_k = z_0 + k·250. The split is by height band: - * C12 rows lie in the taper band (Level underside … end underside, height h2); - * C13 rows lie in the end band (end underside … top, height h1). + * C13 rows (하단 변화구간) lie in the taper band (Level underside … end underside, height h2); + * C12 rows (상단) lie in the end band (end underside … top, height h1). (엑셀 Pier B357–G358, 2026-11-06 결정) * zBreak = end underside + 127 is where the taper line starts at the end face (P1: 42.538). */ export function sideRowLevels(levelBottom: number, endBottom: number, top?: number): { zBottom: number; zBreak: number; zTop?: number } { diff --git a/src/core/rebar/coping.ts b/src/core/rebar/coping.ts index 3a4777a..5fb9bf0 100644 --- a/src/core/rebar/coping.ts +++ b/src/core/rebar/coping.ts @@ -60,16 +60,17 @@ export function shearBars(coping: number, xc = coping / 2, g: CopingGeom = P1_CO /** * 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. + * C12 = 상단 (rows at and above the sloped-bottom break z_7,end, to the end face), C13 = 하단 변화구간 (rows below it, + * cut by the sloped bottom) — 엑셀 Pier B357–G358, 2026-11-06 결정 (was the other way round). */ /** * Side rows on the 250 grid from zBottom (코핑철근.md §6.1). With zTop the grid runs up to zTop (상면 여유 400 mm); - * without it the old seven rows are used. Rows below zBreak are C12 (taper band), the rest C13 (end band). + * without it the old seven rows are used. Rows below zBreak are C13 (taper band, 하단 변화구간), the rest C12 (end band, 상단). */ export function sideRows(zBottom: number, zBreak: number, zTop?: number, rows = 7): { c12: number[]; c13: number[] } { const count = zTop === undefined ? rows : Math.floor((zTop - zBottom + 1e-6) / (COPING_REBAR.spacingSide / 1000)) + 1; const z = Array.from({ length: Math.max(0, count) }, (_, 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) }; + 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. */ @@ -103,8 +104,8 @@ export function shearULength(x: number, coping: number, g: CopingGeom = P1_COPIN return SHEAR_U.flat + 2 * shearUHeight(x, coping, g); } -/** C13 sits 127 in from each end face (코핑철근.md §6.2). */ -export const SIDE_C13_END = 127; +/** The upper rows (C12, 상단) sit 127 in from each end face (코핑철근.md §6.2). */ +export const SIDE_C12_END = 127; /** Leg of each ㄷ형 bar along the side, 1.3·l_d = 1525 (Q-K9). */ export const SIDE_HOOK_LEG = 1525; @@ -120,8 +121,8 @@ export interface SideBar { /** * C12 and C13 rows for a coping of length L (mm), T형 (코핑철근.md §6.2). - * C12 meets the sloped bottom at x_w = c + (z_break − z_k)·(x_L0 − c)/h2, with x_L0 = L/2 − W_L/2. - * C13 starts at x_w = 127. Each row has two 일자 bars (side faces) and two ㄷ bars (one at each end). + * C13 (하단 변화구간) meets the sloped bottom at x_w = c + (z_break − z_k)·(x_L0 − c)/h2, with x_L0 = L/2 − W_L/2. + * C12 (상단) starts at x_w = 127. Each row has two 일자 bars (side faces) and two ㄷ bars (one at each end). */ export function copingSideBars(L: number, zBottom: number, zBreak: number, g: CopingGeom = P1_COPING_GEOM, zTop?: number): { c12: SideBar[]; c13: SideBar[] } { const c = COPING_REBAR.cover; @@ -129,8 +130,8 @@ export function copingSideBars(L: number, zBottom: number, zBreak: number, g: Co const bar = (z: number, xw: number): SideBar => ({ z, xw, straight: L - 2 * xw }); const rows = sideRows(zBottom, zBreak, zTop); return { - c12: rows.c12.map((z) => bar(z, c + ((zBreak - z) * 1000 * (xL0 - c)) / g.h2)), - c13: rows.c13.map((z) => bar(z, SIDE_C13_END)), + c12: rows.c12.map((z) => bar(z, SIDE_C12_END)), + c13: rows.c13.map((z) => bar(z, c + ((zBreak - z) * 1000 * (xL0 - c)) / g.h2)), }; } diff --git a/src/core/rebar/multicol-coping.ts b/src/core/rebar/multicol-coping.ts index 9beba1e..1172872 100644 --- a/src/core/rebar/multicol-coping.ts +++ b/src/core/rebar/multicol-coping.ts @@ -111,7 +111,7 @@ export function multicolCopingBars(model: PierModel, material: Material = DEFAUL else intervals.push([a, b]); } sideRows.push({ z, intervals }); - const name = z < levels.zBreak - 1e-9 ? 'C12' : 'C13', face = B / 2 - 0.127; + const name = z < levels.zBreak - 1e-9 ? 'C13' : 'C12', face = B / 2 - 0.127; for (const [a, b] of intervals) { add(name, 'H16', [[a, z, -face], [b, z, -face]]); add(name, 'H16', [[a, z, face], [b, z, face]]); diff --git a/src/core/rebar/summary.ts b/src/core/rebar/summary.ts index 09de40c..dbdc492 100644 --- a/src/core/rebar/summary.ts +++ b/src/core/rebar/summary.ts @@ -63,8 +63,8 @@ const META: Record bars.flatMap((b) => [{ length: b.straight, count: 2 }, { length: hook, count: 2 }]); - rows.push({ member: 'coping', name: 'C12', size: 'H25', count: 4 * side.c12.length, length: null, pieces: sidePieces(side.c12), note: `측면, 경사 하면에서 끊김, 일자 ${c12Range} (${2 * side.c12.length}) · ㄷ ${Math.round(hook)} (${2 * side.c12.length}) · 도면 6단 중 하면 단(EL ${(model.levelBottom + 0.127).toFixed(3)})은 하면 주철근 C7·C8로 계산 (문서 §6.1)` }); - rows.push({ member: 'coping', name: 'C13', size: 'H25', count: 4 * side.c13.length, length: null, pieces: sidePieces(side.c13), note: `측면, 끝면까지, 일자 (${2 * side.c13.length}) · ㄷ ${Math.round(hook)} (${2 * side.c13.length})` }); + rows.push({ member: 'coping', name: 'C12', size: 'H25', count: 4 * side.c12.length, length: null, pieces: sidePieces(side.c12), note: `측면 상단, 끝면까지, 일자 (${2 * side.c12.length}) · ㄷ ${Math.round(hook)} (${2 * side.c12.length})` }); + rows.push({ member: 'coping', name: 'C13', size: 'H25', count: 4 * side.c13.length, length: null, pieces: sidePieces(side.c13), note: `측면 하단 변화구간, 경사 하면에서 끊김, 일자 ${c13Range} (${2 * side.c13.length}) · ㄷ ${Math.round(hook)} (${2 * side.c13.length}) · 도면 하단 단 중 하면 단(EL ${(model.levelBottom + 0.127).toFixed(3)})은 하면 주철근 C7·C8로 계산 (문서 §6.1)` }); // C14 / C15 / C16 are derived from the model (코핑철근.md §7–8); the document's 38 / 11 / 22 and 3852 / 4070 / 4069 are checked in the tests. const L14 = model.copingL * 1000; - const c14 = c14Bars(L14, model.copingRefTop, [...side.c12, ...side.c13], (x) => undersideAt(L14, x, model.endBottom, model.levelBottom, model.copingGeom.levelWidth), model.copingRebar.mainLayerGap / 1000); + const c14 = c14Bars(L14, model.copingRefTop, [...side.c13, ...side.c12], (x) => undersideAt(L14, x, model.endBottom, model.levelBottom, model.copingGeom.levelWidth), model.copingRebar.mainLayerGap / 1000); const lineCount = (line: number) => c14.filter((b) => b.line === line).length; const omitted = c14.filter((b) => !b.legs).length; rows.push({ member: 'coping', name: 'C14', size: 'H19', count: c14.length, length: c14Length(model.copingB * 1000), note: `위 2 줄 ${lineCount(1)} · ${lineCount(2)}본씩, 아래 ${[3, 4, 5, 6].map(lineCount).join(' · ')} (합 ${c14.length}) · 연직 발 생략 ${omitted}개 (하면 밖)` }); diff --git a/src/view/rebar3d.ts b/src/view/rebar3d.ts index 119349e..4d6c550 100644 --- a/src/view/rebar3d.ts +++ b/src/view/rebar3d.ts @@ -278,7 +278,7 @@ export function buildRebar3D(m: PierModel, common: Material = DEFAULT_MATERIAL, const c14Hook = mm(hookLength('main90', NOMINAL_DIAMETER.H19)); const c14Na = -B / 2 + mm(C14_INSET); const c14Nb = B / 2 - mm(C14_INSET); - for (const b of c14Bars(L * 1000, topEl, [...side.c12, ...side.c13], (x) => undersideAt(L * 1000, x, m.endBottom, m.levelBottom, m.copingGeom.levelWidth), m.copingRebar.mainLayerGap / 1000)) { + for (const b of c14Bars(L * 1000, topEl, [...side.c13, ...side.c12], (x) => undersideAt(L * 1000, x, m.endBottom, m.levelBottom, m.copingGeom.levelWidth), m.copingRebar.mainLayerGap / 1000)) { const xm = mm(b.x); addBar('C14', 0, [xm, b.z, c14Na], [xm, b.z, c14Nb], NOMINAL_DIAMETER.H19, COLOR.coping); // 연직 발 below the coping underside is left out (코핑철근.md §8). diff --git a/tests/coping-geom.test.ts b/tests/coping-geom.test.ts index 1669de1..adcad93 100644 --- a/tests/coping-geom.test.ts +++ b/tests/coping-geom.test.ts @@ -61,7 +61,7 @@ describe('① 코핑 끝단 높이 h1 · 테이퍼 높이 h2 · Level 폭 (1단 }); }); -describe('C12 in the taper band (h2), C13 in the end band (h1)', () => { +describe('C13 in the taper band (h2), C12 in the end band (h1)', () => { const bands = (input: Parameters[0]) => { const m = buildPier(input); const lv = sideRowLevels(m.levelBottom, m.endBottom); @@ -73,13 +73,13 @@ describe('C12 in the taper band (h2), C13 in the end band (h1)', () => { ['h1 800', { copingH1Mm: 800 }], ['h1 1200', { copingH1Mm: 1200 }], ['h2 1800', { copingH2Mm: 1800 }], - ])('%s: every C12 row is in the taper band and every C13 row in the end band', (_name, change) => { + ])('%s: every C13 row is in the taper band and every C12 row in the end band', (_name, change) => { const { m, rows } = bands({ ...loadP1(), ...change }); - for (const z of rows.c12) { + for (const z of rows.c13) { expect(z).toBeGreaterThanOrEqual(m.levelBottom - 1e-9); expect(z).toBeLessThan(m.endBottom + SIDE_COVER_M); } - for (const z of rows.c13) { + for (const z of rows.c12) { expect(z).toBeGreaterThanOrEqual(m.endBottom + SIDE_COVER_M - 1e-9); expect(z).toBeLessThanOrEqual(m.copingRefTop); } @@ -92,23 +92,23 @@ describe('side-row grid (250, 상면 여유 400, 격자 연속)', () => { const lv = sideRowLevels(m.levelBottom, m.endBottom, m.copingRefTop); return { m, rows: sideRows(lv.zBottom, lv.zBreak, lv.zTop) }; }; - it('P1: 5 C12 rows and 2 C13 rows, the top row at or below top − 0.4', () => { + it('P1: 5 C13 rows and 2 C12 rows, the top row at or below top − 0.4', () => { const { m, rows } = rowsFor(loadP1()); - expect(rows.c12).toEqual([41.288, 41.538, 41.788, 42.038, 42.288]); - expect(rows.c13).toEqual([42.538, 42.788]); - expect(Math.max(...rows.c13)).toBeLessThanOrEqual(m.copingRefTop - 0.4 + 1e-9); + expect(rows.c13).toEqual([41.288, 41.538, 41.788, 42.038, 42.288]); + expect(rows.c12).toEqual([42.538, 42.788]); + expect(Math.max(...rows.c12)).toBeLessThanOrEqual(m.copingRefTop - 0.4 + 1e-9); }); - it('taper h2 = 3000: C12 from 39.788 to 42.288 (11 rows), C13 2 rows, no row lost to a fixed count', () => { + it('taper h2 = 3000: C13 from 39.788 to 42.288 (11 rows), C12 2 rows, no row lost to a fixed count', () => { const { rows } = rowsFor({ ...loadP1(), copingH2Mm: 3000 }); - expect(rows.c12.length).toBe(11); - expect(rows.c12[0]).toBeCloseTo(39.788, 3); - expect(rows.c13).toEqual([42.538, 42.788]); + expect(rows.c13.length).toBe(11); + expect(rows.c13[0]).toBeCloseTo(39.788, 3); + expect(rows.c12).toEqual([42.538, 42.788]); }); - it('the C13 first row is the last C12 row plus the pitch (the grid is continuous)', () => { + it('the C12 first row is the last C13 row plus the pitch (the grid is continuous)', () => { for (const h2 of [1000, 1500, 1750, 2000, 3000]) { const { rows } = rowsFor({ ...loadP1(), copingH2Mm: h2 }); - if (rows.c12.length && rows.c13.length) { - expect(rows.c13[0] - rows.c12[rows.c12.length - 1]).toBeCloseTo(0.25, 6); + if (rows.c13.length && rows.c12.length) { + expect(rows.c12[0] - rows.c13[rows.c13.length - 1]).toBeCloseTo(0.25, 6); } } }); diff --git a/tests/rebar-coping.test.ts b/tests/rebar-coping.test.ts index 3c7d0e0..debd846 100644 --- a/tests/rebar-coping.test.ts +++ b/tests/rebar-coping.test.ts @@ -37,18 +37,18 @@ describe('코핑 철근 (코핑철근.md, plan C3)', () => { 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', () => { + it('C13 has 5 rows below the break at 42.538, C12 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]); + expect(r.c13).toEqual([41.288, 41.538, 41.788, 42.038, 42.288]); + expect(r.c12).toEqual([42.538, 42.788]); }); - it('C12 일자 lengths 3717 / 5333 / 6950 / 8567 / 10183 and C13 11746 for L 12000 (코핑철근.md §6.2 table)', () => { + it('C13 일자 lengths 3717 / 5333 / 6950 / 8567 / 10183 and C12 11746 for L 12000 (코핑철근.md §6.2 table)', () => { const s = copingSideBars(12000, 41.288, 42.538); const expected = [3717, 5333, 6950, 8567, 10183]; - s.c12.forEach((r, i) => expect(r.straight).toBeCloseTo(expected[i], 0)); - expect(s.c12[0].xw).toBeCloseTo(4141.7, 1); - s.c13.forEach((r) => { + s.c13.forEach((r, i) => expect(r.straight).toBeCloseTo(expected[i], 0)); + expect(s.c13[0].xw).toBeCloseTo(4141.7, 1); + s.c12.forEach((r) => { expect(r.xw).toBe(127); expect(r.straight).toBe(11746); }); diff --git a/tests/rebar-stairs.test.ts b/tests/rebar-stairs.test.ts index 50f41d7..9e67573 100644 --- a/tests/rebar-stairs.test.ts +++ b/tests/rebar-stairs.test.ts @@ -7,7 +7,7 @@ const m = buildPier(loadP1()); const L = m.copingL * 1000; const B = m.copingB * 1000; const side = copingSideBars(L, 41.288, 42.538); -const rows = [...side.c12, ...side.c13]; +const rows = [...side.c13, ...side.c12]; const under = (x: number) => undersideAt(L, x, m.endBottom, m.levelBottom); describe('C14 / C15 / C16 (코핑철근.md §7–8)', () => { diff --git a/tests/rebar-summary.test.ts b/tests/rebar-summary.test.ts index 27345a6..a324cc0 100644 --- a/tests/rebar-summary.test.ts +++ b/tests/rebar-summary.test.ts @@ -12,13 +12,13 @@ describe('③ 철근표 요약 (P1)', () => { expect(count('S2')).toBe(80); }); - it('coping: C1–C8 12 each, C9 18, C10 9, C11 18, C12 20 (5 rows × 일자 2 + ㄷ 2), C13 8 (2 rows), C14 38, C15 11, C16 22', () => { + it('coping: C1–C8 12 each, C9 18, C10 9, C11 18, C13 20 (5 rows × 일자 2 + ㄷ 2), C12 8 (2 rows), C14 38, C15 11, C16 22', () => { for (const n of ['C1', 'C2', 'C3', 'C4', 'C5', 'C6', 'C7', 'C8']) expect(count(n)).toBe(12); expect(count('C9')).toBe(18); expect(count('C10')).toBe(9); expect(count('C11')).toBe(18); - expect(count('C12')).toBe(20); - expect(count('C13')).toBe(8); + expect(count('C13')).toBe(20); + expect(count('C12')).toBe(8); expect(count('C14')).toBe(38); expect(count('C15')).toBe(11); expect(count('C16')).toBe(22); @@ -52,9 +52,9 @@ describe('③ 철근표 요약 (P1)', () => { expect(rows.find((r) => r.name === 'B6')!.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', () => { - // C13 has several lengths now: its straight bars are pieces (수정 2026-10-23); 11746 is the first 일자. - expect(rows.find((r) => r.name === 'C13')!.pieces!.some((p) => p.length === 11746)).toBe(true); + it('lengths from the documents: C12 11746, C14 3447.5, C15 3852, C16 4070, P3 ≈ C_t + 1.3·l_d, P5 ≈ C_t', () => { + // C12 has several lengths now: its straight bars are pieces (수정 2026-10-23); 11746 is the first 일자. + expect(rows.find((r) => r.name === 'C12')!.pieces!.some((p) => p.length === 11746)).toBe(true); 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); diff --git a/tests/rebar3d.test.ts b/tests/rebar3d.test.ts index 778128e..0b35fe1 100644 --- a/tests/rebar3d.test.ts +++ b/tests/rebar3d.test.ts @@ -33,8 +33,9 @@ describe('3D 철근 모델 (③)', () => { }); it('C12 / C13 in 3D: per row two 일자 and two ㄷ (body + 2 legs each end)', () => { - expect(rb.byName.get('C12')!.length).toBe(5 * (2 + 2 * 3)); - expect(rb.byName.get('C13')!.length).toBe(2 * (2 + 2 * 3)); + // C12 = 상단 2 rows, C13 = 하단 변화구간 5 rows (엑셀 Pier B357–G358, 2026-11-06). + expect(rb.byName.get('C12')!.length).toBe(2 * (2 + 2 * 3)); + expect(rb.byName.get('C13')!.length).toBe(5 * (2 + 2 * 3)); }); it('C1 legs turn down 472 at the ends (to EL 42.812 at the end face)', () => {