/* ============================================================================= * B06_Section_UI_Section_View_Menu.ts * B06 종단 그래프 구조물 조작 길(넣기 · 빼기 · 옮기기) — 페이지가 물려 준다. * * 정본에 바로 쓰지 않는다 — 추가·삭제·이동은 **세션에 예약**하고 [저장]·[확정]에서 * 관 정본(`pipe_points.json`)으로 나간다(CLAUDE.md 5장). 세부 배수유역 재분할은 그때 * 서버가 관 목록을 다시 받아 처리한다. * ========================================================================== */ import type { StructureInstance } from "../B05_Profile/B05_Profile_Api_Structures"; import { structureAnchorM } from "../B05_Profile/B05_Profile_Api_Structures"; import { PIPE_MATCH_M } from "../A00_Common/design/design_selection"; /** 종단 그래프에서 구조물을 넣고 빼는 길 — 페이지가 물려 준다. 없으면 메뉴를 안 붙인다. */ 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; /** 구조물(관 아님) 하나를 폼에 올리고 그 카드로 간다 — 같은 측점 관보다 먼저(52-15). */ selectStructure: (structureId: string) => void; /** 측점 없는 관(물넘이포장) 하나를 폼에 올림 — 카드 안 고름 · 그 자리로 스크롤만(요구 178). */ selectPipe?: (chainageM: number) => void; /** 배수 재계산이 관 종류 · 관경을 바꿈 — 카드(미리 뜬 새 관 그림 포함)를 다시 얹음(273-2). */ pipesChanged?: () => void; } /** * 측점선·알약을 끌어 놓았다 — 그래프 위 id 로 구조물을 찾아 같은 예약 경로로 보낸다. * 종단 측점선의 id 는 **종단 정본 측점 id** 라 알약(구조물 id)과 다를 수 있다. 둘 다 받아 * 같은 자리(누가거리)로 맞춘다. */ export function moveMarkById( markId: string, toChainageM: number, input: { structures: ReadonlyArray; 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) < PIPE_MATCH_M, ); if (near) moveStructureMark(input.structures, input.edit, String(near.structure_id), toChainageM); } /** 측점선·알약을 끌었을 때 — 관인지 구조물인지 갈라 같은 예약 경로로 보낸다. */ function moveStructureMark( structures: ReadonlyArray, 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-"); }