/* ============================================================================= * B05_Profile_UI_PlanContext.ts * B05 흐린 다른 세트 겹(PLAN 635-4 · 요구 546 · 547 · 결정 5 · 6 · 7) — 같은 예정노선의 다른 세트(다른 해 · 같은 해 다른 업체). * 자료 = `GET /api/projects/{B}/plan-context` 실시간 읽기(보기 전용 · 파일 안 만듦 · 업체 이름 없음). * 그림 = 옅은 선 + 세트 이름표 「2028-1」 · 누름 · 고르기 · 고침 없음(겹에 누름 받는 칸 없음 · 기능 표 `others` = 보기만). * 붙기 = 남 세트 끝(이음점)을 긋기 · 노드 끌기 붙을 자리로 내놓음(`joints`) · 고정 안 함 → 벗어나면 `note` 한 줄. * 셈 = `logic/B05_Profile_PlanContext.ts`. * ========================================================================== */ import { themeColor } from "@ui/ui_template_palette"; import { planLabel, strokeCased } from "../D03_PlanAnalysis/D03_PlanAnalysis_UI_DrawPaint"; import type { ScreenMap } from "../D03_PlanAnalysis/D03_PlanAnalysis_UI_Map"; import { requestJson } from "./B05_Profile_Api_Replan"; import { jointGaps, jointNote, jointPoints, labelPoint, linkedJoints, otherSets, type PlanContext, type Vertex, } from "./logic/B05_Profile_PlanContext"; /** 흐린 세트 불투명. */ const FAINT_ALPHA = 0.45; export interface PlanContextLayer { /** 문맥 다시 읽음(연결 없음 · 실패 = 빈 겹). */ load: () => Promise; /** 붙을 자리(남 세트 끝). */ joints: () => Vertex[]; /** 지금 예정노선이 받을 때 이어졌던 이음점에서 벗어났으면 안내 한 줄. */ note: (line: ReadonlyArray) => string | null; paint: (context: CanvasRenderingContext2D, map: Pick) => void; /** 검증 창구 — 흐린 세트 열쇠 · 꼭짓점 수 · 이음점. */ probe: () => { sets: Array<{ key: string; points: number }>; joints: Vertex[]; mine: string | null; }; } export function createPlanContextLayer(projectId: string, redraw: () => void): PlanContextLayer { let context: PlanContext | null = null; return { async load() { try { context = await requestJson( `/projects/${projectId}/plan-context`, { method: "GET" }, 30000, ); } catch { context = null; // 보기 전용 겹 — 못 읽으면 안 그림 } redraw(); }, joints: () => jointPoints(context).map((joint) => joint.point), note: (line) => jointNote(jointGaps(line, linkedJoints(context))), paint(ctx, map) { const sets = otherSets(context); if (!sets.length) return; const color = themeColor("--draw-text-secondary", "#9ca3af"); ctx.save(); ctx.globalAlpha = FAINT_ALPHA; for (const set of sets) for (const line of set.lines) { if (line.length < 2) continue; ctx.beginPath(); line.forEach((point, i) => { const [sx, sy] = map.toScreen(point); if (i) ctx.lineTo(sx, sy); else ctx.moveTo(sx, sy); }); strokeCased(ctx, color, 3); } ctx.globalAlpha = Math.min(1, FAINT_ALPHA + 0.3); for (const set of sets) { const at = labelPoint(set); if (!at) continue; const [sx, sy] = map.toScreen(at); planLabel(ctx, map, sx, sy - 12, set.key, "600 12px"); } ctx.restore(); }, probe: () => ({ sets: otherSets(context).map((set) => ({ key: set.key, points: set.lines.reduce((sum, line) => sum + line.length, 0), })), joints: jointPoints(context).map((joint) => joint.point), mine: context?.mine ?? null, }), }; }