/* ============================================================================= * design_structure_pipe_calc.ts * 배관 명세 계산 — 관 길이(자동) · 보호공 높이 안내. 계산 자리 = ② TS 를 서버 Node 로: * 관 길이는 횡단 기하 `computeStoredLayouts`(B06 · 서버 Node 진입점이 같은 함수로 `pipe_length_m` 를 * 셈 · 시험 test_b06_pipe_length) 그대로 — 여기서 새로 셈하지 않음 · 화면에 보이기만(저장 안 함). * 높이 = 손 입력 · 횡단도 조정 높이는 B06 `withSpecDefaults` 가 같은 키(`*_revet_height_m`)에 실음 · * 빈 칸 안내 = 정의 기본값 · 집수정 모양 기본 = 횡단 기하 `DEFAULT_BASIN_SHAPE`(54-36). * 흙막이 개수 = 주인 측점 유출 「추가 기슭막이(단)」(횡단 설계 patch `extra_wall_counts` · 54-1) 중 지형이 세운 단(54-10) · * 쓰기는 횡단 보기 단 수 제어로만(`setPipeExtraCount` · 54-27). * ========================================================================== */ import type { CrossSection } from "../../B06_Section/B06_Section_Api_Fetch"; import { computeStoredLayouts } from "../../B06_Section/B06_Section_Structure_Layouts"; export { DEFAULT_BASIN_SHAPE } from "../../B06_Section/B06_Section_UI_Cross_Culvert_Basin"; import { PIPE_MATCH_M } from "./design_selection"; /** 횡단 측점 목록을 대는 곳 — 횡단 보기가 자료를 받으면 걸어 둠(종단만 연 동안은 없음). */ let sectionsSource: (() => readonly CrossSection[] | null) | null = null; export function setPipeSectionsSource(source: () => readonly CrossSection[] | null): void { sectionsSource = source; } /** 횡단 측점 목록 읽기 — 노면공 확폭 구간 상자(87-3)가 확폭을 읽음 · 자료 없으면 null. */ export const pipeSections = (): readonly CrossSection[] | null => sectionsSource?.() ?? null; /** 유출 단 수를 대는 곳 — 횡단 보기의 세션 값 우선(`countFor`) · 없으면 정본 patch. */ let extraCountSource: ((section: CrossSection) => number) | null = null; export function setPipeExtraCountSource(source: (section: CrossSection) => number): void { extraCountSource = source; } /** 유출 단 수를 쓰는 곳 — 횡단 보기의 단 수 제어(54-27) · 종단 보기는 횡단 보기를 숨긴 채 세워 씀. */ let extraCountSink: ((section: CrossSection, count: number) => void) | null = null; let crossOpener: (() => Promise) | null = null; export function setPipeExtraCountSink(sink: (section: CrossSection, count: number) => void): void { extraCountSink = sink; } export function setPipeCrossOpener(opener: () => Promise): void { crossOpener = opener; } /** 좌측 「추가 흙막이(단)」 → 주인 측점 유출 단 수(B06 횡단 조정과 같은 저장 자리 · 54-27). */ export async function setPipeExtraCount(chainageM: number | null, count: number): Promise { if (!extraCountSink) await crossOpener?.(); const found = ownerSection(chainageM); if (found) extraCountSink?.(found.owner, count); } /** 유입 형식을 대는 곳 — 횡단 보기의 세션 값 우선(`valueFor`) · 없으면 정본 patch(54-13). */ type InletChoice = NonNullable["inlet_structure"]; let inletSource: ((section: CrossSection) => InletChoice) | null = null; export function setPipeInletSource(source: (section: CrossSection) => InletChoice): void { inletSource = source; } /** 그 관의 주인 측점(관 자리에서 가장 가까운 측점) — 횡단 자료가 없으면 null. */ function ownerSection( chainageM: number | null, ): { owner: CrossSection; sections: readonly CrossSection[] } | null { const sections = sectionsSource?.() ?? null; if (chainageM === null || !sections?.length) return null; const holders = sections.filter( (s) => Math.abs((s.culvert?.chainage_m ?? Infinity) - chainageM) < PIPE_MATCH_M, ); if (!holders.length) return null; const owner = holders.reduce((a, b) => Math.abs(b.chainage_m - chainageM) < Math.abs(a.chainage_m - chainageM) ? b : a, ); return { owner, sections }; } /** 관 길이(m) — 주인 측점 횡단 기하 값 · 못 세우면 저장된 `pipe_length_m` · 그것도 없으면 null. */ export function pipeLengthM(chainageM: number | null): number | null { const found = ownerSection(chainageM); if (!found) return null; const { owner, sections } = found; const drawn = computeStoredLayouts(owner, sections)?.culvert?.pipe?.lengthM; const length = typeof drawn === "number" && drawn > 0 ? drawn : owner.design?.pipe_length_m; return typeof length === "number" && length > 0 ? length : null; } /** 흙막이 개수 = 주인 측점 유출 「추가 기슭막이(단)」 중 지형이 세운 단(54-10) — 횡단 자료가 없으면 * null(모름). 저장 단 수가 더 커도 넘친 단은 컨테이너를 안 세움(값은 파일에 그대로). */ export function pipeExtraCount(chainageM: number | null): number | null { const found = ownerSection(chainageM); if (!found) return null; const { owner, sections } = found; const outlet = extraCountSource?.(owner) ?? owner.design?.extra_wall_counts?.outlet ?? 0; if (!outlet || !owner.design) return outlet; const counts = { basin: 0, ...owner.design.extra_wall_counts, outlet }; const probe = { ...owner, design: { ...owner.design, extra_wall_counts: counts } }; return computeStoredLayouts(probe, sections)?.culvert?.extraWalls.length ?? outlet; } /** 그림이 쓰는 유입 · 유출 값(54-13) — 집수정 모양(없으면 null = 흙막이 벽) · 벽 형태(유입 · 유출 · * 흙막이 n). 폼 빈 · 자동 칸에 보여 주기만. 횡단 자료가 없으면 null(모름). */ export function pipeDrawn(chainageM: number | null): { basin: string | null; forms: Record; extras: Array; } | null { const found = ownerSection(chainageM); const design = found?.owner.design; if (!found || !design) return null; const { owner, sections } = found; const outlet = extraCountSource?.(owner) ?? design.extra_wall_counts?.outlet ?? 0; const counts = { basin: 0, ...design.extra_wall_counts, outlet }; const inlet = inletSource?.(owner) ?? design.inlet_structure; const probe = { ...owner, design: { ...design, extra_wall_counts: counts, inlet_structure: inlet }, }; const layout = computeStoredLayouts(probe, sections)?.culvert; if (!layout) return null; const formOf = (role: string) => layout.walls.find((w) => w.role === role)?.form ?? null; return { basin: layout.basin?.shape ?? null, forms: { inlet: formOf("inlet"), outlet: formOf("outlet") }, extras: layout.extraWalls.map((w) => w.form), }; } /** 관 보호공 [연동] · [경사](54-30) — null = 그 쪽을 쓸 수 없음(벽이 서는 옆 측점 없음 · 구간 없음). */ export interface PipeRevetChoice { linked: boolean | null; grade: boolean | null; } /** 읽고 쓰는 곳 — 횡단 보기의 연동 제어(`revetLink` · 옛 저장 자리)가 걸어 둠. */ interface RevetChoiceBridge { read: (owner: CrossSection, sections: readonly CrossSection[]) => PipeRevetChoice; write: ( owner: CrossSection, sections: readonly CrossSection[], kind: "link" | "grade", on: boolean, ) => void; } let revetChoice: RevetChoiceBridge | null = null; export function setPipeRevetChoiceBridge(bridge: RevetChoiceBridge): void { revetChoice = bridge; } /** 그 관의 [연동] · [경사] 지금 값 — 횡단 자료가 없으면 null(모름). */ export function pipeRevetChoice(chainageM: number | null): PipeRevetChoice | null { const found = ownerSection(chainageM); return found && revetChoice ? revetChoice.read(found.owner, found.sections) : null; } /** 좌측 [연동] · [경사] → 횡단 연동 제어(종단 보기는 횡단 보기를 숨긴 채 세워 씀 · 54-27 과 같은 길). */ export async function setPipeRevetChoice( chainageM: number | null, kind: "link" | "grade", on: boolean, ): Promise { if (!revetChoice) await crossOpener?.(); const found = ownerSection(chainageM); if (found) revetChoice?.write(found.owner, found.sections, kind, on); }