131 lines
4.7 KiB
JavaScript
131 lines
4.7 KiB
JavaScript
/* 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));
|