/* ============================================================================= * 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, ); }