Files
Aislo/resources/tester/helper_366_3_station_fit.cjs
T
eomsangdonandClaude Opus 5.5 a108c3ae11 feat(B05 평면 도구줄): 구분선 · 스냅 간격 고르기 · Ctrl+휠 5° 회전 · [기울기] 기본 꺼짐 (390-1 · 2 · 3)
- [측점 스냅] · 간격 | 분석 지형 구분선(분석 지형 숨으면 같이 숨김) · [기울기] 기본 끔
- 스냅 간격 0.1 · 1 · 10 m · 측점 No.(기본) — 프로젝트별 화면 기억 · 스냅 끔이면 흐림 · MC 를 간격 배수에 · 허용 오차 간격 따라(0.1 m = 0.005 m)
- Ctrl+휠 한 칸 5° 회전(단추 15° 그대로 · 손가락 · 트랙패드 벌림 = 확대 · 추천 지도 안 돎) · 도움말 한 줄

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
2026-10-17 17:17:51 +09:00

124 lines
4.6 KiB
JavaScript

/* 366-3 측점 스냅 = 곡선 가운데(MC)를 일반측점에 — `logic/B05_Profile_RouteEdit_StationFit.ts` 를
* Node 로 돌려 JSON 한 줄을 냄(`test_366_3_station_fit.py` 가 판정).
* `.cjs` = 루트 package.json 이 "type": "module" 이라서.
*/
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) =>
request.startsWith(".")
? loadTs(path.join(path.dirname(full), `${request}.ts`))
: 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 fit = loadTs(path.join(LOGIC, "B05_Profile_RouteEdit_StationFit.ts"));
const curve = loadTs(path.join(LOGIC, "B05_Profile_UI_RouteEdit_Curve.ts"));
const frame = loadTs(path.join(LOGIC, "B05_Profile_UI_RouteEdit_Frame.ts"));
const INTERVAL = 20;
const LIMITS = { minRadiusM: 30, limitRadiusM: 12, limitArcM: 0, intervalM: INTERVAL };
// 지그재그 노선 — 꺾임 다섯(교각 다양) · 직선 넉넉
const ROUTE = [
[0, 0],
[93, 7],
[171, 61],
[262, 48],
[337, 112],
[431, 101],
[503, 160],
];
function columns(planned, lock = []) {
const n = planned.length;
return {
planned,
curveOn: planned.map(() => true),
curveRadius: planned.map(() => null),
curveLock: planned.map(() => null),
curveArc: planned.map(() => null),
apexLock: planned.map((_, i) => i === 0 || i === n - 1 || lock.includes(i)),
};
}
function measure(planned, interval = INTERVAL) {
const built = curve.buildEditedPolyline(
planned,
planned.map(() => true),
planned.map(() => null),
LIMITS.minRadiusM,
);
const spans = frame.curveSpans(built.vertices, built.curves);
const mcs = spans.map(([a, b]) => (a + b) / 2);
const total = frame.stationsAlong(built.vertices, INTERVAL, false);
const length = built.vertices.reduce(
(sum, v, i) =>
i ? sum + Math.hypot(v[0] - built.vertices[i - 1][0], v[1] - built.vertices[i - 1][1]) : 0,
0,
);
return {
curves: built.curves.length,
mcs,
residues: mcs.map((mc) => Math.abs(mc - Math.round(mc / interval) * interval)),
innerStations: total.filter((s) => s > 0 && s < length).length,
off: fit.offStationCount(built.vertices, built.curves, interval),
};
}
const moved = (a, b) => a.map((v, i) => Math.hypot(v[0] - b[i][0], v[1] - b[i][1]));
const before = measure(ROUTE);
const all = fit.fitCurveStations(columns(ROUTE), LIMITS, 1);
const locked = fit.fitCurveStations(columns(ROUTE, [3]), LIMITS, 1);
const fromLater = fit.fitCurveStations(columns(ROUTE), LIMITS, 4);
const everyLocked = fit.fitCurveStations(columns(ROUTE, [1, 2, 3, 4, 5]), LIMITS, 1);
console.log(
JSON.stringify({
before,
all: { ...measure(all), moved: moved(ROUTE, all) },
locked: { ...measure(locked), moved: moved(ROUTE, locked) },
fromLater: { ...measure(fromLater), moved: moved(ROUTE, fromLater) },
everyLocked: { ...measure(everyLocked), moved: moved(ROUTE, everyLocked) },
again: moved(all, fit.fitCurveStations(columns(all), LIMITS, 1)),
// 390-2 스냅 간격 — 0.1 · 1 · 10 m 배수 · 측점 밖 개수도 같은 간격
steps: Object.fromEntries(
[0.1, 1, 10].map((step) => {
const fitted = fit.fitCurveStations(columns(ROUTE), { ...LIMITS, intervalM: step }, 1);
const offBefore = measure(ROUTE, step).off;
return [step, { offBefore, ...measure(fitted, step), moved: moved(ROUTE, fitted) }];
}),
),
tol: [0.1, 1, 10, 20].map((step) => fit.mcTolM(step)),
noInterval: moved(ROUTE, fit.fitCurveStations(columns(ROUTE), { ...LIMITS, intervalM: 0 }, 1)),
start: {
moved4: fit.fitStart(
columns(ROUTE),
columns(ROUTE.map((v, i) => (i === 4 ? [v[0] + 1, v[1]] : v))),
),
radius2: fit.fitStart(columns(ROUTE), {
...columns(ROUTE),
curveRadius: ROUTE.map((_, i) => (i === 2 ? 40 : null)),
}),
dropLast: fit.fitStart(columns(ROUTE), columns(ROUTE.filter((_, i) => i !== 5))),
same: fit.fitStart(columns(ROUTE), columns(ROUTE)),
none: fit.fitStart(null, columns(ROUTE)),
},
}),
);