무슨 일이 있었나 랩탑 줄의 병합 `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
121 lines
4.9 KiB
TypeScript
121 lines
4.9 KiB
TypeScript
/* =============================================================================
|
|
* B05_Profile_UI_RouteEdit_Measure.ts
|
|
* 계획노선 위 **두 점 사이 구간 재기** — 길이와 종단기울기(계획서 0-9 ⑤).
|
|
*
|
|
* Shift+클릭으로 a·b 를 찍는다. 직선·곡선을 가리지 않는다 — 그려지는 폴리라인 위라면 어디든
|
|
* 짚을 수 있고, 누가거리는 노선 길이 표기와 같은 방식으로 잰다.
|
|
*
|
|
* 지반고는 **찍는 순간에만** 서버에 묻는다(`/route/elevations`). 확정된 지표면을 읽기만 하는
|
|
* 통로라 「편집 중에는 계산이 안 나간다」(계획서 0-2 확정 7)와 부딪히지 않는다 — 다만 노드를
|
|
* 끄는 동안에는 한 번도 부르지 않는다.
|
|
* ========================================================================== */
|
|
|
|
import { fetchRouteElevations } from "./B05_Profile_Api_Replan";
|
|
import { routePointAtScreen, type RoutePointHit } from "./B05_Profile_UI_RouteEdit_Input";
|
|
import { formatStation } from "./B05_Profile_Util_Station";
|
|
|
|
type Vertex = [number, number];
|
|
|
|
/** 구간 재기로 노선을 짚었다고 볼 거리(px). */
|
|
const MEASURE_HIT_PX = 14;
|
|
|
|
interface MeasurePoint extends RoutePointHit {
|
|
/** 그 자리의 지반고(m). 아직 못 물었거나 지표면 밖이면 null. */
|
|
z: number | null;
|
|
}
|
|
|
|
export interface MeasureToolParams {
|
|
projectId: string;
|
|
/** 규칙 측점 간격(m) — 측점 표기에 쓴다. */
|
|
stationIntervalM: number;
|
|
/** 지금 그려지는 노선(원호 포함). 편집으로 바뀌므로 함수로 받는다. */
|
|
line: () => Vertex[];
|
|
toScreen: (vertex: Vertex) => [number, number];
|
|
/** 창이 닫혔나 — 늦게 온 응답을 죽은 화면에 적지 않으려고. */
|
|
isClosed: () => boolean;
|
|
/** 상태가 바뀌었다 — 호출부가 상태줄을 다시 적고 다시 그린다. */
|
|
onChange: () => void;
|
|
}
|
|
|
|
export interface MeasureMark {
|
|
point: Vertex;
|
|
/** 시점에서 노선을 따라간 거리(m) — 그리기가 **이 값으로** 구간을 자른다(계획서 0-9 ㉕). */
|
|
chainageM: number;
|
|
}
|
|
|
|
export interface MeasureTool {
|
|
/** 찍힌 자리(0~2개) — 그리기가 쓴다. */
|
|
marks: () => MeasureMark[];
|
|
/** 잰 값 한 줄. 찍은 것이 없으면 빈 문자열. */
|
|
hint: () => string;
|
|
/** 재고 있나 — 작은 창을 띄울지 정하는 값. */
|
|
active: () => boolean;
|
|
/** 한 번 찍기. 두 점이 차면 지반고를 한 번만 물어 온다. */
|
|
pick: (px: number, py: number) => Promise<void>;
|
|
/** 잰 것을 지운다 — 작은 창을 닫을 때(계획서 0-9 ㉔). */
|
|
clear: () => void;
|
|
}
|
|
|
|
export function createMeasureTool(params: MeasureToolParams): MeasureTool {
|
|
/** 찍은 두 점. 셋째를 찍으면 새 구간의 시작이 된다. */
|
|
let picked: MeasurePoint[] = [];
|
|
|
|
const hint = (): string => {
|
|
if (picked.length === 0) return "";
|
|
const first = picked[0];
|
|
if (picked.length === 1) {
|
|
return `구간 재기 — 시작 ${formatStation(first.chainageM, params.stationIntervalM)}. 한 점 더.`;
|
|
}
|
|
const second = picked[1];
|
|
const span = Math.abs(second.chainageM - first.chainageM);
|
|
const head =
|
|
`구간 ${formatStation(first.chainageM, params.stationIntervalM)} → ` +
|
|
`${formatStation(second.chainageM, params.stationIntervalM)} · 길이 ${span.toFixed(1)}m`;
|
|
if (first.z === null || second.z === null || span <= 1e-6) {
|
|
return `${head} · 지반고를 못 읽어 기울기는 못 냅니다.`;
|
|
}
|
|
// 기울기는 **노선을 따라간 길이** 기준이다 — 직선거리로 나누면 곡선부에서 과대평가된다.
|
|
const rise = second.z - first.z;
|
|
return (
|
|
`${head} · 지반고 ${first.z.toFixed(1)} → ${second.z.toFixed(1)}m` +
|
|
` · 종단기울기 ${((rise / span) * 100).toFixed(1)}%`
|
|
);
|
|
};
|
|
|
|
return {
|
|
marks: () => picked.map((entry) => ({ point: entry.point, chainageM: entry.chainageM })),
|
|
hint,
|
|
active: () => picked.length > 0,
|
|
clear() {
|
|
if (picked.length === 0) return;
|
|
picked = [];
|
|
params.onChange();
|
|
},
|
|
async pick(px, py) {
|
|
const hit = routePointAtScreen(params.line(), params.toScreen, px, py, MEASURE_HIT_PX);
|
|
if (!hit) {
|
|
picked = []; // 노선을 빗나가면 재던 것을 접는다.
|
|
params.onChange();
|
|
return;
|
|
}
|
|
picked = picked.length >= 2 ? [{ ...hit, z: null }] : [...picked, { ...hit, z: null }];
|
|
params.onChange();
|
|
if (picked.length < 2) return;
|
|
const asked = picked;
|
|
try {
|
|
const heights = await fetchRouteElevations(
|
|
params.projectId,
|
|
asked.map((entry) => entry.point),
|
|
);
|
|
if (params.isClosed() || picked !== asked) return; // 그 사이 다시 찍었으면 버린다.
|
|
asked.forEach((entry, index) => {
|
|
entry.z = heights[index] ?? null;
|
|
});
|
|
} catch {
|
|
/* 지반고를 못 읽으면 길이만 낸다 — `hint` 가 그렇게 말한다. */
|
|
}
|
|
params.onChange();
|
|
},
|
|
};
|
|
}
|