Files
Pier_v1/src/core/piles.ts
T
ypshimandClaude Haiku 5.5 17b5b24682 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>
2026-10-22 10:34:39 +09:00

90 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 말뚝 열별 본수 (spec §4.6 · §5.4 · plan C4). A 열 is one row across the footing width; 본 are along the length.
* Default: each 열 gets floor((길이 − 2·단부)/간격) + 1 본, so the centre spacing is at least the minimum.
* The user may change any 열's count. Counts below the minimum spacing are allowed and reported as a warning.
*/
export interface PileGeometry {
footingLength: number;
footingWidth: number;
/** 단부 거리 (700 mm). */
end: number;
/** 최소 중심간격 (2250 mm). */
minSpacing: number;
}
/** A count outside this range is refused: fewer than two piles has no spacing, and the cap guards against typing errors. */
export const PILE_COUNT_LIMITS = { min: 2, max: 30 } as const;
/** Largest count along one length with centres at or above the minimum spacing. */
export function maxCount(length: number, g: PileGeometry): number {
return Math.floor((length - 2 * g.end) / g.minSpacing) + 1;
}
/** Number of 열 across the width. */
export function rowsAcross(g: PileGeometry): number {
return maxCount(g.footingWidth, { ...g, minSpacing: g.minSpacing });
}
export function defaultRowCounts(g: PileGeometry): number[] {
return Array.from({ length: rowsAcross(g) }, () => maxCount(g.footingLength, g));
}
/** Counts to use for a footing: the requested 열별 본수 if any, otherwise the defaults. Refuses bad input. */
export function resolveRowCounts(requested: number[] | undefined, g: PileGeometry): number[] {
if (requested === undefined) return defaultRowCounts(g);
const rows = rowsAcross(g);
if (requested.length !== rows) {
throw new Error(`열별 본수는 ${rows}개여야 합니다 (입력 ${requested.length}개).`);
}
for (const c of requested) {
if (!Number.isInteger(c) || c < PILE_COUNT_LIMITS.min || c > PILE_COUNT_LIMITS.max) {
throw new Error(`본수는 ${PILE_COUNT_LIMITS.min}–${PILE_COUNT_LIMITS.max} 사이의 정수여야 합니다 (입력 ${c}).`);
}
}
return [...requested];
}
export interface RowLayout {
count: number;
/** Centre spacing along the length, in m. */
spacing: number;
/** True when the spacing is below the minimum (warning, not refused). */
belowMinimum: boolean;
}
export function rowLayout(counts: number[], g: PileGeometry): RowLayout[] {
return counts.map((count) => {
const spacing = (g.footingLength - 2 * g.end) / (count - 1);
return { count, spacing, belowMinimum: spacing < g.minSpacing - 1e-9 };
});
}
/** Total piles (sum of the counts along each 열). */
export function totalPiles(counts: number[]): number {
return counts.reduce((sum, c) => sum + c, 0);
}
/** Changes one 열's count by delta, staying inside the limits. Returns a new array. */
export function adjustRow(counts: number[], row: number, delta: number): number[] {
const next = [...counts];
next[row] = Math.min(PILE_COUNT_LIMITS.max, Math.max(PILE_COUNT_LIMITS.min, next[row] + delta));
return next;
}
/**
* Display string. Uniform counts keep the 1단계 form, e.g. "4@2250 × 3@2250 = 20본"
* (gaps along the length × gaps across the width). Non-uniform counts list each 열, e.g. "열1 5@2250 · 열2 6@1800 = 23본".
*/
export function layoutText(counts: number[], g: PileGeometry): string {
const layout = rowLayout(counts, g);
const total = totalPiles(counts);
const mm = (v: number) => Math.round(v * 1000);
const across = (g.footingWidth - 2 * g.end) / (counts.length - 1);
const uniform = counts.every((c) => c === counts[0]);
if (uniform) {
return `${layout[0].count - 1}@${mm(layout[0].spacing)} × ${counts.length - 1}@${mm(across)} = ${total}본`;
}
const parts = layout.map((r, j) => `열${j + 1} ${r.count - 1}@${mm(r.spacing)}`);
return `${parts.join(' · ')} = ${total}본`;
}