/* ============================================================================= * common_util_masonry_slope.ts * 벽 전면 기울기(1:n 의 n) — 품셈 13-4-4 [주]⑪ 표준경사 표(원문 L7185~7191)를 읽음. * * ⚠ **파이썬과 짝**(CLAUDE.md 5장 ①) — `B08_Quantity_Engine_UnitQuantity_StoneSpec.face_slope_ratio` * + `common_util_structure_face_role.structure_face_role_of`. 거울 시험 * `resources/tester/test_b06_wall_lean_table.py` 가 네 갈래(직고 × 성토/절토 × 메/찰 + 사용자 칸)를 * 한 칸씩 대조. 한쪽만 고치면 횡단도 벽과 B08 수량이 갈림(2026-09-14 브레인 판정 「양쪽 다 이 표를」). * ⚠ 표는 부르는 쪽이 넘김 — 이 파일은 아무것도 import 하지 않음(거울 시험이 단독으로 돌림). * ========================================================================== */ /** 표준경사 표 — `resources/data_masonry/masonry_slope_*.json` 의 모양. */ export interface MasonrySlopeTable { steps_m?: number[]; table?: Record>; } /** 벽 한 매의 기울기 입력 — B08 이 그 벽을 셀 때 보는 칸과 같음. */ export interface WallLeanInput { /** 형태 — 「돌쌓기(찰)」·「돌쌓기(메)」만 표를 읽음(나머지는 종전 0.3). */ form?: string | null; /** 직고(m) — 순수 높이. */ height_m: number; /** 측점 단면유형 `design.section_mode`. */ section_mode?: string | null; /** 설치 측 — 「자동(성토 쪽)」·「좌」·「우」(없으면 자동). */ side?: string | null; /** 성토/절토 칸 — 고르면 이김. */ face_role?: string | null; /** 전면 기울기 칸 — 수(>0)면 이김(파이썬 `_num` 과 같이 글은 안 봄). */ face_slope_ratio?: number | string | null; } /** 표를 못 고를 때의 종전값 — 파이썬 `LEGACY_FACE_SLOPE_RATIO`. */ export const LEGACY_LEAN_RATIO = 0.3; const FILL = "성토"; const CUT = "절토"; /** 형태 → 찰(true)/메(false) — 파이썬 `REVETMENT_STONE_FORMS`. */ const STONE_FORMS: Record = { "돌쌓기(찰)": true, "돌쌓기(메)": false }; /** 단면유형 → (좌측 역할, 우측 역할). 좌 = +offset. */ const MODE_ROLES: Record = { left_cut: [CUT, FILL], right_cut: [FILL, CUT], both_cut: [CUT, CUT], both_fill: [FILL, FILL], }; const LEFT = new Set(["좌", "left", "L"]); const RIGHT = new Set(["우", "right", "R"]); const AUTO = new Set(["자동(성토 쪽)", "자동", "auto", ""]); /** 성토/절토 — 칸이 이기고, 비면 단면유형 + 설치 측. 못 가르면 `null`(성토로 눅이지 않음). */ export function faceRoleOf( sectionMode: string | null | undefined, side: string | null | undefined, faceRole?: string | null, ): string | null { const picked = String(faceRole ?? "").trim(); if (picked === FILL || picked === CUT) return picked; const roles = MODE_ROLES[String(sectionMode ?? "").trim()]; if (!roles) return null; const where = String(side ?? "").trim(); if (LEFT.has(where)) return roles[0]; if (RIGHT.has(where)) return roles[1]; if (AUTO.has(where)) return roles[0] === CUT && roles[1] === CUT ? null : FILL; return null; } /** 벽 전면 기울기 n — 표를 못 고르면 종전 0.3. */ export function wallLeanRatio(table: MasonrySlopeTable, input: WallLeanInput): number { const wet = STONE_FORMS[String(input.form ?? "").trim()]; if (wet === undefined) return LEGACY_LEAN_RATIO; const given = input.face_slope_ratio; if (typeof given === "number" && Number.isFinite(given) && given > 0) return given; const face = faceRoleOf(input.section_mode, input.side, input.face_role); if (!face) return LEGACY_LEAN_RATIO; const row = table.table?.[wet ? "찰쌓기" : "메쌓기"]?.[face]; if (!row?.length) return LEGACY_LEAN_RATIO; const steps = table.steps_m ?? []; let index = steps.length; for (let i = 0; i < steps.length; i += 1) { if (input.height_m <= steps[i]) { index = i; break; } } return row[Math.min(index, row.length - 1)]; }