/* 457 헬퍼 — 노선 바뀜 뒤 초안 카드(묶음 새 상세) 그림 · 이름 · 시설 맥락 = 지금 현황을 새 측점에 다시 얹은 것. * 진짜 TS(`design_pipeline_cross` · `B05_Profile_Pipeline_Detail` · 측점 · 따라감 · 형상 얹기) · 세션 창구만 가짜. * 재현 E · F(시점 당김) · G(세션에서 놓은 BOX) · H(관 둘 엇갈려 옮김) — 결과 = 표준출력 JSON. 짝 = `test_457_draft_card.py`. */ const fs = require("fs"); const path = require("path"); const ROOT = path.resolve(__dirname, "..", ".."); const ts = require(path.join(ROOT, "config", "node_modules", "typescript")); const CATALOG = require("./helper_322_catalog_module.cjs"); const STUBS = { b_page_state: { readState: () => null }, B06_Section_Section_Canon: { savedDesigns: () => null }, B06_Section_UI_Standard_Panel: { effectiveStandardCross: () => ({ road_width_m: 3 }), readRockBoundaryDefault: () => null, }, B05_Profile_UI_Profile_Data: { toDesignProfile: () => ({}) }, }; const cache = new Map(); function resolveTs(base) { for (const c of [`${base}.ts`, base]) if (fs.existsSync(c) && fs.statSync(c).isFile()) return c; throw new Error(`없음: ${base}`); } function load(full) { if (cache.has(full)) return cache.get(full).exports; 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); const req = (r0) => { if (CATALOG.isCatalogRequest(r0)) return CATALOG; const r = r0.replace(/\.js$/, ""); const stub = STUBS[r.split("/").pop()]; if (stub) return stub; if (r.endsWith(".json")) return JSON.parse(fs.readFileSync(path.join(path.dirname(full), r), "utf8")); if (r.startsWith("@util/")) return load(resolveTs(path.join(ROOT, "common_util", r.slice(6)))); if (r.startsWith(".")) return load(resolveTs(path.join(path.dirname(full), r))); return require(r); }; new Function("exports", "module", "require", js)(box.exports, box, req); return box.exports; } const P = (...a) => path.join(ROOT, ...a); const follow = load(P("common_util", "common_util_route_follow.ts")); const SS = load(P("B05_Profile", "logic", "B05_Profile_Sections_Stations.ts")); const AT = load(P("common_util", "common_util_structure_attach.ts")); const ST = load(P("common_util", "common_util_structure_station.ts")); const CS = load(P("common_util", "common_util_culvert_sets.ts")); const PD = load(P("B05_Profile", "logic", "B05_Profile_Pipeline_Detail.ts")); const CR = load(P("A00_Common", "design", "design_pipeline_cross.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 SHAPES = AT.SHAPE_KEYS; const idOf = (m) => `station_${String(Math.round(m * 1000)).padStart(12, "0")}`; const kindOf = (s) => s.kind === "end" ? s.key.endsWith("EP") ? "ep" : "bp" : s.kind === "grid" ? "regular" : s.kind === "curve" ? "curve" : "irregular"; const ground = (s) => ({ ...s, ground: { centerZ: 100, left: [{ offsetM: 5, z: 101 }], right: [{ offsetM: 5, z: 99 }] }, }); const sectionsFrom = (S) => S.map((s) => ({ station_id: idOf(s.chainageM), chainage_m: s.chainageM, label: "", kind: kindOf(s), center_x: s.x, center_y: s.y, samples: [], design: { design_elevation_m: 101 }, })); const xy = (L) => L.map(([x, y]) => ({ x, y })); function at(L, ch) { let walked = 0; for (let i = 1; i < L.length; i += 1) { 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 pipe = (id, facility, ch, L, options) => { const [x, y] = at(L, ch); return { id, facility, chainage_m: ch, x, y, options }; }; const wall = (L) => { const [x, y] = at(L, 70); return { structure_id: "w1", type_id: "retaining_wall", chainage_m: 70, start_m: 60, end_m: 80, x, y, options: { height_m: 2, side: "left" }, }; }; /** 측점 그림 지문 — 얹힌 형상 열쇠 · 그 누가거리(구간이면 시작~끝). */ const shapesOf = (s) => SHAPES.filter((k) => s[k] !== undefined) .map((k) => { const v = s[k]; const c = Array.isArray(v) ? v.map((x) => `${x.start_m ?? x.chainage_m ?? ""}~${x.end_m ?? ""}`).join("/") : v && (v.chainage_m ?? `${v.start_m ?? ""}~${v.end_m ?? ""}`); return `${k}@${c}`; }) .join(" "); /** 정답 = 새 측점(구조물 없는 꼴)에 지금 현황(따라간 관 · 구조물)을 얹은 그림 · 측점 계획 이름(캐시 `withDraftShapes`). */ function truthOf(S2, pipes, structures) { const bare = S2.map((s) => ({ chainage_m: s.chainageM })); AT.attachShapes(bare, pipes, structures, types); const planned = ST.planStations(pipes, structures, types, CS.registryDefaults(types)); return bare.map((s) => ({ shapes: shapesOf(s), name: planned.find((p) => Math.abs(p.chainage_m - s.chainage_m) < 0.005)?.title ?? null, })); } function scenario(L1, L2, saved, now, opts = {}) { const S1 = SS.buildStations(L1, [], saved.pipes, saved.structures, { types, intervalM: 20 }); // 저장 상세(서버) = 저장 측점에 저장 현황 그림 · 초안 둔 채 열기면 화면 상세가 초안 자리(`canon`) const Scanon = opts.canon ?? S1; const detail = { longitudinal: { stations: [], design_profiles: [], length_m: 0, samples: [] }, cross_sections: sectionsFrom(Scanon), }; AT.attachShapes( detail.cross_sections, opts.canonPipes ?? saved.pipes, opts.canonStructures ?? saved.structures, types, ); const pF = follow.projectPipePoints(now.pipes, xy(L2)); const sF = follow.followStructures(now.structures, xy(L2), undefined, 20).map((m) => m.item); const S2 = SS.buildStations(L2, [], pF, sF, { types, intervalM: 20 }).map(ground); const keysOf = opts.keys ? () => opts.keys : undefined; const source = CR.pipelineCrossSource("p457", detail, S1, 7, keysOf); // 노선 초안 동안 등록 목록이 새 노선 것으로 바뀜 — 처음 본 열쇠 그대로여야 함 const again = opts.keys ? CR.pipelineCrossSource("p457", detail, S1, 7, () => [...opts.keys].reverse()) : source; const status = PD.shapeStatus ? PD.shapeStatus(pF, sF, types) : { pipes: pF, structures: sF, types }; const card = PD.draftSectionDetail( detail, { stations: S2, longitudinal: { chainageM: [0], z: [0] } }, {}, null, again.specs, 20, status, ); const truth = truthOf(S2, pF, sF); const shaped = PD.crossWithShapes ? PD.crossWithShapes(source.input, S2, status) : source.input; const ctx = (key) => { const e = shaped.byKey[key] ?? {}; return [Boolean(e.paved), Boolean(e.tilted), e.tiltPct ?? null, e.surfaceDropM ?? 0]; }; return { rows: S2.map((s, i) => ({ key: s.key, m: s.chainageM, card: shapesOf(card.cross_sections[i]), truth: truth[i].shapes, name: card.cross_sections[i].structure ?? null, truthName: truth[i].name, id: card.cross_sections[i].station_id, ctx: ctx(s.key), })), canonIds: Object.fromEntries( Scanon.map((s, i) => [s.key, detail.cross_sections[i].station_id]), ), sourceKeys: Object.fromEntries([...source.specs].map(([k, v]) => [k, v.station_id])), specFields: [...new Set([...source.specs.values()].flatMap((v) => Object.keys(v)))], }; } const L1 = [ [0, 0], [100, 0], [200, 60], ]; const P1 = [ pipe("p1", "pipe", 47, L1, { pipe_diameter_mm: "800" }), pipe("fp", "ford_pavement", 125, L1, { ford_width_m: 12 }), ]; const facilities = { pipes: P1, structures: [wall(L1)] }; const out = {}; out.E = scenario( L1, [ [-13, 0], [100, 0], [200, 60], ], facilities, facilities, ); out.F = scenario( L1, [ [-7, 0], [100, 0], [200, 60], ], facilities, facilities, ); out.G = scenario( L1, [ [-7, 0], [100, 0], [200, 60], ], facilities, { pipes: [...P1, pipe("p9", "box_culvert", 90, L1, { body_width_m: 2 })], structures: [wall(L1)], }, ); // H — 세션에서 관 둘을 엇갈려 옮김(저장 전) · 초안 둔 채 열기(화면 상세 = 옮긴 자리) → 시점 +7 { const saved = [ pipe("p1", "pipe", 47, L1, { pipe_diameter_mm: "800" }), pipe("p2", "box_culvert", 90, L1, { body_width_m: 2 }), ]; const moved = [ pipe("p1", "pipe", 110, L1, { pipe_diameter_mm: "800" }), pipe("p2", "box_culvert", 30, L1, { body_width_m: 2 }), ]; const Snow = SS.buildStations(L1, [], moved, [], { types, intervalM: 20 }); out.H = scenario( L1, [ [-7, 0], [100, 0], [200, 60], ], { pipes: saved, structures: [] }, { pipes: moved, structures: [] }, { canon: Snow, canonPipes: moved, keys: Snow.map((s) => s.key) }, ); } process.stdout.write(JSON.stringify(out));