- 상세 내용 아래 「확폭 구간」 상자: 범위 안 확폭 > 0 측점을 이어진 구간으로 묶어 구간마다 켬/끔 단추(기본 켬) · 끈 구간 = options.widening_off "시작-끝,…"(측점 m) · 안 맞는 값 무시 · 횡단 자료 없으면 안내 - 횡단 측점 읽기 손잡이 pipeSections(design_structure_pipe_calc) · 구간 규칙 design_structure_widening(순수) - 물넘이 겹침 안내 = 시작 · 종료 측점을 고치라는 문구 - 시험: test_87_widening_runs(새) · ui_checks 52-10 · 52-15 시작 · 종료 측점 꼴로 맞춤 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BVPp2o6Mh2FZJr72yW3ofZ
159 lines
8.0 KiB
TypeScript
159 lines
8.0 KiB
TypeScript
/* =============================================================================
|
|
* 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<void>) | null = null;
|
|
export function setPipeExtraCountSink(sink: (section: CrossSection, count: number) => void): void {
|
|
extraCountSink = sink;
|
|
}
|
|
export function setPipeCrossOpener(opener: () => Promise<void>): void {
|
|
crossOpener = opener;
|
|
}
|
|
|
|
/** 좌측 「추가 흙막이(단)」 → 주인 측점 유출 단 수(B06 횡단 조정과 같은 저장 자리 · 54-27). */
|
|
export async function setPipeExtraCount(chainageM: number | null, count: number): Promise<void> {
|
|
if (!extraCountSink) await crossOpener?.();
|
|
const found = ownerSection(chainageM);
|
|
if (found) extraCountSink?.(found.owner, count);
|
|
}
|
|
|
|
/** 유입 형식을 대는 곳 — 횡단 보기의 세션 값 우선(`valueFor`) · 없으면 정본 patch(54-13). */
|
|
type InletChoice = NonNullable<CrossSection["design"]>["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<string, string | null>;
|
|
extras: Array<string | null>;
|
|
} | 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<void> {
|
|
if (!revetChoice) await crossOpener?.();
|
|
const found = ownerSection(chainageM);
|
|
if (found) revetChoice?.write(found.owner, found.sections, kind, on);
|
|
}
|