- StationFit: 일반측점(측점 간격 배수) 「위」 = curveStations 반올림 폭(mcScale 공유 · 정수 m ±0.5 m) · 폭 안이면 정확히 맞춰 보고 못 맞춰도 「위」 · 10 m N+10 · 간격 격자는 옛 0.05 m 그대로 - offStationCount · offStationMcs 에 측점 간격 인자 · 편집기 · 곡선 띠가 넘김(1 m · 10 m 스냅 동작 전후 같음) - pushedOff — R · 곡선 길이 입력 등 applyEdit 걸음이 측점 위 곡선을 밖으로 밀면 되돌림(꺾임점 · 곡선 켬 같은 걸음만) - 시험: test_432_mc_on_station 새로 · helper_366_3 가 편집기처럼 측점 간격을 넘김(옛 helper 는 간격=측점 간격 가정) · test_390_5 배선 문구 offStation 한 곳으로 바뀜 - build:design-calc 다시 지음 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
118 lines
4.2 KiB
JavaScript
118 lines
4.2 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 { makeLoader } = require("./helper_ts_stub_loader.cjs");
|
|
|
|
const ROOT = path.join(__dirname, "..", "..");
|
|
|
|
const { loadTs } = makeLoader({
|
|
root: ROOT,
|
|
requireFor: (full) => (request) =>
|
|
request.startsWith(".")
|
|
? loadTs(path.join(path.dirname(full), `${request}.ts`))
|
|
: require(request),
|
|
});
|
|
|
|
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, 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, stationM: INTERVAL },
|
|
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)),
|
|
},
|
|
}),
|
|
);
|