Files
Aislo/resources/tester/helper_272_value_hold.mjs
T

357 lines
11 KiB
JavaScript

/* 272 헬퍼(468 · 466-C) — 사진첩 빈 칸 · 세션 맵 빈 쓰기 · 숨은 종단도 파생값 · 배지 자릿수.
* node helper_272_value_hold.mjs <묶음.mjs>
* vite ssr 로 묶은 진짜 모듈(helper_272_entry.ts) · 표준출력 JSON 한 벌.
* 짝 = `test_272_history_empty_cells.py` · `test_272_hidden_summary.py`.
*/
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(),
};
};
// 손길 듣기(pointerdown · keydown)가 붙는 창 — 사진첩 시험(346-4)과 같은 꼴
const win = new EventTarget();
win.setTimeout = setTimeout;
win.clearTimeout = clearTimeout;
win.sessionStorage = store();
win.localStorage = store();
win.innerHeight = 900;
win.matchMedia = () => ({ matches: false, addEventListener: () => undefined });
win.requestAnimationFrame = (run) => setTimeout(run, 0);
globalThis.window = win;
globalThis.screen = {};
globalThis.sessionStorage = win.sessionStorage;
globalThis.localStorage = win.localStorage;
globalThis.requestAnimationFrame = win.requestAnimationFrame;
const any = new Proxy(function () {}, {
get: (_t, k) => (k === Symbol.toPrimitive ? () => 0 : any),
apply: () => any,
construct: () => any,
});
globalThis.document = any;
localStorage.setItem("frd_current_project_id", "p1");
const m = await import(pathToFileURL(process.argv[2]).href);
const out = {};
const S = sessionStorage;
const key = (name) => `aislo:draft:${name}:p1:7`;
const SIX = ["basinadjust", "crossw", "extraspan", "extrawall", "inletstruct", "revetx"];
const touch = () => win.dispatchEvent(new Event("keydown"));
/* ── H1 · 되돌린 뒤 빈 칸이 생김(R4) — 「{}」 · 「null」 · 「""」 쓰기는 걸음 아님 · ↷ 남음 ── */
{
const h = m.createProfileHistory(() => {});
touch();
S.setItem(key("alignment-draft"), JSON.stringify({ station_offsets: { 100: 1 } }));
h.record();
const edited = h.canUndo();
h.undo();
const undone = { undo: h.canUndo(), redo: h.canRedo(), grade: S.getItem(key("alignment-draft")) };
// B06 로 넘어가는 누름(손길) 뒤 첫 마운트 · 갈아끼움 이동(moveSessionMaps)이 빈 맵 여섯을 씀 · 다른 칸은 null · 빈 글
touch();
SIX.forEach((name) => S.setItem(key(name), "{}"));
S.setItem(key("rockb"), "null");
S.setItem(key("cutslope"), "");
h.record();
out.h1 = { edited, undone, after: { undo: h.canUndo(), redo: h.canRedo() } };
// ↷ = 사용자 조작 그대로 한 걸음
out.h1.redone = h.redo();
out.h1.redoGrade = S.getItem(key("alignment-draft"));
out.h1.final = { undo: h.canUndo(), redo: h.canRedo() };
S.clear();
localStorage.setItem("frd_current_project_id", "p1");
}
/* ── H2 · 「[]」 = 목록 초안(관 · 구조물 모두 지움) — 걸음 그대로 · 되돌리면 칸 없음 ── */
{
const h = m.createProfileHistory(() => {});
touch();
S.setItem("aislo:draft:pipes:p1", "[]");
h.record();
const step = h.canUndo();
h.undo();
out.h2 = { step, undonePipes: S.getItem("aislo:draft:pipes:p1") };
h.redo();
out.h2.redonePipes = S.getItem("aislo:draft:pipes:p1");
S.clear();
localStorage.setItem("frd_current_project_id", "p1");
}
/* ── H3 · 빈 칸이 있던 걸음으로 되돌림 = 빈 칸(없음과 같음) · 값 칸은 그대로 ── */
{
S.setItem(key("crossw"), "{}");
S.setItem(key("revetx"), JSON.stringify({ "100.00:inlet": { x: 1 } }));
const h = m.createProfileHistory(() => {});
touch();
S.setItem(key("crossw"), JSON.stringify({ "100.00": 6 }));
h.record();
const step = h.canUndo();
h.undo();
out.h3 = { step, crossw: S.getItem(key("crossw")), revetx: S.getItem(key("revetx")) };
S.clear();
localStorage.setItem("frd_current_project_id", "p1");
}
/* ── H4 · 세션 칸 창구(472-5 5-D · 메모리 맵 없음) — 빈 쓰기 = 칸 지움 · 값 있으면 씀 · 연동 두 겹 ── */
{
const width = m.sessionCells(
() => key("crossw"),
(v) => (typeof v === "number" ? v : undefined),
);
width.clear();
const emptyWrite = S.getItem(key("crossw"));
width.set("100.00", 6);
const filledWrite = S.getItem(key("crossw"));
width.delete("100.00");
const clearedWrite = S.getItem(key("crossw"));
const detached = m.sessionCells(
() => key("revetlink"),
(v) => !!v,
"detached",
);
const grade = m.sessionCells(
() => key("revetlink"),
(v) => !!v,
"grade",
);
detached.set("100.00", true);
grade.set("100.00", false);
const linkBoth = S.getItem(key("revetlink"));
detached.clear();
const linkGrade = S.getItem(key("revetlink"));
grade.clear();
out.h4 = {
emptyWrite,
filledWrite,
clearedWrite,
linkBoth,
linkGrade,
linkCleared: S.getItem(key("revetlink")),
};
S.clear();
localStorage.setItem("frd_current_project_id", "p1");
}
/* ── 숨은 종단도(본문 0 크기) — 낡음 예약 · 유토곡선 셈 · 배분 선반입 여전히 · 펼침과 같은 값 ── */
const samples = [0, 20, 40, 60, 80, 100].map((c) => ({
chainage_m: c,
elevation_m: 100 + c * 0.05,
valid: true,
}));
const plan = [0, 20, 40, 60, 80, 100].map((c) => ({ chainage_m: c, elevation_m: 100 + c * 0.04 }));
const design = (c, cut, fill, z) => ({
station_id: `station_${c}`,
chainage_m: c,
design: {
cut_area_m2: cut,
fill_area_m2: fill,
ground_type: "soil",
design_elevation_m: z,
},
});
const detail = {
longitudinal: {
length_m: 100,
samples,
stations: samples.map((s) => ({
station_id: `station_${s.chainage_m}`,
chainage_m: s.chainage_m,
})),
design_profiles: [{ id: "design_grade_line", samples: plan }],
},
cross_sections: [
design(0, 2, 0, 100),
design(20, 3, 1, 100.8),
design(40, 1, 4, 101.6),
design(60, 0, 6, 999), // 낡음 — 계획선과 어긋난 측점(다시 셈 예약 대상)
design(80, 2, 2, 103.2),
design(100, 4, 0, 104),
],
};
const alignment = {
samples: plan,
edits: { station_offsets: {}, curve_radii: {} },
stations: plan.map((p) => ({
chainage_m: p.chainage_m,
plan_elevation_m: p.elevation_m,
kind: "regular",
})),
curves: [],
balance: { cut_area_m2: 0, fill_area_m2: 0, net_area_m2: 0 },
policy: { station_interval_m: 20, edit_step_m: 0.1 },
};
const ONES = {
soil: { loose: 1, compacted: 1 },
ripping_rock: { loose: 1, compacted: 1 },
blasting_rock: { loose: 1, compacted: 1 },
};
function ctxFor(width, rec) {
const body = {
clientWidth: width,
clientHeight: width ? 400 : 0,
scrollLeft: 0,
parentElement: null,
replaceChildren: () => (rec.painted += 1),
addEventListener: () => undefined,
};
return {
body,
onStaleDesigns: (k) => (rec.stale.push(k), false),
callbacks: undefined,
massHaul: { draw: (p) => rec.mass.push(p) },
tableOverlay: { isOpen: () => false, contentHeight: () => 0, setTable: () => undefined },
store: { edits: () => ({}), savedRadii: () => ({}), resetStation: () => undefined },
detail: () => detail,
alignment: () => alignment,
base: () => null,
stationInterval: () => 20,
irregularStations: () => [],
minCoverTargets: () => [],
structures: () => [],
structureTypes: () => [],
selectedStationId: () => null,
setSelectedStationId: () => undefined,
selectedStructureId: () => null,
setSelectedStructureId: () => undefined,
stationDisplay: () => ({ station: 0, cumulative: 0 }),
setLastSize: () => (rec.sized += 1),
renderBalance: () => (rec.balance += 1),
applyHeightCascade: () => ({ chartHeight: 300 }),
clearMainDragCooldown: () => undefined,
selectStation: () => undefined,
applyEdits: () => undefined,
handleToolPick: () => false,
zoom: () => ({ x: 1 }),
holdElevationRange: (next) => next ?? undefined,
toolActive: () => false,
tools: { selectedRuns: () => [], mountOverlay: () => undefined },
stationIdAtStructure: () => null,
redraw: () => undefined,
setElevationWindowUpdater: () => undefined,
};
}
const pick = (p) =>
p && {
pendingRecalc: p.pendingRecalc,
staleChainages: p.staleChainages,
crossSections: p.crossSections.length,
longitudinal: p.longitudinal,
stations: p.stationSource.stations.map((s) => s.chainage_m),
maxChainageM: p.axis.maxChainageM,
};
const hiddenRec = { stale: [], mass: [], painted: 0, sized: 0, balance: 0 };
m.renderProfile(ctxFor(0, hiddenRec));
const shownRec = { stale: [], mass: [], painted: 0, sized: 0, balance: 0 };
let shownError = null;
try {
m.renderProfile(ctxFor(1200, shownRec));
} catch (error) {
shownError = String(error?.stack ?? error);
}
out.hidden = {
stale: hiddenRec.stale,
mass: pick(hiddenRec.mass[0]),
massCount: hiddenRec.mass.length,
painted: hiddenRec.painted,
sized: hiddenRec.sized,
};
out.shown = {
stale: shownRec.stale,
mass: pick(shownRec.mass[0]),
massCount: shownRec.mass.length,
painted: shownRec.painted,
error: shownError,
};
out.expectedStale = m.staleKey({
longitudinal: { design_profiles: [{ samples: plan }] },
cross_sections: detail.cross_sections,
});
/* 유토곡선 판(진짜) — 판 접힘 · 펼침인데 본문 숨음(자리 0) = 총괄값 · 배분 선반입만 · 그림 없음 */
function massPanel(open) {
localStorage.setItem("aislo:pref:masshaul-open", String(open));
const rec = { summary: [], scheduled: 0 };
const prefetch = {
schedule: () => (rec.scheduled += 1),
current: () => null,
};
const panel = m.createRouteMassHaulPanel(
() => undefined,
(s) => rec.summary.push(s),
prefetch,
);
panel.setContext({ conversion: ONES });
return { panel, rec };
}
const series = m.computeMassHaulSeries(
{ length_m: 100, design_profiles: [{ samples: plan }] },
detail.cross_sections,
ONES,
undefined,
["cross"],
);
const finalM3 = series[0]?.result.points.at(-1)?.cumulative_volume_m3 ?? null;
const massParams = hiddenRec.mass[0];
out.panel = {};
for (const open of [false, true]) {
const { panel, rec } = massPanel(open);
let error = null;
try {
if (massParams) panel.draw({ ...massParams, pendingRecalc: false });
} catch (e) {
error = String(e?.stack ?? e);
}
out.panel[open ? "open" : "closed"] = {
summary: rec.summary.map((s) => s?.finalM3 ?? null),
scheduled: rec.scheduled,
error,
};
}
out.finalM3 = finalM3;
/* ── 배지 표기 — 소수 한 자리 고정 ── */
{
const make = () => {
const el = {
textContent: "",
title: "",
hidden: false,
style: { setProperty: () => undefined },
classList: { toggle: () => undefined, add: () => undefined },
append: () => undefined,
className: "",
};
return el;
};
const made = [];
globalThis.document = { createElement: () => (made.push(make()), made.at(-1)) };
const badge = m.createMassHaulBadge();
const texts = [];
for (const v of [-17570, -17570.7, 1234.56, 0]) {
badge.set({
finalM3: v,
cutNaturalM3: 0,
cutCompactedM3: 0,
fillCompactedM3: 0,
surplusM3: 0,
shortageM3: 0,
});
texts.push(made[2].textContent);
}
out.badge = texts;
globalThis.document = any;
}
process.stdout.write(JSON.stringify(out));
process.exit(0);