fix(B04/B05): 세부유역 서클 번호를 최상단 오버레이로

채움과 함께 그리던 번호가 등고선·흐름 화살표·관 마커에 가려 읽히지 않았다.
번호만 떼어내 프레임 맨 마지막에 얹는다.

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-02 00:00:44 +09:00
co-authored by Claude Opus 5
parent 6d13b3b781
commit a7f09a827e
3 changed files with 66 additions and 9 deletions
+9 -3
View File
@@ -495,10 +495,12 @@ export function createDetailBasinOverlay(onChange: () => void): DetailBasinOverl
draw(context, normalizer, view) { draw(context, normalizer, view) {
normalizerRef = normalizer; normalizerRef = normalizer;
if (!shown) return; if (!shown) return;
// 유역 번호는 **맨 마지막에** 얹는다 — 채움과 함께 그리면 관 마커에 가려진다
// (2026-08-01 사용자 지시). 자리는 채움을 그리면서 같이 모아 둔다.
const labels: Array<{ index: number; number: number; point: [number, number] }> = [];
if (normalizer && basins.length > 0) { if (normalizer && basins.length > 0) {
// 유역 번호를 얹을 자리는 폴리곤을 그리면서 같이 모은다 — 화면 좌표를 두 번 계산하지 // 유역 번호를 얹을 자리는 폴리곤을 그리면서 같이 모은다 — 화면 좌표를 두 번 계산하지
// 않는다. 번호는 채움 위에 한꺼번에 얹어야 이웃 유역 채움에 덮이지 않는다. // 않는다. 번호는 채움 위에 한꺼번에 얹어야 이웃 유역 채움에 덮이지 않는다.
const labels: Array<{ index: number; number: number; point: [number, number] }> = [];
context.save(); context.save();
context.lineJoin = "round"; context.lineJoin = "round";
basins.forEach((basin, index) => { basins.forEach((basin, index) => {
@@ -527,9 +529,11 @@ export function createDetailBasinOverlay(onChange: () => void): DetailBasinOverl
if (!muted) labels.push({ index, number: basin.index, point: ringCentroid(ring) }); if (!muted) labels.push({ index, number: basin.index, point: ringCentroid(ring) });
}); });
context.restore(); context.restore();
drawBasinNumbers(context, labels);
} }
if (pipes.length === 0) return; if (pipes.length === 0) {
drawBasinNumbers(context, labels);
return;
}
context.save(); context.save();
context.textAlign = "center"; context.textAlign = "center";
context.textBaseline = "middle"; context.textBaseline = "middle";
@@ -550,6 +554,8 @@ export function createDetailBasinOverlay(onChange: () => void): DetailBasinOverl
context.fillText(String(index + 1), x, y); context.fillText(String(index + 1), x, y);
}); });
context.restore(); context.restore();
// 유역 번호는 맨 마지막 — 관 마커에도 가리지 않게 한다(2026-08-01 사용자 지시).
drawBasinNumbers(context, labels);
}, },
dispose() { dispose() {
requestSequence += 1; requestSequence += 1;
@@ -604,9 +604,21 @@ export function drawFilledRing(
context.lineWidth = 1.6; context.lineWidth = 1.6;
context.stroke(); context.stroke();
if (!entry.label) return; if (!entry.label) return;
// 면적 중심(정점 평균)에 번호를 원형 배지로 얹는다. drawRingBadge(context, [sumX / entry.ring.length, sumY / entry.ring.length], color, entry.label);
const centerX = sumX / entry.ring.length; }
const centerY = sumY / entry.ring.length;
/** 유역 번호 배지 한 개(원 + 숫자). 화면 좌표를 그대로 받는다.
*
* 채움과 따로 떼어 둔 이유: 번호는 **맨 위에** 있어야 한다. 채움과 함께 그리면 그 위에
* 얹히는 등고선·화살표·관 마커에 가려 읽을 수 없다(2026-08-01 사용자 지시). */
export function drawRingBadge(
context: CanvasRenderingContext2D,
center: readonly [number, number],
color: string,
label: string,
): void {
const [centerX, centerY] = center;
context.save();
context.beginPath(); context.beginPath();
context.arc(centerX, centerY, 11, 0, Math.PI * 2); context.arc(centerX, centerY, 11, 0, Math.PI * 2);
context.fillStyle = color; context.fillStyle = color;
@@ -618,7 +630,24 @@ export function drawFilledRing(
context.textAlign = "center"; context.textAlign = "center";
context.textBaseline = "middle"; context.textBaseline = "middle";
context.fillStyle = themeColor("--map-label-text", "#1f2937"); context.fillStyle = themeColor("--map-label-text", "#1f2937");
context.fillText(entry.label, centerX, centerY); context.fillText(label, centerX, centerY);
context.restore();
}
/** 폴리곤 정점 평균의 화면 좌표 — 배지를 얹을 자리. */
export function ringCenterOnScreen(
ring: ReadonlyArray<readonly [number, number]>,
normalizer: Normalizer,
view: ViewState,
): [number, number] {
const affine = affineOf(view);
let sumX = 0;
let sumY = 0;
ring.forEach(([lon, lat]) => {
sumX += ((lon - normalizer.lonMin) / normalizer.lonRange) * affine.ax + affine.bx;
sumY += (1 - (lat - normalizer.latMin) / normalizer.latRange) * affine.ay + affine.by;
});
return [sumX / ring.length, sumY / ring.length];
} }
/** 상류 세류망 강조 — B04 분석 오버레이와 B05 배수유역도가 같은 굵기·색으로 그린다. */ /** 상류 세류망 강조 — B04 분석 오버레이와 B05 배수유역도가 같은 굵기·색으로 그린다. */
@@ -12,6 +12,8 @@
import { import {
drawFilledRing, drawFilledRing,
drawRingBadge,
ringCenterOnScreen,
drawPreparedLayer, drawPreparedLayer,
drawRidgeRing, drawRidgeRing,
drawUpstreamLines, drawUpstreamLines,
@@ -66,13 +68,23 @@ export function drawDrainageScene(
scene: DrainageScene, scene: DrainageScene,
): void { ): void {
const { meta, normalizer } = scene; const { meta, normalizer } = scene;
// 유역 번호는 **맨 위에** 얹는다 — 채움과 함께 그리면 등고선·화살표·관 마커에 가려진다
// (2026-08-01 사용자 지시). 자리는 채움을 그리면서 같이 모아 둔다.
const badges: Array<{ center: [number, number]; color: string; label: string }> = [];
// 세부유역 채움을 가장 아래에 깔아 등고선·세류 판독을 가리지 않게 한다. // 세부유역 채움을 가장 아래에 깔아 등고선·세류 판독을 가리지 않게 한다.
if (normalizer) { if (normalizer) {
scene.basins.forEach((basin) => { scene.basins.forEach((basin) => {
const color = basinColor(basin.index); 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( drawFilledRing(
context, context,
{ ring: basin.polygon_lonlat, label: String(basin.index) }, { ring: basin.polygon_lonlat },
normalizer, normalizer,
view, view,
// 하나를 고르면 나머지는 옅게 물러난다. // 하나를 고르면 나머지는 옅게 물러난다.
@@ -111,6 +123,7 @@ export function drawDrainageScene(
} }
if (!meta) { if (!meta) {
scene.pipeEditor.draw(context, view, scene.pipeColor); scene.pipeEditor.draw(context, view, scene.pipeColor);
drawBadges(context, badges);
return; return;
} }
const projector = createMetricProjector(meta, view); const projector = createMetricProjector(meta, view);
@@ -141,6 +154,15 @@ export function drawDrainageScene(
scene.maxHotspotArea, scene.maxHotspotArea,
); );
} }
// 배관(관 매설) 마커 — 계획선 위 최상단. // 배관(관 매설) 마커 — 계획선 위.
scene.pipeEditor.draw(context, view, scene.pipeColor); 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));
} }