/* PLAN 458 노선 바뀜 — 진짜 모듈(따라감 · 측점 · 갈아끼움 · 형상 얹기)을 그 자리에서 트랜스파일해 Node 로 돌림. * 저장 노선 (0,0)→(100,0)→(200,60) 위 관 · 세월교 · 옹벽 → 시점 당김 / 밀어냄 → 갈아끼움 + 지금 현황으로 다시 얹기. * 결과 = 표준출력 JSON. 짝 = `test_458_swap_attach.py`. */ const fs = require("fs"); const path = require("path"); const ROOT = path.join(__dirname, "..", ".."); const ts = require(path.join(ROOT, "config", "node_modules", "typescript")); const cache = new Map(); function resolveTs(base) { for (const c of [`${base}.ts`, base, path.join(base, "index.ts")]) if (fs.existsSync(c) && fs.statSync(c).isFile()) return c; throw new Error(`모름: ${base}`); } function load(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 need = (spec0) => { if (/common_util_structure_catalog(\.js)?$/.test(spec0)) return require(path.join(__dirname, "helper_322_catalog_module.cjs")); const spec = spec0.replace(/\.js$/, ""); if (spec.endsWith(".json")) return JSON.parse(fs.readFileSync(path.join(path.dirname(full), spec), "utf8")); if (spec.startsWith("@util/")) return load(resolveTs(path.join(ROOT, "common_util", spec.slice(6)))); if (spec.startsWith(".")) return load(resolveTs(path.join(path.dirname(full), spec))); return require(spec); }; new Function("exports", "module", "require", js)(box.exports, box, need); return box.exports; } const P = (...parts) => path.join(ROOT, ...parts); const follow = load(P("common_util", "common_util_route_follow.ts")); const SS = load(P("B05_Profile", "logic", "B05_Profile_Sections_Stations.ts")); const SW = load(P("B06_Section", "logic", "B06_Section_Station_Swap.ts")); const AT = load(P("common_util", "common_util_structure_attach.ts")); const def = JSON.parse( fs.readFileSync(P("resources", "master_template", "구조물정의.json"), "utf8"), ); const types = def.types .filter((t) => t.enabled !== false) .map((t) => ({ type_id: t.type_id, group: t.group, placement: t.placement, managed_by: t.managed_by, name: t.name, options: (t.options || []).map((o) => ({ key: o.key, label: o.label, default: o.default })), })); const INTERVAL = 20; const xy = (L) => L.map(([x, y]) => ({ x, y })); function at(L, ch) { let walked = 0; for (let i = 1; i < L.length; i++) { const [a, b] = [L[i - 1], L[i]]; const span = Math.hypot(b[0] - a[0], b[1] - a[1]); if (walked + span >= ch) { const t = (ch - walked) / span; return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t]; } walked += span; } return L[L.length - 1]; } const kindOf = (s) => s.kind === "end" ? s.key.endsWith("EP") ? "ep" : "bp" : s.kind === "grid" ? "regular" : s.kind === "curve" ? "curve" : "irregular"; /** 저장 상세 — 서버처럼 격자 자리 시설 측점에 `grid` · 설계에 열쇠 메모. */ const sectionsFrom = (stations) => stations.map((s) => ({ station_id: SW.stationIdOf(s.chainageM), chainage_m: s.chainageM, label: "", kind: kindOf(s), ...(kindOf(s) === "irregular" && s.chainageM % INTERVAL === 0 ? { grid: true } : {}), center_x: s.x, center_y: s.y, center_z: 100, frame: { left_xy: s.left }, samples: [{ offset_m: 0, elevation_m: 100, valid: true }], uphill_side: "left", design: { memo: s.key, design_elevation_m: 101 }, })); const ground = (s) => ({ ...s, ground: { centerZ: 100, left: [{ offsetM: 5, z: 101 }], right: [{ offsetM: 5, z: 99 }] }, }); const KEYS = ["culvert", "ford", "box", "ford_pavement"]; const view = (sections) => sections.map((s) => ({ chainage_m: s.chainage_m, kind: s.kind, grid: !!s.grid, shapes: Object.fromEntries(KEYS.filter((k) => s[k]).map((k) => [k, s[k].chainage_m])), })); function run(L1, L2, pipes, structures) { const S1 = SS.buildStations(L1, [], pipes, structures, { types, intervalM: INTERVAL }); const saved = { longitudinal: { stations: [], length_m: 0 }, cross_sections: sectionsFrom(S1) }; AT.attachShapes(saved.cross_sections, pipes, structures, types); saved.cross_sections[0].preview = true; // 초안 맞춤의 구조물 없는 사본 표시(갈아끼움이 안 들고 감) const pipesF = follow.projectPipePoints(pipes, xy(L2)); const structuresF = follow .followStructures(structures, xy(L2), undefined, INTERVAL) .map((m) => m.item); const S2 = SS.buildStations(L2, [], pipesF, structuresF, { types, intervalM: INTERVAL }).map( ground, ); const swap = SW.swapCrossSections( saved, S2, S1.map((s) => s.key), INTERVAL, ); const now = swap.detail.cross_sections.map((s) => ({ ...s })); const misses = AT.attachShapes(now, pipesF, structuresF, types); // 가게 초안이 없는 채(저장분 현황) 얹기 — 상한이 옛 자리 관을 이웃 측점에 안 붙임 const stale = swap.detail.cross_sections.map((s) => ({ ...s })); const staleMisses = AT.attachShapes(stale, pipes, structures, types); return { ids: swap.detail.cross_sections.map((s) => [s.chainage_m, s.station_id]), preview: swap.detail.cross_sections.filter((s) => s.preview).length, pipes: pipesF.map((p) => [p.id, p.chainage_m]), now: view(now), misses, staleMisses, stale: view(stale), }; } const L1 = [ [0, 0], [100, 0], [200, 60], ]; const pipe = (id, facility, ch, options) => { const [x, y] = at(L1, ch); return { id, facility, chainage_m: ch, x, y, options }; }; const pipes = [ pipe("p1", "pipe", 40, { pipe_diameter_mm: "800" }), // 격자 No.2 위 pipe("p2", "ford_bridge", 150, { ford_width_m: 10 }), ]; const [wx, wy] = at(L1, 70); const wall = { structure_id: "w1", type_id: "retaining_wall", chainage_m: 70, start_m: 60, end_m: 80, x: wx, y: wy, options: { height_m: 2, side: "left" }, }; const out = { // A — 시점 7 m 당김: 관 40 → 47(격자를 떠남) · 세월교 150 → 157 shift: run( L1, [ [-7, 0], [100, 0], [200, 60], ], pipes, [wall], ), // D — 시점을 50 m 로 밀어냄: 관 40 = 새 시점 10 m 바깥 → 누가거리 0 outside: run( L1, [ [50, 0], [100, 0], [200, 60], ], pipes, [wall], ), }; process.stdout.write(JSON.stringify(out));