/* 342-1 · 346-2 헬퍼 — vite ssr 로 묶은 진짜 모듈(helper_342_1_entry.ts)을 부르고 결과를 JSON 한 벌로. * node helper_342_1_station_line.mjs <묶음.mjs> <입력.json> * 입력 = { standard: 파이썬 config 표준단면, policy: 종단 정책 } · 화면 선 = design_profile.ts 의 진짜 * `listed()` 본문(소스에서 떼어 트랜스파일 · helper_177_3 방식). */ import { readFileSync } from "node:fs"; import { createRequire } from "node:module"; import { dirname, join } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; const HERE = dirname(fileURLToPath(import.meta.url)); const ROOT = join(HERE, "..", ".."); const require = createRequire(import.meta.url); const ts = require(join(ROOT, "config", "node_modules", "typescript")); 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(), keys: () => [...m.keys()], }; }; globalThis.window = globalThis; globalThis.sessionStorage = store(); globalThis.localStorage = store(); // 토스트 등 DOM 을 건드리는 자리 — 무엇을 불러도 받아 주는 빈 객체 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 warns = []; console.warn = (...args) => warns.push(args.map(String).join(" ")); const [bundlePath, inputPath] = process.argv.slice(2); const m = await import(pathToFileURL(bundlePath).href); const input = JSON.parse(readFileSync(inputPath, "utf8")); const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const transpile = (code) => ts.transpileModule(code, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, }).outputText; const PID = "p342"; const RID = 7; m.writeState("std-cross", input.standard, PID); /* ── 견본 노선 0~160 m — 서버 비정규 측점 85.054(0.001 m) · 관 지점 85.05(0.01 m) · 종단곡선 안 ── */ const SERVER_IRR = 85.054; const PIPE = 85.05; const regular = [0, 20, 40, 60, 80, 100, 120, 140, 160]; const serverStations = [...regular, SERVER_IRR] .sort((a, b) => a - b) .map((c) => ({ station_id: `s${c}`, chainage_m: c, kind: c === SERVER_IRR ? "irregular" : c === 0 ? "bp" : "regular", label: String(c), ...(c === SERVER_IRR ? { structure: "파형강관 D800" } : {}), })); const groundAt = (c) => 100 + 0.08 * c - 6 * Math.sin(c / 23); const chainage = Array.from({ length: 161 }, (_, i) => i); const basePvi = serverStations.map((s, i) => ({ chainage_m: s.chainage_m, elevation_m: groundAt(s.chainage_m) + (i % 2 ? 1.6 : -1.3), })); const base = { policy: input.policy, onlyExplicitCurves: false, basePvi, chainage, ground: chainage.map(groundAt), stations: serverStations.map((s) => ({ station_id: s.station_id, chainage_m: s.chainage_m })), }; const stored = m.buildAlignment(base, m.emptyEdits()); const crossSamples = (c) => Array.from({ length: 49 }, (_, i) => { const offset = -12 + i * 0.5; return { offset_m: offset, elevation_m: groundAt(c) + 0.3 * offset, valid: true }; }); const freshDesign = () => ({ design_elevation_m: 0, ground_type: "soil", section_mode: "left_cut", ditch_side: "left", ditch_type: "standard", two_stage_slope: false, }); const makeDetail = () => ({ longitudinal: { length_m: 160, stations: serverStations.map((s) => ({ ...s })), samples: chainage.map((c) => ({ chainage_m: c, elevation_m: groundAt(c), valid: true })), profile_alignment: JSON.parse(JSON.stringify(stored)), design_profiles: [m.toDesignProfile(stored, undefined)], }, cross_sections: serverStations.map((s) => ({ station_id: s.station_id, chainage_m: s.chainage_m, samples: crossSamples(s.chainage_m), design: freshDesign(), })), }); const irregularStations = [ { id: "pipe-1", station: 4, remainder: 5.05, chainage_m: PIPE, structure: "파형강관 D800" }, ]; /* 화면 선 = 종단도가 쓰는 진짜 `listedBase`(design_profile_base · 옛 design_profile 안 listed()) */ const screenBase = (detail) => m.listedBase(m.toAlignmentBase(stored), detail, irregularStations); const staleOf = (detail, line) => m.staleKey({ longitudinal: { design_profiles: [m.toDesignProfile(line, undefined)] }, cross_sections: detail.cross_sections, }); const refresh = (detail, extra = {}) => m.refreshCrossDesigns({ projectId: PID, routeId: RID, detail, edits: m.emptyEdits(), ...extra, }); const out = {}; /* 1. 머묾 재현 — 고치기 전 길(저장분 선) 대 화면 선 */ { const detail = makeDetail(); await refresh(detail, { alignment: stored }); // 저장본 = 서버가 저장분 선으로 낸 설계 const screen = m.buildAlignment(screenBase(detail), m.emptyEdits()); out.screenStations = screen.stations.map((s) => s.chainage_m); out.staleBefore = m.staleChainages(staleOf(detail, screen)); await refresh(detail); // 옛 길 — 맡긴 선 없음 → 저장분 선으로 다시 셈 out.staleOldPath = m.staleChainages(staleOf(detail, screen)); const release = m.holdScreenAlignment((id) => (id === RID ? screen : null)); out.staleAgainstScreenBefore = m.staleAgainstScreen(detail, RID); await refresh(detail); // 새 길 — 종단도가 맡긴 화면 선(B06 · 3D 와 같은 길) out.staleNewPath = m.staleChainages(staleOf(detail, screen)); out.staleAgainstScreenAfter = m.staleAgainstScreen(detail, RID); // B06 판정(다시 셈이 얹은 design_profiles)과 B05 판정(화면 선)이 같은가 out.staleB06View = m.staleChainages( m.staleKey({ longitudinal: detail.longitudinal, cross_sections: detail.cross_sections, }), ); release(); } /* 2. 국부 연산 — 같은 입력 다시 셈 0 · 계획고 ▲ 한 번 = 몇 측점만 · 결과는 통째 셈과 같음 */ { const detail = makeDetail(); const screen = m.buildAlignment(screenBase(detail), m.emptyEdits()); await refresh(detail, { alignment: screen }); await refresh(detail, { alignment: screen }); // 첫 셈이 결과 칸(측구 유무 등)을 채운 뒤 한 번 더 — 저장본 상태 const ids = () => detail.cross_sections.map((s) => s.design); let before = ids(); await refresh(detail, { alignment: screen }); out.recomputedSame = ids().filter((d, i) => d !== before[i]).length; const edited = m.buildAlignment(screenBase(detail), { station_offsets: { "140.000": 0.3 }, curve_radii: {}, }); before = ids(); const changed = await refresh(detail, { alignment: edited }); out.recomputedEdit = ids().filter((d, i) => d !== before[i]).length; out.changedEdit = changed.length; out.total = detail.cross_sections.length; // 통째 셈(새 측점 객체 = 기억 없음)과 결과 같음 const whole = makeDetail(); whole.cross_sections.forEach((s, i) => (s.design = { ...detail.cross_sections[i].design })); await refresh(whole, { alignment: stored }); await refresh(whole, { alignment: edited }); out.sameAsWhole = JSON.stringify(whole.cross_sections.map((s) => s.design)) === JSON.stringify(detail.cross_sections.map((s) => s.design)); } /* 3. 다시 셈 뒤에도 남은 낡음 — 기다림(true) → 남음(false) · 콘솔 경고 · 다시 그림 */ { const detail = makeDetail(); const screen = m.buildAlignment(screenBase(detail), m.emptyEdits()); await refresh(detail, { alignment: screen }); const broken = detail.cross_sections[3]; broken.samples = []; // 다시 셈이 못 고치는 측점 broken.design.design_elevation_m += 1; const applied = []; const preview = m.createCrossPreview({ projectId: PID, detail: () => detail, routeId: () => RID, edits: () => m.emptyEdits(), alignment: () => screen, debounceMs: () => 0, onApplied: (updated) => applied.push(updated.length), }); const key = staleOf(detail, screen); out.settleFirst = preview.scheduleIfStale(key); await sleep(50); const again = staleOf(detail, screen); out.settleSameKey = again === key; out.settleAfter = preview.scheduleIfStale(again); out.settleWarns = warns.filter((w) => w.includes("낡은 측점")).length; out.settleRedraws = applied.length; out.settleClear = preview.scheduleIfStale(""); preview.dispose(); } /* 4. 흙 배분 — 실패한 열쇠 다시 보냄 · 받은 열쇠는 다시 안 물음 */ { const replies = [false, true, true]; const posts = []; globalThis.fetch = async (_url, init) => { posts.push(JSON.parse(init.body).result.points.length); const ok = replies.shift() ?? true; return { ok, json: async () => ({ haul_plan: { tag: JSON.parse(init.body).result.points.length } }), }; }; let ready = 0; const prefetch = m.createHaulPlanPrefetch( PID, () => RID, () => (ready += 1), ); const A = { points: [{ cumulative_volume_m3: 1 }, { cumulative_volume_m3: 2 }] }; const B = { points: [{ cumulative_volume_m3: 1 }, { cumulative_volume_m3: 2 }, { cumulative_volume_m3: 3 }], }; prefetch.schedule(A); await sleep(450); out.haulAfterFail = posts.length; // 1 — 실패 prefetch.schedule(A); // 같은 열쇠 — 다시 보냄 await sleep(450); out.haulRetry = posts.length; // 2 prefetch.schedule(B); await sleep(450); out.haulB = posts.length; // 3 prefetch.schedule(A); // 받아 둔 열쇠 — 안 물음 await sleep(450); out.haulKept = posts.length; // 3 out.haulKeptPlan = prefetch.current()?.tag; // 2 (A 의 점 2개) out.haulReady = ready; prefetch.dispose(); } /* 5. [전체 측점 반영] — 다시 셈 한 번 · 측점 선택을 세션에 안 박음 */ { const detail = makeDetail(); const screen = m.buildAlignment(screenBase(detail), m.emptyEdits()); const release = m.holdScreenAlignment((id) => (id === RID ? screen : null)); const cardsCalls = []; const sync = m.createDesignSync({ projectId: PID, routeId: () => RID, detail: () => detail, view: () => ({ refreshCards: (updated) => cardsCalls.push(updated.length), refreshCard: () => cardsCalls.push(-1), detailChart: () => null, }), }); const t0 = performance.now(); await sync.applyPanelToAll(); out.applyAllMs = performance.now() - t0; out.applyAllCalls = cardsCalls; out.applyAllChoiceKeys = sessionStorage.keys().filter((k) => k.includes("crossdesign")).length; release(); } /* 6. 횡단 카드 보이는 것 먼저 — B06_Section_UI_Section_View.ts 의 진짜 refreshCards 본문 */ { const viewSrc = readFileSync(join(ROOT, "B06_Section", "B06_Section_UI_Section_View.ts"), "utf8"); const body = viewSrc.match(/\r?\n const refreshCards = [\s\S]*?\r?\n \};\r?\n/)[0]; const rebuilt = []; let redraws = 0; const top = { 0: 0, 20: 150, 40: 300, 60: 450, 80: 600 }; // 카드 위치(px) · 화면 = 0~320 const factory = new Function( "cards", "cardAt", "rebuildCard", "offscreen", "currentDetail", "redrawProfile", "fillSlopeNotice", "cachedStationInterval", `${transpile(body)}\nreturn refreshCards;`, ); let offscreen; const rebuildCard = (c) => { offscreen.queue.delete(c); rebuilt.push(c); return true; }; offscreen = m.createIdleQueue(rebuildCard, 2); const refreshCards = factory( { getBoundingClientRect: () => ({ top: 0, bottom: 320 }) }, (c) => ({ section: {}, el: { getBoundingClientRect: () => ({ top: top[c], bottom: top[c] + 140, height: 140 }) }, }), rebuildCard, offscreen, { cross_sections: [] }, () => (redraws += 1), { update: () => undefined }, 20, ); refreshCards([80, 0, 60, 20, 40]); out.cardsNow = [...rebuilt]; out.cardsRedraws = redraws; await sleep(120); out.cardsAll = [...rebuilt]; } process.stdout.write(JSON.stringify(out));