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:
@@ -130,16 +130,26 @@ export function followMovedPipe(projectId: string | null, fromM: number, toM: nu
|
||||
);
|
||||
}
|
||||
|
||||
/** 종단 측점 하나를 지움(513-3 · 우클릭 「배관 · 구조물 삭제」 종단 · 평면 공용) — 측점 id = 주인 · `주인:역할`
|
||||
/** 우클릭 삭제의 두 길 — 관 = 관 지점 정본 · 그 밖 = 좌측 칸 [삭제]와 같은 길. */
|
||||
export interface OwnerRemove {
|
||||
pipe: (chainageM: number) => void;
|
||||
structure: (structureId: string) => void;
|
||||
}
|
||||
|
||||
/** 종단 측점 하나를 지움(513-3 · 우클릭 「구조물 삭제」 종단 · 평면 공용) — 측점 id = 주인 · `주인:역할`
|
||||
* (`plannedIrregular`) · 관 기준 측점 = 그 자리 · 관 구간 끝 = 관 자리 · 그 밖 = 구조물 id(좌측 칸 [삭제]와 같은 길). */
|
||||
export function removeStationOwner(
|
||||
station: { id: string; chainage_m: number },
|
||||
pipe: boolean,
|
||||
remove: { pipe: (chainageM: number) => void; structure: (structureId: string) => void },
|
||||
remove: OwnerRemove,
|
||||
): void {
|
||||
if (pipe) return remove.pipe(station.chainage_m);
|
||||
const owner = station.id.replace(/:(start|end)$/, "");
|
||||
const at = pipeChainageOf(owner);
|
||||
removeMarkOwner(station.id.replace(/:(start|end)$/, ""), remove);
|
||||
}
|
||||
|
||||
/** 알약 하나를 지움(514-1 · 측점선 없는 구조물도) — 관 알약 = 관 세션 id 장부 자리 · 그 밖 = 구조물 id. */
|
||||
export function removeMarkOwner(structureId: string, remove: OwnerRemove): void {
|
||||
const at = pipeChainageOf(structureId);
|
||||
if (at !== null) remove.pipe(at);
|
||||
else remove.structure(owner);
|
||||
else remove.structure(structureId);
|
||||
}
|
||||
|
||||
@@ -223,6 +223,7 @@ export function sharedProfile(shared: DesignPageShared): DesignProfile {
|
||||
onIrregularSelect: toView(shared, "onIrregularSelect"),
|
||||
onStructureSelect: toView(shared, "onStructureSelect"),
|
||||
onStructureMarkMove: toView(shared, "onStructureMarkMove"),
|
||||
onStructureMarkRemove: toView(shared, "onStructureMarkRemove"),
|
||||
onStructureTypeAdd: toView(shared, "onStructureTypeAdd"),
|
||||
structureGroups: () => sharedStructures(shared).pickGroups(),
|
||||
onBasinSelected: toView(shared, "onBasinSelected"),
|
||||
|
||||
@@ -66,7 +66,7 @@ import "../../B05_Profile/B05_Profile_UI_Style.css";
|
||||
import "../../B05_Profile/B05_Profile_UI_Style_Table.css";
|
||||
import "../../B05_Profile/B05_Profile_UI_Style_Drainage.css";
|
||||
import "../../B05_Profile/B05_Profile_UI_Style_Structures.css";
|
||||
import { pipeIdAt, removeStationOwner } from "./design_pipe_ids";
|
||||
import { pipeIdAt, removeMarkOwner, removeStationOwner } from "./design_pipe_ids";
|
||||
import { useDesignStore } from "./design_store";
|
||||
import { pageSettingsPanelValues } from "../../B05_Profile/B05_Profile_UI_Page_Latest";
|
||||
import { setPlusStations } from "../../common_util/common_util_structure_station";
|
||||
@@ -172,6 +172,10 @@ export async function mountProfileView(
|
||||
}
|
||||
panel.structures.moveById(structureId, toChainage);
|
||||
};
|
||||
const removeOwner = {
|
||||
pipe: (chainage: number) => profilePanel.drainage.removePipe(chainage),
|
||||
structure: (structureId: string) => void panel.structures.removeById(structureId),
|
||||
};
|
||||
// 종단도 한 벌 — 종단 보기가 보일 때 받는 알림만 건다(계획서 24-3b).
|
||||
const profilePanel = createRouteProfilePanel(
|
||||
shared,
|
||||
@@ -194,12 +198,10 @@ export async function mountProfileView(
|
||||
onStructureMove: (from, to, station) => {
|
||||
if (isPipeStation(station)) profilePanel.drainage.movePipe(from, to);
|
||||
},
|
||||
// 우클릭 삭제 — 관 = 관 지점 정본 · 그 밖 = 좌측 칸 [삭제]와 같은 길(513-3)
|
||||
// 우클릭 삭제 — 관 = 관 지점 정본 · 그 밖 = 좌측 칸 [삭제]와 같은 길(513-3 · 알약 514-1)
|
||||
onStructureRemove: (station) =>
|
||||
removeStationOwner(station, isPipeStation(station), {
|
||||
pipe: (chainage) => profilePanel.drainage.removePipe(chainage),
|
||||
structure: (structureId) => panel.structures.removeById(structureId),
|
||||
}),
|
||||
removeStationOwner(station, isPipeStation(station), removeOwner),
|
||||
onStructureMarkRemove: (structureId) => removeMarkOwner(structureId, removeOwner),
|
||||
// 우클릭 [배관 추가] — 사이드 폼에 종류·측점을 실어 주고 [추가]로 확정(2026-08-18).
|
||||
onPipeAdd: (chainage) => panel.structures.addAt(chainage, "pipe"),
|
||||
onStructureAdd: (chainage, type) =>
|
||||
|
||||
@@ -48,6 +48,8 @@ export interface RouteProfilePanelCallbacks {
|
||||
onStructureSelect?: (structureId: string | null) => void;
|
||||
/** 종단 그래프에서 구조물 서클마크를 끌어 옮김. */
|
||||
onStructureMarkMove?: (structureId: string, toChainageM: number) => void;
|
||||
/** 알약 · 평면 마커 우클릭으로 그 구조물(관 포함)을 지움(514-1). */
|
||||
onStructureMarkRemove?: (structureId: string) => void;
|
||||
/** 종단 그래프 우클릭으로 레지스트리 타입을 지정해 구조물을 넣음. */
|
||||
onStructureTypeAdd?: (chainageM: number, typeId: string) => void;
|
||||
/** 우클릭 군 · 종류 목록 = 사이드 「구조물 배치」 드롭다운 그대로(129-1 · B05 · B06 한 패널). */
|
||||
|
||||
@@ -408,6 +408,7 @@ export function renderProfile(ctx: ProfileRenderContext): void {
|
||||
chainageAt: chainageInverter(longitudinal, width, layout.originOffset),
|
||||
maxChainageM: maxChainageOf(longitudinal),
|
||||
onRemove: (station) => callbacks?.onStructureRemove?.(station),
|
||||
onRemoveMark: (structureId) => callbacks?.onStructureMarkRemove?.(structureId),
|
||||
onRemovePlus: removePlusStation,
|
||||
onAddPipe: (chainage) => callbacks?.onPipeAdd?.(chainage),
|
||||
onAddStructure: (chainage, type) => callbacks?.onStructureAdd?.(chainage, type),
|
||||
@@ -437,6 +438,7 @@ export function renderProfile(ctx: ProfileRenderContext): void {
|
||||
ctx.redraw();
|
||||
},
|
||||
onMove: (structureId, toChainage) => callbacks?.onStructureMarkMove?.(structureId, toChainage),
|
||||
onRemove: (structureId) => callbacks?.onStructureMarkRemove?.(structureId),
|
||||
// 라벨 흙막이 상자 수 = 폼과 같은 유출 단 수(54-1 · 54-14)
|
||||
containerCount: (structure) =>
|
||||
structure.type_id === "pipe"
|
||||
|
||||
@@ -11,11 +11,7 @@
|
||||
|
||||
import { pickPx } from "../A00_Common/touch_bridge";
|
||||
import { createMapContextMenu, type MapContextMenuItem } from "@ui/ui_template_context_menu";
|
||||
import {
|
||||
irregularStationId,
|
||||
isPipeStation,
|
||||
type IrregularStation,
|
||||
} from "./B05_Profile_UI_IrregularStations";
|
||||
import { irregularStationId, type IrregularStation } from "./B05_Profile_UI_IrregularStations";
|
||||
import {
|
||||
structureAnchorM,
|
||||
type StructureInstance,
|
||||
@@ -60,6 +56,8 @@ export interface StructureLineOptions {
|
||||
maxChainageM: number;
|
||||
/** 선을 지울 때. */
|
||||
onRemove: (station: IrregularStation) => void;
|
||||
/** 알약(평면 마커)으로 구조물을 지울 때(514-1 · 측점선 없는 구조물도). */
|
||||
onRemoveMark?: (structureId: string) => void;
|
||||
/** 추가 측점을 지울 때(368-3 · 우클릭 「측점 삭제」). */
|
||||
onRemovePlus?: (chainageM: number) => void;
|
||||
/** 빈 자리에서 우클릭해 배관을 넣을 때. */
|
||||
@@ -89,7 +87,7 @@ export function structureMenuItemsAt(
|
||||
if (near) {
|
||||
if (near.id.startsWith(PLUS_OWNER_PREFIX))
|
||||
return [["측점 삭제", () => options.onRemovePlus?.(near.chainage_m)]];
|
||||
return [[isPipeStation(near) ? "배관 삭제" : "구조물 삭제", () => options.onRemove(near)]];
|
||||
return [["구조물 삭제", () => options.onRemove(near)]]; // 관 · 세월교 · BOX 도 한 글(514-1)
|
||||
}
|
||||
const at = Number(Math.min(Math.max(chainageM, 0), options.maxChainageM).toFixed(2));
|
||||
// 빈 자리 — 사이드 「구조물 배치」와 같은 구조물군 → 종류 2단 메뉴(2026-08-18
|
||||
|
||||
@@ -258,6 +258,26 @@ function showBalloon(
|
||||
: `translate(-50%, calc(-100% - ${gap}px))`;
|
||||
}
|
||||
|
||||
/** 화면 자리에서 가장 가까운 마커 — 집기 거리 밖이면 null(누름 · 우클릭 삭제 514-1 공용). */
|
||||
export function planMarkAt(
|
||||
canvas: HTMLCanvasElement,
|
||||
line: ReadonlyArray<Vertex>,
|
||||
toScreen: (point: Vertex) => [number, number],
|
||||
px: number,
|
||||
py: number,
|
||||
): { mark: PlanMark; distance: number } | null {
|
||||
let best: PlanMark | null = null;
|
||||
let bestDistance = pickPx(MARK_HIT_PX);
|
||||
for (const mark of planMarks(canvas)?.marks ?? []) {
|
||||
const frame = stationFrameAt(line, mark.chainageM);
|
||||
if (!frame) continue;
|
||||
const [x, y] = toScreen(frame.origin);
|
||||
const distance = Math.hypot(x - px, y - py);
|
||||
if (distance <= bestDistance) [best, bestDistance] = [mark, distance];
|
||||
}
|
||||
return best ? { mark: best, distance: bestDistance } : null;
|
||||
}
|
||||
|
||||
export interface StructureMarkInput {
|
||||
/** 마커를 눌렀으면 고르고 끌기 시작 · true(재기 다음 맨 앞 · 409). */
|
||||
press: (px: number, py: number) => boolean;
|
||||
@@ -288,20 +308,8 @@ export function bindStructureMarks(
|
||||
const state = params.state();
|
||||
return state.plannedLine.length ? state.plannedLine : state.planned;
|
||||
};
|
||||
const markAt = (px: number, py: number): { mark: PlanMark; distance: number } | null => {
|
||||
const found = planMarks(canvas);
|
||||
const line = lineNow();
|
||||
let best: PlanMark | null = null;
|
||||
let bestDistance = pickPx(MARK_HIT_PX);
|
||||
for (const mark of found?.marks ?? []) {
|
||||
const frame = stationFrameAt(line, mark.chainageM);
|
||||
if (!frame) continue;
|
||||
const [x, y] = params.toScreen(frame.origin);
|
||||
const distance = Math.hypot(x - px, y - py);
|
||||
if (distance <= bestDistance) [best, bestDistance] = [mark, distance];
|
||||
}
|
||||
return best ? { mark: best, distance: bestDistance } : null;
|
||||
};
|
||||
const markAt = (px: number, py: number): { mark: PlanMark; distance: number } | null =>
|
||||
planMarkAt(canvas, lineNow(), params.toScreen, px, py);
|
||||
const say = (moving: Moving, done: boolean): void => {
|
||||
const interval = sources.get(draftProjectId())?.intervalM() ?? params.stationIntervalM;
|
||||
const shift = moving.to - moving.from;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* 평면 우클릭 구조물 메뉴(513-1 · 요구 327-1).
|
||||
*
|
||||
* · 메뉴 = 종단 그래프 우클릭과 같은 항목 · 같은 동작(`structureMenuItemsAt` · 재료 = 지금 선 종단 그래프).
|
||||
* · 자리 = 누른 곳을 노선에 내린 누가거리 · 가까운 구조물 · 배관 측점이면 삭제.
|
||||
* · 자리 = 누른 곳을 노선에 내린 누가거리 · 가까운 구조물 · 배관 측점이면 삭제 · 측점선 없는 마커도(514-1).
|
||||
* · 노드 위 = 옛 동작 그대로(노드 삭제 · `bindNodeSplice`) · 노선에서 먼 곳 = 메뉴 없음.
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -11,6 +11,7 @@ import { pickPx } from "../A00_Common/touch_bridge";
|
||||
import { createMapContextMenu } from "@ui/ui_template_context_menu";
|
||||
import { liveStructureMenu, structureMenuItemsAt } from "./B05_Profile_UI_Profile_Structures";
|
||||
import { routePointAtScreen } from "./B05_Profile_UI_RouteEdit_Input";
|
||||
import { planMarkAt } from "./B05_Profile_UI_RouteEdit_StructureMarks";
|
||||
import type { RouteEditPointerParams } from "./B05_Profile_UI_RouteEdit_Pointer";
|
||||
import { stationFrameAt } from "./logic/B05_Profile_UI_RouteEdit_Frame";
|
||||
|
||||
@@ -47,10 +48,13 @@ export function bindPlanStructureMenu(
|
||||
.filter((entry) => entry.distance <= pickPx(STATION_HIT_PX))
|
||||
.sort((left, right) => left.distance - right.distance)[0];
|
||||
const host = menu.element.parentElement?.getBoundingClientRect() ?? rect;
|
||||
const mark = near ? null : planMarkAt(canvas, line, toScreen, px, py)?.mark;
|
||||
menu.open(
|
||||
event.clientX - host.left,
|
||||
event.clientY - host.top,
|
||||
structureMenuItemsAt(options, hit.chainageM, near?.station ?? null),
|
||||
mark
|
||||
? [["구조물 삭제", () => options.onRemoveMark?.(mark.id)]]
|
||||
: structureMenuItemsAt(options, hit.chainageM, near?.station ?? null),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import { pickPx } from "../A00_Common/touch_bridge";
|
||||
import { createMapContextMenu } from "@ui/ui_template_context_menu";
|
||||
import {
|
||||
structureAnchorM,
|
||||
type StructureInstance,
|
||||
@@ -73,6 +74,8 @@ export interface StructureMarksOptions {
|
||||
selectedId: string | null;
|
||||
onSelect: (structureId: string | null) => void;
|
||||
onMove: (structureId: string, toChainageM: number) => void;
|
||||
/** 우클릭 「구조물 삭제」(514-1 · 측점선 없는 알약도) — 없으면 메뉴 없음. */
|
||||
onRemove?: (structureId: string) => void;
|
||||
/** 라벨 선택 컨테이너 개수(폼과 같은 값 · 배관 흙막이 = 유출 단 수) — 없으면 저장된 것. */
|
||||
containerCount?: (structure: StructureInstance) => ContainerCount | undefined;
|
||||
}
|
||||
@@ -162,6 +165,7 @@ export function buildStructureLane(options: StructureMarksOptions): HTMLElement
|
||||
|
||||
const typeMap = new Map(options.types.map((type) => [type.type_id, type]));
|
||||
const structures = laneStructures(options.structures);
|
||||
const menu = createMapContextMenu("b05-profile-chart");
|
||||
// 좌측 목록과 같은 중복 이름 번호(2026-08-19 사용자 지시) — 알약 글·툴팁·벌룬 제목이
|
||||
// 모두 이 번호를 따른다.
|
||||
const texts = structurePillTexts(structures, typeMap);
|
||||
@@ -227,6 +231,16 @@ export function buildStructureLane(options: StructureMarksOptions): HTMLElement
|
||||
options.onSelect(selected ? null : id);
|
||||
});
|
||||
attachMarkDrag(mark, structure, options);
|
||||
mark.addEventListener("contextmenu", (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const remove = options.onRemove;
|
||||
if (!remove) return;
|
||||
const rect = lane.getBoundingClientRect();
|
||||
menu.open(event.clientX - rect.left, event.clientY - rect.top, [
|
||||
["구조물 삭제", () => remove(id)],
|
||||
]);
|
||||
});
|
||||
lane.append(mark);
|
||||
|
||||
if (selected)
|
||||
@@ -257,6 +271,7 @@ export function buildStructureLane(options: StructureMarksOptions): HTMLElement
|
||||
axisInner.textContent = "구조물";
|
||||
axis.append(axisInner);
|
||||
lane.prepend(axis);
|
||||
lane.append(menu.element);
|
||||
|
||||
return lane;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
} from "../A00_Common/design/design_selection";
|
||||
import { paintMapPick } from "../A00_Common/design/design_pick_profile";
|
||||
import { ESCAPE_FROM } from "../A00_Common/design/design_pick_escape";
|
||||
import { removeStationOwner } from "../A00_Common/design/design_pipe_ids";
|
||||
import { removeMarkOwner, removeStationOwner } from "../A00_Common/design/design_pipe_ids";
|
||||
import { plantsStation } from "../B05_Profile/B05_Profile_UI_Profile_Structures";
|
||||
import {
|
||||
fetchLatestRoute,
|
||||
@@ -164,6 +164,14 @@ export function createSectionProfile(
|
||||
structure: edit.removeStructure,
|
||||
});
|
||||
},
|
||||
onStructureMarkRemove: (structureId) => {
|
||||
const edit = host.edit();
|
||||
if (edit)
|
||||
removeMarkOwner(structureId, {
|
||||
pipe: edit.removePipe,
|
||||
structure: edit.removeStructure,
|
||||
});
|
||||
},
|
||||
onPipeAdd: (chainage) => host.edit()?.addPipe(chainage),
|
||||
onStructureAdd: (chainage, type) =>
|
||||
host.edit()?.addStructureType(chainage, LEGACY_TYPE_IDS[type] ?? "etc"),
|
||||
|
||||
@@ -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