- 교각점 고름 = 창 없이 근처(40 px) 아이콘 · 멀면 0.7 s 뒤 숨김 · 누름 = 창 열림(멀어져도 유지) - 창 열린 채 다른 교각점 = 열린 채 새 정보 · 닫힌 채 = 닫힌 채 · [닫기] = 창 닫고 고름 풂 - 부품 상자 아이콘 단추 「유리」 꼴 더함(견본 · 시험 552-2) - 새 지킴 시험 test_445_curve_icon · 398-2 · 408 도우미에 가짜 아이콘 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
125 lines
4.2 KiB
TypeScript
125 lines
4.2 KiB
TypeScript
/* =============================================================================
|
|
* 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();
|
|
},
|
|
};
|
|
}
|