- TS `logic/B05_Profile_Profile_Straight.ts` ↔ 파이썬 `is_straight_bend` 짝 · 곡선 넣기 · ▲▼ 축 · 직선 찾기 - 적용 한 곳 `tidyEdits` — 직선 위 기억 R · 0 오프셋 지움(모양 그대로) - 직선화 · 자유지정 끝이 꼭짓점 아니면 지금 높이로 세움 · 고른 직선 기울이기 = 지금 경첩 높이 + 걸음 - 「편집 측점 N개」 = 꺾인 편집 꼭짓점 수 · 시험 test_426 + 헬퍼 스텁 넷 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWh5wNNycMkbwMpACz1rte
190 lines
7.9 KiB
JavaScript
190 lines
7.9 KiB
JavaScript
/* 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 ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
|
|
|
const ROOT = path.join(__dirname, "..", "..");
|
|
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.startsWith(".")) return loadTs(path.join(path.dirname(full), `${request}.ts`));
|
|
return require(request);
|
|
};
|
|
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
|
return box.exports;
|
|
}
|
|
|
|
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)]),
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
);
|