사용자 지적(2026-09-04) — 「유토곡선도 반영해야지. B06 페이지도 있잖아. 같은 로직일 텐데 왜 반영이 안 된 거야」. 그리는 쪽(종단 렌더러)은 공용이라 이미 바뀌어 있었고, **스크롤을 받아 창을 옮기는 쪽이 화면마다 따로**라 B05 만 연결돼 있던 것. - 세로 창 모듈을 `common_util_chart_ywindow.ts` 로 옮겨 B05·B06 이 함께 씀. 좌측 고정 축은 표식(`b05-profile__yaxis--ywindow`)이 붙은 것만 옮김 — B06 은 한 컨테이너에 종단·유토곡선 축이 함께 살아 남의 축까지 옮기면 안 됨. - 세로 과장(B06 조절값)을 각인·반영 — 예전에는 과장이 1일 때만 대상이었음. - 유토곡선은 변환이 아니라 **곡선만 다시 그림**(스크롤마다). 곡선 위 말풍선·EP 표·측점 점처럼 세로로 늘어나면 안 되는 것이 많아, 그것만 따로 옮기는 값이 다시 그리는 값보다 큼 (유토 SVG 234개 = 종단의 1/3). - B06 상단 패널을 통째로 다시 그리던 스크롤 처리를 걷어내고, 종단은 변환·유토곡선은 제자리 교체로 바꿈. 700줄 제한에 맞춰 종단 조립을 `_View_Chart.ts`(116줄)로, 유토곡선 조립기를 `_View_MassHaul.ts` 로 분리(727 → 718줄). 자체검증(공용 브라우저 임시 탭): ① B05 유토곡선 눈금이 스크롤을 따라 4.3㎥ → −1,348.8㎥ 로 이동(예전에는 안 따라옴) ② B06 종단 눈금 830/840/850m → 824/826/828m, 유토곡선 68.2㎥ → −3,375.1㎥ 로 동시 이동 ③ B06 최대 프레임 **35.9 → 23.5ms**, 33ms 초과 1 → **0건**(중앙값 16.7ms 유지) ④ B05 최대 프레임 27.4ms, 33ms 초과 0건 ⑤ 전체 시험 381 통과·17 건너뜀. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
117 lines
4.1 KiB
TypeScript
117 lines
4.1 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;
|
|
}
|
|
|
|
export interface LongitudinalChartResult {
|
|
node: Element;
|
|
axis: { padLeft: number; ticks: Array<{ y: number; label: string }> } | null;
|
|
/** 화면 x(px) → 누가거리(m). 스크롤 갱신이 보이는 구간을 다시 잴 때 쓴다. */
|
|
toChainage: (px: 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 { 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,
|
|
undefined,
|
|
0,
|
|
0,
|
|
visibleElevationRange(detail, fromM, toM) ?? undefined,
|
|
),
|
|
);
|
|
return { node, axis, toChainage, maxChainageM, viewFromM: fromM, viewToM: toM };
|
|
}
|