feat(core,view): per-row pile counts 열별 본수 (plan C4)

- A 열 is one row across the footing width; each has its own count along the
  length. Default floor((L − 1400) / 2250) + 1 per 열, as in spec §4.6.
- Counts are validated: one per 열, whole numbers 2–30. Spacing below the
  2250 mm minimum is shown as a warning, not refused (the plan's 6본 → 5@1800
  example).
- PierInput gains an optional pileRows; absent means the defaults, so existing
  files and the P1 results are unchanged.
- Change tracking compares arrays by value; 열별 본수 can be reverted and
  copied (copy item off by default).
- The footing card has −/+ per 열 with spacing, total, and the display string.
- Tests: defaults 4열 × 5본 = 20 and the 1단계 text; 6본 → 5@1800; limits;
  model-level 21 piles.
- Not verified in a browser.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-10-22 10:34:39 +09:00
1 parent 5b162c2441
commit 17b5b24682
9 files changed
+256 -39

No files matched your search

+20 -25
View File
@@ -1,4 +1,5 @@
import { buildAxis, copingSpan, fromAxis, kinkArc, nearKink, toAxis } from './axis';
import { layoutText, resolveRowCounts, rowLayout, type PileGeometry } from './piles';
import type { PierInput, Row } from './types';
/** Dimensions in m. Sources: intent.PR sheet (P1 rules) and spec.md §3 defaults. */
@@ -363,7 +364,9 @@ export function buildPier(input: PierInput): PierModel {
thickness: DIM.blindingT,
});
if (input.foundationType === '말뚝기초') {
piles.push(...pilesFor(f.s, input.footingBottomEl, pileLength, f.length).map((p) => ({ ...p, k: f.k })));
const g = pileGeometry(f.length);
const counts = resolveRowCounts(input.pileRows, g);
piles.push(...pilesFor(f.s, input.footingBottomEl, pileLength, g, counts).map((p) => ({ ...p, k: f.k })));
}
}
}
@@ -419,7 +422,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(footings[0].length) : null,
pileLayout: input.foundationType === '말뚝기초' ? layoutText(resolveRowCounts(input.pileRows, pileGeometry(footings[0].length)), pileGeometry(footings[0].length)) : null,
pileTipEl: piles.length ? pileTip : null,
blockHeightFront,
blockHeightBack,
@@ -460,38 +463,30 @@ export function footingGroups(colS: number[]): { s: number; length: number }[] {
* 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(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(footingLength);
const countN = span(DIM.footingW);
return {
countS,
countN,
spacingS: (footingLength - 2 * DIM.pileEnd) / (countS - 1),
spacingN: (DIM.footingW - 2 * DIM.pileEnd) / (countN - 1),
};
/** Pile geometry of one footing, from the 1단계 constants (spec §4.6). */
export function pileGeometry(footingLength: number): PileGeometry {
return { footingLength, footingWidth: DIM.footingW, end: DIM.pileEnd, minSpacing: DIM.pileSpacing };
}
/** Display string such as "4@2250 × 3@2250 = 20본" (gaps along the length × gaps across the width). */
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, footingLength: number = DIM.footingL): PileModel[] {
const { countS, countN, spacingS, spacingN } = pileGrid(footingLength);
/**
* Piles under one footing (spec §4.6): one 열 per row across the width, each with its own count along the length.
* counts holds the 본수 of each 열 (see core/piles.ts).
*/
export function pilesFor(s: number, topEl: number, length: number, g: PileGeometry, counts: number[]): PileModel[] {
const rows = rowLayout(counts, g);
const spacingN = (g.footingWidth - 2 * g.end) / (counts.length - 1);
const out: PileModel[] = [];
for (let i = 0; i < countS; i++) {
for (let j = 0; j < countN; j++) {
rows.forEach((row, j) => {
for (let i = 0; i < row.count; i++) {
out.push({
s: s + (i - (countS - 1) / 2) * spacingS,
s: s + (i - (row.count - 1) / 2) * row.spacing,
k: 0,
n: (j - (countN - 1) / 2) * spacingN,
n: (j - (counts.length - 1) / 2) * spacingN,
top: topEl,
bottom: topEl - length,
diameter: DIM.pileDiameter,
});
}
}
});
return out;
}