/* 355-1 종단 ▲▼ 이웃 꺾임 축 돌림 단위검증 헬퍼 — `B05_Profile_UI_Profile_Alignment.ts` · * `_Straighten.ts` 를 그 자리에서 트랜스파일해 세 경우를 Node 로 돌린다. * ① 처음 계획선 ▲ = 옛 값 ② 직선화 뒤 꺾임 ▲ = 사이 측점 한 직선 · 축 표고 그대로 · +0.1 * ③ 직선 안 측점 ▲ = 그 점 새 꺾임 · 축 = 좌우 다음 꺾임(368-1) ④ 꼭짓점 아닌 자리 ▲ = 같은 규칙. * 옛 값 = 355-1 앞 adjustStation(아래 oldAdjust) 그대로. */ 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")); // 전체 측점 폴리라인(20m 간격 · 0~200m) — 서버와 같이 꺾임마다 기본 L 라운드. const stations = Array.from({ length: 11 }, (_, i) => i * 20); const ground = stations.map((s, i) => 100 + 0.3 * i + (i % 2 ? 0.4 : 0)); const base = { 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, }, onlyExplicitCurves: false, basePvi: stations.map((s, i) => ({ chainage_m: s, elevation_m: ground[i] })), chainage: stations, ground, stations: stations.map((s) => ({ station_id: `S${s}`, chainage_m: s })), }; /** 355-1 앞 adjustStation — 누른 측점 하나만 옮김. */ function oldAdjust(edits, chainageM, delta) { const key = align.chainageKey(chainageM); const current = align.buildAlignment(base, edits); const target = align.planElevationAt(current, chainageM) + delta; const baseElevation = straight.baseElevationAt(base, chainageM); let offset = (edits.station_offsets[key] ?? align.planElevationAt(current, chainageM) - baseElevation) + delta; const withOffset = (value) => ({ ...edits, station_offsets: { ...edits.station_offsets, [key]: Number(value.toFixed(6)) }, }); let next = withOffset(offset); for (let pass = 0; pass < 4; pass += 1) { const error = target - align.planElevationAt(align.buildAlignment(base, next), chainageM); if (Math.abs(error) < 1e-4) break; offset += error; next = withOffset(offset); } return next; } const sorted = (record) => Object.fromEntries( Object.keys(record ?? {}) .sort() .map((key) => [key, record[key]]), ); const pviZ = (built, s) => built.pvi.find((n) => Math.abs(n.chainage_m - s) < 1e-6).elevation_m; const out = {}; // ① 처음 계획선 — 마디마다 꺾임 → 축 = 누른 점 ±1 · 옛 값과 같음(▲ · ▼). { const empty = align.emptyEdits(); const up = align.adjustStation(base, empty, 100, 0.1); const down = align.adjustStation(base, empty, 60, -0.1); out.initial = { sameUp: JSON.stringify(sorted(up.station_offsets)) === JSON.stringify(sorted(oldAdjust(empty, 100, 0.1).station_offsets)), sameDown: JSON.stringify(sorted(down.station_offsets)) === JSON.stringify(sorted(oldAdjust(empty, 60, -0.1).station_offsets)), upKeys: Object.keys(up.station_offsets), radiiKeys: Object.keys(up.curve_radii ?? {}), }; } // ② 40~100 · 100~160 직선화 → 100 = 꺾임 · 두 축 40 · 160 · ▲ 100. { let edits = straight.straightenBetween(base, align.emptyEdits(), 40, 100); edits = straight.straightenBetween(base, edits, 100, 160); const before = align.buildAlignment(base, edits); const next = align.adjustStation(base, edits, 100, 0.1); const after = align.buildAlignment(base, next); const p = pviZ(after, 100); const line = (s, aM, aZ) => aZ + ((p - aZ) * (s - aM)) / (100 - aM); const z40 = pviZ(before, 40); const z160 = pviZ(before, 160); out.kink = { planRise: align.planElevationAt(after, 100) - align.planElevationAt(before, 100), pivotRise: p - pviZ(before, 100), axisMove: [pviZ(after, 40) - z40, pviZ(after, 160) - z160], outsideMove: [20, 180].map((s) => pviZ(after, s) - pviZ(before, s)), innerResidual: [60, 80] .map((s) => pviZ(after, s) - line(s, 40, z40)) .concat([120, 140].map((s) => pviZ(after, s) - line(s, 160, z160))), innerMoved: [60, 80, 120, 140].map((s) => pviZ(after, s) - pviZ(before, s)), curvesAt: after.curves.map((c) => c.chainage_m).sort((a, b) => a - b), oldWouldBreak: (() => { const old = align.buildAlignment(base, oldAdjust(edits, 100, 0.1)); return [80, 120].map((s) => pviZ(old, s) - pviZ(before, s)); })(), }; } /** 직선 안 점 ▲ — 축 40 · 100 그대로 · 사이 점 새 직선 위 · 누른 점 꺾임(368-1). */ function tiltInside(pressM, innerS) { let edits = straight.straightenBetween(base, align.emptyEdits(), 40, 100); edits = straight.straightenBetween(base, edits, 100, 160); const before = align.buildAlignment(base, edits); const next = align.adjustStation(base, edits, pressM, 0.1); const after = align.buildAlignment(base, next); const p = pviZ(after, pressM); const z40 = pviZ(before, 40); const z100 = pviZ(before, 100); const line = (s) => s < pressM ? z40 + ((p - z40) * (s - 40)) / (pressM - 40) : z100 + ((p - z100) * (s - 100)) / (pressM - 100); return { planRise: align.planElevationAt(after, pressM) - align.planElevationAt(before, pressM), axisMove: [pviZ(after, 40) - z40, pviZ(after, 100) - z100], outsideMove: [20, 120, 140, 160].map((s) => pviZ(after, s) - pviZ(before, s)), innerResidual: innerS.map((s) => pviZ(after, s) - line(s)), innerMoved: innerS.map((s) => pviZ(after, s) - pviZ(before, s)), curveAtPress: after.curves.some((c) => Math.abs(c.chainage_m - pressM) < 1e-6), pressRadiusKey: Object.prototype.hasOwnProperty.call( next.curve_radii, align.chainageKey(pressM), ), innerRadiusKeys: innerS.filter((s) => Object.prototype.hasOwnProperty.call(next.curve_radii, align.chainageKey(s)), ), }; } // ③ 직선 안 측점(80 · 40~100 직선 위) — 80 새 꺾임 · 축 40 · 100 · 60 새 직선 위. out.inside = tiltInside(80, [60]); // ④ 꼭짓점 아닌 자리(70) — 같은 규칙 · 60 · 80 새 직선 위. out.between = tiltInside(70, [60, 80]); process.stdout.write(JSON.stringify(out));