/* PLAN 115-7 단위검증 헬퍼 — B05 3D 성토 비탈 끝 종단 이음(공통 변형 장 · 새 ① · 제안 ②). * TS 를 그 자리에서 트랜스파일해 Node 로 돌린다. 결과는 표준출력에 JSON 한 벌. * * ⚠ 확장자가 `.cjs` 인 까닭은 `helper_b05_curve_radius_floor.cjs` 머리와 같다. */ const fs = require("fs"); const path = require("path"); const ROOT = path.join(__dirname, "..", ".."); const ts = require(path.join(ROOT, "config", "node_modules", "typescript")); const THREE = require(path.join(ROOT, "config", "node_modules", "three")); const cache = new Map(); function loadTs(file) { const full = path.resolve(file); if (cache.has(full)) return cache.get(full); const js = ts.transpileModule(fs.readFileSync(full, "utf8"), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, }).outputText; const box = { exports: {} }; cache.set(full, box.exports); const localRequire = (request) => { if (request === "three") return THREE; if (request.startsWith("@ui/") || request.startsWith("@config/")) { return new Proxy({}, { get: () => () => undefined }); } const target = request.startsWith("@util/") ? path.join(ROOT, "common_util", request.slice(6)) : path.join(path.dirname(full), request); if (request.endsWith(".json")) return JSON.parse(fs.readFileSync(target, "utf8")); return loadTs(fs.existsSync(`${target}.ts`) ? `${target}.ts` : path.join(target, "index.ts")); }; new Function("exports", "module", "require", js)(box.exports, box, localRequire); return box.exports; } const B05 = (name) => path.join(ROOT, "B05_Profile", `${name}.ts`); const { buildCorridor } = loadTs(B05("B05_Profile_UI_Corridor_Build")); const deform = loadTs(B05("B05_Profile_UI_Corridor_Deform")); const r4 = (v) => (v === null || v === undefined ? v : Math.round(v * 10000) / 10000); const HALF = 3; // 노면부 반폭(차도 2.5 + 노견 0.5) const ROAD_Z = 100; /** x 축 직선 노선 측점 — 좌 성토(1:1.5 · 4 m 아래 · 비탈 끝 9) · 우 절토(1:1 · 4 m 위 · 비탈 끝 −7). */ function station(chainage) { const pts = [ [-7, 104], [-HALF, ROAD_Z], [-2.5, ROAD_Z], [2.5, ROAD_Z], [HALF, ROAD_Z], [9, 96], ]; const samples = []; for (let o = -30; o <= 30; o += 1) samples.push({ offset_m: o, elevation_m: o > 0 ? 96 : 104 }); return { station_id: `S${chainage}`, chainage_m: chainage, label: "", kind: "regular", center_x: chainage, center_y: 0, center_z: ROAD_Z, frame: { left_xy: [0, 1] }, samples, design: { section_mode: "right_cut", road_edges: { left: { offset_m: HALF, elevation_m: ROAD_Z }, right: { offset_m: -HALF, elevation_m: ROAD_Z }, }, carriageway_edges: { left: { offset_m: 2.5, elevation_m: ROAD_Z }, right: { offset_m: -2.5, elevation_m: ROAD_Z }, }, design_line: pts.map(([offset_m, elevation_m]) => ({ offset_m, elevation_m })), ditch: { type: "none" }, ditch_enabled: false, ditch_side: "left", }, }; } const SECTIONS = [0, 20, 40, 60, 80, 100].map(station); function fieldOf(groundAt) { const build = buildCorridor(SECTIONS, [ { x: 0, y: 0 }, { x: 100, y: 0 }, ]); build.sections = SECTIONS; const field = deform.buildDeformField(build, deform.stationProfileSource(SECTIONS), groundAt); return field; } /** 행 · 측 성토 비탈 끝 열(노선 길이 순). */ function catches(field, si, ki) { return field.rows.map((row, r) => ({ ch: row.chainage_m, c: field.catchAt(r, si, ki) })); } /** 지느러미 행 — 성토 행끼리 ±3 m 중앙값 + 3 m 넘음. */ function fins(list) { let count = 0; for (const { ch, c } of list) { if (c <= 0) continue; const near = list .filter((o) => o.c > 0 && Math.abs(o.ch - ch) <= 3) .map((o) => o.c) .sort((a, b) => a - b); if (c > near[near.length >> 1] + 3) count += 1; } return count; } const wobble = (list) => { let sum = 0; for (let i = 1; i < list.length; i += 1) { if (list[i].c > 0 && list[i - 1].c > 0) sum += Math.abs(list[i].c - list[i - 1].c); } return (sum / (list[list.length - 1].ch - list[0].ch)) * 100; }; const results = {}; // ① 도로에 직각인 골짜기 — 좁은 골(49~51 m) · 넓은 골(64~72 m)에서 지반이 3 m 더 깊다. 옛 규칙은 // 골 행만 연장 구배로 10.5 m 까지 뻗어 지느러미 · 수직 벽. 새 규칙은 이웃 좋은 행(6 m)으로 보간 + // 비탈 끝 밖 2 m 덧댐. 우측 절토는 같은 골을 그대로 따른다(절토는 안 바꿈). { const gully = (x) => (x > 49 && x < 51) || (x > 64 && x < 72); const groundAt = (x, y) => { if (y >= 0) return gully(x) && y > 5 ? 93 : 96; return gully(x) && y < -5 ? 101 : 104; }; const field = fieldOf(groundAt); const fill = catches(field, 0, 0); const inGully = fill.filter((o) => gully(o.ch)); const cutGully = catches(field, 1, 1).filter((o) => gully(o.ch) && o.ch > 50 && o.ch < 50.5); const zAt = (x, s) => field.deformZ(x, HALF + s, groundAt(x, HALF + s)); results.valley = { fins: fins(fill), gullyMax: r4(Math.max(...inGully.map((o) => o.c))), gullyMin: r4(Math.min(...inGully.map((o) => o.c))), skirtMid: r4(zAt(68, 7)), // 비탈 끝(6) 밖 1 m — 96 에서 93 으로 반쯤 skirtOut: r4(zAt(68, 8.5)), // 덧댐 밖 — 원지반 그대로 outside: r4(zAt(30, 8)), // 골 밖 비탈 끝 밖 — 원지반 cutGully: r4(Math.max(...cutGully.map((o) => o.c))), cutFlat: r4( field.catchAt( field.rows.findIndex((row) => row.chainage_m >= 30), 1, 1, ), ), }; } // ② 계단 지형 — 1 m 띠마다 지반이 0.6 m 오르내린다(비탈 끝 6 ↔ 5.1). 원뿔 넓히기로 출렁임이 준다. { const step = (x) => (Math.floor(x) % 2 ? 0.6 : 0); const groundAt = (x, y) => (y >= 0 ? 96 + step(x) : 104); const field = fieldOf(groundAt); const fill = catches(field, 0, 0).filter((o) => o.ch > 1 && o.ch < 99); // 날것 출렁임 — 띠 경계마다 0.9 m(6 − 5.1) 씩. const raw = fill.map((o) => ({ ch: o.ch, c: step(o.ch) ? 5.1 : 6 })); results.steps = { wobble: r4(wobble(fill)), rawWobble: r4(wobble(raw)), fins: fins(fill) }; } // ③ ④ ⑤ 열 단위 — 못 닫힘 보간 · 측점 행 날것 · 0 행 끊음 / 짧은 틈 이음. { const n = 101; const ch = Array.from({ length: n }, (_, i) => i * 0.2); const raw = new Array(n).fill(6); const open = new Array(n).fill(0); const station = new Array(n).fill(0); const shaped = new Array(n).fill(1); for (let i = 20; i <= 24; i += 1) { raw[i] = 15; open[i] = 1; } station[50] = 1; raw[50] = 9; // 측점 행 — 튀어도 날것 station[60] = 1; raw[60] = 20; open[60] = 1; // 못 닫힘 측점 — 보간 for (let i = 70; i <= 72; i += 1) raw[i] = 0; // 짧은 틈(0.4 m) — 단면 있음 → 잇는다 for (let i = 80; i <= 82; i += 1) { raw[i] = 0; shaped[i] = 0; // 단면 없음 → 끊음 } const out = deform.smoothFillCatches(ch, raw, open, station, shaped); const gap = []; for (let i = 0; i < n; i += 1) if (raw[i] === 0 && shaped[i] === 0) gap.push(out[i]); results.column = { openMax: r4(Math.max(...out.slice(20, 25))), stationKept: r4(out[50]), stationOpen: r4(out[60]), bridged: r4(Math.min(...out.slice(70, 73))), cutGap: r4(Math.max(...gap)), neighbourStep: r4( Math.max( ...out .slice(1) .map((c, i) => (out[i] > 0 && c > 0 ? Math.abs(c - out[i]) / 0.2 : 0)) .filter((_, i) => i < 49 || i > 51), ), ), }; } process.stdout.write(JSON.stringify(results));