From ad59c0c74d92c6bdb62b4ccb891d12efb588f3e2 Mon Sep 17 00:00:00 2001 From: ypshim Date: Thu, 22 Oct 2026 09:01:40 +0900 Subject: [PATCH] =?UTF-8?q?feat(core):=20=EC=9D=BC=EC=B2=B4=EA=B8=B0?= =?UTF-8?q?=EC=B4=88=20footings=20for=202=C2=B73=EC=A3=BC=ED=98=95=20with?= =?UTF-8?q?=20per-footing=20pile=20grid?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Columns closer than one footing length (10400) share a 일체기초 whose length is the outer-column distance + 10400, centred between them (spec §4.6). - Pile grid is computed from each footing's length (rows across 8150, pile count along the length from (length − 1400) / 2250 + 1). - Blinding concrete follows each combined footing. Co-Authored-By: Claude Haiku 5.5 --- src/core/geometry.ts | 61 ++++++++++++++++++--------- src/core/validate.ts | 1 - tests/validate-and-foundation.test.ts | 24 +++++++++++ 3 files changed, 64 insertions(+), 22 deletions(-) diff --git a/src/core/geometry.ts b/src/core/geometry.ts index cbefd64..a3c6a81 100644 --- a/src/core/geometry.ts +++ b/src/core/geometry.ts @@ -240,21 +240,23 @@ export function buildPier(input: PierInput): PierModel { const pileLength = input.pileLengthMm / 1000; const pileTip = input.footingBottomEl - pileLength; - for (const s of colS) { - if (input.foundationType === '현장타설말뚝') { + if (input.foundationType === '현장타설말뚝') { + for (const s of colS) { piles.push({ s, n: 0, top: input.footingBottomEl, bottom: pileTip, diameter: DIM.columnDiameter }); - continue; } - footings.push({ s, top: footingTop, bottom: input.footingBottomEl, length: DIM.footingL, width: DIM.footingW }); - blindings.push({ - s, - top: input.footingBottomEl, - length: DIM.footingL + DIM.blindingMargin, - width: DIM.footingW + DIM.blindingMargin, - thickness: DIM.blindingT, - }); - if (input.foundationType === '말뚝기초') { - piles.push(...pilesFor(s, input.footingBottomEl, pileLength)); + } else { + for (const g of footingGroups(colS)) { + footings.push({ s: g.s, top: footingTop, bottom: input.footingBottomEl, length: g.length, width: DIM.footingW }); + blindings.push({ + s: g.s, + top: input.footingBottomEl, + length: g.length + DIM.blindingMargin, + width: DIM.footingW + DIM.blindingMargin, + thickness: DIM.blindingT, + }); + if (input.foundationType === '말뚝기초') { + piles.push(...pilesFor(g.s, input.footingBottomEl, pileLength, g.length)); + } } } @@ -306,7 +308,7 @@ export function buildPier(input: PierInput): PierModel { columnHeight: levelBottom - columns[0].bottom, soilCover: input.groundEl - (input.foundationType === '현장타설말뚝' ? input.footingBottomEl : footingTop), pileCount: piles.length, - pileLayout: input.foundationType === '말뚝기초' ? pileLayoutText() : null, + pileLayout: input.foundationType === '말뚝기초' ? pileLayoutText(footings[0].length) : null, pileTipEl: piles.length ? pileTip : null, blockHeightFront, blockHeightBack, @@ -326,30 +328,47 @@ function segmentTop(segments: Segment[], s: number): number { return hit.top; } +/** + * Footings (spec §4.6). Adjacent columns closer than one footing length share a 일체기초 whose length is + * the distance between its outer columns + DIM.footingL, centred between them. Other columns get their own footing. + */ +export function footingGroups(colS: number[]): { s: number; length: number }[] { + const groups: number[][] = []; + for (const s of [...colS].sort((a, b) => a - b)) { + const last = groups[groups.length - 1]; + if (last && s - last[last.length - 1] < DIM.footingL) last.push(s); + else groups.push([s]); + } + return groups.map((g) => ({ + s: (g[0] + g[g.length - 1]) / 2, + length: g[g.length - 1] - g[0] + DIM.footingL, + })); +} + /** * Piles under one footing (spec §4.6): rows across the footing width, piles along the length, * both with an end distance of 0.7 m and an even spacing that never exceeds 2.25 m. */ -export function pileGrid(): { countS: number; countN: number; spacingS: number; spacingN: number } { +export function pileGrid(footingLength: number = DIM.footingL): { countS: number; countN: number; spacingS: number; spacingN: number } { const span = (size: number) => Math.floor((size - 2 * DIM.pileEnd) / DIM.pileSpacing) + 1; - const countS = span(DIM.footingL); + const countS = span(footingLength); const countN = span(DIM.footingW); return { countS, countN, - spacingS: (DIM.footingL - 2 * DIM.pileEnd) / (countS - 1), + spacingS: (footingLength - 2 * DIM.pileEnd) / (countS - 1), spacingN: (DIM.footingW - 2 * DIM.pileEnd) / (countN - 1), }; } /** Display string such as "4@2250 × 3@2250 = 20본" (gaps along the length × gaps across the width). */ -export function pileLayoutText(): string { - const g = pileGrid(); +export function pileLayoutText(footingLength: number = DIM.footingL): string { + const g = pileGrid(footingLength); return `${g.countS - 1}@${Math.round(g.spacingS * 1000)} × ${g.countN - 1}@${Math.round(g.spacingN * 1000)} = ${g.countS * g.countN}본`; } -export function pilesFor(s: number, topEl: number, length: number): PileModel[] { - const { countS, countN, spacingS, spacingN } = pileGrid(); +export function pilesFor(s: number, topEl: number, length: number, footingLength: number = DIM.footingL): PileModel[] { + const { countS, countN, spacingS, spacingN } = pileGrid(footingLength); const out: PileModel[] = []; for (let i = 0; i < countS; i++) { for (let j = 0; j < countN; j++) { diff --git a/src/core/validate.ts b/src/core/validate.ts index 1719470..a4d92c9 100644 --- a/src/core/validate.ts +++ b/src/core/validate.ts @@ -8,7 +8,6 @@ export const NOT_IMPLEMENTED_RULES = [ 'E04 fck 미입력 (배근 단계, 3단계)', 'W06 속성 복사 건너뜀 (2단계)', 'W08 솔플레이트 T1−T2 검사 (2단계)', - '일체기초 판정 (2·3주형)', ] as const; /** Input-level checks (spec §10 E01, E02). Returns false when the pier must not be built. */ diff --git a/tests/validate-and-foundation.test.ts b/tests/validate-and-foundation.test.ts index 05894c4..871b9f4 100644 --- a/tests/validate-and-foundation.test.ts +++ b/tests/validate-and-foundation.test.ts @@ -106,3 +106,27 @@ describe('bearing and block checks (spec §10 W03, W04)', () => { expect(issues.map((i) => i.code)).toContain('W04'); }); }); + +describe('일체기초 (spec §4.6)', () => { + it('2주형 L 12000: columns 7200 apart share one footing of 17600 with 4 × 8 piles', () => { + const m = buildPier({ ...loadP1(), type: '2주형', columnCount: 2, copingLengthMm: 12000, foundationType: '말뚝기초' }); + expect(m.footings).toHaveLength(1); + expect(m.footings[0].length).toBeCloseTo(17.6, 6); + expect(m.footings[0].s).toBeCloseTo(6.0, 6); + expect(m.blindings[0].length).toBeCloseTo(17.8, 6); + expect(m.piles).toHaveLength(32); + expect(m.derived.pileLayout).toBe('7@2314 × 3@2250 = 32본'); + }); + + it('3주형 L 12000: three columns 3600 apart share one footing', () => { + const m = buildPier({ ...loadP1(), type: '3주형', columnCount: 3, copingLengthMm: 12000, foundationType: '직접기초' }); + expect(m.footings).toHaveLength(1); + expect(m.footings[0].length).toBeCloseTo(17.6, 6); + }); + + it('2주형 with columns 18000 apart: separate footings of 10400', () => { + const m = buildPier({ ...loadP1(), type: '2주형', columnCount: 2, copingLengthMm: 30000, foundationType: '직접기초' }); + expect(m.footings).toHaveLength(2); + expect(m.footings.every((f) => Math.abs(f.length - 10.4) < 1e-9)).toBe(true); + }); +});