/* ============================================================================= * B06_Section_UI_Section_View_Chart.ts * B06 상단 패널의 **종단면도 몫**만 떼어 낸 조립기 (2026-09-04 · 700줄 제한). * * 세로는 **보이는 구간에 자동으로 맞춘다**(2026-09-04 사용자 확정, B05 와 같은 규칙·같은 * 함수). 가로 스크롤 위치와 컨테이너 폭이 곧 보이는 구간이다. 스크롤할 때 그래프를 다시 * 만들지 않고 세로 창을 **변환으로** 옮기는 것도 B05 와 같다 * (`common_util_chart_ywindow`). * ========================================================================== */ import type { SectionDetailResponse } from "./B06_Section_Api_Fetch"; import { createLongitudinalProfile } from "./B06_Section_UI_Longitudinal"; import { LONG_PAD, longitudinalMaxChainage, windowElevationRange, } from "./B06_Section_UI_Section_Common"; import { unwrapChart } from "./B06_Section_UI_Section_View_Panel"; export interface LongitudinalChartInput { detail: SectionDetailResponse; selectedStationId: string | null; /** 세로 과장(사용자 조절값). */ exaggeration: number; selectStation: (stationId: string) => void; stationInterval: number; chartWidth: number; chartHeight: number; minWidth: number; /** 지금 보이는 구간을 정하는 값 — 가로 스크롤 위치와 컨테이너 안쪽 폭. */ scrollLeft: number; viewportWidth: number; /** 구조물(비정규) 측점선을 끌어 옮겼다. 안 넘기면 그 선은 못 잡는다. */ onDragStation?: (stationId: string, toChainageM: number) => void; /** X축·측점 라벨을 바닥에서 이만큼(px) 올린다 — 계획고 편집 ▼ 버튼과 겹치지 않게. */ bottomInsetPx?: number; /** 세로 창을 가로채는 자리 — 계획고를 ▲▼ 로 만지는 동안 창을 **고정**하는 데 쓴다. * 안 넘기면 늘 보이는 구간에 맞춘다(종전 동작). */ holdRange?: ( next: { min: number; max: number } | null, ) => { min: number; max: number } | undefined; } export interface LongitudinalChartResult { node: Element; axis: { padLeft: number; ticks: Array<{ y: number; label: string }> } | null; /** 화면 x(px) → 누가거리(m). 스크롤 갱신이 보이는 구간을 다시 잴 때 쓴다. */ toChainage: (px: number) => number; /** 누가거리(m) → 화면 x(px). 구조물 알약 레인이 그래프와 같은 자리를 쓰게 한다. */ toX: (chainageM: number) => number; maxChainageM: number; viewFromM: number; viewToM: number; } /** 지금 보이는 구간(누가거리 m)을 잰다 — 그릴 때와 스크롤 갱신 때가 **같은 식**을 쓴다. */ export function visibleChainageRange( toChainage: (px: number) => number, maxChainageM: number, scrollLeft: number, viewportWidth: number, ): { fromM: number; toM: number } { return { fromM: Math.max(0, toChainage(scrollLeft)), toM: Math.min(maxChainageM, toChainage(scrollLeft + viewportWidth)), }; } /** 보이는 구간의 표고 범위 — 종단 세로 창의 기준. */ export function visibleElevationRange( detail: SectionDetailResponse, fromM: number, toM: number, ): { min: number; max: number } | null { return ( windowElevationRange( [ detail.longitudinal.samples, ...(detail.longitudinal.design_profiles ?? []).map((profile) => profile.samples), ], fromM, toM, ) ?? null ); } export function buildLongitudinalChart(input: LongitudinalChartInput): LongitudinalChartResult { const { detail } = input; const maxChainageM = longitudinalMaxChainage(detail.longitudinal); const plotWidth = Math.max(1, input.chartWidth - LONG_PAD.left - LONG_PAD.right); const toChainage = (px: number): number => ((px - LONG_PAD.left) / plotWidth) * maxChainageM; const toX = (chainageM: number): number => LONG_PAD.left + (maxChainageM > 0 ? (chainageM / maxChainageM) * plotWidth : 0); const { fromM, toM } = visibleChainageRange( toChainage, maxChainageM, input.scrollLeft, input.viewportWidth, ); // Y축 눈금을 렌더러에서 받아 가로 스크롤 고정 오버레이로 얹는다(2026-08-04 사용자 // 지시 — 테이블 행 이름표처럼 스크롤해도 계속 보이게, B05와 같은 방식). let axis: { padLeft: number; ticks: Array<{ y: number; label: string }> } | null = null; const node = unwrapChart( createLongitudinalProfile( detail.longitudinal, input.selectedStationId, input.exaggeration, // 공통 Y 스케일(횡단 카드 몫)을 여기 넘기면 종단이 전 구간 축에 묶여 눌린다. undefined, input.selectStation, input.stationInterval, input.chartWidth, input.chartHeight, input.minWidth, detail.longitudinal.design_profiles ?? [], 0, (next) => { axis = next; }, undefined, // 구조물(비정규) 측점선 끌어 옮기기 — B05 와 같은 조작이다(2026-09-12 일원화). input.onDragStation, input.bottomInsetPx ?? 0, 0, input.holdRange ? input.holdRange(visibleElevationRange(detail, fromM, toM)) : (visibleElevationRange(detail, fromM, toM) ?? undefined), ), ); return { node, axis, toChainage, toX, maxChainageM, viewFromM: fromM, viewToM: toM }; }