- 맞춤 규칙 한 벌 — common_util_structure_station(파이썬 · TS 짝 · 거울 시험) - 입력 — 좌측 폼 칸(1 m 단계 · 친 값만) · 끌기 · 우클릭 · 방향키(1 m 걸음) · 배수유역도 관 · B06 관 옮김 · 추가 - 구간형 시작 · 끝도 정수 m · 일반 측점 = 횡단 상세를 읽을 때 등록 - 서버 자동 배치 · 노선 재투영 자리도 정수 m(확정본은 단추로만) - [정수 맞춤] — 구조물 목록 머리 · 확인 창 · 관은 하나씩 폼 [수정] 길 · 계획고 편집 따라감 · 결과 n건 알림 - 찾는 폭 한 값 — 손으로 적은 0.51 을 PIPE_MATCH_M 으로 - 시험 — test_234_1 새로 · 1 m 걸음 · 정수 입력에 맞춘 기대값 고침(219_1 · station_roundtrip) · 헬퍼 대역 보탬 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3mdb91125mPoC6tpsQx4P
161 lines
7.1 KiB
TypeScript
161 lines
7.1 KiB
TypeScript
/* =============================================================================
|
|
* B05_Profile_UI_Structures_Snap.ts
|
|
* 구조물 기준 측점 정수 맞춤(234-1) — 끌기 · 방향키로 옮기는 구조물 한 건의 모양과
|
|
* 기존 소수 자료를 옮기는 [정수 맞춤] 단추(좌측 구조물 목록 머리 · B05 · B06 한 벌).
|
|
*
|
|
* 맞춤 규칙은 공용 `common_util_structure_station`(파이썬 짝) 한 벌. 단추는 확인 창 뒤
|
|
* 구조물은 한 번에, 관은 하나씩 폼 수정과 같은 길(`onPipeUpdate`)로 옮긴다 — 계획고 편집
|
|
* 따라가기(213-2) · 시설 속성 이어받기가 손으로 옮길 때와 같다. 정본은 [저장]에서 나간다.
|
|
* ========================================================================== */
|
|
|
|
import { showConfirmDialog, 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 snappedEnd = snapStructureStation(start + length);
|
|
const end = snappedEnd > start ? snappedEnd : start + length;
|
|
return {
|
|
...target,
|
|
chainage_m: Math.min(Math.max(anchor, start), end),
|
|
start_m: start,
|
|
end_m: end,
|
|
};
|
|
}
|
|
|
|
/** 사라진 측점의 계획고 편집이 따라갈 새 측점 — 맞춘 자리(0.006m 안 · 213-2 와 같은 폭)의 것. */
|
|
export function followSnapped<T extends { chainage_m: number }>(
|
|
station: T,
|
|
born: ReadonlyArray<T>,
|
|
): 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; pipe: PipeFacilityItem }>;
|
|
/** 맞춘 자리에 다른 관이 있어 그대로 둔 관 수. */
|
|
skipped: number;
|
|
}
|
|
|
|
/** 옮길 것 셈 — 구조물은 옮긴 모양, 관은 (옛 자리 → 새 자리) · 자리 겹친 관은 건너뜀. */
|
|
export function snapPlan(
|
|
structures: ReadonlyArray<StructureInstance>,
|
|
pipes: ReadonlyArray<PipeFacilityItem>,
|
|
): 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))
|
|
.sort((left, right) => left.chainage_m - right.chainage_m)
|
|
.forEach((pipe) => {
|
|
const to = snapStructureStation(pipe.chainage_m);
|
|
if (taken.some((other) => Math.abs(other - to) < PIPE_MATCH_M)) {
|
|
plan.skipped += 1;
|
|
return;
|
|
}
|
|
taken.push(to);
|
|
plan.pipes.push({ from: pipe.chainage_m, to, pipe });
|
|
});
|
|
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 SnapDockContext {
|
|
/** 패널이 쥔 구조물 배열(참조) — 옮긴 것을 제자리에 갈아 끼움. */
|
|
structures: StructureInstance[];
|
|
pipes: () => ReadonlyArray<PipeFacilityItem>;
|
|
/** 목록이 바뀜 알림(한 번). */
|
|
emit: () => void;
|
|
/** 관 한 건 옮김 — 폼 [수정]과 같은 길. */
|
|
movePipe: (from: number, to: number, attributes: FacilityAttributes) => void;
|
|
/** 열린 폼을 닫음 — 옛 측점을 쥔 폼이 다음 [수정]에 되돌리지 않게. */
|
|
closeForm: () => void;
|
|
}
|
|
|
|
/** [정수 맞춤] 단추를 얹은 목록 뿌리 — 아래 고정 영역에 목록 대신 놓인다. */
|
|
export function snapDock(list: HTMLElement, ctx: SnapDockContext): HTMLElement {
|
|
const button = document.createElement("button");
|
|
button.type = "button";
|
|
button.className = "b05-route__irregular-btn b05-structure__snap";
|
|
button.textContent = "정수 맞춤";
|
|
button.title = "소수 기준 측점(관 · 구조물)을 정수 m 로 — 일반 측점 1 m 안이면 그 측점";
|
|
button.addEventListener("click", () => void runSnap(ctx));
|
|
const head = document.createElement("div");
|
|
head.className = "b05-structure__snap-row";
|
|
head.append(button);
|
|
const dock = document.createElement("div");
|
|
dock.className = "b05-structure__list-dock";
|
|
dock.append(head, list);
|
|
return dock;
|
|
}
|
|
|
|
export async function runSnap(ctx: SnapDockContext): Promise<number> {
|
|
const plan = snapPlan(ctx.structures, ctx.pipes());
|
|
const count = plan.structures.length + plan.pipes.length;
|
|
if (!count) {
|
|
showToast(
|
|
plan.skipped
|
|
? `소수 측점 관 ${plan.skipped}건은 맞출 자리에 다른 관이 있어 그대로 둡니다.`
|
|
: "소수 기준 측점이 없습니다.",
|
|
"info",
|
|
);
|
|
return 0;
|
|
}
|
|
const ok = await showConfirmDialog(
|
|
`소수 기준 측점 ${count}건(관 ${plan.pipes.length} · 구조물 ${plan.structures.length})을 ` +
|
|
"정수 m(일반 측점 1 m 안이면 그 측점)로 옮깁니다. 계획고 편집도 따라가며, [저장]을 눌러야 남습니다.",
|
|
"정수 맞춤",
|
|
);
|
|
if (!ok) return 0;
|
|
ctx.closeForm();
|
|
if (plan.structures.length) {
|
|
plan.structures.forEach(({ index, next }) => (ctx.structures[index] = next));
|
|
ctx.emit();
|
|
}
|
|
plan.pipes.forEach(({ from, to, pipe }) => ctx.movePipe(from, to, pipeAttributes(pipe)));
|
|
showToast(
|
|
`정수 맞춤 ${count}건` +
|
|
(plan.skipped ? ` · 자리 겹친 관 ${plan.skipped}건은 그대로` : "") +
|
|
" — [저장]을 눌러 남기세요.",
|
|
"success",
|
|
);
|
|
return count;
|
|
}
|