/* ============================================================================= * A00_Common/design/design_pipeline_cross.ts * 계산 묶음 cross 입력 모음(PLAN 346-2 붙임) — 저장 노선 상세의 측점마다 횡단 입력 · 얹힌 시설 제원을 * **측점 열쇠**로 모음. 노선이 바뀌어도 같은 열쇠 측점(격자 번호 · 관 id · 구조물 id)이 그 입력을 받음. * 열쇠 = 저장 노선을 stations 단계와 같은 함수로 짚은 측점(`buildStations`)과 누가거리로 짝지음 — * 묶음 측점과 같은 열쇠 규칙(관 = 저장 관 id). * * 넘기는 칸 = 서버 노선 저장(`CARRY_INPUT_KEYS`)과 같은 일곱(지반 유형 · 측구 종류 · 측구 선택 · 2단 경사 · * 암 경계 · 암 경사 · 소단) + 얹힌 시설 맥락(포장 · 물넘이 기울임 · 세월교 노면 하강). 단면 유형 · 측구 * 방향은 새 지형이 정함(`logic/B05_Profile_Pipeline_Detail.ts` `crossWithUphill`) · 노견 확장 · 소단 * 깊이는 안 넘김. 값 = 상세에 얹힌 설계 위에 세션 고침(측점 열쇠 칸 · 지반 유형 · 측구 · 2단 경사 · 암 경계 · * 암 경사) — 노선 초안 동안 B06 고침은 저장 상세에 안 얹혀도 입력에 들어감(365-2). 부를 때마다 모음. * ========================================================================== */ import { readState } from "../b_page_state"; import type { CrossSection, SectionDetailResponse } from "../../B06_Section/B06_Section_Api_Fetch"; import { effectiveStandardCross, readRockBoundaryDefault, } from "../../B06_Section/B06_Section_UI_Standard_Panel"; import type { CrossInput, SectionSpecs } from "../../B05_Profile/logic/B05_Profile_Pipeline_Detail"; import type { PipelineStation } from "./logic/design_pipeline_types"; export interface PipelineCrossSource { input: CrossInput; /** 측점 열쇠 → 얹힌 시설 제원. */ specs: Map; } const SPEC_KEYS = [ "structure", "culvert", "ford", "box", "ford_pavement", "revetment", "pavement", "underdrain", ] as const; /** 같은 측점으로 보는 누가거리 차(m) — 측점 열쇠 글 자릿수(0.01 m). */ const SAME_M = 0.01; const finite = (value: unknown): number | null => typeof value === "number" && Number.isFinite(value) ? value : null; /** 측점 하나의 넘기는 입력(cross 단계 `CrossStationInput` 꼴). */ function carried(section: CrossSection): Record { const design = (section.design ?? {}) as unknown as Record; const berm = design.berm as { width_m: number; interval_m: number; slope_deg: number; depth_m?: number } | undefined; const cutSlope = finite(design.cut_slope_ratio_user); const drop = Number(section.ford?.overflow_depth_m); return { ...(typeof design.ground_type === "string" ? { groundType: design.ground_type } : {}), ...(typeof design.ditch_type === "string" ? { ditchType: design.ditch_type } : {}), ditchChoice: typeof design.ditch_choice === "boolean" ? design.ditch_choice : null, twoStageSlope: Boolean(design.two_stage_slope), rockBoundaryOffsetM: finite(design.rock_boundary_offset_m), cutSlopeRatio: cutSlope !== null && cutSlope > 0 ? cutSlope : null, berm: berm ? { widthM: berm.width_m, intervalM: berm.interval_m, slopeDeg: berm.slope_deg, depthM: berm.depth_m, } : null, paved: Boolean(section.pavement?.length) || Boolean(section.ford_pavement), tilted: Boolean(section.ford_pavement || section.underdrain), tiltPct: section.ford_pavement?.slope_pct ?? section.underdrain?.slope_pct ?? null, surfaceDropM: drop > 0 ? drop : 0, }; } /** 세션 칸 하나를 측점 열쇠마다 — 옛 열쇠(누가거리 · `at:`)는 읽는 쪽이 옮긴 뒤에 들어옴(빼고 봄). */ function sessionCells( name: "crossdesign" | "rockb" | "cutslope", projectId: string, routeId: number, ): Array<[string, unknown]> { const raw = readState>(name, projectId, routeId); if (!raw || typeof raw !== "object") return []; return Object.entries(raw).filter(([key]) => key.includes(":") && !key.startsWith("at:")); } /** 세션 횡단 고침 → 측점 열쇠마다 넘기는 입력 조각(`carried` 와 같은 칸 이름). */ function sessionCarried(projectId: string, routeId: number): CrossInput["byKey"] { const out: CrossInput["byKey"] = {}; for (const [key, value] of sessionCells("crossdesign", projectId, routeId)) { const choice = (value ?? {}) as Record; const entry = (out[key] ??= {}); if (typeof choice.ground_type === "string") entry.groundType = choice.ground_type; if (typeof choice.ditch_type === "string") entry.ditchType = choice.ditch_type; if ("ditch_choice" in choice) entry.ditchChoice = typeof choice.ditch_choice === "boolean" ? choice.ditch_choice : null; if ("two_stage_slope" in choice) entry.twoStageSlope = Boolean(choice.two_stage_slope); } for (const [key, value] of sessionCells("rockb", projectId, routeId)) if (finite(value) !== null) (out[key] ??= {}).rockBoundaryOffsetM = value; for (const [key, value] of sessionCells("cutslope", projectId, routeId)) { const ratio = finite(value); if (ratio !== null) (out[key] ??= {}).cutSlopeRatio = ratio > 0 ? ratio : null; } return out; } /** 저장 상세 + 저장 노선 측점(열쇠) + 세션 고침(`routeId` 칸) → cross 입력 · 시설 제원 — 표준단면 · * 상세가 없으면 null. */ export function pipelineCrossSource( projectId: string, detail: SectionDetailResponse | null, savedStations: ReadonlyArray, routeId: number | null = null, ): PipelineCrossSource | null { const standard = detail && effectiveStandardCross(projectId); if (!detail || !standard) return null; const byKey: CrossInput["byKey"] = {}; const specs = new Map(); for (const section of detail.cross_sections) { let best: PipelineStation | null = null; for (const station of savedStations) { const gap = Math.abs(station.chainageM - section.chainage_m); if (gap < SAME_M && (!best || gap < Math.abs(best.chainageM - section.chainage_m))) best = station; } if (!best || byKey[best.key]) continue; byKey[best.key] = carried(section); const own: Record = {}; for (const name of SPEC_KEYS) if (section[name] !== undefined) own[name] = section[name]; specs.set(best.key, own as SectionSpecs); } if (routeId !== null) for (const [key, entry] of Object.entries(sessionCarried(projectId, routeId))) byKey[key] = { ...byKey[key], ...entry }; return { // 암 경계 기본값 = 서버 config(화면이 문맥에서 기억) — 입력 없는 측점(초안 새 측점)도 서버처럼(349-0p) input: { standard, byKey, rockBoundaryDefaultM: readRockBoundaryDefault(projectId) }, specs, }; }