Files
Aislo/B05_Profile/B05_Profile_UI_Drainage_Render.ts
eomsangdonandClaude Opus 5 f659aec8a1 feat(B05): 평면(지도)에도 구간형 구조물을 띠로 — 종단과 같은 자료
사용자 지시(2026-09-07). 계획서 3-6 마지막 항목 — 산마루측구·도수로·옹벽처럼 **구간**으로
놓이는 시설이 종단에는 띠로 보이는데 **평면(배수유역도)에는 아무 표시가 없었음**.

**「부품이 없어 새로 짜야 함」이 사실이 아니었음** — 계획선 표본(`strengthSamples`)이 **1m 간격**
이라 **배열 인덱스가 곧 누가거리**임(`resampleRoute`). 구간 → 화면 선은 그 토막을 잘라 굵게
긋기만 하면 됨. 새 파일 하나(88줄)로 끝남.

- `B05_Profile_UI_Drainage_Spans.ts` — 띠 그리기(`drawRouteSpans`)와 자료 만들기
  (`routeSpansFromStructures`). 구간형만 띠가 되고, 색은 **레지스트리 표시색**을 그대로 씀.
- 그리는 자리 — **계획선 바로 위, 강도 색칠·마커 아래**. 굵기 7px·투명도 0.45 라 계획선을
  덮지 않음. 이름은 안 적음(종단에 이미 있고, 지도 글자는 눈금·유역 번호와 겹침).
- 목록이 바뀌면 따라옴 — `setStructures`·`setStructureTypes` 가 띠를 다시 넘김.
- 저장분에 노선보다 긴 구간이 남아 있어도 잘라서 그리고, 시작=끝인 구간도 한 칸은 보임.

⚠ 프로필 패널이 이미 700줄을 넘겨 있어(843) **매핑 로직은 띠 모듈로 뺐음** — 패널에는 3줄만
남김(827줄). 그 파일의 초과는 이전부터임.

시험 5건 — 부품 존재 · 범위 밖 구간 안전 · **쌓는 순서**(계획선 → 띠 → 강도 색칠) ·
종단과 같은 자료에서 뽑는지 · 목록 변화가 따라오는지.

⚠ 실화면 확인은 아직임 — 공용 브라우저가 소단 무한루프(별건, `80db17d4` 로 고침) 로 멈춰 있어
사용자 새로고침 대기 중임.

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

212 lines
8.4 KiB
TypeScript

/* =============================================================================
* 배수유역도 캔버스 렌더러 (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<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;
/** 선택된 측점의 누가거리(m). 계획선 위 그 자리에 선택 표식을 그린다(null=없음). */
markedChainage: number | null;
/** 규칙 측점 간격(m) — 눈금·번호 표기 기준 (2026-09-04 사용자 지시). */
stationIntervalM: number;
/** 구간형 구조물이 놓인 자리 — 계획선 위에 띠로 얹는다(계획서 3-6). 없으면 빈 배열. */
intervalSpans: ReadonlyArray<RouteSpanBand>;
}
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<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);
// 구간형 구조물 띠 — 계획선 바로 위, 강도 색칠 아래. 종단 레인의 띠와 같은 뜻이다.
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));
}