Files
Aislo/common_util/common_util_berm_spans.ts
T
eomsangdonandClaude Opus 5.5 403722e687 refactor(B05·B06): 소단 = 옹벽처럼 측점에 제원 얹음 — 형상 얹기 한 벌 · 세션 사본 berm 걷음 (PLAN 577-2 · 요구 434 · 437)
- 얹기: attachShapes · attach_shapes 에 소단(section.berm · 덮는 소단 없음 = null) · SHAPE_KEYS 에 berm · TS attachBermSpecs ↔ 파이썬 attach_berm_specs · berm_spans_from 짝
- 읽기: 저장 노선 다시 셈(브라우저 · Node cross.refresh) = 얹힌 section.berm(칸 없음 = 저장분 design.berm) · 노선 초안 · 서버 Node 묶음 cross = crossWithShapes 의 얹힌 제원(bermAt · crossWithBerm 걷음)
- 계단은 계산 안쪽 그대로 · [저장] 길(구조물 정본 → cross.berm_spans) 그대로
- 지움: 시설 줄 syncBerm · putBerm(C4.copy) · writeBermSpans · 세션 칸 berm · bermRequest · design_shared bermAt
- 시험: test_272_berm_source 측점 얹기 거울로 · test_323_4 소단 얹음 · 477 · 511-A · 472 떠남 · 365 · 272 고름 헬퍼가 사본 대신 얹힌 소단 · design_pipeline_page 700 줄 → 699(주석 한 줄)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 11:33:06 +09:00

120 lines
5.0 KiB
TypeScript

/* =============================================================================
* common_util_berm_spans.ts
* 소단 구간 — 구조물 목록(표 형상 berm)에서 구간 · 측점마다 제원(PLAN 472-4 C4) · 측점에 얹기(577-2 ·
* 벽처럼 형상 얹기 한 벌 `attachShapes` 가 `section.berm` 을 세움 · 계단은 계산 안쪽 그대로).
* 브라우저(형상 얹기 · 다시 셈)와 서버 Node 입구(`design_calc_node` · 등록부 `cross.berm_spans` · `facility.attach`)가 같은 셈.
* 파이썬 짝 = `B06_Section_Engine_Structures_Wall.py` `berm_spans_from` · `load_berm_spans` · `berm_span_at` ·
* `attach_berm_specs`(Node 를 못 부를 때만 · 거울 시험 `test_272_berm_source.py` · `test_323_4_attach_mirror.py`).
* ========================================================================== */
import {
BERM_DEFAULT_DEPTH_M,
BERM_DEFAULT_INTERVAL_M,
BERM_DEFAULT_SLOPE_DEG,
BERM_DEFAULT_WIDTH_M,
} from "./common_util_cross_berm";
import { catalogRow } from "./common_util_structure_catalog";
/** 측점 하나의 소단 제원(`section.berm`) — 서버 payload 와 같은 이름을 쓴다(그대로 실어 보낸다). */
export interface BermSessionSpec {
width_m: number;
interval_m: number;
slope_deg: number;
/** 노면 끝 아래 깊이(m · ≥ 0 · 178-2). */
depth_m: number;
}
/**
* 사용자가 놓은 소단 **한 구간** — 종단 범위 + 제원.
*
* 사용자는 측점 하나가 아니라 **구간**에 놓는다(2026-09-07 확정: 「길이 + 기준측점 전·후」).
* 그래서 세션에는 구간 목록으로 두고, 측점별 제원은 읽는 자리에서 편다 — 구간을 측점으로
* 펴서 저장하면 나중에 「어디부터 어디까지 놓았나」를 되짚을 수 없다.
*/
export interface BermSpan extends BermSessionSpec {
start_m: number;
end_m: number;
}
/** 소단 = 표 형상 berm(323-4 · 파이썬 `load_berm_spans` 짝) — 종류 키로 안 가름. */
export const isBermType = (typeId: string): boolean => catalogRow(typeId)?.shape === "berm";
/**
* 구조물 목록에서 **소단 구간**을 뽑는다 — 사용자는 「구조물 배치」에서 놓는다
* (2026-09-07 사용자 확정: 별도 폼이 아니라 다른 옹벽·기슭막이와 같은 자리).
*/
export function bermSpansFromStructures(
structures: ReadonlyArray<{
type_id: string;
start_m?: number | null;
end_m?: number | null;
chainage_m?: number | null;
options?: Record<string, string | number>;
}>,
): BermSpan[] {
const spans: BermSpan[] = [];
for (const item of structures) {
if (!isBermType(item.type_id)) continue;
const anchor = item.chainage_m ?? item.start_m ?? null;
const start = item.start_m ?? anchor;
const end = item.end_m ?? anchor;
if (start === null || end === null) continue;
const options = item.options ?? {};
const number = (key: string, fallback: number): number => {
const parsed = Number(options[key]);
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
};
const width = number("width_m", BERM_DEFAULT_WIDTH_M);
const interval = number("interval_m", BERM_DEFAULT_INTERVAL_M);
if (width <= 0) continue; // 폭이 0이면 계단이 없다(간격은 성토 한 단에 안 씀 · 178-2)
// 깊이 = 노면 끝 아래 · 음수는 0(도로보다 높을 수 없음) · 빈 값은 기본 1.0
const rawDepth = Number(options.depth_m);
const depth =
options.depth_m === undefined || options.depth_m === "" || !Number.isFinite(rawDepth)
? BERM_DEFAULT_DEPTH_M
: Math.max(rawDepth, 0);
spans.push({
start_m: Math.min(start, end),
end_m: Math.max(start, end),
width_m: width,
interval_m: interval,
slope_deg: number("slope_deg", BERM_DEFAULT_SLOPE_DEG),
depth_m: depth,
});
}
return spans;
}
/** 그 측점을 덮는 소단 제원 — 없으면 null. 겹치면 먼저 놓은 것이 이긴다. */
export function bermSpecAt(spans: BermSpan[], chainageM: number): BermSessionSpec | null {
const found = spans.find(
(span) =>
chainageM >= Math.min(span.start_m, span.end_m) - 1e-6 &&
chainageM <= Math.max(span.start_m, span.end_m) + 1e-6,
);
return found
? {
width_m: found.width_m,
interval_m: found.interval_m,
slope_deg: found.slope_deg,
depth_m: found.depth_m ?? BERM_DEFAULT_DEPTH_M,
}
: null;
}
/** 측점마다 소단 제원을 얹음(파이썬 `attach_berm_specs` 짝 · 제자리) — 덮는 소단 없음 = null(얹었음 표시 ·
* 칸 없음 = 아직 안 얹음 → 다시 셈이 저장분 `design.berm` 으로 물러섬). 얹은 측점 수. */
export function attachBermSpecs(
sections: Array<Record<string, unknown>>,
spans: BermSpan[],
): number {
let attached = 0;
for (const section of sections) {
const chainage = section.chainage_m;
if (typeof chainage !== "number" || !Number.isFinite(chainage)) continue;
section.berm = bermSpecAt(spans, chainage);
if (section.berm) attached += 1;
}
return attached;
}