- 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>
141 lines
5.2 KiB
JavaScript
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);
|
|
})(),
|
|
},
|
|
}),
|
|
);
|