/** * 말뚝 열별 본수 (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; /** True when the spacing is above the maximum 2.25 m (warning: add piles). */ aboveMaximum: boolean; } /** Maximum centre spacing of the piles along a 열 (m), spec §4.6 (승인 2026-10-23: 2250 mm). */ export const PILE_SPACING_MAX = 2.25; 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, aboveMaximum: spacing > PILE_SPACING_MAX + 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}본`; }