/* PLAN 295 단위검증 헬퍼 — 횡단 카드 고르기 차례 · [상세] 모달 고르기 차례 · 첫 보기 · 구조물 선 안 나눔. * 화면 TS 를 그 자리에서 트랜스파일해 Node 로 돌림 · 결과 = 표준출력 JSON 한 벌. * 짝 = `test_295_pick_order_detail.py`. */ const fs = require("fs"); const path = require("path"); const { makeLoader } = require("./helper_ts_stub_loader.cjs"); const ROOT = path.join(__dirname, "..", ".."); const B06 = path.join(ROOT, "B06_Section"); const common = fs.readFileSync(path.join(B06, "B06_Section_UI_Section_Common.ts"), "utf8"); const CROSS_PAD = Function(`return ${/export const CROSS_PAD = (\{[^}]*\})/.exec(common)[1]}`)(); globalThis.window = { sessionStorage: new Map(), localStorage: new Map() }; globalThis.document = { addEventListener: () => {}, removeEventListener: () => {} }; const STUBS = { "@ui/ui_template_context_menu": { createMapContextMenu: () => ({}) }, "./B06_Section_UI_Section_Common": { CROSS_PAD }, }; const resolve = (base) => (fs.existsSync(`${base}.ts`) ? `${base}.ts` : base); const { loadTs } = makeLoader({ root: ROOT, cache: false, requireFor: (full) => (request) => { if (STUBS[request]) return STUBS[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 P = loadTs(path.join(B06, "B06_Section_UI_Cross_Pick_Order.ts")); const G = loadTs(path.join(B06, "logic/B06_Section_UI_Cross_Detail_Geom.ts")); const S = loadTs(path.join(B06, "B06_Section_UI_Cross_Detail_Select.ts")); const H = loadTs(path.join(B06, "B06_Section_UI_Cross_Detail_Home.ts")); const out = {}; /* ── 카드: 흉내 층 ───────────────────────────────────────────────────── */ function node(name, classes, points) { return { name, parent: null, tagName: points && classes.includes("b06-chart__design-hit") ? "polyline" : "polygon", classList: { contains: (c) => classes.includes(c) }, getAttribute: (a) => (a === "points" ? points : null), }; } function layer(kids) { const listeners = []; const self = { children: [], append: (...nodes) => nodes.forEach((n) => { const at = self.children.indexOf(n); if (at >= 0) self.children.splice(at, 1); self.children.push(n); }), addEventListener: (type, fn, capture) => listeners.push({ type, fn, capture }), fire: (type, event) => { let stopped = false; const full = { ...event, stopPropagation: () => (stopped = true) }; listeners.filter((l) => l.type === type && l.capture).forEach((l) => l.fn(full)); return stopped; }, }; self.append(...kids); return self; } const box = (x0, y0, x1, y1) => `${x0},${y0} ${x1},${y0} ${x1},${y1} ${x0},${y1}`; const HIT = "b06-chart__culvert-revet-hit"; // 꽂힌 옛 차례 — 면적 띠(맨 아래) · 보라 선(구조물 판정면 앞) · 형상 · 도수로 띠 · 벽 · 관 const kids = [ node("band", ["b06-chart__area"], box(0, 0, 600, 300)), node("slope", ["b06-chart__design-hit"], "400,100 500,200"), node("top", ["b06-chart__design-hit"], "300,100 400,100"), node("shape", ["b06-chart__shape-hit"], box(300, 100, 310, 110)), // 100 node("chute", [HIT, "b06-chart__chute-hit"], box(390, 90, 510, 210)), // 14400 — 성토 사면을 덮음 node("wall", [HIT], box(200, 100, 220, 300)), // 4000 node("pipe", [HIT], box(250, 250, 255, 255)), // 25 ]; const L = layer(kids); P.stackPickTargets(L); out.cardOrder = L.children.map((n) => n.name); // 누른 자리에서 맨 위 판정(뒤에서부터) — 다각형 안 · 선은 5 px 안 const inside = (p, pts) => { let inn = false; for (let i = 0, j = pts.length - 1; i < pts.length; j = i++) { const [ax, ay] = pts[i]; const [bx, by] = pts[j]; if (ay > p[1] !== by > p[1] && p[0] < ((bx - ax) * (p[1] - ay)) / (by - ay) + ax) inn = !inn; } return inn; }; const gap = (p, [a, b]) => { const d = [b[0] - a[0], b[1] - a[1]]; const t = Math.max( 0, Math.min(1, ((p[0] - a[0]) * d[0] + (p[1] - a[1]) * d[1]) / (d[0] ** 2 + d[1] ** 2)), ); return Math.hypot(p[0] - a[0] - t * d[0], p[1] - a[1] - t * d[1]); }; const ptsOf = (n) => n .getAttribute("points") .split(" ") .map((q) => q.split(",").map(Number)); const topAt = (p, order) => [...order] .reverse() .find((n) => n.classList.contains("b06-chart__design-hit") ? gap(p, ptsOf(n)) <= 5 : inside(p, ptsOf(n)), )?.name; out.slopeClick = topAt([450, 150], L.children); // 띠 안 성토 사면 위 out.slopeClickOld = topAt([450, 150], kids); // 옛 차례 out.chuteClick = topAt([480, 120], L.children); // 띠 안 · 선 밖 out.pipeOverWall = topAt([252, 252], [...L.children]); out.area = P.pointsArea(box(0, 0, 10, 20)); // 끌기 = 고름 아님 L.fire("pointerdown", { clientX: 10, clientY: 10 }); out.dragStopped = L.fire("click", { clientX: 20, clientY: 10 }); L.fire("pointerdown", { clientX: 10, clientY: 10 }); out.clickStopped = L.fire("click", { clientX: 11, clientY: 11 }); /* ── 모달: 고르기 차례 ───────────────────────────────────────────────── */ const sq = (x0, y0, x1, y1) => [ [x0, y0], [x1, y0], [x1, y1], [x0, y1], ]; const big = sq(0, 0, 100, 100); const small = sq(40, 40, 60, 60); const edges = (ring) => ring.map((p, i) => [p, ring[(i + 1) % ring.length]]); const paths = [...edges(big), ...edges(small)]; const pieces = paths.map((points) => ({ line: "design", points })); const rings = [big, small]; const areas = [10000, 400]; const pick = (p, band = null) => S.pickAt(p, 1, paths, rings, areas, pieces, band); out.modal = { line: pick([50, 41]), // 작은 면 둘레 1 px small: pick([50, 50]), big: pick([20, 20]), bandSmaller: pick([50, 50], 100), bandBigger: pick([50, 50], 5000), bandBigFace: pick([20, 20], 5000), bandOnly: S.pickAt([500, 500], 1, paths, rings, areas, pieces, 50), lineOverBand: pick([50, 41], 1), }; /* ── 모달: 첫 보기 ───────────────────────────────────────────────────── */ const plot = { x: 50, y: 10, width: 500, height: 280 }; const center = [300, 150]; const drawn = [ [100, 120], [500, 140], [300, 360], // 아래로 210 — 큰 쪽 [200, 100], [5000, 9000], // 가로 창 밖 = 셈 안 함 ]; const home = H.detailHomeView(drawn, center, plot, 1, 16); const map = ([x, y]) => [home.tx + home.scale * x, home.ty + home.scale * y]; out.home = { scale: home.scale, center: map(center), plotCenter: [plot.x + plot.width / 2, plot.y + plot.height / 2], fits: drawn.slice(0, 4).every(([x, y]) => { const [sx, sy] = map([x, y]); return ( sx >= plot.x - 1e-6 && sx <= plot.x + plot.width + 1e-6 && sy >= plot.y - 1e-6 && sy <= plot.y + plot.height + 1e-6 ); }), flat: H.detailHomeView([[300, 150]], center, plot, 2, 16).scale, }; /* ── 모달: 구조물 선 안 나눔 ─────────────────────────────────────────── */ // 상자(구조물 네 변 · 변마다 묶음) + 가로지르는 원지반선 const boxLines = edges(sq(0, 0, 10, 10)).map((points, group) => ({ line: "structure", points, group, })); const split = G.splitAtCrossings([ ...boxLines, { line: "ground", points: [ [-5, 5], [15, 5], ], }, ]); const left = split.findIndex( (p) => p.line === "structure" && p.points.every(([x]) => Math.abs(x) < 1e-9), ); const members = S.groupMembers(split, left); const chosen = S.toggleMembers([], members, false); out.structure = { pieces: split.length, groundPieces: split.filter((p) => p.line === "ground").length, members: members.length, chosen: chosen.length, again: S.toggleMembers(chosen, members, false).length, additiveOff: S.toggleMembers([...chosen, 99], members, true), label: S.selectionLabel( undefined, chosen.map((i) => split[i]), ), faces: G.findFaces(split) .map((f) => Math.round(f.area * 100) / 100) .sort(), // 옛 저장 조각(구조물 선 아래 반쪽) — 그대로 다시 고름 oldPick: split.filter((p) => S.samePiece(p, { line: "structure", start: [0, 0], end: [0, 5] })) .length, wholeValue: G.piecesValue( "length", chosen.map((i) => G.pieceSource(split[i])), {}, ), }; // collectPieces — 구조물 선(설계선 표식 없음)에 묶음 · 설계선은 없음 const axes = { widthPx: 600, heightPx: 300, pixelsPerMeter: 10, maxOffset: 20, displayMax: 120, elevationMid: 110, exaggeration: 1, }; const px = ([offset, elevation]) => [ CROSS_PAD.left + (axes.maxOffset - offset) * axes.pixelsPerMeter, CROSS_PAD.top + (axes.displayMax - (axes.elevationMid + (elevation - axes.elevationMid) * axes.exaggeration)) * axes.pixelsPerMeter, ].join(","); const el = (classes, points, attrs = {}) => ({ tagName: "polyline", classList: { contains: (name) => classes.includes(name) }, hasAttribute: (name) => name in attrs, getAttribute: (name) => (name === "points" ? points.map(px).join(" ") : (attrs[name] ?? null)), }); const collected = S.collectPieces( [ el( ["b06-chart__design-cross"], [ [-10, 110], [10, 110], ], { "data-design-line": "" }, ), el( ["b06-chart__design-cross"], [ [0, 105], [0, 115], ], ), ], axes, ); out.collected = collected.map((p) => ({ line: p.line, group: p.group ?? null })); process.stdout.write(JSON.stringify(out));