1) 사이드바 폼으로 이름을 "배관"이라 적어 넣은 항목은 origin이 "user"라 [초기화]로 지워지지 않고 배수유역도와도 어긋난 채 남았다. isPipeStation()(origin이 pipe이거나 이름이 "배관")으로 판정을 통일해 목록 교체·이동·삭제·선택 동기화가 같은 규칙을 쓴다. 2) 대시보드에서 곧장 B05로 들어오면 배수유역도가 비어 보이고 새로고침해야 나왔다. 패널이 배치되기 전(0×0)에 fitToRoute()가 돌아 엉뚱한 배율이 굳은 것이다. 크기가 2px 미만이면 맞춤을 미뤘다가 첫 배치 때 다시 맞춘다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
74 lines
3.4 KiB
TypeScript
74 lines
3.4 KiB
TypeScript
/* =============================================================================
|
|
* 종단 테이블 구조물 배치 라인 (B05)
|
|
*
|
|
* 종단 **그래프** 위 우클릭 메뉴. 구조물 조작은 전부 그래프에서 한다 — 측점선을 끌어 옮기고,
|
|
* 우클릭으로 배관을 넣거나 지운다(2026-08-01 사용자 지시). 테이블은 값만 보여 준다.
|
|
*
|
|
* 가까운 구조물이 있으면 삭제, 없으면 그 자리에 배관 추가를 띄운다. 배관은 배수유역도의 관 지점이
|
|
* 투영된 것이라(`origin: "pipe"`), 지우면 관 지점 정본(`pipe_points.json`)이 바뀌고 세부유역도
|
|
* 함께 다시 나뉜다 — 값을 두 곳에 따로 쌓지 않기 위해서다.
|
|
* ========================================================================== */
|
|
|
|
import { createMapContextMenu } from "@ui/ui_template_context_menu";
|
|
import { isPipeStation, type IrregularStation } from "./B05_wf2_Route_UI_IrregularStations";
|
|
|
|
/** 선을 잡았다고 볼 좌우 여유(px). 선 자체는 얇아 그대로는 집기 어렵다. */
|
|
const GRAB_SLACK_PX = 6;
|
|
|
|
export interface StructureLineOptions {
|
|
/** 테이블에 얹을 구조물 측점 목록. */
|
|
stations: ReadonlyArray<IrregularStation>;
|
|
/** chainage → x(px). 테이블·그래프와 같은 매핑을 쓴다. */
|
|
x: (chainageM: number) => number;
|
|
/** x(px) → chainage. 선을 끌 때 역변환에 쓴다. */
|
|
chainageAt: (px: number) => number;
|
|
/** 노선 총 연장(m). 이 범위를 벗어난 자리로는 옮기지 않는다. */
|
|
maxChainageM: number;
|
|
/** 선을 지울 때. */
|
|
onRemove: (station: IrregularStation) => void;
|
|
/** 빈 자리에서 우클릭해 배관을 넣을 때. */
|
|
onAddPipe: (chainageM: number) => void;
|
|
}
|
|
|
|
/**
|
|
* 그래프 칸에 우클릭 메뉴를 붙인다. 그래프는 매 그리기마다 새로 만들어지므로 이 함수도
|
|
* 그때마다 다시 부른다 — 상태를 밖에 남기지 않는다.
|
|
*/
|
|
export function mountStructureMenu(host: HTMLElement, options: StructureLineOptions): void {
|
|
const menu = createMapContextMenu("b05-profile-chart");
|
|
|
|
function clamp(chainageM: number): number {
|
|
return Math.min(Math.max(chainageM, 0), options.maxChainageM);
|
|
}
|
|
|
|
// 우클릭 — 가까운 구조물이 있으면 삭제, 없으면 그 자리에 배관을 넣는다.
|
|
host.addEventListener("contextmenu", (event) => {
|
|
if (menu.contains(event.target)) {
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
const rect = host.getBoundingClientRect();
|
|
const localX = event.clientX - rect.left;
|
|
const chainage = clamp(options.chainageAt(localX));
|
|
// 선을 그리지 않으므로 화면 거리로 가장 가까운 구조물을 찾는다.
|
|
const near = options.stations
|
|
.map((station) => ({ station, distance: Math.abs(options.x(station.chainage_m) - localX) }))
|
|
.filter((entry) => entry.distance <= GRAB_SLACK_PX)
|
|
.sort((left, right) => left.distance - right.distance)[0];
|
|
event.preventDefault();
|
|
menu.open(localX, event.clientY - rect.top, [
|
|
near
|
|
? [
|
|
isPipeStation(near.station) ? "배관 삭제" : "구조물 삭제",
|
|
() => options.onRemove(near.station),
|
|
]
|
|
: ["배관 추가", () => options.onAddPipe(Number(chainage.toFixed(2)))],
|
|
]);
|
|
});
|
|
host.addEventListener("pointerdown", (event) => {
|
|
if (!menu.contains(event.target)) menu.close();
|
|
});
|
|
|
|
host.append(menu.element);
|
|
}
|