Files
Aislo/B06_Section/logic/B06_Section_UI_Page_Patches.ts
T
eomsangdonandClaude Opus 5.5 e4befd158a refactor(472-3 B06 구조물): 관 · BOX · 세월교 · 물넘이 모양 계산을 logic 으로 옮김(518-2)
- logic 새 파일 9 — B06_Section_Cross_Culvert · _Culvert_Geom · _Culvert_Extra · _Culvert_Tiers ·
  _Culvert_Types · _Culvert_Wire · _Box_Geom · _Ford_Geom · _Ford_Pavement
- 그림 파일(UI_Cross_Culvert · _Culvert_Types · _Box · _Ford · _Ford_Pavement)은 받은 좌표로 그리기만 ·
  관 겹 꼭짓점(pipeShellCorners) · 물넘이 노면 끝 · 월류선 · 유출 성토선은 logic 으로
- 옛 순수 기하 6 파일(UI_Cross_Culvert_Geom · _Extra · _Tiers · _Wire · _Box_Geom · _Ford_Geom) 지움 ·
  부르는 쪽(B05 Corridor · Structure_Layouts · Route_Carry_Patch · Wall 등)은 import 줄만
- test_472_3 GEOMETRY_PULLS 자기 11 줄 + 더는 안 끌리는 Design_Surface 1 줄 걷음
- 시험 · helper 경로를 새 logic 파일로 — 옮긴 파일을 가리키던 낡은 경로

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 19:17:23 +09:00

130 lines
6.6 KiB
TypeScript

/* =============================================================================
* B06_Section_UI_Page_Patches.ts
* 확정·임시저장이 보내는 **측점별 편집분(cross_patches) 조립** — 페이지
* (`_UI_Page.ts`)에서 700줄 제한으로 분리했다(2026-08-25).
*
* 세션·제어기에 흩어져 있던 조작값을 누가거리 하나로 합친다. 값은 만들지 않고
* 모으기만 한다 — 판정·한계는 각 제어기가 이미 끝냈다.
* ========================================================================== */
import type { CrossSectionPatch, StoredWallAdjust, StoredWallSpan } from "../B06_Section_Api_Fetch";
import type { BasinAdjust } from "./B06_Section_Cross_Culvert_Types";
import type { FordAdjust } from "./B06_Section_Cross_Ford_Geom";
import type { BoxAdjust } from "./B06_Section_Cross_Box_Geom";
import type { InletStructureChoice } from "./B06_Section_Cross_Culvert";
export interface CrossPatchSources {
/** 암 경계선 오프셋(누가거리 문자열 키). */
rockOffsets: Map<string, number>;
/** 측점별 암 절토 경사비(1:n 의 n). **0 은 「표준값을 씀」**(되돌리기)이다. */
cutSlopeRatios: Map<string, number>;
/** 측점 개별 표시 반폭(m). */
stationWidths: Map<string, number>;
inletStructures: Map<string, InletStructureChoice>;
basinAdjustments: Map<string, BasinAdjust>;
/** 벽 역할 → 값 · null = 되돌림(바탕 dict 에서 그 벽을 뺌 · 445 M2). */
revetAdjusts: Map<number, Record<string, StoredWallAdjust | null>>;
/** 세션에 있는 쪽만(445 H3 — 없는 쪽은 바탕 값 그대로). */
extraCounts: Map<number, Partial<{ outlet: number; basin: number }>>;
/** 다단 단별 구간값(2026-08-29) — 키는 벽 키("extra0"…/"bextra0"…). */
extraSpans: Map<number, Record<string, StoredWallSpan>>;
fordAdjusts: Map<number, FordAdjust>;
boxAdjusts: Map<number, BoxAdjust>;
linkFlags: Map<number, { detached?: boolean; followGrade?: boolean }>;
}
/** 누가거리별 패치를 합쳐 한 배열로 돌려준다. */
export function buildCrossPatches(sources: CrossPatchSources): CrossSectionPatch[] {
const patchByChainage = new Map<number, CrossSectionPatch>();
const patchFor = (chainageM: number): CrossSectionPatch => {
const existing = patchByChainage.get(chainageM);
if (existing) return existing;
const created: CrossSectionPatch = { chainage_m: chainageM };
patchByChainage.set(chainageM, created);
return created;
};
sources.rockOffsets.forEach((offset, chainage) => {
patchFor(Number(chainage)).rock_boundary_offset_m = offset;
});
// 측점별 암 절토 경사(2026-09-07) — 0 이면 「표준값을 씀」이라 정본의 옛 값을 덮어 지운다.
sources.cutSlopeRatios.forEach((ratio, chainage) => {
patchFor(Number(chainage)).cut_slope_ratio_user = ratio;
});
// 개별 표시 반폭(2026-08-06) — design에 병합돼 재접근 시 유지된다.
sources.stationWidths.forEach((width, chainage) => {
patchFor(Number(chainage)).display_half_width_m = width;
});
sources.inletStructures.forEach((structure, chainage) => {
patchFor(Number(chainage)).inlet_structure = structure;
});
sources.basinAdjustments.forEach((adjust, chainage) => {
patchFor(Number(chainage)).basin_adjust = adjust;
});
// 기슭막이 4축·다단 단 수 — 세션 전용이던 값을 정본에 실어 확정한다(2026-08-24).
// 되돌림(null) · 한쪽 단 수는 바탕과 합칠 때 풀림(`mergeCrossPatches`) — 혼자 나가면 null 걷고 빈 쪽 0
sources.revetAdjusts.forEach((adjusts, chainage) => {
patchFor(chainage).revet_adjust = adjusts as Record<string, StoredWallAdjust>;
});
sources.extraCounts.forEach((counts, chainage) => {
patchFor(chainage).extra_wall_counts = counts as { outlet: number; basin: number };
});
// 다단 단별 구간값 — 세션에서 만진 값을 정본에 싣는다(2026-08-29 사용자).
sources.extraSpans.forEach((spans, chainage) => {
patchFor(chainage).extra_spans = spans;
});
// 세월교 측벽 조작값(2026-08-25) — 배수관 값과 같은 자리에 실어 3D·재계산이 잇는다.
sources.fordAdjusts.forEach((adjust, chainage) => {
patchFor(chainage).ford_adjust = adjust;
});
sources.boxAdjusts.forEach((adjust, chainage) => {
patchFor(chainage).box_adjust = adjust;
});
// 연동 해제(측점별)·종단경사 반영(전체 공통) — 같은 체계로 정본에 싣는다.
sources.linkFlags.forEach((flags, chainage) => {
if (flags.detached !== undefined) patchFor(chainage).revet_link_detached = flags.detached;
if (flags.followGrade !== undefined) patchFor(chainage).revet_follow_grade = flags.followGrade;
});
return [...patchByChainage.values()];
}
/** 벽 · 단 · 쪽 열쇠로 담는 칸 — 세션은 만진 열쇠만 들고 있어 바탕(캐시 설계)과 **열쇠마다** 합침(445 H3). */
const DICT_KEYS = new Set(["revet_adjust", "extra_spans", "extra_wall_counts"]);
type Loose = Record<string, unknown>;
const isDict = (value: unknown): value is Loose =>
!!value && typeof value === "object" && !Array.isArray(value);
/** 안쪽 null = 되돌림 — 그 열쇠를 뺌(서버는 dict 를 통째 갈아 끼움). */
const withoutNulls = (dict: Loose): Loose =>
Object.fromEntries(Object.entries(dict).filter(([, value]) => value !== null));
/**
* 저장 patch = 바탕(캐시 설계의 사용자 칸 · 측점마다 통째) 위에 세션 고침을 얹음(445 H2 · H3 · M2).
* 같은 측점 = 누가거리 0.01 m 글. 벽 · 단 · 쪽 dict 는 열쇠마다 합치고 안쪽 null 은 뺌 · 나머지 칸은 세션이 이김.
* 바탕에 없는 측점의 세션 고침도 그대로 나감(안쪽 null 걷고 · 단 수 빈 쪽 0).
*/
export function mergeCrossPatches(
base: readonly CrossSectionPatch[],
overlay: readonly CrossSectionPatch[],
): CrossSectionPatch[] {
const out = new Map<string, Loose>();
const join = (patch: CrossSectionPatch): void => {
const at = patch.chainage_m.toFixed(2);
const into = out.get(at) ?? { chainage_m: patch.chainage_m };
for (const [key, value] of Object.entries(patch)) {
if (key === "chainage_m" || value === undefined) continue;
into[key] =
DICT_KEYS.has(key) && isDict(value)
? withoutNulls({ ...(isDict(into[key]) ? into[key] : {}), ...value })
: value;
}
out.set(at, into);
};
base.forEach(join);
overlay.forEach(join);
for (const patch of out.values()) {
const counts = patch.extra_wall_counts;
if (isDict(counts)) patch.extra_wall_counts = { outlet: 0, basin: 0, ...counts };
}
return [...out.values()] as unknown as CrossSectionPatch[];
}