/* ============================================================================= * D03_PlanAnalysis_Expect.ts * D03 예상 횡단(PLAN 544-1 · 601-1 · 요구 363-1 · 463-21) — 측점 지반 → 표준 횡단 기본값 설계선 · 절 · 성토 ㎡. * 셈 = 공용 `computeCrossDesign`(① 짝 `compute_cross_design` · 거울 `test_b06_cross_design_mirror.py`) * · 기본 = B 초기 설계와 같음(리핑암 · 산쪽 절토 · 일반 측구 · 암 경계 = 서버 기본) · 계획고 = 측점 중심 지반고 * · 미교차 = `extendUnclosed`(격자 있을 때) · 곡선 확폭 없음 · 보기만(저장 없음). * 3D = 같은 단면을 B05 `buildCorridor` 가 읽는 측점 꼴로(`expectSections`) · 노선망 전체 = 선마다(`expectLine`) · * 갈래 머리 = 부모 노면(차도 반 + 노견)만큼 잘라 냄(두 노면 겹침 막음). * 시험 `resources/tester/test_544_1_plan_expect.py`. * ========================================================================== */ import { computeCrossDesign, type CrossDesignOptions, type CrossDesignResult, type StandardCrossSectionSpec, } from "../../common_util/common_util_cross_design"; import type { DtmGrid } from "../../common_util/common_util_surface_sampler"; import { extendUnclosed, type CrossGroundPoint, type StationGround, } from "../../B05_Profile/logic/B05_Profile_Sections_Ground"; import { lineLength, sliceLine, stationChainages, type Vertex } from "./D03_PlanAnalysis_Draw"; /** 서버 `sections/context` 의 표준 횡단(config) · 암 경계 기본 오프셋. */ export interface ExpectStandard { standard: StandardCrossSectionSpec; rockBoundaryM: number | null; } export interface ExpectCross { design: CrossDesignResult; /** 설계에 쓴 지반 표본(미교차로 넓혔으면 넓힌 것). */ samples: CrossGroundPoint[]; } /** B 초기 설계 기본 — 리핑암 · 산쪽 절토(모르면 좌) · 일반 측구. */ export function expectOptions( uphill: "left" | "right" | null, std: ExpectStandard, ): CrossDesignOptions { return { groundType: "ripping_rock", sectionMode: `${uphill ?? "left"}_cut`, ditchType: "standard", standard: std.standard, rockBoundaryOffsetM: std.rockBoundaryM, }; } /** 측점 하나 예상 단면 — 지반고 · 표본이 없으면 null. */ export function expectCross( station: StationGround, std: ExpectStandard, grid?: DtmGrid | null, ): ExpectCross | null { const elevation = station.center_z; if (elevation === null) return null; const options = expectOptions(station.uphill_side, std); try { let samples = station.samples; let design = computeCrossDesign(samples, elevation, options); const wide = design.slope_unclosed && grid ? extendUnclosed( samples, [station.center_x, station.center_y], station.left_xy, (next) => computeCrossDesign(next, elevation, options), grid, ) : null; if (wide) { samples = wide.samples as CrossGroundPoint[]; design = computeCrossDesign(samples, elevation, options); } return { design, samples }; } catch { return null; // 표본 부족 — B 와 같은 태도(그 측점 건너뜀) } } /** B06 `CrossSection` 중 `buildCorridor` 가 읽는 칸(B06 형 파일은 별칭 import 라 여기 안 끌어옴). */ export interface ExpectSection { station_id: string; chainage_m: number; label: string; kind: "regular"; center_z: number | null; azimuth_deg: null; center_x: number; center_y: number; uphill_side: "left" | "right" | null; frame: { left_xy: [number, number] }; samples: CrossGroundPoint[]; design: CrossDesignResult; } /** 3D 코리도 입력 — 측점마다 예상 단면(못 세운 측점 뺌) · 누가 = 연도 노선 위 누가. */ export function expectSections( stations: StationGround[], std: ExpectStandard, grid?: DtmGrid | null, ): ExpectSection[] { return stations.flatMap((station) => { const cross = expectCross(station, std, grid); if (!cross) return []; return [ { station_id: String(station.chainage_m), chainage_m: station.chainage_m, label: "", kind: "regular", center_z: station.center_z, azimuth_deg: null, center_x: station.center_x, center_y: station.center_y, uphill_side: station.uphill_side, frame: { left_xy: station.left_xy }, samples: cross.samples, design: cross.design, }, ]; }); } /** 갈래 머리 잘라 낼 길이(m) — 리핑암 표준 차도 반 + 넓은 노견(부모 노면 안). */ export function branchTrimM(std: ExpectStandard): number { const rock = std.standard.rock; if (!rock) return 0; return ( (rock.road_width_m ?? 0) / 2 + Math.max(rock.shoulder_left_m ?? 0, rock.shoulder_right_m ?? 0) ); } /** 3D 한 선 — 그릴 꺾은선 · 그 위 측점(꺾은선 첫 점 = 0) · 원래 누가 = 측점 + `offsetM`. */ export interface ExpectLine { line: Vertex[]; offsetM: number; stations: number[]; } /** 선 하나 3D 입력 — 측점 = 원래 누가(`intervalM`) · `trimM` > 0(갈래)이면 머리를 잘라 낸 자리부터. * 남는 길이가 1 m 안 되면 null. */ export function expectLine( vertices: ReadonlyArray, trimM: number, intervalM: number, ): ExpectLine | null { const length = lineLength(vertices); const offsetM = Math.max(0, trimM); if (vertices.length < 2 || length - offsetM < 1) return null; const line = offsetM > 0 ? sliceLine(vertices, offsetM, length) : [...vertices]; const stations = [ 0, ...stationChainages(length, intervalM) .filter((m) => m > offsetM + 1e-6) .map((m) => m - offsetM), ]; return { line, offsetM, stations }; }