Merge remote-tracking branch 'origin/dev' into sub_laptop_7
This commit is contained in:
@@ -404,7 +404,7 @@ async function run() {
|
||||
marked: (dragRows ?? []).filter((row) => row[2] === 999).length,
|
||||
};
|
||||
|
||||
// ⑧ 중심점 좌우(356-3 꼴) — 노선 한 걸음 → 새 종단이 오면 계획고 맞춤 · 첫 차례는 cross 전에 밀림(무거운 셈 한 번)
|
||||
// ⑧ 노선 한 걸음 + 계획고 맞춤 — 새 종단이 오면 계획고 맞춤 · 첫 차례는 cross 전에 밀림(무거운 셈 한 번)
|
||||
let graded = false;
|
||||
const stopShift = qPipe.subscribe((event) => {
|
||||
if (event.kind !== "stage" || event.stage !== "profile" || graded) return;
|
||||
@@ -413,7 +413,7 @@ async function run() {
|
||||
void Promise.resolve().then(() =>
|
||||
pageMod.pipelineGrade(Q, { station_offsets: { [key]: 0.1 }, curve_radii: {} }),
|
||||
);
|
||||
}, "중심점 좌우");
|
||||
}, "노선 한 걸음 + 계획고 맞춤");
|
||||
mark = runs().length;
|
||||
qBridge.edited(draftOf(shift(1, 0, 1)));
|
||||
await settle(mark + 2);
|
||||
|
||||
@@ -204,6 +204,110 @@ function sweep(fixedR) {
|
||||
}
|
||||
const swept = { auto: sweep(false), fixed: sweep(true) };
|
||||
|
||||
// ④-2 손잡이(BC · EC) 끌기 훑기(433-21 ①) — `_Pointer` 손잡이 갈래 그대로: 맞추기 전 자리에서 셈 · 하한 막힘 ·
|
||||
// 미리보기 측점 밖이 누를 때보다 늘면 그 걸음 되돌림(`guard`) · 놓을 때 정본 맞춤. IP 셋 × 두 끝 × 12 방향 × 3 거리.
|
||||
function sweepHandle(fixedR, guard = true) {
|
||||
const start = columns(fit.fitCurveStations(columns(ROUTE), LIMITS, 1));
|
||||
if (fixedR)
|
||||
start.curveRadius = edits
|
||||
.rebuildLine(copy(start), LIMITS)
|
||||
.nodes.map((n) => (n.radius_m === null ? null : Math.round(n.radius_m * 100) / 100));
|
||||
const floor = (built) =>
|
||||
edits.curveShortfalls(built.nodes, LIMITS.limitRadiusM, LIMITS.limitArcM);
|
||||
const off = (built) => fit.offStationCount(built.vertices, built.curves, INTERVAL);
|
||||
const tally = { drags: 0, released: 0, offAfter: 0, strayed: 0, worst: [] };
|
||||
for (const K of [2, 3, 4])
|
||||
for (const which of ["start", "end"])
|
||||
for (let deg = 0; deg < 360; deg += 30)
|
||||
for (const reach of [9, 17, 26]) {
|
||||
const hist = copy(start);
|
||||
let st = { ...copy(hist), curveOn: [...hist.curveOn] };
|
||||
let built = edits.rebuildLine(st, LIMITS);
|
||||
const offBefore = off(built);
|
||||
const c0 = built.curves.find((c) => c.node_first === K);
|
||||
if (!c0) continue;
|
||||
const h0 = which === "start" ? c0.start : c0.end;
|
||||
let raw = null;
|
||||
let movedOnce = false;
|
||||
const preview = () => {
|
||||
raw = st.planned.map(([x, y]) => [x, y]);
|
||||
st = { ...st, planned: fit.fitDrag(st, hist.planned, K, LIMITS) };
|
||||
built = edits.rebuildLine(st, LIMITS);
|
||||
};
|
||||
for (let t = 0.5; t <= reach + 1e-9; t += 0.5) {
|
||||
if (raw) raw.forEach((v, i) => (st.planned[i] = v));
|
||||
built = edits.rebuildLine(st, LIMITS);
|
||||
const c = built.curves.find((entry) => entry.node_first === K);
|
||||
const to = [
|
||||
h0[0] + Math.cos((deg * Math.PI) / 180) * t,
|
||||
h0[1] + Math.sin((deg * Math.PI) / 180) * t,
|
||||
];
|
||||
const moved = c
|
||||
? curve.dragHandleTo(
|
||||
st.planned[K - 1],
|
||||
[c.apex[0], c.apex[1]],
|
||||
st.planned[K + 1],
|
||||
which,
|
||||
to,
|
||||
)
|
||||
: null;
|
||||
if (!moved) {
|
||||
if (raw) preview();
|
||||
continue;
|
||||
}
|
||||
const before = floor(built);
|
||||
const kept = [st.planned[K], st.curveRadius[K], st.curveOn[K]];
|
||||
const back = () => {
|
||||
raw?.forEach((v, i) => (st.planned[i] = v));
|
||||
[st.planned[K], st.curveRadius[K], st.curveOn[K]] = kept;
|
||||
built = edits.rebuildLine(st, LIMITS);
|
||||
preview();
|
||||
};
|
||||
st.planned[K] = moved.apex;
|
||||
st.curveRadius[K] = Math.max(
|
||||
edits.radiusFloorM(
|
||||
built.nodes[K]?.inner_angle_deg,
|
||||
LIMITS.limitRadiusM,
|
||||
LIMITS.limitArcM,
|
||||
),
|
||||
Math.round(moved.radius * 100) / 100,
|
||||
);
|
||||
st.curveOn[K] = true;
|
||||
built = edits.rebuildLine(st, LIMITS);
|
||||
if (edits.shortfallCrossed(before, floor(built))) {
|
||||
back();
|
||||
continue;
|
||||
}
|
||||
preview();
|
||||
if (guard && off(built) > offBefore) {
|
||||
back();
|
||||
tally.strayed += 1;
|
||||
continue;
|
||||
}
|
||||
movedOnce = true;
|
||||
}
|
||||
tally.drags += 1;
|
||||
if (!movedOnce) continue;
|
||||
built = edits.rebuildLine(st, LIMITS);
|
||||
if (edits.floorStop(hist, st.planned, built.nodes, LIMITS)) continue;
|
||||
const from = fit.fitStart(hist, st);
|
||||
st = { ...st, planned: fit.fitCurveStations(st, LIMITS, from) };
|
||||
built = edits.rebuildLine(st, LIMITS);
|
||||
tally.released += 1;
|
||||
const left = fit.offStationMcs(built.vertices, built.curves, INTERVAL);
|
||||
if (left.length) {
|
||||
tally.offAfter += 1;
|
||||
if (tally.worst.length < 5) tally.worst.push({ K, which, deg, reach, mc: left });
|
||||
}
|
||||
}
|
||||
return tally;
|
||||
}
|
||||
const sweptHandle = {
|
||||
auto: sweepHandle(false),
|
||||
fixed: sweepHandle(true),
|
||||
unguarded: { auto: sweepHandle(false, false), fixed: sweepHandle(true, false) },
|
||||
};
|
||||
|
||||
// ③ MC 측점 값 · 눈금 글
|
||||
const input = JSON.parse(process.argv[2] || '{"grid":[],"spans":[]}');
|
||||
const snapped = Object.fromEntries(
|
||||
@@ -227,6 +331,7 @@ console.log(
|
||||
keys,
|
||||
drag,
|
||||
swept,
|
||||
sweptHandle,
|
||||
snapped,
|
||||
labels,
|
||||
scale: [0.1, 1, 10, null].map(frame.mcScale),
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
/* 453 평면 집기 · 스냅만 바꾼 초안 헬퍼 — 진짜 `_RouteEdit_Input`(pressWinner · handleAtScreen) ·
|
||||
* `_RouteEdit_History` · `_RouteEdit_Draft`(다리 감싸기)를 트랜스파일해 돌림. 결과 JSON 한 벌(표준출력).
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const data = new Map();
|
||||
globalThis.sessionStorage = {
|
||||
get length() {
|
||||
return data.size;
|
||||
},
|
||||
key: (i) => [...data.keys()][i] ?? null,
|
||||
getItem: (k) => (data.has(k) ? data.get(k) : null),
|
||||
setItem: (k, v) => data.set(k, String(v)),
|
||||
removeItem: (k) => data.delete(k),
|
||||
};
|
||||
const win = new EventTarget();
|
||||
win.setTimeout = setTimeout;
|
||||
win.clearTimeout = clearTimeout;
|
||||
win.sessionStorage = globalThis.sessionStorage;
|
||||
win.localStorage = { getItem: () => null };
|
||||
globalThis.window = win;
|
||||
|
||||
const STUBS = {
|
||||
"@ui/ui_template_palette": { themeColor: (_name, fallback) => fallback },
|
||||
"@util/common_util_svg": { stationLabel: (c, i) => `No.${Math.round(c / i)}` },
|
||||
};
|
||||
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) => {
|
||||
if (STUBS[request]) return STUBS[request];
|
||||
if (request.startsWith(".")) return loadTs(`${path.resolve(path.dirname(full), request)}.ts`);
|
||||
return require(request);
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
||||
return box.exports;
|
||||
}
|
||||
const page = path.join(ROOT, "B05_Profile");
|
||||
const input = loadTs(path.join(page, "B05_Profile_UI_RouteEdit_Input.ts"));
|
||||
const edit = loadTs(path.join(page, "B05_Profile_UI_RouteEdit_History.ts"));
|
||||
const draftLink = loadTs(path.join(page, "B05_Profile_UI_RouteEdit_Draft.ts"));
|
||||
|
||||
// ① 누름 판정 — `_Pointer.chooseAt` 와 같은 셈(노드 · 손잡이는 화면 8 px 떨어짐 · 433-14 ③ 낮은 확대 R 12)
|
||||
const toScreen = ([x, y]) => [x, y];
|
||||
const node = [100, 100];
|
||||
const curves = [{ node_first: 1, start: [108, 100], end: [100, 108] }];
|
||||
const choose = (px, py, markDistance = Infinity) => {
|
||||
const pick = input.pickAtScreen([[0, 0], node, [200, 200]], toScreen, px, py, 12, 6, null);
|
||||
const at = pick && "node" in pick ? toScreen(node) : null;
|
||||
const handle = input.handleAtScreen(curves, toScreen, px, py, 14);
|
||||
return input.pressWinner(
|
||||
at ? Math.hypot(at[0] - px, at[1] - py) : Infinity,
|
||||
6,
|
||||
markDistance,
|
||||
handle?.distance ?? Infinity,
|
||||
);
|
||||
};
|
||||
const pick = {
|
||||
handleSide: choose(107, 100), // 노드 7 px(가운데 밖) · 손잡이 1 px — 손잡이
|
||||
handleCenter: choose(108, 100), // 손잡이 위 · 노드 8 px — 손잡이
|
||||
coreEdge: choose(106, 100), // 노드 6 px(가운데 끝) · 손잡이 2 px — 노드
|
||||
nodeCore: choose(102, 100), // 노드 2 px(가운데 안) — 노드
|
||||
nodeCloser: choose(93, 100), // 노드 7 px · 손잡이 15 px(문턱 밖) — 노드
|
||||
handleOnly: choose(116, 100), // 노드 16 px(문턱 밖) · 손잡이 8 px — 손잡이
|
||||
nothing: choose(150, 150), // 빈 곳 — pick(null 그대로)
|
||||
markOnCore: choose(103, 100, 1), // 마커 1 px 이어도 노드 가운데 3 px — 노드(433-21 ③)
|
||||
markCloser: choose(93, 100, 3), // 노드 7 px · 마커 3 px — 마커
|
||||
nodeBeatsMark: choose(93, 100, 9), // 노드 7 px · 마커 9 px — 노드
|
||||
tie: input.pressWinner(8, 6, 8, 8), // 같으면 마커 → 노드 → 손잡이
|
||||
handleDistance: input.handleAtScreen(curves, toScreen, 111, 100, 14)?.distance ?? null,
|
||||
};
|
||||
|
||||
// ② 스냅만 바꾼 뒤 노선 처음으로 — 다리 초안(433-14 ②)
|
||||
const snap = (x) => ({
|
||||
planned: [
|
||||
[0, 0],
|
||||
[x, 0],
|
||||
[100, 0],
|
||||
],
|
||||
curveOn: [true, true, true],
|
||||
curveRadius: [null, null, null],
|
||||
curveLock: [null, null, null],
|
||||
curveArc: [null, null, null],
|
||||
apexLock: [false, false, false],
|
||||
picked: -1,
|
||||
});
|
||||
const draft = (snapChanged) => {
|
||||
const told = [];
|
||||
const bridge = { edited: (d) => told.push(d ? d.nodes[1][0] : null), saved: () => null };
|
||||
const history = draftLink.watch(
|
||||
edit.createRouteEditHistory(snap(50)),
|
||||
bridge,
|
||||
() => ({ stationSnap: true, snapStepM: 1 }),
|
||||
() => snapChanged,
|
||||
);
|
||||
history.commit(snap(60));
|
||||
history.undo();
|
||||
history.redo();
|
||||
history.undo();
|
||||
return told;
|
||||
};
|
||||
const old = (() => {
|
||||
const told = [];
|
||||
const bridge = { edited: (d) => told.push(d ? d.nodes[1][0] : null), saved: () => null };
|
||||
const history = draftLink.watch(edit.createRouteEditHistory(snap(50)), bridge, () => ({}));
|
||||
history.commit(snap(60));
|
||||
history.undo();
|
||||
return told;
|
||||
})();
|
||||
|
||||
// ③ 맞추지 않은 기준에서 막힌 걸음만 하고 놓음(433-14 ① · req269 idem 재현) — 막힌 걸음 미리보기
|
||||
// (`fitPreview` = `fitDrag`)가 노선을 바꿈 → `endDrag` 가 맞추기 전 자리(`dragRaw`)로 되돌림.
|
||||
const fit = loadTs(path.join(page, "logic", "B05_Profile_RouteEdit_StationFit.ts"));
|
||||
const LIMITS = { minRadiusM: 30, limitRadiusM: 12, limitArcM: 0, intervalM: 20 };
|
||||
const ROUTE = [[0, 0]];
|
||||
{
|
||||
let seed = 1;
|
||||
const rnd = () => ((seed = (seed * 16807) % 2147483647), seed / 2147483647);
|
||||
let a = 0;
|
||||
for (let i = 0; i < 14; i += 1) {
|
||||
a += (rnd() - 0.5) * 1.6;
|
||||
const L = 35 + rnd() * 60;
|
||||
const q = ROUTE[ROUTE.length - 1];
|
||||
ROUTE.push([q[0] + Math.cos(a) * L, q[1] + Math.sin(a) * L]);
|
||||
}
|
||||
}
|
||||
const cols = (planned) => ({
|
||||
planned,
|
||||
curveOn: planned.map(() => true),
|
||||
curveRadius: planned.map(() => null),
|
||||
curveLock: planned.map(() => null),
|
||||
curveArc: planned.map(() => null),
|
||||
apexLock: planned.map((_, i) => i === 0 || i === planned.length - 1),
|
||||
});
|
||||
const gap = (a, b) => Math.max(...a.map((v, i) => Math.hypot(v[0] - b[i][0], v[1] - b[i][1])));
|
||||
const blocked = { drags: 0, previewMoved: 0, leftAfterRelease: 0 };
|
||||
for (let K = 1; K < ROUTE.length - 1; K += 1) {
|
||||
const base = ROUTE.map(([x, y]) => [x, y]);
|
||||
const dragRaw = base.map(([x, y]) => [x, y]); // 막힌 걸음 = 누르기 전 자리 그대로 맞춤
|
||||
let planned = fit.fitDrag(cols(base), base, K, LIMITS);
|
||||
blocked.drags += 1;
|
||||
if (gap(planned, base) > 1e-6) blocked.previewMoved += 1;
|
||||
dragRaw.forEach((vertex, index) => (planned[index] = vertex)); // endDrag(!dragMoved && dragRaw)
|
||||
if (gap(planned, base) > 1e-6) blocked.leftAfterRelease += 1;
|
||||
}
|
||||
|
||||
process.stdout.write(
|
||||
JSON.stringify({ pick, blocked, draft: { snapChanged: draft(true), same: draft(false), old } }),
|
||||
);
|
||||
@@ -12,7 +12,7 @@
|
||||
- B05 오른쪽 횡단 판 = 묶음 그 측점 — 계획고 · 절토 면적 · 표본 = B06 카드 · 서버 길 없음.
|
||||
못박는 것(365-4)
|
||||
- 저장 노선 ▲▼ 한 번 = 횡단 한 벌 — 묶음 cross 가 다시 셈 창구(상세 설계) 한 번 · 종단도 예약 끔.
|
||||
- 중심점 좌우 = 노선 차례는 cross 전에 밀림 → cross 한 번(계획고 맞춤 차례만).
|
||||
- 노선 한 걸음 + 계획고 맞춤 = 노선 차례는 cross 전에 밀림 → cross 한 번(계획고 맞춤 차례만).
|
||||
- B06 다시 그림 = 겹친 신호(갈아끼움 · 묶음 cross · 초안) 한 번 · profile 차례는 안 그림.
|
||||
못박는 것(365-5)
|
||||
- 들어온 화면 무관 — 설계 칸 = 세션 저장 상세 · 초안 자리 시설 = 차례 짝 · 제원 지문 ·
|
||||
@@ -147,7 +147,7 @@ def test_365_4_저장_노선_계획고_횡단_한_벌(r):
|
||||
assert s["fromDetail"] # 묶음 cross = 상세 설계 그대로(엔진 따로 안 셈)
|
||||
|
||||
|
||||
def test_365_4_중심점_좌우_횡단_한_번(r):
|
||||
def test_365_4_노선_한_걸음_계획고_맞춤_횡단_한_번(r):
|
||||
runs = r["shiftRuns"]
|
||||
assert [x["outcome"] for x in runs] == ["dropped", "done"], runs
|
||||
assert [x["cross"] for x in runs] == [False, True]
|
||||
|
||||
@@ -45,12 +45,12 @@ def test_집기_맴돔_한_함수():
|
||||
assert "pickAtScreen(" in pointer
|
||||
down = pointer[pointer.index('addEventListener("pointerdown"') :]
|
||||
down = down[: down.index('addEventListener("pointermove"')]
|
||||
assert "hover(null);" in down and "const pick = pickAt(px, py);" in down
|
||||
assert "hover(null);" in down and "const pick = choice.pick;" in down # chooseAt 안 pickAt(433)
|
||||
assert "nodeAt(px, py)" not in down and "stationAtScreen(" not in down
|
||||
move = pointer[pointer.index('addEventListener("pointermove"') :]
|
||||
move = move[: move.index("const endDrag")]
|
||||
# 끄는 동안은 앞에서 돌아감 — 맴돔은 끌기 갈래 뒤
|
||||
assert move.index("if (dragNode >= 0)") < move.index("pickAt(px, py)")
|
||||
assert move.index("if (dragNode >= 0)") < move.index("chooseAt(px, py)")
|
||||
assert "hover(over);" in move
|
||||
assert 'addEventListener("pointerleave"' in pointer
|
||||
|
||||
|
||||
@@ -93,6 +93,16 @@ def test_끌기_훑기_놓은_뒤_측점_밖_0(got, mode) -> None:
|
||||
assert swept["strayed"] == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", ["auto", "fixed"])
|
||||
def test_손잡이_끌기_훑기_놓은_뒤_측점_밖_0(got, mode) -> None:
|
||||
"""433-21 ① — 손잡이(BC · EC) 끌기도 노드 갈래와 같게: 미리보기 측점 밖이 늘면 그 걸음 되돌림 →
|
||||
놓은 뒤 MC 가 다 측점 위(되돌림 없으면 216 중 20 남음 = 기준이 고침을 잼)."""
|
||||
swept = got["sweptHandle"][mode]
|
||||
assert swept["drags"] == 216 and swept["released"] > 0
|
||||
assert swept["offAfter"] == 0, swept["worst"]
|
||||
assert got["sweptHandle"]["unguarded"][mode]["offAfter"] > 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("step", ["0.1", "1", "10", "20"])
|
||||
def test_미리보기는_쌓이지_않고_놓을_때와_같음(got, step) -> None:
|
||||
drag = got["drag"][step]
|
||||
@@ -105,7 +115,7 @@ def test_끌기_방향키_편집기_이음() -> None:
|
||||
pointer = (B05 / "B05_Profile_UI_RouteEdit_Pointer.ts").read_text(encoding="utf-8")
|
||||
keys = (B05 / "B05_Profile_UI_RouteEdit_Keys.ts").read_text(encoding="utf-8")
|
||||
edit = (B05 / "B05_Profile_UI_RouteEdit.ts").read_text(encoding="utf-8")
|
||||
assert "params.fitDrag(dragNode)" in pointer
|
||||
assert "params.fitDrag(index)" in pointer and "fitPreview(after.planned, node);" in pointer
|
||||
assert "params.fitDrag(now.index)" in keys and "params.snapStepM()" in keys
|
||||
assert "fit.fitDrag(snapshotNow(), base, index, fitLimits())" in edit
|
||||
# 하한 검사는 맞추기 전 노선으로(맞춤이 IP 를 ±간격/2 밀어 10 m · 측점 No. 에서 헛걸림)
|
||||
@@ -116,6 +126,12 @@ def test_끌기_방향키_편집기_이음() -> None:
|
||||
move = pointer[pointer.index("const previous = state.planned[dragNode];") :]
|
||||
assert move.index("shortfallCrossed(") < move.index("params.offStation() > dragOff")
|
||||
assert move.index("params.offStation() > dragOff") < move.index("dragMoved = true;")
|
||||
# 손잡이 갈래도 같은 차례(433-21 ①) — 하한 · 측점 밖 막힘 뒤에 옮김 표시
|
||||
handle = pointer[pointer.index("const node = dragHandle.node;") :]
|
||||
handle = handle[: handle.index("if (dragNode >= 0) {")]
|
||||
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 edit.count("\n") <= 695
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ def test_공용_고름과_같은_길() -> None:
|
||||
def test_집기_차례는_재기_다음_맨_앞() -> None:
|
||||
pointer = _read("B05_Profile", "B05_Profile_UI_RouteEdit_Pointer.ts")
|
||||
measure = pointer.index("void params.measure.pick(px, py);")
|
||||
mark = pointer.index("if (marks.press(px, py)) {")
|
||||
mark = pointer.index("if (choice.mark && marks.press(px, py)) {") # 433-21 ③ 가까운 것일 때
|
||||
release = pointer.index("marks.deselect();")
|
||||
node = pointer.index("if (hitNode >= 0) {")
|
||||
handle = pointer.index("} else if (dragHandle) {")
|
||||
|
||||
@@ -60,7 +60,7 @@ def test_빈_곳_누름은_마커가_고른_것만_풂(got) -> None:
|
||||
|
||||
def test_누름_마커_먼저_교각점_풂() -> None:
|
||||
pointer = _read("B05_Profile", "B05_Profile_UI_RouteEdit_Pointer.ts")
|
||||
down = pointer[pointer.index("if (marks.press(px, py)) {") :]
|
||||
down = pointer[pointer.index("if (choice.mark && marks.press(px, py)) {") :]
|
||||
branch = down[: down.index("marks.deselect();")]
|
||||
assert "params.setPicked(-1);" in branch and "params.syncCurveBar();" in branch
|
||||
assert "canvas.setPointerCapture(event.pointerId);\n return;" in branch
|
||||
@@ -68,9 +68,10 @@ def test_누름_마커_먼저_교각점_풂() -> None:
|
||||
|
||||
|
||||
def test_맴돔_커서_마커_먼저() -> None:
|
||||
"""433-21 ③ — 맴돔 · 커서는 누름과 같은 판정(`chooseAt` · 노드 가운데 밖이면 가까운 것)."""
|
||||
pointer = _read("B05_Profile", "B05_Profile_UI_RouteEdit_Pointer.ts")
|
||||
over = pointer.index("const onMark = !measuring && marks.over(px, py);")
|
||||
pick = pointer.index("const over = measuring || onMark ? null : pickAt(px, py);")
|
||||
over = pointer.index("const onMark = choice?.mark === true;")
|
||||
pick = pointer.index("const over = choice?.pick ?? null;")
|
||||
cursor = pointer.index("canvas.style.cursor = onMark")
|
||||
assert over < pick < cursor
|
||||
|
||||
|
||||
@@ -157,7 +157,13 @@ def test_편집기_배선() -> None:
|
||||
assert "curveBar.noteFit(snap.text(), before, index)" in main # 놓을 때 맞춤 보고
|
||||
snap = main.split("bindStationSnap(chrome", 1)[1][:400]
|
||||
assert "tellDraft();" in snap # 끔 · 간격 바꿈 즉시 계산 줄
|
||||
assert "snap.changed()" in main.split("function tellDraft()", 1)[1][:400]
|
||||
assert (
|
||||
"lineChanged(history) || otherChanged()" in main.split("function tellDraft()", 1)[1][:400]
|
||||
)
|
||||
assert (
|
||||
"const otherChanged = (): boolean => pickedTerrain() !== designTerrain || snap.changed();"
|
||||
in main
|
||||
) # 433-14 ②
|
||||
assert "plan.node_edits" in main
|
||||
assert "nodes: () => ({ planned, curveOn, curveRadius, curveLock, curveArc })" in main
|
||||
assert len(main.splitlines()) <= 695
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
"""453 B05 평면 편집 — 집기 한 판정 · 손잡이 끌기 · 막힌 걸음 놓음 · 스냅만 바꾼 초안.
|
||||
|
||||
못박는 것
|
||||
- 433-14 ③ · 433-21 ③ 노드 가운데 6 px = 노드 · 밖 = 마커 · 노드 · 손잡이 중 가까운 것 · 맴돔 커서 같은 판정.
|
||||
- 433-14 ① 막힌 걸음만 하고 놓으면 미리보기 맞춤을 누르기 전 자리로(맞추지 않은 기준 13/13 → 0).
|
||||
- 433-14 ② 스냅 · 분석 지형만 바꾼 뒤 노선이 처음으로 돌아가도 다리 초안 있음(null 아님).
|
||||
- 433-21 ① 손잡이 끌기 훑기는 `test_390_5_snap_step_ops`.
|
||||
|
||||
짝: `helper_453_plan_pick_draft.cjs`(진짜 TS `_Input` · `_History` · `_Draft` · `StationFit`).
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
B05 = HERE.parents[1] / "B05_Profile"
|
||||
|
||||
|
||||
def _read(name: str) -> str:
|
||||
return (B05 / name).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def got() -> dict:
|
||||
proc = subprocess.run(
|
||||
["node", str(HERE / "helper_453_plan_pick_draft.cjs")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=60,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def test_노드_가운데만_노드_밖은_가까운_것(got) -> None:
|
||||
p = got["pick"]
|
||||
assert (
|
||||
p["handleSide"] == "handle" and p["handleCenter"] == "handle"
|
||||
) # 예전 = 노드 12 px 안이라 노드
|
||||
assert p["coreEdge"] == "pick" and p["nodeCore"] == "pick"
|
||||
assert p["nodeCloser"] == "pick" and p["handleOnly"] == "handle" and p["nothing"] == "pick"
|
||||
assert p["handleDistance"] == 3
|
||||
|
||||
|
||||
def test_마커_겹친_교점_가운데는_교점(got) -> None:
|
||||
p = got["pick"]
|
||||
assert p["markOnCore"] == "pick" # 예전 = 마커 먼저
|
||||
assert p["markCloser"] == "mark" and p["nodeBeatsMark"] == "pick" and p["tie"] == "mark"
|
||||
|
||||
|
||||
def test_누름_맴돔_커서_한_판정() -> None:
|
||||
pointer = _read("B05_Profile_UI_RouteEdit_Pointer.ts")
|
||||
down = pointer[pointer.index('addEventListener("pointerdown"') :]
|
||||
down = down[: down.index('addEventListener("pointermove"')]
|
||||
assert "const choice = chooseAt(px, py);" in down
|
||||
assert "if (choice.mark && marks.press(px, py)) {" in down
|
||||
assert "dragHandle = choice.handle;" in down and "handleAt(px, py)" not in down
|
||||
move = pointer[pointer.index('addEventListener("pointermove"') :]
|
||||
move = move[: move.index("const endDrag")]
|
||||
hover = move[move.index("const measuring") :]
|
||||
assert "chooseAt(px, py)" in hover and "handleAt(px, py)" not in hover
|
||||
assert "marks.over(" not in pointer and "pressWinner(" in pointer
|
||||
|
||||
|
||||
def test_막힌_걸음만_놓으면_되돌림(got) -> None:
|
||||
b = got["blocked"]
|
||||
assert b["drags"] == 13 and b["previewMoved"] == 13 and b["leftAfterRelease"] == 0
|
||||
pointer = _read("B05_Profile_UI_RouteEdit_Pointer.ts")
|
||||
end = pointer[pointer.index("const endDrag") :]
|
||||
assert end.index("if (!dragMoved && dragRaw) {") < end.index("if (dragMoved) {")
|
||||
|
||||
|
||||
def test_스냅만_바꾼_뒤_처음으로_초안_있음(got) -> None:
|
||||
d = got["draft"]
|
||||
assert d["snapChanged"] == [60, 50, 60, 50] # 처음 노선(50)이어도 초안
|
||||
assert d["same"] == [60, None, 60, None] and d["old"] == [60, None]
|
||||
edit = _read("B05_Profile_UI_RouteEdit.ts")
|
||||
assert "draftLink.watch(history, options.draft, snap.draft, otherChanged);" in edit
|
||||
assert "const dirty = lineChanged(history) || otherChanged();" in edit
|
||||
assert "changed: () => (!!history && lineChanged(history)) || otherChanged()," in edit
|
||||
Reference in New Issue
Block a user