/* ============================================================================= * B05_Profile_UI_RouteEdit_Cross.ts * 계획노선 편집 중 **측점 횡단** — 메인 창 오른쪽 세로 칸에 **붙박이 두 판**(계획서 0-9 ⑱⑲). * * · 위 판 = **지금 횡단**. 측점 눈금을 누르면 그 측점을 셈해 여기에 낸다. * · 아래 판 = **이전 횡단**. 평소에는 빈 화면이고, 노선을 고쳐 **노드를 놓는 순간** * 위 판의 것이 이리로 내려오고 새로 셈한 것이 위로 올라간다 — 전후를 나란히 본다. * * 보이는 것은 셋뿐이다(2026-09-12 사용자 확정) — **원지반 횡단선 · 기본 계획 횡단선 · * 계획 횡단의 성토사면 길이**. 구조물은 그리지 않는다. * * ⚠ **계획고는 편집 중에 없다** — [확인] 뒤 전 체인이 낳는 값이다. 그래서 서버가 그 측점의 * 지반고를 그대로 계획고로 놓고(지반 추종) 사면만 세운 「기본 계획 횡단」을 낸다. * * 셈은 **B05·B06 정본을 그대로 재사용**한다 — 측점·지반 샘플은 `generate_sections`, 설계선은 * `compute_cross_design`(서버), 성토사면 길이는 B06 화면이 쓰는 `fillSlopeLengths`. * ========================================================================== */ import { attachSvgZoomPan } from "../A00_Common/b_svg_zoom_pan"; import { fetchCrossPreview, type CrossPreviewResponse } from "./B05_Profile_Api_Replan"; import { buildCrossSvg, CROSS_PLOT_PAD, summarizeCross, } from "./B05_Profile_UI_RouteEdit_Cross_Draw"; import { formatStation } from "./B05_Profile_Util_Station"; export interface CrossPreviewParams { projectId: string; /** 두 판이 들어앉을 오른쪽 세로 칸. */ side: HTMLElement; /** 지금 편집값 — 셈을 부르는 순간에 읽는다. */ request: () => { vertices: Array<{ x: number; y: number; curve: boolean; radius_m: number | null }>; min_radius_m: number; station_interval_m: number; }; } export interface CrossPreviewWindow { /** 그 측점의 횡단을 위 판에 낸다. 아래 판은 비운다 — 전후 비교는 노선을 고쳤을 때만. */ open: (chainageM: number) => Promise; /** 노선을 고쳤다 — 보던 측점을 **다시 셈해** 전후로 늘어놓는다. 보던 것이 없으면 아무 일도 없다. */ refresh: () => Promise; } interface CrossPane { root: HTMLElement; /** 셈해 온 횡단을 그린다. `null` 이면 빈 화면으로 되돌린다. */ show: (preview: CrossPreviewResponse | null, intervalM: number) => void; /** 기다리는 중임을 알린다. */ wait: (text: string) => void; } function createPane(title: string, empty: string): CrossPane { const root = document.createElement("section"); root.className = "b05-routeedit__cross"; root.innerHTML = `
${title}
${empty}
`; const station = root.querySelector(".b05-routeedit__cross-station")!; const foot = root.querySelector(".b05-routeedit__cross-foot")!; const box = root.querySelector(".b05-routeedit__cross-box")!; return { root, show(preview, intervalM) { box.replaceChildren(); if (!preview) { station.textContent = ""; foot.textContent = empty; return; } // 측점은 **누가거리가 아니라 측점 표기**로 낸다(계획서 0-9 ㉑) — B05 왼쪽 아래 구조물 // 목록이 쓰는 그 규칙이다. 서버가 주는 `STA.0+100.000` 을 그대로 쓰면 표기가 갈린다. station.textContent = formatStation(preview.chainage_m, intervalM); // 판의 실제 크기로 짓는다 — `viewBox` 가 픽셀과 1:1 이라야 휠·팬이 커서를 따라간다. // 칸이 접혀 있으면(1500px 미만) 0 이 나오므로 최소치를 깐다. const rect = box.getBoundingClientRect(); const plot = buildCrossSvg( preview, Math.max(Math.round(rect.width), 240), Math.max(Math.round(rect.height), 150), ); box.append(plot.svg); // 확대·이동은 **공용 조각**이 맡는다 — B06 횡단 카드와 한 코드다(2026-09-12 지시 ③). // 판이 둘뿐이라 여기서는 휠을 줌에 묶는다(카드 목록인 B06 은 안 묶는다). attachSvgZoomPan({ svg: plot.svg, layer: plot.layer, widthPx: plot.width, heightPx: plot.height, pad: CROSS_PLOT_PAD, content: plot.content, wheelZoom: true, }); foot.textContent = summarizeCross(preview); }, wait(text) { box.replaceChildren(); foot.textContent = text; }, }; } export function createCrossPreview(params: CrossPreviewParams): CrossPreviewWindow { const current = createPane("횡단", "측점 눈금을 누르면 그 측점 횡단이 뜹니다."); const previous = createPane("이전 횡단", "노선을 고치면 고치기 전 횡단이 여기 남습니다."); params.side.append(current.root, previous.root); /** 지금 보고 있는 측점(누가거리). 아직 없으면 null. */ let watching: number | null = null; /** 위 판에 그려 둔 것 — 다음 번에 아래로 내릴 재료. */ let shown: CrossPreviewResponse | null = null; /** 지금 부른 셈 — 늦게 온 응답을 새 자리에 적지 않으려고 든다. */ let ticket = 0; async function load(chainageM: number, keepPrevious: boolean): Promise { const mine = ++ticket; const request = params.request(); if (keepPrevious && shown) previous.show(shown, request.station_interval_m); current.wait("읽는 중…"); try { const preview = await fetchCrossPreview(params.projectId, { ...request, chainage_m: chainageM, }); if (mine !== ticket) return; // 그 사이 다른 측점을 눌렀다. shown = preview; current.show(preview, request.station_interval_m); } catch (error) { if (mine !== ticket) return; shown = null; current.wait(error instanceof Error ? error.message : "횡단을 읽지 못했습니다."); } } return { async open(chainageM) { // 아래 판은 **노선을 고쳤을 때만** 채운다(2026-09-12 사용자 지시 ⑤). 예전에는 같은 // 측점을 두 번 누르면 위·아래에 **같은 그림 두 장**이 떠 전후 비교가 되지 않았다. previous.show(null, params.request().station_interval_m); watching = chainageM; await load(chainageM, false); }, async refresh() { if (watching === null) return; await load(watching, true); }, }; }