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>
121 lines
4.4 KiB
TypeScript
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 };
|
|
}
|