- 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>
130 lines
6.6 KiB
TypeScript
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[];
|
|
}
|