/* PLAN 259 단위검증 헬퍼 — [상세] 공용 팬 누름 풂 · 암 경계선 나눔 · 닫힌 영역 · 표 칸 → 그래프 고름. * 화면 TS 를 그 자리에서 트랜스파일해 Node 로 돌림. 입력 = 표준입력 JSON {design, samples, axes, rock} · * 결과 = 표준출력 JSON 한 벌. 짝 = `test_259_detail_region.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 Z = loadTs(path.join(ROOT, "A00_Common", "b_svg_zoom_pan.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 T = loadTs(path.join(B06, "B06_Section_UI_Cross_Detail_Table.ts")); const { design, samples, axes } = JSON.parse(fs.readFileSync(0, "utf8")); const out = {}; /* ① 공용 팬 — 가운데 단추 팬(click 없음) 뒤 왼쪽 누름 click 이 안 삼켜짐 */ function panCase(panStart) { const listeners = {}; const svg = { addEventListener: (name, fn) => (listeners[name] ??= []).push(fn), setAttribute: () => {}, classList: { add: () => {}, remove: () => {} }, setPointerCapture: () => {}, releasePointerCapture: () => {}, getBoundingClientRect: () => ({ left: 0, top: 0, width: 600, height: 300 }), }; const layer = { setAttribute: () => {} }; Z.attachSvgZoomPan({ svg, layer, widthPx: 600, heightPx: 300, pad: { left: 10, right: 10, top: 10, bottom: 10 }, content: { x: -600, y: -300, width: 1800, height: 900 }, panStart, }); const fire = (name, init = {}) => { let stopped = false; const event = { button: 0, clientX: 0, clientY: 0, pointerId: 1, preventDefault: () => {}, stopPropagation: () => (stopped = true), ...init, }; (listeners[name] ?? []).forEach((fn) => fn(event)); return stopped; }; const drag = (button) => { fire("pointerdown", { button, clientX: 100, clientY: 100 }); fire("pointermove", { button, clientX: 140, clientY: 120 }); fire("pointerup", { button, clientX: 140, clientY: 120 }); }; const click = (x = 50) => { fire("pointerdown", { clientX: x, clientY: 50 }); fire("pointerup", { clientX: x, clientY: 50 }); return fire("click", { clientX: x, clientY: 50 }); }; return { drag, click, fire }; } { const middle = panCase(); middle.drag(1); // 가운데 단추 팬 — 브라우저는 click 을 안 보냄(auxclick) out.panMiddle = { firstClickStopped: middle.click(), secondClickStopped: middle.click() }; // 거리이정표 꼴 — 빈 곳 왼쪽 끌기 팬(끌기로 끝난 click 은 막고 · 다음 누름은 통과) const left = panCase((event) => event.button === 1 || event.button === 0); left.drag(0); // 끌기 끝 click(왼쪽은 브라우저가 보냄) — 막혀야 함 out.panLeft = { afterDragStopped: left.fire("click"), nextClickStopped: left.click() }; } /* ② 암 경계선 — 그림 요소 → 조각(설계선과 만나는 점에서 나눔) */ 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 designPts = design.design_line.map((p) => [p.offset_m, p.elevation_m]); const groundPts = samples.filter((s) => s.valid !== false).map((s) => [s.offset_m, s.elevation_m]); const rockPts = G.rockRuns(design, samples)[0]; const elements = [ el(["b06-chart__design-cross"], designPts, { "data-design-line": "" }), el(["b06-chart__cross-profile"], groundPts), el(["b06-chart__rock-boundary"], rockPts), ]; const pieces = S.collectPieces(elements, axes); out.pieces = pieces; const rockPieces = pieces.filter((p) => p.line === "rock"); out.rockLabel = S.selectionLabel(design, [rockPieces[1]]); out.rockSource = S.selectionSource("length", [rockPieces[1]]); out.rockLocal = T.localCellValue(out.rockSource, design, samples); out.rockLength = G.pathLength(rockPieces[1].points); /* ③ 닫힌 영역(259-2) — 면 찾기 · 열린 영역 제외 · 영역 고름 값 · 붙은 두 영역 = 맞닿은 변 뺌 */ const faces = G.findFaces(pieces); out.faces = faces.map((face) => ({ area: face.area, lines: face.edges.map((i) => pieces[i].line).sort(), })); const one = faces.map((face) => S.regionLabel( [face], face.edges.map((i) => pieces[i]), ), ); out.faceLabels = one; const union = S.regionEdges(faces); const unionPieces = union.map((i) => pieces[i]); out.union = { lines: unionPieces.map((p) => p.line).sort(), label: S.regionLabel(faces, unionPieces), selectLabel: S.selectionLabel(design, unionPieces), source: S.selectionSource("area", unionPieces), }; out.union.local = T.localCellValue(out.union.source, design, samples); // 열린 원지반만 = 면 없음 out.openFaces = G.findFaces(pieces.filter((p) => p.line === "ground")).length; // 정사각형 하나 + 매달린 꼬리 — 꼬리 지우고 면 하나(넓이 1) out.square = G.findFaces([ { line: "ground", points: [ [0, 0], [1, 0], ], }, { line: "design", points: [ [1, 0], [1, 1], [0, 1], ], }, { line: "rock", points: [ [0, 1], [0, 0], ], }, { line: "spoil", points: [ [1, 1], [3, 3], ], }, ]).map((face) => ({ area: face.area, edges: face.edges.sort() })); /* ④ 표 칸 → 그래프(259-2) — 저장된 조각 짝 · 칸 그래프 짝 · 띠 값 글 */ const stored = G.pieceSource(rockPieces[1]); out.same = { forward: S.samePiece(rockPieces[1], stored), reverse: S.samePiece(rockPieces[1], { ...stored, start: stored.end, end: stored.start }), otherLine: S.samePiece(rockPieces[1], { ...stored, line: "ground" }), otherPiece: S.samePiece(rockPieces[0], stored), }; const table = { cells: { fence: out.rockSource, cut_soil: { src: "area", key: "cut_rock" }, "r:r1:0": { src: "text", text: "글" }, }, rows: [], columns: [], }; out.cellGraph = ["fence", "cut_soil", "r:r1:0", "embankment"].map((key) => T.cellGraph(table, key)); out.bandLabel = [ T.bandLabel("cut_soil", { cut_soil_area_m2: 1.234 }), T.bandLabel("cut_soil", {}), T.bandLabel("ground", { cut_soil_area_m2: 1 }), ]; /* ⑤ 표 강조 바깥 한 줄(259-3) — 합친 묶음 이름 칸(두 줄) + 둘째 줄 이름 · 값 = ㄴ 모양 */ const P = loadTs(path.join(B06, "B06_Section_UI_Cross_Detail_Pick.ts")); out.outline = P.unionOutline([ [0, 0, 10, 20], [10, 10, 20, 20], [20, 10, 30, 20], ]); process.stdout.write(JSON.stringify(out));