- ↶ ↷ 글 없이 기호 + aria-label · title(단축키) - 저장 전으로 단추 · history.reset · 초안 다리 reset 지움 - 시험 chrome_modal · 346_4 · 346_1 고쳐 씀 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmC34fsxxbweEG3RUjs3Mn
361 lines
14 KiB
JavaScript
361 lines
14 KiB
JavaScript
/* PLAN 346-1 단위검증 헬퍼 — 계산 묶음을 페이지 · 노선 편집에 붙인 것.
|
|
* 진짜 TS(`design_pipeline` · `design_pipeline_page` · `design_pipeline_stations` · `logic/*Pipeline*` ·
|
|
* `_RouteEdit_Draft` · sub1 · sub8 · sub6 짝)를 그 자리에서 트랜스파일해 Node 로 돌림. 서버 창구
|
|
* (`Api_Replan` · `Api_Structures` · `Api_TerrainGrid`)만 가짜. 입력 argv[2] JSON · argv[3] 격자 이진.
|
|
* 결과 = 표준출력 JSON 한 벌. 짝 = `test_346_1_pipeline_page.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;
|
|
},
|
|
};
|
|
}
|
|
const session = memoryStorage();
|
|
globalThis.window = { sessionStorage: session, localStorage: memoryStorage() };
|
|
globalThis.fetch = async () => ({ ok: false });
|
|
|
|
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("@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" };
|
|
|
|
let remoteCalls = 0;
|
|
STUBS["B05_Profile_Api_Replan"] = {
|
|
analysisTerrain: () => "las",
|
|
fetchRoutePlan: async () => ({ planned: job.line, curves: [], min_radius_m: job.minRadius }),
|
|
};
|
|
STUBS["B05_Profile_Api_Structures"] = {
|
|
fetchStructures: async () => ({ structures: [] }),
|
|
fetchStructureTypes: async () => job.types,
|
|
};
|
|
STUBS["B05_Profile_Api_TerrainGrid"] = {
|
|
designTerrainGridNow: () => local,
|
|
loadDesignTerrainGrid: async () => local,
|
|
groundStageDeps: () => ({ grid: () => local, remote: async () => (remoteCalls++, []) }),
|
|
};
|
|
// 346-2 붙임 뒤 창구(관 목록 · 표준단면 · 흙 배분) — 이 시험은 노선 · 종단만 봄
|
|
STUBS["B04_PreProcess_Api_Fetch"] = { fetchDetailPipePoints: async () => ({ pipe_points: [] }) };
|
|
STUBS["B06_Section_UI_Standard_Panel"] = {
|
|
effectiveStandardCross: () => null,
|
|
readRockBoundaryDefault: () => -0.5,
|
|
};
|
|
STUBS["B05_Profile_Api_HaulPlan"] = { createHaulStage: () => async () => null };
|
|
|
|
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 footprint = load("B05_Profile", "logic", "B05_Profile_Footprint_Stage.ts");
|
|
const alignment = load("B05_Profile", "logic", "B05_Profile_UI_Profile_Alignment.ts");
|
|
const draftLink = load("B05_Profile", "B05_Profile_UI_RouteEdit_Draft.ts");
|
|
const page = load("A00_Common", "b_page_state.ts");
|
|
|
|
const tick = (ms = 0) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
const n = job.nodes.length;
|
|
const draftOf = (nodes) => ({
|
|
rev: 1,
|
|
nodes,
|
|
curveOn: Array(n).fill(true),
|
|
curveRadius: Array(n).fill(null),
|
|
curveLock: Array(n).fill(null),
|
|
curveArc: Array(n).fill(null),
|
|
stationSnap: false,
|
|
terrain: null,
|
|
});
|
|
const movedNodes = job.nodes.map((node, i) => (i === n - 1 ? [node[0] + 6, node[1] - 4] : node));
|
|
const inputs = (route = null, edits = alignment.emptyEdits()) => ({
|
|
route,
|
|
alignment: edits,
|
|
cross: null,
|
|
pipes: [],
|
|
structures: [],
|
|
});
|
|
|
|
/** 단계 한 벌(실제) — 저장 기준은 부르는 쪽. */
|
|
function realStages(savedBase, remote = null) {
|
|
return {
|
|
route: routeGround.createRouteStage({
|
|
minRadiusM: () => job.minRadius,
|
|
savedLine: () => ({ line: job.line, curves: [] }),
|
|
}),
|
|
follow: grade.followStage,
|
|
stations: stationsMod.createStationsStage({ types: () => job.types }),
|
|
ground: routeGround.createGroundStage(
|
|
remote ? { grid: () => null, remote } : { grid: () => local, remote: async () => [] },
|
|
),
|
|
planline: grade.createPlanlineStage({ savedBase: () => savedBase, savedLine: () => job.line }),
|
|
profile: grade.profileStage,
|
|
footprint: footprint.footprintStage,
|
|
};
|
|
}
|
|
|
|
async function run() {
|
|
const out = { n };
|
|
|
|
// ⓪ 저장 기준 — 저장 노선 지반으로 첫 계획선(서버 정책 그대로)
|
|
const seed = pipeMod.createDesignPipeline({
|
|
stages: { ...realStages(null), planline: undefined, profile: undefined, footprint: undefined },
|
|
});
|
|
await seed.start(inputs());
|
|
const ground0 = seed.output("ground");
|
|
const samples = ground0.longitudinal.chainageM.map((c, i) => ({
|
|
chainage_m: c,
|
|
elevation_m: ground0.longitudinal.z[i],
|
|
}));
|
|
const profile0 = gradeGround.groundProfile(samples);
|
|
const savedBase = {
|
|
policy: job.policy,
|
|
onlyExplicitCurves: false,
|
|
basePvi: gradeGround.groundFollowingBasePvi({
|
|
samples,
|
|
stations: ground0.stations.map((s) => s.chainageM),
|
|
policy: job.policy,
|
|
}),
|
|
chainage: profile0.chainage,
|
|
ground: profile0.ground,
|
|
stations: ground0.stations.map((s) => ({
|
|
station_id: grade.stationIdOf(s.chainageM),
|
|
chainage_m: s.chainageM,
|
|
})),
|
|
};
|
|
|
|
// ① 실제 단계 한 번 — 저장 노선 · 노선 한 점 옮김 · 계획고 한 점
|
|
{
|
|
const pipe = pipeMod.createDesignPipeline({ stages: realStages(savedBase) });
|
|
const first = await pipe.start(inputs());
|
|
const savedPlanline = pipe.output("planline") === savedBase;
|
|
const beforeLine = pipe.output("route").line;
|
|
const beforeKeys = pipe.output("ground").stations.map((s) => [s.key, s.chainageM]);
|
|
const draft = draftOf(movedNodes);
|
|
const after = routeGround.draftLine(draft, job.minRadius).line;
|
|
const range = grade.routeRange(beforeLine, after);
|
|
const routeRun = await pipe.update("route", draft, range);
|
|
const moved = {
|
|
basePvi: pipe.output("planline").basePvi,
|
|
stations: pipe.output("ground").stations.map((s) => [s.key, s.chainageM]),
|
|
profileStations: pipe.output("profile").stations.length,
|
|
footprint: pipe.output("footprint") !== null,
|
|
};
|
|
const nodes = pipe.output("planline").basePvi.map((p) => p.chainage_m);
|
|
const target = nodes[Math.floor(nodes.length / 2)];
|
|
const edits = { station_offsets: { [alignment.chainageKey(target)]: 0.8 }, curve_radii: {} };
|
|
const gRange = grade.gradeRange(alignment.emptyEdits(), edits, nodes);
|
|
const gradeRun = await pipe.update("grade", edits, gRange);
|
|
// 꼭짓점 높이(종단곡선은 그 아래로 둥글게 지나감)
|
|
const pviAt = (profile) =>
|
|
profile.pvi.find((p) => Math.abs(p.chainage_m - target) < 1e-6).elevation_m;
|
|
const raised = pviAt(pipe.output("profile"));
|
|
const plain = pviAt(alignment.buildAlignment(pipe.output("planline"), alignment.emptyEdits()));
|
|
out.real = {
|
|
first: { outcome: first.outcome, totalMs: first.totalMs, stages: first.stages },
|
|
savedPlanline,
|
|
range,
|
|
routeRun: {
|
|
outcome: routeRun.outcome,
|
|
totalMs: routeRun.totalMs,
|
|
stages: routeRun.stages,
|
|
skipped: routeRun.skipped,
|
|
},
|
|
savedBasePvi: savedBase.basePvi,
|
|
moved,
|
|
beforeKeys,
|
|
gradeRange: gRange,
|
|
target,
|
|
gradeRun: {
|
|
outcome: gradeRun.outcome,
|
|
stages: Object.keys(gradeRun.stages),
|
|
skipped: gradeRun.skipped,
|
|
totalMs: gradeRun.totalMs,
|
|
},
|
|
raised: raised - plain,
|
|
log: globalThis.__aisloPipeline.runs.length,
|
|
};
|
|
}
|
|
|
|
// ② 늦은 응답 버림 — 격자 없음(서버 길) · 첫 지반 응답이 늦는 사이 노선을 또 옮김
|
|
{
|
|
const waits = [];
|
|
const remote = (points, _terrain, signal) =>
|
|
new Promise((resolve, reject) => {
|
|
waits.push(() => resolve(points.map(([x, y]) => sampler.heightAt(grid, x, y))));
|
|
signal.addEventListener("abort", () => reject(new Error("aborted")));
|
|
});
|
|
const pipe = pipeMod.createDesignPipeline({ stages: realStages(savedBase, remote) });
|
|
const commits = [];
|
|
pipe.subscribe((e) => e.kind === "stage" && commits.push(`${e.stage}@${e.generation}`));
|
|
const a = pipe.start(inputs(draftOf(movedNodes)));
|
|
await tick(5);
|
|
const b = pipe.update("route", draftOf(job.nodes), { fromM: 0, toM: Infinity });
|
|
await tick(5);
|
|
waits.forEach((go) => go());
|
|
const ra = await a;
|
|
await tick(5);
|
|
waits.slice(1).forEach((go) => go());
|
|
const rb = await b;
|
|
out.late = {
|
|
a: ra.outcome,
|
|
b: rb.outcome,
|
|
commits,
|
|
profileGen: commits.filter((c) => c.startsWith("profile")),
|
|
};
|
|
}
|
|
|
|
// ③ 편집기 다리 — 사진첩 감싸기 · 초안 되살림
|
|
{
|
|
const told = [];
|
|
const dirty = true;
|
|
const fake = {
|
|
commit: () => undefined,
|
|
undo: () => ({
|
|
planned: [
|
|
[0, 0],
|
|
[1, 1],
|
|
],
|
|
curveOn: [true, true],
|
|
curveRadius: [null, null],
|
|
curveLock: [null, null],
|
|
curveArc: [null, null],
|
|
apexLock: [false, false],
|
|
picked: -1,
|
|
}),
|
|
redo: () => null,
|
|
canUndo: () => true,
|
|
canRedo: () => false,
|
|
isDirty: () => dirty,
|
|
};
|
|
const bridge = {
|
|
edited: (d) => told.push(d && d.nodes.length),
|
|
saved: () => draftOf(job.nodes),
|
|
};
|
|
const watched = draftLink.watch(fake, bridge, () => true);
|
|
watched.commit({
|
|
...fake.undo(),
|
|
planned: [
|
|
[0, 0],
|
|
[2, 2],
|
|
[3, 3],
|
|
],
|
|
});
|
|
watched.undo();
|
|
watched.redo();
|
|
const restored = [];
|
|
draftLink.restore(bridge, (snap, message, record) =>
|
|
restored.push([snap.planned.length, record]),
|
|
);
|
|
draftLink.restore({ saved: () => null }, () => restored.push("none"));
|
|
out.draftLink = {
|
|
told,
|
|
restored,
|
|
unwatched: draftLink.watch(fake, undefined, () => false) === fake,
|
|
};
|
|
}
|
|
|
|
// ④ 페이지 묶음 — 다리 → 노선 초안 캐시 칸 · 종단도 받기 · 떠남 · 다시 엶(초안 살아남음) · ▲▼
|
|
{
|
|
const P = "proj-3461";
|
|
const adopted = [];
|
|
let edits = alignment.emptyEdits();
|
|
const link = {
|
|
pipelineSource: () => ({ base: savedBase, edits }),
|
|
adoptPipeline: (base, nextEdits, draft) =>
|
|
adopted.push({ draft, saved: base === savedBase, stations: base.stations.length }),
|
|
};
|
|
const bridge = pageMod.routeDraftBridge(P, link, { stationIntervalM: 20 });
|
|
await tick(10);
|
|
const before = { saved: bridge.saved(), pipe: pageMod.pagePipeline(P) !== null };
|
|
const edit = { ...draftOf(movedNodes) };
|
|
delete edit.rev;
|
|
delete edit.terrain;
|
|
bridge.edited(edit);
|
|
await tick(50);
|
|
const key = `aislo:draft:route-draft:${P}`;
|
|
const cached = JSON.parse(session.getItem(key));
|
|
bridge.edited(edit); // 같은 노선 — 판 번호 그대로 · 다시 안 셈
|
|
const sameRev = JSON.parse(session.getItem(key)).rev;
|
|
await tick(50);
|
|
const adoptedDraft = adopted.slice();
|
|
bridge.edited(null); // 열 때로(되돌리기 끝)
|
|
await tick(50);
|
|
const afterRevert = { session: session.getItem(key), last: adopted[adopted.length - 1] };
|
|
bridge.edited(edit);
|
|
await tick(50);
|
|
pageMod.disposePagePipeline(P);
|
|
const disposed = pageMod.pagePipeline(P);
|
|
adopted.length = 0;
|
|
const again = pageMod.routeDraftBridge(P, link, { stationIntervalM: 20 });
|
|
const survived = again.saved();
|
|
await tick(80); // 받기 끝 → 저장 안 한 초안이 있으면 그 노선으로 셈
|
|
const reopened = adopted.slice();
|
|
edits = { station_offsets: { "100.000": 0.5 }, curve_radii: {} };
|
|
pageMod.pipelineGrade(P, edits);
|
|
await tick(30);
|
|
const gradeRuns = globalThis.__aisloPipeline.runs.slice(-1)[0];
|
|
page.clearDrafts(P);
|
|
out.page = {
|
|
before,
|
|
cached: { rev: cached.rev, terrain: cached.terrain, nodes: cached.nodes.length },
|
|
sameRev,
|
|
adoptedDraft,
|
|
afterRevert,
|
|
disposed,
|
|
survived: survived && survived.nodes.length,
|
|
reopened,
|
|
grade: { sources: gradeRuns.sources, stages: Object.keys(gradeRuns.stages) },
|
|
clearedDraft: session.getItem(key),
|
|
remoteCalls,
|
|
};
|
|
pageMod.disposePagePipeline(P);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
run()
|
|
.then((out) => {
|
|
process.stdout.write(JSON.stringify(out));
|
|
process.exit(0);
|
|
})
|
|
.catch((error) => {
|
|
process.stderr.write(String(error && error.stack ? error.stack : error));
|
|
process.exit(1);
|
|
});
|