/* ============================================================================= * B05_Profile_UI_RouteEdit_ContourLock.ts * 평면 등고선 자물쇠(353-2) — 등고선 위를 지나거나 고른 가닥 위에 뜨는 작은 단추. * * 누르면 그 높이를 고정(빈 곳 · 다른 가닥을 눌러도 고른 것이 안 바뀜) · 다시 누르면 풂. * 높이값으로 쥐어 지형을 바꿔도 같은 높이를 다시 짚음 · 페이지 동안만(저장 안 함). * 규칙은 `logic/B05_Profile_ContourLock` · 호버 판정은 집기와 같은 `hitPreparedLayer`. * ========================================================================== */ import { pickPx } from "../A00_Common/touch_bridge"; import { hitPreparedLayer, type PreparedLayer, type ViewState, } from "../B04_PreProcess/B04_PreProcess_UI_MapRender"; import { canPickContour, featureOfLevel, lockButtonState, toggleLock, } from "./logic/B05_Profile_ContourLock"; import "./B05_Profile_UI_Style_RouteEdit_ContourLock.css"; /** 집기(`_Pointer` CONTOUR_HIT_PX)와 같은 거리(px). */ const HOVER_HIT_PX = 6; /** 커서가 선을 떠난 뒤 단추가 남는 시간(ms) — 단추로 옮겨 갈 틈. */ const HIDE_DELAY_MS = 700; /** 단추는 커서 오른쪽 위로 이만큼(px) — 선과 커서를 안 가림. */ const OFFSET_PX = 12; const LOCK_SVG = ``; /** 호버 판정에 쓰는 지금 화면 — 등고선이 없으면 null. */ export interface ContourLockScene { layer: PreparedLayer; view: ViewState; /** 지금 화면 등고선 간격 — 그리지 않은 줄은 호버도 안 됨. */ stepM: number; rotation: { unrotate: (px: number, py: number) => [number, number] }; } export interface ContourLock { /** 고른 가닥을 바꿔도 되는지(잠긴 동안 false). */ canPick: () => boolean; /** 지형을 바꿔 새 가닥들이 왔다 — 잠근 높이의 가닥 번호(없으면 -1). */ reattach: (layer: PreparedLayer | null) => number; } /** 자물쇠 단추를 캔버스 칸 위에 세움 · `pick` = 잠글 때 그 가닥을 고른 것으로(다시 그리기까지). */ export function createContourLock( canvas: HTMLCanvasElement, scene: () => ContourLockScene | null, pick: (index: number) => void, ): ContourLock { const wrap = canvas.parentElement ?? document.body; const button = document.createElement("button"); button.type = "button"; button.className = "ui-btn ui-btn--glass b05-contourlock"; button.dataset.act = "contour-lock"; button.hidden = true; button.innerHTML = LOCK_SVG; wrap.append(button); let lockedLevel: number | null = null; /** 단추가 가리키는 가닥 — 누르면 이 가닥을 잠금. */ let target = -1; let hideTimer = 0; let frame = 0; let last: PointerEvent | null = null; const levelOf = (index: number): number | null => index >= 0 ? (scene()?.layer.features[index]?.labelValue ?? null) : null; const paint = (): void => { const pressed = lockedLevel !== null; button.setAttribute("aria-pressed", String(pressed)); const label = pressed ? `등고선 ${lockedLevel}m 고정 풀기 — 풀면 다른 등고선을 고를 수 있음` : `등고선 ${levelOf(target) ?? ""}m 고정 — 빈 곳 · 다른 등고선을 눌러도 안 바뀜`; button.setAttribute("aria-label", label); button.title = label; }; const hideLater = (): void => { if (hideTimer) return; hideTimer = window.setTimeout(() => { hideTimer = 0; button.hidden = true; }, HIDE_DELAY_MS); }; const keep = (): void => { window.clearTimeout(hideTimer); hideTimer = 0; }; const hover = (event: PointerEvent): void => { const now = scene(); // 단추를 누른 채(노드 · 손잡이 끌기 · 화면 이동) 지나가면 안 봄 if (!now || event.buttons !== 0) return hideLater(); const rect = canvas.getBoundingClientRect(); const [px, py] = now.rotation.unrotate(event.clientX - rect.left, event.clientY - rect.top); const hitPx = pickPx(HOVER_HIT_PX); const index = hitPreparedLayer(now.layer, now.view, px, py, hitPx, now.stepM); const state = lockButtonState(levelOf(index), lockedLevel); if (!state.visible) return hideLater(); keep(); target = index; const origin = wrap.getBoundingClientRect(); button.style.left = `${Math.round(event.clientX - origin.left + OFFSET_PX)}px`; button.style.top = `${Math.round(event.clientY - origin.top - OFFSET_PX)}px`; button.hidden = false; paint(); }; // 한 프레임에 한 번만 잼 — 도엽 등고선은 수천 가닥. canvas.addEventListener("pointermove", (event) => { last = event; if (frame) return; frame = requestAnimationFrame(() => { frame = 0; if (last) hover(last); }); }); // 고른 직후(누름을 뗀 자리)에도 띄움 — 터치는 호버가 없음. canvas.addEventListener("pointerup", hover); canvas.addEventListener("pointerleave", hideLater); button.addEventListener("pointerenter", keep); button.addEventListener("pointerleave", hideLater); button.addEventListener("pointerdown", (event) => event.stopPropagation()); button.addEventListener("click", (event) => { event.stopPropagation(); lockedLevel = toggleLock(lockedLevel, levelOf(target)); if (lockedLevel !== null) pick(target); paint(); }); return { canPick: () => canPickContour(lockedLevel), reattach: (layer) => { target = -1; button.hidden = true; return featureOfLevel( layer?.features.map((feature) => feature.labelValue) ?? [], lockedLevel, ); }, }; }