/* 346-2 4파 헬퍼 — 측점 셋 늘고 둘 빠진 목록으로 상세 갈아끼움 · 카드 수 · 열쇠 이어 받기 · 보이는 카드 먼저. * node helper_346_2_station_swap.mjs <묶음.mjs> * vite ssr 로 묶은 진짜 모듈(helper_346_2_swap_entry.ts) · 표준출력 JSON 한 벌. */ import { pathToFileURL } from "node:url"; const store = () => { const m = new Map(); return { getItem: (k) => (m.has(k) ? m.get(k) : null), setItem: (k, v) => m.set(k, String(v)), removeItem: (k) => m.delete(k), key: (i) => [...m.keys()][i] ?? null, get length() { return m.size; }, clear: () => m.clear(), }; }; globalThis.window = globalThis; globalThis.sessionStorage = store(); globalThis.localStorage = store(); const any = new Proxy(function () {}, { get: (_t, k) => (k === Symbol.toPrimitive ? () => 0 : any), apply: () => any, construct: () => any, }); globalThis.document = any; globalThis.requestAnimationFrame = (run) => setTimeout(run, 0); const toasts = []; globalThis.__toasts = toasts; const m = await import(pathToFileURL(process.argv[2]).href); const P = "p346"; const R = 9; const out = {}; /* 옛 목록 7 — 시점 · 격자 넷 · 관 하나 · 종점(간격 20 m) */ const section = (chainage_m, kind, extra = {}) => ({ station_id: `station_${String(Math.round(chainage_m * 1000)).padStart(12, "0")}`, chainage_m, label: "", kind, center_z: 100, azimuth_deg: 90, center_x: chainage_m, center_y: 0, frame: { left_xy: [0, 1] }, samples: [{ offset_m: 0, elevation_m: 100, valid: true }], design: { design_elevation_m: 101, mark: `old-${chainage_m}` }, ...extra, }); const oldDetail = () => ({ longitudinal: { length_m: 100, stations: [], profile_alignment: { policy: { station_interval_m: 20 } }, }, cross_sections: [ section(0, "bp"), section(20, "regular"), section(40, "regular"), section(45.054, "irregular", { structure: "파형강관 D800", culvert: {} }), section(60, "regular"), section(80, "regular"), section(100, "ep"), ], }); const OLD_KEYS = ["end:BP", "grid:1", "grid:2", "pipe:pipe-s1", "grid:3", "grid:4", "end:EP"]; /* 새 목록 8 — 셋 늘고(st-1 · st-2 · st-3) 둘 빠짐(grid:2 · grid:4) · 관 47 m 로 · 종점 90 m 로 */ const station = (key, chainageM, kind, id = null, dy = 0) => ({ key, chainageM, x: chainageM, y: dy, azimuthDeg: 90, left: [0, 1], kind, id, ground: { centerZ: 99, left: [{ offsetM: 0.5, z: 99.1 }], right: [{ offsetM: 0.5, z: 98.9 }], }, planRadiusM: null, curveOuterSide: null, curveWideningM: null, }); const NEW = (dy) => [ station("end:BP", 0, "end", null, dy), station("grid:1", 20, "grid", null, dy), station("pipe:pipe-s1", 47, "pipe", "pipe-s1", dy), station("structure:st-1", 52, "structure", "st-1", dy), station("grid:3", 60, "grid", null, dy), station("structure:st-2", 65, "structure", "st-2", dy), station("structure:st-3", 75, "structure", "st-3", dy), station("end:EP", 90, "end", null, dy), ]; /* 1. 순수 갈아끼움 */ { const before = oldDetail(); const swap = m.swapCrossSections(before, NEW(0), OLD_KEYS, 20); out.pure = { count: swap.detail.cross_sections.length, longCount: swap.detail.longitudinal.stations.length, added: swap.added, removed: swap.removed, moves: Object.fromEntries(swap.moves), moved: swap.moved, keptMark: swap.detail.cross_sections.map((s) => s.design?.mark ?? null), kinds: swap.detail.cross_sections.map((s) => s.kind), labels: [swap.detail.cross_sections[2].label, swap.detail.cross_sections[7].label], samples: swap.detail.cross_sections[1].samples.map((s) => s.offset_m), movedSamples: swap.detail.cross_sections[2].samples.map((s) => s.offset_m), oldUntouched: before.cross_sections.length === 7 && before.cross_sections[3].chainage_m === 45.054, length: swap.detail.longitudinal.length_m, same: m.sameStationList(before.cross_sections, OLD_KEYS, NEW(0)), sameSelf: m.sameStationList( swap.detail.cross_sections, NEW(0).map((s) => s.key), NEW(0), ), }; out.format = [0, 45.054, 1234.5].map((c) => m.formatStation(c)); } /* 2. 가게 → 상세 갈아끼움 · 열쇠 이어 받기 · 노선 밖 고침 */ { m.writeState("section-detail", oldDetail(), P, R); m.writeState("pipe-ids", { next: 2, ids: [{ id: "pipe-s1", at: 45.054 }] }, P); const first = await m.loadSectionDetail(P, R); // 세션 한 벌 → 캐시 · 측점 열쇠 세움 const choice = (ground) => ({ ground_type: ground, section_mode: "right_cut", ditch_side: "right", }); m.writeCrossDesignChoice(P, R, 20, choice("soil")); m.writeCrossDesignChoice(P, R, 40, choice("soil")); m.writeCrossDesignChoice(P, R, 45.054, choice("blasting_rock")); // 조정창 칸 = 세션 칸이 주인(472-5 5-D) — 갈아끼움 이동은 세션 칸을 바로 옮김(`Station_Keys`) m.writeState("crossw", { 45.05: 12, "45.05:outlet": 3, "20.00": 8 }, P, R); const heard = []; // 관 · 구조물 자리 장부 — 새 목록에서 관은 47 m · 구조물 id 는 측점 계획이 줌 m.writeState("pipe-ids", { next: 2, ids: [{ id: "pipe-s1", at: 47 }] }, P); m.useStructureIds(P, [ { structure_id: "st-1", chainage_m: 52 }, { structure_id: "st-2", chainage_m: 65 }, { structure_id: "st-3", chainage_m: 75 }, ]); const stopListen = m.subscribeSectionDetail(P, R, (detail, moves) => heard.push({ count: detail.cross_sections.length, moves: Object.fromEntries(moves) }), ); let redraws = 0; const stop = m.followSwaps( P, () => R, { visible: () => true }, async () => { redraws += 1; }, ); m.useDesignStore(P, null).set("stations", NEW(0.5)); // 좌표가 움직임 = 노선 모양 바뀜 await new Promise((resolve) => setTimeout(resolve, 30)); const now = await m.loadSectionDetail(P, R); out.store = { replaced: now !== first, count: now.cross_sections.length, heard, choices: [...m.crossDesignChoices(P, R).entries()] .sort((a, b) => a[0] - b[0]) .map(([at, c]) => [at, c.ground_type, c.section_mode ?? null]), rawKeys: Object.keys(m.readState("crossdesign", P, R) ?? {}) .filter((k) => !k.startsWith("@")) .sort(), widths: m.readState("crossw", P, R), redraws, }; // 같은 목록을 다시 내면 갈아끼우지 않음 const again = await m.swapSectionStations(P, R, NEW(0.5)); out.store.again = again === null; stop(); stopListen(); } /* 2-2. 종단 보기(B05) · 3D 측점선 — 횡단 보기 없이도 가게 측점으로 갈아끼우고 보일 때만 다시 그림 */ { const R2 = 11; m.writeState("section-detail", oldDetail(), P, R2); const renders = []; const lines = []; let shown = true; const sections = m.createProfileSections({ projectId: P, state: { confirmedSurface: null, latest: { route_points: [] }, roadWidths: { g: 5 }, detail: null, corridorVisible: false, corridorPending: false, routeReady: true, restoring: false, }, viewer: { renderStationLines: (list) => lines.push(list.length), markers: {} }, panel: () => ({ values: () => ({ gradeClass: "g", stationInterval: 20, enforcePipeClearance: false }), gradeLimitPct: () => 12, stationDisplayOffset: () => ({ station: 0, cumulative: 0 }), setCorridorPending: () => undefined, }), profilePanel: () => ({ render: (detail) => renders.push(detail.cross_sections.length), setGradeLimit: () => undefined, setEnforceMinCover: () => undefined, setStationDisplay: () => undefined, setIrregularStations: () => undefined, alignmentSamples: () => null, setLoading: () => undefined, clear: () => undefined, }), bridge: () => ({ irregularStations: () => [], stationsReady: () => true }), // 시설 줄 입력 다 옴(473) visible: () => shown, }); await sections.restoreSections(R2); const shop = m.useDesignStore(P, null); shop.set("stations", NEW(0)); // 보임 — 바로 다시 await new Promise((resolve) => setTimeout(resolve, 30)); const afterShown = [...renders]; shown = false; shop.set("stations", NEW(0).slice(0, 6)); // 숨음 — 안 그림 await new Promise((resolve) => setTimeout(resolve, 30)); const whileHidden = [...renders]; shown = true; sections.flushSwap(); // 다시 보임 out.profile = { afterShown, whileHidden, afterShow: [...renders], lines: lines.length }; sections.stopSwap(); } /* 3. 보이는 카드 먼저 — 바로 그릴 수 */ out.eager = [ m.eagerCardCount({ clientHeight: 600, scrollTop: 0 }, 3, 250, 12), m.eagerCardCount({ clientHeight: 600, scrollTop: 1310 }, 3, 250, 12), m.eagerCardCount({ clientHeight: 0, scrollTop: 0 }, 2, 250, 12), ]; out.rename = Object.fromEntries( m.renameChainageKeys( new Map([ ["45.05", 1], ["b45.05", 2], ["45.05:outlet", 3], ["60.00", 4], ]), new Map([["45.05", "47.00"]]), ), ); process.stdout.write(JSON.stringify(out));