B05 에는 있고 B06 에 없던 장치였음. ▲▼ 를 누를 때마다 세로 창이 새 계획선에 맞춰 다시 잡혀, 값은 바뀌는데 선은 제자리에 있는 것처럼 보였음. B05 와 같이 누르고 있는 동안 창을 고정하고 손을 뗀 뒤 한 번만 다시 맞춤. 실측(랩탑_보조 534.0m 측점): 계획고 837.24 → 839.84m 일 때 설계선이 3.2px 올라감. 종전에는 같은 조작에서 0.4px 만 움직였음. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fe1QWPTfw11PaKh2LjwXdR
133 lines
5.3 KiB
TypeScript
133 lines
5.3 KiB
TypeScript
/* =============================================================================
|
|
* 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 };
|
|
}
|