Files
Aislo/A00_Common/design/design_structure_pipe_calc.ts
T
eomsangdonandClaude Opus 5.5 cb8836c6a3 wip(B05·B06): 54-27 · 54-28 유출 「추가 흙막이(단)」 칸 · 자동 자리 글 삭제 — 중지(사용자) · 남은 일 횡단 흙막이 옮김 → 좌측 관 길이 화면 확인
- 관 보호공(유출) 상자 안 「추가 흙막이(단)」 칸(- 숫자 + · 0~9 · 지형이 세운 만큼) — 횡단 조정 단 수와 같은 자리
- 종단 보기에서 바꾸면 횡단 보기를 숨긴 채 세워 같은 단 수 제어로 씀
- 늘어난 흙막이 상자 구조 칸 「흙막이 (기본)」
- 배관 유출 칸은 옛 단 수 줄을 안 받음(중복 방지)
- 관 길이 · 좌우 · 상하(사면) 자동 자리 읽기 글과 문구 키 삭제
- 벽 조작값이 바뀌면 좌측 관 길이 다시 셈(120ms 모음)
- 시험 test_54_27_soil_tier_field(단 칸 · N → 상자 N · 램프 자리 · 자동 자리 글 없음)

화면 확인(B05 12+18): 단 0→1→2→1 에 상자 0→1→2→1 · 램프 3→4→5→4 · 자동 자리 글 0 · B06 카드 다단 벽 2
남은 일: 횡단 흙막이 옮김 → 좌측 관 길이 바뀜 화면 확인

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVPp2o6Mh2FZJr72yW3ofZ
2026-10-06 13:00:32 +09:00

125 lines
6.6 KiB
TypeScript

/* =============================================================================
* design_structure_pipe_calc.ts
* 배관 명세 계산 — 관 길이(자동) · 보호공 높이 안내. 계산 자리 = ② TS 를 서버 Node 로:
* 관 길이는 횡단 기하 `computeStoredLayouts`(B06 · 서버 Node 진입점이 같은 함수로 `pipe_length_m` 를
* 셈 · 시험 test_b06_pipe_length) 그대로 — 여기서 새로 셈하지 않음 · 화면에 보이기만(저장 안 함).
* 높이 = 손 입력 · 횡단도 조정 높이는 B06 `withSpecDefaults` 가 같은 키(`*_revet_height_m`)에 실음 ·
* 빈 칸 안내 = 관경 기준 최소 높이(`pipeWallMinHeightM` ↔ 파이썬 `pipe_wall_min_height_m` 짝).
* 흙막이 개수 = 주인 측점 유출 「추가 기슭막이(단)」(횡단 설계 patch `extra_wall_counts` · 54-1) 중 지형이 세운 단(54-10) ·
* 쓰기는 횡단 보기 단 수 제어로만(`setPipeExtraCount` · 54-27).
* ========================================================================== */
import { pipeWallMinHeightM } from "../../common_util/common_util_culvert_sets";
import type { CrossSection } from "../../B06_Section/B06_Section_Api_Fetch";
import { computeStoredLayouts } from "../../B06_Section/B06_Section_Structure_Layouts";
import { PIPE_MATCH_M } from "./design_selection";
/** 횡단 측점 목록을 대는 곳 — 횡단 보기가 자료를 받으면 걸어 둠(종단만 연 동안은 없음). */
let sectionsSource: (() => readonly CrossSection[] | null) | null = null;
export function setPipeSectionsSource(source: () => readonly CrossSection[] | null): void {
sectionsSource = source;
}
/** 유출 단 수를 대는 곳 — 횡단 보기의 세션 값 우선(`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),
};
}
/** 흙막이 높이 빈 칸 안내 — 관경 기준 최소 높이(엔진이 빈 칸을 이 값으로 그림). */
export function guardHeightHint(diameterMm: string): string {
const d = Number(diameterMm || "1000") / 1000;
return `비우면 관경 기준 ${pipeWallMinHeightM(d)}`;
}