From 99db6a1f528c72e2f250f35b9f0a92d6b4818984 Mon Sep 17 00:00:00 2001 From: ypshim Date: Tue, 27 Oct 2026 09:48:05 +0900 Subject: [PATCH] =?UTF-8?q?Revert=20"feat(rebar):=20B1=E2=80=93B4=EC=99=80?= =?UTF-8?q?=20B5=E2=80=93B8=20=EB=B0=A9=ED=96=A5=20=EA=B5=90=ED=99=98=20(B?= =?UTF-8?q?1=E2=80=93B4=EB=8A=94=20W=20=EB=B0=A9=ED=96=A5,=20B5=E2=80=93B8?= =?UTF-8?q?=EC=9D=80=20L=20=EB=B0=A9=ED=96=A5)"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 이 교환은 9,350 방향이 L이라는 가정에서 적용됐으나, 단면 삽도(B5–B8 선, 교각축 직각 단면)와 재료표가 원래 배치(B1–B4 L 방향, B5–B8 W 방향)와 맞는 것으로 확인되어 되돌림. Co-Authored-By: Claude Haiku 5.5 --- src/core/rebar/footing-settings.ts | 20 +++---- src/core/rebar/footing.ts | 43 +++++++------- src/core/rebar/summary.ts | 10 ++-- src/main.ts | 14 ++--- src/view/footing-section-svg.ts | 87 ++++++++++++++++------------ src/view/rebar3d.ts | 43 +++++++------- tests/footing-rebar-inputs.test.ts | 10 ++-- tests/footing-rebar-settings.test.ts | 24 ++++---- tests/footing-section-svg.test.ts | 11 ++-- tests/rebar-footing.test.ts | 47 ++++++++------- tests/rebar-member-fck.test.ts | 4 +- tests/rebar-summary.test.ts | 14 ++--- 12 files changed, 167 insertions(+), 160 deletions(-) diff --git a/src/core/rebar/footing-settings.ts b/src/core/rebar/footing-settings.ts index 951e84a..cc27213 100644 --- a/src/core/rebar/footing-settings.ts +++ b/src/core/rebar/footing-settings.ts @@ -3,8 +3,8 @@ * Defaults are the 확대기초철근 (1).md values; the ranges are proposed and pending the design-standard check. * Fixed values (k = 2, Δ₂ = 100, 전단 4s · 엇갈림 2s, cycle 교대 배치) are not inputs. */ -/** Which grid spacing a helper uses: along L (sL) or along W (sW). */ -export type FootingGridKey = 'sL' | 'sW'; +/** Which grid spacing a helper uses: the 교각축 (sAxis) or the 교각축 직각 (sPerp). */ +export type FootingGridKey = 'sAxis' | 'sPerp'; export interface FootingRebarSettings { /** 상면 피복 (최외측 철근 중심까지, mm). */ @@ -15,10 +15,10 @@ export interface FootingRebarSettings { cBottom: number; /** 첫 철근 최소 여유 (측면 피복 다음, mm). */ eMin: number; - /** 격자 실간격 · L 방향 (mm): B1–B4 spacing, the B9 grid and B11 (승인 2026-10-26, 방향 교환). */ - sL: number; - /** 격자 실간격 · W 방향 (mm): B5–B8 spacing and B12 (승인 2026-10-26, 방향 교환). */ - sW: number; + /** 격자 실간격 · 교각축 (mm): B1–B4 along W, and the B9 grid (승인 2026-10-26). */ + sAxis: number; + /** 격자 실간격 · 교각축 직각 (mm): B5–B8 along L, and B11·B12 (승인 2026-10-26). */ + sPerp: number; /** B10 높이 간격 s_v (mm). */ sV: number; /** B5 아래 최소 여유 e_top (mm). */ @@ -36,8 +36,8 @@ export const DEFAULT_FOOTING_REBAR: FootingRebarSettings = { cSide: 100, cBottom: 150, eMin: 150, - sL: 125, - sW: 125, + sAxis: 125, + sPerp: 125, sV: 200, eTop: 150, n: 2, @@ -51,8 +51,8 @@ export const FOOTING_REBAR_RANGES: Record a + assemblyGridIndices(nL, f.kB11, idx).length, 0); const b12 = levelsJ.reduce((a, _, idx) => a + assemblyGridIndices(nW, f.kB12, idx).length, 0); const hook = Math.round(hookLength('main90', d('S16'))); // 246 mm, rounded as in the document (§8.3) @@ -187,7 +187,7 @@ export function assemblyBars(L: number, W: number, H: number, f: FootingRebarSet } /** Plane positions of the grid along one side, mm from the footing centre: x_i = −side/2 + c_s + e + i·s (§3.1). */ -export function gridPositions(side: number, f: FootingRebarSettings = DEFAULT_FOOTING_REBAR, key: FootingGridKey = 'sL'): number[] { +export function gridPositions(side: number, f: FootingRebarSettings = DEFAULT_FOOTING_REBAR, key: FootingGridKey = 'sAxis'): number[] { const g = grid(side, f, key); return Array.from({ length: g.positions }, (_, i) => -side / 2 + f.cSide + g.e + i * f[key]); } @@ -204,9 +204,8 @@ export interface B9Bar { * than the column radius plus half a bar to a column axis (Q-B20, applied: the 기둥 구역 is removed). */ export function b9Bars(L: number, W: number, columnX: number[], columnD: number = 2000, f: FootingRebarSettings = DEFAULT_FOOTING_REBAR): { rows: number; perRow: number[]; bars: B9Bar[] } { - // B9: columns along L use the L grid (sL); rows along W use the W grid (sW) (방향 교환 후 물리적 격자, 승인 2026-10-26). - const xs = gridPositions(L, f, 'sL'); - const ns = gridPositions(W, f, 'sW'); + const xs = gridPositions(L, f); + const ns = gridPositions(W, f); const rowIndices: number[] = []; for (let i = 1; i < ns.length; i += 4) rowIndices.push(i); const halfBar = NOMINAL_DIAMETER.H16 / 2; @@ -266,8 +265,8 @@ export function assemblyHeights(H: number, f: FootingRebarSettings = DEFAULT_FOO * rebar table and the 3D view use, so the panel cannot drift from the calculation. H is the footing height (mm). */ export function footingRuleValues(H: number, f: FootingRebarSettings = DEFAULT_FOOTING_REBAR): { - upper2L: number; - upper2W: number; + upper2sAxis: number; + upper2sPerp: number; delta2: number; b2Leg: number; b6Leg: number; @@ -280,15 +279,15 @@ export function footingRuleValues(H: number, f: FootingRebarSettings = DEFAULT_F } { const z = layers(H, f); return { - upper2L: 2 * f.sL, - upper2W: 2 * f.sW, + upper2sAxis: 2 * f.sAxis, + upper2sPerp: 2 * f.sPerp, delta2: DELTA2, b2Leg: legB2(H, f), b6Leg: z.B5 - z.B68, - b9Pitch: 4 * f.sL, - b9Stagger: 2 * f.sL, - b11Pitch: f.kB11 * f.sL, - b12Pitch: f.kB12 * f.sW, + b9Pitch: 4 * f.sAxis, + b9Stagger: 2 * f.sAxis, + b11Pitch: f.kB11 * f.sPerp, + b12Pitch: f.kB12 * f.sAxis, assemblyVertical: f.n * f.sV, assemblyLevels: assemblyJs(H, f).length, }; diff --git a/src/core/rebar/summary.ts b/src/core/rebar/summary.ts index 274b199..fc24150 100644 --- a/src/core/rebar/summary.ts +++ b/src/core/rebar/summary.ts @@ -166,16 +166,16 @@ export function linkedSpacing(name: string, model: PierModel): Pick`); parts.push(line('base', '', x(-W / 2), x(W / 2), y(0), COLOR.outline, 1)); - // Cover lines on the W faces (측면 피복 c_s). + // B5–B8: teal lines along W between the covers. B5 at the top, B6 and B8 at the 하면 1단, B7 at the 하면 2단. const xa = x(-W / 2 + fs.cSide); const xb = x(W / 2 - fs.cSide); - // B1–B4 (방향 교환, 승인 2026-10-26): run along W, so in this section they are lines from cover to cover. - // The B2 legs turn up to the B1 height at both W ends (측면 피복선), drawn heavier. - parts.push(line('b1', 'B1', xa, xb, y(z.B1), COLOR.b14, 1.4)); - parts.push(line('b2', 'B2', xa, xb, y(z.B24), COLOR.b14, 1.6)); - parts.push(line('b3', 'B3', xa, xb, y(z.B3), COLOR.b14, 1.2)); - parts.push(line('b4', 'B4', xa, xb, y(z.B24), COLOR.b14, 1.2)); - for (const xx of [xa, xb]) { - parts.push(``); - } - // B6 leg note: the B6 leg runs from the 하면 1단 up to the B5 height (B6 다리 → B5 높이), shown in the right margin below. + parts.push(line('b5', 'B5', xa, xb, y(z.B5), COLOR.b5, 1.6)); + parts.push(line('b5', 'B6', xa, xb, y(z.B68), COLOR.b5, 1.2)); + parts.push(line('b5', 'B7', xa, xb, y(z.B7), COLOR.b5, 1.2)); + parts.push(line('b5', 'B8', xa, xb, y(z.B68), COLOR.b5, 1.2)); - // B5–B8 (방향 교환): run along L and are spaced along W (sW). In this section they are points on the W grid: - // odd positions carry B5, B6, B7 and even positions carry B8 (same parity rule as crossBars). - const xsW = gridPositions(W, fs, 'sW'); - const last = xsW.length - 1; - xsW.forEach((mm, i) => { + // B1–B4 on the grid along W (교각축, orange points): odd grid positions carry B1, B2 and B3; even carry B4 (as bottomBars). + const xs = gridPositions(W, fs, 'sAxis'); + xs.forEach((mm, i) => { const xx = x(mm); if (i % 2 === 1) { - parts.push(dot('b5', 'B5', xx, y(z.B5), COLOR.b5)); - parts.push(dot('b6', 'B6', xx, y(z.B68), COLOR.b5)); - parts.push(dot('b7', 'B7', xx, y(z.B7), COLOR.b5)); + parts.push(dot('b1', 'B1', xx, y(z.B1), COLOR.b14)); + parts.push(dot('b2', 'B2', xx, y(z.B24), COLOR.b14)); + parts.push(dot('b3', 'B3', xx, y(z.B3), COLOR.b14)); } else { - parts.push(dot('b8', 'B8', xx, y(z.B68), COLOR.b5)); + parts.push(dot('b4', 'B4', xx, y(z.B24), COLOR.b14)); } }); - // Outermost columns (승인 2026-10-26 추천, 방향 교환 반영): the missing B5–B8 points at the first and last W position are drawn as 'ext' points. + + // Outermost columns (승인 2026-10-26, 추천): B1·B2·B3·B4 all sit in the first and last grid column (확대기초철근 (1).md §3, §5). + // The missing ones are drawn as 'ext' points (they do not change the table counts). Their top (B1) and bottom (B2·B4) runs extend + // to the side cover c_s, and a leg line at the cover runs from the 하면 1단 (B2) up to the B1 height. + const xSideL = x(-W / 2 + fs.cSide); + const xSideR = x(W / 2 - fs.cSide); + const last = xs.length - 1; for (const i of [0, last]) { - const xx = x(xsW[i]); - const drawnHere = i % 2 === 1 ? ['B5', 'B6', 'B7'] : ['B8']; - for (const [name, zz] of [['B5', z.B5], ['B6', z.B68], ['B7', z.B7], ['B8', z.B68]] as const) { - if (!drawnHere.includes(name)) parts.push(dot('ext', name, xx, y(zz), COLOR.b5)); + const drawnHere = i % 2 === 1 ? ['B1', 'B2', 'B3'] : ['B4']; + const xx = x(xs[i]); + for (const [name, zz] of [['B1', z.B1], ['B2', z.B24], ['B3', z.B3], ['B4', z.B24]] as const) { + if (!drawnHere.includes(name)) parts.push(dot('ext', name, xx, y(zz), COLOR.b14)); } + const xSide = i === 0 ? xSideL : xSideR; + parts.push(line('ext', 'B1', xx, xSide, y(z.B1), COLOR.b14, 1)); + parts.push(line('ext', 'B2', xx, xSide, y(z.B24), COLOR.b14, 1)); + // The B2 side leg (up to the B1 height at the side cover) is drawn heavier, so the corner reads as one run (승인 2026-10-26). + parts.push(``); } // B9: one purple vertical line per row along W (the rows of b9Bars). @@ -123,12 +126,12 @@ export function footingSectionSvg(model: PierModel): string { js.forEach((j, idx) => { const zA = fs.cBottom + j * fs.sV; parts.push(line('b11', 'B11', xa, xb, y(zA), COLOR.b11, 1.2, '6 3')); - const xsB12 = gridPositions(W, fs, 'sW'); + const xsB12 = gridPositions(W, fs, 'sAxis'); for (const i of assemblyGridIndices(xsB12.length - 1, fs.kB12, idx)) parts.push(dot('b12', 'B12', x(xsB12[i]), y(zA + dB10), COLOR.b12, 2.2)); }); // Dimensions: the width line with ticks and the grid string, and the right-hand heights. - const g = grid(W, fs, 'sW'); + const g = grid(W, fs, 'sAxis'); const yDim = Y0 + 40; parts.push(``); parts.push(tick(x(-W / 2), yDim) + tick(x(W / 2), yDim)); @@ -138,7 +141,7 @@ export function footingSectionSvg(model: PierModel): string { parts.push(text(x(-W / 2 + fs.cSide) + 2, yDim + 14, `${g.e.toFixed(1)}`, 'start', COLOR.dim)); parts.push(text(x(W / 2 - fs.cSide) - 2, yDim + 14, `${g.e.toFixed(1)}`, 'end', COLOR.dim)); // The grid string sits right of centre, so the bottom leaders (at the centre and the two sides) do not cross it. - parts.push(text(cx + 150, yDim - 6, `${g.n}@${fs.sW}=${g.G}`, 'middle', COLOR.dim)); + parts.push(text(cx + 150, yDim - 6, `${g.n}@${fs.sAxis}=${g.G}`, 'middle', COLOR.dim)); parts.push(text(cx, 20, `교각축 직각 단면 · 폭 ${W.toFixed(0)}`, 'middle', '#1F2937')); const xr = X0 + W_PX + 18; parts.push(``); @@ -157,22 +160,32 @@ export function footingSectionSvg(model: PierModel): string { const edgeY = by < ty ? by + 11 : by - 11; return `${name}`; }; - const zB11 = js.length > 0 ? fs.cBottom + js[0] * fs.sV : fs.cBottom; const yTop = 40; const yBot = Y0 + 100; // Each bottom bubble sits straight below its own bar (B2 left of centre, B4 at centre, B3 right of centre) on a bar of the right parity. - parts.push(bubble('B5', xa + 12, yTop, x(xsW[1]), y(z.B5))); - parts.push(bubble('B1', cx - 60, yTop, cx - 60, y(z.B1))); + const nearest = (target: number, parity: number) => { + let best = -1; + xs.forEach((mm, i) => { + if (i % 2 === parity && (best < 0 || Math.abs(mm - target) < Math.abs(xs[best] - target))) best = i; + }); + return best; + }; + const iB2 = nearest(-1930, 1); + const iB4 = nearest(0, 0); + const iB3 = nearest(1930, 1); + const zB11 = js.length > 0 ? fs.cBottom + js[0] * fs.sV : fs.cBottom; + parts.push(bubble('B5', xa + 12, yTop, xa + 12, y(z.B5))); + parts.push(bubble('B1', x(xs[1]) + 60, yTop, x(xs[1]), y(z.B1))); parts.push(bubble('B11', xb - 20, yTop, xb, y(zB11))); parts.push(bubble('B10-1', X0 - 18, y(fs.cBottom + (levels - 1) * fs.sV / 2), X0 + 2, y(fs.cBottom + (levels - 1) * fs.sV / 2))); - parts.push(bubble('B2', cx - 70, yBot, cx - 70, y(z.B24))); - parts.push(bubble('B4', cx, yBot, cx, y(z.B24))); - parts.push(bubble('B3', cx + 70, yBot, cx + 70, y(z.B3))); + parts.push(bubble('B2', x(xs[iB2]), yBot, x(xs[iB2]), y(z.B24))); + parts.push(bubble('B4', x(xs[iB4]), yBot, x(xs[iB4]), y(z.B24))); + parts.push(bubble('B3', x(xs[iB3]), yBot, x(xs[iB3]), y(z.B3))); // Legend (선 모양별, 삽도 기준) and the units note. const yLeg = 352; - parts.push(`${text(28, yLeg + 3, '주철근 (B5–B8)', 'start', COLOR.outline)}`); - parts.push(`${text(212, yLeg + 3, '직각·보조 (B1–B4)', 'start', COLOR.outline)}`); + parts.push(`${text(50, yLeg + 3, '주철근 (B5–B8)', 'start', COLOR.outline)}`); + parts.push(`${text(188, yLeg + 3, '직각·보조 (B1–B4)', 'start', COLOR.outline)}`); parts.push(`${text(338, yLeg + 3, '전단·띠 (B9)', 'start', COLOR.outline)}`); parts.push(`${text(28, yLeg + 25, '측면 수평 (B10-1 · B10-2)', 'start', COLOR.outline)}`); parts.push(`${text(280, yLeg + 25, '조립 (B11·B12)', 'start', COLOR.outline)}`); diff --git a/src/view/rebar3d.ts b/src/view/rebar3d.ts index ecc9e0f..29b869c 100644 --- a/src/view/rebar3d.ts +++ b/src/view/rebar3d.ts @@ -384,11 +384,10 @@ export function buildRebar3D(m: PierModel, common: Material = DEFAULT_MATERIAL, // 확대기초 입력 (승인 2026-10-26): the grid, covers and layer heights come from the model's footing settings. const fs = m.footingRebar; const z = layers(Hf, fs); - // 방향 교환 (승인 2026-10-26): B1–B4 run along W and are spaced along L (sL); B5–B8 run along L and are spaced along W (sW). - const gW = grid(Wf * 1000, fs, 'sW'); - const gL = grid(Lf * 1000, fs, 'sL'); - const posW = (j: number) => -Wf / 2 + mm(fs.cSide + gW.e + j * fs.sW); - const posL = (i: number) => -Lf / 2 + mm(fs.cSide + gL.e + i * fs.sL); + const gW = grid(Wf * 1000, fs); + const gL = grid(Lf * 1000, fs, 'sPerp'); + const posW = (j: number) => -Wf / 2 + mm(fs.cSide + gW.e + j * fs.sAxis); + const posL = (i: number) => -Lf / 2 + mm(fs.cSide + gL.e + i * fs.sPerp); const sA = -Lf / 2 + 0.1; const sB = Lf / 2 - 0.1; const nA = -Wf / 2 + 0.1; @@ -409,28 +408,26 @@ export function buildRebar3D(m: PierModel, common: Material = DEFAULT_MATERIAL, const at = (t: number, h: number): [number, number, number] => [f.s + s, h, t]; path(name, legTo === undefined ? [at(nA, y), at(nB, y)] : [at(nA, legTo), at(nA, y), at(nB, y), at(nB, legTo)], dMm); }; - // B1–B4 run along W (at each L position s): odd positions carry B1, B2, B3, even carry B4 (same parity rule as bottomBars). - for (let i = 0; i <= gL.n; i++) { - const sPos = posL(i); - if (i % 2 === 1) { + for (let j = 0; j <= gW.n; j++) { + const n = posW(j); + if (j % 2 === 1) { // B1 ⊓ down, B2 ⊔ up to the B1 height, B3 ⊔ up. B1·B3 sit beside the B2 leg (+25.4 / +22.25, §5.3). - across('B1', sPos + mm(off.B1), bottom + mm(z.B1), bottom + mm(z.B1) - mm(legLength('S25')), NOMINAL_DIAMETER.S25); - across('B2', sPos, bottom + mm(z.B24), bottom + mm(z.B1), NOMINAL_DIAMETER.S25); - across('B3', sPos + mm(off.B3), bottom + mm(z.B3), bottom + mm(z.B3) + mm(legLength('S19')), NOMINAL_DIAMETER.S19); + along('B1', n + mm(off.B1), bottom + mm(z.B1), bottom + mm(z.B1) - mm(legLength('S25')), NOMINAL_DIAMETER.S25); + along('B2', n, bottom + mm(z.B24), bottom + mm(z.B1), NOMINAL_DIAMETER.S25); + along('B3', n + mm(off.B3), bottom + mm(z.B3), bottom + mm(z.B3) + mm(legLength('S19')), NOMINAL_DIAMETER.S19); } else { - across('B4', sPos, bottom + mm(z.B24), undefined, NOMINAL_DIAMETER.S25); + along('B4', n, bottom + mm(z.B24), undefined, NOMINAL_DIAMETER.S25); } } - // B5–B8 run along L (at each W position n): odd positions carry B5, B6, B7, even carry B8. - for (let j = 0; j <= gW.n; j++) { - const nPos = posW(j); - if (j % 2 === 1) { + for (let i = 0; i <= gL.n; i++) { + const s = posL(i); + if (i % 2 === 1) { // B5 ⊓ down, B6 ⊔ up to the B5 height, B7 ⊔ up. B5·B7 sit beside the B6 leg (+28.6 / +23.85, §6.3). - along('B5', nPos + mm(off.B5), bottom + mm(z.B5), bottom + mm(z.B5) - mm(legLength('S29')), NOMINAL_DIAMETER.S29); - along('B6', nPos, bottom + mm(z.B68), bottom + mm(z.B5), NOMINAL_DIAMETER.S29); - along('B7', nPos + mm(off.B7), bottom + mm(z.B7), bottom + mm(z.B7) + mm(legLength('S19')), NOMINAL_DIAMETER.S19); + across('B5', s + mm(off.B5), bottom + mm(z.B5), bottom + mm(z.B5) - mm(legLength('S29')), NOMINAL_DIAMETER.S29); + across('B6', s, bottom + mm(z.B68), bottom + mm(z.B5), NOMINAL_DIAMETER.S29); + across('B7', s + mm(off.B7), bottom + mm(z.B7), bottom + mm(z.B7) + mm(legLength('S19')), NOMINAL_DIAMETER.S19); } else { - along('B8', nPos, bottom + mm(z.B68), undefined, NOMINAL_DIAMETER.S29); + across('B8', s, bottom + mm(z.B68), undefined, NOMINAL_DIAMETER.S29); } } // B9 (확대기초철근 (1).md §9, rule applied): vertical H16 bars on the grid, outside the column zones, with 135° hooks. @@ -475,8 +472,8 @@ export function buildRebar3D(m: PierModel, common: Material = DEFAULT_MATERIAL, } } // B11 (across W, spread along L) and B12 (along L, spread across W) at the assembly levels, 4s apart, 2s stagger per level. - const xsL = gridPositions(Lf * 1000, fs, 'sL'); - const ysW = gridPositions(Wf * 1000, fs, 'sW'); + const xsL = gridPositions(Lf * 1000, fs, 'sPerp'); + const ysW = gridPositions(Wf * 1000, fs, 'sAxis'); const dAsm = assemblyEndOffset(); const nAsm = Wf / 2 - mm(d1 + dAsm); const sAsm = Lf / 2 - mm(d2 + dAsm); diff --git a/tests/footing-rebar-inputs.test.ts b/tests/footing-rebar-inputs.test.ts index 36ba6cb..cd96124 100644 --- a/tests/footing-rebar-inputs.test.ts +++ b/tests/footing-rebar-inputs.test.ts @@ -9,17 +9,17 @@ const footingRows = (input: Parameters[0]) => const count = (rows: ReturnType, name: string) => rows.find((r) => r.name === name)!.count; describe('확대기초 철근은 입력 치수를 따른다 (3단계, 승인 2026-10-23)', () => { - it('P1 footing rebar is unchanged with the default inputs (B1 40, B5 31, B9 306; 방향 교환 후)', () => { + it('P1 footing rebar is unchanged with the default inputs (B1 31, B5 40, B9 306)', () => { const rows = footingRows(loadP1()); - expect(count(rows, 'B1')).toBe(40); - expect(count(rows, 'B5')).toBe(31); + expect(count(rows, 'B1')).toBe(31); + expect(count(rows, 'B5')).toBe(40); expect(count(rows, 'B9')).toBe(306); }); it('a wider footing (W 9000) and a longer one (L 11000) add bars to B1–B4 and B5–B8', () => { const rows = footingRows({ ...loadP1(), footingWidthMm: 9000, footingLengthMm: 11000 }); - expect(count(rows, 'B1')).toBeGreaterThan(40); - expect(count(rows, 'B5')).toBeGreaterThan(31); + expect(count(rows, 'B1')).toBeGreaterThan(31); + expect(count(rows, 'B5')).toBeGreaterThan(40); }); it('a different footing height (2200) is computed without error, and the 3D rebar still builds', () => { diff --git a/tests/footing-rebar-settings.test.ts b/tests/footing-rebar-settings.test.ts index 2e1d3ab..be2ce4c 100644 --- a/tests/footing-rebar-settings.test.ts +++ b/tests/footing-rebar-settings.test.ts @@ -20,8 +20,8 @@ describe('확대기초 배근 입력 (승인 2026-10-26, 시안 ③ 확대기초 }); it('the grid spacing s reaches the grid positions and the rebar table', () => { - const m = buildPier({ ...loadP1(), footingRebar: { sL: 150 } }); - expect(m.footingRebar.sL).toBe(150); + const m = buildPier({ ...loadP1(), footingRebar: { sAxis: 150 } }); + expect(m.footingRebar.sAxis).toBe(150); const xs = gridPositions(8000, m.footingRebar); expect(xs[1] - xs[0]).toBeCloseTo(150, 6); const b4 = rebarSummary(m).rows.find((r) => r.name === 'B4')!; @@ -36,7 +36,7 @@ describe('확대기초 배근 입력 (승인 2026-10-26, 시안 ③ 확대기초 const b10 = base.find((r) => r.name === 'B10-1')!; expect([b10.spacing, b10.basis]).toEqual(['200', 'default']); - const rows = rebarSummary(buildPier({ ...loadP1(), footingRebar: { sL: 150 } })).rows; + const rows = rebarSummary(buildPier({ ...loadP1(), footingRebar: { sAxis: 150 } })).rows; const b2s = rows.find((r) => r.name === 'B2')!; expect([b2s.spacing, b2s.actual, b2s.basis]).toEqual(['300', '150', 'input']); const b1s = rows.find((r) => r.name === 'B1')!; @@ -44,20 +44,18 @@ describe('확대기초 배근 입력 (승인 2026-10-26, 시안 ③ 확대기초 expect(rows.find((r) => r.name === 'B10-1')!.basis).toBe('default'); }); - // 방향 교환 (승인 2026-10-26): sL = spacing along L (B1–B4, B9 columns, B11); sW = spacing along W (B5–B8, B9 rows, B12). - it('방향 (교환 후): sW는 B5–B8·B12, sL은 B1–B4·B9·B11', () => { - const rows = rebarSummary(buildPier({ ...loadP1(), footingRebar: { sW: 150 } })).rows; + it('방향 (문서 기준, 승인 2026-10-26): B1–B4·B9·B12는 교각축(sAxis), B5–B8·B11은 교각축 직각(sPerp)', () => { + const rows = rebarSummary(buildPier({ ...loadP1(), footingRebar: { sPerp: 150 } })).rows; expect(rows.find((r) => r.name === 'B5')!.spacing).toBe('300'); expect(rows.find((r) => r.name === 'B5')!.basis).toBe('input'); - expect(rows.find((r) => r.name === 'B12')!.spacing).toBe('1200 (8s)'); - expect(rows.find((r) => r.name === 'B11')!.basis).toBe('default'); + expect(rows.find((r) => r.name === 'B11')!.spacing).toBe('1200 (8s)'); + expect(rows.find((r) => r.name === 'B12')!.basis).toBe('default'); expect(rows.find((r) => r.name === 'B1')!.basis).toBe('default'); - const rowsL = rebarSummary(buildPier({ ...loadP1(), footingRebar: { sL: 150 } })).rows; - expect(rowsL.find((r) => r.name === 'B1')!.spacing).toBe('300'); - expect(rowsL.find((r) => r.name === 'B9')!.spacing).toBe('600 (4s)'); - expect(rowsL.find((r) => r.name === 'B11')!.spacing).toBe('1200 (8s)'); - expect(rowsL.find((r) => r.name === 'B5')!.basis).toBe('default'); + const rowsAxis = rebarSummary(buildPier({ ...loadP1(), footingRebar: { sAxis: 150 } })).rows; + expect(rowsAxis.find((r) => r.name === 'B9')!.spacing).toBe('600 (4s)'); + expect(rowsAxis.find((r) => r.name === 'B12')!.spacing).toBe('1200 (8s)'); + expect(rowsAxis.find((r) => r.name === 'B5')!.basis).toBe('default'); }); it('k 배수: k 4 → 간격 500, 개수 60 / 47 (문서 4s 값)', () => { diff --git a/tests/footing-section-svg.test.ts b/tests/footing-section-svg.test.ts index 2ffc93e..69071c6 100644 --- a/tests/footing-section-svg.test.ts +++ b/tests/footing-section-svg.test.ts @@ -17,15 +17,16 @@ describe('확대기초 교각축 직각 단면 (폭 W) (승인 2026-10-26, 추 const svg = footingSectionSvg(m); it('point and line counts match the rebar table families', () => { - const cross = crossBars(L, W, H, fs); - // B1–B4 run along W: one line per family in this section; B5–B8 run along L: points on the W grid (방향 교환). - for (const name of ['B1', 'B2', 'B3', 'B4']) expect(countMark(svg, name.toLowerCase(), name)).toBe(1); - for (const name of ['B5', 'B6', 'B7', 'B8']) expect(countMark(svg, name.toLowerCase(), name)).toBe(cross.find((b) => b.name === name)!.count); + const bottom = bottomBars(L, W, H, fs); + expect(countMark(svg, 'b1', 'B1')).toBe(bottom.find((b) => b.name === 'B1')!.count); + expect(countMark(svg, 'b2', 'B2')).toBe(bottom.find((b) => b.name === 'B2')!.count); + expect(countMark(svg, 'b4', 'B4')).toBe(bottom.find((b) => b.name === 'B4')!.count); expect(countMark(svg, 'b9', 'B9')).toBe(b9Bars(L, W, [], 2000, fs).rows); expect(countMark(svg, 'b10', 'B10-1')).toBe(sideBars(L, W, H, fs).find((b) => b.name === 'B10-1')!.count); expect(countMark(svg, 'b11', 'B11')).toBe(assemblyJs(H, fs).length); expect(countMark(svg, 'b12', 'B12')).toBe(assemblyBars(L, W, H, fs).find((b) => b.name === 'B12')!.count); - expect(cross.length).toBe(4); + for (const name of ['B5', 'B6', 'B7', 'B8']) expect(countMark(svg, 'b5', name)).toBe(1); + expect(crossBars(L, W, H, fs).length).toBe(4); }); it('shows the width, the grid string, and the B10 levels as dimensions', () => { diff --git a/tests/rebar-footing.test.ts b/tests/rebar-footing.test.ts index 54da35f..5536899 100644 --- a/tests/rebar-footing.test.ts +++ b/tests/rebar-footing.test.ts @@ -31,25 +31,24 @@ describe('확대기초 철근 (확대기초철근 (1).md, plan C5)', () => { expect(z.B7).toBeCloseTo(277, 6); }); - // 방향 교환 (승인 2026-10-26): B1–B4 run along W (grid L, 79 steps → 80 positions); B5–B8 run along L (grid W). - it('B2 leg 1723, B2 length 11396 (W − 2c + 2 × 1723), count 40', () => { + it('B2 leg 1723, B2 length 13646 (over the 12000 bar, splice needed)', () => { expect(legB2(H)).toBeCloseTo(1723, 6); const b2 = bottomBars(L, W, H).find((b) => b.name === 'B2')!; - expect(b2.length).toBeCloseTo(11396, 0); - expect(b2.count).toBe(40); + expect(b2.length).toBeCloseTo(13646, 0); + expect(b2.count).toBe(31); }); - it('B1–B4 counts 40 / 40 / 40 / 40 (grid along L) and B4 length 7950 (W − 2c)', () => { + it('B1–B4 counts 31 / 31 / 31 / 31 and B4 length 10200', () => { const bars = bottomBars(L, W, H); - expect(bars.map((b) => b.count)).toEqual([40, 40, 40, 40]); - expect(bars.find((b) => b.name === 'B4')!.length).toBeCloseTo(7950, 6); + expect(bars.map((b) => b.count)).toEqual([31, 31, 31, 31]); + expect(bars.find((b) => b.name === 'B4')!.length).toBeCloseTo(10200, 6); }); - it('B5–B8 counts 31 each (grid along W: n 61), B8 length 10200 (L − 2c), B6 length 13646', () => { + it('B5–B8 counts 40 each (grid along L: n 79), B8 length 7950, B6 length 11396', () => { const bars = crossBars(L, W, H); - expect(bars.map((b) => b.count)).toEqual([31, 31, 31, 31]); - expect(bars.find((b) => b.name === 'B8')!.length).toBeCloseTo(10200, 6); - expect(bars.find((b) => b.name === 'B6')!.length).toBeCloseTo(13646, 0); + expect(bars.map((b) => b.count)).toEqual([40, 40, 40, 40]); + expect(bars.find((b) => b.name === 'B8')!.length).toBeCloseTo(7950, 6); + expect(bars.find((b) => b.name === 'B6')!.length).toBeCloseTo(11396, 0); }); it('B10-1 16 bars ≈ 10748 (lowest level removed), B10-2 16 bars ≈ 8494', () => { @@ -109,29 +108,29 @@ describe('확대기초 배근도 재현 (확대기초철근 (1).md §13)', () => expect(h.B12.map((z) => Math.round(z * 10) / 10)).toEqual([569.1, 969.1, 1369.1]); }); - it('배근도 재현 W 7800 → B8 30, B6 29 (B5–B8 follow W, odd count split)', () => { - const bars = crossBars(10400, 7800, 2000); - expect(bars.find((b) => b.name === 'B8')!.count).toBe(30); - expect(bars.find((b) => b.name === 'B6')!.count).toBe(29); + it('배근도 재현 W 7800 → B4 30, B2 29 (odd count split)', () => { + const bars = bottomBars(10400, 7800, 2000); + expect(bars.find((b) => b.name === 'B4')!.count).toBe(30); + expect(bars.find((b) => b.name === 'B2')!.count).toBe(29); }); - it('배근도 재현 L 9350 → B4 36, B2 35 (B1–B4 follow L)', () => { - const bars = bottomBars(9350, 8150, 2000); - expect(bars.find((b) => b.name === 'B4')!.count).toBe(36); - expect(bars.find((b) => b.name === 'B2')!.count).toBe(35); + it('배근도 재현 L 9350 → B8 36, B6 35', () => { + const bars = crossBars(9350, 8150, 2000); + expect(bars.find((b) => b.name === 'B8')!.count).toBe(36); + expect(bars.find((b) => b.name === 'B6')!.count).toBe(35); }); it('variables: W 8000 with s 150 → 50 grid steps, e 150, B4 26 / B2 25; W 12000 → 92 steps, e 150', () => { - const g = grid(8000, { ...DEFAULT_FOOTING_REBAR, sL: 150 }); + const g = grid(8000, { ...DEFAULT_FOOTING_REBAR, sAxis: 150 }); expect(g.n).toBe(50); expect(g.e).toBeCloseTo(150, 6); expect(grid(12000).n).toBe(92); expect(grid(12000).e).toBeCloseTo(150, 6); }); - it('splice cases: W 8700 → B2 11946 (no splice); W 12400 → B4 12200; L 8800 → B6 12046', () => { - expect(bottomBars(10400, 8700, 2000).find((b) => b.name === 'B2')!.length).toBeCloseTo(11946, 0); - expect(bottomBars(10400, 12400, 2000).find((b) => b.name === 'B4')!.length).toBeCloseTo(12200, 0); - expect(crossBars(8800, 8150, 2000).find((b) => b.name === 'B6')!.length).toBeCloseTo(12046, 0); + it('splice cases: L 8700 → B2 11946 (no splice); L 12400 → B4 12200; W 8800 → B6 12046', () => { + expect(bottomBars(8700, 8150, 2000).find((b) => b.name === 'B2')!.length).toBeCloseTo(11946, 0); + expect(bottomBars(12400, 8150, 2000).find((b) => b.name === 'B4')!.length).toBeCloseTo(12200, 0); + expect(crossBars(10400, 8800, 2000).find((b) => b.name === 'B6')!.length).toBeCloseTo(12046, 0); }); }); diff --git a/tests/rebar-member-fck.test.ts b/tests/rebar-member-fck.test.ts index 989184d..fbe47be 100644 --- a/tests/rebar-member-fck.test.ts +++ b/tests/rebar-member-fck.test.ts @@ -41,11 +41,11 @@ describe('② 부재별 fck (spec §6.2 · ②)', () => { expect(changedIn(rows, 'column').length).toBeGreaterThan(0); }); - it('a footing fck changes only the footing rows (B6 splice, 방향 교환)', () => { + it('a footing fck changes only the footing rows (B2 splice)', () => { const rows = rebarSummary(model, DEFAULT_MATERIAL, { footing: 35 }).rows; expect(changedIn(rows, 'coping')).toEqual([]); expect(changedIn(rows, 'column')).toEqual([]); - expect(changedIn(rows, 'footing')).toContain('B6'); + expect(changedIn(rows, 'footing')).toContain('B2'); }); it('a per-member fck equal to the 공통 value changes nothing', () => { diff --git a/tests/rebar-summary.test.ts b/tests/rebar-summary.test.ts index 27345a6..6e286c6 100644 --- a/tests/rebar-summary.test.ts +++ b/tests/rebar-summary.test.ts @@ -35,21 +35,21 @@ describe('③ 철근표 요약 (P1)', () => { expect(count('P6')).toBe(32); }); - it('footing (방향 교환): B1–B4 40 each, B5–B8 31 each, B10-1 16, B10-2 16, B11 30, B12 24 (k = 8)', () => { - for (const n of ['B1', 'B2', 'B3', 'B4']) expect(count(n)).toBe(40); - for (const n of ['B5', 'B6', 'B7', 'B8']) expect(count(n)).toBe(31); + it('footing: B1–B4 31 each, B5–B8 40 each, B10-1 16, B10-2 16, B11 30, B12 24 (k = 8)', () => { + for (const n of ['B1', 'B2', 'B3', 'B4']) expect(count(n)).toBe(31); + for (const n of ['B5', 'B6', 'B7', 'B8']) expect(count(n)).toBe(40); expect(count('B10-1')).toBe(16); expect(count('B10-2')).toBe(16); expect(count('B11')).toBe(30); expect(count('B12')).toBe(24); }); - it('B9: 306 bars (320 on the grid less 14 in the column zone); warnings: B6 needs a splice (13646 > 12000)', () => { + it('B9: 306 bars (320 on the grid less 14 in the column zone); warnings: B2 needs a splice (13646 > 12000)', () => { expect(count('B9')).toBe(306); expect(warnings.map((w) => w.code)).toEqual(['I02', 'RB-S04', 'RB-F08']); // defaults are assumed values (I02); bearing embedment 619–669 < 882 (RB-S04) - expect(warnings.find((w) => w.code === 'RB-F08')!.bar).toBe('B6'); - // B6 is split into two pieces (추천안): each piece is about half the 13646 bar plus half the lap. - expect(rows.find((r) => r.name === 'B6')!.length!).toBeCloseTo(7776, 0); + expect(warnings.find((w) => w.code === 'RB-F08')!.bar).toBe('B2'); + // 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', () => {