Files
Aislo/resources/tester/helper_432_mc_on_station.cjs
T
eomsangdonandClaude Opus 5.5 7e0580ffd1 fix(노선 편집): MC 를 일반측점 위에 두면 그 뒤 스냅대로 — 「위」 판정 폭 = 측점 목록 「(MC)」 규칙 · 밀어내는 걸음 막음 (PLAN 576-1 · 요구 432)
- 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>
2026-10-28 22:59:17 +09:00

141 lines
5.2 KiB
JavaScript

/* 432 MC 를 일반측점 위에 두면 그 뒤 스냅대로 — 측점 No.(20 m) 노선 곡선 하나를 손으로 측점 + 0.3 m 에 두고
* 「위」 판정 폭 · 맞춤 · 밀어내는 걸음 막힘(`pushedOff`)을 JSON 한 줄로(`test_432_mc_on_station.py` 가 판정).
*/
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 edits = loadTs(path.join(LOGIC, "B05_Profile_UI_RouteEdit_Edits.ts"));
const frame = loadTs(path.join(LOGIC, "B05_Profile_UI_RouteEdit_Frame.ts"));
const STATION = 20;
const LIMITS = {
minRadiusM: 30,
limitRadiusM: 12,
limitArcM: 0,
intervalM: STATION,
stationM: STATION,
};
const ROUTE = [
[0, 0],
[100, 0],
[180, 60],
[280, 60],
[360, 0],
[460, 0],
];
const K = 2; // 손으로 측점에 둘 곡선의 꺾임점
const columns = (p, lock = []) => ({
planned: p.map(([x, y]) => [x, y]),
curveOn: p.map(() => true),
curveRadius: p.map(() => null),
curveLock: p.map(() => null),
curveArc: p.map(() => null),
apexLock: p.map((_, i) => i === 0 || i === p.length - 1 || lock.includes(i)),
});
const built = (cols, limits = LIMITS) =>
edits.rebuildLine({ ...cols, curveRadius: [...cols.curveRadius] }, limits);
const mcs = (cols, limits) => {
const b = built(cols, limits);
return frame.curveSpans(b.vertices, b.curves).map(([s, e]) => (s + e) / 2);
};
const off = (cols, limits = LIMITS) => {
const b = built(cols, limits);
return fit.offStationCount(b.vertices, b.curves, limits.intervalM, limits.stationM);
};
/** IP `k` 를 들어오는 직선 따라 밀어 곡선 `k` MC = target(손으로 끈 자리) — 할선 몇 번. */
function place(cols, k, target) {
const base = cols.planned[k];
const prev = cols.planned[k - 1];
const len = Math.hypot(base[0] - prev[0], base[1] - prev[1]);
const ux = (base[0] - prev[0]) / len;
const uy = (base[1] - prev[1]) / len;
const at = (d) => {
const c = {
...cols,
planned: cols.planned.map((v, i) => (i === k ? [base[0] + ux * d, base[1] + uy * d] : v)),
};
return { c, mc: mcs(c, LIMITS)[k - 1] };
};
let d = 0;
let got = at(0);
for (let i = 0; i < 8; i += 1) {
d += target - got.mc;
got = at(d);
}
return got;
}
const fitted = columns(fit.fitCurveStations(columns(ROUTE), LIMITS, 1));
const fittedMcs = mcs(fitted, LIMITS);
const station = Math.round(fittedMcs[K - 1] / STATION) * STATION;
// 손으로 측점 + 0.3 m(폭 안) · + 0.7 m(폭 밖) — 맞춤이 못 옮기게 그 IP 고정(못 맞춰도 「위」)
const inside = place(fitted, K, station + 0.3);
const outside = place(fitted, K, station + 0.7);
const lockIn = { ...inside.c, apexLock: inside.c.apexLock.map((v, i) => v || i === K) };
const lockInFit = { ...lockIn, planned: fit.fitCurveStations(lockIn, LIMITS, 1) };
// 고정 없이 폭 안 = 같은 측점에 정확히 맞춤
const freeFit = { ...inside.c, planned: fit.fitCurveStations(inside.c, LIMITS, 1) };
// 그 뒤 걸음 — 앞 교점(K-1)을 끌어 그 곡선 MC 를 밀어냄 · R 바꿈 · 폭 안 작은 걸음
const nudge = (cols, k, dx, dy) => {
const now = {
...cols,
planned: cols.planned.map((v, i) => (i === k ? [v[0] + dx, v[1] + dy] : v)),
};
return { ...now, planned: fit.fitAfter(cols, now, LIMITS) };
};
const drag = nudge(lockInFit, K - 1, 0, 4);
const small = nudge(lockInFit, K - 1, 0, 0.1);
const withRadius = (cols, r) => {
const now = { ...cols, curveRadius: cols.curveRadius.map((x, i) => (i === K ? r : x)) };
return { ...now, planned: fit.fitAfter(cols, now, LIMITS) };
};
const radius = withRadius(lockInFit, 100);
const radiusIn = withRadius(lockInFit, 60);
const freeRadius = withRadius(freeFit, 100);
// 1 m 스냅 · 측점 No. 20 m — 측점 밖 정수 m 자리는 옛 판정(0.05 m) 그대로
const one = { ...LIMITS, intervalM: 1 };
const odd = place(fitted, K, station + 3.3);
console.log(
JSON.stringify({
station,
fittedOff: off(fitted),
inside: { mc: inside.mc, off: off(inside.c) },
outside: { mc: outside.mc, off: off(outside.c) },
lockInFit: { mc: mcs(lockInFit, LIMITS)[K - 1], off: off(lockInFit) },
freeFit: { mc: mcs(freeFit, LIMITS)[K - 1], off: off(freeFit) },
drag: { mc: mcs(drag, LIMITS)[K - 1], pushed: fit.pushedOff(lockInFit, drag, LIMITS) },
small: { mc: mcs(small, LIMITS)[K - 1], pushed: fit.pushedOff(lockInFit, small, LIMITS) },
radius: { mc: mcs(radius, LIMITS)[K - 1], pushed: fit.pushedOff(lockInFit, radius, LIMITS) },
radiusIn: {
mc: mcs(radiusIn, LIMITS)[K - 1],
pushed: fit.pushedOff(lockInFit, radiusIn, LIMITS),
},
freeRadius: {
mc: mcs(freeRadius, LIMITS)[K - 1],
pushed: fit.pushedOff(freeFit, freeRadius, LIMITS),
},
oneStep: {
insideOff: off(inside.c, one),
oddMc: odd.mc,
oddOff: (() => {
const b = built(odd.c, one);
return fit.offStationMcs(b.vertices, b.curves, 1, STATION);
})(),
},
}),
);