Files
Aislo/B05_Profile/B05_Profile_UI_RouteEdit_Cross_Draw.ts
T
eomsangdonandClaude Opus 5 f952ac7ffd fix(git): 병합이 떨군 파일 22개와 되돌아간 파일 35개를 되살림
무슨 일이 있었나
랩탑 줄의 병합 `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
2026-09-12 18:18:57 +09:00

121 lines
5.1 KiB
TypeScript

/* =============================================================================
* B05_Profile_UI_RouteEdit_Cross_Draw.ts
* 횡단 한 장을 캔버스에 그린다 — **원지반선·기본 계획 횡단선**과 아래 한 줄 요약.
*
* `B05_Profile_UI_RouteEdit_Cross.ts` 에서 떼어낸 조각이다(2026-09-12, 700줄 규정).
* 값은 서버가 B05·B06 정본으로 낸 것을 그대로 그린다 — 여기서 기하를 만들지 않는다.
* ========================================================================== */
import type { CrossSection } from "./../B06_Section/B06_Section_Api_Fetch";
import { fillSlopeLengths } from "./../B06_Section/B06_Section_UI_Cross_Fit";
import type { CrossPreviewResponse } from "./B05_Profile_Api_Replan";
/** 그림 가장자리 여백(px). */
const PAD = 24;
/** 성토사면 길이·절성토 면적 한 줄. */
export function summarizeCross(preview: CrossPreviewResponse): string {
const design = preview.design;
if (!design) return "계획고를 못 세워 계획 횡단을 그리지 못했습니다.";
// 성토사면 길이는 **B06 화면이 쓰는 그 함수**를 그대로 부른다 — 두 화면이 다른 길이를
// 말하면 안 된다. 필요한 것은 `samples` 와 `design` 둘뿐이라 그만 담아 넘긴다.
const lengths = fillSlopeLengths({
samples: preview.samples,
design,
} as unknown as CrossSection);
const sides = (["left", "right"] as const)
.filter((side) => lengths[side] !== null)
.map((side) => {
const value = lengths[side]!;
// 계산 반폭 안에서 원지반을 못 만난 사면은 거기까지만 잰 하한값이라 「≥」로 구분한다.
return `${side === "left" ? "좌" : "우"} ${value.open ? "≥" : ""}${value.lengthM.toFixed(2)}m`;
});
const slope = sides.length ? `성토사면 ${sides.join(" · ")}` : "성토측 없음";
return `${slope} · 절토 ${design.cut_area_m2.toFixed(2)}㎡ · 성토 ${design.fill_area_m2.toFixed(2)}㎡`;
}
/**
* 원지반선과 기본 계획 횡단선을 한 판에 그린다. 좌(+offset)가 화면 왼쪽이다
* (`generate_sections` cad_exchange 규약과 같은 방향).
*
* **가로·세로를 같은 배율로** 둔다 — 따로 늘리면 사면 기울기가 거짓으로 보인다. 횡단도는
* 기울기를 눈으로 읽는 그림이라 왜곡하면 안 된다(2026-09-12 실화면: 노면이 안 보였다).
*/
export function drawCross(
context: CanvasRenderingContext2D,
canvas: HTMLCanvasElement,
preview: CrossPreviewResponse,
): void {
const ground = preview.samples
.filter((sample) => sample.valid && sample.elevation_m !== null)
.map((sample) => [Number(sample.offset_m), Number(sample.elevation_m)] as [number, number]);
const design = (preview.design?.design_line ?? []).map(
(point) => [point.offset_m, point.elevation_m] as [number, number],
);
const all = [...ground, ...design];
context.clearRect(0, 0, canvas.width, canvas.height);
if (all.length < 2) return;
const offsets = all.map((point) => point[0]);
const heights = all.map((point) => point[1]);
const minOffset = Math.min(...offsets);
const maxOffset = Math.max(...offsets);
const minZ = Math.min(...heights);
const maxZ = Math.max(...heights);
const spanX = maxOffset - minOffset || 1;
const spanZ = maxZ - minZ || 1;
const scale = Math.min((canvas.width - PAD * 2) / spanX, (canvas.height - PAD * 2) / spanZ);
const centerOffset = (minOffset + maxOffset) / 2;
const centerZ = (minZ + maxZ) / 2;
const toScreen = (point: [number, number]): [number, number] => [
canvas.width / 2 + (centerOffset - point[0]) * scale,
canvas.height / 2 + (centerZ - point[1]) * scale,
];
const stroke = (points: Array<[number, number]>, color: string, width: number): void => {
if (points.length < 2) return;
context.beginPath();
points.forEach((point, index) => {
const [x, y] = toScreen(point);
if (index === 0) context.moveTo(x, y);
else context.lineTo(x, y);
});
context.strokeStyle = color;
context.lineWidth = width;
context.stroke();
};
// 중심선 — 어디가 노선 가운데인지 먼저 보이게.
const [centerX] = toScreen([0, centerZ]);
context.save();
context.setLineDash([4, 4]);
context.strokeStyle = "rgba(148,163,184,0.7)";
context.lineWidth = 1;
context.beginPath();
context.moveTo(centerX, PAD / 2);
context.lineTo(centerX, canvas.height - PAD / 2);
context.stroke();
context.restore();
stroke(ground, "#94a3b8", 1.6); // 원지반
stroke(design, "#f97316", 2.2); // 기본 계획 횡단
context.font = "11px system-ui, sans-serif";
context.textBaseline = "top";
context.fillStyle = "#94a3b8";
context.textAlign = "left";
context.fillText("원지반", PAD, 4);
context.fillStyle = "#f97316";
context.textAlign = "right";
context.fillText("기본 계획 횡단", canvas.width - PAD, 4);
context.fillStyle = "#94a3b8";
context.textAlign = "center";
context.textBaseline = "bottom";
context.fillText(
`좌 ${maxOffset.toFixed(0)}m ← 중심 → 우 ${Math.abs(minOffset).toFixed(0)}m` +
` · 표고 ${minZ.toFixed(1)}~${maxZ.toFixed(1)}m`,
canvas.width / 2,
canvas.height - 2,
);
}