feat(B05): 367-1 유토곡선 늘 전체 맞춤 · 측점 연계 보기 지움 · 기본 = 횡단 기준 + 토량 분배 · [도형 위치 초기화] 횡단 기준 왼쪽
- 유토곡선 = 늘 노선 전체 한 화면(측점선 · 스크롤 동기 · 휠 · 스크롤마다 다시 그림 지움) - 범례 기본값에 토량 분배 더함 · masshaul-visible 판 v6 - [토량 분배] = 분배 그림 보임/숨김 · [도형 위치 초기화] 범례 맨 앞 - 시험 354_1 · 354_2 · 354_3 · 224_1b 고쳐 씀 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QfHKQs7J3hQELKMWQMUJEy
This commit is contained in:
@@ -17,8 +17,8 @@
|
||||
* ── 배치 (354-1 · 요구 235-7 「유토곡선도 분리」) ─────────────────────
|
||||
* 종단 열(그래프 · 표) 아래 칸에 따로 선다 — 종단도 · 표 위를 덮지 않고 높이 나눔 · 표 쌓기에도
|
||||
* 안 낀다. 높이는 판 위 경계 리사이저(`createPanelResizer`)로 조절하고 세션 동안 유지한다.
|
||||
* 머리줄([창 분할] · 접기 손잡이)은 접어도 남는다. 곡선 SVG는 판 안 자체 가로 스크롤러에
|
||||
* 들어가며, 종단 스크롤러와 scrollLeft를 양방향 동기화해 측점 세로선이 어긋나지 않게 한다.
|
||||
* 접으면 손잡이만 남는다(367-2). 곡선은 늘 노선 전체를 한 화면에 맞춰 그린다 — 종단 측점과
|
||||
* 맞물리는 보기 · 스크롤 동기는 없앰(367-1).
|
||||
* ========================================================================== */
|
||||
|
||||
import { readStateRaw, stateKey, writeStateRaw } from "../A00_Common/b_page_state";
|
||||
@@ -46,16 +46,13 @@ import { resetBalloonOffsets } from "@util/common_util_mass_haul_balance_view";
|
||||
import {
|
||||
createMassHaulChart,
|
||||
createMassHaulLegend,
|
||||
createMassHaulWindowState,
|
||||
MASS_HAUL_MIN_HEIGHT,
|
||||
scheduleMassHaulSettle,
|
||||
} from "@util/common_util_mass_haul_view";
|
||||
import { Y_WINDOW_BAKED_ATTR } from "../B06_Section/B06_Section_UI_Longitudinal";
|
||||
import { createWorkflowPanelHandle } from "@ui/ui_template_overlay";
|
||||
import { createPanelResizer } from "@ui/ui_template_resizer";
|
||||
import "@util/common_util_mass_haul.css";
|
||||
import "./B05_Profile_UI_Style_MassHaul.css";
|
||||
import { attachWheelHorizontalScroll, linkScrollLeft } from "./B05_Profile_UI_Profile_Wheel";
|
||||
import { frameOf } from "../common_util/common_util_view_window";
|
||||
|
||||
/** 2차 패널 펼침 여부 — 세션 동안만 유지(패널 높이·접힘과 같은 수명). */
|
||||
@@ -76,10 +73,8 @@ const HEIGHT_VAR = "--b05-masshaul-height";
|
||||
* 표시 상태가 갈리면 "같은 곡선인데 왜 다르게 보이나"가 된다(2026-08-03 사용자 확정).
|
||||
* 정의처는 B06 `_UI_Section_View`와 이 파일 두 곳뿐이며 값이 반드시 같아야 한다.
|
||||
*/
|
||||
/** 처음 열 때는 **곡선만** 보인다 — 토량 분배(평형선·운반 블록)는 범례에서 켠다
|
||||
* (2026-09-06 사용자 지시). 판을 v4 → v5 로 올려 이미 켜 둔 브라우저도 새 기본값으로
|
||||
* 시작하게 한다. */
|
||||
const DEFAULT_VISIBLE: string[] = [...MASS_HAUL_DEFAULT_VISIBLE];
|
||||
/** 기본 = 횡단 기준 + 토량 분배 켬(367-1) — 판 v6. [토량 분배] 는 분배 그림 보임/숨김. */
|
||||
const DEFAULT_VISIBLE: string[] = [...MASS_HAUL_DEFAULT_VISIBLE, MASS_HAUL_BALANCE_KEY];
|
||||
|
||||
/** 유토곡선 계산에 필요한 프로젝트 설정 — B05 Page가 `fetchSectionContext()`에서 받아 넘긴다. */
|
||||
export interface RouteMassHaulContext {
|
||||
@@ -150,12 +145,8 @@ export interface RouteMassHaulPanel {
|
||||
overlay: HTMLElement;
|
||||
isOpen(): boolean;
|
||||
setContext(next: RouteMassHaulContext | null): void;
|
||||
/** 종단 가로 스크롤러와 scrollLeft를 양방향 동기화한다(설치 1회). */
|
||||
attachScrollSync(main: HTMLElement): void;
|
||||
/** 판 내용을 다시 그린다. 접혀 있으면 머리줄만 남기고 끝낸다. */
|
||||
draw(params: RouteMassHaulDrawParams): void;
|
||||
/** 가로 스크롤로 보이는 구간만 바뀜 — 창을 따를 때만 · 한 틀 한 번 · 다시 셈 없음(354-3). */
|
||||
drawWindow(params: RouteMassHaulDrawParams): void;
|
||||
}
|
||||
|
||||
function readVisible(): Set<string> {
|
||||
@@ -248,10 +239,6 @@ export function createRouteMassHaulPanel(
|
||||
pendingChip.hidden = true;
|
||||
overlay.append(resizer.root, head, bar, scroll, legendLayer, pendingChip);
|
||||
|
||||
// 종단 그래프 영역과 같은 문법 — 세로 휠을 가로 이동으로 돌린다(2026-08-04 사용자 지시).
|
||||
// scrollLeft 동기화 덕에 위 종단 그래프도 함께 움직인다. Shift+휠은 브라우저 기본 그대로.
|
||||
attachWheelHorizontalScroll(scroll);
|
||||
|
||||
/** 곡선을 그리는가 — 펼침이거나 새 창으로 뗌(뗀 창엔 접기가 없음 · 354-1). */
|
||||
const shown = (): boolean => open || overlay.classList.contains("is-popout");
|
||||
|
||||
@@ -279,29 +266,14 @@ export function createRouteMassHaulPanel(
|
||||
onChanged();
|
||||
}
|
||||
|
||||
// 종단 스크롤러와의 양방향 동기화 — 메아리는 공용 도우미가 걸러 냄(354-3).
|
||||
// 토량 분배(전체 맞춤) 중에는 끊는다 — 유토곡선이 0 으로 붙으며 낸 scroll 이 종단 그래프 ·
|
||||
// 표까지 0 으로 되돌렸다(PLAN 79-1). 유토곡선만 스크롤이 없는 상태.
|
||||
let fitted = false;
|
||||
let syncedMain: HTMLElement | null = null;
|
||||
let putScroll: ((to: HTMLElement, left: number) => void) | null = null;
|
||||
/** 종단과 맞물려 그린 곡선이 놓인 문서 — 처음 · 분배 맞춤 뒤 · 창 옮김 뒤면 종단 자리로 맞춘다. */
|
||||
let syncedIn: Document | null = null;
|
||||
function attachScrollSync(main: HTMLElement): void {
|
||||
if (syncedMain === main) return;
|
||||
syncedMain = main;
|
||||
putScroll = linkScrollLeft(main, scroll, () => fitted);
|
||||
}
|
||||
|
||||
/** 곡선을 한 번이라도 그렸는지 — 재계산 대기 중 화면을 비우지 않기 위한 표시. */
|
||||
let drawnOnce = false;
|
||||
/** 마지막으로 낸 계열 — 가로 스크롤(보이는 구간만 바뀜)은 다시 셈 없이 이걸로 그린다(354-3). */
|
||||
/** 마지막으로 낸 계열 — 같은 계열이면 총괄값을 다시 내지 않는다. */
|
||||
let lastSeries: MassHaulSeries[] | null = null;
|
||||
|
||||
function note(message: string): void {
|
||||
drawnOnce = false;
|
||||
lastSeries = null;
|
||||
syncedIn = null;
|
||||
pendingChip.hidden = true;
|
||||
bar.replaceChildren();
|
||||
scroll.replaceChildren();
|
||||
@@ -312,11 +284,6 @@ export function createRouteMassHaulPanel(
|
||||
bar.append(empty);
|
||||
}
|
||||
|
||||
// 세로창 버티기·부드러운 이동 상태 — 이 패널이 사는 동안 하나만 둔다(2026-09-04).
|
||||
const windowState = createMassHaulWindowState();
|
||||
/** 마지막으로 그린 입력 — 이동이 아직 안 끝났으면 다음 프레임에 이걸로 한 번 더 그린다. */
|
||||
let lastParams: RouteMassHaulDrawParams | null = null;
|
||||
|
||||
/** 이미 낸 계열에서 총괄값만 꺼낸다 — 계산은 하지 않는다. */
|
||||
function emitSummary(series: MassHaulSeries[]): void {
|
||||
if (!onSummary) return;
|
||||
@@ -348,33 +315,16 @@ export function createRouteMassHaulPanel(
|
||||
);
|
||||
}
|
||||
|
||||
/** 그리기 몫 기록(349-0s) — 묶음 마지막 차례 `work.유토곡선`. `reuse` = 마지막 계열로(다시 셈 없음). */
|
||||
function draw(params: RouteMassHaulDrawParams, reuse = false): void {
|
||||
pipelineWork("유토곡선", () => drawNow(params, reuse));
|
||||
/** 그리기 몫 기록(349-0s) — 묶음 마지막 차례 `work.유토곡선`. */
|
||||
function draw(params: RouteMassHaulDrawParams): void {
|
||||
pipelineWork("유토곡선", () => drawNow(params));
|
||||
}
|
||||
|
||||
/** 가로 스크롤로 보이는 구간만 바뀜(354-3) — 곡선이 창을 따를 때만(열림 · 분배 맞춤 아님 · 붙음) ·
|
||||
* 한 틀 한 번 · 다시 셈 없이 세로 창만 다시 그림. 접힘 · 분배 · 새 창은 스크롤마다 할 일 없음. */
|
||||
let windowDirty = false;
|
||||
function drawWindow(params: RouteMassHaulDrawParams): void {
|
||||
if (!open || fitted || overlay.classList.contains("is-popout") || !lastSeries) return;
|
||||
lastParams = params;
|
||||
// 미끄러짐 틀이 이미 잡혀 있으면 그 틀이 이 값으로 그림
|
||||
if (windowDirty || windowState.frame) return;
|
||||
windowDirty = true;
|
||||
frameOf(overlay, () => {
|
||||
if (windowDirty && lastParams) draw(lastParams, true);
|
||||
});
|
||||
}
|
||||
|
||||
function drawNow(params: RouteMassHaulDrawParams, reuse: boolean): void {
|
||||
lastParams = params;
|
||||
windowDirty = false;
|
||||
function drawNow(params: RouteMassHaulDrawParams): void {
|
||||
overlay.classList.toggle("is-collapsed", !open);
|
||||
// 접혀 있어도 상단줄에는 값이 서야 한다 — 이때는 **횡단 기준만** 적분한다.
|
||||
// 펼친 경우에는 아래에서 낸 계열로 총괄값을 낸다(계산 한 번).
|
||||
if (!shown()) {
|
||||
syncedIn = null;
|
||||
reportSummary(params);
|
||||
return;
|
||||
}
|
||||
@@ -407,17 +357,13 @@ export function createRouteMassHaulPanel(
|
||||
// (354-2 · 계산 코드는 공용에 남김).
|
||||
// 결과를 아껴 두지 **않는다** — 횡단 설계가 제자리에서 갈리므로 객체 동일성으로는
|
||||
// 바뀐 것을 못 잰다(2026-09-04 캐시를 넣었다가 곡선이 안 따라와 되돌림).
|
||||
// 스크롤로 창만 바뀐 그리기(`reuse`)는 직전 계열 그대로 — 같은 입력의 두 번째 적분이었다(354-3).
|
||||
const series: MassHaulSeries[] =
|
||||
reuse && lastSeries
|
||||
? lastSeries
|
||||
: computeMassHaulSeries(
|
||||
params.longitudinal,
|
||||
params.crossSections,
|
||||
context.conversion,
|
||||
context.naturalSpoilMinSlope,
|
||||
["cross"],
|
||||
);
|
||||
const series: MassHaulSeries[] = computeMassHaulSeries(
|
||||
params.longitudinal,
|
||||
params.crossSections,
|
||||
context.conversion,
|
||||
context.naturalSpoilMinSlope,
|
||||
["cross"],
|
||||
);
|
||||
// 총괄값은 방금 낸 계열에서 꺼낸다 — 접힘 경로처럼 따로 적분하지 않는다.
|
||||
if (series !== lastSeries) emitSummary(series);
|
||||
lastSeries = series;
|
||||
@@ -446,29 +392,16 @@ export function createRouteMassHaulPanel(
|
||||
(overlay.clientHeight || OVERLAY_DEFAULT_HEIGHT) - bar.offsetHeight - 12;
|
||||
const chartHeight = Math.max(scrollHeight, MASS_HAUL_MIN_HEIGHT);
|
||||
|
||||
// 토량 분배를 켜면 **곡선 전체가 한 화면에** 들어와야 한다(2026-09-06 사용자 지시).
|
||||
// 평형선·운반 블록은 노선 전체를 놓고 읽는 그림이라, 종단 그래프와의 X 맞춤(가로
|
||||
// 스크롤)과 보이는 구간만 보는 Y 창을 둘 다 버리고 스크롤러 안쪽 폭·전 구간 Y로 그린다.
|
||||
// 종단과 맞물릴 필요가 없으므로 반 칸 들여쓰기(originOffset)도 쓰지 않는다 — 그만큼
|
||||
// 좌우가 비어 곡선이 가운데로 쪼그라들었다(2026-09-06 사용자 보고: 「우측 여유 과다」).
|
||||
const fitAll = haulPlan !== null;
|
||||
fitted = fitAll;
|
||||
const widthPx = fitAll ? scroll.clientWidth || params.widthPx : params.widthPx;
|
||||
const chartAxis = fitAll
|
||||
? {
|
||||
...params.axis,
|
||||
padLeft: params.axis.axisX ?? params.axis.padLeft,
|
||||
padRight: MASS_FIT_PAD_RIGHT,
|
||||
viewRange: undefined,
|
||||
window: undefined,
|
||||
hideStations: true,
|
||||
}
|
||||
: {
|
||||
...params.axis,
|
||||
// 새 창으로 뗀 판은 넓어 메인 보이는 구간 세로면 잘림 — 노선 전체 세로(325-2).
|
||||
viewRange: overlay.classList.contains("is-popout") ? undefined : params.axis.viewRange,
|
||||
window: windowState,
|
||||
};
|
||||
// 늘 **곡선 전체가 한 화면에**(367-1 · 측점 연계 보기 없앰) — 스크롤러 안쪽 폭 · 전 구간 Y ·
|
||||
// 측점선 없음. 반 칸 들여쓰기(originOffset)도 안 씀(2026-09-06 「우측 여유 과다」).
|
||||
const widthPx = scroll.clientWidth || params.widthPx;
|
||||
const chartAxis = {
|
||||
...params.axis,
|
||||
padLeft: params.axis.axisX ?? params.axis.padLeft,
|
||||
padRight: MASS_FIT_PAD_RIGHT,
|
||||
viewRange: undefined,
|
||||
hideStations: true,
|
||||
};
|
||||
|
||||
let massAxis: { padLeft: number; ticks: Array<{ y: number; label: string }> } | null = null;
|
||||
const chart = createMassHaulChart(
|
||||
@@ -495,33 +428,17 @@ export function createRouteMassHaulPanel(
|
||||
wrap.append(chart);
|
||||
// 앵커는 첫 자식 — SVG 뒤면 흐름 위치가 밀려 스크롤 시 축이 안 보인다(B06과 같은 규칙).
|
||||
if (massAxis) wrap.prepend(buildStickyYAxis(massAxis, chartHeight));
|
||||
// 이미 종단과 맞물려 그린 곡선이면 제 자리 그대로 — 종단 값으로 되쓰면 그 사이 유토 쪽 휠이
|
||||
// 더 간 만큼 뒤로 튐(354-3). 처음 · 분배 맞춤 뒤 · 창 옮김 뒤만 종단 자리에서 시작한다.
|
||||
const keepLeft = !fitAll && syncedIn === scroll.ownerDocument ? scroll.scrollLeft : null;
|
||||
scroll.replaceChildren(wrap);
|
||||
// 전체 맞춤일 때는 스크롤할 것이 없으므로 왼쪽 끝에 둔다.
|
||||
if (fitAll) scroll.scrollLeft = 0;
|
||||
else if (keepLeft !== null) scroll.scrollLeft = keepLeft;
|
||||
else if (syncedMain) putScroll?.(scroll, syncedMain.scrollLeft);
|
||||
syncedIn = fitAll ? null : scroll.ownerDocument;
|
||||
// 스크롤할 것이 없으므로 왼쪽 끝에 둔다.
|
||||
scroll.scrollLeft = 0;
|
||||
|
||||
// 범례는 계열 · 켬이 바뀔 때만 — 스크롤 그리기는 그대로 둔다.
|
||||
if (!reuse || !legendLayer.firstChild)
|
||||
legendLayer.replaceChildren(
|
||||
createMassHaulLegend(series, visible, toggleSeries, () => {
|
||||
resetBalloonOffsets();
|
||||
onChanged();
|
||||
}),
|
||||
);
|
||||
drawnOnce = true;
|
||||
// 세로창이 아직 목표까지 안 갔으면 다음 프레임에 한 걸음 더 — 확 튀지 않고 미끄러진다.
|
||||
scheduleMassHaulSettle(
|
||||
windowState,
|
||||
() => {
|
||||
if (lastParams) draw(lastParams, true);
|
||||
},
|
||||
overlay,
|
||||
legendLayer.replaceChildren(
|
||||
createMassHaulLegend(series, visible, toggleSeries, () => {
|
||||
resetBalloonOffsets();
|
||||
onChanged();
|
||||
}),
|
||||
);
|
||||
drawnOnce = true;
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -531,8 +448,6 @@ export function createRouteMassHaulPanel(
|
||||
setContext(next) {
|
||||
context = next;
|
||||
},
|
||||
attachScrollSync,
|
||||
draw: (params) => draw(params),
|
||||
drawWindow,
|
||||
draw,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -66,7 +66,6 @@ export interface ProfileRenderContext {
|
||||
callbacks: RouteProfilePanelCallbacks | undefined;
|
||||
massHaul: {
|
||||
draw: (params: RouteMassHaulDrawParams) => void;
|
||||
drawWindow: (params: RouteMassHaulDrawParams) => void;
|
||||
};
|
||||
tableOverlay: {
|
||||
isOpen: () => boolean;
|
||||
@@ -337,8 +336,6 @@ export function renderProfile(ctx: ProfileRenderContext): void {
|
||||
});
|
||||
// 세로 창 갱신기 — 가로로 스크롤할 때마다 패널이 이것을 부른다. 도형은 그대로 두고
|
||||
// 겹 하나의 변환만 갈아 끼우므로 매 프레임 불러도 된다(2026-09-04 사용자 확정).
|
||||
// 유토곡선 갱신기 — 아래에서 유토곡선을 그린 뒤 채워진다(그리기 순서상 여기서는 아직 없다).
|
||||
let massHaulUpdater: ((fromM: number, toM: number) => void) | null = null;
|
||||
ctx.setElevationWindowUpdater(() => {
|
||||
const from = Math.max(0, toChainage(body.scrollLeft));
|
||||
const to = Math.min(maxChainageM, toChainage(body.scrollLeft + body.clientWidth));
|
||||
@@ -349,7 +346,6 @@ export function renderProfile(ctx: ProfileRenderContext): void {
|
||||
to,
|
||||
) ?? null,
|
||||
);
|
||||
massHaulUpdater?.(from, to);
|
||||
return applyElevationWindow(chartWrap, next);
|
||||
});
|
||||
|
||||
@@ -452,15 +448,5 @@ export function renderProfile(ctx: ProfileRenderContext): void {
|
||||
onSelectStation: ctx.selectStation,
|
||||
};
|
||||
massHaul.draw(massHaulParams);
|
||||
// 유토곡선도 **같은 창**을 본다(2026-09-04 사용자 확정) — 스크롤할 때마다 곡선만 다시
|
||||
// 그린다. 종단처럼 변환으로 옮기지 않는 이유: 곡선 위에 앉는 말풍선·EP 표·측점 점이
|
||||
// 세로로 늘어나면 안 되는 것들이라, 그것만 따로 옮기는 값이 곡선 자체를 다시 그리는
|
||||
// 값보다 크다(요소 수가 종단의 1/3). 열림 · 분배 맞춤 아닐 때만 · 한 틀 한 번 · 다시 셈 없음(354-3).
|
||||
massHaulUpdater = (fromM, toM) => {
|
||||
massHaul.drawWindow({
|
||||
...massHaulParams,
|
||||
axis: { ...massHaulParams.axis, viewRange: { fromM, toM } },
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,14 +129,12 @@
|
||||
background: var(--color-surface);
|
||||
}
|
||||
|
||||
/* 곡선 가로 스크롤러 — 종단 스크롤러와 scrollLeft를 동기화한다(JS). 세로는 잘라낸다.
|
||||
스크롤바는 항상 표시(scroll) — auto면 생겼다 없어질 때 곡선 높이가 널뛰고,
|
||||
clientHeight 기반 높이 계산도 함께 흔들린다. */
|
||||
/* 곡선 칸 — 늘 전체 맞춤이라 스크롤 없음(367-1). */
|
||||
.b05-profile__masshaul-scroll {
|
||||
position: relative;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow-x: scroll;
|
||||
overflow-x: hidden;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user