/* B05 잘린 자리 윤곽(`cut-merged`) 단위검증 헬퍼 — `_Corridor_Cut.ts`를 그 자리에서 * 트랜스파일해 Node로 돌린다(타입 import뿐이라 의존 없음). 2026-09-02 */ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); const source = fs.readFileSync( path.join(__dirname, "..", "..", "B05_Profile", "B05_Profile_UI_Corridor_Cut.ts"), "utf8", ); const js = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, }).outputText; const moduleBox = { exports: {} }; new Function("exports", "module", "require", js)(moduleBox.exports, moduleBox, require); const { maskFillByPlanCurves } = moduleBox.exports; /** 직선 노선 성토 리본 — x = 종방향(1m/행), y = 횡방향(1m/열), z = 0. */ function ribbonOf(rows, cols) { const positions = new Float64Array(rows * cols * 3); for (let row = 0; row < rows; row += 1) { for (let col = 0; col < cols; col += 1) { const i = (row * cols + col) * 3; positions[i] = row; positions[i + 1] = col; positions[i + 2] = 0; } } return { kind: "fill", side: "left", colCount: cols, chainages: Array.from({ length: rows }, (_v, row) => row), positions, groundZ: new Float64Array(rows * cols), }; } /** 절취 영역 — 직사각형 x∈[x0,x1], y∈[y0,y1] (닫힘점 포함). */ function rectCurve(x0, x1, y0, y1) { return { setChainageM: (x0 + x1) / 2, source: "slope-projected", role: "inlet", side: "left", planZ: 10, loops: [ [ [x0, y0, 10], [x1, y0, 10], [x1, y1, 10], [x0, y1, 10], [x0, y0, 10], ], ], }; } function insideLoop(loop, x, y) { let hit = false; for (let i = 0, j = loop.length - 1; i < loop.length; j = i, i += 1) { const [xi, yi] = loop[i]; const [xj, yj] = loop[j]; if (yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi) hit = !hit; } return hit; } function run(rows, cols, curve) { const ribbon = ribbonOf(rows, cols); const { ribbons, curves } = maskFillByPlanCurves([ribbon], [curve]); const cut = ribbons[0]; const merged = curves.filter((c) => c.source === "cut-merged"); const loops = merged.flatMap((c) => c.loops); let masked = 0; let covered = 0; const uncovered = []; if (cut.cellMask) { for (let row = 0; row < rows - 1; row += 1) { for (let col = 0; col < cols - 1; col += 1) { if (!cut.cellMask[row * (cols - 1) + col]) continue; masked += 1; const cx = row + 0.5; const cy = col + 0.5; if (loops.some((loop) => insideLoop(loop, cx, cy))) covered += 1; else uncovered.push([row, col]); } } } return { masked, covered, uncovered, loops: loops.map((loop) => ({ n: loop.length, closed: loop.length > 1 && loop[0][0] === loop[loop.length - 1][0] && loop[0][1] === loop[loop.length - 1][1], points: loop.map(([x, y]) => [x, y]), })), }; } const out = { // 직사각형 — 시작 모서리(첫 행 첫 열)가 살아야 4모서리 + 닫힘점 = 5. rect: run(12, 6, rectCurve(1.9, 7.1, -0.5, 3.4)), // 한 셀짜리 — 가장 작은 고리도 4모서리를 다 지킨다. single: run(6, 4, rectCurve(1.9, 3.1, 0.9, 2.1)), }; process.stdout.write(JSON.stringify(out));