Files
Aislo/resources/tester/helper_426_profile_straight.cjs
T
eomsangdonandClaude Opus 5.5 309415a481 fix(B05 종단): 직선 판정 한 벌(꼭짓점 높이 1e-5 m) · 끝점 꼭짓점 · 기울이기 누름 쌓임 · 편집 측점 수 (426)
- 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
2026-10-18 15:51:08 +09:00

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)]),
},
],
},
}),
);