- 구조물 › 배관 1본 길이 · 토공 › 연암 · 경암 % 입력(공용 compact 입력 · 접이 카드) - 조작 = 캐시 b08-options · [저장] = 프로젝트 층 M02 구조물집계표 변수 · project_settings rock_ratios_pct - 브라우저 짝 식 파일 B08_Quantity_Options_Calc.ts Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BVPp2o6Mh2FZJr72yW3ofZ
109 lines
4.7 KiB
TypeScript
109 lines
4.7 KiB
TypeScript
/* =============================================================================
|
||
* B08_Quantity_Options_Calc.ts
|
||
* 좌측 패널 옵션 — 조작 중 브라우저가 바로 다시 세는 식(화면 없음 · PLAN 122).
|
||
*
|
||
* 짝(파이썬 · 같은 식) — 거울 시험 `test_122_b08_options_mirror`:
|
||
* couplings · recountCouplings ↔ `B08_Quantity_Router_Summary.py` couplings(관마다 `pipes`)
|
||
* rockSplit · splitRockColumns · splitRockSummary
|
||
* ↔ `B08_Quantity_Engine_Earthwork_Summary.py` rock_split ·
|
||
* split_rock_columns · summary_rows(연암 · 경암 네 줄)
|
||
* [저장] 뒤에는 서버가 정본으로 다시 셈 — 여기 값을 받아 적지 않음.
|
||
* import 없음 — 시험이 이 파일만 옮겨 돌림.
|
||
* ========================================================================== */
|
||
|
||
/** 커플링 = 올림(L ÷ 1본) − 1(0 아래 없음) · 길이나 1본이 빈칸이면 null. */
|
||
export function couplings(lengthM: number | null | undefined, oneM: number | null): number | null {
|
||
if (!lengthM || !oneM) return null;
|
||
return Math.max(Math.ceil(lengthM / oneM - 1e-9) - 1, 0);
|
||
}
|
||
|
||
export interface PipeLength {
|
||
chainage_m: number;
|
||
/** 커플링 열 id(`pipe:cp|관경`) */
|
||
column: string;
|
||
length_m: number | null;
|
||
}
|
||
|
||
interface CouplingSummary {
|
||
rows: Array<{ chainage_m: number; values: Record<string, number> }>;
|
||
total: Record<string, number>;
|
||
pipes: PipeLength[];
|
||
}
|
||
|
||
/** 커플링 열만 새 1본 길이로 — 같은 측점(1 cm)의 같은 열은 더함 · 계 = 줄 합. */
|
||
export function recountCouplings<S extends CouplingSummary>(summary: S, oneM: number | null): S {
|
||
const at = (chainage: number): number => Math.round(chainage * 100);
|
||
const columns = new Set(summary.pipes.map((pipe) => pipe.column));
|
||
const sums = new Map<string, number>();
|
||
for (const pipe of summary.pipes) {
|
||
const count = couplings(pipe.length_m, oneM);
|
||
if (count === null) continue;
|
||
const key = `${at(pipe.chainage_m)}|${pipe.column}`;
|
||
sums.set(key, (sums.get(key) ?? 0) + count);
|
||
}
|
||
const keep = (values: Record<string, number>): Record<string, number> =>
|
||
Object.fromEntries(Object.entries(values).filter(([id]) => !columns.has(id)));
|
||
const rows = summary.rows.map((row) => {
|
||
const values = keep(row.values);
|
||
for (const column of columns) {
|
||
const count = sums.get(`${at(row.chainage_m)}|${column}`);
|
||
if (count !== undefined) values[column] = count;
|
||
}
|
||
return { ...row, values };
|
||
});
|
||
const total = keep(summary.total);
|
||
for (const row of rows)
|
||
for (const column of columns)
|
||
if (row.values[column] !== undefined)
|
||
total[column] = (total[column] ?? 0) + row.values[column];
|
||
return { ...summary, rows, total };
|
||
}
|
||
|
||
/** 0~100 으로 자름 · 숫자가 아니면 null. */
|
||
export function clampPct(value: number): number | null {
|
||
return Number.isFinite(value) ? Math.min(Math.max(value, 0), 100) : null;
|
||
}
|
||
|
||
/** 암 → [연암, 경암] — 연암 = 암 × 비율(0~100 자름) · 경암 = 암 − 연암. */
|
||
export function rockSplit(value: number, softPct: number): [number, number] {
|
||
const soft = (value * Math.min(Math.max(softPct, 0), 100)) / 100;
|
||
return [soft, value - soft];
|
||
}
|
||
|
||
const ROCK_SPLIT = [
|
||
["cut_rock", "cut_soft_rock", "cut_hard_rock"],
|
||
["ditch_rock", "ditch_soft_rock", "ditch_hard_rock"],
|
||
] as const;
|
||
const ROCK_SUFFIXES = ["area_m2", "volume_m3", "adjusted_m3"] as const;
|
||
|
||
/** 줄 · 합계의 암 칸마다 연암 · 경암 칸을 덮어씀(숫자인 칸만). */
|
||
export function splitRockColumns(record: Record<string, unknown>, softPct: number): void {
|
||
for (const [base, softKey, hardKey] of ROCK_SPLIT)
|
||
for (const suffix of ROCK_SUFFIXES) {
|
||
const value = record[`${base}_${suffix}`];
|
||
if (typeof value !== "number") continue;
|
||
const [soft, hard] = rockSplit(value, softPct);
|
||
record[`${softKey}_${suffix}`] = soft;
|
||
record[`${hardKey}_${suffix}`] = hard;
|
||
}
|
||
}
|
||
|
||
/** 토공집계표 연암 · 경암 줄 — 흙깍기 = 절토 암 · 측구 터파기 = 측구 암 합계 × 비율 · 반올림 정수. */
|
||
const ROCK_ROWS: Record<string, string> = {
|
||
흙깍기: "cut_rock_volume_m3",
|
||
"측구 터파기": "ditch_rock_volume_m3",
|
||
};
|
||
|
||
export function splitRockSummary<R extends { group: string; kind: string; value: number | null }>(
|
||
rows: readonly R[],
|
||
totals: Record<string, number>,
|
||
softPct: number,
|
||
): R[] {
|
||
return rows.map((row) => {
|
||
const base = ROCK_ROWS[row.group];
|
||
if (!base || (row.kind !== "연암" && row.kind !== "경암")) return row;
|
||
const [soft, hard] = rockSplit(totals[base] ?? 0, softPct);
|
||
return { ...row, value: Math.round(row.kind === "연암" ? soft : hard) };
|
||
});
|
||
}
|