/* ============================================================================= * common_util_berm_spans.ts * 소단 구간 — 구조물 목록(표 형상 berm)에서 구간 · 측점마다 제원(PLAN 472-4 C4 · 화면 파일 * `B06_Section_UI_Page_Persist.ts` 에서 옮김 · 그쪽이 다시 내보냄). * 브라우저(시설 줄 · 다시 셈)와 서버 Node 입구(`design_calc_node` · 등록부 `cross.berm_spans`)가 같은 셈. * 파이썬 짝 = `B06_Section_Engine_Structures_Wall.py` `load_berm_spans` · `berm_span_at` * (Node 를 못 부를 때만 · 거울 시험 `test_272_berm_source.py`). * ========================================================================== */ import { BERM_DEFAULT_DEPTH_M, BERM_DEFAULT_INTERVAL_M, BERM_DEFAULT_SLOPE_DEG, BERM_DEFAULT_WIDTH_M, } from "./common_util_cross_berm"; import { catalogRow } from "./common_util_structure_catalog"; /** 측점 하나의 소단 제원 — 서버 payload 와 같은 이름을 쓴다(그대로 실어 보낸다). */ export interface BermSessionSpec { width_m: number; interval_m: number; slope_deg: number; /** 노면 끝 아래 깊이(m · ≥ 0 · 178-2). */ depth_m: number; } /** * 사용자가 놓은 소단 **한 구간** — 종단 범위 + 제원. * * 사용자는 측점 하나가 아니라 **구간**에 놓는다(2026-09-07 확정: 「길이 + 기준측점 전·후」). * 그래서 세션에는 구간 목록으로 두고, 측점별 제원은 읽는 자리에서 편다 — 구간을 측점으로 * 펴서 저장하면 나중에 「어디부터 어디까지 놓았나」를 되짚을 수 없다. */ export interface BermSpan extends BermSessionSpec { start_m: number; end_m: number; } /** 소단 = 표 형상 berm(323-4 · 파이썬 `load_berm_spans` 짝) — 종류 키로 안 가름. */ export const isBermType = (typeId: string): boolean => catalogRow(typeId)?.shape === "berm"; /** * 구조물 목록에서 **소단 구간**을 뽑는다 — 사용자는 「구조물 배치」에서 놓는다 * (2026-09-07 사용자 확정: 별도 폼이 아니라 다른 옹벽·기슭막이와 같은 자리). * * 세션 열쇠 `berm` 은 그 결과를 담는 **사본**이다. 재계산(브라우저·서버)이 측점마다 값을 * 읽어야 하는데 구조물 목록은 비동기로 오므로, 목록이 바뀔 때마다 여기서 펴 두고 계산은 * 그 사본만 본다. */ export function bermSpansFromStructures( structures: ReadonlyArray<{ type_id: string; start_m?: number | null; end_m?: number | null; chainage_m?: number | null; options?: Record; }>, ): BermSpan[] { const spans: BermSpan[] = []; for (const item of structures) { if (!isBermType(item.type_id)) continue; const anchor = item.chainage_m ?? item.start_m ?? null; const start = item.start_m ?? anchor; const end = item.end_m ?? anchor; if (start === null || end === null) continue; const options = item.options ?? {}; const number = (key: string, fallback: number): number => { const parsed = Number(options[key]); return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback; }; const width = number("width_m", BERM_DEFAULT_WIDTH_M); const interval = number("interval_m", BERM_DEFAULT_INTERVAL_M); if (width <= 0) continue; // 폭이 0이면 계단이 없다(간격은 성토 한 단에 안 씀 · 178-2) // 깊이 = 노면 끝 아래 · 음수는 0(도로보다 높을 수 없음) · 빈 값은 기본 1.0 const rawDepth = Number(options.depth_m); const depth = options.depth_m === undefined || options.depth_m === "" || !Number.isFinite(rawDepth) ? BERM_DEFAULT_DEPTH_M : Math.max(rawDepth, 0); spans.push({ start_m: Math.min(start, end), end_m: Math.max(start, end), width_m: width, interval_m: interval, slope_deg: number("slope_deg", BERM_DEFAULT_SLOPE_DEG), depth_m: depth, }); } return spans; } /** 그 측점을 덮는 소단 제원 — 없으면 null. 겹치면 먼저 놓은 것이 이긴다. */ export function bermSpecAt(spans: BermSpan[], chainageM: number): BermSessionSpec | null { const found = spans.find( (span) => chainageM >= Math.min(span.start_m, span.end_m) - 1e-6 && chainageM <= Math.max(span.start_m, span.end_m) + 1e-6, ); return found ? { width_m: found.width_m, interval_m: found.interval_m, slope_deg: found.slope_deg, depth_m: found.depth_m ?? BERM_DEFAULT_DEPTH_M, } : null; }