/* PLAN 190-1 단위검증 헬퍼 — B06 집수정 판정면 = 부재 외곽 가상 사각형(세월교 측벽 꼴). * 화면 TS 를 그 자리에서 트랜스파일해 Node 로 돌림 · 결과 = 표준출력 JSON 한 벌. * 입력 = `fixtures/b07_cross_overlay_sections.json` 배관 측점 + design.inlet_structure 강제(U · L · I). * 짝 = `test_190_1_basin_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(); const STUBS = { "@ui/ui_template_toast": { showToast: () => undefined }, "@ui/ui_template_elements": { showToast: () => undefined }, }; // ── SVG 그릇 흉내 — 오버레이가 쓰는 만큼만 ───────────────────────────────── class FakeNode { constructor(tag) { this.tagName = String(tag).toUpperCase(); this.children = []; this.attrs = {}; this.listeners = {}; this.textContent = ""; this._classes = new Set(); const self = this; this.classList = { add: (...names) => names.forEach((n) => self._classes.add(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), }; } append(...kids) { kids.forEach((k) => this.children.push(k)); } appendChild(kid) { this.children.push(kid); return kid; } setAttribute(name, value) { this.attrs[name] = String(value); if (name === "class") this._classes = new Set(String(value).split(/\s+/).filter(Boolean)); } getAttribute(name) { return this.attrs[name] ?? null; } addEventListener(type, fn) { (this.listeners[type] ??= []).push(fn); } click() { let stopped = false; const event = { stopPropagation: () => (stopped = true), preventDefault: () => {} }; (this.listeners.click ?? []).forEach((fn) => fn(event)); return stopped; } } globalThis.document = { createElement: (tag) => new FakeNode(tag), createElementNS: (_ns, tag) => new FakeNode(tag), }; 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]; 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 { computeStoredLayouts } = loadTs(B06("B06_Section_Structure_Layouts")); const { appendCulvertOverlay } = loadTs(B06("B06_Section_UI_Cross_Culvert")); const { outlineBox } = loadTs(B06("B06_Section_UI_Cross_Culvert_Types")); const fixture = JSON.parse( fs.readFileSync(path.join(__dirname, "fixtures", "b07_cross_overlay_sections.json"), "utf8"), ); const sections = fixture.cross_sections; const pipeSection = sections.find((s) => s.culvert && !s.culvert.hidden_pipe); const BASIN_TIP = "유입 집수정 선택 — 조정창에서 구조물 형식 선택"; const X = (o) => -o * 10; const Y = (e) => -e * 10; const pointsOf = (node) => node.attrs.points.split(" ").map((pair) => pair.split(",").map(Number)); function basinCase(shape) { const target = { ...pipeSection, design: { ...pipeSection.design, inlet_structure: shape } }; const layout = computeStoredLayouts(target, sections).culvert; const layer = new FakeNode("g"); const picked = []; const setter = appendCulvertOverlay(layer, layout, X, Y, (key) => picked.push(key)); const hits = layer.children.filter((n) => n.classList.contains("b06-chart__culvert-revet-hit")); const basinHits = hits.filter((n) => n.children[0]?.textContent === BASIN_TIP); const all = layout.basin.parts.flatMap((part) => part.points); // 기대 외곽(화면 좌표) — 부재 전 꼭짓점의 최소 · 최대 const xs = all.map((p) => X(p.offset)); const ys = all.map((p) => Y(p.elevation)); const expect = { minX: Math.min(...xs), maxX: Math.max(...xs), minY: Math.min(...ys), maxY: Math.max(...ys), }; const got = basinHits.length ? pointsOf(basinHits[0]) : []; const gotBox = got.length ? { minX: Math.min(...got.map(([px]) => px)), maxX: Math.max(...got.map(([px]) => px)), minY: Math.min(...got.map(([, py]) => py)), maxY: Math.max(...got.map(([, py]) => py)), } : null; const stopped = basinHits.length ? basinHits[0].click() : false; // 그림 모양은 그대로 — 집수정 부재 폴리곤 = 부재 점열 그대로 · 고르면 부재 강조 const drawn = layer.children.filter((n) => n.classList.contains("b06-chart__culvert-basin")); const drawnSame = drawn.every( (n, i) => n.attrs.points === layout.basin.parts[i].points.map((p) => `${X(p.offset)},${Y(p.elevation)}`).join(" "), ); setter("inlet"); const activeOnInlet = drawn.every((n) => n.classList.contains("is-active")); return { shape: layout.basin.shape, parts: layout.basin.parts.length, basinHits: basinHits.length, corners: got.length, expect, got: gotBox, picked, stopped, lastHitIsBasin: hits[hits.length - 1] === basinHits[0], drawnParts: drawn.length, drawnSame, activeOnInlet, }; } const out = { U: basinCase("U"), L: basinCase("L"), I: basinCase("I") }; // 세월교 측벽과 같은 함수 — ㄴ 점열의 외곽 const ell = [ { offset: 0, elevation: 0 }, { offset: 0.3, elevation: 0 }, { offset: 0.3, elevation: 2 }, { offset: 1.5, elevation: 2 }, { offset: 1.5, elevation: 2.3 }, { offset: 0, elevation: 2.3 }, ]; out.outline = outlineBox(ell); // 판정면 없으면(onSelect 없음 · B07) 집수정 판정면도 없음 { const target = { ...pipeSection, design: { ...pipeSection.design, inlet_structure: "U" } }; const layout = computeStoredLayouts(target, sections).culvert; const layer = new FakeNode("g"); appendCulvertOverlay(layer, layout, X, Y); out.noSelectHits = layer.children.filter((n) => n.classList.contains("b06-chart__culvert-revet-hit"), ).length; } process.stdout.write(JSON.stringify(out));