Files
Aislo/B05_wf2_Route/B05_wf2_Route_UI_Drainage_Render.ts
T
eomsangdonandClaude Opus 5 a7f09a827e fix(B04/B05): 세부유역 서클 번호를 최상단 오버레이로
채움과 함께 그리던 번호가 등고선·흐름 화살표·관 마커에 가려 읽히지 않았다.
번호만 떼어내 프레임 맨 마지막에 얹는다.

- MapRender에 drawRingBadge()·ringCenterOnScreen()을 분리 노출.
  drawFilledRing은 라벨이 있을 때 drawRingBadge를 부르는 형태로 바뀐다.
- B04: drawBasinNumbers() 호출을 관 마커 뒤로 이동(관이 없어도 번호는 그린다).
- B05: 채움은 라벨 없이 그리고 배지 자리만 모아 두었다가 배관 마커 뒤에 그린다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 00:00:44 +09:00

169 lines
6.3 KiB
TypeScript

/* =============================================================================
* 배수유역도 캔버스 렌더러 (B05)
*
* 한 프레임에 무엇을 어떤 순서로 그릴지만 담는다. 상태는 갖지 않고 패널이 매 프레임 넘긴
* 묶음(`DrainageScene`)만 본다 — 패널 본체(`_UI_Drainage_Panel.ts`)가 700줄 한계에 닿아
* 분리했다.
*
* 쌓는 순서(아래 → 위)
* 세부유역 채움 · 전체 유역 외곽선 → 도엽 레이어 → 상류 세류 → 계획선 →
* 유입 강도 색칠 → 평균 흐름 화살표 → 유입 집중점 → 배관 마커
* ========================================================================== */
import {
drawFilledRing,
drawRingBadge,
ringCenterOnScreen,
drawPreparedLayer,
drawRidgeRing,
drawUpstreamLines,
routeLineColor,
ROUTE_LINE_WIDTH,
type Normalizer,
type PreparedLayer,
type ViewState,
} from "../B04_wf1_Surface/B04_wf1_Surface_UI_MapRender";
import type { DetailBasin, VWorldMeta } from "../B04_wf1_Surface/B04_wf1_Surface_Api_Fetch";
import { drawFlowArrows, type FlowArrow } from "../B04_wf1_Surface/B04_wf1_Surface_UI_FlowArrows";
import { drawStrengthLine } from "../B04_wf1_Surface/B04_wf1_Surface_UI_FlowRamp";
import type { RoutePoint } from "../B04_wf1_Surface/B04_wf1_Surface_UI_RouteSamples";
import {
basinColor,
createMetricProjector,
drawHotspots,
DRAINAGE_LAYERS,
layerColor,
type DrainageLayer,
} from "./B05_wf2_Route_UI_Drainage_Parts";
import type { PipeEditor } from "./B05_wf2_Route_UI_Drainage_Pipes";
export interface DrainageScene {
meta: VWorldMeta | null;
normalizer: Normalizer | null;
basins: ReadonlyArray<DetailBasin>;
selectedBasin: number | null;
mainBoundary: ReadonlyArray<[number, number]>;
preparedLayers: ReadonlyMap<DrainageLayer, PreparedLayer>;
activeLayers: ReadonlySet<DrainageLayer>;
routeLayer: PreparedLayer | null;
upstreamLines: ReadonlyArray<Array<[number, number]>>;
showUpstream: boolean;
strengthSamples: ReadonlyArray<RoutePoint>;
strength: Float64Array;
maxStrength: number;
showStrength: boolean;
flowArrows: ReadonlyArray<FlowArrow>;
arrowSpacingM: number;
showArrows: boolean;
hotspots: ReadonlyArray<{ chainage: number; area: number }>;
maxHotspotArea: number;
showHotspots: boolean;
pipeEditor: PipeEditor;
pipeColor: (chainage: number, position: number) => string;
}
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 },
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<Array<[number, number]>>,
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);
// 유입 강도 색칠 — 계획선 위, 배관 마커 아래. 색띠는 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);
// 유역 번호 — 무엇에도 가리지 않게 맨 마지막.
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));
}