feat(514-1 알약 우클릭 삭제): 측점선 없는 구조물(대피로 · 노출형 횡단수로 등)도 종단 알약 · 평면 마커 우클릭 「구조물 삭제」 — 513-3 주인 가르기를 알약 id 로(removeMarkOwner · 관 알약 = 관 지점 정본 · 그 밖 = 좌측 칸 [삭제]와 같은 길) · B05 종단 보기 · B06 횡단 보기 같은 길(onStructureMarkRemove) · 캐시에만 · [저장] 때 나감 · 우클릭 삭제 글 = 관 · 세월교 · BOX 도 「구조물 삭제」 하나 · 시험 test_514_1 · test_513_1 글 기대 고침 — 요구 328-1 · 328-2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,7 @@ const STUBS = {
|
||||
},
|
||||
"./B05_Profile_Api_Structures": { structureAnchorM: (s) => s.chainage_m ?? 0 },
|
||||
"../A00_Common/design/design_selection": { PIPE_MATCH_M: 0.51 },
|
||||
"./B05_Profile_UI_RouteEdit_StructureMarks": { planMarkAt: () => null }, // 마커 없음(514-1 몫)
|
||||
};
|
||||
const { loadTs } = makeLoader({ root: path.join(__dirname, "..", ".."), stubs: STUBS, ui });
|
||||
const shared = loadTs(path.join(B05, "B05_Profile_UI_Profile_Structures.ts"));
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
/* 514-1 알약 · 평면 마커 우클릭 「구조물 삭제」(요구 328-1) — `design_pipe_ids.removeMarkOwner` ·
|
||||
* `_RouteEdit_StructureMenu` 를 Node 로 돌려 JSON 한 줄(`test_514_1_mark_remove.py` 가 판정).
|
||||
* 관 장부 · 메뉴 판 · 마커 집기는 가짜 · 주인 가르기 · 평면 항목 짓기는 진짜.
|
||||
* node helper_514_1_mark_remove.cjs
|
||||
*/
|
||||
const path = require("path");
|
||||
const { makeLoader } = require("./helper_ts_stub_loader.cjs");
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const B05 = path.join(ROOT, "B05_Profile");
|
||||
const opens = [];
|
||||
const calls = [];
|
||||
const ui = () => ({
|
||||
createMapContextMenu: () => ({
|
||||
element: { parentElement: null },
|
||||
open: (x, y, items) => opens.push({ x, y, items }),
|
||||
close() {},
|
||||
contains: () => false,
|
||||
}),
|
||||
});
|
||||
// 평면 마커 = 측점선 없는 대피로 20 m(화면 40 px)
|
||||
const MARK = { id: "evac-1", chainageM: 20 };
|
||||
const STUBS = {
|
||||
"../b_page_state": {
|
||||
readState: () => ({ next: 4, ids: [{ id: "pipe-s3", at: 55 }] }),
|
||||
writeState() {},
|
||||
},
|
||||
"./design_selection": {
|
||||
PIPE_MATCH_M: 0.51,
|
||||
isNear: () => false,
|
||||
nearestWithin: () => null,
|
||||
readSelection: () => null,
|
||||
selectDesign() {},
|
||||
},
|
||||
common_util_pick_tag: { setPipeTagSource() {} },
|
||||
"../A00_Common/touch_bridge": { pickPx: (v) => v },
|
||||
"./B05_Profile_UI_IrregularStations": { irregularStationId: (id) => `irr:${id}` },
|
||||
"./B05_Profile_Api_Structures": { structureAnchorM: (s) => s.chainage_m ?? 0 },
|
||||
"../A00_Common/design/design_selection": { PIPE_MATCH_M: 0.51 },
|
||||
"./B05_Profile_UI_RouteEdit_StructureMarks": {
|
||||
planMarkAt: (_c, _l, toScreen, px, py) => {
|
||||
const [x, y] = toScreen([MARK.chainageM, 0]);
|
||||
const distance = Math.hypot(x - px, y - py);
|
||||
return distance <= 10 ? { mark: MARK, distance } : null;
|
||||
},
|
||||
},
|
||||
};
|
||||
const { loadTs } = makeLoader({ root: ROOT, stubs: STUBS, ui });
|
||||
const ids = loadTs(path.join(ROOT, "A00_Common", "design", "design_pipe_ids.ts"));
|
||||
ids.usePipeIds("p1");
|
||||
const shared = loadTs(path.join(B05, "B05_Profile_UI_Profile_Structures.ts"));
|
||||
const plan = loadTs(path.join(B05, "B05_Profile_UI_RouteEdit_StructureMenu.ts"));
|
||||
|
||||
const out = {};
|
||||
// 알약 주인 가르기 — 관 세션 id · 옛 자리 이름 관 = 관 지점 · 그 밖 = 구조물 id
|
||||
const remove = {
|
||||
pipe: (m) => calls.push(["pipe", m]),
|
||||
structure: (id) => calls.push(["structure", id]),
|
||||
};
|
||||
["pipe-s3", "pipe-12.50", "evac-1", "cross-2"].forEach((id) => ids.removeMarkOwner(id, remove));
|
||||
out.owners = calls.splice(0);
|
||||
|
||||
function fakeElement() {
|
||||
const listeners = {};
|
||||
return {
|
||||
isConnected: true,
|
||||
addEventListener: (type, fn) => (listeners[type] = listeners[type] || []).push(fn),
|
||||
getBoundingClientRect: () => ({ left: 0, top: 0 }),
|
||||
append(child) {
|
||||
child.parentElement = this;
|
||||
},
|
||||
parentElement: null,
|
||||
fire(type, clientX, clientY) {
|
||||
(listeners[type] || []).forEach((fn) =>
|
||||
fn({ clientX, clientY, target: null, preventDefault() {} }),
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
// 종단 그래프 = 재료(관 측점 40 m 하나)
|
||||
const host = fakeElement();
|
||||
shared.mountStructureMenu(host, {
|
||||
stations: [{ id: "pipe-1", chainage_m: 40, origin: "pipe" }],
|
||||
x: (m) => m * 2,
|
||||
chainageAt: (px) => px / 2,
|
||||
maxChainageM: 100,
|
||||
onRemove: (station) => calls.push(["remove", station.id]),
|
||||
onRemoveMark: (id) => calls.push(["removeMark", id]),
|
||||
onAddPipe: () => {},
|
||||
structureGroups: () => [{ label: "A군", types: [{ type: { name: "배수관", type_id: "pipe" } }] }],
|
||||
onAddStructureType: () => {},
|
||||
});
|
||||
const canvas = fakeElement();
|
||||
canvas.parentElement = fakeElement();
|
||||
const line = [
|
||||
[0, 0],
|
||||
[100, 0],
|
||||
];
|
||||
plan.bindPlanStructureMenu(
|
||||
{
|
||||
canvas,
|
||||
toScreen: (v) => [v[0] * 2, v[1] * 2],
|
||||
unrotate: (x, y) => [x, y],
|
||||
state: () => ({ plannedLine: line, planned: line }),
|
||||
},
|
||||
() => -1,
|
||||
);
|
||||
const planAt = (x, y) => {
|
||||
canvas.fire("contextmenu", x, y);
|
||||
const items = opens.at(-1).items;
|
||||
return items.map((item) => (Array.isArray(item) ? item[0] : item.label));
|
||||
};
|
||||
out.planMark = planAt(41, 3); // 대피로 마커
|
||||
opens.at(-1).items[0][1]();
|
||||
out.planPipe = planAt(81, 2); // 관 측점선 = 측점 먼저
|
||||
opens.at(-1).items[0][1]();
|
||||
out.planEmpty = planAt(140, 2); // 빈 자리 = 추가 메뉴
|
||||
out.calls = calls;
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -32,7 +32,7 @@ def got() -> dict:
|
||||
def test_평면_메뉴는_종단_메뉴와_같음(got) -> None:
|
||||
assert got["liveBefore"] is True # 종단 그래프가 서기 전 = 재료 없음
|
||||
assert got["profileEmpty"] == got["planEmpty"] == [["A군", ["배수관"]]]
|
||||
assert got["profilePipe"] == got["planPipe"] == ["배관 삭제"]
|
||||
assert got["profilePipe"] == got["planPipe"] == ["구조물 삭제"] # 글 하나(514-1)
|
||||
assert got["planPlus"] == ["측점 삭제"]
|
||||
assert got["planMenuAt"] == [141, -4]
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
"""514-1 알약 · 평면 마커 우클릭 「구조물 삭제」(요구 328-1 · 328-2) — 측점선 없는 구조물(대피로 · 노출형 횡단수로)도
|
||||
알약 · 평면 마커 우클릭으로 지움 · 513-3 과 같은 주인 가르기(`removeMarkOwner`) · 삭제 글 = 「구조물 삭제」 하나.
|
||||
|
||||
TS 셈은 `helper_514_1_mark_remove.cjs`(Node · 주인 가르기 · 평면 항목 짓기는 진짜).
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_514_1_mark_remove.py -q
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
HELPER = Path(__file__).with_name("helper_514_1_mark_remove.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["owners"] == [
|
||||
["pipe", 55], # 관 세션 id = 장부 자리
|
||||
["pipe", 12.5], # 옛 자리 이름 관
|
||||
["structure", "evac-1"],
|
||||
["structure", "cross-2"],
|
||||
]
|
||||
|
||||
|
||||
def test_평면_마커_우클릭(got) -> None:
|
||||
assert got["planMark"] == ["구조물 삭제"]
|
||||
assert got["planPipe"] == ["구조물 삭제"] # 관도 같은 글
|
||||
assert got["planEmpty"] == ["A군"]
|
||||
assert got["calls"] == [["removeMark", "evac-1"], ["remove", "pipe-1"]]
|
||||
|
||||
|
||||
def _read(*parts: str) -> str:
|
||||
return (ROOT.joinpath(*parts)).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_배선() -> None:
|
||||
marks = _read("B05_Profile", "B05_Profile_UI_Structures_Marks.ts")
|
||||
assert 'mark.addEventListener("contextmenu"' in marks
|
||||
assert '["구조물 삭제", () => remove(id)]' in marks
|
||||
render = _read("B05_Profile", "B05_Profile_UI_Profile_Render.ts")
|
||||
assert render.count("callbacks?.onStructureMarkRemove?.(structureId)") == 2 # 알약 · 평면
|
||||
assert "배관 삭제" not in _read("B05_Profile", "B05_Profile_UI_Profile_Structures.ts")
|
||||
assert 'toView(shared, "onStructureMarkRemove")' in _read(
|
||||
"A00_Common", "design", "design_shared.ts"
|
||||
)
|
||||
view = _read("A00_Common", "design", "design_view_profile.ts")
|
||||
assert "removeMarkOwner(structureId, removeOwner)" in view
|
||||
cross = _read("B06_Section", "B06_Section_UI_Section_View_Profile.ts")
|
||||
assert "removeMarkOwner(structureId, {" in cross
|
||||
Reference in New Issue
Block a user