/* 계획노선 편집 창 **측점 법선 · 고름 연동**(계획서 22-1 · 382-2) 단위검증 헬퍼 — * `B05_Profile_UI_RouteEdit_Cross.ts` 를 트랜스파일해 Node 로 돌린다. * 계산 묶음 · 가게 · 그림 조각은 가짜로 채운다 — 공용 고름(`design_selection`)은 진짜. * 묶음 측점 = 일반측점 grid:2(40) · grid:3(60) · grid:5(100) · 관 pipe:a(80) · 원지반 표본 좌우 20 m. */ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); const ROOT = path.join(__dirname, "..", ".."); function memoryStorage() { const map = new Map(); return { getItem: (key) => (map.has(key) ? map.get(key) : null), setItem: (key, value) => map.set(key, String(value)), removeItem: (key) => map.delete(key), key: (index) => [...map.keys()][index] ?? null, get length() { return map.size; }, }; } const session = memoryStorage(); Object.defineProperty(session, "length", { get: () => session._keys().length }); session._keys = () => { const keys = []; for (let i = 0; i < 1000; i += 1) { const key = session.key(i); if (key === null) break; keys.push(key); } return keys; }; // `Object.keys(sessionStorage)` 가 저장된 키를 돌려주도록 실제 브라우저처럼 속성으로도 둔다. const sessionProxy = new Proxy(session, { ownKeys: () => [...new Set(session._keys())], getOwnPropertyDescriptor: (target, key) => session._keys().includes(key) ? { enumerable: true, configurable: true, value: target.getItem(key) } : Reflect.getOwnPropertyDescriptor(target, key), }); globalThis.window = { localStorage: memoryStorage(), sessionStorage: sessionProxy }; const fakeEl = () => ({ className: "", textContent: "", innerHTML: "", querySelector: () => fakeEl(), replaceChildren() {}, setAttribute() {}, append() {}, getBoundingClientRect: () => ({ width: 300, height: 200 }), }); globalThis.document = { createElement: fakeEl }; // node 에는 ResizeObserver 가 없음 — 횡단 판 크기 따라가기는 이 시험 밖 globalThis.ResizeObserver = class { observe() {} disconnect() {} }; const grid = (k, chainageM) => ({ key: `grid:${k}`, kind: "grid", chainageM }); let stations = [ grid(2, 40), grid(3, 60), { key: "pipe:a", kind: "pipe", chainageM: 80 }, grid(5, 100), ]; let footprint = null; let route = { v: 1 }; // 접힌 판 펴기 — 몸(side)에 묶인 패널 묶음 흉내 // 393-1 — 측점 눈금이 연 판은 기억 안 함(remember 거짓) const panels = { now: null, current: () => panels.now, open: (id, remember = true) => ((panels.now = id), (panels.remember = remember)), }; // 393-2 — 판 그림 = B06 카드(가짜) · 그린 단면 id 를 적음 const cards = []; const side = { append() {} }; const STUBS = { "../A00_Common/b_svg_zoom_pan": { attachSvgZoomPan: () => {} }, "../common_util/common_util_station": { stationLabel: (m) => String(m) }, "./B05_Profile_UI_RouteEdit_Cross_Draw": { buildCrossSvg: () => ({ svg: {}, layer: {}, width: 1, height: 1, content: {} }), CROSS_PLOT_PAD: 0, summarizeCross: () => "", }, "./B05_Profile_UI_RouteEdit_Chrome": { sidePanelsOf: new WeakMap([[side, panels]]) }, "./B05_Profile_UI_RouteEdit_Cross_Card": { routeCrossSource: (projectId, pipe, chainageM, pane) => ({ section: { station_id: `b05-${pane}-s${chainageM}` }, }), routeCrossCard: (source) => (cards.push(source.section.station_id), fakeEl()), }, "../A00_Common/design/design_pipeline_page": { pagePipeline: () => ({ output: (name) => (name === "ground" ? { stations } : null), input: () => route, subscribe: () => () => undefined, }), }, "../A00_Common/design/design_store": { useDesignStore: () => ({ subscribe: () => () => undefined, get: (name) => (name === "footprint" ? footprint : null), }), }, "./logic/B05_Profile_Pipeline_Detail": { stationSection: (station) => ({ chainage_m: station.chainageM, label: null, samples: [-20, 0, 20].map((offset_m) => ({ offset_m, elevation_m: 100, valid: true })), }), }, }; const cache = new Map(); function loadTs(file) { const full = path.resolve(file); 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 { createCrossPreview } = loadTs( path.join(ROOT, "B05_Profile", "B05_Profile_UI_RouteEdit_Cross.ts"), ); const pick = loadTs(path.join(ROOT, "A00_Common", "design", "design_selection.ts")); // 페이지 노선 id 한 칸(386-2b) — 판이 읽는 적용값 = 이 노선 것만 const pageRoute = loadTs(path.join(ROOT, "A00_Common", "design", "design_page_route.ts")); const record = (key, chainage_m, left, right, leftCut, rightCut) => ({ key, chainage_m, left_edge_m: 2.5, left_end_m: left, right_edge_m: -2.5, right_end_m: right, left_cut: leftCut, right_cut: rightCut, }); async function run() { let shownCount = 0; const cross = createCrossPreview({ projectId: "p-1", side, request: () => ({ vertices: [], min_radius_m: 12, station_interval_m: 20 }), onShown: () => (shownCount += 1), }); const out = {}; out.before = cross.watched(); await cross.open(100); out.empty = { watched: cross.watched() }; pageRoute.setPageRouteId("p-1", 3); session.setItem("aislo:draft:cross-display:p-1:3", "15"); await cross.refresh(); out.applied15 = { watched: cross.watched() }; session.setItem("aislo:draft:cross-display:p-1:12", "8.5"); session.setItem("aislo:draft:cross-display:p-2:99", "40"); await cross.refresh(); out.otherRoute = { watched: cross.watched() }; // 다른 노선 · 다른 프로젝트 값은 안 봄 pageRoute.setPageRouteId("p-1", 12); await cross.refresh(); out.latestRoute = { watched: cross.watched() }; session.setItem("aislo:draft:cross-display:p-1:12", "abc"); await cross.refresh(); out.badValue = { watched: cross.watched() }; // 382-2 발자국 기록 = 비탈 끝 좌 · 우 · 깎기 여부 footprint = { records: [record("grid:5", 100, 7.5, -6, true, false)] }; out.footprint = cross.watched(); // 노선 고침 — 같은 열쇠 측점이 102 로 밀리고 발자국도 새로(열쇠로 다시 짚음) stations = [ grid(2, 40), grid(3, 60), { key: "pipe:a", kind: "pipe", chainageM: 80 }, grid(5, 102), ]; footprint = { records: [record("grid:5", 102, 9, -4, false, true)] }; route = { v: 2 }; cards.length = 0; await cross.refresh(); out.afterEdit = cross.watched(); out.cardsAfterEdit = cards.slice(); // 평면 눈금 클릭 = 공용 고름에 적음 · 판이 접혀 있으면 폄 const heard = []; pick.subscribeSelection("p-1", (selection, from) => heard.push({ at: selection.chainageM, from }), ); panels.now = null; await cross.open(41); out.planClick = { selection: pick.readSelection("p-1"), heard: heard.slice(), panel: panels.now, remember: panels.remember, watched: cross.watched(), }; // 다른 보기 고름 — 일반측점 = 그 측점(판 폄) · 관 측점 · 구조물 id = 마름모 · 풂 = 없음 panels.now = null; pick.selectDesign("p-1", { chainageM: 60, structureId: null }, { from: "b06-card" }); out.otherGrid = { watched: cross.watched(), panel: panels.now }; pick.selectDesign("p-1", { chainageM: 80, structureId: null }, { from: "profile" }); out.otherPipe = cross.watched(); pick.selectDesign("p-1", { chainageM: 60, structureId: "s-1" }, { from: "list" }); out.otherStructure = cross.watched(); const before = shownCount; pick.selectDesign("p-1", { chainageM: null, structureId: null }, { from: "escape" }); out.cleared = { watched: cross.watched(), redrawn: shownCount > before }; // 393-4 — 보기 반폭 한 벌(B06 입력칸 · B05 판) = 세션 조절값 > 측점 저장 최댓값 > 기본 10 const { displayHalfWidth } = loadTs( path.join(ROOT, "B06_Section", "B06_Section_Display_Width.ts"), ); const saved = [12, 14, null].map((w) => ({ design: { display_half_width_m: w } })); out.halfWidth = { none: displayHalfWidth("p-9", 5, []), saved: displayHalfWidth("p-9", 5, saved), }; session.setItem("aislo:draft:cross-display:p-9:5", "18"); out.halfWidth.session = displayHalfWidth("p-9", 5, saved); out.halfWidth.otherRoute = displayHalfWidth("p-9", 6, []); out.halfWidth.noRoute = displayHalfWidth("p-9", null, saved); return out; } run().then((out) => console.log(JSON.stringify(out)));