- logic/B06_Section_Route_Carry.ts = 측점 열쇠 목록 · 측점 열쇠 칸 · 누가거리 칸 넘김(Station_Keys 몸통) · Station_Keys = 세션 겉 - B06_Section_Route_Carry_Patch.ts = 조정창 · 옮긴 고침 · 관 옵션 얹기 · crossPatchesFromCache · 서버용 routeCarryPatches(옛 carryAfterRouteSave 차례) · Section_Adjusts · Section_Store = 겉 · 내줌 - 칸 목록 USER_TOUCHED_KEYS · STRUCTURE_ADJUST_KEYS = logic/B06_Section_Design_Keys.ts · PIPE_MATCH_M = common_util_pipe_match.ts(옛 자리는 내줌) - 시험 헬퍼 · 글 단언 시험 = 옮긴 자리로(목록 한 벌 자리만 바뀜) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
437 lines
20 KiB
TypeScript
437 lines
20 KiB
TypeScript
/* =============================================================================
|
|
* B06_Section_Route_Carry_Patch.ts
|
|
* 세션 고침 → 상세 설계 칸 얹기 · [저장] 횡단 patch 꼴 **한 벌**(472-6 6-B) — 화면 · 세션 · fetch 없음.
|
|
*
|
|
* · `layDraftAdjusts` · `layCarriedEdits` — 조정창 · 옮긴 고침 칸(읽는 함수를 받음 · 브라우저 세션 겉 =
|
|
* `B06_Section_Section_Adjusts`) · `layPipeOptions` — 관 옵션의 조정값 몫(387-4b).
|
|
* · `crossPatchesFromCache` — 캐시 설계의 사용자 칸 = [저장] patch 바탕(445 · 상세 가게가 내줌).
|
|
* · `routeCarryPatches` — 노선 바뀐 [저장]: 옛 노선 세션 칸 → 새 노선 서버 상세(측점 열쇠 넘김
|
|
* `logic/B06_Section_Route_Carry`) → 얹기 → patch(옛 브라우저 길 `carryAfterRouteSave` 와 같은 차례) ·
|
|
* 서버 Node 입구 `stations.carry` 가 부름(`A00_Common/design/logic/design_calc_node_carry.ts`).
|
|
* logic 밖 = 구조물 그림 기하(관 벽 · 단 · 구간 · 세월 · BOX 기본값)를 끔 — 서버 Node 묶음 알려진 끌림.
|
|
* ========================================================================== */
|
|
|
|
import type {
|
|
CrossDesign,
|
|
CrossSectionPatch,
|
|
SectionDetailResponse,
|
|
} from "./B06_Section_Api_Fetch";
|
|
import { DEFAULT_BASIN_ADJUST, ZERO_ADJUST } from "./B06_Section_UI_Cross_Culvert_Types";
|
|
import type { WallAdjust } from "./B06_Section_UI_Cross_Culvert_Types";
|
|
import { fordAdjustOf } from "./B06_Section_UI_Cross_Ford_Geom";
|
|
import { DEFAULT_BOX_SIDE_ADJUST } from "./B06_Section_UI_Cross_Box_Geom";
|
|
import { PIPE_MATCH_M } from "../common_util/common_util_pipe_match";
|
|
import { inferStationInterval } from "../common_util/common_util_svg";
|
|
import { applySpanPatch, WALL_MIN_PURE_HEIGHT_M } from "./logic/B06_Section_UI_Cross_Culvert_Const";
|
|
import { tierSpanOf } from "./B06_Section_UI_Cross_Culvert_Wire";
|
|
import { computeStoredLayouts } from "./B06_Section_Structure_Layouts";
|
|
import { ROW_CARRY_KEYS } from "./logic/B06_Section_Station_Swap";
|
|
import { mergeCrossPatches } from "./logic/B06_Section_UI_Page_Patches";
|
|
import { STRUCTURE_ADJUST_KEYS, USER_TOUCHED_KEYS } from "./logic/B06_Section_Design_Keys";
|
|
import {
|
|
carryChainageStates,
|
|
carryKeyedStates,
|
|
readKeyedIn,
|
|
stationKeysFrom,
|
|
type Keyed,
|
|
type KeyedState,
|
|
type Owner,
|
|
type Raw,
|
|
} from "./logic/B06_Section_Route_Carry";
|
|
|
|
export type Cells = Record<string, unknown>;
|
|
/** 누가거리(숫자) → 쪽마다 값(세션에 있는 쪽만 · 노견 확장 · 소단 깊이). */
|
|
export type SideValues = ReadonlyMap<number, Partial<Record<"left" | "right", number>>>;
|
|
type Obj = Record<string, unknown>;
|
|
const INLET = ["auto", "revet", "I", "L", "U"];
|
|
const isObj = (value: unknown): value is Obj => !!value && typeof value === "object";
|
|
const count = (value: unknown): number =>
|
|
Number.isInteger(value) && (value as number) > 0 ? (value as number) : 0;
|
|
|
|
/** 조정창 읽기와 같은 꼴 — 옛 숫자 = x 이동량 · d 0 = 기본 자리(null). */
|
|
function wallAdjust(value: unknown): WallAdjust | null {
|
|
if (typeof value === "number" && Number.isFinite(value)) return { ...ZERO_ADJUST, x: value };
|
|
if (!isObj(value)) return null;
|
|
const partial = value as Partial<WallAdjust>;
|
|
return { ...ZERO_ADJUST, ...partial, d: partial.d ? partial.d : null };
|
|
}
|
|
|
|
/** 저장 안 한 조정창 값(누가거리 열쇠 칸 · `read`)을 상세 설계 칸에 얹음 — 조정 4축 · 형식 · 단 수 · 구간 · 연동 ·
|
|
* 세월교 · BOX(387-4 · 브라우저 세션 = `withDraftAdjusts` · 서버 = 노선 바뀐 [저장] 넘김 · 472-6). */
|
|
export function layDraftAdjusts(
|
|
detail: SectionDetailResponse,
|
|
read: (name: string) => Cells,
|
|
): void {
|
|
const revet = read("revetx");
|
|
const basin = read("basinadjust");
|
|
const inlet = read("inletstruct");
|
|
const extra = read("extrawall");
|
|
const spans = read("extraspan");
|
|
const ford = read("fordadjust");
|
|
const box = read("boxadjust");
|
|
const link = read("revetlink") as { detached?: Cells; grade?: Cells };
|
|
for (const section of detail.cross_sections) {
|
|
const design = section.design as (CrossDesign & Obj) | null | undefined;
|
|
if (!design) continue;
|
|
const at = section.chainage_m.toFixed(2);
|
|
const own = (cells: Cells): Array<[string, unknown]> =>
|
|
Object.entries(cells)
|
|
.filter(([key]) => key.startsWith(`${at}:`))
|
|
.map(([key, value]) => [key.slice(at.length + 1), value]);
|
|
for (const [role, value] of own(revet)) {
|
|
const adjust = wallAdjust(value);
|
|
if (adjust) design.revet_adjust = { ...(design.revet_adjust ?? {}), [role]: adjust };
|
|
else if (value === null && design.revet_adjust?.[role]) {
|
|
// null = 되돌림 — 저장분 그 벽을 뺌(445 M2)
|
|
const { [role]: _gone, ...rest } = design.revet_adjust;
|
|
design.revet_adjust = rest;
|
|
}
|
|
}
|
|
if (typeof inlet[at] === "string" && INLET.includes(inlet[at] as string))
|
|
design.inlet_structure = inlet[at] as CrossDesign["inlet_structure"];
|
|
if (isObj(basin[at])) design.basin_adjust = { ...DEFAULT_BASIN_ADJUST, ...basin[at] };
|
|
// 세션에 있는 쪽만 — 없는 쪽은 저장분 그대로(445 H3)
|
|
const saved = design.extra_wall_counts;
|
|
if (at in extra || `b${at}` in extra)
|
|
design.extra_wall_counts = {
|
|
outlet: at in extra ? count(extra[at]) : (saved?.outlet ?? 0),
|
|
basin: `b${at}` in extra ? count(extra[`b${at}`]) : (saved?.basin ?? 0),
|
|
};
|
|
for (const [wall, value] of own(spans)) {
|
|
const span = value as { lengthM: number; beforeM: number; afterM: number } | null;
|
|
if (!span || !Number.isFinite(span.beforeM) || !Number.isFinite(span.afterM)) continue;
|
|
design.extra_spans = {
|
|
...(design.extra_spans ?? {}),
|
|
[wall]: { length_m: span.lengthM, before_m: span.beforeM, after_m: span.afterM },
|
|
};
|
|
}
|
|
if (isObj(link.detached) && at in link.detached)
|
|
design.revet_link_detached = !!link.detached[at];
|
|
if (isObj(link.grade) && at in link.grade) design.revet_follow_grade = !!link.grade[at];
|
|
if (isObj(ford[at])) design.ford_adjust = fordAdjustOf(ford[at]);
|
|
const sides = box[at] as { left?: Obj; right?: Obj } | undefined;
|
|
if (isObj(sides))
|
|
design.box_adjust = {
|
|
left: { ...DEFAULT_BOX_SIDE_ADJUST, ...sides.left },
|
|
right: { ...DEFAULT_BOX_SIDE_ADJUST, ...sides.right },
|
|
};
|
|
}
|
|
}
|
|
|
|
/** 노선 저장 뒤 새 노선으로 옮긴 세션 고침(417) — 사면 쉬프트 · 소단 깊이 · 측점 폭 · 메모 · 상세 표를 설계 칸에 얹음 ·
|
|
* 같은 [저장]의 횡단 패치(`crossPatchesFromCache`)가 실음. 조절 4축 · 형식은 `layDraftAdjusts`(브라우저 세션 =
|
|
* `withCarriedEdits`). */
|
|
export function layCarriedEdits(
|
|
detail: SectionDetailResponse,
|
|
read: (name: string) => Cells,
|
|
sideValues: { shoulder: SideValues; berm: SideValues },
|
|
): void {
|
|
const cells: Array<[string, Cells]> = [
|
|
["display_half_width_m", read("crossw")],
|
|
["structure_note", read("structure-notes")],
|
|
["structure_note_cad", read("structure-note-cads")],
|
|
["detail_table", read("detail-table")],
|
|
];
|
|
const sides: Array<[string, SideValues]> = [
|
|
["shoulder_extend", sideValues.shoulder],
|
|
["berm_depth", sideValues.berm],
|
|
];
|
|
for (const section of detail.cross_sections) {
|
|
const design = section.design as (CrossDesign & Obj) | null | undefined;
|
|
if (!design) continue;
|
|
const at = section.chainage_m.toFixed(2);
|
|
for (const [field, values] of cells) if (values[at] !== undefined) design[field] = values[at];
|
|
for (const [field, values] of sides) {
|
|
const own = values.get(Number(at));
|
|
for (const side of ["left", "right"] as const)
|
|
if (own?.[side] !== undefined) design[`${field}_${side}_m`] = own[side];
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 캐시에 얹힌 **사용자 수정분**을 저장 payload로 뽑는다(2026-08-24 사용자 확정 흐름).
|
|
*
|
|
* 초기 계산값은 원복용으로 서버에 그대로 있고, 사용자가 만진 값은 이 캐시 한 세트가
|
|
* 최종본이다. B05 [임시저장]도 이 세트를 함께 보내야 B06에서 만진 구조물 조정이
|
|
* 영구저장소에 남는다 — 안 보내면 캐시만 비워져 편집이 사라진다.
|
|
*
|
|
* 초기 계산값에는 없는 키(사용자가 만져야 생기는 키)를 측점마다 **통째로** 싣는다 — [저장] patch 의 바탕
|
|
* (445 H3 · 세션 고침은 그 위에 `mergeCrossPatches`). `savedAt`(서버 행 누가거리)을 주면 그 자리 행이 없는
|
|
* 측점(옮긴 관 · 구조물 · 새 측점 = 서버가 기본 설계로 새로 세움)은 지반 등 입력(`ROW_CARRY_KEYS`)도 실음(445 H2).
|
|
*/
|
|
export function crossPatchesFromCache(
|
|
detail: SectionDetailResponse,
|
|
kindChanged: readonly number[] = [],
|
|
savedAt?: readonly number[],
|
|
): CrossSectionPatch[] {
|
|
const patches: CrossSectionPatch[] = [];
|
|
// 관 종류를 바꾼 측점(318-4) — 옛 종류 조정값은 서버가 비움 · 캐시에 남은 옛 값을 다시 안 실음
|
|
const structureKeys: readonly string[] = STRUCTURE_ADJUST_KEYS;
|
|
const reset = (at: number) => kindChanged.some((c) => Math.abs(c - at) < PIPE_MATCH_M);
|
|
const rowless = (at: number) => !!savedAt && savedAt.every((c) => Math.abs(c - at) >= 0.01);
|
|
for (const at of kindChanged) patches.push({ chainage_m: at, structure_reset: true });
|
|
for (const section of detail.cross_sections) {
|
|
const design = section.design;
|
|
if (!design) continue;
|
|
const patch: CrossSectionPatch = { chainage_m: section.chainage_m };
|
|
const skip = reset(section.chainage_m);
|
|
let touched = false;
|
|
// 필드를 손으로 나열하지 않는다(2026-09-07) — 목록은 `USER_TOUCHED_KEYS` 한 벌뿐이고
|
|
// 서버 목록과 짝이다. 나열이 흩어져 있던 탓에 새 값을 더할 때 한 곳이 빠져 그 값이
|
|
// 조용히 사라졌다(`extra_spans` 실사고 `b6941bd2`).
|
|
const source = design as unknown as Record<string, unknown>;
|
|
const target = patch as unknown as Record<string, unknown>;
|
|
const keys = rowless(section.chainage_m)
|
|
? [...USER_TOUCHED_KEYS, ...ROW_CARRY_KEYS]
|
|
: USER_TOUCHED_KEYS;
|
|
for (const key of keys) {
|
|
const value = source[key];
|
|
if (value === undefined || value === null || (skip && structureKeys.includes(key))) continue;
|
|
target[key] = value;
|
|
touched = true;
|
|
}
|
|
if (touched) patches.push(patch);
|
|
}
|
|
return patches;
|
|
}
|
|
|
|
/** 관 하나 — 자리 · 폼 칸(관 초안 · 저장 관 `pipe_points` 같은 꼴). */
|
|
export interface OptionPipe {
|
|
chainage_m: number;
|
|
options?: Record<string, unknown> | null;
|
|
}
|
|
|
|
const isDefault = (a: WallAdjust): boolean =>
|
|
Math.abs(a.x) < 1e-9 && a.d == null && a.h == null && a.m == null;
|
|
const round1 = (value: number): number => Math.round(value * 10) / 10;
|
|
|
|
/** 조정창 `update` 와 같은 꼴 — x · d 는 그대로 · h · m 만(폼 한 칸이 주인 · 빈 칸 = 자동 null). */
|
|
function setWall(design: CrossDesign, role: string, h: number | null, m: string | null): void {
|
|
const current = { ...ZERO_ADJUST, ...(design.revet_adjust?.[role] ?? {}) } as WallAdjust;
|
|
const next: WallAdjust = { ...current, h: h === null ? null : round1(h), m };
|
|
const bucket = { ...(design.revet_adjust ?? {}) };
|
|
if (isDefault(next)) delete bucket[role];
|
|
else bucket[role] = next;
|
|
design.revet_adjust = bucket;
|
|
}
|
|
|
|
/** 관 옵션의 조정값 몫을 상세에 얹음(폼 칸 전부 = 되보내기와 같은 규칙) — 같은 값이면 그대로. */
|
|
export function layPipeOptions(
|
|
detail: SectionDetailResponse,
|
|
pipes: ReadonlyArray<OptionPipe>,
|
|
): void {
|
|
const sections = detail.cross_sections;
|
|
for (const pipe of pipes) {
|
|
const options = pipe.options;
|
|
if (!options) continue;
|
|
const near = (at: number): boolean => Math.abs(at - pipe.chainage_m) < PIPE_MATCH_M;
|
|
// 물넘이포장 = 제원만(66-2) · 벽 없음
|
|
if (sections.some((s) => s.ford_pavement && near(s.ford_pavement.chainage_m ?? s.chainage_m)))
|
|
continue;
|
|
const owner = sections.find((s) => near(s.chainage_m));
|
|
const culvert = owner?.culvert;
|
|
const design = owner?.design;
|
|
if (!owner || !culvert || !design) continue;
|
|
const num = (key: string): number | undefined => {
|
|
const value = Number(options[key]);
|
|
return Number.isFinite(value) ? value : undefined;
|
|
};
|
|
for (const role of ["inlet", "outlet"] as const) {
|
|
const form = options[`${role}_revet_form`];
|
|
setWall(
|
|
design,
|
|
role,
|
|
num(`${role}_revet_height_m`) ?? null,
|
|
typeof form === "string" ? form : null,
|
|
);
|
|
}
|
|
if (culvert.hidden_pipe && !owner.ford && !owner.box) continue;
|
|
// 흙막이 n 단 = 지형이 세운 유출 다단(`pipeExtraCount` 와 같은 셈)
|
|
const outlet = design.extra_wall_counts?.outlet ?? 0;
|
|
const probe = {
|
|
...owner,
|
|
design: { ...design, extra_wall_counts: { basin: 0, ...design.extra_wall_counts, outlet } },
|
|
};
|
|
const tiers = outlet
|
|
? (computeStoredLayouts(probe, sections)?.culvert?.extraWalls.length ?? outlet)
|
|
: 0;
|
|
for (let n = 1; n <= tiers; n++) {
|
|
const role = `extra${n - 1}`;
|
|
const value = (item: string): number | undefined => {
|
|
const raw = options[`soil_add_${n}_${item}`];
|
|
return raw === undefined || raw === "" || !Number.isFinite(Number(raw))
|
|
? undefined
|
|
: Number(raw);
|
|
};
|
|
const form = options[`soil_add_${n}_form`];
|
|
const height = value("height");
|
|
setWall(
|
|
design,
|
|
role,
|
|
height === undefined ? null : Number(Math.max(height, WALL_MIN_PURE_HEIGHT_M).toFixed(1)),
|
|
typeof form === "string" && form ? form : null,
|
|
);
|
|
const span = { lengthM: value("length"), beforeM: value("before"), afterM: value("after") };
|
|
if (Object.values(span).every((v) => v === undefined)) continue;
|
|
const at = tierSpanOf(owner, role);
|
|
const current = {
|
|
lengthM: Math.round((at.beforeM + at.afterM) * 10) / 10,
|
|
beforeM: Math.round(at.beforeM * 10) / 10,
|
|
afterM: Math.round(at.afterM * 10) / 10,
|
|
};
|
|
const next = applySpanPatch(current, span);
|
|
design.extra_spans = {
|
|
...(design.extra_spans ?? {}),
|
|
[role]: { length_m: next.lengthM, before_m: next.beforeM, after_m: next.afterM },
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
/** 노선 바뀐 [저장] 넘김 입력(브라우저 `routeCarryInputs` + 서버 저장분 · 472-6 6-B). */
|
|
export interface RouteCarryInput {
|
|
/** 새 노선 서버 상세 — 관 · 구조물 · 추가 측점 · 계획고 차례 뒤(옛 길 `loadSectionDetail` 과 같은 때). */
|
|
detail: SectionDetailResponse;
|
|
/** 옛 노선 세션 칸 원본 — 측점 열쇠 칸 다섯 · 누가거리 칸 · `kindreset`(상단측 뺌 · 요구 274). */
|
|
states: Record<string, unknown>;
|
|
/** 옛 노선 측점 열쇠 목록(브라우저가 세운 것 · 없으면 null). */
|
|
from_keys: Keyed[] | null;
|
|
/** 측점을 덮는 관 · 구조물(측점 계획 · 구조물 목록). */
|
|
owners: Owner[];
|
|
/** 관 세션 장부(`pipe-ids`). */
|
|
pipe_ids: { next: number; ids: Array<{ id: string; at: number }> } | null;
|
|
/** 노선 저장 직전 저장 관(id · 누가거리). */
|
|
pipes_before: Array<{ id?: string | null; chainage_m: number }>;
|
|
/** 관 차례 뒤 저장 관(id · 누가거리 · 옵션). */
|
|
pipes_after: Array<OptionPipe & { id?: string | null }>;
|
|
}
|
|
|
|
/** 관 세션 장부 사본 — 찾기 · 없으면 새 차례(브라우저 `pipeIdNear` · `pipeIdAt` · `pipeChainageOf` 와 같은 규칙). */
|
|
function pipeBook(book: RouteCarryInput["pipe_ids"]) {
|
|
const ids = [...(book?.ids ?? [])];
|
|
let next = book?.next ?? 1;
|
|
const near = (at: number): string | null => {
|
|
let best: { id: string; at: number } | undefined;
|
|
let gap = PIPE_MATCH_M;
|
|
for (const entry of ids)
|
|
if (Math.abs(entry.at - at) < gap) [best, gap] = [entry, Math.abs(entry.at - at)];
|
|
return best?.id ?? null;
|
|
};
|
|
return {
|
|
near,
|
|
at: (at: number): string => {
|
|
const hit = near(at);
|
|
if (hit) return hit;
|
|
const id = `pipe-s${next++}`;
|
|
ids.push({ id, at });
|
|
return id;
|
|
},
|
|
chainageOf: (id: string): number | null => {
|
|
if (!id.startsWith("pipe-")) return null;
|
|
const hit = ids.find((entry) => entry.id === id);
|
|
if (hit) return hit.at;
|
|
const legacy = Number(id.slice("pipe-".length));
|
|
return Number.isFinite(legacy) ? legacy : null;
|
|
},
|
|
};
|
|
}
|
|
|
|
/** 누가거리(0.01 m 글) 열쇠 맵 → 누가거리(숫자) 맵(값이 있는 것만). */
|
|
function byChainage<V>(cells: ReadonlyMap<string, V>): Map<number, V> {
|
|
const out = new Map<number, V>();
|
|
for (const [key, value] of cells) {
|
|
const chainage = Number(key);
|
|
if (Number.isFinite(chainage) && value) out.set(chainage, value);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/** 카드 선택 한 벌(세션 `crossdesign` · `CrossDesignChoice` 꼴). */
|
|
interface CardChoice {
|
|
ground_type?: string;
|
|
section_mode?: string;
|
|
ditch_side?: string | null;
|
|
ditch_type?: string | null;
|
|
paved?: boolean;
|
|
two_stage_slope?: boolean;
|
|
ditch_choice?: boolean | null;
|
|
}
|
|
|
|
type Sides = Partial<Record<"left" | "right", number>>;
|
|
|
|
/**
|
|
* 노선 바뀐 [저장] 횡단 patch — 옛 브라우저 길(`carryAfterRouteSave` → `saveCachedCrossPatches`)과 같은 차례:
|
|
* 새 측점 열쇠 → 관 옵션 얹기 → 측점 열쇠 칸 · 누가거리 칸 넘김 → 조정 · 옮긴 고침 얹기 → 캐시 사용자 칸 바탕 +
|
|
* 카드 선택 · 암선 · 암 경사. `outside` = 노선 밖으로 버린 고침 수(측점 단위). `detail` 을 제자리에서 고침.
|
|
*/
|
|
export function routeCarryPatches(input: RouteCarryInput): {
|
|
patches: CrossSectionPatch[];
|
|
outside: number;
|
|
} {
|
|
const { detail } = input;
|
|
const sections = detail.cross_sections;
|
|
const book = pipeBook(input.pipe_ids);
|
|
const policy = (
|
|
detail.longitudinal?.profile_alignment as
|
|
{ policy?: { station_interval_m?: number } } | undefined
|
|
)?.policy;
|
|
const interval = policy?.station_interval_m ?? inferStationInterval(sections);
|
|
const from = input.from_keys ?? undefined;
|
|
const to = stationKeysFrom(sections, interval, input.owners, book);
|
|
layPipeOptions(detail, input.pipes_after);
|
|
const states = input.states;
|
|
const keyed = carryKeyedStates((name) => (states[name] as Raw | undefined) ?? {}, from, to);
|
|
const newAt = (id: string | null | undefined): number | null =>
|
|
input.pipes_after.find((pipe) => id != null && pipe.id === id)?.chainage_m ?? null;
|
|
const chain = carryChainageStates(
|
|
(name) => (states[name] as Record<string, unknown> | undefined) ?? null,
|
|
from,
|
|
to,
|
|
{
|
|
keyAt: (key) => {
|
|
const id = key.startsWith("pipe:") ? key.slice(5) : null;
|
|
if (!id) return null;
|
|
return id.startsWith("pipe-") ? book.chainageOf(id) : newAt(id);
|
|
},
|
|
movedAt: (chainageM) =>
|
|
newAt(input.pipes_before.find((pipe) => Math.abs(pipe.chainage_m - chainageM) < 0.01)?.id),
|
|
},
|
|
);
|
|
const cells = (name: string): Cells => chain.states[name] ?? {};
|
|
const read = <V>(name: KeyedState) => readKeyedIn<V>(keyed.states[name], to);
|
|
layDraftAdjusts(detail, cells);
|
|
layCarriedEdits(detail, cells, {
|
|
shoulder: byChainage(read<Sides>("shoulderext")),
|
|
berm: byChainage(read<Sides>("bermdepth")),
|
|
});
|
|
const kindChanged = Object.keys(chain.states.kindreset ?? {}).map(Number);
|
|
const savedAt = sections.map((section) => section.chainage_m);
|
|
const base = crossPatchesFromCache(detail, kindChanged, savedAt);
|
|
// 세션에만 있는 고침(카드 선택 · 암선 · 암 경사) — 옛 `sessionOverlay` 와 같은 칸(445 H5)
|
|
const overlay: CrossSectionPatch[] = [];
|
|
byChainage(read<CardChoice>("crossdesign")).forEach((choice, chainage) => {
|
|
const patch: Record<string, unknown> = {
|
|
chainage_m: chainage,
|
|
ground_type: choice.ground_type,
|
|
section_mode: choice.section_mode,
|
|
};
|
|
if (choice.ditch_side !== undefined) patch.ditch_side = choice.ditch_side;
|
|
if (choice.ditch_type !== undefined) patch.ditch_type = choice.ditch_type;
|
|
if (choice.paved !== undefined) patch.paved = choice.paved;
|
|
if (choice.two_stage_slope !== undefined) patch.two_stage_slope = choice.two_stage_slope;
|
|
if (typeof choice.ditch_choice === "boolean") patch.ditch_choice = choice.ditch_choice;
|
|
overlay.push(patch as unknown as CrossSectionPatch);
|
|
});
|
|
for (const [chainage, offset] of read<unknown>("rockb"))
|
|
if (typeof offset === "number")
|
|
overlay.push({ chainage_m: Number(chainage), rock_boundary_offset_m: offset });
|
|
for (const [chainage, ratio] of read<number>("cutslope"))
|
|
overlay.push({ chainage_m: Number(chainage), cut_slope_ratio_user: ratio });
|
|
const patches = mergeCrossPatches(
|
|
base,
|
|
overlay.filter((patch) => Number.isFinite(patch.chainage_m)),
|
|
);
|
|
return { patches, outside: keyed.outside.size + chain.outside.size };
|
|
}
|