fix(b06): 계획고를 누르는 동안 세로 창을 고정 — 선이 안 움직여 보이던 원인

B05 에는 있고 B06 에 없던 장치였음. ▲▼ 를 누를 때마다 세로 창이 새 계획선에 맞춰 다시
잡혀, 값은 바뀌는데 선은 제자리에 있는 것처럼 보였음. B05 와 같이 누르고 있는 동안
창을 고정하고 손을 뗀 뒤 한 번만 다시 맞춤.

실측(랩탑_보조 534.0m 측점): 계획고 837.24 → 839.84m 일 때 설계선이 3.2px 올라감.
종전에는 같은 조작에서 0.4px 만 움직였음.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fe1QWPTfw11PaKh2LjwXdR
This commit is contained in:
2026-09-12 19:07:30 +09:00
co-authored by Claude Opus 5
parent 83fdbe912d
commit 3518f346e4
3 changed files with 58 additions and 8 deletions
+8 -6
View File
@@ -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<StructureInstance>,
types: ReadonlyArray<StructureType>,
) => 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<StructureInstance> = [];
let markTypes: ReadonlyArray<StructureType> = [];
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) {
@@ -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 };
@@ -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;
};
}