/* 513-1 B05 평면 우클릭 구조물 메뉴(요구 327-1) — `_Profile_Structures` · `_RouteEdit_StructureMenu` 를 Node 로 * 돌려 JSON 한 줄(`test_513_1_plan_structure_menu.py` 가 판정). 메뉴 판 · 측점 판정은 가짜 · 항목 짓기 · 노선 셈은 진짜. * node helper_513_1_plan_structure_menu.cjs */ const path = require("path"); const { makeLoader } = require("./helper_ts_stub_loader.cjs"); const B05 = path.join(__dirname, "..", "..", "B05_Profile"); const opens = []; const calls = []; /** 가짜 메뉴 — 연 자리 · 항목만 적음. */ const ui = () => ({ createMapContextMenu: () => ({ element: { parentElement: null }, open: (x, y, items) => opens.push({ x, y, items }), close() {}, contains: () => false, }), }); const STUBS = { "../A00_Common/touch_bridge": { pickPx: (v) => v }, "./B05_Profile_UI_IrregularStations": { irregularStationId: (id) => `irr:${id}`, isPipeStation: (station) => station.origin === "pipe", }, "./B05_Profile_Api_Structures": { structureAnchorM: (s) => s.chainage_m ?? 0 }, "../A00_Common/design/design_selection": { PIPE_MATCH_M: 0.51 }, "./B05_Profile_UI_RouteEdit_StructureMarks": { planMarkAt: () => null }, // 마커 없음(514-1 몫) }; const { loadTs } = makeLoader({ root: path.join(__dirname, "..", ".."), stubs: STUBS, ui }); const shared = loadTs(path.join(B05, "B05_Profile_UI_Profile_Structures.ts")); const plan = loadTs(path.join(B05, "B05_Profile_UI_RouteEdit_StructureMenu.ts")); /** 듣기만 적는 가짜 요소. */ function fakeElement(connected = true) { const listeners = {}; return { isConnected: connected, listeners, addEventListener: (type, fn) => (listeners[type] = listeners[type] || []).push(fn), getBoundingClientRect: () => ({ left: 0, top: 0 }), append(child) { child.parentElement = this; }, parentElement: null, fire(type, clientX, clientY) { let prevented = false; (listeners[type] || []).forEach((fn) => fn({ clientX, clientY, target: null, preventDefault: () => (prevented = true) }), ); return prevented; }, }; } // 노선 0 → 100 m 직선 · 화면 2 px/m · 배관 측점 40 m · 추가 측점 70 m const PLUS = loadTs( path.join(__dirname, "..", "..", "common_util", "common_util_structure_station.ts"), ).PLUS_OWNER_PREFIX; const stations = [ { id: "pipe-1", chainage_m: 40, origin: "pipe" }, { id: `${PLUS}x`, chainage_m: 70 }, ]; const options = { stations, x: (m) => m * 2, chainageAt: (px) => px / 2, maxChainageM: 100, onRemove: (station) => calls.push(["remove", station.id]), onRemovePlus: (m) => calls.push(["removePlus", m]), onAddPipe: (m) => calls.push(["addPipe", m]), structureGroups: () => [{ label: "A군", types: [{ type: { name: "배수관", type_id: "pipe" } }] }], onAddStructureType: (m, typeId) => calls.push(["addType", m, typeId]), }; const labels = (items) => items.map((item) => Array.isArray(item) ? item[0] : [item.label, item.children.map((c) => c[0])], ); const pickFirst = (items) => { const first = items[0]; (Array.isArray(first) ? first[1] : first.children[0][1])(); }; const out = {}; out.liveBefore = shared.liveStructureMenu() === null; // 종단 그래프 — 빈 자리 · 배관 위 const host = fakeElement(); shared.mountStructureMenu(host, options); host.fire("contextmenu", 120, 5); // 60 m out.profileEmpty = labels(opens.at(-1).items); pickFirst(opens.at(-1).items); host.fire("contextmenu", 81, 5); // 40.5 m — 배관 out.profilePipe = labels(opens.at(-1).items); // 평면 — 같은 재료 const wrap = fakeElement(); const canvas = fakeElement(); canvas.parentElement = wrap; const line = [ [0, 0], [100, 0], ]; const params = { canvas, toScreen: (v) => [v[0] * 2, v[1] * 2], unrotate: (x, y) => [x, y], state: () => ({ plannedLine: line, planned: line }), }; const nodeAt = (px, py) => (Math.hypot(px, py) <= 6 ? 0 : Math.hypot(px - 200, py) <= 6 ? 1 : -1); plan.bindPlanStructureMenu(params, nodeAt); const planAt = (x, y) => { const before = opens.length; canvas.fire("contextmenu", x, y); return opens.length > before ? labels(opens.at(-1).items) : null; }; out.planEmpty = planAt(120, 8); // 60 m · 노선에서 8 px pickFirst(opens.at(-1).items); out.planPipe = planAt(82, 6); // 40 m 배관 근처 pickFirst(opens.at(-1).items); out.planPlus = planAt(141, -4); // 70 m 추가 측점 out.planMenuAt = [opens.at(-1).x, opens.at(-1).y]; // 누른 화면 자리(캔버스 그릇 기준) pickFirst(opens.at(-1).items); out.planFar = planAt(120, 40); // 노선에서 40 px out.planNode = planAt(2, 1); // 시점 노드 위 = 노드 삭제 몫 host.isConnected = false; // 종단 그래프가 떠남 out.planGone = planAt(120, 8); out.calls = calls; process.stdout.write(JSON.stringify(out));