/* 344-3 · 346-2 2파 헬퍼 — vite ssr 로 묶은 진짜 모듈(helper_344_3_entry.ts)을 부르고 결과를 JSON 한 벌로. * node helper_344_3_station_keys.mjs <묶음.mjs> <입력.json> * 입력 = { standard: 파이썬 config 표준단면, policy: 종단 정책 }. */ import { readFileSync } from "node:fs"; 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 [bundlePath, inputPath] = process.argv.slice(2); const m = await import(pathToFileURL(bundlePath).href); const input = JSON.parse(readFileSync(inputPath, "utf8")); const P = "p344"; const R = 7; const R2 = 8; const raw = (name, route) => m.readState(name, P, route) ?? {}; const keysOf = (name, route) => Object.keys(raw(name, route)) .filter((k) => k !== "@at") .sort(); const section = (chainage_m, kind, extra = {}) => ({ station_id: `s${chainage_m}`, chainage_m, kind, label: String(chainage_m), samples: [], ...extra, }); const detailOf = (sections) => ({ longitudinal: { stations: sections, profile_alignment: { policy: { station_interval_m: 20 } }, }, cross_sections: sections, }); const choice = (ground) => ({ ground_type: ground, section_mode: "right_cut", ditch_side: "right", ditch_type: "l_type", paved: true, two_stage_slope: true, ditch_choice: false, }); const out = {}; /* 1. 옛 초안(누가거리 열쇠) → 측점 열쇠로 옮김 · 지금 측점에 없는 것은 남겨 둠 */ const oldRoute = detailOf([ section(0, "bp"), section(20, "regular"), section(40, "regular"), section(45.054, "irregular", { structure: "파형강관 D800", culvert: {} }), section(52.5, "irregular", { structure: "대피로" }), section(60, "regular"), section(70, "ep"), ]); m.useStructureIds(P, [{ structure_id: "st-1", chainage_m: 52.5 }]); m.useStationKeys(P, R, oldRoute); m.writeState( "crossdesign", { "20.00": choice("soil"), 45.05: choice("blasting_rock"), "52.50": choice("soil"), "60.00": choice("soil"), "99.00": choice("soil"), }, P, R, ); out.legacyRead = [...m.crossDesignChoices(P, R).keys()].sort((a, b) => a - b); out.legacyKeys = keysOf("crossdesign", R); /* 2. 쓰기 = 측점 열쇠 · 누가거리로 다시 읽음 · 암 경계 저장소도 같은 열쇠 */ m.writeCrossDesignChoice(P, R, 40, choice("ripping_rock")); m.writeShoulderExtend(P, R, 40, "left", 0.5); out.writeKey = keysOf("crossdesign", R).includes("grid:2"); out.shoulderKeys = keysOf("shoulderext", R); const rock = m.createRockBoundaryStore({ sessionKey: () => m.stateKey("rockb", P, R), detail: () => oldRoute, refreshCard: () => undefined, recompute: () => undefined, }); rock.control.adjust(20, -0.3); out.rockKeys = keysOf("rockb", R); rock.load(); out.rockLoaded = Object.fromEntries(rock.offsets); out.rockSession = m.readRockBoundarySession(P, R); /* 3. 구조물 id 를 아직 모를 때(첫 열기) — 쓴 때 자리(@at)로 그 측점 고침을 읽음 */ m.useStructureIds(P, []); m.useStationKeys(P, R, oldRoute); out.hintKeyNow = m.keyAt(P, R, 52.5); out.hintRead = m.crossDesignChoices(P, R).has(52.5); /* 4. 노선이 바뀜 — 측점 열쇠로 넘김 · 넘김 = 카드 선택 넷 · 암 경계 · 노견 확장 안 넘김 · 사라진 측점 = 노선 밖 */ m.writeState("pipe-ids", { next: 2, ids: [{ id: "pipe-s1", at: 47 }] }, P); // 관이 47 m 로 옮김 m.useStructureIds(P, [{ structure_id: "st-1", chainage_m: 55 }]); m.useStationKeys( P, R2, detailOf([ section(0, "bp"), section(20, "regular"), section(40, "regular"), section(47, "irregular", { structure: "파형강관 D800", culvert: {} }), section(55, "irregular", { structure: "대피로" }), section(58, "ep"), ]), ); // 옛 노선 목록(st-1 은 52.5 · 관 장부는 옛 자리 기준으로 이미 열쇠가 섬) out.outside = m.carryStationDrafts(P, R, R2); out.carriedKeys = keysOf("crossdesign", R2); out.carriedChoice = raw("crossdesign", R2)["grid:1"]; out.carriedRead = [...m.crossDesignChoices(P, R2).keys()].sort((a, b) => a - b); out.carriedRock = m.readRockBoundarySession(P, R2); out.carriedShoulder = keysOf("shoulderext", R2); /* 5. cross 단계 — 가짜 입력 · 같은 입력 0 · 측점 하나 고침 = 1 · 범위 밖 재사용 */ const grid = [0, 20, 40, 60, 80, 100]; const groundAt = (c) => 100 + 0.05 * c - 3 * Math.sin(c / 17); const base = { policy: input.policy, onlyExplicitCurves: false, basePvi: grid.map((c, i) => ({ chainage_m: c, elevation_m: groundAt(c) + (i % 2 ? 0.8 : -0.6) })), chainage: Array.from({ length: 101 }, (_, i) => i), ground: Array.from({ length: 101 }, (_, i) => groundAt(i)), stations: grid.map((c) => ({ station_id: `s${c}`, chainage_m: c })), }; const profile = m.buildAlignment(base, m.emptyEdits()); const side = (c, sign) => Array.from({ length: 24 }, (_, i) => ({ offsetM: (i + 1) * 0.5, z: groundAt(c) + sign * 0.3 * (i + 1) * 0.5, })); const ground = { stations: grid.map((c, i) => ({ key: i === 0 ? "end:BP" : i === grid.length - 1 ? "end:EP" : `grid:${i}`, chainageM: c, x: c, y: 0, azimuthDeg: 90, left: [0, 1], kind: i === 0 || i === grid.length - 1 ? "end" : "grid", id: null, ground: { centerZ: groundAt(c), left: side(c, 1), right: side(c, -1) }, planRadiusM: null, curveOuterSide: null, curveWideningM: null, })), longitudinal: { chainageM: [], z: [] }, }; const samples = m.groundSamples(ground.stations[1]); out.samplesSorted = samples.every((s, i) => i === 0 || s.offset_m > samples[i - 1].offset_m); out.samplesCount = samples.length; const { stage, counts } = m.createCrossStage(); const ctxOf = (cross, range, prev, prof = profile) => ({ generation: 1, range, signal: new AbortController().signal, inputs: { route: null, alignment: null, cross, pipes: null, structures: null }, out: { profile: prof, ground }, before: {}, prev, }); const crossInput = { standard: input.standard, byKey: { "grid:2": { groundType: "soil" } } }; const first = stage(ctxOf(crossInput, null, null)); out.crossFirst = { ...counts(), keys: Object.keys(first.byKey).length }; out.crossHasArea = Object.values(first.byKey).every((r) => Number.isFinite(r.cutAreaM2)); stage(ctxOf(crossInput, null, first)); out.crossSame = counts(); const touched = { ...crossInput, byKey: { ...crossInput.byKey, "grid:3": { groundType: "soil" } } }; const third = stage( ctxOf(touched, { sources: ["cross"], fromM: 60, toM: 60, keys: ["grid:3"] }, first), ); out.crossOne = counts(); out.crossOneChanged = third.byKey["grid:3"].detail.ground_type; const edited = m.buildAlignment(base, { station_offsets: { "40.000": 0.4 }, curve_radii: {} }); stage(ctxOf(touched, { sources: ["grade"], fromM: 20, toM: 60, keys: [] }, third, edited)); out.crossGrade = counts(); /* 6. masshaul 단계 — 계열 · 흙 배분 입력 */ const conversion = { soil: { loose: 1.25, compacted: 0.9 }, ripping_rock: { loose: 1.5, compacted: 1.2 }, blasting_rock: { loose: 1.65, compacted: 1.35 }, }; const massStage = m.createMassHaulStage(() => ({ conversion })); const mass = massStage({ ...ctxOf(crossInput, null, null), out: { profile, cross: first } }); out.massSeries = mass.series.map((s) => s.basis); out.massHaulInput = Array.isArray(mass.haulInput?.points); /* 7. haul 단계 — 같은 열쇠 안 물음 · 끊김 = 앞 배분 그대로 */ const posts = []; globalThis.fetch = async (_url, init) => { if (init.signal?.aborted) throw new Error("aborted"); posts.push(1); return { ok: true, json: async () => ({ haul_plan: { n: posts.length } }) }; }; const haul = m.createHaulStage(P, () => R); const haulCtx = (masshaul, prev, signal = new AbortController().signal) => ({ ...ctxOf(crossInput, null, prev), signal, out: { masshaul }, }); const plan1 = await haul(haulCtx(mass, null)); const plan2 = await haul(haulCtx(mass, plan1)); const other = massStage({ ...ctxOf(crossInput, null, null), out: { profile: edited, cross: third }, }); const aborted = new AbortController(); aborted.abort(); const plan3 = await haul(haulCtx(other, plan1, aborted.signal)); out.haul = { posts: posts.length, plan1, plan2, plan3 }; process.stdout.write(JSON.stringify(out));