/* ============================================================================= * 배수유역도 캔버스 렌더러 (B05) * * 한 프레임에 무엇을 어떤 순서로 그릴지만 담는다. 상태는 갖지 않고 패널이 매 프레임 넘긴 * 묶음(`DrainageScene`)만 본다 — 패널 본체(`_UI_Drainage_Panel.ts`)가 700줄 한계에 닿아 * 분리했다. * * 쌓는 순서(아래 → 위) * 세부유역 채움 · 전체 유역 외곽선 → 도엽 레이어 → 상류 세류 → 계획선 → * 유입 강도 색칠 → 평균 흐름 화살표 → 유입 집중점 → 배관 마커 * ========================================================================== */ import { drawPreparedLayer, routeLineColor, ROUTE_LINE_WIDTH, type Normalizer, type PreparedLayer, type ViewState, } from "../B04_PreProcess/B04_PreProcess_UI_MapRender"; import { drawFilledRing, drawRidgeRing, drawRingBadge, drawStationTicks, drawUpstreamLines, ringCenterOnScreen, } from "../B04_PreProcess/B04_PreProcess_UI_MapOverlays"; import type { DetailBasin, VWorldMeta } from "../B04_PreProcess/B04_PreProcess_Api_Fetch"; import { drawFlowArrows, type FlowArrow } from "../B04_PreProcess/B04_PreProcess_UI_FlowArrows"; import { drawStrengthLine } from "../B04_PreProcess/B04_PreProcess_UI_FlowRamp"; import type { RoutePoint } from "../B04_PreProcess/B04_PreProcess_UI_RouteSamples"; import { basinColor, createMetricProjector, drawHotspots, DRAINAGE_LAYERS, layerColor, type DrainageLayer, } from "./B05_Profile_UI_Drainage_Parts"; import type { PipeEditor } from "./B05_Profile_UI_Drainage_Pipes"; import { drawRouteSpans, type RouteSpanBand } from "./B05_Profile_UI_Drainage_Spans"; export interface DrainageScene { meta: VWorldMeta | null; normalizer: Normalizer | null; basins: ReadonlyArray; selectedBasin: number | null; mainBoundary: ReadonlyArray<[number, number]>; preparedLayers: ReadonlyMap; activeLayers: ReadonlySet; routeLayer: PreparedLayer | null; upstreamLines: ReadonlyArray>; showUpstream: boolean; strengthSamples: ReadonlyArray; strength: Float64Array; maxStrength: number; showStrength: boolean; flowArrows: ReadonlyArray; arrowSpacingM: number; showArrows: boolean; hotspots: ReadonlyArray<{ chainage: number; area: number }>; maxHotspotArea: number; showHotspots: boolean; pipeEditor: PipeEditor; pipeColor: (chainage: number, position: number) => string; /** 선택된 측점의 누가거리(m). 계획선 위 그 자리에 선택 표식을 그린다(null=없음). */ markedChainage: number | null; /** 규칙 측점 간격(m) — 눈금·번호 표기 기준 (2026-09-04 사용자 지시). */ stationIntervalM: number; /** 구간형 구조물이 놓인 자리 — 계획선 위에 띠로 얹는다(계획서 3-6). 없으면 빈 배열. */ intervalSpans: ReadonlyArray; } export function drawDrainageScene( context: CanvasRenderingContext2D, view: ViewState, scene: DrainageScene, ): void { const { meta, normalizer } = scene; // 유역 번호는 **맨 위에** 얹는다 — 채움과 함께 그리면 등고선·화살표·관 마커에 가려진다 // (2026-08-01 사용자 지시). 자리는 채움을 그리면서 같이 모아 둔다. const badges: Array<{ center: [number, number]; color: string; label: string }> = []; // 세부유역 채움을 가장 아래에 깔아 등고선·세류 판독을 가리지 않게 한다. if (normalizer) { scene.basins.forEach((basin) => { const color = basinColor(basin.index); if (basin.polygon_lonlat.length >= 3) { badges.push({ center: ringCenterOnScreen(basin.polygon_lonlat, normalizer, view), color, label: String(basin.index), }); } drawFilledRing( context, { ring: basin.polygon_lonlat, rings: basin.polygon_rings_lonlat }, normalizer, view, // 하나를 고르면 나머지는 옅게 물러난다. scene.selectedBasin === null || scene.selectedBasin === basin.index ? color : color.replace(/0\.45\)$/, "0.18)"), ); }); // 전체 유역 외곽선 = 분수령(능선). 해석 결과를 그대로 그린다 — 손으로 고치지 않는다. if (scene.mainBoundary.length > 2) { drawRidgeRing(context, scene.mainBoundary as Array<[number, number]>, normalizer, view); } } // 등고선을 얇게 깔고 세류를 그 위에, 노선을 맨 위에 둔다. DRAINAGE_LAYERS.forEach((layer) => { if (!scene.activeLayers.has(layer)) return; const prepared = scene.preparedLayers.get(layer); if (!prepared) return; context.lineWidth = layer === "도엽_등고선" ? 0.7 : 1.5; context.strokeStyle = layerColor(layer); drawPreparedLayer(context, prepared, view, "dot"); }); // 상류 세류선 강조 — 유역 채움 위, 흐름 화살표 아래. B04와 같은 공용 렌더러. if (scene.showUpstream && normalizer && scene.upstreamLines.length > 0) { drawUpstreamLines( context, scene.upstreamLines as Array>, normalizer, view, ); } if (scene.routeLayer) { context.lineWidth = ROUTE_LINE_WIDTH; context.strokeStyle = routeLineColor(); drawPreparedLayer(context, scene.routeLayer, view, "dot"); } if (!meta) { scene.pipeEditor.draw(context, view, scene.pipeColor); drawBadges(context, badges); return; } const projector = createMetricProjector(meta, view); // 구간형 구조물 띠 — 계획선 바로 위, 강도 색칠 아래. 종단 레인의 띠와 같은 뜻이다. if (scene.intervalSpans.length > 0) { drawRouteSpans(context, scene.strengthSamples, scene.intervalSpans, projector.toScreen); } // 유입 강도 색칠 — 계획선 위, 배관 마커 아래. 색띠는 B04 지도와 공용이다. if (scene.showStrength && scene.strength.length > 0) { drawStrengthLine(context, scene.strengthSamples, scene.strength, scene.maxStrength, (point) => projector.toScreen(point.x, point.y), ); } // 평균 흐름 화살표 — 좌표는 사업지 CRS(m)라 도엽 메타로 바로 화면에 옮긴다. if (scene.showArrows && scene.flowArrows.length > 0) { drawFlowArrows( context, scene.flowArrows as FlowArrow[], scene.arrowSpacingM, projector.pxPerMeter, projector.toScreen, view, ); } // 유입 집중점 마커 — 강도 색칠 위, 배관 마커 아래. if (scene.showHotspots && scene.hotspots.length > 0) { drawHotspots( context, projector.toScreen, scene.strengthSamples, scene.hotspots, scene.maxHotspotArea, ); } // 배관(관 매설) 마커 — 계획선 위. scene.pipeEditor.draw(context, view, scene.pipeColor); // 측점 선택 마킹 — 계획선 위 다이아몬드 표식(2026-08-05 사용자 지시. 3자 선택 동기화의 // 배수유역도 쪽 표현. 유역이 없는 구조물 측점도 위치가 보여야 한다). if (scene.markedChainage !== null && scene.strengthSamples.length > 0) { const index = Math.min( scene.strengthSamples.length - 1, Math.max(0, Math.round(scene.markedChainage)), ); const sample = scene.strengthSamples[index]; const [x, y] = projector.toScreen(sample.x, sample.y); context.save(); context.translate(x, y); context.rotate(Math.PI / 4); const half = 7; context.beginPath(); context.rect(-half, -half, half * 2, half * 2); context.fillStyle = "rgba(255, 196, 0, 0.35)"; context.fill(); context.lineWidth = 2.5; context.strokeStyle = "#ff9800"; context.stroke(); context.restore(); } // 측점 눈금·번호 — 관 마커 위, 유역 번호 아래 (2026-09-04 사용자 지시). B04 지도와 공용. drawStationTicks(context, scene.strengthSamples, { intervalM: scene.stationIntervalM, pxPerMeter: projector.pxPerMeter, toScreen: projector.toScreen, avoidChainages: scene.pipeEditor.chainages(), }); // 유역 번호 — 무엇에도 가리지 않게 맨 마지막. drawBadges(context, badges); } function drawBadges( context: CanvasRenderingContext2D, badges: ReadonlyArray<{ center: [number, number]; color: string; label: string }>, ): void { badges.forEach(({ center, color, label }) => drawRingBadge(context, center, color, label)); }