- 옮김: B04 2 · B05 23 · B06 11 · A00_Common/design 13 (9,299 줄) · 이미 logic/ 에 있던 16 파일 그대로 - 안 옮김: 묶음 입구(package.json build:*) · 파이썬 · 순수지만 계산 아닌 8 (보기 결선 · 콜백 · API 타입 · 갈아끼움 알림판) - import 266 줄(142 파일) · 시험 · 주석 · 짝 표시 문자열 경로 고침 · B05 · B06 CLAUDE.md 짝 줄 logic/ - 시험 도우미 새 경로: 가짜 require 열쇠 · B05()/B06() 부름 · 형제 import 자리 · tsc 묶어 돌리는 시험 · 정규식 · rglob - design_view_cross 줄 한도 시험 685 → 688 (긴 import 경로 줄바꿈 3줄) - 확인: typecheck 0 · 서버 묶음 넷(corridor · server-calc · b07-extent · b07-note) 다시 지어 코드 같음(//#region 경로 주석만 다름) · 관련 시험 270 파일 — 남은 실패 19 는 옮기기 전과 같음 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014utrjfoW831Vc5wJUd13kL
144 lines
5.6 KiB
TypeScript
144 lines
5.6 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;
|
|
}
|
|
|
|
/**
|
|
* 확폭 면적(㎡ · 138-2 · 표시만) — 확폭(좌 + 우) 측점 사이 사다리꼴 합 Σ(wᵢ + wᵢ₊₁)/2 × Δ.
|
|
* 구간 앞뒤 이웃 측점(확폭 0)까지 이어 붙이고 · 노면공 범위 `[start, end]` 밖은 잘라 냄(자른 끝은 직선 보간).
|
|
*/
|
|
export function wideningArea(
|
|
stations: readonly WideningStation[],
|
|
run: WideningRun,
|
|
range: { start: number; end: number },
|
|
): number {
|
|
const sorted = [...stations].sort((a, b) => a.chainage_m - b.chainage_m);
|
|
const first = sorted.findIndex((s) => Math.abs(s.chainage_m - run.start_m) <= WIDENING_TOL_M);
|
|
const last = sorted.findIndex((s) => Math.abs(s.chainage_m - run.end_m) <= WIDENING_TOL_M);
|
|
if (first < 0 || last < 0) return 0;
|
|
const points = sorted
|
|
.slice(Math.max(first - 1, 0), last + 2)
|
|
.map((s) => [s.chainage_m, wideningOf(s)] as const);
|
|
let area = 0;
|
|
for (let i = 0; i + 1 < points.length; i += 1) {
|
|
const [x0, w0] = points[i];
|
|
const [x1, w1] = points[i + 1];
|
|
const a = Math.max(x0, range.start);
|
|
const b = Math.min(x1, range.end);
|
|
if (b <= a || x1 <= x0) continue;
|
|
const at = (x: number): number => w0 + ((w1 - w0) * (x - x0)) / (x1 - x0);
|
|
area += ((at(a) + at(b)) / 2) * (b - a);
|
|
}
|
|
return area;
|
|
}
|
|
|
|
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));
|