Files
Aislo/B05_Profile/B05_Profile_UI_Structures_Snap.ts
T
eomsangdonandClaude Opus 5.5 93e86f87c6 fix(b05,b06): 측점 한 벌 — 정수 m · 같은 m 측점 하나 · 목록 전체 다시 맞춤 (272)
- 규칙: 구조물 · 관 기준 측점 = 정수 m 사사오입(일반 측점 1 m 붙기 뺌) · 측점 심는 자리 TS 짝(plantedChainages ↔ resolve_extra_stations) · 같은 m 면 구조물 측점 하나(격자 자리 grid) · 떠나면 일반 측점(oneStationList ↔ _merge_irregular_into_longitudinal)
- 서버: B05 [저장] · B06 저장 · [측점 만들기]가 목록 전체 다시 맞춤(빈 목록도 · 옛 글 · 횡단 파일 글) · 계획선 측점 줄 다시 셈 · 정본에 없는 횡단 행은 0.5 m 안 새 측점으로 옮기고 나머지 지움 · 옛 소수 측점 꼭짓점 · 편집은 정수 자리로 · 맞춤 실패는 저장을 안 막음
- 화면: 종단 그래프 · 3D · 표 · ▲▼ 한 목록(초안 반영) · C군 구간형 · 독립 기슭막이 시작 · 끝 측점 · B06 카드는 초안에 없는 옛 구조물 측점 숨김 · 고름은 같은 m 구조물 측점 먼저 · 폼 거리 칸 정수 표기 · C군 계산 구간 · 포장 나눔 경계 정수
- 옛 자료: [정수 맞춤] 단추 없앰 · 종단 보기 열 때 한 번 자동 정수 · 알림 한 줄 · [저장]하면 남음
- 시험: test_272_1 · test_272_2 새로 · 234_1 · 238_1 · b05_extra_stations_supplier · b06_patch_station · b05_station_roundtrip · b05_b06_save_roundtrip 맞춤

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LLCk2y5xE8YGVgjbTntWsY
2026-10-11 19:53:24 +09:00

136 lines
6.4 KiB
TypeScript

/* =============================================================================
* 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<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; attributes: FacilityAttributes }>;
/** 맞춘 자리에 다른 관이 있어 그대로 둔 관 수. */
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) || 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<PipeFacilityItem>;
/** 목록이 바뀜 알림(한 번). */
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;
}