데이터 흐름 문제 - B04 해석 산출물 7개(7.2MB)가 B05로 전량 복사되고 있었고, 관 지점 정본 (pipe_points.json)은 B05가 아예 참조하지 않았다. B04에서 확정한 관이 B05에 보이지 않고 진입할 때마다 자동 배치로 되돌아갔다. - 사본은 "B05 편집이 원본을 덮어쓴다"를 막으려던 것인데, 외곽선 편집을 제거한 뒤로 B05는 아무것도 저장하지 않아 존재 이유가 사라졌다. 일원화 - B05 전용 모듈 3개 삭제: Engine_Drainage_Store / Engine_Drainage_Basin / Router_Drainage. 두 화면이 B04_wf1_Surface/drainage/ 한 폴더를 직접 읽는다. - POST /drainage/basins 폐기 → GET/PUT /drainage/pipe-points · POST /drainage/detail-basins 한 벌로 통합. 응답에 route_lonlat · main_polygon_lonlat · flow_arrows · upstream_lonlat · inflow_hotspots를 추가해 B05가 그리는 데 필요한 값을 같은 응답으로 받는다. - 경로 확정 시 관 지점을 B04와 같은 저장소에 커밋한다. B05 화면 - 유역 폴리곤을 눌러 고르기(지도에서도 선택), [집중유역] 토글, 관 개수·유역 수· 종단 Z 출처 요약 줄 추가. - 관 마커는 좌클릭으로만 잡는다(우클릭은 메뉴, 가운데 버튼은 팬). - 유역 목록은 3행까지 보이고 나머지는 스크롤. 종단 테이블 구조물 라인 - 관 매설 지점을 "배관" 구조물로 실체화(origin: "pipe"). 정본은 관 지점 파일이며 목록은 그 투영이라, 어디서 옮기든 pipeEditor 한 경로로 전파된다. - 테이블 위 세로선을 끌어 이동(배관은 세부유역까지 재산정), 우클릭으로 배관 추가/구조물 삭제. 정리 - B05_wf2_Route/drainage/ 사본과 debug/(삭제된 Subdivide 엔진 검증 스크립트) 삭제. - config DRAINAGE_B05_DIRNAME 제거. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
129 lines
5.8 KiB
TypeScript
129 lines
5.8 KiB
TypeScript
/* =============================================================================
|
|
* 종단 테이블 구조물 배치 라인 (B05)
|
|
*
|
|
* 구조물 측점을 테이블 위에 세로선으로 얹고, 그 선을 끌어 위치를 옮긴다. 우클릭하면 그 자리에
|
|
* 배관을 넣거나 선을 지운다.
|
|
*
|
|
* 배관 선은 배수유역도의 관 매설 지점이 투영된 것이다(`origin: "pipe"`). 그래서 여기서 옮기면
|
|
* 관 지점 정본(`pipe_points.json`)이 바뀌고 세부유역도 함께 다시 나뉜다 — 값을 두 곳에 따로
|
|
* 쌓지 않기 위해서다(2026-08-01 사용자 지시).
|
|
* ========================================================================== */
|
|
|
|
import { createMapContextMenu } from "@ui/ui_template_context_menu";
|
|
import { irregularLabel, type IrregularStation } from "./B05_wf2_Route_UI_IrregularStations";
|
|
|
|
/** 선을 잡았다고 볼 좌우 여유(px). 선 자체는 얇아 그대로는 집기 어렵다. */
|
|
const GRAB_SLACK_PX = 6;
|
|
/** 이만큼(px) 이하로 움직였다 뗐으면 이동이 아니라 고르기로 본다. */
|
|
const DRAG_SLOP_PX = 3;
|
|
|
|
export interface StructureLineOptions {
|
|
/** 테이블에 얹을 구조물 측점 목록. */
|
|
stations: ReadonlyArray<IrregularStation>;
|
|
/** chainage → x(px). 테이블·그래프와 같은 매핑을 쓴다. */
|
|
x: (chainageM: number) => number;
|
|
/** x(px) → chainage. 선을 끌 때 역변환에 쓴다. */
|
|
chainageAt: (px: number) => number;
|
|
/** 노선 총 연장(m). 이 범위를 벗어난 자리로는 옮기지 않는다. */
|
|
maxChainageM: number;
|
|
/** 선을 옮겼을 때. 옛 누가거리와 새 누가거리를 넘긴다. */
|
|
onMove: (fromChainageM: number, toChainageM: number, station: IrregularStation) => void;
|
|
/** 선을 지울 때. */
|
|
onRemove: (station: IrregularStation) => void;
|
|
/** 빈 자리에서 우클릭해 배관을 넣을 때. */
|
|
onAddPipe: (chainageM: number) => void;
|
|
/** 선을 눌렀을 때(끌지 않음) — 값 열 오버레이 선택과 맞춘다. */
|
|
onSelect?: (station: IrregularStation) => void;
|
|
}
|
|
|
|
/**
|
|
* 테이블 요소 위에 구조물 라인 레이어를 얹는다. 테이블은 매 그리기마다 새로 만들어지므로
|
|
* 이 함수도 그때마다 다시 부른다 — 상태를 밖에 남기지 않는다.
|
|
*/
|
|
export function mountStructureLines(table: HTMLElement, options: StructureLineOptions): void {
|
|
const layer = document.createElement("div");
|
|
layer.className = "b05-profile-table__structures";
|
|
const menu = createMapContextMenu("b05-profile-table");
|
|
|
|
/** 끌고 있는 선. null이면 잡은 것이 없다. */
|
|
let dragging: { station: IrregularStation; element: HTMLElement; startX: number } | null = null;
|
|
let moved = false;
|
|
|
|
function clamp(chainageM: number): number {
|
|
return Math.min(Math.max(chainageM, 0), options.maxChainageM);
|
|
}
|
|
|
|
options.stations.forEach((station) => {
|
|
const line = document.createElement("div");
|
|
line.className =
|
|
"b05-profile-table__structure" + (station.origin === "pipe" ? " is-pipe" : " is-user");
|
|
line.style.left = `${options.x(station.chainage_m)}px`;
|
|
line.title = `${irregularLabel(station)} · ${station.structure || "구조물"}`;
|
|
const label = document.createElement("span");
|
|
label.className = "b05-profile-table__structure-label";
|
|
label.textContent = station.structure || "구조물";
|
|
line.append(label);
|
|
|
|
line.addEventListener("pointerdown", (event) => {
|
|
// 이동은 좌클릭 전용 — 우클릭은 메뉴다(2026-08-01 사용자 지시).
|
|
if (event.button !== 0) return;
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
menu.close();
|
|
dragging = { station, element: line, startX: event.clientX };
|
|
moved = false;
|
|
line.setPointerCapture(event.pointerId);
|
|
});
|
|
line.addEventListener("pointermove", (event) => {
|
|
if (!dragging || dragging.station.id !== station.id) return;
|
|
if (Math.abs(event.clientX - dragging.startX) > DRAG_SLOP_PX) moved = true;
|
|
const rect = table.getBoundingClientRect();
|
|
line.style.left = `${options.x(clamp(options.chainageAt(event.clientX - rect.left)))}px`;
|
|
});
|
|
line.addEventListener("pointerup", (event) => {
|
|
if (!dragging || dragging.station.id !== station.id) return;
|
|
const rect = table.getBoundingClientRect();
|
|
const next = clamp(options.chainageAt(event.clientX - rect.left));
|
|
dragging = null;
|
|
if (!moved) {
|
|
// 끌지 않고 눌렀다 뗐으면 고르기다 — 원래 자리로 되돌린다.
|
|
line.style.left = `${options.x(station.chainage_m)}px`;
|
|
options.onSelect?.(station);
|
|
return;
|
|
}
|
|
options.onMove(station.chainage_m, Number(next.toFixed(2)), station);
|
|
});
|
|
line.addEventListener("contextmenu", (event) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
const rect = table.getBoundingClientRect();
|
|
menu.open(event.clientX - rect.left, event.clientY - rect.top, [
|
|
[station.origin === "pipe" ? "배관 삭제" : "구조물 삭제", () => options.onRemove(station)],
|
|
]);
|
|
});
|
|
layer.append(line);
|
|
});
|
|
|
|
// 빈 자리 우클릭 — 그 누가거리에 배관을 넣는다.
|
|
table.addEventListener("contextmenu", (event) => {
|
|
if (menu.contains(event.target)) {
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
const rect = table.getBoundingClientRect();
|
|
const chainage = clamp(options.chainageAt(event.clientX - rect.left));
|
|
event.preventDefault();
|
|
menu.open(event.clientX - rect.left, event.clientY - rect.top, [
|
|
["배관 추가", () => options.onAddPipe(Number(chainage.toFixed(2)))],
|
|
]);
|
|
});
|
|
table.addEventListener("pointerdown", (event) => {
|
|
if (!menu.contains(event.target)) menu.close();
|
|
});
|
|
|
|
table.append(layer, menu.element);
|
|
}
|
|
|
|
/** 선을 잡았다고 볼 여유 — 스타일에서 선 폭을 정할 때 함께 쓴다. */
|
|
export const STRUCTURE_GRAB_SLACK_PX = GRAB_SLACK_PX;
|