/* B05 패치 스커트 단위검증 헬퍼 — TS를 그 자리에서 트랜스파일해 Node로 돌린다. * (프론트에 JS 테스트 러너가 없어 pytest가 이 스크립트를 호출한다 — 2026-08-27) */ 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_Skirt.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 { buildPatchSkirts } = moduleBox.exports; /** 직선 노선 패치 리본 하나 — 바깥 열 z를 행마다 지정해 만든다. */ function ribbonOf(outerZ, options = {}) { const cols = 3; const rows = outerZ.length; 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; // x = 종방향 1m 간격 positions[i + 1] = col; // y = 횡방향(마지막 열이 바깥) positions[i + 2] = col === cols - 1 ? outerZ[row] : 100; } } return { kind: "fill", side: "left", colCount: cols, chainages: outerZ.map((_z, row) => row), positions, patch: true, ...options, }; } const results = {}; // ① 뜬 모서리 — 바깥 끝이 지반(90)보다 0.5m 위: 마디마다 판 2장씩. { const ribbon = ribbonOf([90.5, 90.5, 90.5]); const { walls, stats } = buildPatchSkirts([ribbon], () => 90); results.floating = { walls: walls.length, triangles: stats.triangles, hatched: walls.every((w) => w.uvs && w.uvs.length === (w.positions.length / 3) * 2), marked: walls.every((w) => w.patchSkirt === true), }; } // ② 잠긴 모서리 — 바깥 끝이 지반 아래: 판 없음(지형이 덮는다). { const ribbon = ribbonOf([89.5, 89.5, 89.5]); const { walls, stats } = buildPatchSkirts([ribbon], () => 90); results.submerged = { walls: walls.length, skipped: stats.skippedSubmerged }; } // ③ 구조물에서 끝나는 줄 — 소유 측점(가운데 행) 낙차 2m > 허용 0.5m: 리본 전체 제외. { const ribbon = ribbonOf([92, 92, 92]); const { walls, stats } = buildPatchSkirts([ribbon], () => 90); results.toeGate = { walls: walls.length, reaches: stats.detail[0].reachesGround }; } // ④ 낙차 상한 — 게이트는 통과(가운데 0.2m)하되 한 마디가 3m 넘게 뜨면 그 마디만 건너뜀. { const ribbon = ribbonOf([90.2, 94, 90.2, 90.2]); const { walls, stats } = buildPatchSkirts([ribbon], () => 90); results.tall = { walls: walls.length, skippedTall: stats.skippedTall, tris: stats.triangles }; } // ⑤ 비패치·절토 리본은 무시. { const plain = ribbonOf([90.5, 90.5], { patch: undefined }); const cut = ribbonOf([90.5, 90.5], { kind: "cut" }); const { walls } = buildPatchSkirts([plain, cut], () => 90); results.ignored = { walls: walls.length }; } process.stdout.write(JSON.stringify(results));