feat(B04): 세부유역 경계를 링 목록(외곽+구멍)으로 넓혀 중첩·빈공간 해소
단일 링 자료형이 곧 중첩·빈공간이었다. 아래 유역이 위 유역을 감싸면 구멍이 사라져 위 유역을 통째로 덮고(합성 실측 256㎡ 전량), 한 관의 유역이 두 조각이면 작은 쪽이 사라져 빈공간이 됐다(144㎡). - `polygon_parts()` 신설 — 조각마다 [외곽 링, 구멍 링...], 넓은 조각부터. - `WatershedBasin.boundary_parts` 로 교체. `boundary_xy`(가장 넓은 조각의 외곽)와 `boundary_rings`(편 링 목록)는 파생 속성이라 옛 소비처는 그대로 동작한다. - API 에 `polygon_rings_lonlat` 추가. 저장 GeoJSON 은 구멍을 가진 Polygon, 조각이 여럿이면 MultiPolygon. - 캔버스 4곳(B04 유역화면 채움·선택, B05 배수유역도 채움·선택)은 even-odd 로 한 번에 채우고 `pointInRings()` 로 판정 — 구멍 안을 눌러도 바깥 유역이 잡히지 않는다. 중복이던 지역 `pointInRing` 은 삭제하고 공용 것으로 통일. 검증: 단위 5건 신규(도넛 구멍 보존·조각 2개 보존·파생 속성·빈 경계·조립 경로 전체에서 링 2개), 전체 168 passed, typecheck 통과. 브라우저 — API 가 유역 22개에 링 목록을 실어 보내고, 캔버스 even-odd 실측(가운데 알파 0 / 고리 255)으로 구멍이 실제로 뚫린다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
* 확정 전에 화면을 떠나면 저장된 값으로 되돌아온다.
|
||||
* ========================================================================== */
|
||||
|
||||
import { pointInRings } from "./B04_PreProcess_UI_MapOverlays";
|
||||
import { createMapContextMenu } from "@ui/ui_template_context_menu";
|
||||
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
|
||||
import { themeColor } from "@ui/ui_template_palette";
|
||||
@@ -71,17 +72,6 @@ const BASIN_ALPHA_PLAIN = 0.22;
|
||||
const BASIN_ALPHA_SELECTED = 0.38;
|
||||
const BASIN_ALPHA_MUTED = 0.06;
|
||||
|
||||
/** 화면 좌표 폴리곤 안에 점이 있는지(홀짝 규칙). 유역을 눌러 고를 때 쓴다. */
|
||||
function pointInRing(ring: ReadonlyArray<[number, number]>, x: number, y: number): boolean {
|
||||
let inside = false;
|
||||
for (let index = 0, previous = ring.length - 1; index < ring.length; previous = index++) {
|
||||
const [xi, yi] = ring[index];
|
||||
const [xj, yj] = ring[previous];
|
||||
if (yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi) inside = !inside;
|
||||
}
|
||||
return inside;
|
||||
}
|
||||
|
||||
interface PipeMarker {
|
||||
chainage: number;
|
||||
source: PipeSource;
|
||||
@@ -289,10 +279,11 @@ export function createDetailBasinOverlay(onChange: () => void): DetailBasinOverl
|
||||
let smallest = Number.POSITIVE_INFINITY;
|
||||
basins.forEach((basin) => {
|
||||
if (basin.polygon_lonlat.length < 3) return;
|
||||
const ring = basin.polygon_lonlat.map(([lon, lat]) =>
|
||||
lonLatToScreen(normalizerRef as Normalizer, view, lon, lat),
|
||||
// 구멍 안(= 안에 든 다른 유역)을 누르면 바깥 유역이 잡히지 않는다.
|
||||
const rings = (basin.polygon_rings_lonlat ?? [basin.polygon_lonlat]).map((ring) =>
|
||||
ring.map(([lon, lat]) => lonLatToScreen(normalizerRef as Normalizer, view, lon, lat)),
|
||||
);
|
||||
if (!pointInRing(ring, x, y)) return;
|
||||
if (!pointInRings(rings, x, y)) return;
|
||||
if (basin.area_m2 < smallest) {
|
||||
smallest = basin.area_m2;
|
||||
hit = basin.index;
|
||||
@@ -508,12 +499,18 @@ export function createDetailBasinOverlay(onChange: () => void): DetailBasinOverl
|
||||
const ring = basin.polygon_lonlat.map(([lon, lat]) =>
|
||||
lonLatToScreen(normalizer, view, lon, lat),
|
||||
);
|
||||
// 조각·구멍을 한 경로에 담아 even-odd로 채운다 — 구멍이 실제로 뚫린다.
|
||||
const rings = (basin.polygon_rings_lonlat ?? [basin.polygon_lonlat]).map((part) =>
|
||||
part.map(([lon, lat]) => lonLatToScreen(normalizer, view, lon, lat)),
|
||||
);
|
||||
context.beginPath();
|
||||
ring.forEach(([px, py], order) => {
|
||||
if (order === 0) context.moveTo(px, py);
|
||||
else context.lineTo(px, py);
|
||||
rings.forEach((part) => {
|
||||
part.forEach(([px, py], order) => {
|
||||
if (order === 0) context.moveTo(px, py);
|
||||
else context.lineTo(px, py);
|
||||
});
|
||||
context.closePath();
|
||||
});
|
||||
context.closePath();
|
||||
// 하나를 고르면 나머지는 옅게 물러난다 — 고른 유역의 경계를 눈으로 좇을 수 있게.
|
||||
const muted = selectedBasin !== null && selectedBasin !== basin.index;
|
||||
const alpha = muted
|
||||
@@ -522,7 +519,7 @@ export function createDetailBasinOverlay(onChange: () => void): DetailBasinOverl
|
||||
? BASIN_ALPHA_SELECTED
|
||||
: BASIN_ALPHA_PLAIN;
|
||||
context.fillStyle = basinColor(index, alpha);
|
||||
context.fill();
|
||||
context.fill("evenodd");
|
||||
context.strokeStyle = basinColor(index, muted ? 0.3 : 0.95);
|
||||
context.lineWidth = selectedBasin === basin.index ? 2.8 : 1.8;
|
||||
context.stroke();
|
||||
|
||||
Reference in New Issue
Block a user