diff --git a/src/core/rebar/column.ts b/src/core/rebar/column.ts index 23a5f9f..abc3e6a 100644 --- a/src/core/rebar/column.ts +++ b/src/core/rebar/column.ts @@ -48,9 +48,9 @@ export function columnInputFor(model: PierModel, base: ColumnInput = COLUMN_P1): const seg = model.segments.find((g) => col.s >= g.s0 - 1e-9 && col.s <= g.s1 + 1e-9) ?? model.segments[0]; const Hc = Math.round((model.levelBottom - model.footings[0].top) * 1000 * 10) / 10; const hcop = Math.round((seg.top - model.levelBottom) * 1000 * 10) / 10; - // C3 (상면 3단) = the lowest coping top −127 (C1 cover) −2·57.2 (layer spacing 상면 주철근 단 간격); P1: 43.170. + // C3 (상면 3단) = the lowest coping top −127 (C1 cover) −2·s_lay (상면 주철근 단 간격 input); P1 was 43.170 at 57.2. // P1 top = C3 − 100 (계산 2026-10-23), so e_P1 = step top − (C3 − 0.1) = 485 on P1. - const c3 = model.copingRefTop - 0.127 - 2 * 0.0572; + const c3 = model.copingRefTop - 0.127 - 2 * (model.copingRebar.mainLayerGap / 1000); const eP1 = Math.round((seg.top - (c3 - 0.1)) * 1000); // e_top: the coping middle level (Level 하면 + 계단 상면)/2 is the top of P3 and P6 (승인 2026-10-23). const eTop = Math.round((seg.top - (model.levelBottom + seg.top) / 2) * 1000); diff --git a/src/core/rebar/coping-main.ts b/src/core/rebar/coping-main.ts index 5efb564..1efd6f8 100644 --- a/src/core/rebar/coping-main.ts +++ b/src/core/rebar/coping-main.ts @@ -1,4 +1,5 @@ import { DEFAULT_MATERIAL, fyOf, hookLength, Material, NOMINAL_DIAMETER, spliceLength } from './common'; +import { DEFAULT_COPING_REBAR } from './coping-settings'; /** * C1–C8 상·하면 주철근 (plan C3 continued). Source: RAG/코핑철근.md §5, 2026-10-22 최종 정리. @@ -10,9 +11,8 @@ export const MAIN = { /** 기본 간격 s_m and minimum end gap g_min. */ spacing: 125, endGapMin: 80, - /** 상면 단 간격 s_lay (EL, m) and its horizontal step (mm). */ - layEl: 0.057, - layHorizontal: 57.2, + /** 상면 단 가로 간격 = 2d (H29, mm). Fixed; the vertical step is the 상면 주철근 단 간격 input. */ + layHorizontal: 2 * NOMINAL_DIAMETER.H29, /** 90° 갈고리 l_h (mm, as in the document). */ hook: 472, /** 하면 경사 구간 수평 길이 (mm, each side) and Level width. */ @@ -56,9 +56,9 @@ export function widthLayout(B: number, dSt = NOMINAL_DIAMETER.H25, dM = NOMINAL_ }; } -/** Top-layer EL of the k-th row (k = 1, 2, 3): z_k = z_1 − (k − 1)·s_lay (m). */ -export function topRowEl(z1: number, k: number): number { - return Math.round((z1 - (k - 1) * MAIN.layEl) * 1000) / 1000; +/** Top-layer EL of the k-th row (k = 1, 2, 3): z_k = z_1 − (k − 1)·s_lay (m). s_lay is the 상면 주철근 단 간격 input. */ +export function topRowEl(z1: number, k: number, layEl = DEFAULT_COPING_REBAR.mainLayerGap / 1000): number { + return Math.round((z1 - (k - 1) * layEl) * 1000) / 1000; } /** Length of one top bar on row k: path L − 2·(c + (k−1)·step), plus two 90° hooks. */ diff --git a/src/core/rebar/coping-settings.ts b/src/core/rebar/coping-settings.ts index a4b9623..5201a1f 100644 --- a/src/core/rebar/coping-settings.ts +++ b/src/core/rebar/coping-settings.ts @@ -9,7 +9,7 @@ export interface CopingRebarSettings { mainSpacing: number; /** 주철근 끝단 최소 여유 (mm). */ mainEndGap: number; - /** 상면 주철근 단 간격 (mm). */ + /** 상면 주철근 단 간격 · 세로 (mm). The horizontal step is fixed at 2d (H29, 57.2 mm). */ mainLayerGap: number; /** 첫 전단철근 최소 여유 (피복 다음, mm). */ firstShearGap: number; @@ -33,7 +33,7 @@ export const DEFAULT_COPING_REBAR: CopingRebarSettings = { cover: 100, mainSpacing: 125, mainEndGap: 80, - mainLayerGap: 57.2, + mainLayerGap: 100, firstShearGap: 200, shearLegs: 6, spacingS: 250, @@ -49,7 +49,7 @@ export const COPING_REBAR_RANGES: Record number): C14Bar[] { +export function c14Bars(L: number, topEl: number, sideRows: { z: number; xw: number }[], under: (x: number) => number, layEl = DEFAULT_COPING_REBAR.mainLayerGap / 1000): C14Bar[] { const leg = Math.round(hookLength('main90', NOMINAL_DIAMETER.H19)) / 1000; // 296 mm, EL m const xc = L / 2; const lo = COPING_REBAR.cover + C14_END_GAP; @@ -186,7 +187,7 @@ export function c14Bars(L: number, topEl: number, sideRows: { z: number; xw: num const out: C14Bar[] = []; const dUpper = (NOMINAL_DIAMETER.H29 + NOMINAL_DIAMETER.H19) / 2000; // m, over C2 / C3 (H29) for (const k of [2, 3]) { - const z = topEl - 0.127 - (k - 1) * 0.057 + dUpper; + const z = topEl - 0.127 - (k - 1) * layEl + dUpper; for (let q = -10; q <= 10; q++) { const x = xc + q * 1000; if (x >= lo && x <= hi) out.push({ line: k - 1, x, z, legs: z - leg >= under(x) }); diff --git a/src/core/rebar/summary.ts b/src/core/rebar/summary.ts index a9ee2dc..b180217 100644 --- a/src/core/rebar/summary.ts +++ b/src/core/rebar/summary.ts @@ -200,7 +200,7 @@ export function rebarSummary( 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})` }); // 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)); + 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 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/main.ts b/src/main.ts index d581878..e199ecc 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1079,7 +1079,7 @@ const COPING_REBAR_FIELDS: { key: keyof CopingRebarSettings; label: string }[] = { key: 'cover', label: '피복 (철근 중심, mm)' }, { key: 'mainSpacing', label: '주철근 기본간격 (mm)' }, { key: 'mainEndGap', label: '주철근 끝단 최소 여유 (mm)' }, - { key: 'mainLayerGap', label: '상면 주철근 단 간격 (mm)' }, + { key: 'mainLayerGap', label: '상면 주철근 단 간격 · 세로 (mm, 가로는 2d 고정)' }, { key: 'firstShearGap', label: '첫 전단철근 최소 여유 (피복 다음, mm)' }, { key: 'shearLegs', label: '전단철근 다리 수' }, { key: 'spacingS', label: '전단철근 간격 · 경사 (mm)' }, diff --git a/src/view/rebar3d.ts b/src/view/rebar3d.ts index 094f2e3..1927475 100644 --- a/src/view/rebar3d.ts +++ b/src/view/rebar3d.ts @@ -150,7 +150,7 @@ export function buildRebar3D(m: PierModel, common: Material = DEFAULT_MATERIAL, const topEl = m.copingRefTop; const hookTop = MAIN.hook / 1000; for (let k = 1; k <= 3; k++) { - const z = topEl - 0.127 - (k - 1) * 0.057; + const z = topEl - 0.127 - (k - 1) * (m.copingRebar.mainLayerGap / 1000); const xk = cover + ((k - 1) * MAIN.layHorizontal) / 1000; xs.forEach((n, j) => { const name = `C${k + (j % 2 === 1 ? 0 : 3)}`; @@ -239,7 +239,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))) { + 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)) { 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/column-model.test.ts b/tests/column-model.test.ts index 6746eac..8ba4ffa 100644 --- a/tests/column-model.test.ts +++ b/tests/column-model.test.ts @@ -10,7 +10,7 @@ describe('기둥 철근 높이는 모델에서 (Hc · h_cop, 승인 2026-10-23)' expect(input.hcop).toBeCloseTo(2644, 0); const col = columnRebar(input); expect(col.zTop2).toBeCloseTo(14539, 0); - expect(col.zTop1).toBeCloseTo(14862, 0); + expect(col.zTop1).toBeCloseTo(14776, 0); }); it('taper h2 = 3000: Level underside 39.411, Hc 11203, P2 top 13039 (= Level underside + l_d)', () => { @@ -22,7 +22,7 @@ describe('기둥 철근 높이는 모델에서 (Hc · h_cop, 승인 2026-10-23)' const col = columnRebar(input); expect(col.zTop2).toBeCloseTo(13039, 0); // The P1 top is anchored to the step top (and C3), so it does not move with h2. - expect(col.zTop1).toBeCloseTo(14862, 0); + expect(col.zTop1).toBeCloseTo(14776, 0); }); it('P3 and P6 end at the coping middle level: P1 e_top 1322; taper 3000 gives e_top 2072, P3 37 ties, P6 32 bars', () => { diff --git a/tests/rebar-coping-main.test.ts b/tests/rebar-coping-main.test.ts index 3df66f2..650bb93 100644 --- a/tests/rebar-coping-main.test.ts +++ b/tests/rebar-coping-main.test.ts @@ -19,10 +19,10 @@ describe('C1–C8 상·하면 주철근 (코핑철근.md §5, plan C3)', () => { expect(w.text).toBe('127 · 110.5 · 21@125 · 110.5 · 127'); }); - it('heights: 43.284 / 43.227 / 43.170 for the three top rows', () => { + it('heights: 43.284 / 43.184 / 43.084 for the three top rows (상면 단 간격 100)', () => { expect(topRowEl(43.284, 1)).toBe(43.284); - expect(topRowEl(43.284, 2)).toBe(43.227); - expect(topRowEl(43.284, 3)).toBe(43.17); + expect(topRowEl(43.284, 2)).toBe(43.184); + expect(topRowEl(43.284, 3)).toBe(43.084); }); it('top bar lengths 12744 / 12629.6 / 12515.2 (path + 2 × 472 hook)', () => { diff --git a/tests/rebar-stairs.test.ts b/tests/rebar-stairs.test.ts index 50e8b1d..532e141 100644 --- a/tests/rebar-stairs.test.ts +++ b/tests/rebar-stairs.test.ts @@ -20,11 +20,11 @@ describe('C14 / C15 / C16 (코핑철근.md §7–8)', () => { expect(bars.length).toBe(38); }); - it('C14 heights: upper 43.251 / 43.194, lower 42.810 / 42.310 / 41.810 / 41.310', () => { + it('C14 heights: upper 43.208 / 43.108, lower 42.810 / 42.310 / 41.810 / 41.310 (상면 단 간격 100)', () => { const bars = c14Bars(L, m.copingRefTop, rows, under); const z = (line: number) => bars.find((b) => b.line === line)!.z; - expect(z(1)).toBeCloseTo(43.251, 3); - expect(z(2)).toBeCloseTo(43.194, 3); + expect(z(1)).toBeCloseTo(43.208, 3); + expect(z(2)).toBeCloseTo(43.108, 3); expect([3, 4, 5, 6].map(z)).toEqual([42.81, 42.31, 41.81, 41.31].map((v) => expect.closeTo(v, 2))); });