Merge remote-tracking branch 'origin/sub_laptop_1' into main_laptop_1
This commit is contained in:
@@ -145,10 +145,6 @@ export const STATE_REGISTRY = {
|
||||
* 암 경계선(`rockb`)과 같은 자리·같은 꼴이다. 재계산에 **계산 전에** 실어 보내야
|
||||
* 설계선이 새 경사로 그려진다(값만 베껴 붙이면 그림과 숫자가 어긋난다). */
|
||||
cutslope: { bucket: "draft", scope: "route" },
|
||||
/** 소단 제원(측점키 → {width_m, interval_m, slope_deg}) — 계획서 3-9.
|
||||
* 사용자가 구간에 놓은 값이라 재계산에 함께 실어 보내야 한다. 안 실으면 계획선을
|
||||
* 고치는 순간 계단이 사라진다(암 경계선이 옛 키를 보던 것과 같은 자리). */
|
||||
berm: { bucket: "draft", scope: "route" },
|
||||
/** 표준 횡단면 설정 패널의 편집값.
|
||||
*
|
||||
* 칸이 비었으면 `sections/context` 의 저장분(`stored_standard_cross_section`)으로 채운다
|
||||
@@ -239,6 +235,9 @@ export const STATE_REGISTRY = {
|
||||
"design-stations": { bucket: "result", scope: "project" },
|
||||
"design-profile": { bucket: "result", scope: "project" },
|
||||
"design-footprint": { bucket: "result", scope: "project" },
|
||||
/** 소단 사본 — 구조물(소단)에서 셈한 구간 목록(`design_facility_lane` 만 씀 · 고침은 `structures` 칸).
|
||||
* 다시 셈에 실어 보냄 · 안 실으면 계획선을 고치는 순간 계단이 사라짐(3-9 · 477-4). */
|
||||
berm: { bucket: "result", scope: "route" },
|
||||
} as const satisfies Record<string, StateEntry>;
|
||||
|
||||
export type StateName = keyof typeof STATE_REGISTRY;
|
||||
|
||||
@@ -31,7 +31,6 @@ const NOT_EDITS = new Set<StateName>([
|
||||
"structure-pick",
|
||||
"structure-type-cache",
|
||||
"std-cross",
|
||||
"berm", // 소단 사본 — 열 때 시설 줄이 구조물에서 씀(272 · 고침은 `structures` 칸)
|
||||
]);
|
||||
const OTHER_PAGE = /^(b0[89]|d0\d|e0\d)-/;
|
||||
|
||||
|
||||
@@ -114,10 +114,18 @@ const changed = (): void => hooks.forEach((entry) => entry.change?.());
|
||||
function settle(): void {
|
||||
clearTimeout(settleTimer);
|
||||
settling = true;
|
||||
settleTimer = setTimeout(() => {
|
||||
settling = false;
|
||||
active?.record();
|
||||
}, SETTLE_MS);
|
||||
settleTimer = setTimeout(flushSettle, SETTLE_MS);
|
||||
// ↶ 는 기다리지 않고 켬(477-3) — 같은 손길의 칸 쓰기가 끝난 한 틱 뒤 단추만 맞춤
|
||||
setTimeout(changed, 0);
|
||||
}
|
||||
|
||||
/** 기다리던 찍기를 지금 — 기다림이 끝났거나 그 사이 ↶ ↷ 를 누름(477-3 · 한 걸음 그대로). */
|
||||
function flushSettle(): void {
|
||||
if (!settling) return;
|
||||
clearTimeout(settleTimer);
|
||||
settling = false;
|
||||
active?.recordNow();
|
||||
changed();
|
||||
}
|
||||
|
||||
function watchPointer(): void {
|
||||
@@ -167,6 +175,8 @@ function restoreSnapshot(projectId: string, snapshot: HistorySnapshot): void {
|
||||
export interface ProfileHistory {
|
||||
/** 조작이 끝난 뒤 현재 상태를 이력에 쌓는다(직전과 같으면 무시). */
|
||||
record(): void;
|
||||
/** 누름이 남아 있어도 지금 찍음 — ↶ 단추 누름 중 기다리던 찍기(477-3). */
|
||||
recordNow(): void;
|
||||
/** 연속 조작(길게 누르기) 동안 기록을 미룬다 — 20칸 이동이 되돌리기 20번이 되는 것을 막는다. */
|
||||
pause(): void;
|
||||
/** 연속 조작이 끝났음을 알린다 — 그 동안의 변화를 **한 덩어리로** 한 번만 쌓는다. */
|
||||
@@ -224,6 +234,10 @@ export function createProfileHistory(
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 찍기를 기다리는 동안 세션이 지금 걸음과 다름 — ↶ 를 바로 켜는 몫(477-3). */
|
||||
const pending = (): boolean =>
|
||||
settling && !restoring && !sameSnapshot(takeSnapshot(projectId, stack[cursor]), stack[cursor]);
|
||||
|
||||
const history: ProfileHistory = {
|
||||
record() {
|
||||
if (restoring || paused) return;
|
||||
@@ -231,6 +245,10 @@ export function createProfileHistory(
|
||||
releaseWaiters.add(history.record);
|
||||
return;
|
||||
}
|
||||
history.recordNow();
|
||||
},
|
||||
recordNow() {
|
||||
if (restoring || paused) return;
|
||||
sync();
|
||||
restAt = gestures;
|
||||
const snapshot = takeSnapshot(projectId, stack[cursor]);
|
||||
@@ -253,10 +271,11 @@ export function createProfileHistory(
|
||||
paused = false;
|
||||
history.record();
|
||||
},
|
||||
undo: () => apply(cursor - 1),
|
||||
redo: () => apply(cursor + 1),
|
||||
canUndo: () => cursor > 0,
|
||||
canRedo: () => cursor < stack.length - 1,
|
||||
// 기다리던 찍기 먼저 — 바로 누른 ↶ = 방금 조작 되돌림 · 묶음 한 걸음(477-3)
|
||||
undo: () => (flushSettle(), apply(cursor - 1)),
|
||||
redo: () => (flushSettle(), apply(cursor + 1)),
|
||||
canUndo: () => cursor > 0 || pending(),
|
||||
canRedo: () => cursor < stack.length - 1 && !pending(),
|
||||
};
|
||||
rebaseActive = () => {
|
||||
if (restoring || paused || pointerHeld || settling || gestures !== restAt) return;
|
||||
|
||||
@@ -16,13 +16,7 @@ import {
|
||||
} from "./B06_Section_Api_Fetch";
|
||||
import type { DesignViewSave } from "../A00_Common/design/design_shared";
|
||||
import { buildCrossPatches, type CrossPatchSources } from "./logic/B06_Section_UI_Page_Patches";
|
||||
import {
|
||||
clearState,
|
||||
readByKey,
|
||||
readState,
|
||||
writeByKey,
|
||||
writeState,
|
||||
} from "../A00_Common/b_page_state";
|
||||
import { clearState, readByKey, writeByKey, writeState } from "../A00_Common/b_page_state";
|
||||
import { kindChangedChainages } from "./B06_Section_Api_Culvert_Options";
|
||||
import {
|
||||
crossDesignChoices,
|
||||
@@ -130,17 +124,7 @@ function putCell(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 세션에 쌓인 소단 구간 목록. 없거나 손상되면 빈 목록.
|
||||
*
|
||||
* 암 경계선과 같은 성격이다 — 확정 전에는 세션에만 있으므로 계획선 재계산에 **함께 실어
|
||||
* 보내야** 한다. 안 실으면 계획선을 고치는 순간 계단이 사라진다(계획서 3-9).
|
||||
*/
|
||||
export function readBermSpans(projectId: string, routeId: number): BermSpan[] {
|
||||
const stored = readState<BermSpan[]>("berm", projectId, routeId);
|
||||
return Array.isArray(stored) ? stored : [];
|
||||
}
|
||||
|
||||
/** 소단 사본(결과 통 · 구조물에서 셈 · 477-4) — 쓰는 곳은 페이지 시설 줄 하나. */
|
||||
export function writeBermSpans(projectId: string, routeId: number, spans: BermSpan[]): void {
|
||||
writeState("berm", spans, projectId, routeId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
"""477-3 · 477-4 (요구 433) — ↶ 바로 켜짐 · 소단 사본 = 결과 통.
|
||||
|
||||
못박는 것
|
||||
- 477-3 조작 놓은 직후(찍기 기다림 400 ms 안) ↶ 켜짐 · 단추 맞춤 알림 · 바뀐 것 없는 누름은 안 켜짐.
|
||||
- 기다림 중 ↶ = 먼저 찍고 되돌림(한 번에 조작 전) · ↷ = 다시 · 서버 다시 셈 뒤 쓰기 묶음은 그대로 한 걸음.
|
||||
- 477-4 `berm` = 결과 통(사진 · 떠남 물음 밖) · 떠남 제외 목록에 사본 줄 없음.
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_477_undo_now_berm.py -q
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
TS_FILE = ROOT / "B05_Profile" / "B05_Profile_UI_Profile_History.ts"
|
||||
LOADER = Path(__file__).resolve().parent / "helper_ts_cjs_load.cjs"
|
||||
|
||||
DRIVER = r"""
|
||||
import { writeFileSync } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
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),
|
||||
};
|
||||
globalThis.window = new EventTarget();
|
||||
window.setTimeout = setTimeout;
|
||||
window.sessionStorage = sessionStorage;
|
||||
window.localStorage = { getItem: (k) => (k === "frd_current_project_id" ? "p1" : null) };
|
||||
const { loadTs } = createRequire(import.meta.url)(process.argv[3]);
|
||||
const { createProfileHistory, onPageHistory } = loadTs(process.argv[4]);
|
||||
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const PIPES = "aislo:draft:pipes:p1";
|
||||
const CROSS = "aislo:draft:crossdesign:p1:7";
|
||||
const BERM = "aislo:result:berm:p1:7";
|
||||
const out = {};
|
||||
let changes = 0;
|
||||
onPageHistory({ change: () => (changes += 1) });
|
||||
const press = () => window.dispatchEvent(new Event("pointerdown"));
|
||||
const lift = () => window.dispatchEvent(new Event("pointerup"));
|
||||
|
||||
// 1. 관 지움 — 놓은 한 틱 뒤 ↶ 켜짐(기다림 400 ms 안)
|
||||
sessionStorage.setItem(PIPES, '[{"c":10},{"c":20}]');
|
||||
sessionStorage.setItem(BERM, "[]");
|
||||
const h = createProfileHistory(() => {});
|
||||
out.openCanUndo = h.canUndo();
|
||||
press();
|
||||
sessionStorage.setItem(PIPES, '[{"c":10}]');
|
||||
lift();
|
||||
await wait(30);
|
||||
out.soonCanUndo = h.canUndo();
|
||||
out.soonChanges = changes;
|
||||
// 바로 ↶ 단추(누름 · 놓음 · 누름 처리) — 기다리던 찍기 먼저 · 한 번에 조작 전
|
||||
press();
|
||||
lift();
|
||||
out.undo = h.undo();
|
||||
out.afterUndo = sessionStorage.getItem(PIPES);
|
||||
out.undoLeft = h.canUndo();
|
||||
out.canRedo = h.canRedo();
|
||||
await wait(450);
|
||||
out.redo = h.redo();
|
||||
out.afterRedo = sessionStorage.getItem(PIPES);
|
||||
out.redoLeft = h.canRedo();
|
||||
|
||||
// 2. 서버 다시 셈 뒤 쓰기(200 ms) — 기다림이 끝나면 조작과 한 걸음
|
||||
data.clear();
|
||||
sessionStorage.setItem(PIPES, "a");
|
||||
sessionStorage.setItem(CROSS, "x");
|
||||
const h2 = createProfileHistory(() => {});
|
||||
press();
|
||||
sessionStorage.setItem(PIPES, "b");
|
||||
lift();
|
||||
await wait(200);
|
||||
sessionStorage.setItem(CROSS, "y");
|
||||
sessionStorage.setItem(BERM, '[{"s":1}]'); // 사본 = 결과 통 — 사진 밖
|
||||
await wait(300);
|
||||
h2.undo();
|
||||
out.grouped = [sessionStorage.getItem(PIPES), sessionStorage.getItem(CROSS)];
|
||||
out.groupedLeft = h2.canUndo();
|
||||
out.bermKept = sessionStorage.getItem(BERM);
|
||||
|
||||
// 3. 바뀐 것 없는 누름 — ↶ 안 켜짐(깜빡임 없음)
|
||||
data.clear();
|
||||
sessionStorage.setItem(PIPES, "a");
|
||||
const h3 = createProfileHistory(() => {});
|
||||
press();
|
||||
lift();
|
||||
await wait(30);
|
||||
out.idleCanUndo = h3.canUndo();
|
||||
writeFileSync(process.argv[2], JSON.stringify(out));
|
||||
"""
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def result(tmp_path_factory: pytest.TempPathFactory) -> dict:
|
||||
out = tmp_path_factory.mktemp("ts_477")
|
||||
(out / "driver.mjs").write_text(DRIVER, encoding="utf-8")
|
||||
run = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", str(out / "driver.mjs"), str(out / "out.json"), str(LOADER), str(TS_FILE)],
|
||||
capture_output=True, text=True, cwd=str(out),
|
||||
) # fmt: skip
|
||||
assert run.returncode == 0, run.stderr
|
||||
return json.loads((out / "out.json").read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def test_놓은_직후_되돌리기_켜짐(result) -> None:
|
||||
assert result["openCanUndo"] is False
|
||||
assert result["soonCanUndo"] is True
|
||||
assert result["soonChanges"] >= 1 # 단추 맞춤 알림이 기다림 전에 감
|
||||
|
||||
|
||||
def test_기다림_중_되돌리기_먼저_찍고_한_걸음(result) -> None:
|
||||
assert result["undo"] is True
|
||||
assert result["afterUndo"] == '[{"c":10},{"c":20}]'
|
||||
assert result["undoLeft"] is False and result["canRedo"] is True
|
||||
assert result["redo"] is True and result["afterRedo"] == '[{"c":10}]'
|
||||
assert result["redoLeft"] is False
|
||||
|
||||
|
||||
def test_서버_다시_셈_묶음_그대로(result) -> None:
|
||||
assert result["grouped"] == ["a", "x"] and result["groupedLeft"] is False
|
||||
assert result["bermKept"] == '[{"s":1}]' # 결과 통 사본은 되돌리기가 안 건드림
|
||||
|
||||
|
||||
def test_바뀐_것_없는_누름은_안_켜짐(result) -> None:
|
||||
assert result["idleCanUndo"] is False
|
||||
|
||||
|
||||
def test_소단_사본은_결과_통() -> None:
|
||||
state = (ROOT / "A00_Common" / "b_page_state.ts").read_text(encoding="utf-8")
|
||||
assert re.search(r'^\s*berm: \{ bucket: "result", scope: "route" \}', state, re.M)
|
||||
leave = (ROOT / "A00_Common" / "design" / "design_leave.ts").read_text(encoding="utf-8")
|
||||
assert '"berm"' not in leave
|
||||
Reference in New Issue
Block a user