/* ============================================================================= * B06_Section_Cross_Enforce.ts * 계산 묶음 cross 단계의 [확정] 보정(PLAN 349-0n) — 서버 `_enforce_stored_designs` 차례 그대로 * (미교차 넓힘 → 포장 구간 → 세월교 노면 하강). 넓힘은 `B06_Section_Cross_Stage`. * * facilityRanges · facilityAt 새 노선 관 · 구조물 자리 → 측점마다 포장 · 기울임 · 노면 하강. * ⚠ ① 짝 — `B06_Section_Router_Design.py` `pavement_ranges` · `ford_surface_drops` · `paved_at` · * `ford_drop_at` + `B06_Section_Engine_Tilt.py` `tilt_ranges` · `tilt_args`. * 거울 시험 `resources/tester/test_349_0n_cross_enforce_mirror.py`. * 세트 · 포장 · 맹암거 제원은 이미 있는 짝(`buildCulvertSets` · `pavementSpecsFrom` · `underdrainSpecsFrom`). * 순수 — 화면 · fetch · 세션 없음. * ========================================================================== */ import { buildCulvertSets, type CulvertPipePoint } from "@util/common_util_culvert_sets"; import { catalogRow } from "@util/common_util_structure_catalog"; import { pavementSpecsFrom, type PavementStructureInput, } from "@util/common_util_structure_pavement"; import { underdrainSpecsFrom, type UnderdrainInput } from "@util/common_util_structure_underdrain"; /** 포장 · 기울임 · 노면 하강 판정 폭(m) — 파이썬 `EDGE_TOLERANCE_M` · `_FORD_DROP_TOLERANCE_M`. */ const EDGE_M = 0.02; export type FacilityStructure = PavementStructureInput & UnderdrainInput & { structure_id?: string | null }; export interface FacilityRanges { paved: Array<[number, number]>; tilt: Array<[number, number, number | null]>; drops: Array<[number, number]>; } const num = (value: unknown): number | null => { const parsed = typeof value === "string" && value.trim() ? Number(value) : value; return typeof parsed === "number" && Number.isFinite(parsed) ? parsed : null; }; /** 관 · 구조물 → 포장 구간 · 기울임 구간 · 세월교 하강(서버와 같은 차례 — 첫 것이 이김). */ export function facilityRanges( pipes: readonly CulvertPipePoint[], structures: readonly FacilityStructure[], ): FacilityRanges { // ponytail: 레지스트리 기본값 없이 세트를 지음 — 물넘이 폭 · 경사를 비운 관만 기본 상수(10 m · 3°) const sets = buildCulvertSets(pipes, {}); const pavementIds = new Set( structures.filter((s) => catalogRow(s.type_id)?.shape === "pavement").map((s) => s.type_id), ); const paved: FacilityRanges["paved"] = pavementSpecsFrom(structures, pavementIds).map((s) => [ s.start_m, s.end_m, ]); const tilt: FacilityRanges["tilt"] = []; const drops: FacilityRanges["drops"] = []; for (const [at, spec] of sets) { if (spec.type === "ford_pavement") { const half = (num(spec.span_m) ?? 0) / 2; paved.push([at - half, at + half]); tilt.push([at - half, at + half, num(spec.slope_pct)]); } const depth = num(spec.overflow_depth_m) ?? 0; if (spec.type === "ford" && depth > 0) drops.push([at, depth]); } // 노면 = 표준 경사 · 암거 경사는 맹암거 하단 경계만(371-3 · 파이썬 `tilt_ranges` 짝) for (const drain of underdrainSpecsFrom(structures)) tilt.push([drain.chainage_m, drain.chainage_m, null]); return { paved, tilt, drops }; } /** 그 측점의 포장 · 기울임 · 노면 하강 — 횡단 엔진 입력 칸 이름 그대로. */ export function facilityAt(ranges: FacilityRanges, chainageM: number) { const inside = (start: number, end: number) => start - EDGE_M <= chainageM && chainageM <= end + EDGE_M; const hit = ranges.tilt.find(([start, end]) => inside(start, end)); return { paved: ranges.paved.some(([start, end]) => inside(start, end)), tilted: hit !== undefined, tiltPct: hit?.[2] ?? null, surfaceDropM: ranges.drops.find(([at]) => Math.abs(chainageM - at) <= EDGE_M)?.[1] ?? 0, }; }