Files
Aislo/A00_Common/design/design_structure_widening.ts
T
eomsangdonandClaude Sonnet 5.5 33119bcc67 feat(B05): 노면공 확폭 구간 상자 · 물넘이 겹침 안내 문구 · 화면 시험 맞춤(87-3 · 87-4)
- 상세 내용 아래 「확폭 구간」 상자: 범위 안 확폭 > 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
2026-10-08 12:08:15 +09:00

115 lines
4.4 KiB
TypeScript

/* =============================================================================
* design_structure_widening.ts
* 노면공 › 확폭 구간(87-3) — 범위 안에서 확폭(좌 + 우) > 0 인 횡단 측점을 이어진 구간으로 묶고,
* 끈 구간을 `options.widening_off = "시작-끝,시작-끝"`(측점 m · 빈 글 = 모두 켬)으로 읽고 씀.
* 켬 = 확폭 포함 · 끔 = 규격 폭만. 노선이 바뀌어 구간 끝이 안 맞는 끈 값은 무시(= 켬).
* 횡단 형상 쪽(88장)도 이 키를 같은 글로 읽음 — 측점이 범위 ±0.02 m 안.
* import 없음 — 시험이 이 파일만 옮겨 돌림.
* ========================================================================== */
/** 구조물 범위 · 구간 끝 맞춤 허용치(m). */
export const WIDENING_TOL_M = 0.02;
/** 횡단 측점에서 읽는 몫만 — 횡단 설계 확폭(좌 · 우) · 설계 없으면 저장된 곡선 확폭(바깥쪽 한 방향). */
export interface WideningStation {
chainage_m: number;
curve_widening_m?: number | null;
curve_outer_side?: "left" | "right" | null;
design?: { widening_left_m?: number; widening_right_m?: number } | null;
}
export interface WideningRun {
start_m: number;
end_m: number;
/** 구간 안 확폭(좌 + 우) 최댓값(m). */
widening_m: number;
}
/** 한 측점의 확폭(좌 + 우, m) — 없으면 0. */
export function wideningOf(station: WideningStation): number {
const sum = station.design
? (station.design.widening_left_m ?? 0) + (station.design.widening_right_m ?? 0)
: station.curve_outer_side
? (station.curve_widening_m ?? 0)
: 0;
return Number.isFinite(sum) && sum > 0 ? sum : 0;
}
/** 범위 `[start, end]` 안 확폭 > 0 측점을 이어진 구간(범위 안 측점 목록에서 이웃)으로 묶음. */
export function wideningRuns(
stations: readonly WideningStation[],
start: number,
end: number,
): WideningRun[] {
const inside = stations
.filter((s) => s.chainage_m >= start - WIDENING_TOL_M && s.chainage_m <= end + WIDENING_TOL_M)
.sort((a, b) => a.chainage_m - b.chainage_m);
const runs: WideningRun[] = [];
let open: WideningRun | null = null;
for (const station of inside) {
const width = wideningOf(station);
if (width <= 0) {
open = null;
continue;
}
if (open) {
open.end_m = station.chainage_m;
open.widening_m = Math.max(open.widening_m, width);
} else {
open = { start_m: station.chainage_m, end_m: station.chainage_m, widening_m: width };
runs.push(open);
}
}
return runs;
}
const round = (value: number): number => Number(value.toFixed(2));
/** 저장 글 → 끈 구간 쌍. 틀린 조각은 버림. */
export function parseOff(text: unknown): Array<[number, number]> {
if (typeof text !== "string") return [];
const pairs: Array<[number, number]> = [];
for (const part of text.split(",")) {
const [a, b] = part.split("-").map((token) => Number(token.trim()));
if (part.includes("-") && Number.isFinite(a) && Number.isFinite(b) && b >= a)
pairs.push([a, b]);
}
return pairs;
}
export const formatOff = (pairs: ReadonlyArray<readonly [number, number]>): string =>
pairs.map(([a, b]) => `${round(a)}-${round(b)}`).join(",");
/** 끈 쌍이 이 구간과 맞는가(시작 · 끝 각각 허용치 안). */
export const pairMatches = (pair: readonly [number, number], run: WideningRun): boolean =>
Math.abs(pair[0] - run.start_m) <= WIDENING_TOL_M &&
Math.abs(pair[1] - run.end_m) <= WIDENING_TOL_M;
/** 지금 구간들 중 끈 것만 남긴 글(안 맞는 옛 값은 빠짐). */
export function offForRuns(runs: readonly WideningRun[], text: unknown): string {
const pairs = parseOff(text);
return formatOff(
runs
.filter((run) => pairs.some((p) => pairMatches(p, run)))
.map((run) => [run.start_m, run.end_m] as const),
);
}
/** 구간 하나의 켬/끔을 뒤집은 새 글(맞는 구간 쌍만 남김). */
export function toggleRun(
runs: readonly WideningRun[],
text: unknown,
target: WideningRun,
): string {
const pairs = parseOff(text);
const isOff = (run: WideningRun): boolean => pairs.some((p) => pairMatches(p, run));
return formatOff(
runs
.filter((run) => (run === target ? !isOff(run) : isOff(run)))
.map((run) => [run.start_m, run.end_m] as const),
);
}
export const isRunOff = (run: WideningRun, text: unknown): boolean =>
parseOff(text).some((p) => pairMatches(p, run));