/* 411-A B05 평면 오른쪽 횡단 판 — 구조물 · 관을 골라도 그 측점 횡단 · 옮긴 뒤에도 열쇠로 따라감. * `_RouteEdit_Cross` 를 Node 로 돌려 JSON 한 줄(`test_411_a_cross_structure.py` 가 판정). 묶음 · 그림 · 가게는 가짜 · * 고름 구독 · 측점 짚기 · 판 채우기는 진짜. * node helper_411_a_cross_structure.cjs */ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); const B05 = path.join(__dirname, "..", "..", "B05_Profile"); const out = {}; /* 가짜 DOM — 판(제목 · 상자 · 발) 글만 봄 */ function fakeElement() { const parts = new Map(); const element = { textContent: "", children: [], isConnected: true, clientHeight: 150, setAttribute() {}, append: (...items) => element.children.push(...items), replaceChildren: (...items) => (element.children = items), getBoundingClientRect: () => ({ width: 240, height: 150 }), querySelector: (selector) => { if (!parts.has(selector)) parts.set(selector, fakeElement()); return parts.get(selector); }, }; return element; } global.document = { createElement: fakeElement }; global.ResizeObserver = class { observe() {} disconnect() {} }; let listener = null; let stations = []; const route = { id: 1 }; const subscribers = []; const pipe = { output: (stage) => (stage === "ground" ? { stations } : {}), input: () => route, subscribe: (fn) => (subscribers.push(fn), () => {}), }; const crossStage = () => subscribers.forEach((fn) => fn({ kind: "stage", stage: "cross" })); const STUBS = { "../A00_Common/b_svg_zoom_pan": { attachSvgZoomPan() {} }, "../A00_Common/design/design_pipeline_page": { pagePipeline: () => pipe }, "../A00_Common/design/design_selection": { nearestWithin: (items, at, of) => { let best; let gap = 0.51; for (const item of items) if (Math.abs(of(item) - at) < gap) [best, gap] = [item, Math.abs(of(item) - at)]; return best; }, selectDesign() {}, subscribeSelection: (_projectId, fn) => ((listener = fn), () => {}), }, "../A00_Common/design/design_pick_escape": { PLAN_PICK_FROM: "b05-plan" }, "../A00_Common/design/design_store": { useDesignStore: () => ({ subscribe: () => () => {}, get: () => null }), }, "./logic/B05_Profile_Pipeline_Detail": { stationSection: (station) => ({ chainage_m: station.chainageM, samples: [] }), }, "./B05_Profile_UI_RouteEdit_Cross_Draw": { buildCrossSvg: () => ({ svg: fakeElement(), layer: null, width: 240, height: 150 }), CROSS_PLOT_PAD: 0, summarizeCross: (preview) => `횡단 ${preview.chainage_m}`, }, "../A00_Common/b_page_state": { readStateRaw: () => null }, "../A00_Common/design/design_page_route": { pageRouteId: () => null }, "../common_util/common_util_station": { stationLabel: (chainageM) => String(chainageM) }, "./B05_Profile_UI_RouteEdit_Chrome": { sidePanelsOf: new Map() }, "./B05_Profile_UI_RouteEdit_Cross_Card": { routeCrossCard: () => fakeElement(), routeCrossSource: () => null, }, }; const cache = new Map(); function loadTs(full) { if (cache.has(full)) return cache.get(full); const js = ts.transpileModule(fs.readFileSync(full, "utf8"), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, }).outputText; const box = { exports: {} }; cache.set(full, box.exports); const localRequire = (request) => { if (STUBS[request]) return STUBS[request]; if (request.startsWith(".")) return loadTs(path.join(path.dirname(full), `${request}.ts`)); return require(request); }; new Function("exports", "module", "require", js)(box.exports, box, localRequire); cache.set(full, box.exports); return box.exports; } const cross = loadTs(path.join(B05, "B05_Profile_UI_RouteEdit_Cross.ts")); const station = (key, chainageM, kind, id = null) => ({ key, chainageM, kind, id }); const grid = [ station("end:BP", 0, "end"), station("grid:1", 20, "grid"), station("grid:2", 40, "grid"), ]; const side = fakeElement(); const preview = cross.createCrossPreview({ projectId: "p1", side, request: () => ({ vertices: [], min_radius_m: 15, station_interval_m: 20 }), }); const [current] = side.children; const shownNow = () => ({ station: current.querySelector(".b05-routeedit__cross-station").textContent, foot: current.querySelector(".b05-routeedit__cross-foot").textContent, }); const pick = (chainageM, structureId, from) => listener({ chainageM, structureId }, from); // ① 구조물 고름(id) = 그 측점 횡단 · 법선은 마커 몫(structure) stations = [ ...grid, station("structure:A", 45, "structure", "A"), station("pipe:p1", 80.17, "pipe", "p1"), ]; pick(45, "A", "b05"); out.structurePick = { ...shownNow(), structure: preview.watched()?.structure ?? null }; // ② 옮김 — 묶음이 새 자리로(같은 열쇠) · cross 차례 = 새 자리 횡단(고름은 옛 45 그대로) stations = [ ...grid, station("structure:A", 52, "structure", "A"), station("pipe:p1", 80.17, "pipe", "p1"), ]; crossStage(); out.structureMoved = { ...shownNow(), at: preview.watched()?.chainageM ?? null }; // ③ 관 고름(누가거리만) = 그 측점 횡단 pick(80.17, null, "b05"); out.pipePick = { ...shownNow(), structure: preview.watched()?.structure ?? null }; // ④ 평면 마커 고름 풂(b05-plan) = 판 그대로 pick(null, null, "b05-plan"); out.planRelease = shownNow(); // ⑤ 일반측점 고름 = 그 측점 횡단 · 법선(structure 없음) pick(20, null, "b05"); out.gridPick = { ...shownNow(), structure: preview.watched()?.structure ?? null }; // ⑥ 묶음 전 구조물 고름 — 열쇠 없이 기다렸다 묶음 뒤 열쇠로 붙듦 · 그 뒤 옮김도 따라감 stations = []; pick(60, "B", "b05"); out.beforePipeline = shownNow(); stations = [...grid, station("structure:B", 60, "structure", "B")]; crossStage(); out.afterPipeline = shownNow(); stations = [...grid, station("structure:B", 66, "structure", "B")]; crossStage(); out.latchedMoved = shownNow(); // ⑦ 다른 보기가 풂(null · b05) = 판 비움(382-2 그대로) pick(null, null, "b05"); out.otherRelease = { ...shownNow(), watched: preview.watched() }; console.log(JSON.stringify(out));