/* PLAN 306 단위검증 헬퍼 — 맹암거 돌 무늬(306-1) · 고르기 차례(306-2). * 화면 TS 를 그 자리에서 트랜스파일해 Node 로 돌림 · 결과 = 표준출력 JSON 한 벌. * 짝 = `test_306_underdrain_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 load = (file) => { if (cache.has(file)) return cache.get(file); const js = ts.transpileModule(fs.readFileSync(file, "utf8"), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, }).outputText; const box = { exports: {} }; cache.set(file, box.exports); const near = (m) => { if (m.startsWith("@util/")) return load(path.join(ROOT, "common_util", `${m.slice(6)}.ts`)); if (m.startsWith(".")) return load(path.join(path.dirname(file), `${m}.ts`)); return new Proxy({}, { get: () => () => {} }); }; new Function("exports", "module", "require", js)(box.exports, box, near); return box.exports; }; /* ── 흉내 DOM ─────────────────────────────────────────────────────────── */ class Node { constructor(tag, name = tag) { this.tag = tag; this.name = name; this.attrs = {}; this.children = []; this.listeners = []; const self = this; this.classList = { contains: (c) => (self.attrs.class ?? "").split(/\s+/).includes(c), add: (c) => (self.attrs.class = `${self.attrs.class ?? ""} ${c}`.trim()), }; } setAttribute(k, v) { this.attrs[k] = String(v); } getAttribute(k) { return this.attrs[k] ?? null; } hasAttribute(k) { return k in this.attrs; } /** `[속성]` 꼴만 — 형상 고름이 쓰는 것. */ querySelectorAll(sel) { const attr = /^\[([\w-]+)\]$/.exec(sel)[1]; return this.children.flatMap((k) => [ ...(k.hasAttribute(attr) ? [k] : []), ...k.querySelectorAll(sel), ]); } append(...kids) { kids.forEach((k) => { const at = this.children.indexOf(k); if (at >= 0) this.children.splice(at, 1); this.children.push(k); }); } prepend(...kids) { kids.forEach((k) => { const at = this.children.indexOf(k); if (at >= 0) this.children.splice(at, 1); }); this.children.unshift(...kids); } insertBefore(k, ref) { const at = this.children.indexOf(k); if (at >= 0) this.children.splice(at, 1); const to = ref ? this.children.indexOf(ref) : -1; this.children.splice(to >= 0 ? to : this.children.length, 0, k); } cloneNode() { const copy = new Node(this.tag, `${this.name}+edge`); copy.attrs = { ...this.attrs }; return copy; } addEventListener(type, fn) { this.listeners.push({ type, fn }); } dispatchEvent(event) { this.listeners.filter((l) => l.type === event.type).forEach((l) => l.fn(event)); return true; } } globalThis.document = { createElementNS: (_ns, tag) => new Node(tag) }; globalThis.MouseEvent = class { constructor(type) { this.type = type; this.clientX = 0; this.clientY = 0; } stopPropagation() {} }; const clickEvent = () => ({ type: "click", clientX: 0, clientY: 0, stopPropagation() {} }); const draw = load(path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Underdrain.ts")); const shapePick = load(path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Shape_Pick.ts")); const order = load(path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Pick_Order.ts")); const out = {}; /* ── 306-1 돌 무늬 ────────────────────────────────────────────────────── */ // 288 시험과 같은 단면 — 좌 절토 · 우 성토 · 노면 6 m · 암거 높이 1 m · 1 m = 20 px const design = { road_edges: { left: { offset_m: 3, elevation_m: 100.15 }, right: { offset_m: -3, elevation_m: 99.85 }, }, design_line: [ { offset_m: 6, elevation_m: 102.9 }, { offset_m: 3, elevation_m: 100.15 }, { offset_m: -3, elevation_m: 99.85 }, { offset_m: -7, elevation_m: 97.2 }, ], }; const PPM = 20; const X = (o) => 300 - o * PPM; const Y = (e) => 2100 - e * PPM; const svg = new Node("g", "layer"); const group = draw.appendUnderdrainOverlay( svg, { structure_id: "ud-1", chainage_m: 40, height_m: 1, slope_pct: 5 }, design, X, Y, ); const stones = group.children.find((k) => k.tag === "g").children; const rotate = (s) => /rotate\(([-\d.]+) ([-\d.]+) ([-\d.]+)\)/.exec(s.attrs.transform); out.stones = stones.map((s) => { const [, deg, cx, cy] = rotate(s).map(Number); return { cls: s.attrs.class, deg, cx, cy, w: Number(s.attrs.width), h: Number(s.attrs.height), }; }); out.groupClass = group.attrs.class; /* ── 306-2 고르기 차례 ────────────────────────────────────────────────── */ const poly = (name, cls, pts) => { const n = new Node("polygon", name); n.setAttribute("class", cls); n.setAttribute("points", pts.map((p) => p.join(",")).join(" ")); return n; }; const box = (x0, y0, x1, y1) => [ [x0, y0], [x1, y0], [x1, y1], [x0, y1], ]; // 꽂힌 옛 차례(카드와 같음) — 지면선 · 면적 띠 · 포장 · 맹암거 무리 · 관 판정면 · 보라 선 const layer = new Node("g", "plot"); const band = new Node("g", "band"); band.setAttribute("class", "b06-chart__area b06-chart__area--fill"); band.append(poly("bandPoly", "", box(300, 100, 460, 160))); const body = group.children.find((k) => k.attrs.class === "b06-chart__underdrain-body"); const bodyPts = body.attrs.points.split(" ").map((p) => p.split(",").map(Number)); layer.append( band, group, poly("bigPipe", "b06-chart__culvert-revet-hit", box(100, 111, 600, 117)), // 맹암거(≈2600 px²)보다 넓음 poly("smallPipe", "b06-chart__culvert-revet-hit", box(340, 110, 346, 114)), ); const picked = []; shapePick.attachShapePick(layer, (id) => picked.push(id)); layer.children.find((k) => k.classList.contains("b06-chart__shape-hit")).name = "underdrain"; const line = new Node("polyline", "road"); line.setAttribute("class", "b06-chart__design-hit"); line.setAttribute("points", `${X(3)},${Y(100.15)} ${X(-3)},${Y(99.85)}`); layer.append(line); // 옛 차례(306 전) — 형상 판정면이 넓이 순으로 면 판정 사이에 섬 const SURF = ["b06-chart__culvert-revet-hit", "b06-chart__shape-hit"]; const area = (n) => order.pointsArea(n.attrs.points ?? null); const oldKids = [...layer.children]; const oldOrder = [ ...oldKids.filter((k) => !SURF.some((c) => k.classList.contains(c)) && k !== line), ...oldKids .filter((k) => SURF.some((c) => k.classList.contains(c))) .sort((a, b) => area(b) - area(a)), line, ]; order.stackPickTargets(layer); out.order = layer.children.map((k) => k.name); /* 누른 자리 → 맨 위에서 받는 것(흉내) — 그림 전용(맹암거 무리)은 안 받음 */ const inside = (pts, [x, y]) => pts.reduce((inn, [ax, ay], i) => { const [bx, by] = pts[(i + 1) % pts.length]; return ay > y !== by > y && x < ax + ((y - ay) * (bx - ax)) / (by - ay) ? !inn : inn; }, false); const segGap = ([px, py], [ax, ay], [bx, by]) => { const dx = bx - ax; const dy = by - ay; const t = Math.max(0, Math.min(1, ((px - ax) * dx + (py - ay) * dy) / (dx * dx + dy * dy || 1))); return Math.hypot(px - ax - t * dx, py - ay - t * dy); }; const ptsOf = (n) => n.attrs.points.split(" ").map((p) => p.split(",").map(Number)); const ringGap = (pts, p) => Math.min(...pts.map((a, i) => segGap(p, a, pts[(i + 1) % pts.length]))); function receiver(kids, p) { for (const k of [...kids].reverse()) { if (k.classList.contains("b06-chart__underdrain")) continue; // pointer-events: none if (k.classList.contains("b06-chart__design-hit")) { const pts = ptsOf(k); if (segGap(p, pts[0], pts[1]) <= 5) return k.name; } else if (k.classList.contains("b06-chart__shape-hit--edge")) { if (ringGap(ptsOf(k), p) <= 4) return k.name; } else if (k.classList.contains("b06-chart__area")) { if (k.children.some((c) => inside(ptsOf(c), p))) return k.name; } else if (k.attrs.points && inside(ptsOf(k), p)) return k.name; } return null; } // 맹암거 몸 = (240,97) (360,103) (392.7,124.6) (263.1,118.2) · 띠 = x 300 부터 · 큰 관 = y 111~117 const PROBES = { bandInUnderdrain: [330, 107], // 띠 · 맹암거 겹침(관 밖) pipeInUnderdrain: [330, 114], // 큰 관 · 띠 · 맹암거 겹침 smallPipe: [343, 112], underdrainOnly: [280, 107], // 맹암거만 border: [320, 121], // 아랫변 위 · 띠 안 road: [300, 100], // 노면 = 맹암거 윗변 · 보라 선 }; out.probesNew = Object.fromEntries( Object.entries(PROBES).map(([k, p]) => [k, receiver(layer.children, p)]), ); out.probesOld = Object.fromEntries( Object.entries(PROBES).map(([k, p]) => [k, receiver(oldOrder, p)]), ); // 테두리 누름 = 안쪽 판정면 창구 → 맹암거 id const edge = layer.children.find((k) => k.classList.contains("b06-chart__shape-hit--edge")); edge.listeners.filter((l) => l.type === "click").forEach((l) => l.fn(clickEvent())); out.picked = picked; out.body = bodyPts; out.edgeCount = layer.children.filter((k) => k.classList.contains("b06-chart__shape-hit--edge"), ).length; process.stdout.write(JSON.stringify(out));