/* ============================================================================= * B05_Profile_UI_RouteEdit_CurveIcon.ts * 교각점 정보 아이콘(445-C1) — 교각점을 고른 채 마우스가 근처일 때만 교각점 옆에 뜨는 작은 단추. * 누르면 정보 창이 열림(멀어져도 유지) · 멀어지면 0.7 s 뒤 숨김 · 터치(마우스 없음)는 고르면 늘 보임. * 규칙은 `logic/B05_Profile_RouteEdit_CurveIcon` · 창 열림 기억은 `_CurveBar`. * ========================================================================== */ import { createIconButton } from "@ui/ui_template_elements"; import { iconVisible, isNear } from "./logic/B05_Profile_RouteEdit_CurveIcon"; import "./B05_Profile_UI_Style_RouteEdit_CurveIcon.css"; /** 멀어진 뒤 아이콘이 남는 시간(ms) — 아이콘으로 옮겨 갈 틈(등고선 자물쇠와 같은 값). */ const HIDE_DELAY_MS = 700; /** 아이콘은 교각점 오른쪽 위로 이만큼(px). */ const OFFSET_PX = 14; export interface CurveIcon { /** 고름 · 창 열림 · 교각점 화면 자리(클라이언트 좌표 · 고름 없으면 null)를 알린다. */ update: (anchor: [number, number] | null, open: boolean) => void; destroy: () => void; } export function createCurveIcon(canvas: HTMLCanvasElement, onOpen: () => void): CurveIcon { const wrap = canvas.parentElement ?? document.body; const button = createIconButton({ icon: "adjustments", title: "교각점 정보 열기", size: 24, variant: "glass", }); button.classList.add("b05-curveicon"); button.dataset.act = "curve-icon"; button.hidden = true; wrap.append(button); let anchor: [number, number] | null = null; let open = false; let touch = false; let mouse: [number, number] | null = null; let overIcon = false; let pressing = false; let hideTimer = 0; const keep = (): void => { window.clearTimeout(hideTimer); hideTimer = 0; }; const hideNow = (): void => { keep(); button.hidden = true; }; const hideLater = (): void => { if (hideTimer) return; hideTimer = window.setTimeout(() => { hideTimer = 0; button.hidden = true; }, HIDE_DELAY_MS); }; /** 지금 상태로 보임 · 숨김을 다시 정함 — 멀면 바로 숨기지 않고 틈 뒤. */ const refresh = (): void => { if (!anchor || open || pressing) return hideNow(); const distance = mouse ? Math.hypot(mouse[0] - anchor[0], mouse[1] - anchor[1]) : Infinity; if (!iconVisible(0, false, isNear(distance, overIcon), touch)) { if (!button.hidden) hideLater(); return; } keep(); const origin = wrap.getBoundingClientRect(); button.style.left = `${Math.round(anchor[0] - origin.left + OFFSET_PX)}px`; button.style.top = `${Math.round(anchor[1] - origin.top - OFFSET_PX - 24)}px`; button.hidden = false; }; canvas.addEventListener("pointermove", (event) => { touch = event.pointerType === "touch"; mouse = [event.clientX, event.clientY]; if (event.buttons === 0) refresh(); }); canvas.addEventListener("pointerdown", (event) => { touch = event.pointerType === "touch"; mouse = [event.clientX, event.clientY]; pressing = true; refresh(); }); // 놓은 뒤 다시 판정 — 고른 자리가 바뀌었으면 `update` 가 곧 이어서 부른다. const release = (): void => { pressing = false; refresh(); }; canvas.addEventListener("pointerup", release); canvas.addEventListener("pointercancel", release); canvas.addEventListener("wheel", hideNow, { passive: true }); canvas.addEventListener("pointerleave", () => { mouse = null; if (!overIcon && !button.hidden) hideLater(); }); button.addEventListener("pointerenter", () => { overIcon = true; keep(); }); button.addEventListener("pointerleave", () => { overIcon = false; hideLater(); }); button.addEventListener("pointerdown", (event) => event.stopPropagation()); button.addEventListener("click", (event) => { event.stopPropagation(); hideNow(); onOpen(); }); return { update: (nextAnchor, nextOpen) => { anchor = nextAnchor; open = nextOpen; refresh(); }, destroy: () => { hideNow(); button.remove(); }, }; }