/* 426 종단 직선 판정 한 벌 단위검증 헬퍼 — `B05_Profile_UI_Profile_Alignment.ts` · `_Straighten.ts` · * `_Pivot.ts` · `_Profile_Straight.ts` · `design_profile_free.ts` 를 그 자리에서 트랜스파일해 돌린다. * 붙은 측점(100/100.4 · 120/120.25) 직선화 · 끝점 꼭짓점 아님 · 자유지정 R · 기울이기 누름 쌓임 · * 편집 측점 수 · 0 오프셋 정리 + 파이썬 거울 입력(base · edits · TS 곡선)을 JSON 으로 낸다. */ const fs = require("fs"); const path = require("path"); const { makeLoader } = require("./helper_ts_stub_loader.cjs"); const ROOT = path.join(__dirname, "..", ".."); const { loadTs } = makeLoader({ root: ROOT, }); const LOGIC = path.join(ROOT, "B05_Profile", "logic"); const align = loadTs(path.join(LOGIC, "B05_Profile_UI_Profile_Alignment.ts")); const straight = loadTs(path.join(LOGIC, "B05_Profile_UI_Profile_Straighten.ts")); const pivot = loadTs(path.join(LOGIC, "B05_Profile_UI_Profile_Pivot.ts")); const judge = loadTs(path.join(LOGIC, "B05_Profile_Profile_Straight.ts")); const free = loadTs(path.join(ROOT, "A00_Common", "design", "logic", "design_profile_free.ts")); const policy = { station_interval_m: 20, curve_radius_ratio: 1, curve_tangent_max_ratio: 0.5, curve_skip_legal_exception: false, default_curve_length_m: 15, default_curve_radius_m: 300, balance_tolerance_percent: 10, edit_step_m: 0.1, grade_violation_policy: "warn", max_grade_pct: 14, curve_skip_delta_pct: 0, paved: true, }; const regular = Array.from({ length: 11 }, (_, i) => i * 20); const groundAt = (s) => 100 + 0.015 * s + 0.4 * Math.sin(s / 7) + 0.0371 * Math.cos(s * 3.1); const all = [...regular, 100.4, 120.25].sort((a, b) => a - b); /** 붙은 측점까지 꼭짓점(A) · 정규 측점만 꼭짓점(B). */ function makeBase(pviS) { return { policy, onlyExplicitCurves: false, basePvi: pviS.map((s) => ({ chainage_m: s, elevation_m: Number(groundAt(s).toFixed(6)) })), chainage: all, ground: all.map((s) => Number(groundAt(s).toFixed(6))), stations: all.map((s) => ({ station_id: `S${s}`, chainage_m: s })), }; } const baseA = makeBase(all); const baseB = makeBase(regular); const empty = align.emptyEdits(); const build = (base, edits) => align.buildAlignment(base, edits); const curvesIn = (alignment, from, to) => alignment.curves .filter((c) => c.chainage_m > from + 1e-6 && c.chainage_m < to - 1e-6) .map((c) => c.chainage_m); const planAt = (alignment, s) => align.planElevationAt(alignment, s); const r6 = (v) => Number(v.toFixed(6)); // ① 붙은 측점 직선화 40 → 140 (A) const sA = straight.straightenBetween(baseA, empty, 40, 140); const alA = build(baseA, sA); // 옛 판정(대수차 1e-9)이면 꺾임으로 읽혔을 안쪽 꼭짓점 수 — 시험이 반올림 흠을 실제로 거치는지 let noiseBends = 0; alA.pvi.forEach((node, i) => { if (i === 0 || i === alA.pvi.length - 1) return; if (node.chainage_m <= 40 + 1e-6 || node.chainage_m >= 140 - 1e-6) return; const g = (a, b) => (b.elevation_m - a.elevation_m) / (b.chainage_m - a.chainage_m); if (Math.abs(g(node, alA.pvi[i + 1]) - g(alA.pvi[i - 1], node)) >= 1e-9) noiseBends += 1; }); const runA = straight.detectStraightRun(alA, 70); const axesA = pivot.pivotAxes(alA, 80); const upA = align.adjustStation(baseA, sA, 80, 0.1); const alUp = build(baseA, upA); const gradesUp = alUp.segments .filter((seg) => seg.from_m >= 40 - 1e-6 && seg.to_m <= 140 + 1e-6) .map((seg) => r6(seg.grade_percent)); const straightened = { noiseBends, curvesInside: curvesIn(alA, 40, 140), radiiKeys: Object.keys(sA.curve_radii), run: runA ? [runA.fromM, runA.toM] : null, axes: axesA ? [axesA.fromM, axesA.toM] : null, pressRise: r6(planAt(alUp, 80) - planAt(alA, 80)), curvesAfterPress: curvesIn(alUp, 40, 140), gradesUp, editedCount: judge.bentEditCount(alA), offsetCount: Object.keys(sA.station_offsets).length, }; // ② 끝점 꼭짓점 아님(B) — 100.4 → 160 직선화 · 120.25 → 180 자유지정 const alB = build(baseB, empty); const sB = straight.straightenBetween(baseB, empty, 100.4, 160); const alSB = build(baseB, sB); const line = (a, b, s) => a[1] + ((b[1] - a[1]) * (s - a[0])) / (b[0] - a[0]); const endA = [100.4, planAt(alB, 100.4)]; const endB = [160, align.controlElevationAt(alB, 160)]; const endpoint = { // 끝 꼭짓점 = 직선화 전 그 자리 계획고(꼭짓점 높이 · 그 라운드는 직선의 접선) endHeld: r6(align.controlElevationAt(alSB, 100.4) - planAt(alB, 100.4)), endIsVertex: alSB.pvi.some((n) => Math.abs(n.chainage_m - 100.4) < 1e-6), curvesInside: curvesIn(alSB, 100.4, 160), innerResidual: [120, 120.25, 140].map((s) => r6(planAt(alSB, s) - line(endA, endB, s))), }; const z140 = planAt(alB, 140) + 1; const fEdits = free.buildFreeEdits(baseB, empty, [ { chainage_m: 120.25, elevation_m: align.controlElevationAt(alB, 120.25) }, { chainage_m: 140, elevation_m: z140 }, { chainage_m: 180, elevation_m: align.controlElevationAt(alB, 180) }, ]); const alF = build(baseB, fEdits); // 자유지정 뒤 ▼ 로 140 을 다시 120.25 · 180 잇는 선 위로 → 직선 → 정리하면 R 없음 const back = line( [120.25, align.controlElevationAt(alF, 120.25)], [180, align.controlElevationAt(alF, 180)], 140, ) - planAt(alF, 140); const downF = align.adjustStation(baseB, fEdits, 140, back); const tidyF = straight.tidyEdits(baseB, downF); const alTidyF = build(baseB, tidyF); const freeCase = { endHeld: r6(align.controlElevationAt(alF, 120.25) - align.controlElevationAt(alB, 120.25)), radiusAt140: fEdits.curve_radii["140.000"] ?? null, curveAt140: alF.curves.some((c) => Math.abs(c.chainage_m - 140) < 1e-6), curvesInsideF: curvesIn(alF, 120.25, 180), radiusKeptRaw: downF.curve_radii["140.000"] !== undefined, radiusAfterTidy: tidyF.curve_radii["140.000"] ?? null, curvesAfterTidy: curvesIn(alTidyF, 120.25, 180), sameShape: r6(planAt(alTidyF, 140) - planAt(build(baseB, downF), 140)), }; // ③ 고른 직선 기울이기 — 누름 쌓임 · 되돌아오면 R 없음 (A) const run = straight.detectStraightRun(alA, 70); let e = sA; const rises = []; const pivotM = 80; // 40 · 140 가운데 = 90 → 가장 가까운 안쪽 꼭짓점(80 · 100 중 앞선 80) [0.1, 0.1, -0.1, -0.1].forEach((d) => { e = straight.tiltStraightRun(baseA, e, run, d); rises.push(r6(align.controlElevationAt(build(baseA, e), pivotM) - planAt(alA, pivotM))); }); const tiltOnce = straight.tiltStraightRun(baseA, sA, run, 0.1); const tilt = { rises, finalRadii: Object.keys(e.curve_radii), finalCurves: curvesIn(build(baseA, e), 40, 140), editedAfterOne: judge.bentEditCount(build(baseA, tiltOnce)), }; // ④ 0 오프셋 정리 const tidyZero = straight.tidyEdits(baseB, { station_offsets: { "60.000": 0, "80.000": 0.3 }, curve_radii: {}, }); console.log( JSON.stringify({ straightened, endpoint, freeCase, tilt, tidyZero: Object.keys(tidyZero.station_offsets), mirror: { base: baseA, cases: [ { edits: sA, curves: alA.curves.map((c) => [r6(c.chainage_m), r6(c.r_m)]) }, { edits: upA, curves: alUp.curves.map((c) => [r6(c.chainage_m), r6(c.r_m)]) }, { edits: tiltOnce, curves: build(baseA, tiltOnce).curves.map((c) => [r6(c.chainage_m), r6(c.r_m)]), }, ], }, }), );