/* 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 { makeLoader } = require("./helper_ts_stub_loader.cjs"); const ROOT = path.join(__dirname, "..", ".."); // 조정창 공용 부품(Design_Pick → Cross_Panel_Base · 151-3)이 불러올 때 문서에 귀를 닮 globalThis.document = { addEventListener: () => {}, removeEventListener: () => {} }; const resolve = (base) => (fs.existsSync(`${base}.ts`) ? `${base}.ts` : base); const { loadTs } = makeLoader({ root: ROOT, requireFor: (full) => (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))); }, }); const pick = loadTs(path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Detail_Pick.ts")); // ── 그림 복제 흉내 — 평평한 요소 목록 · 「tag.class」 · 「.class」 · 「g.class line」 · 「:not(.class)」 만 ── 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 nots = [...part.matchAll(/:not\(\.([\w-]+)\)/g)].map((m) => m[1]); // 370-3 암 판정 선 뺌 const [tag, ...cls] = part.replace(/:not\([^)]*\)/g, "").split("."); return ( (!tag || el.tag === tag) && cls.every((c) => el.set.has(c)) && !nots.some((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 ground = node("g", ["b06-chart__ground-center"]); // 원지반 십자(413) const els = [ ground, 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__ground-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", "planned", "cut", "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); } // 164-2 — 그 카드에 대상이 있는 키(암석 띠 · 사토장 없는 카드 = 그 칸 빠짐) out.present = [...pick.presentKeys(fakeSvg(20, 300), design)].sort(); const bare = fakeSvg(20, 300); const kept = bare.els.filter( (el) => !el.set.has("b06-chart__area--cut_rock") && !el.set.has("b06-chart__spoil-fill"), ); bare.els.splice(0, bare.els.length, ...kept); out.presentBare = [...pick.presentKeys(bare, design)].sort(); process.stdout.write(JSON.stringify(out));