B05 에서만 되던 끌어 옮기기를 B06 종단에도 붙임 — 측점선과 알약 어느 쪽을 끌어도 같은 길을 타고, 관은 예약 이동(저장·확정 때 정본과 배수유역 재분할), 구조물은 정본 이동임. 끌기 판별과 대상 찾기는 새 모듈(B06_Section_UI_Section_View_Menu)로 모아, 종단 화면 본체가 더 커지지 않게 둠. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fe1QWPTfw11PaKh2LjwXdR
131 lines
5.7 KiB
TypeScript
131 lines
5.7 KiB
TypeScript
/* =============================================================================
|
|
* B06_Section_UI_Section_View_Menu.ts
|
|
* B06 종단 그래프 위 **우클릭 메뉴** — B05 와 같은 부품(`mountStructureMenu`)을 그대로 쓴다.
|
|
*
|
|
* 왜 여기서도 여나(2026-09-12 사용자) — B05·B06 은 한 페이지다. 같은 그래프를 보면서
|
|
* 한쪽에서만 넣고 뺄 수 있으면 사용자가 화면을 오가야 하고 직관도 깨진다.
|
|
*
|
|
* 정본에 바로 쓰지 않는다 — 추가·삭제·이동은 **세션에 예약**하고 [저장]·[확정]에서
|
|
* 관 정본(`pipe_points.json`)으로 나간다(CLAUDE.md 5장). 세부 배수유역 재분할은 그때
|
|
* 서버가 관 목록을 다시 받아 처리한다.
|
|
* ========================================================================== */
|
|
|
|
import type { StructureInstance, StructureType } from "../B05_Profile/B05_Profile_Api_Structures";
|
|
import { structureAnchorM } from "../B05_Profile/B05_Profile_Api_Structures";
|
|
import type { IrregularStation } from "../B05_Profile/B05_Profile_UI_IrregularStations";
|
|
import { mountStructureMenu } from "../B05_Profile/B05_Profile_UI_Profile_Structures";
|
|
|
|
/** 종단 그래프에서 구조물을 넣고 빼는 길 — 페이지가 물려 준다. 없으면 메뉴를 안 붙인다. */
|
|
export interface SectionStructureEdit {
|
|
/** 관(계곡 통과 시설)을 그 자리에 넣는다. */
|
|
addPipe: (chainageM: number) => void;
|
|
/** 관을 뺀다. */
|
|
removePipe: (chainageM: number) => void;
|
|
/** 레지스트리 종류를 골라 구조물을 넣는다(좌측 「구조물 배치」와 같은 2단 메뉴). */
|
|
addStructureType: (chainageM: number, typeId: string) => void;
|
|
/** 구조물(관 아님)을 뺀다. */
|
|
removeStructure: (structureId: string) => void;
|
|
/** 관을 다른 측점으로 옮긴다(측점선·알약 끌기). */
|
|
movePipe: (fromChainageM: number, toChainageM: number) => void;
|
|
/** 구조물(관 아님)을 다른 측점으로 옮긴다. */
|
|
moveStructure: (structureId: string, toChainageM: number) => void;
|
|
}
|
|
|
|
/**
|
|
* 측점선·알약을 끌어 놓았다 — 그래프 위 id 로 구조물을 찾아 같은 예약 경로로 보낸다.
|
|
* 종단 측점선의 id 는 **종단 정본 측점 id** 라 알약(구조물 id)과 다를 수 있다. 둘 다 받아
|
|
* 같은 자리(누가거리)로 맞춘다.
|
|
*/
|
|
export function moveMarkById(
|
|
markId: string,
|
|
toChainageM: number,
|
|
input: {
|
|
structures: ReadonlyArray<StructureInstance>;
|
|
stations: ReadonlyArray<{ station_id: string; chainage_m: number }>;
|
|
edit: SectionStructureEdit;
|
|
},
|
|
): void {
|
|
const direct = input.structures.find((entry) => String(entry.structure_id) === markId);
|
|
if (direct) {
|
|
moveStructureMark(input.structures, input.edit, markId, toChainageM);
|
|
return;
|
|
}
|
|
const station = input.stations.find((entry) => String(entry.station_id) === markId);
|
|
if (!station) return;
|
|
// 측점선 id 로 온 경우 — 그 측점 자리에 선 구조물을 찾는다(관 매칭과 같은 0.51m).
|
|
const near = input.structures.find(
|
|
(entry) => Math.abs(structureAnchorM(entry) - station.chainage_m) < 0.51,
|
|
);
|
|
if (near) moveStructureMark(input.structures, input.edit, String(near.structure_id), toChainageM);
|
|
}
|
|
|
|
/** 측점선·알약을 끌었을 때 — 관인지 구조물인지 갈라 같은 예약 경로로 보낸다. */
|
|
function moveStructureMark(
|
|
structures: ReadonlyArray<StructureInstance>,
|
|
edit: SectionStructureEdit,
|
|
structureId: string,
|
|
toChainageM: number,
|
|
): void {
|
|
const hit = structures.find((entry) => String(entry.structure_id) === structureId);
|
|
if (!hit) return;
|
|
const from = structureAnchorM(hit);
|
|
if (Math.abs(from - toChainageM) < 0.005) return;
|
|
if (isPipeMark(hit)) edit.movePipe(from, toChainageM);
|
|
else edit.moveStructure(structureId, toChainageM);
|
|
}
|
|
|
|
/** 알약·우클릭이 쓰는 「관인가」 판정 — 관은 정본이 달라 지우는 길도 다르다. */
|
|
export function isPipeMark(structure: StructureInstance): boolean {
|
|
return String(structure.structure_id ?? "").startsWith("pipe-");
|
|
}
|
|
|
|
/**
|
|
* 종단 그래프 칸에 우클릭 메뉴를 붙인다. 그래프는 그릴 때마다 새로 만들어지므로 이
|
|
* 함수도 그때마다 다시 부른다 — 상태를 밖에 남기지 않는다(B05 와 같은 규칙).
|
|
*/
|
|
export function mountSectionStructureMenu(
|
|
host: HTMLElement,
|
|
input: {
|
|
structures: ReadonlyArray<StructureInstance>;
|
|
types: ReadonlyArray<StructureType>;
|
|
x: (chainageM: number) => number;
|
|
chainageAt: (px: number) => number;
|
|
maxChainageM: number;
|
|
edit: SectionStructureEdit;
|
|
},
|
|
): void {
|
|
// 메뉴가 쓰는 최소 정보만 옮겨 담는다 — B06 에는 B05 의 비정규 측점 목록이 없다.
|
|
const stations: IrregularStation[] = input.structures.map((structure) => {
|
|
const chainage = structureAnchorM(structure);
|
|
const pipe = isPipeMark(structure);
|
|
return {
|
|
id: String(structure.structure_id),
|
|
station: 0,
|
|
remainder: 0,
|
|
chainage_m: chainage,
|
|
structure:
|
|
input.types.find((type) => type.type_id === structure.type_id)?.name ??
|
|
String(structure.type_id),
|
|
origin: pipe ? "pipe" : "user",
|
|
} as IrregularStation;
|
|
});
|
|
|
|
mountStructureMenu(host, {
|
|
stations,
|
|
x: input.x,
|
|
chainageAt: input.chainageAt,
|
|
maxChainageM: input.maxChainageM,
|
|
onRemove: (station) => {
|
|
if (station.origin === "pipe") input.edit.removePipe(station.chainage_m);
|
|
else input.edit.removeStructure(station.id);
|
|
},
|
|
onAddPipe: (chainageM) => input.edit.addPipe(chainageM),
|
|
structureTypes: input.types.map((type) => ({
|
|
type_id: type.type_id,
|
|
group: type.group,
|
|
name: type.name,
|
|
})),
|
|
onAddStructureType: (chainageM, typeId) => input.edit.addStructureType(chainageM, typeId),
|
|
});
|
|
}
|