/* ============================================================================= * B05_Profile_UI_Structures_Snap.ts * 구조물 기준 측점 정수 맞춤(234-1 · 272) — 끌기 · 방향키로 옮기는 구조물 한 건의 모양과 * 옛 소수 자료를 정수 m 로 옮기는 자동 맞춤(종단 보기를 열 때 한 번 · 단추 없음 · 요구 167). * * 맞춤 규칙은 공용 `common_util_structure_station`(파이썬 짝) 한 벌. 구조물은 한 번에, 관은 하나씩 * 폼 수정과 같은 길(`onPipeUpdate`)로 옮긴다 — 계획고 편집 따라가기(213-2) · 시설 속성 이어받기가 * 손으로 옮길 때와 같다. 정본은 [저장]에서 나간다(알림 한 줄). * ========================================================================== */ import { showToast } from "@ui/ui_template_elements"; import { isSnappedStation, snapStructureStation, } from "../common_util/common_util_structure_station"; import { PIPE_MATCH_M } from "../A00_Common/design/design_selection"; export { snapStructureStation }; import { structureAnchorM, type StructureInstance } from "./B05_Profile_Api_Structures"; import type { FacilityAttributes } from "./B05_Profile_UI_Drainage_Facility"; import type { PipeFacilityItem } from "./B05_Profile_UI_Structures_Panel_Types"; /** 구조물을 그 측점으로 옮긴 모양 — 구간은 기준과의 거리 · 길이를 지키되 시작 · 끝도 맞춤. */ export function movedStructure(target: StructureInstance, toChainageM: number): StructureInstance { const anchor = snapStructureStation(toChainageM); if (target.placement !== "interval") return { ...target, chainage_m: anchor }; const length = (target.end_m ?? 0) - (target.start_m ?? 0); const start = snapStructureStation(anchor - (structureAnchorM(target) - (target.start_m ?? 0))); const end = Math.max(snapStructureStation(start + length), start + 1); // 끝도 정수 · 1 m 이상(167) return { ...target, chainage_m: Math.min(Math.max(anchor, start), end), start_m: start, end_m: end, }; } /** 사라진 측점의 계획고 편집이 따라갈 새 측점 — 맞춘 자리(0.006m 안 · 213-2 와 같은 폭)의 것. */ export function followSnapped( station: T, born: ReadonlyArray, ): T | null { const to = snapStructureStation(station.chainage_m); return born.find((next) => Math.abs(next.chainage_m - to) < 0.006) ?? null; } const offGrid = (value: number | null | undefined): boolean => typeof value === "number" && !isSnappedStation(value); /** 기준 · 시작 · 끝 중 하나라도 맞춤 밖(소수)인 구조물인가. */ export const structureOffGrid = (structure: StructureInstance): boolean => offGrid(structure.chainage_m) || offGrid(structure.start_m) || offGrid(structure.end_m); export interface SnapPlan { /** 목록 자리 · 옮긴 모양. */ structures: Array<{ index: number; next: StructureInstance }>; /** 옛 자리 → 새 자리 · 시작 · 끝도 맞춘 속성(독립 기슭막이 구간). */ pipes: Array<{ from: number; to: number; attributes: FacilityAttributes }>; /** 맞춘 자리에 다른 관이 있어 그대로 둔 관 수. */ skipped: number; } /** 옮길 것 셈 — 구조물은 옮긴 모양, 관은 (옛 자리 → 새 자리) · 자리 겹친 관은 건너뜀. */ export function snapPlan( structures: ReadonlyArray, pipes: ReadonlyArray, ): SnapPlan { const moved = structures.flatMap((structure, index) => structureOffGrid(structure) ? [{ index, next: movedStructure(structure, structureAnchorM(structure)) }] : [], ); const taken = pipes.filter((pipe) => !offGrid(pipe.chainage_m)).map((pipe) => pipe.chainage_m); const plan: SnapPlan = { structures: moved, pipes: [], skipped: 0 }; [...pipes] .filter((pipe) => offGrid(pipe.chainage_m) || offGrid(pipe.start_m) || offGrid(pipe.end_m)) .sort((left, right) => left.chainage_m - right.chainage_m) .forEach((pipe) => { const to = snapStructureStation(pipe.chainage_m); if (to !== pipe.chainage_m && taken.some((other) => Math.abs(other - to) < PIPE_MATCH_M)) { plan.skipped += 1; return; } taken.push(to); const attributes = pipeAttributes(pipe); if (attributes.start_m !== undefined) attributes.start_m = snapStructureStation(attributes.start_m); if (attributes.end_m !== undefined) attributes.end_m = snapStructureStation(attributes.end_m); plan.pipes.push({ from: pipe.chainage_m, to, attributes }); }); return plan; } /** 관 한 건의 지금 속성 그대로 — 폼 [수정]과 같은 꼴(비운 칸 = 지움 규칙에 옛 값이 다 실림). */ export const pipeAttributes = (pipe: PipeFacilityItem): FacilityAttributes => ({ facility: pipe.facility, ...(pipe.start_m !== undefined ? { start_m: pipe.start_m } : {}), ...(pipe.end_m !== undefined ? { end_m: pipe.end_m } : {}), options: { ...(pipe.options ?? {}) }, summary_item: pipe.summary_item ?? null, group_items: pipe.group_items ?? null, }); export interface SnapContext { /** 패널이 쥔 구조물 배열(참조) — 옮긴 것을 제자리에 갈아 끼움. */ structures: StructureInstance[]; pipes: () => ReadonlyArray; /** 목록이 바뀜 알림(한 번). */ emit: () => void; /** 관 한 건 옮김 — 폼 [수정]과 같은 길. */ movePipe: (from: number, to: number, attributes: FacilityAttributes) => void; /** 열린 폼을 닫음 — 옛 측점을 쥔 폼이 다음 [수정]에 되돌리지 않게. */ closeForm: () => void; } /** 옛 소수 측점(관 · 구조물)을 정수 m 로 옮긴다(요구 167 · 확인 창 없이) — 옮긴 수. */ export function snapOldStations(ctx: SnapContext): number { const plan = snapPlan(ctx.structures, ctx.pipes()); const count = plan.structures.length + plan.pipes.length; if (count) { ctx.closeForm(); if (plan.structures.length) { plan.structures.forEach(({ index, next }) => (ctx.structures[index] = next)); ctx.emit(); } plan.pipes.forEach(({ from, to, attributes }) => ctx.movePipe(from, to, attributes)); } if (count || plan.skipped) showToast( (count ? `옛 소수 측점 ${count}건을 정수 m 로 옮겼습니다 — [저장]하면 남습니다.` : "") + (plan.skipped ? ` 맞출 자리에 다른 관이 있는 관 ${plan.skipped}건은 그대로 둡니다.` : ""), plan.skipped ? "info" : "success", ); return count; }