Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JywHxQuWPk6UYXTebyrBoU
135 lines
4.7 KiB
JavaScript
135 lines
4.7 KiB
JavaScript
/* PLAN 150-4 단위검증 헬퍼 — [상세] 수량표 칸 → 그림 복제 강조(`B06_Section_UI_Cross_Detail_Pick.ts`).
|
||
* 화면 TS 를 그 자리에서 트랜스파일해 Node 로 돌림 · 결과 = 표준출력 JSON 한 벌. 짝 = `test_150_detail_pick.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();
|
||
// 조정창 공용 부품(Design_Pick → Cross_Panel_Base · 151-3)이 불러올 때 문서에 귀를 닮
|
||
globalThis.document = { addEventListener: () => {}, removeEventListener: () => {} };
|
||
const resolve = (base) => (fs.existsSync(`${base}.ts`) ? `${base}.ts` : base);
|
||
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 (request.endsWith(".css")) return {};
|
||
if (request.startsWith("@util/"))
|
||
return loadTs(resolve(path.join(ROOT, "common_util", request.slice(6))));
|
||
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 pick = loadTs(path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Detail_Pick.ts"));
|
||
|
||
// ── 그림 복제 흉내 — 평평한 요소 목록 · 「tag.class」 · 「.class」 · 「g.class line」 만 ──
|
||
function node(tag, classes, attrs = {}, parent = null) {
|
||
const set = new Set(classes);
|
||
return {
|
||
tag,
|
||
parent,
|
||
set,
|
||
getAttribute: (name) => attrs[name] ?? null,
|
||
classList: {
|
||
add: (c) => set.add(c),
|
||
remove: (c) => set.delete(c),
|
||
contains: (c) => set.has(c),
|
||
},
|
||
};
|
||
}
|
||
const one = (el, part) => {
|
||
const [tag, ...cls] = part.split(".");
|
||
return (!tag || el.tag === tag) && cls.every((c) => el.set.has(c));
|
||
};
|
||
const matches = (el, selector) => {
|
||
const parts = selector.split(" ");
|
||
if (!one(el, parts.at(-1))) return false;
|
||
return parts.length === 1 || (el.parent !== null && one(el.parent, parts[0]));
|
||
};
|
||
function fakeSvg(ppm, x0) {
|
||
const center = node("g", ["b06-chart__center"]);
|
||
const els = [
|
||
center,
|
||
node("line", [], { x1: String(x0) }, center),
|
||
node("g", ["b06-chart__area", "b06-chart__area--cut_soil"]),
|
||
node("g", ["b06-chart__area", "b06-chart__area--cut_rock", "b06-chart__area--cut_rr"]),
|
||
node("g", ["b06-chart__area", "b06-chart__area--fill", "is-active"]), // 카드에서 묻어 온 강조
|
||
node("polyline", ["b06-chart__spoil-fill"]),
|
||
];
|
||
// 판정 선: offset 구간(m) → 화면 x = x0 − offset × ppm
|
||
const hit = (a, b) =>
|
||
node("polyline", ["b06-chart__design-hit"], {
|
||
points: `${x0 - a * ppm},10 ${x0 - b * ppm},20`,
|
||
});
|
||
const hits = {
|
||
road: hit(-2, 2),
|
||
ditch: hit(2.5, 3.0),
|
||
cutSlope: hit(3.0, 6.0),
|
||
fillSlope: hit(-2.5, -6.0),
|
||
};
|
||
els.push(...Object.values(hits));
|
||
return {
|
||
els,
|
||
hits,
|
||
getAttribute: (name) => (name === "data-px-per-m" ? String(ppm) : null),
|
||
querySelectorAll: (sel) =>
|
||
els.filter((el) => sel.split(",").some((s) => matches(el, s.trim()))),
|
||
querySelector: (sel) => els.find((el) => matches(el, sel)) ?? null,
|
||
};
|
||
}
|
||
const design = {
|
||
road_edges: {
|
||
left: { offset_m: 2.5, elevation_m: 99.95 },
|
||
right: { offset_m: -2.5, elevation_m: 99.95 },
|
||
},
|
||
ditch: { type: "standard", top_width_m: 0.5, bottom_width_m: 0.2, depth_m: 0.3 },
|
||
ditch_side: "left",
|
||
ditch_enabled: true,
|
||
section_mode: "left_cut",
|
||
};
|
||
const activeOf = (svg) => {
|
||
const names = [];
|
||
for (const [name, el] of Object.entries(svg.hits)) if (el.set.has("is-active")) names.push(name);
|
||
for (const el of svg.els)
|
||
if (el.set.has("is-active") && !el.set.has("b06-chart__design-hit"))
|
||
names.push(
|
||
[...el.set].find(
|
||
(c) => c.includes("--") || c === "b06-chart__center" || c === "b06-chart__spoil-fill",
|
||
),
|
||
);
|
||
return names;
|
||
};
|
||
|
||
const out = { targets: Object.keys(pick.DETAIL_TARGETS), zone: {}, active: {} };
|
||
out.zone = {
|
||
road: pick.lineZone(design, 0),
|
||
ditch: pick.lineZone(design, 2.8),
|
||
cutSlope: pick.lineZone(design, 4),
|
||
fillSlope: pick.lineZone(design, -4),
|
||
};
|
||
for (const key of [
|
||
null,
|
||
"cut_soil",
|
||
"cut_rock",
|
||
"embankment",
|
||
"spoil_fill",
|
||
"ground",
|
||
"ditch_soil",
|
||
"grading_cut",
|
||
"grading_fill",
|
||
"road_compaction",
|
||
"fence",
|
||
]) {
|
||
const svg = fakeSvg(20, 300);
|
||
pick.highlightDetail(svg, design, key);
|
||
out.active[String(key)] = activeOf(svg);
|
||
}
|
||
process.stdout.write(JSON.stringify(out));
|