무슨 일이 있었나 랩탑 줄의 병합 `20ba886c`(Merge origin/main_desktop_1·main_laptop_1·sub_desktop_1 into sub_laptop_1)가 우리 파일 22개를 떨구고 35개 파일의 내용을 옛것으로 되돌림. 손으로 지운 커밋은 없고 **병합 자체가 떨군 것**임. 그것이 `origin/dev`·`main_laptop_1`·`sub_laptop_1`· `CODEX` 까지 퍼졌고(데스크탑 둘만 무사), 이 창의 병합 `d92c1f2b` 로 들어옴. 잃었던 것 - 공용 — `common_util_provenance.py` · `ui_template_provenance.ts` - B08 — 근거 사전 · 좌측 패널 상자 모듈 · 토량환산계수 칸 - B09 — 근거 사전 셋 - B05 — 계획노선 편집 모듈 아홉 · 지형 라우터 · B04 지도 모듈 - 시험 셋과, 35개 파일 안의 최근 작업(환산계수 고르기 · 근거 호버 배선 등) 어떻게 되살렸나 `611a2b40`(병합 직전, 전부 온전)에서 `git show <커밋>:<경로>` 로 내용만 꺼내 되돌림. 이력은 안 건드림. ⚠ HEAD 에만 있던 「추가 816줄」은 랩탑의 새 작업이 아니라 **되살아난 옛 코드**였음(B05 편집은 모듈로 쪼개기 전 덩어리 · B08 라우터는 환산계수 고르기 전 옛 상수판). 되돌릴 시점 이후의 **진짜 새 커밋은 둘뿐**이라 그 둘만 패치로 다시 얹음 — `9f827bf6`(리로드 빌드 고리 끊기, 데스크탑 보조) · `b9bca6b3`(B06 조정창 1px, 랩탑). 위키 여덟은 코덱스 몫이라 손대지 않음. 자체검증 — 양쪽 작업이 다 살아 있음을 짚어 확인: `main.py` 의 「개발 서버는 살려 둔다」 · `B05_Profile_Engine_Grade.py` 의 `plan_curve_length_limit_m` · `B08_..._EarthworkGrid.ts` 의 `attachProvenance`. `tsc --noEmit` 통과 · `pytest -q` **1317 passed, 28 skipped** (되살리기 전에는 시험 둘이 수집 단계에서 깨져 있었음). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017RANEBHns1S4tkmsYwewtk
112 lines
5.0 KiB
TypeScript
112 lines
5.0 KiB
TypeScript
/* =============================================================================
|
|
* B05_Profile_UI_RouteEdit_Contour.ts
|
|
* 계획노선 편집 모달이 바탕에 깔 **등고선 한 벌**을 고른다.
|
|
*
|
|
* **어느 등고선을 쓰나**(2026-09-12 사용자 지시 ⑥) — 도엽 등고선과 LAS 로 만든 등고선은
|
|
* 서로 어긋난다. 노선은 실제 지형 위에 놓여야 하므로 **확정 지표면 모델이 있으면 LAS 쪽**을
|
|
* 쓰고, 없는 프로젝트에서만 지금까지처럼 도엽 등고선을 쓴다.
|
|
*
|
|
* 둘은 생김새가 다르다 — 도엽은 위경도 GeoJSON(표고는 `등고수치` 속성), LAS 는 사업지
|
|
* 좌표(m) 점렬(표고는 `level`)이다. 여기서 **같은 `PreparedLayer` 한 꼴로 맞춰** 내보내
|
|
* 그리기·라벨·집기가 출처를 안 가리게 한다.
|
|
* ========================================================================== */
|
|
|
|
import { API_BASE_URL } from "@config/config_frontend";
|
|
import { fetchCachedJson } from "../A00_Common/b_asset_cache";
|
|
import {
|
|
type GeoJsonCollection,
|
|
type Normalizer,
|
|
type PreparedLayer,
|
|
} from "../B04_PreProcess/B04_PreProcess_UI_MapRender";
|
|
import {
|
|
prepareLayer,
|
|
prepareMetricPolylines,
|
|
} from "../B04_PreProcess/B04_PreProcess_UI_MapRender_Prepare";
|
|
import type { VWorldMeta } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
|
|
|
/** 도엽 등고선의 표고 속성 이름 — B04 지도가 쓰는 것과 같은 키. */
|
|
const SHEET_ELEVATION_KEYS = ["등고수치"];
|
|
/** 표고를 못 읽었을 때 라벨 솎기에 쓸 간격(m). */
|
|
const FALLBACK_INTERVAL_M = 5;
|
|
/** LAS 등고선을 **몇 m 단위로** 낼지 — 도엽 등고선과 같은 눈금(2026-09-12 사용자 지시 ⑭).
|
|
*
|
|
* LAS 자료는 1m 간격으로 뽑혀 있어 그대로 쓰면 확대할 때 1m·2m 짜리까지 나온다. 서버에 5m
|
|
* 로 다시 뽑아 달라고 하면 첫 한 번이 오래 걸리므로 **받아 둔 1m 자료에서 5의 배수만 고른다**
|
|
* — 그림도 라벨도 도엽 쪽과 같은 눈금이 된다. */
|
|
const LAS_CONTOUR_UNIT_M = 5;
|
|
|
|
export interface RouteEditContours {
|
|
layer: PreparedLayer;
|
|
/** 등고선 간격(m) — 라벨을 몇 줄마다 낼지 정하는 기준. */
|
|
intervalM: number;
|
|
source: "las" | "sheet";
|
|
}
|
|
|
|
interface ContourResponse {
|
|
contours: Array<{ level: number; coordinates: Array<[number, number, number]> }>;
|
|
}
|
|
|
|
/**
|
|
* 바탕 등고선을 읽는다. 확정 지표면 모델이 있으면 LAS, 없으면 이미 받아 둔 도엽 컬렉션.
|
|
*
|
|
* LAS 쪽을 못 읽으면 **조용히 도엽으로 내려앉는다** — 등고선이 아예 없는 화면보다 낫고,
|
|
* 어느 쪽을 쓰고 있는지는 `source` 로 나가 상태줄에 적힌다.
|
|
*/
|
|
export async function loadRouteEditContours(
|
|
projectId: string,
|
|
meta: VWorldMeta,
|
|
normalizer: Normalizer,
|
|
sheet: GeoJsonCollection | null,
|
|
options: { surfaceModelId: number | null; intervalM: number; smooth: boolean },
|
|
): Promise<RouteEditContours> {
|
|
if (options.surfaceModelId !== null) {
|
|
// 받아 오는 간격은 프로젝트 설정 그대로(보관함에 이미 있는 파일을 쓰려는 것) —
|
|
// **보이는 눈금**은 아래에서 5m 로 맞춘다.
|
|
const interval = options.intervalM > 0 ? options.intervalM : 1;
|
|
try {
|
|
// 3D 뷰어가 쓰는 것과 **같은 파일**이다 — 보관함에 있으면 다시 내려받지 않는다.
|
|
const data = await fetchCachedJson<ContourResponse>(
|
|
projectId,
|
|
`${API_BASE_URL}/projects/${projectId}/surface/models/${options.surfaceModelId}` +
|
|
`/contour?interval=${interval}&smooth=${options.smooth}`,
|
|
);
|
|
const lines = (data.contours ?? [])
|
|
// 5m 단위만 남긴다 — 1m 자료를 다 들고 있으면 그리기·집기가 다섯 배로 무겁다.
|
|
.filter((contour) => Math.abs(contour.level % LAS_CONTOUR_UNIT_M) < 1e-6)
|
|
.map((contour) => ({
|
|
points: contour.coordinates.map(([x, y]) => [x, y] as const),
|
|
label: contour.level,
|
|
}))
|
|
.filter((line) => line.points.length >= 2);
|
|
if (lines.length > 0) {
|
|
return {
|
|
layer: prepareMetricPolylines(lines, meta),
|
|
intervalM: LAS_CONTOUR_UNIT_M,
|
|
source: "las",
|
|
};
|
|
}
|
|
} catch {
|
|
/* 내려앉는다 — 아래 도엽 갈래로 이어 간다. */
|
|
}
|
|
}
|
|
const layer = prepareLayer(sheet ?? undefined, normalizer, SHEET_ELEVATION_KEYS);
|
|
return { layer, intervalM: inferIntervalM(layer), source: "sheet" };
|
|
}
|
|
|
|
/** 도엽 등고선의 간격(m) — 표고 값들의 **가장 좁은 칸**을 간격으로 본다. */
|
|
function inferIntervalM(layer: PreparedLayer): number {
|
|
const levels = [
|
|
...new Set(
|
|
layer.features
|
|
.map((feature) => feature.labelValue)
|
|
.filter((value): value is number => value !== null),
|
|
),
|
|
].sort((a, b) => a - b);
|
|
let smallest = Infinity;
|
|
for (let index = 1; index < levels.length; index += 1) {
|
|
const gap = levels[index] - levels[index - 1];
|
|
if (gap > 0 && gap < smallest) smallest = gap;
|
|
}
|
|
return Number.isFinite(smallest) ? smallest : FALLBACK_INTERVAL_M;
|
|
}
|