diff --git a/src/core/coping-geom.ts b/src/core/coping-geom.ts index 5dcac46..89bb389 100644 --- a/src/core/coping-geom.ts +++ b/src/core/coping-geom.ts @@ -24,6 +24,11 @@ export const SIDE_COVER_M = 0.127; * C13 rows lie in the end band (end underside … top, height h1). * zBreak = end underside + 127 is where the taper line starts at the end face (P1: 42.538). */ -export function sideRowLevels(levelBottom: number, endBottom: number): { zBottom: number; zBreak: number } { - return { zBottom: levelBottom + SIDE_COVER_M + 0.25, zBreak: endBottom + SIDE_COVER_M }; +export function sideRowLevels(levelBottom: number, endBottom: number, top?: number): { zBottom: number; zBreak: number; zTop?: number } { + // Top margin (상면 여유) at least 400 mm (코핑철근.md §6.1): the highest side row lies at or below top − 0.4. + return { zBottom: levelBottom + SIDE_COVER_M + 0.25, zBreak: endBottom + SIDE_COVER_M, zTop: top === undefined ? undefined : top - SIDE_TOP_MARGIN_M }; } + +/** Pitch of the side-row grid (m), and the top margin 400 mm (상면 여유). */ +export const SIDE_PITCH_M = 0.25; +export const SIDE_TOP_MARGIN_M = 0.4; diff --git a/src/core/rebar/coping.ts b/src/core/rebar/coping.ts index c0a2588..cbb6c60 100644 --- a/src/core/rebar/coping.ts +++ b/src/core/rebar/coping.ts @@ -61,8 +61,13 @@ 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. */ -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); +/** + * 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). + */ +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) }; } @@ -117,11 +122,11 @@ export interface SideBar { * 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). */ -export function copingSideBars(L: number, zBottom: number, zBreak: number, g: CopingGeom = P1_COPING_GEOM): { c12: SideBar[]; c13: SideBar[] } { +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; const xL0 = L / 2 - g.levelWidth / 2; const bar = (z: number, xw: number): SideBar => ({ z, xw, straight: L - 2 * xw }); - const rows = sideRows(zBottom, zBreak); + 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)), diff --git a/src/core/rebar/summary.ts b/src/core/rebar/summary.ts index 41e04f5..65dabe6 100644 --- a/src/core/rebar/summary.ts +++ b/src/core/rebar/summary.ts @@ -190,8 +190,8 @@ export function rebarSummary( warnings.push({ code: 'RB-M01', message: `다주형 ${nCol}기둥: C14는 T형 기준으로 두었습니다 (기둥마다 반복 규칙 미정, Q-K11).`, target: 'coping' }); } // Per row: two 일자 (side faces) and two ㄷ (one at each end), so 4 bars per row (코핑철근.md §6.2). - const levels = sideRowLevels(model.levelBottom, model.endBottom); - const side = copingSideBars(model.copingL * 1000, levels.zBottom, levels.zBreak, model.copingGeom); + const levels = sideRowLevels(model.levelBottom, model.endBottom, model.copingRefTop); + const side = copingSideBars(model.copingL * 1000, levels.zBottom, levels.zBreak, model.copingGeom, levels.zTop); const hook = model.copingB * 1000 - 2 * 127 + 2 * SIDE_HOOK_LEG; // ㄷ body across the width + two legs const c12Range = side.c12.length ? `${Math.round(side.c12[0].straight)}–${Math.round(side.c12[side.c12.length - 1].straight)}` : '—'; // Per bar: 2 일자 (straight) and 2 ㄷ (hook body + legs), so 4 bars per row position. diff --git a/src/view/rebar3d.ts b/src/view/rebar3d.ts index e0877cf..27a2962 100644 --- a/src/view/rebar3d.ts +++ b/src/view/rebar3d.ts @@ -204,8 +204,8 @@ export function buildRebar3D(m: PierModel, common: Material = DEFAULT_MATERIAL, // and two ㄷ bars: a body across the width at x_w, 25.4 above the row, with legs 1525 inward along the faces. const sideFace = B / 2 - 0.127; const dSide = NOMINAL_DIAMETER.H25; - const levels = sideRowLevels(m.levelBottom, m.endBottom); - const side = copingSideBars(L * 1000, levels.zBottom, levels.zBreak, m.copingGeom); + const levels = sideRowLevels(m.levelBottom, m.endBottom, m.copingRefTop); + const side = copingSideBars(L * 1000, levels.zBottom, levels.zBreak, m.copingGeom, levels.zTop); for (const [name, rows] of [['C12', side.c12], ['C13', side.c13]] as const) { for (const r of rows) { const xw = mm(r.xw); diff --git a/tests/coping-geom.test.ts b/tests/coping-geom.test.ts index df7b2cb..1669de1 100644 --- a/tests/coping-geom.test.ts +++ b/tests/coping-geom.test.ts @@ -85,3 +85,31 @@ describe('C12 in the taper band (h2), C13 in the end band (h1)', () => { } }); }); + +describe('side-row grid (250, 상면 여유 400, 격자 연속)', () => { + const rowsFor = (input: Parameters[0]) => { + const m = buildPier(input); + 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', () => { + 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); + }); + it('taper h2 = 3000: C12 from 39.788 to 42.288 (11 rows), C13 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]); + }); + it('the C13 first row is the last C12 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); + } + } + }); +});