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>
This commit is contained in:
@@ -31,6 +31,7 @@ import { createFacilityStore } from "./B05_Profile_UI_Drainage_Facility";
|
||||
import { writePendingPipes } from "./B05_Profile_Api_Pipes_Draft";
|
||||
import { createDrainageChrome } from "./B05_Profile_UI_Drainage_Chrome";
|
||||
import { bindDrainageInteractions } from "./B05_Profile_UI_Drainage_Interact";
|
||||
import type { RouteSpanBand } from "./B05_Profile_UI_Drainage_Spans";
|
||||
import { drawDrainageScene } from "./B05_Profile_UI_Drainage_Render";
|
||||
import {
|
||||
mountDrainageToggles,
|
||||
@@ -121,6 +122,9 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra
|
||||
let selectedBasin: number | null = null;
|
||||
// 측점 선택 마킹(계획선 위 누가거리). 유역이 없는 구조물 측점도 위치를 보여 준다.
|
||||
let markedChainage: number | null = null;
|
||||
// 구간형 구조물이 놓인 자리 — 계획선 위에 띠로 얹는다(계획서 3-6). 밖(종단 패널)이
|
||||
// 구조물 목록을 받을 때마다 넣어 준다. 여기서는 그리기만 하고 판정하지 않는다.
|
||||
let intervalSpans: ReadonlyArray<RouteSpanBand> = [];
|
||||
// 마지막으로 밖에 알린 관 선택(누가거리) — 같은 값 재알림을 막는다.
|
||||
let lastNotifiedPipeChainage: number | null = null;
|
||||
// 배관 편집기 — 마커 선택/추가/이동/삭제 시 재그리기와 버튼 상태만 갱신한다.
|
||||
@@ -255,6 +259,7 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra
|
||||
pipeColor,
|
||||
markedChainage,
|
||||
stationIntervalM: MAP_STATION_INTERVAL_M,
|
||||
intervalSpans,
|
||||
});
|
||||
updateImageTransform();
|
||||
}
|
||||
@@ -590,6 +595,10 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra
|
||||
markedChainage = chainageM;
|
||||
scheduleDraw();
|
||||
},
|
||||
setIntervalSpans(spans) {
|
||||
intervalSpans = spans;
|
||||
scheduleDraw();
|
||||
},
|
||||
addPipe(chainageM, attributes) {
|
||||
// 시설 정보를 먼저 보관해야 addAtChainage가 촉발하는 재계산 요청에 실려 간다.
|
||||
facilityStore.set(chainageM, attributes ?? null);
|
||||
|
||||
@@ -11,6 +11,7 @@ import type { RoutePoint } from "./B05_Profile_Api_Fetch";
|
||||
import type { FacilityAttributes } from "./B05_Profile_UI_Drainage_Facility";
|
||||
import type { PipeFacility, PipeSource } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
||||
import type { MapContextMenuItem } from "@ui/ui_template_context_menu";
|
||||
import type { RouteSpanBand } from "./B05_Profile_UI_Drainage_Spans";
|
||||
|
||||
export interface DrainagePanel {
|
||||
root: HTMLElement;
|
||||
@@ -44,6 +45,9 @@ export interface DrainagePanel {
|
||||
selectPipeAtChainage: (chainageM: number | null) => void;
|
||||
/** 측점 선택 마킹 — 계획선 위 해당 누가거리에 표식을 그린다(null이면 지움). */
|
||||
markStation: (chainageM: number | null) => void;
|
||||
/** 구간형 구조물이 놓인 자리 — 계획선 위에 띠로 얹는다(계획서 3-6).
|
||||
* 종단 레인의 띠와 같은 뜻이고, 목록이 바뀔 때마다 통째로 넣어 준다. */
|
||||
setIntervalSpans: (spans: ReadonlyArray<RouteSpanBand>) => void;
|
||||
dispose: () => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
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;
|
||||
@@ -67,6 +68,8 @@ export interface DrainageScene {
|
||||
markedChainage: number | null;
|
||||
/** 규칙 측점 간격(m) — 눈금·번호 표기 기준 (2026-09-04 사용자 지시). */
|
||||
stationIntervalM: number;
|
||||
/** 구간형 구조물이 놓인 자리 — 계획선 위에 띠로 얹는다(계획서 3-6). 없으면 빈 배열. */
|
||||
intervalSpans: ReadonlyArray<RouteSpanBand>;
|
||||
}
|
||||
|
||||
export function drawDrainageScene(
|
||||
@@ -134,6 +137,10 @@ export function drawDrainageScene(
|
||||
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) =>
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
/* =============================================================================
|
||||
* B05_Profile_UI_Drainage_Spans.ts
|
||||
* 배수유역도(평면)에서 **구간형 구조물이 놓인 자리**를 계획선 위에 띠로 그린다.
|
||||
*
|
||||
* 왜 (계획서 3-6) — 산마루측구·도수로·옹벽처럼 **구간**으로 놓이는 시설은 종단에는 띠로
|
||||
* 보이는데 평면에는 아무 표시가 없었다. 「노선 위에 임의 구간을 얹는 부품이 없다」가
|
||||
* 그동안의 걸림돌이었는데, 계획선 표본(`strengthSamples`)이 **1m 간격이라 배열 인덱스가
|
||||
* 곧 누가거리**여서 구간 → 화면 선은 잘라 붙이기만 하면 된다.
|
||||
*
|
||||
* 그리는 자리 — **계획선 바로 위, 강도 색칠·마커 아래**. 굵고 반투명해서 계획선을 덮지
|
||||
* 않는다. 이름은 안 적는다(종단에 이미 있고, 지도에 글자를 얹으면 눈금·유역 번호와 겹친다).
|
||||
* ========================================================================== */
|
||||
|
||||
import type { RoutePoint } from "../B04_PreProcess/B04_PreProcess_UI_RouteSamples";
|
||||
import type { StructureInstance, StructureType } from "./B05_Profile_Api_Structures";
|
||||
|
||||
/** 계획선 위에 얹을 구간 하나 — 누가거리 두 값과 색. */
|
||||
export interface RouteSpanBand {
|
||||
startM: number;
|
||||
endM: number;
|
||||
/** 구조물 레지스트리의 표시색(`style.color`). */
|
||||
color: string;
|
||||
}
|
||||
|
||||
/** 띠 굵기(px) — 계획선(2px 안팎)보다 확실히 굵되 유역 채움을 가리지 않는 값. */
|
||||
const BAND_WIDTH_PX = 7;
|
||||
|
||||
/**
|
||||
* 구간 띠를 그린다. 표본이 없거나 구간이 비면 아무 것도 하지 않는다.
|
||||
*
|
||||
* 인덱스는 **누가거리(m)** 다 — 표본이 1m 간격이라 그렇다(`resampleRoute`). 범위를 벗어난
|
||||
* 값은 잘라 쓰고, 한 점짜리 구간(시작=끝)은 짧은 토막으로라도 보이게 한 칸을 준다.
|
||||
*/
|
||||
export function drawRouteSpans(
|
||||
context: CanvasRenderingContext2D,
|
||||
samples: ReadonlyArray<RoutePoint>,
|
||||
spans: ReadonlyArray<RouteSpanBand>,
|
||||
toScreen: (x: number, y: number) => [number, number],
|
||||
): void {
|
||||
if (samples.length < 2 || spans.length === 0) return;
|
||||
const last = samples.length - 1;
|
||||
context.save();
|
||||
context.lineCap = "round";
|
||||
context.lineJoin = "round";
|
||||
context.lineWidth = BAND_WIDTH_PX;
|
||||
for (const span of spans) {
|
||||
const from = Math.max(0, Math.min(last, Math.floor(Math.min(span.startM, span.endM))));
|
||||
const to = Math.max(from + 1, Math.min(last, Math.ceil(Math.max(span.startM, span.endM))));
|
||||
context.beginPath();
|
||||
for (let index = from; index <= to; index += 1) {
|
||||
const [x, y] = toScreen(samples[index].x, samples[index].y);
|
||||
if (index === from) context.moveTo(x, y);
|
||||
else context.lineTo(x, y);
|
||||
}
|
||||
context.strokeStyle = span.color;
|
||||
context.globalAlpha = 0.45;
|
||||
context.stroke();
|
||||
context.globalAlpha = 1;
|
||||
}
|
||||
context.restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* 구조물 정본 + 타입 레지스트리 → 띠 목록. **종단 레인과 같은 자료**를 본다(계획서 3-6).
|
||||
*
|
||||
* · 구간형(`interval`)만 띠가 된다 — 점형 시설은 마커로 이미 보인다.
|
||||
* · 색은 레지스트리 표시색을 그대로 쓴다(여기서 새로 정하지 않는다).
|
||||
* · 시작·끝이 없으면 기준 측점으로 대신한다. 그것도 없으면 뺀다.
|
||||
*/
|
||||
export function routeSpansFromStructures(
|
||||
structures: ReadonlyArray<StructureInstance>,
|
||||
types: ReadonlyArray<StructureType>,
|
||||
): RouteSpanBand[] {
|
||||
const colorOf = new Map(types.map((type) => [type.type_id, type.style?.color]));
|
||||
const spans: RouteSpanBand[] = [];
|
||||
for (const item of structures) {
|
||||
if (item.placement !== "interval") continue;
|
||||
const start = item.start_m ?? item.chainage_m ?? null;
|
||||
const end = item.end_m ?? item.chainage_m ?? null;
|
||||
if (start === null || end === null) continue;
|
||||
spans.push({
|
||||
startM: Math.min(start, end),
|
||||
endM: Math.max(start, end),
|
||||
color: colorOf.get(item.type_id) ?? "#8a8a8a",
|
||||
});
|
||||
}
|
||||
return spans;
|
||||
}
|
||||
@@ -10,6 +10,7 @@
|
||||
* `saveProfileAlignment()`로 편집 델타만 보낸다.
|
||||
* ========================================================================== */
|
||||
|
||||
import { routeSpansFromStructures } from "./B05_Profile_UI_Drainage_Spans";
|
||||
import { readStateRaw, stateKey, writeStateRaw } from "../A00_Common/b_page_state";
|
||||
import type { SectionDetailResponse } from "../B06_Section/B06_Section_Api_Fetch";
|
||||
import { createWorkflowPanelHandle } from "@ui/ui_template_overlay";
|
||||
@@ -655,6 +656,11 @@ export function createRouteProfilePanel(
|
||||
);
|
||||
setCollapsed(readStateRaw("profile-collapsed") === "true");
|
||||
|
||||
/** 배수유역도(평면)에 **구간형 구조물 띠**를 넘긴다 — 종단 레인과 같은 자료다(계획서 3-6). */
|
||||
function syncDrainageSpans(): void {
|
||||
drainagePanel.setIntervalSpans(routeSpansFromStructures(structures, structureTypes));
|
||||
}
|
||||
|
||||
return {
|
||||
root,
|
||||
render(nextDetail: SectionDetailResponse, nextStationInterval?: number, nextRouteId?: number) {
|
||||
@@ -747,6 +753,7 @@ export function createRouteProfilePanel(
|
||||
/** 구조물 타입 레지스트리를 받아 마크 색·약호·우클릭 메뉴에 쓴다(최초 1회). */
|
||||
setStructureTypes(types: StructureType[]) {
|
||||
structureTypes = types;
|
||||
syncDrainageSpans();
|
||||
draw();
|
||||
},
|
||||
/** 구조물 정본 목록을 반영해 그래프 서클마크를 다시 그린다. */
|
||||
@@ -755,6 +762,7 @@ export function createRouteProfilePanel(
|
||||
if (selectedStructureId && !next.some((s) => s.structure_id === selectedStructureId)) {
|
||||
selectedStructureId = null;
|
||||
}
|
||||
syncDrainageSpans();
|
||||
draw();
|
||||
},
|
||||
/** 사이드 목록에서 고른 구조물을 그래프 알약·측점 세로선 선택에 함께 맞춘다. */
|
||||
|
||||
Reference in New Issue
Block a user