/* ============================================================================= * B05_Profile_UI_RouteEdit_Label.ts * 고른 꺾임점 **옆에 뜨는 R 라벨** — 반지름을 바꾸고, 곡선을 지우고 넣는다. * * 왜 옮겼나(2026-09-07 사용자 지시 ③) — 예전에는 모달 **맨 아래 줄**이었다. 지금 고른 것이 * 지도 어디인지 눈으로 이어지지 않아, 값을 바꾸면서도 어느 곡선을 만지는지 몰랐다. * 「해당 요소를 선택하면 우측에 라벨 같은 입력이 보이게」가 사용자의 말 그대로다. * * 캔버스 밖으로 나가지 않게 가장자리에서 **반대쪽으로 접어 넣는다** — 노선 끝의 꺾임점을 * 골라도 입력칸이 잘리지 않아야 한다. * ========================================================================== */ /** 라벨과 노드 사이 여백(px). */ const GAP_PX = 14; /** 캔버스 가장자리에서 이만큼은 띄운다(px). */ const EDGE_PX = 8; export interface CurveLabelState { /** 몇 번째 꺾임점인지 — 0부터 센 자리. 표시는 +1 해서 낸다. */ seat: number; /** 그 꺾임점의 화면 좌표(캔버스 기준 px). */ at: [number, number]; /** 캔버스 크기(px) — 라벨을 안쪽으로 접어 넣는 데 쓴다. */ canvas: { width: number; height: number }; curveOn: boolean; /** 지금 보일 반지름(m). 곡선이 없으면 null. */ radiusShown: number | null; /** 사용자가 못박은 값인지(아니면 자동). */ forced: boolean; innerAngleDeg: number | null; minRadiusM: number; } export interface CurveLabelHandlers { /** R 칸을 고쳤다 — null 이면 「자동」. */ onRadius: (value: number | null) => void; /** 곡선을 지우거나 넣었다. */ onCurveOn: (on: boolean) => void; /** 반지름을 자동으로 되돌렸다. */ onAuto: () => void; } export interface CurveLabel { show: (state: CurveLabelState) => void; hide: () => void; } /** 라벨을 만들어 `host`(캔버스를 감싼 칸, `position: relative`)에 붙인다. */ export function createCurveLabel(host: HTMLElement, handlers: CurveLabelHandlers): CurveLabel { const root = document.createElement("div"); root.className = "b05-routeedit__label"; root.hidden = true; root.innerHTML = `
`; host.append(root); const seatText = root.querySelector(".b05-routeedit__curve-label")!; const toggle = root.querySelector('[data-act="curve-toggle"]')!; const radius = root.querySelector(".b05-routeedit__curve-radius")!; const auto = root.querySelector('[data-act="curve-auto"]')!; const info = root.querySelector(".b05-routeedit__curve-info")!; // 라벨 위에서 누른 것이 캔버스로 새어 나가면 노드가 딸려 움직인다. root.addEventListener("pointerdown", (event) => event.stopPropagation()); root.addEventListener("dblclick", (event) => event.stopPropagation()); root.addEventListener("contextmenu", (event) => event.stopPropagation()); let curveOn = true; radius.addEventListener("change", () => { const value = Number(radius.value); handlers.onRadius(Number.isFinite(value) && value > 0 ? value : null); }); toggle.addEventListener("click", () => handlers.onCurveOn(!curveOn)); auto.addEventListener("click", () => handlers.onAuto()); function place(at: [number, number], canvas: { width: number; height: number }): void { // 먼저 보여야 크기를 잴 수 있다 — 그 다음 자리를 잡는다. const width = root.offsetWidth; const height = root.offsetHeight; // 기본은 오른쪽. 오른쪽이 좁으면 왼쪽으로 접는다(사용자 말대로 「우측에」가 기본). let left = at[0] + GAP_PX; if (left + width > canvas.width - EDGE_PX) left = at[0] - GAP_PX - width; left = Math.max(EDGE_PX, Math.min(left, canvas.width - width - EDGE_PX)); let top = at[1] - height / 2; top = Math.max(EDGE_PX, Math.min(top, canvas.height - height - EDGE_PX)); root.style.left = `${Math.round(left)}px`; root.style.top = `${Math.round(top)}px`; } return { show(state) { curveOn = state.curveOn; root.hidden = false; seatText.textContent = `${state.seat + 1}번째 꺾임점`; toggle.textContent = state.curveOn ? "곡선 지우기" : "곡선 넣기"; radius.disabled = !state.curveOn; auto.disabled = !state.curveOn || !state.forced; radius.value = state.radiusShown === null ? "" : String(Math.round(state.radiusShown * 10) / 10); const inner = state.innerAngleDeg; info.textContent = state.curveOn ? `${state.forced ? "값 지정" : "자동"}${inner ? ` · 내각 ${Math.round(inner)}°` : ""}` + ` · 법정 하한 ${state.minRadiusM}m` : "곡선 없음 — 직선이 그대로 꺾입니다"; place(state.at, state.canvas); }, hide() { root.hidden = true; }, }; }