/* ============================================================================= * B06_Section_UI_Page_Span_Control.ts * 구조물 **구간값(길이·전/후)** 제어 — 다단 단별 구간값과 유입·유출·집수정 구간값. * * `B06_Section_UI_Page_Station_Controls` 에서 떼어냈다(700줄 제한, 2026-09-02). * 세션 맵(`extraSpans`)과 캐시(design)·정본 큐(`culvertOptions`)를 함께 만지는 * 자리라 한 덩어리로 옮겼다. 판정 규칙은 종전대로 `_Cross_Culvert_Const` 몫이다. * ========================================================================== */ import type { CrossDesign, CrossSection, StoredWallSpan } from "./B06_Section_Api_Fetch"; import { tierSpanOf, type SpanValues, type StructureSpanControl, } from "./B06_Section_UI_Cross_Culvert_Wire"; import * as CulvertConst from "./B06_Section_UI_Cross_Culvert_Const"; /** 구간값 제어가 페이지·상위 제어기에서 받아 쓰는 창구. */ export interface SpanControlDeps { detail: () => { cross_sections: CrossSection[] } | null; refreshCard: (chainageM: number) => void; /** 연동 구조물의 소유 측점 — 값은 소유 측점 하나에만 담는다. */ ownerOf: (section: CrossSection) => CrossSection | null; /** 세션 맵(누가거리:벽키 → 구간값)과 저장. */ extraSpans: Map; persistExtraSpans: () => void; patchCachedDesign: (chainageM: number, patch: Partial) => void; /** 정본 반영 큐(관 옵션). */ culvertOptions: { queue: (chainageM: number, values: Record) => void }; } /** 구간값 제어기와 단별 구간값 조회를 만든다. */ export function createSpanControl(deps: SpanControlDeps): { control: StructureSpanControl; /** 이 측점의 단별 구간값 전부(세션 우선) — 상위 제어기가 payload에 실을 때 쓴다. */ tierSpansOf: (owner: CrossSection) => Record; spanKeyOf: (chainageM: number, wall: string) => string; } { const { extraSpans, persistExtraSpans, patchCachedDesign, ownerOf, culvertOptions } = deps; const spanKeyOf = (chainageM: number, wall: string): string => `${chainageM.toFixed(2)}:${wall}`; const storedTierSpan = (owner: CrossSection, wall: string): SpanValues => { const span = tierSpanOf(owner, wall); return { lengthM: Math.round((span.beforeM + span.afterM) * 10) / 10, beforeM: Math.round(span.beforeM * 10) / 10, afterM: Math.round(span.afterM * 10) / 10, }; }; /** 이 측점의 단별 구간값 전부(세션 우선) — 캐시·payload에 실을 모양으로. */ const tierSpansOf = (owner: CrossSection): Record => { const prefix = `${owner.chainage_m.toFixed(2)}:`; const result: Record = { ...(owner.design?.extra_spans ?? {}) }; extraSpans.forEach((value, key) => { if (!key.startsWith(prefix)) return; result[key.slice(prefix.length)] = { length_m: value.lengthM, before_m: value.beforeM, after_m: value.afterM, }; }); return result; }; const control: StructureSpanControl = { ownerOf, tierValuesFor: (section, key) => { const owner = ownerOf(section) ?? section; return extraSpans.get(spanKeyOf(owner.chainage_m, key)) ?? storedTierSpan(owner, key); }, updateTier: (section, key, patch) => { const owner = ownerOf(section); if (!owner) return; const mapKey = spanKeyOf(owner.chainage_m, key); const current = extraSpans.get(mapKey) ?? storedTierSpan(owner, key); const next = CulvertConst.applySpanPatch(current, patch); extraSpans.set(mapKey, next); persistExtraSpans(); // 캐시(design)에도 얹는다 — 링크 판정·3D가 순수 함수로 이 값을 읽는다. patchCachedDesign(owner.chainage_m, { extra_spans: tierSpansOf(owner) }); // 연장이 바뀌면 링크되는 옆 측점이 달라진다 — 옛 연장·새 연장을 합친 구간만. const reach = Math.max(current.beforeM, current.afterM, next.beforeM, next.afterM); for (const other of deps.detail()?.cross_sections ?? []) { if (Math.abs(other.chainage_m - owner.chainage_m) <= reach + 1e-9) { deps.refreshCard(other.chainage_m); } } }, valuesFor: (section, role) => { const owner = ownerOf(section); return owner ? CulvertConst.spanValuesOf(owner, role) : null; }, update: (section, role, patch) => { const owner = ownerOf(section); if (!owner?.culvert) return; const current = CulvertConst.spanValuesOf(owner, role); if (!current) return; // 길이↔전/후 산식은 다단과 공용(`applySpanPatch`). const { lengthM, beforeM, afterM } = CulvertConst.applySpanPatch(current, patch); const spec = role === "outlet" ? owner.culvert.outlet : owner.culvert.inlet; const keys = CulvertConst.SPAN_OPTION_KEYS[role]; if (role === "basin") { spec.basin_length_m = lengthM; spec.basin_before_m = beforeM; spec.basin_after_m = afterM; } else { spec.revet_length_m = lengthM; spec.revet_before_m = beforeM; spec.revet_after_m = afterM; } culvertOptions.queue(owner.chainage_m, { [keys.length]: lengthM, [keys.before]: beforeM, [keys.after]: afterM, }); // 연장이 바뀌면 링크되는 옆 측점 목록이 달라진다. 다시 그릴 대상은 **옛 연장과 // 새 연장을 합친 구간**뿐이다 — 측점 23개를 통째로 다시 그리면 조정창이 매번 // 새로 만들어져 연타한 +/-가 중간에 삼켜진다(2026-08-24 화면 실측: 3번 눌러 // 2번만 반영). const reach = Math.max(current.beforeM, current.afterM, beforeM, afterM); for (const other of deps.detail()?.cross_sections ?? []) { if (Math.abs(other.chainage_m - owner.chainage_m) <= reach + 1e-9) { deps.refreshCard(other.chainage_m); } } }, }; return { control, tierSpansOf, spanKeyOf }; }