Files
Aislo/B06_Section/B06_Section_UI_Section_View_Chart.ts
T
eomsangdonandClaude Opus 5 679726124b feat(B06): 종단 구조물 표시를 B05 와 통일 — 같은 알약 레인
PLAN 2-5 (2026-09-07 사용자 지시 4 「구조물 표시 통일」). B06 종단에는 구조물 마크가
아예 없었고 배수관·세월교만 다른 계통으로 그려졌음.

- B05 가 쓰는 **같은 부품**(`buildStructureLane`)을 B06 종단 그래프 아래에 붙임.
  이름·중복 번호·툴팁·약호가 전부 B05 규칙 그대로 따라옴.
- 종단 차트가 `toX`(누가거리 → px)를 내보내 레인이 그래프와 같은 자리를 씀.
- 좌측 「구조물 배치」가 목록을 받을 때마다 `setStructureMarks` 로 넘김(구조물 정본 +
  관 정본). 관을 가상 구조물로 바꾸는 `pipesToStructureMarks` 는 B05 안에 있던 것을
  공용 자리(`B05_Profile_Api_Structures`)로 올려 **한 벌만** 둠.
- 알약을 누르면 그 자리에 가장 가까운 측점 카드를 고름(좌측 목록 클릭과 같은 규칙).
  끌기는 B05 몫이라 여기서는 자리만 보임(배수유역 재분할이 걸림).
- 검증 훅 `window.__b06Marks` 추가(`__corridorBuild` 등과 같은 용도).

자체검증 — 실화면 B06 종단에 레인 1줄·알약 10개(「배수관 1~8 · 세월교 1~2」)가 B05 와
같은 표기로 뜸. 훅 값 `{structures:10, types:36}`. tsc 통과.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 08:20:59 +09:00

121 lines
4.4 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;
/** 누가거리(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,
undefined,
0,
0,
visibleElevationRange(detail, fromM, toM) ?? undefined,
),
);
return { node, axis, toChainage, toX, maxChainageM, viewFromM: fromM, viewToM: toM };
}