- H1: 관 · 구조물 밖 측점이 움직인 때만 「노선 바뀜」 넘김 규칙 · 시설만 옮기면 열쇠째 옮김 (사면 쉬프트 · 소단 깊이 · 단면 유형 · 관 측점 고침 그대로) - H1 덧: 초안 없을 때도 저장분 계획으로 열쇠 주인(관 = 저장 id · 계산 묶음 열쇠와 같음 · 첫 옮김도 열쇠째) · 묶음이 세운 열쇠 목록을 같은 측점이면 주인 규칙으로 다시 세지 않음 · 갈아끼운 옛 상세 맞춤 끝냄 - H2: [저장] 도중 관 · 구조물 먹이기 · 맞춤 고리 쉼 · 끝나면 저장 목록 다시 읽고 맞춤(design_pipeline_facilities) - M4: 새 시설 측점 서버 미리보기(preview-stations) 걷음 · 새 자리 = 계산 묶음 갈아끼움 · 카드 이름만 얹음 · 종류 바뀐 자리 = 구조물 없는 사본 - 415-2: 되돌리기 · 다시하기(세션만 되씀) 뒤 관 · 구조물을 묶음 입력과 견줘 다시 넣음 - 시험: test_415_page_split 새로 · 미리보기 전제 시험(273-2 · 298 · 317 · 318 · 322-0 · 322-3)을 새 규칙으로 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TteK8mfYZetk5P3bEZkyVZ
279 lines
10 KiB
JavaScript
279 lines
10 KiB
JavaScript
/* 415 H2 헬퍼 — [저장] 도중 계산 묶음 먹이기 멈춤 · 끝나면 저장 목록 다시 읽음. 진짜 TS(`design_pipeline_page` ·
|
|
* 단계 파일 전부)를 트랜스파일해 Node 로 돌림 · 서버 창구 · 저장 중 표시만 가짜(틀 = `helper_365_page_pipeline.cjs`).
|
|
* 입력 argv[2] JSON · argv[3] 격자 이진 · 결과 = 표준출력 JSON. 짝 = `test_415_page_split.py`. */
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
|
const CATALOG = require("./helper_322_catalog_module.cjs");
|
|
|
|
const ROOT = path.join(__dirname, "..", "..");
|
|
const job = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
|
|
function memoryStorage() {
|
|
const map = new Map();
|
|
return {
|
|
getItem: (key) => (map.has(key) ? map.get(key) : null),
|
|
setItem: (key, value) => map.set(key, String(value)),
|
|
removeItem: (key) => map.delete(key),
|
|
key: (index) => [...map.keys()][index] ?? null,
|
|
get length() {
|
|
return map.size;
|
|
},
|
|
};
|
|
}
|
|
globalThis.window = {
|
|
sessionStorage: memoryStorage(),
|
|
localStorage: memoryStorage(),
|
|
setTimeout,
|
|
clearTimeout,
|
|
};
|
|
|
|
// 흙 배분 서버 — 늦게 답함 · 끊기면 버림
|
|
const haul = { calls: 0, aborted: 0, answered: 0 };
|
|
globalThis.fetch = (url, init) =>
|
|
new Promise((resolve, reject) => {
|
|
haul.calls += 1;
|
|
const timer = setTimeout(() => {
|
|
haul.answered += 1;
|
|
resolve({ ok: true, json: async () => ({ haul_plan: { blocks: [], url } }) });
|
|
}, job.haulDelayMs);
|
|
init.signal.addEventListener("abort", () => {
|
|
clearTimeout(timer);
|
|
haul.aborted += 1;
|
|
reject(new Error("aborted"));
|
|
});
|
|
});
|
|
|
|
const STUBS = {};
|
|
const cache = new Map();
|
|
function loadTs(full) {
|
|
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 localRequire = (request) => {
|
|
if (CATALOG.isCatalogRequest(request)) return CATALOG;
|
|
const key = Object.keys(STUBS).find((name) => request.endsWith(name));
|
|
if (key) return STUBS[key];
|
|
if (request.startsWith("@config/")) return { API_BASE_URL: "http://fake" };
|
|
if (request.startsWith("@util/"))
|
|
return loadTs(path.join(ROOT, "common_util", `${request.slice(6)}.ts`));
|
|
if (request.startsWith(".")) return loadTs(path.join(path.dirname(full), `${request}.ts`));
|
|
return require(request);
|
|
};
|
|
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
|
return box.exports;
|
|
}
|
|
const load = (...parts) => loadTs(path.join(ROOT, ...parts));
|
|
|
|
const sampler = load("common_util", "common_util_surface_sampler.ts");
|
|
const bytes = fs.readFileSync(process.argv[3]);
|
|
const grid = sampler.parseDtmGrid(
|
|
bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.length),
|
|
);
|
|
const local = { grid, design: "las" };
|
|
|
|
STUBS["B05_Profile_Api_Replan"] = {
|
|
analysisTerrain: () => "las",
|
|
fetchRoutePlan: async () => ({
|
|
planned: job.line,
|
|
curves: [],
|
|
min_radius_m: job.minRadius,
|
|
station_snap: false,
|
|
}),
|
|
};
|
|
STUBS["B05_Profile_Api_Structures"] = {
|
|
fetchStructures: async () => ({ structures: [] }),
|
|
fetchStructureTypes: async () => job.types,
|
|
};
|
|
STUBS["B05_Profile_Api_TerrainGrid"] = {
|
|
loadDesignTerrainGrid: async () => local,
|
|
groundStageDeps: () => ({ grid: () => local, remote: async () => [] }),
|
|
};
|
|
/** 서버 관 저장분 — [저장] 뒤 바뀜 · 읽은 횟수(저장 뒤 다시 읽음). */
|
|
let serverPipes = job.pipes;
|
|
let pipeReads = 0;
|
|
STUBS["B04_PreProcess_Api_Fetch"] = {
|
|
fetchDetailPipePoints: async () => {
|
|
pipeReads += 1;
|
|
return { pipe_points: serverPipes.map((pipe) => ({ ...pipe, lonlat: [0, 0] })) };
|
|
},
|
|
};
|
|
/** [저장] 도중 — 묶음이 늦게 불러오는 저장 창구 가짜(415 H2). */
|
|
let saving = false;
|
|
STUBS["design_save"] = { savingDesign: () => saving };
|
|
STUBS["B06_Section_UI_Standard_Panel"] = {
|
|
effectiveStandardCross: () => job.standard,
|
|
readRockBoundaryDefault: () => -0.5,
|
|
};
|
|
|
|
const pipeMod = load("A00_Common", "design", "logic", "design_pipeline.ts");
|
|
const pageMod = load("A00_Common", "design", "design_pipeline_page.ts");
|
|
const stationsMod = load("A00_Common", "design", "logic", "design_pipeline_stations.ts");
|
|
const routeGround = load("B05_Profile", "logic", "B05_Profile_Pipeline_RouteGround.ts");
|
|
const grade = load("B05_Profile", "logic", "B05_Profile_Pipeline_Grade.ts");
|
|
const gradeGround = load("B05_Profile", "logic", "B05_Profile_Grade_Ground.ts");
|
|
const detailMod = load("B05_Profile", "logic", "B05_Profile_Pipeline_Detail.ts");
|
|
const alignment = load("B05_Profile", "logic", "B05_Profile_UI_Profile_Alignment.ts");
|
|
const editMod = load("B05_Profile", "B05_Profile_UI_Profile_Edit.ts");
|
|
const page = load("A00_Common", "b_page_state.ts");
|
|
|
|
const tick = (ms = 0) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
const runs = () => globalThis.__aisloPipeline?.runs ?? [];
|
|
async function settle(count) {
|
|
for (let i = 0; i < 400; i += 1) {
|
|
const last = runs()[runs().length - 1];
|
|
if (runs().length >= count && last && last.outcome !== "running") return last;
|
|
await tick(5);
|
|
}
|
|
throw new Error("묶음이 끝나지 않음");
|
|
}
|
|
const n = job.nodes.length;
|
|
const draftOf = (nodes) => ({
|
|
nodes,
|
|
curveOn: Array(n).fill(true),
|
|
curveRadius: Array(n).fill(null),
|
|
curveLock: Array(n).fill(null),
|
|
curveArc: Array(n).fill(null),
|
|
stationSnap: false,
|
|
});
|
|
const shift = (index, dx, dy) =>
|
|
job.nodes.map((node, i) => (i === index ? [node[0] + dx, node[1] + dy] : node));
|
|
|
|
/** 저장 기준 — 저장 노선 지반 첫 계획선(관 측점 포함 · 서버 정책 그대로). */
|
|
async function savedBaseOf() {
|
|
const seed = pipeMod.createDesignPipeline({
|
|
stages: {
|
|
route: routeGround.createRouteStage({
|
|
minRadiusM: () => job.minRadius,
|
|
savedLine: () => ({ line: job.line, curves: [] }),
|
|
}),
|
|
stations: stationsMod.createStationsStage({
|
|
intervalM: job.interval,
|
|
types: () => job.types,
|
|
}),
|
|
ground: routeGround.createGroundStage({ grid: () => local, remote: async () => [] }),
|
|
},
|
|
});
|
|
await seed.start({ route: null, alignment: null, cross: null, pipes: job.pipes, structures: [] });
|
|
const ground = seed.output("ground");
|
|
seed.dispose();
|
|
const samples = ground.longitudinal.chainageM.map((c, i) => ({
|
|
chainage_m: c,
|
|
elevation_m: ground.longitudinal.z[i],
|
|
}));
|
|
const profile = gradeGround.groundProfile(samples);
|
|
return {
|
|
policy: job.policy,
|
|
onlyExplicitCurves: false,
|
|
basePvi: gradeGround.groundFollowingBasePvi({
|
|
samples,
|
|
stations: ground.stations.map((s) => s.chainageM),
|
|
policy: job.policy,
|
|
}),
|
|
chainage: profile.chainage,
|
|
ground: profile.ground,
|
|
stations: ground.stations.map((s) => ({
|
|
station_id: grade.stationIdOf(s.chainageM),
|
|
chainage_m: s.chainageM,
|
|
})),
|
|
};
|
|
}
|
|
|
|
const store = load("A00_Common", "design", "design_store.ts");
|
|
|
|
const pipeAt = () =>
|
|
pageMod
|
|
.pagePipeline(P)
|
|
.input("pipes")
|
|
.map((pipe) => pipe.chainage_m);
|
|
const stationAt = () =>
|
|
(shop.get("stations") ?? []).filter((s) => s.kind === "pipe").map((s) => s.chainageM);
|
|
const P = "proj-415";
|
|
const shop = store.useDesignStore(P, null);
|
|
|
|
async function run() {
|
|
window.localStorage.setItem("frd_current_project_id", P);
|
|
const savedBase = await savedBaseOf();
|
|
const link = {
|
|
pipelineSource: () => ({
|
|
base: savedBase,
|
|
edits: alignment.emptyEdits(),
|
|
detail: job.detail,
|
|
routeId: 7,
|
|
}),
|
|
adoptPipeline: () => undefined,
|
|
redraw: () => undefined,
|
|
};
|
|
pageMod.routeDraftBridge(P, link, { stationIntervalM: job.interval });
|
|
pageMod.pipelineEarthwork(P, { conversion: job.conversion });
|
|
await settle(runs().length + 1);
|
|
const out = { open: pipeAt() };
|
|
|
|
// ① 관 5 m 옮김(저장 전 초안) — 묶음이 새 자리로
|
|
const moved = job.pipes.map((pipe) => ({ ...pipe, chainage_m: pipe.chainage_m + 5 }));
|
|
shop.set("pipes", moved);
|
|
await settle(runs().length + 1);
|
|
out.moved = { input: pipeAt(), stations: stationAt() };
|
|
|
|
// ② [저장] 도중 — 관 차례가 서버에 쓰고 초안을 비움 · 맞춤 고리도 불림
|
|
saving = true;
|
|
serverPipes = moved;
|
|
const mark = runs().length;
|
|
const reads = pipeReads;
|
|
shop.set("pipes", null);
|
|
const rounds = await pageMod.reconcilePagePipeline(P);
|
|
await tick(150);
|
|
out.during = {
|
|
runs: runs().length - mark,
|
|
rounds,
|
|
input: pipeAt(),
|
|
stations: stationAt(),
|
|
reads: pipeReads - reads,
|
|
};
|
|
|
|
// ③ 저장 끝 — 저장 목록 다시 읽고 맞춤(같은 자리 = 다시 안 셈)
|
|
saving = false;
|
|
await tick(400);
|
|
await pageMod.pagePipeline(P).settled();
|
|
out.after = {
|
|
runs: runs().length - mark,
|
|
input: pipeAt(),
|
|
stations: stationAt(),
|
|
reads: pipeReads - reads,
|
|
sources: runs()
|
|
.slice(mark)
|
|
.map((r) => r.sources),
|
|
};
|
|
// ④ 저장 뒤 맞춤 고리 — 저장분(새 자리) 대 묶음 입력 같음 = 0 바퀴
|
|
out.reconcile = await pageMod.reconcilePagePipeline(P);
|
|
|
|
// ⑤ 관 옮김 → 되돌리기(415-2) — 세션만 되쓰는 사진첩 · 묶음 입력 · 측점이 원래로 · 다시하기 = 옮긴 자리
|
|
const history = load("B05_Profile", "B05_Profile_UI_Profile_History.ts");
|
|
const book = history.createProfileHistory(() => undefined);
|
|
const again = moved.map((pipe) => ({ ...pipe, chainage_m: pipe.chainage_m + 5 }));
|
|
shop.set("pipes", again);
|
|
await settle(runs().length + 1);
|
|
book.record();
|
|
const movedAgain = { input: pipeAt(), stations: stationAt() };
|
|
book.undo();
|
|
await tick(50);
|
|
await pageMod.pagePipeline(P).settled();
|
|
const undone = { input: pipeAt(), stations: stationAt(), draft: shop.get("pipes") };
|
|
book.redo();
|
|
await tick(50);
|
|
await pageMod.pagePipeline(P).settled();
|
|
out.undo = { movedAgain, undone, redone: { input: pipeAt(), stations: stationAt() } };
|
|
pageMod.disposePagePipeline(P);
|
|
return out;
|
|
}
|
|
|
|
run()
|
|
.then((out) => process.stdout.write(JSON.stringify(out)))
|
|
.catch((error) => {
|
|
process.stderr.write(String(error?.stack ?? error));
|
|
process.exit(1);
|
|
});
|