diff --git a/src/core/rebar/footing-settings.ts b/src/core/rebar/footing-settings.ts index d6bfe52..874e026 100644 --- a/src/core/rebar/footing-settings.ts +++ b/src/core/rebar/footing-settings.ts @@ -25,6 +25,10 @@ export interface FootingRebarSettings { eTop: number; /** 조립 단 배수 n (조립 단 간격 = n·s_v). */ n: number; + /** B11 수평간격 배수 k: 간격 = k · s교각축직각 (L 방향 격자, 승인 2026-10-26). */ + kB11: number; + /** B12 수평간격 배수 k: 간격 = k · s교각축 (W 방향 격자, 승인 2026-10-26). */ + kB12: number; } export const DEFAULT_FOOTING_REBAR: FootingRebarSettings = { @@ -37,6 +41,8 @@ export const DEFAULT_FOOTING_REBAR: FootingRebarSettings = { sV: 200, eTop: 150, n: 2, + kB11: 8, + kB12: 8, }; /** Allowed range per input (inclusive, mm; n is a count). */ @@ -50,6 +56,8 @@ export const FOOTING_REBAR_RANGES: Record + // Bars every k grid steps (승인 2026-10-26: k·s); the levels alternate a 2-step stagger (엇갈림 2s). + const perLevel = (n: number, k: number) => levelsJ.map((_, idx) => { - const i0 = idx % 2 === 0 ? startOdd : startEven; - return Math.floor((n - i0) / 4) + 1; + const i0 = idx % 2 === 0 ? 0 : 2; + return Math.floor((n - i0) / k) + 1; }); - const nW = grid(W, f, 'sPerp').positions - 1; // 61 (B12 uses the 교각축 직각 value, 승인 2026-10-26) - const nL = grid(L, f, 'sPerp').positions - 1; // 79 (B11 along L) - // B11 is spread along L (n = 79) and B12 along W (n = 61); each level has its own count. - const b11 = perLevel(nL, 0, 2).reduce((a, b) => a + b, 0); - const b12 = perLevel(nW, 0, 2).reduce((a, b) => a + b, 0); + // B11 is spread along L (교각축 직각 grid) and B12 along W (교각축 grid), per 확대기초철근 (1).md §8. + const nW = grid(W, f, 'sAxis').positions - 1; + const nL = grid(L, f, 'sPerp').positions - 1; + const b11 = perLevel(nL, f.kB11).reduce((a, b) => a + b, 0); + const b12 = perLevel(nW, f.kB12).reduce((a, b) => a + b, 0); const hook = Math.round(hookLength('main90', d('S16'))); // 246 mm, rounded as in the document (§8.3) const depth1 = sideDepth(NOMINAL_DIAMETER.S29, NOMINAL_DIAMETER.S19, f); const depth2 = sideDepth(NOMINAL_DIAMETER.S25, NOMINAL_DIAMETER.S19, f); @@ -245,7 +246,8 @@ export function footingRuleValues(H: number, f: FootingRebarSettings = DEFAULT_F b6Leg: number; b9Pitch: number; b9Stagger: number; - assemblyPlanPitch: number; + b11Pitch: number; + b12Pitch: number; assemblyVertical: number; assemblyLevels: number; } { @@ -258,7 +260,8 @@ export function footingRuleValues(H: number, f: FootingRebarSettings = DEFAULT_F b6Leg: z.B5 - z.B68, b9Pitch: 4 * f.sAxis, b9Stagger: 2 * f.sAxis, - assemblyPlanPitch: 4 * f.sPerp, + 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 91b0f94..fc24150 100644 --- a/src/core/rebar/summary.ts +++ b/src/core/rebar/summary.ts @@ -173,7 +173,9 @@ export function linkedSpacing(name: string, model: PierModel): Pick r.name === 'B10-1')!.basis).toBe('default'); }); - it('교각축 직각 값은 B5–B8·B11·B12에만, 교각축 값은 B1–B4·B9에 적용 (승인 2026-10-26)', () => { + it('방향 (문서 기준, 승인 2026-10-26): B1–B4·B9·B12는 교각축(sAxis), B5–B8·B11은 교각축 직각(sPerp)', () => { const rows = rebarSummary(buildPier({ ...loadP1(), footingRebar: { sPerp: 150 } })).rows; - const b5 = rows.find((r) => r.name === 'B5')!; - expect([b5.spacing, b5.basis]).toEqual(['300', 'input']); - const b12 = rows.find((r) => r.name === 'B12')!; - expect([b12.spacing, b12.basis]).toEqual(['600 (4s)', 'input']); + 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 === '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'); - expect(rows.find((r) => r.name === 'B9')!.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('패널 규칙 값 (P1, H 2000): 상면·2단 250 · Δ₂ 100 · B2·B6 1723 · B9 500·250 · 조립 500 · 3단 @400', () => { - const r = footingRuleValues(2000, DEFAULT_FOOTING_REBAR); - expect([r.upper2sAxis, r.upper2sPerp, r.delta2]).toEqual([250, 250, 100]); - expect(r.b2Leg).toBeCloseTo(1723, 0); - expect(r.b6Leg).toBeCloseTo(1723, 0); - expect([r.b9Pitch, r.b9Stagger]).toEqual([500, 250]); - expect([r.assemblyPlanPitch, r.assemblyVertical, r.assemblyLevels]).toEqual([500, 400, 3]); + it('k 배수: k 4 → 간격 500, 개수 60 / 47 (문서 4s 값)', () => { + const rows = rebarSummary(buildPier({ ...loadP1(), footingRebar: { kB11: 4, kB12: 4 } })).rows; + expect(rows.find((r) => r.name === 'B11')!.count).toBe(60); + expect(rows.find((r) => r.name === 'B12')!.count).toBe(47); }); it('the 조립 단 배수 n reaches the assembly levels', () => { diff --git a/tests/rebar-footing.test.ts b/tests/rebar-footing.test.ts index 049d9c2..29e4dfb 100644 --- a/tests/rebar-footing.test.ts +++ b/tests/rebar-footing.test.ts @@ -66,11 +66,12 @@ describe('확대기초 철근 (확대기초철근 (1).md, plan C5)', () => { expect(assemblyJs(2200)).toEqual([2, 4, 6, 8]); }); - it('B11 60 bars (20 / 20 / 20) ≈ 8359, B12 47 bars (16 / 15 / 16) ≈ 10613', () => { + // k = 8 default (승인 2026-10-26): every 8 grid steps (1000 mm). The doc's 4s (k = 4) gives 60 / 47. + it('B11 30 bars (10 / 10 / 10) ≈ 8359, B12 24 bars (8 / 8 / 8) ≈ 10613 at k = 8', () => { const [b11, b12] = assemblyBars(L, W, H); - expect(b11.count).toBe(60); + expect(b11.count).toBe(30); expect(b11.length).toBeCloseTo(8359, 0); - expect(b12.count).toBe(47); + expect(b12.count).toBe(24); expect(b12.length).toBeCloseTo(10612.5, 1); // the document prints ≈ 10613 }); diff --git a/tests/rebar-summary.test.ts b/tests/rebar-summary.test.ts index cc77061..b8915ab 100644 --- a/tests/rebar-summary.test.ts +++ b/tests/rebar-summary.test.ts @@ -35,13 +35,13 @@ describe('③ 철근표 요약 (P1)', () => { expect(count('P6')).toBe(32); }); - it('footing: B1–B4 31 each, B5–B8 40 each, B10-1 18, B10-2 18, B11 60, B12 47', () => { + it('footing: B1–B4 31 each, B5–B8 40 each, B10-1 18, B10-2 18, 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(18); expect(count('B10-2')).toBe(18); - expect(count('B11')).toBe(60); - expect(count('B12')).toBe(47); + 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: B2 needs a splice (13646 > 12000)', () => { diff --git a/tests/rebar3d.test.ts b/tests/rebar3d.test.ts index 73b64e8..d0d70e6 100644 --- a/tests/rebar3d.test.ts +++ b/tests/rebar3d.test.ts @@ -108,11 +108,11 @@ describe('확대기초 B9 · B10 · B11 · B12 in 3D (확대기초철근 (1).md expect(rb.byName.get('B9')!.length).toBe(306 * 3); }); - it('B10-1 / B10-2: 18 each (9 levels × 2 faces); B11 60, B12 47 — one tube per bar (bends rounded, 수정 2026-10-23)', () => { + it('B10-1 / B10-2: 18 each (9 levels × 2 faces); B11 30, B12 24 at k = 8 — one tube per bar (bends rounded, 수정 2026-10-23)', () => { expect(rb.byName.get('B10-1')!.length).toBe(18); expect(rb.byName.get('B10-2')!.length).toBe(18); - expect(rb.byName.get('B11')!.length).toBe(60); - expect(rb.byName.get('B12')!.length).toBe(47); + expect(rb.byName.get('B11')!.length).toBe(30); + expect(rb.byName.get('B12')!.length).toBe(24); }); });