Files
Aislo/resources/tester/helper_355_1_pivot_tilt.cjs
T
eomsangdonandClaude Opus 5.5 1ac1a7d936 feat(B05): 종단 ▲▼ 꺾임 누름 시 이웃 꺾임 축으로 사이 직선 돌림 (355-1)
- adjustStation — 누른 점이 꺾임이면 pivotAxes 로 좌우 다음 꺾임(없으면 시점 · 종점)을 축으로 사이 측점을 새 직선 위로 · 사이 R 키 지움 · 직선 안쪽 · 처음 계획선은 옛 값 그대로
- 새 logic/B05_Profile_UI_Profile_Pivot.ts — 타입만 import(순환 없음)
- 시험 test_355_1 · helper_355_1 세 경우 · test_213_1/2 tsc 산출 ESM import 에 .js 붙임

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013QyNXY2TUKXCFMwAca2APG
2026-10-16 20:58:31 +09:00

151 lines
6.0 KiB
JavaScript

/* 355-1 종단 ▲▼ 이웃 꺾임 축 돌림 단위검증 헬퍼 — `B05_Profile_UI_Profile_Alignment.ts` ·
* `_Straighten.ts` 를 그 자리에서 트랜스파일해 세 경우를 Node 로 돌린다.
* ① 처음 계획선 ▲ = 옛 값 ② 직선화 뒤 꺾임 ▲ = 사이 측점 한 직선 · 축 표고 그대로 · +0.1
* ③ 직선 안쪽 ▲ = 옛 값. 옛 값 = 355-1 앞 adjustStation(아래 oldAdjust) 그대로.
*/
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);
cache.set(full, box.exports);
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"));
// 전체 측점 폴리라인(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));
})(),
};
}
// ③ 직선 안쪽(80 · 40~100 직선 위) — 꺾임 아님 → 옛 값.
{
let edits = straight.straightenBetween(base, align.emptyEdits(), 40, 100);
edits = straight.straightenBetween(base, edits, 100, 160);
const next = align.adjustStation(base, edits, 80, 0.1);
out.inside = {
same:
JSON.stringify(sorted(next.station_offsets)) ===
JSON.stringify(sorted(oldAdjust(edits, 80, 0.1).station_offsets)),
planRise:
align.planElevationAt(align.buildAlignment(base, next), 80) -
align.planElevationAt(align.buildAlignment(base, edits), 80),
};
}
process.stdout.write(JSON.stringify(out));