- 칠한 결과(고른 측점선 · 없으면 고른 구조물 알약)를 보고 보이는 칸 밖일 때만 가운데로 옮김(createSelectionReveal · design_profile_viewport) - 같은 고름을 다시 칠하면 안 밂(사용자가 옮겨 본 자리 지킴) · 그래프 안 누름은 보이는 자리라 안 움직임 · 고른 뒤 다시 그리기가 끊지 않게 바로 옮김 - 요구 168 원문 기록 · 시험 test_273_1 새로 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LLCk2y5xE8YGVgjbTntWsY
101 lines
5.0 KiB
TypeScript
101 lines
5.0 KiB
TypeScript
/* =============================================================================
|
|
* design_profile_viewport.ts
|
|
* 종단 스크롤러의 가로 스크롤 · 휠 · 크기 바뀜 듣개(213-3 · 700줄 한계로 뗌).
|
|
* 다시 그리기 · 높이 동기화는 종단도가 넘긴 함수를 부른다.
|
|
* ========================================================================== */
|
|
|
|
import { attachWheelHorizontalScroll } from "../../B05_Profile/B05_Profile_UI_Profile_Wheel";
|
|
import { needsFullRedraw, type ElevationWindowResult } from "@util/common_util_chart_ywindow";
|
|
import { viewOf } from "@util/common_util_view_window";
|
|
|
|
/** 그래프를 통째로 다시 만들 때까지 기다리는 시간(ms) — 눈금 갱신으로도 못 살릴 때만. */
|
|
const SCROLL_SETTLE_MS = 80;
|
|
|
|
export interface ProfileViewportDeps {
|
|
body: HTMLElement;
|
|
/** ▲▼ 계획고 편집으로 Y 축을 잡고 있는 중. */
|
|
editing: () => boolean;
|
|
/** 세로 창 갱신기 — 그릴 때마다 렌더러가 새로 준다(없으면 null). */
|
|
fitElevation: () => ElevationWindowResult | null;
|
|
draw: () => void;
|
|
/** 마지막으로 그린 본문 크기 — 같으면 크기 바뀜을 무시한다. */
|
|
lastSize: () => { width: number; height: number };
|
|
scheduleLightSync: () => void;
|
|
scheduleDraw: () => void;
|
|
}
|
|
|
|
/** 듣개를 붙이고 떼는 함수를 돌려준다. */
|
|
export function bindProfileViewport(deps: ProfileViewportDeps): () => void {
|
|
const { body } = deps;
|
|
/** 마지막으로 통째로 다시 만든 가로 위치 — 같은 자리면 다시 그리지 않는다. */
|
|
let settledScrollLeft = 0;
|
|
let scrollSettleTimer = 0;
|
|
/** 시계를 건 창 — 새 창으로 빼면 그 창 시계를 쓴다(가려진 메인 창 시계는 늦게 돎 · 230-1). */
|
|
let timerView: Window = window;
|
|
// 세로 맞춤은 **그리기가 아니라 변환**이다(2026-09-04 사용자 확정) — 스크롤마다 겹 하나의
|
|
// 변환만 갈아 끼우므로 실시간으로 따라온다. 눈금 간격이 어긋나면 눈금층(요소 20개 안팎)만
|
|
// 새로 만든다. 그래프를 통째로 다시 만드는 34ms 짜리 재구성은 그래도 안 될 때만 남는다.
|
|
body.addEventListener("scroll", () => {
|
|
if (deps.editing()) return;
|
|
const fitted = deps.fitElevation();
|
|
timerView.clearTimeout(scrollSettleTimer);
|
|
if (!needsFullRedraw(fitted)) return;
|
|
timerView = viewOf(body);
|
|
scrollSettleTimer = timerView.setTimeout(() => {
|
|
if (deps.editing() || Math.abs(body.scrollLeft - settledScrollLeft) < 1) return;
|
|
settledScrollLeft = body.scrollLeft;
|
|
deps.draw();
|
|
}, SCROLL_SETTLE_MS);
|
|
});
|
|
|
|
// 종단면도는 가로로 매우 길다. 세로 휠을 가로 스크롤로 돌려 스크롤바를 잡지 않고도
|
|
// 노선을 훑을 수 있게 한다 (Shift+휠은 브라우저 기본 가로 스크롤이라 그대로 둔다).
|
|
attachWheelHorizontalScroll(body);
|
|
|
|
const resizeObserver = new ResizeObserver(() => {
|
|
const last = deps.lastSize();
|
|
if (
|
|
body.clientWidth <= 0 ||
|
|
body.clientHeight <= 0 ||
|
|
(Math.abs(body.clientWidth - last.width) < 1 && Math.abs(body.clientHeight - last.height) < 1)
|
|
)
|
|
return;
|
|
// 끌리는 동안엔 프레임당 경량 높이 동기화만 — 무거운 전체 재구성은 손을 뗀 뒤 한 번.
|
|
// (150ms 디바운스만 있던 예전 방식은 중간 프레임이 없어 툭툭 끊겼다. 2026-08-05 사용자 보고)
|
|
deps.scheduleLightSync();
|
|
deps.scheduleDraw();
|
|
});
|
|
resizeObserver.observe(body);
|
|
return () => resizeObserver.disconnect();
|
|
}
|
|
|
|
/** 고른 것이 보이는 칸 가장자리에서 이만큼(폭 비율) 안쪽이면 그대로 둔다. */
|
|
const REVEAL_EDGE = 0.1;
|
|
|
|
/**
|
|
* 고른 측점선(없으면 고른 구조물 알약)이 종단도 보이는 칸 밖이면 가운데로 민다 — 다른 영역(횡단 카드 ·
|
|
* 3D · 배수유역도 · 좌측 목록)에서 고른 자리도 종단에서 바로 보이게. 칠한 결과(강조된 선 · 알약)를
|
|
* 보고 옮긴다 · 같은 고름을 다시 칠할 때는 안 밂(사용자가 옮겨 본 자리를 지킴) · 그래프 안에서
|
|
* 누른 것은 이미 보이는 자리라 안 움직임. `key` = 고름 열쇠(풀면 null).
|
|
*/
|
|
export function createSelectionReveal(body: HTMLElement): (key: string | null) => void {
|
|
let shown: string | null = null;
|
|
return (key) => {
|
|
if (key === null || key === shown) {
|
|
shown = key;
|
|
return;
|
|
}
|
|
const mark =
|
|
body.querySelector(".b06-chart__station--selected") ??
|
|
body.querySelector(".b05-structure__mark.is-selected");
|
|
if (!mark || body.clientWidth <= 0) return; // 아직 안 그려짐 — 다음 칠하기에서
|
|
shown = key;
|
|
const box = mark.getBoundingClientRect();
|
|
const view = body.getBoundingClientRect();
|
|
const at = box.left + box.width / 2 - view.left;
|
|
if (at >= view.width * REVEAL_EDGE && at <= view.width * (1 - REVEAL_EDGE)) return;
|
|
// 바로 옮김 — 고른 뒤 잇따르는 다시 그리기가 부드러운 이동을 끊음(다시 그리기는 지금 자리를 지킴)
|
|
body.scrollLeft += at - view.width / 2;
|
|
};
|
|
}
|