- 옮김: 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
189 lines
8.3 KiB
TypeScript
189 lines
8.3 KiB
TypeScript
/* =============================================================================
|
|
* B05_Profile_Pipeline_RouteGround.ts
|
|
* 실시간 계산 묶음(PLAN 346-1)의 **route · ground 단계** — 계약 = `design_pipeline_types.ts`.
|
|
*
|
|
* route — 노선 초안(RouteDraft) → 그린 노선(원호 포함) · 누가거리. 선은 이미 있는 짝
|
|
* `buildEditedPolyline`(↔ `common_util_route_polyline.py`) 그대로.
|
|
* ground — 측점(지반 null) → 지반 채운 측점 · 종단 지반선(1 m). 표본 = 지형 격자 짝
|
|
* (`common_util_surface_sampler.ts` · `B05_Profile_Sections_Ground.ts`) · 격자가 없거나
|
|
* (아직 못 받음 · DTM 아님) 분석 지형 ≠ 설계 지형이면 주입받은 서버 표본(`remote`).
|
|
* 바뀐 범위 밖 측점 · 종단은 앞 출력 재사용(국부 연산) — 자리 · 축 · 지형이 같을 때만.
|
|
*
|
|
* 화면 · fetch · 세션 안 씀 — 격자 · 서버 길은 부르는 쪽이 넣음(`B05_Profile_Api_TerrainGrid.ts`).
|
|
* ========================================================================== */
|
|
|
|
import type {
|
|
DesignCurve,
|
|
GroundOutput,
|
|
GroundSample,
|
|
PipelineStation,
|
|
RouteDraft,
|
|
RouteLine,
|
|
StageFn,
|
|
StationGround,
|
|
Vertex,
|
|
} from "../../A00_Common/design/logic/design_pipeline_types";
|
|
import { type DtmGrid, heightAt } from "../../common_util/common_util_surface_sampler";
|
|
import { buildEditedPolyline } from "./B05_Profile_UI_RouteEdit_Curve";
|
|
import { applyArcLocks } from "./B05_Profile_UI_RouteEdit_Edits";
|
|
import {
|
|
crossOffsets,
|
|
longitudinalPoints,
|
|
round6,
|
|
SECTION_CROSS_HALF_WIDTH_M,
|
|
SECTION_CROSS_SAMPLE_INTERVAL_M,
|
|
} from "./B05_Profile_Sections_Ground";
|
|
|
|
/* ── route ─────────────────────────────────────────────────────────────── */
|
|
|
|
export interface RouteStageDeps {
|
|
/** 곡선 기본 반지름(m) — 편집기 `minRadiusM`(계획 기준 · 0 = 없음). */
|
|
minRadiusM(): number;
|
|
/** 초안이 없을 때(= 저장분 그대로) 그릴 노선 · 곡선표 — 없으면 null. */
|
|
savedLine(): { line: Vertex[]; curves: DesignCurve[] } | null;
|
|
}
|
|
|
|
/** 정점마다 누가거리 — 첫 점 0. */
|
|
export function routeLineOf(line: Vertex[], curves: DesignCurve[] = []): RouteLine {
|
|
const chainage = [0];
|
|
for (let i = 1; i < line.length; i += 1) {
|
|
chainage.push(
|
|
chainage[i - 1] + Math.hypot(line[i][0] - line[i - 1][0], line[i][1] - line[i - 1][1]),
|
|
);
|
|
}
|
|
return { line, curves, chainage, lengthM: chainage[chainage.length - 1] };
|
|
}
|
|
|
|
/** 초안 → 그린 노선 · 곡선표(계약 꼴). 길이를 붙든 곡선은 교각에 맞춰 R 을 다시 잡음(편집기
|
|
* `markEdited` 와 같은 차례). */
|
|
export function draftLine(
|
|
draft: RouteDraft,
|
|
minRadiusM: number,
|
|
): { line: Vertex[]; curves: DesignCurve[] } {
|
|
const nodes = draft.nodes.map((node): Vertex => [node[0], node[1]]);
|
|
const radius = [...draft.curveRadius];
|
|
applyArcLocks(nodes, [...draft.curveLock], [...draft.curveArc], radius);
|
|
const built = buildEditedPolyline(nodes, draft.curveOn, radius, minRadiusM);
|
|
return {
|
|
line: built.vertices,
|
|
curves: built.curves.map(({ start, apex, end, radius_m }) => ({ start, apex, end, radius_m })),
|
|
};
|
|
}
|
|
|
|
export function createRouteStage(deps: RouteStageDeps): StageFn<"route"> {
|
|
return (ctx) => {
|
|
const draft = ctx.inputs.route;
|
|
const drawn = draft ? draftLine(draft, deps.minRadiusM()) : deps.savedLine();
|
|
if (!drawn || drawn.line.length < 2) throw new Error("그릴 노선이 없습니다.");
|
|
return routeLineOf(drawn.line, drawn.curves);
|
|
};
|
|
}
|
|
|
|
/* ── ground ────────────────────────────────────────────────────────────── */
|
|
|
|
export interface GroundStageDeps {
|
|
/** 지금 쓸 수 있는 설계 지형 격자 — 아직 못 받았거나 없으면 null(그동안은 서버 길). */
|
|
grid(): { grid: DtmGrid; design: "las" | "sheet" } | null;
|
|
/** 서버 표본(`/route/elevations`) — 점 차례 그대로 · 무효 null · `signal` 로 끊김. */
|
|
remote(
|
|
points: Vertex[],
|
|
terrain: string | null,
|
|
signal: AbortSignal,
|
|
): Promise<Array<number | null>>;
|
|
/** 횡단 반폭(m) — 비면 서버 기본(20 m). */
|
|
crossHalfWidthM?(): number | null;
|
|
}
|
|
|
|
const samePlace = (a: PipelineStation, b: PipelineStation): boolean =>
|
|
a.x === b.x && a.y === b.y && a.left[0] === b.left[0] && a.left[1] === b.left[1];
|
|
|
|
const outside = (chainageM: number, range: { fromM: number; toM: number } | null): boolean =>
|
|
range !== null && (chainageM < range.fromM || chainageM > range.toM);
|
|
|
|
export function createGroundStage(deps: GroundStageDeps): StageFn<"ground"> {
|
|
/** 앞 출력이 쓴 지형 · 노선 · 반폭 — 다르면 재사용 안 함. */
|
|
let last: { terrain: string; line: Vertex[] | null; halfWidth: number } | null = null;
|
|
|
|
return async (ctx) => {
|
|
const stations = ctx.out.stations ?? [];
|
|
const line = ctx.out.route?.line ?? null;
|
|
const terrain = ctx.inputs.route?.terrain ?? null;
|
|
const halfWidth = deps.crossHalfWidthM?.() ?? SECTION_CROSS_HALF_WIDTH_M;
|
|
const offsets = crossOffsets(halfWidth, SECTION_CROSS_SAMPLE_INTERVAL_M);
|
|
const local = deps.grid();
|
|
const useGrid = local !== null && (terrain === null || terrain === local.design);
|
|
|
|
const tag = terrain ?? "design";
|
|
const reusable = ctx.prev !== null && last?.terrain === tag && last.halfWidth === halfWidth;
|
|
const range = ctx.range;
|
|
const prevByKey = new Map((reusable ? ctx.prev!.stations : []).map((s) => [s.key, s]));
|
|
|
|
// 다시 셀 측점 — 범위 안 · 앞 출력에 없음 · 자리나 축이 바뀜.
|
|
const kept = stations.map((station) => {
|
|
const before = prevByKey.get(station.key);
|
|
return before?.ground && outside(station.chainageM, range) && samePlace(before, station)
|
|
? before.ground
|
|
: null;
|
|
});
|
|
|
|
const long = line ? longitudinalPoints(line) : { chainage: [], xy: [] as Vertex[], lengthM: 0 };
|
|
// 종단 — 노선이 그대로면 통째 · 범위 앞쪽은 같은 노선 위라 그대로.
|
|
const prevLong = reusable ? ctx.prev!.longitudinal : null;
|
|
const longKept: Array<number | null | undefined> = long.chainage.map((c, i) => {
|
|
if (!prevLong || prevLong.chainageM[i] !== round6(c)) return undefined;
|
|
if (last?.line === line || (range !== null && c < range.fromM)) return prevLong.z[i];
|
|
return undefined;
|
|
});
|
|
|
|
const points: Vertex[] = [];
|
|
stations.forEach((station, i) => {
|
|
if (kept[i]) return;
|
|
for (const offset of offsets) {
|
|
points.push([station.x + station.left[0] * offset, station.y + station.left[1] * offset]);
|
|
}
|
|
});
|
|
long.xy.forEach((xy, i) => {
|
|
if (longKept[i] === undefined) points.push(xy);
|
|
});
|
|
|
|
let heights: Array<number | null>;
|
|
if (useGrid) {
|
|
heights = points.map(([x, y]) => {
|
|
const z = heightAt(local!.grid, x, y);
|
|
return Number.isNaN(z) ? null : round6(z);
|
|
});
|
|
} else {
|
|
heights = points.length ? await deps.remote(points, terrain, ctx.signal) : [];
|
|
}
|
|
|
|
let cursor = 0;
|
|
const filled: PipelineStation[] = stations.map((station, i) => {
|
|
if (kept[i]) return { ...station, ground: kept[i] };
|
|
const zs = offsets.map(() => heights[cursor++] ?? null);
|
|
return { ...station, ground: stationGround(offsets, zs) };
|
|
});
|
|
const z = long.chainage.map((_, i) =>
|
|
longKept[i] === undefined ? (heights[cursor++] ?? null) : (longKept[i] as number | null),
|
|
);
|
|
last = { terrain: tag, line, halfWidth };
|
|
const output: GroundOutput = {
|
|
stations: filled,
|
|
longitudinal: { chainageM: long.chainage.map(round6), z },
|
|
};
|
|
return output;
|
|
};
|
|
}
|
|
|
|
/** offset 열 표고 → 계약 꼴(중심 · 좌 · 우 · 중심에서 바깥 차례 · offsetM 양수). */
|
|
function stationGround(offsets: number[], zs: Array<number | null>): StationGround {
|
|
const left: GroundSample[] = [];
|
|
const right: GroundSample[] = [];
|
|
let centerZ: number | null = null;
|
|
offsets.forEach((offset, i) => {
|
|
if (Math.abs(offset) <= 1e-9) centerZ = zs[i];
|
|
else if (offset > 0) left.push({ offsetM: round6(offset), z: zs[i] });
|
|
else right.unshift({ offsetM: round6(-offset), z: zs[i] });
|
|
});
|
|
return { centerZ, left, right };
|
|
}
|