diff --git a/B06_Section/B06_Section_UI_Section_View.ts b/B06_Section/B06_Section_UI_Section_View.ts index 1466a6c3..49afaa2f 100644 --- a/B06_Section/B06_Section_UI_Section_View.ts +++ b/B06_Section/B06_Section_UI_Section_View.ts @@ -47,6 +47,7 @@ import { STRUCTURE_LANE_HEIGHT_PX } from "../B05_Profile/B05_Profile_UI_Structur import type { SectionStructureEdit } from "./B06_Section_UI_Section_View_Menu"; import { attachWindowScroll, + createElevationHold, drawLongitudinalPanel, type LongitudinalPanelInput, } from "./B06_Section_UI_Section_View_Draw"; @@ -119,10 +120,9 @@ export interface SectionViewController { structures: ReadonlyArray, types: ReadonlyArray, ) => void; - /** 종단 그래프 우클릭으로 구조물을 넣고 빼는 길 — 없으면 메뉴가 안 뜬다 - * (2026-09-12 B05·B06 일원화). */ + /** 종단 그래프 우클릭으로 구조물을 넣고 빼는 길(2026-09-12 B05·B06 일원화). */ setStructureEdit: (edit: SectionStructureEdit | null) => void; - /** 계획선 편집 ▲/▼ — 그릴 때마다 불러 지금 선형·편집 함수를 받는다(null = 버튼 없음). */ + /** 계획선 편집 ▲/▼ — 그릴 때마다 불러 선형·편집 함수를 받는다(null = 버튼 없음). */ setGradeEdit: (provider: (() => LongitudinalPanelInput["grade"]) | null) => void; dispose: () => void; } @@ -157,7 +157,7 @@ export function createSectionView( let markStructures: ReadonlyArray = []; let markTypes: ReadonlyArray = []; let structureEdit: SectionStructureEdit | null = null; - let gradeEditProvider: (() => LongitudinalPanelInput["grade"]) | null = null; + let gradeEdit: (() => LongitudinalPanelInput["grade"]) | null = null; let renderWidth = 0; let resizeTimer = 0; let panelResizeTimer = 0; @@ -515,7 +515,8 @@ export function createSectionView( naturalSpoilSlope: currentNaturalSpoilSlope, selectStation: (stationId) => selectStation(stationId, true), setMassBadge: (values) => massBadge.set(values), - grade: gradeEditProvider?.() ?? null, + grade: gradeEdit?.() ?? null, + holdRange, }); // 상단 패널이 sticky라 선택 카드가 그 아래로 숨는다 — 패널 높이만큼 스크롤 여백을 잡아 준다. syncScrollMargin(); @@ -534,6 +535,7 @@ export function createSectionView( } attachWindowScroll(chartWrap, () => updateChartWindow, drawPanel); + const holdRange = createElevationHold(chartWrap, drawPanel); const draw = (): void => { if (!currentDetail || !Number.isFinite(renderWidth) || renderWidth <= 0) return; @@ -668,7 +670,7 @@ export function createSectionView( drawPanel(); }, setGradeEdit(provider) { - gradeEditProvider = provider; + gradeEdit = provider; drawPanel(); }, setStructureMarks(structures, types) { diff --git a/B06_Section/B06_Section_UI_Section_View_Chart.ts b/B06_Section/B06_Section_UI_Section_View_Chart.ts index 1e9c073f..5ed3d1b6 100644 --- a/B06_Section/B06_Section_UI_Section_View_Chart.ts +++ b/B06_Section/B06_Section_UI_Section_View_Chart.ts @@ -34,6 +34,11 @@ export interface LongitudinalChartInput { onDragStation?: (stationId: string, toChainageM: number) => void; /** X축·측점 라벨을 바닥에서 이만큼(px) 올린다 — 계획고 편집 ▼ 버튼과 겹치지 않게. */ bottomInsetPx?: number; + /** 세로 창을 가로채는 자리 — 계획고를 ▲▼ 로 만지는 동안 창을 **고정**하는 데 쓴다. + * 안 넘기면 늘 보이는 구간에 맞춘다(종전 동작). */ + holdRange?: ( + next: { min: number; max: number } | null, + ) => { min: number; max: number } | undefined; } export interface LongitudinalChartResult { @@ -118,7 +123,9 @@ export function buildLongitudinalChart(input: LongitudinalChartInput): Longitudi input.onDragStation, input.bottomInsetPx ?? 0, 0, - visibleElevationRange(detail, fromM, toM) ?? undefined, + input.holdRange + ? input.holdRange(visibleElevationRange(detail, fromM, toM)) + : (visibleElevationRange(detail, fromM, toM) ?? undefined), ), ); return { node, axis, toChainage, toX, maxChainageM, viewFromM: fromM, viewToM: toM }; diff --git a/B06_Section/B06_Section_UI_Section_View_Draw.ts b/B06_Section/B06_Section_UI_Section_View_Draw.ts index 8a9402fb..96d51824 100644 --- a/B06_Section/B06_Section_UI_Section_View_Draw.ts +++ b/B06_Section/B06_Section_UI_Section_View_Draw.ts @@ -55,6 +55,10 @@ export interface LongitudinalPanelInput { conversion?: EarthworkConversion; naturalSpoilSlope?: number; selectStation: (stationId: string) => void; + /** 세로 창 가로채기 — 계획고를 만지는 동안 창을 고정한다(B05 와 같은 장치). */ + holdRange?: ( + next: { min: number; max: number } | null, + ) => { min: number; max: number } | undefined; /** 계획선 편집 — 넘기면 종단 그래프 위에 ▲/▼ 버튼층이 선다(B05 와 같은 부품). * 안 넘기면 버튼이 없고 그래프만 보인다(옛 저장분처럼 선형이 없는 노선). */ grade?: { @@ -104,6 +108,7 @@ export function drawLongitudinalPanel( // 계획고 편집 ▼ 버튼이 바닥에 붙으므로 X축·측점 라벨을 그만큼 밀어 올린다 // (B05 와 같은 값 15px). 버튼이 없으면 0 — 종전 여백 그대로다. bottomInsetPx: input.grade ? 15 : 0, + holdRange: input.holdRange, // 측점선을 끌면 그 구조물이 옮겨 간다 — 관은 예약 이동, 구조물은 정본 이동. onDragStation: edit ? moveMark : undefined, }); @@ -208,7 +213,11 @@ export function drawLongitudinalPanel( chartWrap.scrollLeft, chartWrap.clientWidth || chartWidth, ); - return applyElevationWindow(chartWrap, visibleElevationRange(detail, fromM, toM) ?? undefined); + const next = visibleElevationRange(detail, fromM, toM); + return applyElevationWindow( + chartWrap, + input.holdRange ? input.holdRange(next) : (next ?? undefined), + ); }; } @@ -239,3 +248,35 @@ export function attachWindowScroll( }, SCROLL_SETTLE_MS); }); } + +/** + * 계획고 ▲▼ 를 **누르고 있는 동안 세로 창을 고정**한다(B05 와 같은 장치). + * + * 안 그러면 창이 새 계획선에 맞춰 다시 잡혀, 값은 바뀌는데 선은 제자리에 있는 것처럼 + * 보인다(2026-09-12 사용자 보고: 「버튼을 눌러도 계획 종단선이 안 바뀐다」). 손을 뗀 뒤 + * 한 번만 다시 맞춘다. + */ +export function createElevationHold( + chartWrap: HTMLElement, + redraw: () => void, +): (next: { min: number; max: number } | null) => { min: number; max: number } | undefined { + let editing = false; + let held: { min: number; max: number } | undefined; + chartWrap.addEventListener("pointerdown", (event) => { + if (!(event.target as HTMLElement).closest(".b05-profile-edit__btn")) return; + editing = true; + const release = (): void => { + editing = false; + window.removeEventListener("pointerup", release); + window.removeEventListener("pointercancel", release); + redraw(); + }; + window.addEventListener("pointerup", release); + window.addEventListener("pointercancel", release); + }); + return (next) => { + if (editing) return held; + held = next ?? undefined; + return held; + }; +}