fix(B05 평면 구조물 마커): 마커 맨 위 · 다른 곳 누름 · Esc 고름 풂 · 오른쪽 판 구조물 · 관 측점 횡단 (409 · 411-A)
- 마커 그림 = 맨 마지막(교각점 · 손잡이 · 맴돔 위) · 누름 · 맴돔 · 커서 = 재기 다음 마커 먼저 · 마커 누름 = 고른 교각점 풂 - 마커 아닌 곳 누름 · Esc = 공용 고름을 b05-plan 으로 비움(오른쪽 횡단 판 그대로) - 오른쪽 판 = 구조물 · 관 고름도 그 측점 횡단 · 묶음 뒤 열쇠로 붙듦(옮긴 뒤 새 자리) - 시험 399 차례 고쳐 씀 · 새 409 · 411-A Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWh5wNNycMkbwMpACz1rte
This commit is contained in:
@@ -36,10 +36,19 @@ global.window = {
|
||||
clearTimeout: () => {},
|
||||
clearInterval: () => {},
|
||||
};
|
||||
let prevented = 0;
|
||||
const key = (type, name) =>
|
||||
(listeners[type] || []).forEach((fn) =>
|
||||
fn({ key: name, repeat: false, target: null, preventDefault() {}, stopPropagation() {} }),
|
||||
fn({
|
||||
key: name,
|
||||
repeat: false,
|
||||
target: null,
|
||||
preventDefault: () => (prevented += 1),
|
||||
stopPropagation() {},
|
||||
}),
|
||||
);
|
||||
/** 공용 고름에 적은 것(409 풀기) — [값, 낸 쪽]. */
|
||||
const written = [];
|
||||
|
||||
const STUBS = {
|
||||
"../A00_Common/touch_bridge": {
|
||||
@@ -50,7 +59,16 @@ const STUBS = {
|
||||
"../A00_Common/input_prefs": { isTrackpadPinch: () => false },
|
||||
"../B04_PreProcess/B04_PreProcess_UI_MapRender": { computeMaxScale: () => 16 },
|
||||
"../A00_Common/design/design_store": { draftProjectId: () => "p1" },
|
||||
"../A00_Common/design/design_selection": { PIPE_MATCH_M: 0.51, readSelection: () => selection },
|
||||
"../A00_Common/design/design_selection": {
|
||||
PIPE_MATCH_M: 0.51,
|
||||
NO_SELECTION: { chainageM: null, structureId: null },
|
||||
readSelection: () => selection,
|
||||
selectDesign: (projectId, value, options) => {
|
||||
written.push([value.chainageM, options.from]);
|
||||
selection = value.chainageM === null ? null : value;
|
||||
},
|
||||
},
|
||||
"./B05_Profile_UI_RouteEdit_Cross": { PLAN_PICK_FROM: "b05-plan" },
|
||||
"../B06_Section/B06_Section_UI_Cross_Panel_Base": { lastPointerWithin: () => true },
|
||||
"./B05_Profile_UI_Profile_Edit": { HOLD_DELAY_MS: 500, HOLD_INTERVAL_MS: 100 },
|
||||
"./B05_Profile_Api_Structures": {
|
||||
@@ -315,7 +333,24 @@ const was = redrawn;
|
||||
marks.planMarksChanged("p1");
|
||||
out.redrawnOnChange = redrawn - was;
|
||||
|
||||
// ⑥ 순수 셈
|
||||
// ⑥ 고름 풀기(409) — 고른 것 없으면 아무 일 없음 · 있으면 공용 고름을 b05-plan 으로 비움 · Esc 도
|
||||
selection = null;
|
||||
out.deselectNone = [input.deselect(), written.length];
|
||||
selection = { chainageM: 40, structureId: "A" };
|
||||
out.deselectPicked = [input.deselect(), ...written.slice(-1), selection];
|
||||
selection = { chainageM: 80.17, structureId: null }; // 관 고름(누가거리만)
|
||||
prevented = 0;
|
||||
key("keydown", "Escape");
|
||||
out.escapePicked = [...written.slice(-1), selection, prevented];
|
||||
const writes = written.length;
|
||||
key("keydown", "Escape"); // 고른 것 없음 = 공용 Esc 몫(안 막음)
|
||||
out.escapeNone = [written.length - writes, prevented];
|
||||
// 마커 누름 = 고름(공용 고름 길) · 법선 · 창 — 그린 차례는 `_Render` 맨 끝(시험이 원본으로 봄)
|
||||
input.press(80, 0);
|
||||
input.release();
|
||||
out.pressAfterRelease = selects[selects.length - 1];
|
||||
|
||||
// ⑦ 순수 셈
|
||||
out.logic = {
|
||||
round: [logic.markChainage(41.4, 200), logic.markChainage(41.6, 200)],
|
||||
clamp: [logic.markChainage(-3, 200), logic.markChainage(250, 199.6)],
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
/* 411-A B05 평면 오른쪽 횡단 판 — 구조물 · 관을 골라도 그 측점 횡단 · 옮긴 뒤에도 열쇠로 따라감.
|
||||
* `_RouteEdit_Cross` 를 Node 로 돌려 JSON 한 줄(`test_411_a_cross_structure.py` 가 판정). 묶음 · 그림 · 가게는 가짜 ·
|
||||
* 고름 구독 · 측점 짚기 · 판 채우기는 진짜.
|
||||
* node helper_411_a_cross_structure.cjs
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const B05 = path.join(__dirname, "..", "..", "B05_Profile");
|
||||
const out = {};
|
||||
|
||||
/* 가짜 DOM — 판(제목 · 상자 · 발) 글만 봄 */
|
||||
function fakeElement() {
|
||||
const parts = new Map();
|
||||
const element = {
|
||||
textContent: "",
|
||||
children: [],
|
||||
isConnected: true,
|
||||
clientHeight: 150,
|
||||
setAttribute() {},
|
||||
append: (...items) => element.children.push(...items),
|
||||
replaceChildren: (...items) => (element.children = items),
|
||||
getBoundingClientRect: () => ({ width: 240, height: 150 }),
|
||||
querySelector: (selector) => {
|
||||
if (!parts.has(selector)) parts.set(selector, fakeElement());
|
||||
return parts.get(selector);
|
||||
},
|
||||
};
|
||||
return element;
|
||||
}
|
||||
global.document = { createElement: fakeElement };
|
||||
global.ResizeObserver = class {
|
||||
observe() {}
|
||||
disconnect() {}
|
||||
};
|
||||
|
||||
let listener = null;
|
||||
let stations = [];
|
||||
const route = { id: 1 };
|
||||
const subscribers = [];
|
||||
const pipe = {
|
||||
output: (stage) => (stage === "ground" ? { stations } : {}),
|
||||
input: () => route,
|
||||
subscribe: (fn) => (subscribers.push(fn), () => {}),
|
||||
};
|
||||
const crossStage = () => subscribers.forEach((fn) => fn({ kind: "stage", stage: "cross" }));
|
||||
|
||||
const STUBS = {
|
||||
"../A00_Common/b_svg_zoom_pan": { attachSvgZoomPan() {} },
|
||||
"../A00_Common/design/design_pipeline_page": { pagePipeline: () => pipe },
|
||||
"../A00_Common/design/design_selection": {
|
||||
nearestWithin: (items, at, of) => {
|
||||
let best;
|
||||
let gap = 0.51;
|
||||
for (const item of items)
|
||||
if (Math.abs(of(item) - at) < gap) [best, gap] = [item, Math.abs(of(item) - at)];
|
||||
return best;
|
||||
},
|
||||
selectDesign() {},
|
||||
subscribeSelection: (_projectId, fn) => ((listener = fn), () => {}),
|
||||
},
|
||||
"../A00_Common/design/design_store": {
|
||||
useDesignStore: () => ({ subscribe: () => () => {}, get: () => null }),
|
||||
},
|
||||
"./logic/B05_Profile_Pipeline_Detail": {
|
||||
stationSection: (station) => ({ chainage_m: station.chainageM, samples: [] }),
|
||||
},
|
||||
"./B05_Profile_UI_RouteEdit_Cross_Draw": {
|
||||
buildCrossSvg: () => ({ svg: fakeElement(), layer: null, width: 240, height: 150 }),
|
||||
CROSS_PLOT_PAD: 0,
|
||||
summarizeCross: (preview) => `횡단 ${preview.chainage_m}`,
|
||||
},
|
||||
"../A00_Common/b_page_state": { readStateRaw: () => null },
|
||||
"../A00_Common/design/design_page_route": { pageRouteId: () => null },
|
||||
"../common_util/common_util_station": { stationLabel: (chainageM) => String(chainageM) },
|
||||
"./B05_Profile_UI_RouteEdit_Chrome": { sidePanelsOf: new Map() },
|
||||
"./B05_Profile_UI_RouteEdit_Cross_Card": {
|
||||
routeCrossCard: () => fakeElement(),
|
||||
routeCrossSource: () => null,
|
||||
},
|
||||
};
|
||||
const cache = new Map();
|
||||
function loadTs(full) {
|
||||
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.join(path.dirname(full), `${request}.ts`));
|
||||
return require(request);
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
||||
cache.set(full, box.exports);
|
||||
return box.exports;
|
||||
}
|
||||
|
||||
const cross = loadTs(path.join(B05, "B05_Profile_UI_RouteEdit_Cross.ts"));
|
||||
const station = (key, chainageM, kind, id = null) => ({ key, chainageM, kind, id });
|
||||
const grid = [
|
||||
station("end:BP", 0, "end"),
|
||||
station("grid:1", 20, "grid"),
|
||||
station("grid:2", 40, "grid"),
|
||||
];
|
||||
const side = fakeElement();
|
||||
const preview = cross.createCrossPreview({
|
||||
projectId: "p1",
|
||||
side,
|
||||
request: () => ({ vertices: [], min_radius_m: 15, station_interval_m: 20 }),
|
||||
});
|
||||
const [current] = side.children;
|
||||
const shownNow = () => ({
|
||||
station: current.querySelector(".b05-routeedit__cross-station").textContent,
|
||||
foot: current.querySelector(".b05-routeedit__cross-foot").textContent,
|
||||
});
|
||||
const pick = (chainageM, structureId, from) => listener({ chainageM, structureId }, from);
|
||||
|
||||
// ① 구조물 고름(id) = 그 측점 횡단 · 법선은 마커 몫(structure)
|
||||
stations = [
|
||||
...grid,
|
||||
station("structure:A", 45, "structure", "A"),
|
||||
station("pipe:p1", 80.17, "pipe", "p1"),
|
||||
];
|
||||
pick(45, "A", "b05");
|
||||
out.structurePick = { ...shownNow(), structure: preview.watched()?.structure ?? null };
|
||||
// ② 옮김 — 묶음이 새 자리로(같은 열쇠) · cross 차례 = 새 자리 횡단(고름은 옛 45 그대로)
|
||||
stations = [
|
||||
...grid,
|
||||
station("structure:A", 52, "structure", "A"),
|
||||
station("pipe:p1", 80.17, "pipe", "p1"),
|
||||
];
|
||||
crossStage();
|
||||
out.structureMoved = { ...shownNow(), at: preview.watched()?.chainageM ?? null };
|
||||
// ③ 관 고름(누가거리만) = 그 측점 횡단
|
||||
pick(80.17, null, "b05");
|
||||
out.pipePick = { ...shownNow(), structure: preview.watched()?.structure ?? null };
|
||||
// ④ 평면 마커 고름 풂(b05-plan) = 판 그대로
|
||||
pick(null, null, "b05-plan");
|
||||
out.planRelease = shownNow();
|
||||
// ⑤ 일반측점 고름 = 그 측점 횡단 · 법선(structure 없음)
|
||||
pick(20, null, "b05");
|
||||
out.gridPick = { ...shownNow(), structure: preview.watched()?.structure ?? null };
|
||||
// ⑥ 묶음 전 구조물 고름 — 열쇠 없이 기다렸다 묶음 뒤 열쇠로 붙듦 · 그 뒤 옮김도 따라감
|
||||
stations = [];
|
||||
pick(60, "B", "b05");
|
||||
out.beforePipeline = shownNow();
|
||||
stations = [...grid, station("structure:B", 60, "structure", "B")];
|
||||
crossStage();
|
||||
out.afterPipeline = shownNow();
|
||||
stations = [...grid, station("structure:B", 66, "structure", "B")];
|
||||
crossStage();
|
||||
out.latchedMoved = shownNow();
|
||||
// ⑦ 다른 보기가 풂(null · b05) = 판 비움(382-2 그대로)
|
||||
pick(null, null, "b05");
|
||||
out.otherRelease = { ...shownNow(), watched: preview.watched() };
|
||||
console.log(JSON.stringify(out));
|
||||
@@ -65,18 +65,23 @@ def test_공용_고름과_같은_길() -> None:
|
||||
assert " onStructureSelect,\n onStructureMarkMove,\n" in view # 알약 콜백 = 같은 함수
|
||||
assert "select: onStructureSelect," in view and "move: onStructureMarkMove," in view
|
||||
render = _read("B05_Profile", "B05_Profile_UI_RouteEdit_Render.ts")
|
||||
labels = render.index("drawContourLabels(context, scene, band, style);\n drawStructureMarks")
|
||||
labels = render.index("drawContourLabels(context, scene, band, style);")
|
||||
nodes = render.index("scene.planned.forEach((vertex, index) => {")
|
||||
assert labels < nodes # 라벨 위 · 교각점 아래
|
||||
hover = render.index("drawPickHover(context, scene, line, style, NODE_R);")
|
||||
marks = render.index("drawStructureMarks(context, scene, line, style);")
|
||||
turned = render.index("context.restore(); // 회전 끝")
|
||||
assert labels < nodes < hover < marks < turned # 맨 위(409 — 교각점 · 손잡이 · 맴돔 위)
|
||||
|
||||
|
||||
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)) {")
|
||||
release = pointer.index("marks.deselect();")
|
||||
node = pointer.index("if (hitNode >= 0) {")
|
||||
handle = pointer.index("} else if (dragHandle) {")
|
||||
mark = pointer.index("} else if (marks.press(px, py)) {")
|
||||
tick = pointer.index('if (!pick || !("chainage" in pick)) return false;')
|
||||
assert node < handle < mark < tick
|
||||
assert measure < mark < release < node < handle < tick # 409
|
||||
|
||||
|
||||
def test_끌기_1m_정수_놓을_때_한_번(got) -> None:
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
"""409 B05 평면 구조물 마커 맨 위 · 다른 곳 누르면 고름 풀림(요구 252-4).
|
||||
|
||||
그림 = 맨 마지막(교각점 · 손잡이 · 맴돔 위) · 누름 · 맴돔 · 커서 = 재기 다음 마커 먼저 ·
|
||||
마커 누르면 고른 교각점 풂 ·
|
||||
마커 아닌 곳 누름 · Esc = 공용 고름을 `b05-plan` 으로 비움(오른쪽 횡단 판 그대로).
|
||||
TS 셈은 `helper_399_plan_structure_marks.cjs`(⑥ 고름 풀기).
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_409_mark_top_deselect.py -q
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
HELPER = Path(__file__).with_name("helper_399_plan_structure_marks.cjs")
|
||||
|
||||
|
||||
def _read(*parts: str) -> str:
|
||||
return (ROOT.joinpath(*parts)).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
@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:
|
||||
assert got["deselectNone"] == [False, 0]
|
||||
|
||||
|
||||
def test_풀기_공용_고름을_b05_plan_으로_비움(got) -> None:
|
||||
assert got["deselectPicked"] == [True, [None, "b05-plan"], None]
|
||||
|
||||
|
||||
def test_Esc_마커_고름만_풂(got) -> None:
|
||||
assert got["escapePicked"] == [[None, "b05-plan"], None, 1] # 관 고름도 · 공용 Esc 막음
|
||||
assert got["escapeNone"] == [0, 1] # 고른 것 없음 = 공용 Esc 몫
|
||||
|
||||
|
||||
def test_마커_누름은_그대로_고름(got) -> None:
|
||||
assert got["pressAfterRelease"] == "A"
|
||||
|
||||
|
||||
def test_누름_마커_먼저_교각점_풂() -> None:
|
||||
pointer = _read("B05_Profile", "B05_Profile_UI_RouteEdit_Pointer.ts")
|
||||
down = pointer[pointer.index("if (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
|
||||
assert "else if (marks.press(" not in pointer # 옛 차례(교각점 · 손잡이 다음) 걷힘
|
||||
|
||||
|
||||
def test_맴돔_커서_마커_먼저() -> None:
|
||||
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);")
|
||||
cursor = pointer.index("canvas.style.cursor = onMark")
|
||||
assert over < pick < cursor
|
||||
|
||||
|
||||
def test_오른쪽_판은_제_이름_알림을_거름() -> None:
|
||||
cross = _read("B05_Profile", "B05_Profile_UI_RouteEdit_Cross.ts")
|
||||
assert "if (from !== PLAN_PICK_FROM) follow(selection);" in cross
|
||||
marks = _read("B05_Profile", "B05_Profile_UI_RouteEdit_StructureMarks.ts")
|
||||
assert "selectDesign(projectId, NO_SELECTION, { from: PLAN_PICK_FROM });" in marks
|
||||
@@ -0,0 +1,58 @@
|
||||
"""411-A B05 평면 오른쪽 횡단 판 — 구조물 · 관을 골라도 그 측점 횡단(요구 252-6) ·
|
||||
옮긴 뒤에도 그 측점.
|
||||
|
||||
TS 셈은 `helper_411_a_cross_structure.cjs`(Node · `_RouteEdit_Cross` 진짜 · 묶음 · 그림 가짜).
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_411_a_cross_structure.py -q
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
HELPER = Path(__file__).with_name("helper_411_a_cross_structure.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:
|
||||
assert got["structurePick"] == {"station": "45", "foot": "횡단 45", "structure": True}
|
||||
|
||||
|
||||
def test_옮긴_뒤_열쇠로_새_자리(got) -> None:
|
||||
assert got["structureMoved"] == {"station": "52", "foot": "횡단 52", "at": 52}
|
||||
|
||||
|
||||
def test_관_고름_그_측점_횡단(got) -> None:
|
||||
assert got["pipePick"] == {"station": "80.17", "foot": "횡단 80.17", "structure": True}
|
||||
|
||||
|
||||
def test_평면_마커_풂은_판_그대로(got) -> None:
|
||||
assert got["planRelease"] == {"station": "80.17", "foot": "횡단 80.17"}
|
||||
|
||||
|
||||
def test_일반측점_그대로(got) -> None:
|
||||
assert got["gridPick"] == {"station": "20", "foot": "횡단 20", "structure": None}
|
||||
|
||||
|
||||
def test_묶음_전_고름도_열쇠로_붙듦(got) -> None:
|
||||
assert got["beforePipeline"]["foot"] == "계산 중…"
|
||||
assert got["afterPipeline"] == {"station": "60", "foot": "횡단 60"}
|
||||
assert got["latchedMoved"] == {"station": "66", "foot": "횡단 66"}
|
||||
|
||||
|
||||
def test_다른_보기_풂은_판_비움(got) -> None:
|
||||
assert got["otherRelease"]["station"] == "" and got["otherRelease"]["watched"] is None
|
||||
Reference in New Issue
Block a user