/* ============================================================================= * B05_Profile_UI_RouteEdit_StructureMenu.ts * 평면 우클릭 구조물 메뉴(513-1 · 요구 327-1). * * · 메뉴 = 종단 그래프 우클릭과 같은 항목 · 같은 동작(`structureMenuItemsAt` · 재료 = 지금 선 종단 그래프). * · 자리 = 누른 곳을 노선에 내린 누가거리 · 가까운 구조물 · 배관 측점이면 삭제. * · 노드 위 = 옛 동작 그대로(노드 삭제 · `bindNodeSplice`) · 노선에서 먼 곳 = 메뉴 없음. * ========================================================================== */ import { pickPx } from "../A00_Common/touch_bridge"; import { createMapContextMenu } from "@ui/ui_template_context_menu"; import { liveStructureMenu, structureMenuItemsAt } from "./B05_Profile_UI_Profile_Structures"; import { routePointAtScreen } from "./B05_Profile_UI_RouteEdit_Input"; import type { RouteEditPointerParams } from "./B05_Profile_UI_RouteEdit_Pointer"; import { stationFrameAt } from "./logic/B05_Profile_UI_RouteEdit_Frame"; /** 노선 위로 볼 거리(px) — 이보다 멀면 구조물 메뉴를 안 띄움. */ const ROUTE_HIT_PX = 12; /** 구조물 · 배관 측점을 잡았다고 볼 거리(px) — 평면 마커 집기와 같은 값. */ const STATION_HIT_PX = 10; /** 평면 캔버스에 구조물 우클릭 메뉴를 붙임 — 캔버스와 수명이 같음. */ export function bindPlanStructureMenu( params: RouteEditPointerParams, nodeAt: (px: number, py: number) => number, ): void { const { canvas, toScreen } = params; const menu = createMapContextMenu("b05-profile-chart"); canvas.parentElement?.append(menu.element); canvas.addEventListener("contextmenu", (event) => { menu.close(); // 노선 밖 · 노드 위 우클릭이면 옛 메뉴도 닫힌 채 const options = liveStructureMenu(); const rect = canvas.getBoundingClientRect(); const [px, py] = params.unrotate(event.clientX - rect.left, event.clientY - rect.top); if (!options || nodeAt(px, py) >= 0) return; // 노드 삭제 몫 const state = params.state(); const line = state.plannedLine.length ? state.plannedLine : state.planned; const hit = routePointAtScreen(line, toScreen, px, py, pickPx(ROUTE_HIT_PX)); if (!hit) return; const near = options.stations .map((station) => { const frame = stationFrameAt(line, station.chainage_m); const [x, y] = frame ? toScreen(frame.origin) : [Infinity, Infinity]; return { station, distance: Math.hypot(x - px, y - py) }; }) .filter((entry) => entry.distance <= pickPx(STATION_HIT_PX)) .sort((left, right) => left.distance - right.distance)[0]; const host = menu.element.parentElement?.getBoundingClientRect() ?? rect; menu.open( event.clientX - host.left, event.clientY - host.top, structureMenuItemsAt(options, hit.chainageM, near?.station ?? null), ); }); }