Files
Aislo/B06_Section/B06_Section_UI_Section_View_Chart.ts
T
eomsangdonandClaude Opus 5 685d44ac39 perf(B05,B06): 유토곡선·B06 페이지도 세로 창을 실시간으로 따라오게
사용자 지적(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>
2026-09-04 22:13:03 +09:00

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 };
}