- B06 중심점 칸 = 버튼 없음 · 「▲▼ = 종단 계획고 · 좌우 이동은 평면에서」 · ◀▶ 키 먹어 막음 - B06_Section_UI_Cross_Center = 판정 원 · 한 축 끌기(암 경계선)만 남김 - B05 노선 좌우 host(_RouteEdit_Shift) · 고름 계산(logic/B05_Profile_RouteShift) 지움 · 페이지 · 편집기 배선 한 줄씩 걷음 - 시험 356 = 가짜 DOM 으로 중심점 고름 · 버튼 없음 · 키 · 끌기 없음 확인 · 356_4 = 중심점 좌우 걸음 뺌 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
238 lines
8.1 KiB
JavaScript
238 lines
8.1 KiB
JavaScript
/* PLAN 424 단위검증 헬퍼 — 횡단 카드 중심점 = ▲▼(종단 계획고)만 · 좌우는 평면에서만(요구 260).
|
|
* 화면 TS(`B06_Section_UI_Cross_Design_Pick` · `_Cross_Center`)를 트랜스파일해 Node 로 돌림(가짜 DOM =
|
|
* `helper_370_2_toe_stop.cjs` 꼴) · 결과 = 표준출력 JSON 한 벌. 입력 = argv[2] 표준단면 JSON. 짝 = `test_356_b06_card_center.py`.
|
|
*/
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
|
|
|
const ROOT = path.join(__dirname, "..", "..");
|
|
const cache = new Map();
|
|
const STUBS = {
|
|
"@ui/ui_template_toast": { showToast: () => undefined },
|
|
"@ui/ui_template_elements": { showToast: () => undefined },
|
|
};
|
|
|
|
class FakeNode {
|
|
constructor(tag) {
|
|
this.tagName = String(tag);
|
|
this.children = [];
|
|
this.attrs = {};
|
|
this.listeners = {};
|
|
this.parent = null;
|
|
this.hidden = false;
|
|
this.title = "";
|
|
this._text = "";
|
|
this._classes = new Set();
|
|
const self = this;
|
|
this.classList = {
|
|
add: (...names) => names.forEach((n) => self._classes.add(n)),
|
|
remove: (...names) => names.forEach((n) => self._classes.delete(n)),
|
|
toggle: (name, force) => {
|
|
const on = force === undefined ? !self._classes.has(name) : !!force;
|
|
on ? self._classes.add(name) : self._classes.delete(name);
|
|
return on;
|
|
},
|
|
contains: (name) => self._classes.has(name),
|
|
};
|
|
this.style = { setProperty: () => {} };
|
|
}
|
|
set className(value) {
|
|
this._classes = new Set(String(value).split(/\s+/).filter(Boolean));
|
|
}
|
|
get className() {
|
|
return [...this._classes].join(" ");
|
|
}
|
|
set textContent(value) {
|
|
this._text = String(value);
|
|
this.children = [];
|
|
}
|
|
get textContent() {
|
|
return this._text + this.children.map((c) => c.textContent).join("");
|
|
}
|
|
get childElementCount() {
|
|
return this.children.length;
|
|
}
|
|
get isConnected() {
|
|
return true;
|
|
}
|
|
_detach(kid) {
|
|
if (kid.parent) kid.parent.children = kid.parent.children.filter((k) => k !== kid);
|
|
kid.parent = this;
|
|
}
|
|
append(...kids) {
|
|
kids.forEach((k) => {
|
|
if (typeof k === "string") return;
|
|
this._detach(k);
|
|
this.children.push(k);
|
|
});
|
|
}
|
|
prepend(...kids) {
|
|
kids.reverse().forEach((k) => {
|
|
this._detach(k);
|
|
this.children.unshift(k);
|
|
});
|
|
}
|
|
appendChild(kid) {
|
|
this.append(kid);
|
|
return kid;
|
|
}
|
|
insertBefore(kid, ref) {
|
|
this._detach(kid);
|
|
const at = ref ? this.children.indexOf(ref) : -1;
|
|
if (at < 0) this.children.push(kid);
|
|
else this.children.splice(at, 0, kid);
|
|
return kid;
|
|
}
|
|
remove() {
|
|
if (this.parent) this.parent.children = this.parent.children.filter((k) => k !== this);
|
|
}
|
|
get descendants() {
|
|
return this.children.flatMap((c) => [c, ...c.descendants]);
|
|
}
|
|
/** `.이름` 한 개만 — 방향키 듣개가 슬롯 단추를 찾는 꼴(◀▶ 단추가 있으면 누름). */
|
|
querySelector(sel) {
|
|
return this.descendants.find((n) => n.classList.contains(sel.slice(1))) ?? null;
|
|
}
|
|
getClientRects() {
|
|
return this.classList.contains("is-hidden") ? [] : [{}];
|
|
}
|
|
setAttribute(name, value) {
|
|
this.attrs[name] = String(value);
|
|
if (name === "class") this.className = value;
|
|
}
|
|
getAttribute(name) {
|
|
return name === "class" ? this.className : (this.attrs[name] ?? null);
|
|
}
|
|
hasAttribute(name) {
|
|
return name in this.attrs;
|
|
}
|
|
addEventListener(type, fn) {
|
|
(this.listeners[type] ??= []).push(fn);
|
|
}
|
|
click() {
|
|
const event = { stopPropagation: () => {}, preventDefault: () => {} };
|
|
(this.listeners.click ?? []).forEach((fn) => fn(event));
|
|
}
|
|
}
|
|
const docKeys = new Set(); // 방향키 듣개(379-1 · 정확히 0 자리)
|
|
globalThis.document = {
|
|
createElement: (tag) => new FakeNode(tag),
|
|
createElementNS: (_ns, tag) => new FakeNode(tag),
|
|
addEventListener: (type, fn) => type === "keydown" && docKeys.add(fn),
|
|
removeEventListener: (type, fn) => type === "keydown" && docKeys.delete(fn),
|
|
};
|
|
globalThis.window = { setTimeout, clearTimeout, addEventListener: () => {} };
|
|
|
|
function loadTs(file) {
|
|
const full = path.resolve(file);
|
|
if (cache.has(full)) return cache.get(full);
|
|
if (full.endsWith(".json")) {
|
|
const data = JSON.parse(fs.readFileSync(full, "utf8"));
|
|
cache.set(full, data);
|
|
return data;
|
|
}
|
|
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
|
|
compilerOptions: {
|
|
module: ts.ModuleKind.CommonJS,
|
|
target: ts.ScriptTarget.ES2020,
|
|
esModuleInterop: true,
|
|
},
|
|
}).outputText;
|
|
const box = { exports: {} };
|
|
cache.set(full, box.exports);
|
|
const localRequire = (request) => {
|
|
if (STUBS[request]) return STUBS[request];
|
|
if (request.endsWith(".css")) return {};
|
|
const alias = { "@util/": "common_util", "@ui/": "ui_template", "@config/": "config" };
|
|
for (const [prefix, dir] of Object.entries(alias))
|
|
if (request.startsWith(prefix))
|
|
return loadTs(resolve(path.join(ROOT, dir, request.slice(prefix.length))));
|
|
return loadTs(resolve(path.join(path.dirname(full), request)));
|
|
};
|
|
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
|
cache.set(full, box.exports);
|
|
return box.exports;
|
|
}
|
|
const resolve = (base) =>
|
|
[base, `${base}.ts`, path.join(base, "index.ts")].find(
|
|
(p) => fs.existsSync(p) && fs.statSync(p).isFile(),
|
|
) ?? `${base}.ts`;
|
|
|
|
const B06 = (f) => path.join(ROOT, "B06_Section", `${f}.ts`);
|
|
const pick = loadTs(B06("B06_Section_UI_Cross_Design_Pick"));
|
|
const centerMod = loadTs(B06("B06_Section_UI_Cross_Center"));
|
|
const { appendCrossDesignOverlay } = loadTs(B06("B06_Section_UI_Cross_Design"));
|
|
const { computeCrossDesign } = loadTs(
|
|
path.join(ROOT, "common_util", "common_util_cross_design.ts"),
|
|
);
|
|
const standard = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
|
|
// 평지보다 조금 높은 지반 = 양쪽 절토(교차점 손잡이 없음 · 중심점 원 하나)
|
|
const samples = Array.from({ length: 49 }, (_, i) => ({
|
|
offset_m: -12 + i * 0.5,
|
|
elevation_m: 100.3,
|
|
valid: true,
|
|
}));
|
|
const design = {
|
|
...computeCrossDesign(samples, 100, { standard, groundType: "soil", sectionMode: "both_cut" }),
|
|
shoulder_extend_left_m: 0,
|
|
shoulder_extend_right_m: 0,
|
|
};
|
|
const section = { station_id: "S1", chainage_m: 20, samples, design };
|
|
const x = (o) => 300 - o * 20;
|
|
const y = (e) => 400 - (e - 100) * 20;
|
|
const layer = new FakeNode("g");
|
|
appendCrossDesignOverlay(layer, design, x, y, samples, undefined);
|
|
layer.children.forEach((el) => el.setAttribute(pick.DESIGN_LINE_ATTR, ""));
|
|
const picks = [];
|
|
const changes = [];
|
|
const moves = pick.designLineMoves(section, (_c, change) => changes.push(change));
|
|
const handle = pick.attachDesignLinePick({
|
|
layer,
|
|
design,
|
|
x,
|
|
toDisplayY: y,
|
|
onPick: (i) => picks.push(i),
|
|
...moves,
|
|
});
|
|
const centers = layer.children.filter(
|
|
(k) =>
|
|
k.classList.contains("b06-chart__center-hit") && !k.classList.contains("b06-chart__toe-hit"),
|
|
);
|
|
const center = centers[0];
|
|
center.click();
|
|
handle.setActive(picks[0]);
|
|
const row = handle.cell.children[1];
|
|
const press = (key) => {
|
|
let prevented = false;
|
|
const event = { key, target: null, preventDefault: () => (prevented = true) };
|
|
[...docKeys].forEach((fn) => fn(event));
|
|
return prevented;
|
|
};
|
|
const out = {
|
|
moves: { center: moves.center },
|
|
count: centers.length,
|
|
cx: center ? Number(center.getAttribute("cx")) : null,
|
|
x0: x(0),
|
|
active: center.classList.contains("is-active"),
|
|
title: handle.cell.children[0].textContent,
|
|
value: row.children.at(-1).textContent,
|
|
// 보이는 버튼(숨은 버튼 = 슬롯 없음)
|
|
slots: row.children
|
|
.filter((k) => /cell-btn--/.test(k.className) && !k.hidden)
|
|
.map((k) => k.className.match(/cell-btn--(\w+)/)[1]),
|
|
panelOpen: !handle.cell.classList.contains("is-empty"),
|
|
// 방향키 — ◀▶ = 먹어 막음(아무것도 안 옮김) · ▲▼ = 안 막음(종단도 듣개 = 종단 계획고)
|
|
keys: {
|
|
left: press("ArrowLeft"),
|
|
right: press("ArrowRight"),
|
|
up: press("ArrowUp"),
|
|
down: press("ArrowDown"),
|
|
},
|
|
dragListeners: (center.listeners.pointerdown ?? []).length,
|
|
changes: changes.length,
|
|
exports: Object.keys(centerMod).sort(),
|
|
};
|
|
process.stdout.write(JSON.stringify(out));
|