Merge remote-tracking branch 'origin/sub_laptop_3' into main_laptop_1
This commit is contained in:
@@ -378,7 +378,9 @@ export async function mountRouteEditor(
|
||||
routeHeadText(expected, plannedLine.length ? plannedLine : planned, planned.length);
|
||||
const contourHint = (): string => contourHintText(contours, pickedContour);
|
||||
|
||||
/** 상태줄 꼬리 — 셈은 `_Edits` 몫. */
|
||||
const offStation = (): number =>
|
||||
stationSnap ? fit.offStationCount(plannedLine, curveInfo, snap(), stationIntervalM) : 0;
|
||||
/** 상태줄 꼬리 · 측점 밖 곡선 수(441 막힘 · 432) — 셈은 `_Edits` · `StationFit` 몫. */
|
||||
const curveHint = (): string =>
|
||||
edits.curveSummary({
|
||||
nodeCount: planned.length,
|
||||
@@ -390,7 +392,7 @@ export async function mountRouteEditor(
|
||||
violationCount: nodeInfo.filter((node) => node.violations.length).length,
|
||||
minRadiusM,
|
||||
fresh: nodeInfo.length === 0,
|
||||
offStation: stationSnap ? fit.offStationCount(plannedLine, curveInfo, snap()) : 0,
|
||||
offStation: offStation(),
|
||||
flags: edits.routeFlags(planned, curveOn, curveInfo), // 겹친 노드 · 못 그린 곡선(430)
|
||||
});
|
||||
|
||||
@@ -440,6 +442,8 @@ export async function mountRouteEditor(
|
||||
fitLimits(),
|
||||
]);
|
||||
markEdited();
|
||||
if (stationSnap && fitting && before && fit.pushedOff(before, snapshotNow(), fitLimits()))
|
||||
return restoreSnapshot(before, "곡선 가운데를 측점에 못 맞춰 되돌렸습니다.", false); // 432
|
||||
writeRouteLocks(projectId, planned, apexLock); // 고정은 [취소] · 다시 열기에도 남는다(22-5).
|
||||
syncCurveBar();
|
||||
status.textContent = `${routeHead()} — ${message} ${curveHint()}`;
|
||||
@@ -549,7 +553,7 @@ export async function mountRouteEditor(
|
||||
if (base) markEdited();
|
||||
return base !== undefined;
|
||||
},
|
||||
offStation: () => (stationSnap ? fit.offStationCount(plannedLine, curveInfo, snap()) : 0),
|
||||
offStation,
|
||||
});
|
||||
|
||||
bindNodeSplice({
|
||||
|
||||
@@ -165,7 +165,7 @@ export function createCurveBar(params: CurveBarParams): CurveBar {
|
||||
function fitTail(before: Vertex[], picked: number, hand: boolean): string {
|
||||
const { curveInfo, planned, line, snap } = params.state();
|
||||
if (!snap) return "";
|
||||
const off = offStationMcs(line, curveInfo, snap.stepM);
|
||||
const off = offStationMcs(line, curveInfo, snap.stepM, snap.stationM);
|
||||
return fitNote(before, planned, picked, off, snap.stationM, snap.stepM, hand);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
* · 유지(280-4) — 고치기 전 노선(`before`)에서 MC 가 측점 · 10 m 배수(No.N+10)였던 곡선은 그 IP · 곡선 칸이
|
||||
* 그대로면 옛 꼴(측점 간격 배수 + 0 · 10)로만 맞춤(밀 거리 = 측점 간격) · 손으로 고친 곡선만 고른 간격.
|
||||
* MC 가 고른 간격 배수 · 10 m 배수면 「위」(No.N+10 특별측점 · 저장 칸 없음 = 노선 모양으로 기억).
|
||||
* · 일반측점(측점 간격 배수) 「위」 폭 = 측점 목록 「(MC)」 규칙과 같음(`onStation` · 정수 m = ±0.5 m · 432) — 폭 안이면 정확히 맞춰 보고
|
||||
* 못 맞춰도 「위」로 둠(손으로 측점에 둔 곡선) · 그 곡선을 폭 밖으로 미는 걸음은 부르는 쪽이 막음(`pushedOff`).
|
||||
* · 끄는 동안 · 방향키 걸음마다 미리보기(`fitDrag` · 390-5) = 누르기 전 노선 + 옮긴 IP 하나로 매번 새로
|
||||
* 맞춤(앞 프레임 맞춤이 쌓이지 않음) · 놓을 때 `fitCurveStations` 정본 맞춤이 같은 값.
|
||||
* 파이썬 짝 아님 — [저장] 때 서버가 저장된 IP 로 선을 다시 짬(`buildEditedPolyline` 짝)이라 MC 그대로.
|
||||
@@ -24,7 +26,7 @@ import {
|
||||
shortfallCrossed,
|
||||
type NodeColumns,
|
||||
} from "./B05_Profile_UI_RouteEdit_Edits";
|
||||
import { curveSpans } from "./B05_Profile_UI_RouteEdit_Frame";
|
||||
import { curveSpans, mcScale } from "./B05_Profile_UI_RouteEdit_Frame";
|
||||
|
||||
/** MC 가 측점에서 이만큼 안이면 「위」(m) — 파이썬 `MC_TOL_M` 과 같은 값. */
|
||||
export const MC_TOL_M = 0.05;
|
||||
@@ -32,6 +34,10 @@ export const MC_TOL_M = 0.05;
|
||||
/** 간격별 허용 오차 — 0.1 m 간격도 실제로 맞게 간격의 1/20 이하(390-2 · 1 m 이상은 `MC_TOL_M`). */
|
||||
export const mcTolM = (intervalM: number): number => Math.min(MC_TOL_M, intervalM / 20);
|
||||
|
||||
/** MC 가 측점 `c` 위 — 측점 목록 「(MC)」 규칙(`curveStations` 반올림)과 같은 폭(432). */
|
||||
const onStation = (mc: number, c: number, intervalM: number): boolean =>
|
||||
Math.abs(Math.floor(mc * mcScale(intervalM) + 0.5) / mcScale(intervalM) - c) < 0.005;
|
||||
|
||||
/** 특별측점 간격(m) — MC 가 일반측점 사이 10 m 배수(No.N+10)면 측점 위로 봄(280-4). */
|
||||
export const SPECIAL_STATION_M = 10;
|
||||
|
||||
@@ -66,16 +72,18 @@ export function offStationCount(
|
||||
line: ReadonlyArray<Vertex>,
|
||||
curves: ReadonlyArray<EditedCurve>,
|
||||
intervalM: number,
|
||||
stationM = intervalM,
|
||||
): number {
|
||||
return offStationMcs(line, curves, intervalM).length;
|
||||
return offStationMcs(line, curves, intervalM, stationM).length;
|
||||
}
|
||||
|
||||
/** 측점 밖 곡선의 MC 누가거리(408-1 · 상태줄 「못 맞춘 곡선」) — `offStationCount` 와 같은 판정 ·
|
||||
* 간격 배수 · 10 m 배수(No.N+10 · 280-4) 둘 다 「위」. */
|
||||
* 간격 배수 · 10 m 배수(No.N+10 · 280-4) 둘 다 「위」 · 일반측점(`stationM` 배수)은 「(MC)」 폭(432). */
|
||||
export function offStationMcs(
|
||||
line: ReadonlyArray<Vertex>,
|
||||
curves: ReadonlyArray<EditedCurve>,
|
||||
intervalM: number,
|
||||
stationM = intervalM,
|
||||
): number[] {
|
||||
if (!(intervalM > 0)) return [];
|
||||
const tol = mcTolM(intervalM);
|
||||
@@ -83,9 +91,13 @@ export function offStationMcs(
|
||||
const off: number[] = [];
|
||||
for (const [start, end] of curveSpans(line, curves)) {
|
||||
const mc = (start + end) / 2;
|
||||
const at = [intervalM, SPECIAL_STATION_M]
|
||||
.map((step) => Math.round(mc / step) * step)
|
||||
.find((c) => c > tol && Math.abs(mc - c) <= tol && !hits.has(Math.round(c * 1000)));
|
||||
const free = (c: number): boolean => !hits.has(Math.round(c * 1000));
|
||||
const wide = Math.round(mc / stationM) * stationM;
|
||||
const at =
|
||||
[intervalM, SPECIAL_STATION_M]
|
||||
.map((step) => Math.round(mc / step) * step)
|
||||
.find((c) => c > tol && Math.abs(mc - c) <= tol && free(c)) ??
|
||||
(wide > 0 && onStation(mc, wide, intervalM) && free(wide) ? wide : undefined);
|
||||
if (at !== undefined) hits.add(Math.round(at * 1000));
|
||||
else off.push(mc);
|
||||
}
|
||||
@@ -222,6 +234,9 @@ export function fitCurveStations(
|
||||
];
|
||||
const onAt = (mc: number, last: number, grid: Grid) =>
|
||||
[grid, ...loose].flatMap((g) => near(mc, last, g)).find((c) => Math.abs(mc - c) <= tol);
|
||||
/** 폭 안 측점(432) — 정확히 맞출 자리 · 못 맞춰도 「위」. */
|
||||
const wideAt = (mc: number, last: number) =>
|
||||
near(mc, last, { step: stationM, offset: 0, reach: 0 }).find((c) => onStation(mc, c, interval));
|
||||
/** 곡선 `k` 를 측점 `c` 에 — 하한을 새로 깨거나 앞 곡선(`previous`) MC 를 흔들면 null. */
|
||||
const attempt = (at: Fit, k: number, mc: number, c: number, previous: number): Fit | null => {
|
||||
const fitted = solve(cols, limits, at.planned, k, c, mc, gridOf(k).reach);
|
||||
@@ -244,7 +259,7 @@ export function fitCurveStations(
|
||||
for (let j = k + 1; j < at.planned.length - 1; j += 1) {
|
||||
const mc = mcOf(at.built, j);
|
||||
if (mc === null) continue;
|
||||
if (onAt(mc, last, gridOf(j)) !== undefined) return true;
|
||||
if (onAt(mc, last, gridOf(j)) ?? wideAt(mc, last)) return true;
|
||||
const grid = gridOf(j);
|
||||
return (
|
||||
!cols.apexLock[j] && near(mc, last, grid).some((c) => attempt(at, j, mc, c, k) !== null)
|
||||
@@ -258,10 +273,11 @@ export function fitCurveStations(
|
||||
const mc = mcOf(built, k);
|
||||
if (mc === null) continue;
|
||||
const on = onAt(mc, lastStation, gridOf(k));
|
||||
const wide = wideAt(mc, lastStation);
|
||||
if (on !== undefined) lastStation = on;
|
||||
else if (k >= from && !cols.apexLock[k]) {
|
||||
let pick: { fit: Fit; c: number } | null = null;
|
||||
for (const c of near(mc, lastStation, gridOf(k))) {
|
||||
for (const c of wide !== undefined ? [wide] : near(mc, lastStation, gridOf(k))) {
|
||||
const fit = attempt({ planned, built }, k, mc, c, previous);
|
||||
if (!fit) continue;
|
||||
pick ??= { fit, c };
|
||||
@@ -271,7 +287,8 @@ export function fitCurveStations(
|
||||
}
|
||||
}
|
||||
if (pick) (({ planned, built } = pick.fit), (lastStation = pick.c));
|
||||
}
|
||||
else if (wide !== undefined) lastStation = wide;
|
||||
} else if (wide !== undefined) lastStation = wide;
|
||||
previous = k;
|
||||
}
|
||||
return planned;
|
||||
@@ -286,6 +303,26 @@ export function fitAfter(
|
||||
return fitCurveStations(now, limits, fitStart(before, now), before);
|
||||
}
|
||||
|
||||
/** 편집 한 걸음이 측점 위 곡선을 밖으로 밀었나(432 · 끌기 · 방향키 막힘과 같은 개수 견줌 · R · 곡선 길이 입력) —
|
||||
* 꺾임점 · 곡선 켬이 같은 걸음만(교점 넣기 · 지우기 · 곡선 켬은 새 곡선이라 안 견줌). */
|
||||
export function pushedOff(
|
||||
before: Columns & Pick<NodeColumns, "planned">,
|
||||
now: Columns & Pick<NodeColumns, "planned">,
|
||||
limits: FitLimits,
|
||||
): boolean {
|
||||
if (
|
||||
!(limits.intervalM > 0) ||
|
||||
before.planned.length !== now.planned.length ||
|
||||
before.curveOn.some((on, i) => on !== now.curveOn[i])
|
||||
)
|
||||
return false;
|
||||
const off = (cols: Columns & Pick<NodeColumns, "planned">): number => {
|
||||
const built = build(cols, cols.planned, limits);
|
||||
return offStationCount(built.vertices, built.curves, limits.intervalM, limits.stationM);
|
||||
};
|
||||
return off(now) > off(before);
|
||||
}
|
||||
|
||||
/** 끄는 중 미리보기(390-5) — `base`(누르기 전 IP) 에 IP `k` 만 지금 자리(`cols.planned[k]`)로 두고
|
||||
* `k` 앞 곡선부터 맞춤(`fitStart` 와 같은 시작) · `before` = 누르기 전 노선(없으면 `base` + 지금 칸 · 280-4 유지). */
|
||||
export function fitDrag(
|
||||
|
||||
@@ -66,7 +66,7 @@ function measure(planned, interval = INTERVAL) {
|
||||
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),
|
||||
off: fit.offStationCount(built.vertices, built.curves, interval, INTERVAL),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -89,7 +89,11 @@ console.log(
|
||||
// 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 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) }];
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
/* 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);
|
||||
})(),
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -133,7 +133,7 @@ def test_끌기_방향키_편집기_이음() -> None:
|
||||
assert "dragRaw.forEach((vertex, index) => (state.planned[index] = vertex));" in handle
|
||||
assert handle.index("shortfallCrossed(") < handle.index("params.offStation() > dragOff")
|
||||
assert handle.index("params.offStation() > dragOff") < handle.index("dragMoved = true;")
|
||||
assert "offStation: () => (stationSnap ? fit.offStationCount(" in edit
|
||||
assert "stationSnap ? fit.offStationCount(" in edit and " offStation,\n" in edit
|
||||
assert edit.count("\n") <= 695
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
"""432 MC 를 일반측점 위에 두면 그 뒤 스냅대로.
|
||||
|
||||
「위」 판정 폭 = 측점 목록 「(MC)」 규칙(`curveStations` 반올림 · 정수 m = ±0.5 m) ·
|
||||
폭 안이면 정확히 맞춰 보고 못 맞춰도 「위」 · 그 곡선을 폭 밖으로 미는 걸음(끌기 · R 입력)은
|
||||
`pushedOff` 로 막음 · 측점 밖 정수 m 자리(1 m 스냅)는 옛 판정 그대로.
|
||||
|
||||
TS 셈은 `helper_432_mc_on_station.cjs`(Node · `StationFit` 그대로).
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_432_mc_on_station.py -q
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
HELPER = Path(__file__).with_name("helper_432_mc_on_station.cjs")
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def got() -> dict:
|
||||
run = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", str(HELPER)], capture_output=True, text=True, encoding="utf-8", timeout=120
|
||||
)
|
||||
assert run.returncode == 0, run.stderr
|
||||
return json.loads(run.stdout)
|
||||
|
||||
|
||||
def test_폭_안은_위_폭_밖은_측점_밖(got) -> None:
|
||||
station = got["station"]
|
||||
assert got["fittedOff"] == 0
|
||||
assert got["inside"]["mc"] == pytest.approx(station + 0.3, abs=1e-6)
|
||||
assert got["inside"]["off"] == 0
|
||||
assert got["outside"]["mc"] == pytest.approx(station + 0.7, abs=1e-6)
|
||||
assert got["outside"]["off"] == 1
|
||||
|
||||
|
||||
def test_못_맞춰도_위_맞출_수_있으면_정확히(got) -> None:
|
||||
station = got["station"]
|
||||
assert got["lockInFit"]["off"] == 0 # 고정 IP · 그 자리 그대로 「위」
|
||||
assert got["lockInFit"]["mc"] == pytest.approx(station + 0.3, abs=1e-3)
|
||||
assert got["freeFit"]["off"] == 0 # 같은 측점에 정확히(다른 측점으로 안 옮김)
|
||||
assert abs(got["freeFit"]["mc"] - station) <= 0.05
|
||||
|
||||
|
||||
def test_폭_밖으로_미는_걸음_막음(got) -> None:
|
||||
station = got["station"]
|
||||
for case in ("drag", "radius"):
|
||||
assert abs(got[case]["mc"] - station) > 0.5 and got[case]["pushed"] is True
|
||||
for case in ("small", "radiusIn"):
|
||||
assert abs(got[case]["mc"] - station) < 0.5 and got[case]["pushed"] is False
|
||||
assert abs(got["freeRadius"]["mc"] - station) <= 0.05 and got["freeRadius"]["pushed"] is False
|
||||
|
||||
|
||||
def test_1m_스냅_측점_밖은_옛_판정(got) -> None:
|
||||
one = got["oneStep"]
|
||||
assert one["insideOff"] == 0 # 일반측점 + 0.3 = 「(MC)」 폭
|
||||
assert one["oddOff"][0] == pytest.approx(one["oddMc"], abs=1e-6) # 정수 m + 0.3 = 측점 밖
|
||||
|
||||
|
||||
def test_편집기_배선() -> None:
|
||||
edit = (ROOT / "B05_Profile" / "B05_Profile_UI_RouteEdit.ts").read_text(encoding="utf-8")
|
||||
apply_edit = edit[edit.index("function applyEdit(") :]
|
||||
apply_edit = apply_edit[: apply_edit.index("writeRouteLocks(")]
|
||||
assert (
|
||||
"stationSnap && fitting && before && fit.pushedOff(before, snapshotNow(), fitLimits())"
|
||||
in apply_edit
|
||||
)
|
||||
assert "fit.offStationCount(plannedLine, curveInfo, snap(), stationIntervalM)" in edit
|
||||
bar = (ROOT / "B05_Profile" / "B05_Profile_UI_RouteEdit_CurveBar.ts").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
assert "offStationMcs(line, curveInfo, snap.stepM, snap.stationM)" in bar
|
||||
Reference in New Issue
Block a user