auto: 2026-09-02 16:52 (ESD_LAPTOP)

This commit is contained in:
2026-09-02 16:52:38 +09:00
parent d93bd1cba8
commit 9b1c816992
6 changed files with 318 additions and 268 deletions
+14 -93
View File
@@ -39,106 +39,27 @@ import {
slerpLeft,
type PieceControl,
} from "./B05_Profile_UI_Corridor_Frames";
import {
buildCorridorStructures,
type CorridorStructure,
type RouteFrame,
} from "./B05_Profile_UI_Corridor_Structures";
import { buildCorridorStructures, type RouteFrame } from "./B05_Profile_UI_Corridor_Structures";
export type { CorridorStructure, StructureFrame } from "./B05_Profile_UI_Corridor_Structures";
import type {
CorridorBuildResult,
CorridorCap,
CorridorRibbon,
} from "./B05_Profile_UI_Corridor_Build_Types";
// 결과 자료형은 `_Build_Types` 에 있다(2026-09-02 분리) — 호출부 경로 유지를 위해 재수출.
export type {
CorridorBuildResult,
CorridorCap,
CorridorCutWall,
CorridorRibbon,
} from "./B05_Profile_UI_Corridor_Build_Types";
import { buildStructurePatchRibbons } from "./B05_Profile_UI_Corridor_Carve";
import { structureSilhouettes } from "./B05_Profile_UI_Corridor_Station_Structure";
import { buildPlanCurves, type PlanCurve } from "./B05_Profile_UI_Corridor_Plan";
import { buildPlanCurves } from "./B05_Profile_UI_Corridor_Plan";
import { maskFillByPlanCurves } from "./B05_Profile_UI_Corridor_Cut";
export type { PlanCurve } from "./B05_Profile_UI_Corridor_Plan";
/** 리본 하나 — rows[i]는 프레임 i의 단면 점(colCount개, 모델 x/y/z 평탄 배열). */
export interface CorridorRibbon {
kind: CorridorKind;
side: CorridorSide;
colCount: number;
chainages: number[];
/**
* 구조물 **패치 리본**(2026-08-25) — 투영커브로 잘린 자리에 다시 그리는 변형 성토면.
* 절단 대상에서 빠진다(패치까지 자르면 구멍이 도로 뚫린다).
*/
patch?: boolean;
/**
* 패치를 **절취 영역 안쪽으로 클립**한다(2026-08-27 사용자: 삐져나오지도 벌어지지도
* 않게). `maskFillByPlanCurves`가 기본 성토면을 지우는 규칙의 **정확한 반대**를 건다 —
* 영역 밖 셀은 지우고, 걸친 셀은 안쪽 조각만 남긴다. 두 면이 같은 경계를 공유하니
* 겹침도 구멍도 안 생긴다. **세월교 전용** — 구조물별로 로직이 다르다.
*/
patchClip?: boolean;
/**
* rowCount × colCount × 3 (모델 좌표).
*
* **Float64**다 — 모델 좌표는 EPSG 절대값(십만 m대)이라 Float32에 담으면 해상도가
* 0.14m까지 떨어져, 0.2m로 세분한 점들이 양자화 계단을 타며 최대 35°짜리 지그재그가
* 생겼다(2026-08-23 실측). 화면에 올릴 때 원점을 빼면서 Float32로 줄인다.
*/
positions: Float64Array;
/**
* 셀 마스크 — `(rowCount-1) × (colCount-1)`, 1이면 **그 셀은 안 그린다**
* (2026-08-26 투영 커브 안쪽 지우기). 정점은 그대로 두고 삼각형만 뺀다.
*/
cellMask?: Uint8Array;
/**
* 축 고정 커브(BOX암거)가 **걸친 셀**을 커브에서 잘라 되살린 조각 삼각형
* (모델 좌표, 삼각형당 9). 격자로는 못 담는 비스듬한 경계를 여기로 메운다
* (2026-08-26 사용자: 이원화하여 관리).
*/
trimTris?: Float64Array;
/** `trimTris`와 짝인 UV(삼각형당 6) — 빗금이 실척으로 이어지게 한다. */
trimUvs?: Float32Array;
/**
* 격자점별 **원지반 표고**(rowCount × colCount) — 성토 리본에만 싣는다.
* 잘린 자리 측벽을 성토면에서 원지반까지 세우는 밑선이다(2026-08-27 사용자).
*/
groundZ?: Float64Array;
}
/**
* 잘린 자리 **측벽 서피스**(2026-08-27 사용자) — 리본 메시에 섞지 않고 구조물처럼
* 따로 그린다. 삼각형 수프(정점당 3, 삼각형당 9). 빗금이 없어 UV는 담지 않는다.
*/
export interface CorridorCutWall {
side: "left" | "right";
positions: Float64Array;
/** 패치 바깥 끝 스커트인가(2026-08-27) — 뷰어가 지형 색인으로 뒤늦게 붙이므로,
* 다시 붙일 때 옛 것을 걷어내려면 표식이 있어야 한다(`_Corridor_Skirt.ts`). */
patchSkirt?: boolean;
/**
* 정점별 UV(정점당 2) — **빗금 있는 성토 서피스**로 그릴 때만 싣는다.
* 절취 측벽은 일반 서피스(빗금 없음)이고, 패치 스커트는 성토면의 연장이라 빗금을
* 넣는다(2026-08-27 사용자: "변형 성토면 하단 끝단부 서피스는 메시 형태").
*/
uvs?: Float32Array;
}
/** 시·종점 마구리(캡) — 설계선과 지반선 사이를 세로로 봉인하는 스트립(모델 좌표). */
export interface CorridorCap {
/** [x, y, z설계, z지반] × N — offset 오름차순. */
points: Array<[number, number, number, number]>;
}
export interface CorridorBuildResult {
ribbons: CorridorRibbon[];
/** 클리핑 경계 — 프레임별 좌/우 최외곽(catch) XY(모델). 좌우 같은 길이. */
outline: { chainages: number[]; left: Array<[number, number]>; right: Array<[number, number]> };
/** 노선 시·종점 단면 봉인(2026-08-23 품질 개선). 캡이 없으면 빈 배열. */
caps: CorridorCap[];
/** 배수관 세트 구조물(기슭막이·집수정·배관) 솔리드(2026-08-23). 없으면 빈 배열. */
structures: CorridorStructure[];
/**
* 구조물 세트별 **탑뷰 투영 커브**(2026-08-26 사용자) — 세트 구조물 상단 5m
* 평면에 눕힌 닫힌 고리들. 구조물·비탈(변형/원본)을 유입·유출로 나눠 담는다.
* 저장 코리도엔 없을 수 있다.
*/
planCurves?: PlanCurve[];
/** 절취 측벽 서피스(2026-08-27) — 저장본에도 담는다. 없으면 빈 배열. */
cutWalls?: CorridorCutWall[];
}
/**
* 구조물 구간 **성토 패치 리본** 적용 여부(2026-08-27 사용자). B06 횡단이 내는
* 변형 성토선을 3D 포켓에 되메운다 — 다단이면 `성토 > 구조물 > 성토`라 성토선이
@@ -0,0 +1,99 @@
/* =============================================================================
* B05_Profile_UI_Corridor_Build_Types.ts
* 코리도 빌더가 내는 **결과 자료형** — 리본·절취 측벽·캡·빌드 결과.
*
* `B05_Profile_UI_Corridor_Build` 에서 떼어냈다(700줄 제한, 2026-09-02). 계산은 전혀
* 없고 모양과 그 뜻(주석)만 있다. 빌더가 그대로 다시 내보내므로 기존 import 경로
* (`... from "./B05_Profile_UI_Corridor_Build"`)는 전부 유효하다.
* ========================================================================== */
import type { CorridorKind, CorridorSide } from "./B05_Profile_UI_Corridor_Station";
import type { CorridorStructure } from "./B05_Profile_UI_Corridor_Structures";
import type { PlanCurve } from "./B05_Profile_UI_Corridor_Plan";
export interface CorridorRibbon {
kind: CorridorKind;
side: CorridorSide;
colCount: number;
chainages: number[];
/**
* 구조물 **패치 리본**(2026-08-25) — 투영커브로 잘린 자리에 다시 그리는 변형 성토면.
* 절단 대상에서 빠진다(패치까지 자르면 구멍이 도로 뚫린다).
*/
patch?: boolean;
/**
* 패치를 **절취 영역 안쪽으로 클립**한다(2026-08-27 사용자: 삐져나오지도 벌어지지도
* 않게). `maskFillByPlanCurves`가 기본 성토면을 지우는 규칙의 **정확한 반대**를 건다 —
* 영역 밖 셀은 지우고, 걸친 셀은 안쪽 조각만 남긴다. 두 면이 같은 경계를 공유하니
* 겹침도 구멍도 안 생긴다. **세월교 전용** — 구조물별로 로직이 다르다.
*/
patchClip?: boolean;
/**
* rowCount × colCount × 3 (모델 좌표).
*
* **Float64**다 — 모델 좌표는 EPSG 절대값(십만 m대)이라 Float32에 담으면 해상도가
* 0.14m까지 떨어져, 0.2m로 세분한 점들이 양자화 계단을 타며 최대 35°짜리 지그재그가
* 생겼다(2026-08-23 실측). 화면에 올릴 때 원점을 빼면서 Float32로 줄인다.
*/
positions: Float64Array;
/**
* 셀 마스크 — `(rowCount-1) × (colCount-1)`, 1이면 **그 셀은 안 그린다**
* (2026-08-26 투영 커브 안쪽 지우기). 정점은 그대로 두고 삼각형만 뺀다.
*/
cellMask?: Uint8Array;
/**
* 축 고정 커브(BOX암거)가 **걸친 셀**을 커브에서 잘라 되살린 조각 삼각형
* (모델 좌표, 삼각형당 9). 격자로는 못 담는 비스듬한 경계를 여기로 메운다
* (2026-08-26 사용자: 이원화하여 관리).
*/
trimTris?: Float64Array;
/** `trimTris`와 짝인 UV(삼각형당 6) — 빗금이 실척으로 이어지게 한다. */
trimUvs?: Float32Array;
/**
* 격자점별 **원지반 표고**(rowCount × colCount) — 성토 리본에만 싣는다.
* 잘린 자리 측벽을 성토면에서 원지반까지 세우는 밑선이다(2026-08-27 사용자).
*/
groundZ?: Float64Array;
}
/**
* 잘린 자리 **측벽 서피스**(2026-08-27 사용자) — 리본 메시에 섞지 않고 구조물처럼
* 따로 그린다. 삼각형 수프(정점당 3, 삼각형당 9). 빗금이 없어 UV는 담지 않는다.
*/
export interface CorridorCutWall {
side: "left" | "right";
positions: Float64Array;
/** 패치 바깥 끝 스커트인가(2026-08-27) — 뷰어가 지형 색인으로 뒤늦게 붙이므로,
* 다시 붙일 때 옛 것을 걷어내려면 표식이 있어야 한다(`_Corridor_Skirt.ts`). */
patchSkirt?: boolean;
/**
* 정점별 UV(정점당 2) — **빗금 있는 성토 서피스**로 그릴 때만 싣는다.
* 절취 측벽은 일반 서피스(빗금 없음)이고, 패치 스커트는 성토면의 연장이라 빗금을
* 넣는다(2026-08-27 사용자: "변형 성토면 하단 끝단부 서피스는 메시 형태").
*/
uvs?: Float32Array;
}
/** 시·종점 마구리(캡) — 설계선과 지반선 사이를 세로로 봉인하는 스트립(모델 좌표). */
export interface CorridorCap {
/** [x, y, z설계, z지반] × N — offset 오름차순. */
points: Array<[number, number, number, number]>;
}
export interface CorridorBuildResult {
ribbons: CorridorRibbon[];
/** 클리핑 경계 — 프레임별 좌/우 최외곽(catch) XY(모델). 좌우 같은 길이. */
outline: { chainages: number[]; left: Array<[number, number]>; right: Array<[number, number]> };
/** 노선 시·종점 단면 봉인(2026-08-23 품질 개선). 캡이 없으면 빈 배열. */
caps: CorridorCap[];
/** 배수관 세트 구조물(기슭막이·집수정·배관) 솔리드(2026-08-23). 없으면 빈 배열. */
structures: CorridorStructure[];
/**
* 구조물 세트별 **탑뷰 투영 커브**(2026-08-26 사용자) — 세트 구조물 상단 5m
* 평면에 눕힌 닫힌 고리들. 구조물·비탈(변형/원본)을 유입·유출로 나눠 담는다.
* 저장 코리도엔 없을 수 있다.
*/
planCurves?: PlanCurve[];
/** 절취 측벽 서피스(2026-08-27) — 저장본에도 담는다. 없으면 빈 배열. */
cutWalls?: CorridorCutWall[];
}
@@ -20,6 +20,7 @@
* 폴리곤을 갈아 끼워(`polygons`) 측점 사이를 Catmull-Rom으로 잇는다. 벽은 여전히 하나다.
* ========================================================================== */
import { structureHashParts } from "./B05_Profile_UI_Corridor_Structures_Hash";
import type { CrossSection, CulvertSideSpec } from "../B06_Section/B06_Section_Api_Fetch";
import { computeCulvertLayout } from "../B06_Section/B06_Section_UI_Cross_Culvert";
import { restrictToSide, tierSpanOf } from "../B06_Section/B06_Section_UI_Cross_Culvert_Wire";
@@ -600,115 +601,5 @@ export function buildCorridorStructures(
return solids;
}
/** 해시 입력용 요약 — 구조물에 영향을 주는 값만 짧게 이어 붙인다(Corridor.ts가 쓴다). */
export function structureHashParts(section: CrossSection): Array<string | number> {
// 물넘이 파임·독립 기슭막이는 다른 세트와 함께 설 수 있으니 앞에 이어 붙인다 —
// 빼먹으면 값을 고쳐도 저장 코리도가 만료되지 않는다(2026-08-28).
const extras: Array<string | number> = [];
const fordPave = section.ford_pavement;
if (fordPave) {
extras.push("fp", fordPave.span_m, fordPave.depth_m ?? "", fordPave.slope_pct ?? "");
}
const ownRevet = section.revetment;
if (ownRevet) {
extras.push(
"rv",
ownRevet.start_m,
ownRevet.end_m,
ownRevet.anchor_m,
ownRevet.height_m ?? "",
ownRevet.side ?? "",
ownRevet.form ?? "",
ownRevet.tiers ?? "",
ownRevet.lift_m ?? "",
ownRevet.shift_m ?? "",
// 조정창 조작값이 바뀌면 저장 코리도를 만료시킨다.
JSON.stringify(section.design?.revet_adjust?.own ?? ""),
);
}
const box = section.box;
if (box) {
return [
...extras,
"bx",
box.inner_width_m,
box.inner_height_m,
box.span_m,
box.cover_m,
box.wing_in.slab_extend_m,
box.wing_in.height_m ?? "",
box.wing_out.slab_extend_m,
box.wing_out.height_m ?? "",
// 조작값이 정본에 실리므로 해시에도 넣는다 — 안 넣으면 저장 코리도가 안 만료된다.
...(["left", "right"] as const).map((side) => {
const value = section.design?.box_adjust?.[side];
return value ? `${side}:${value.lengthM},${value.riseM}` : "";
}),
];
}
const ford = section.ford;
if (ford) {
const adjust = section.design?.ford_adjust;
const wall = (role: "inlet" | "outlet"): string => {
const value = adjust?.[role];
return value ? `${value.heightM ?? ""},${value.lateralM},${value.slopeM}` : "";
};
return [
...extras,
"fd",
ford.diameter_m,
ford.pipe_count,
ford.span_m,
ford.wing_in.slab_extend_m,
ford.wing_out.slab_extend_m,
wall("inlet"),
wall("outlet"),
];
}
const culvert = section.culvert;
if (!culvert) return extras;
const side = (spec: CulvertSideSpec): string =>
[
spec.structure,
spec.revet_form ?? "",
spec.revet_height_m ?? "",
spec.revet_length_m ?? "",
spec.revet_before_m ?? "",
spec.revet_after_m ?? "",
spec.basin_length_m ?? "",
spec.basin_before_m ?? "",
spec.basin_after_m ?? "",
].join(",");
const basin = section.design?.basin_adjust;
const revet = section.design?.revet_adjust;
const counts = section.design?.extra_wall_counts;
return [
...extras,
"cv",
culvert.diameter_m,
culvert.pipe_kind ?? "",
side(culvert.inlet),
side(culvert.outlet),
section.design?.inlet_structure ?? "",
basin ? `${basin.innerWidthM},${basin.innerHeightM},${basin.lateralM},${basin.slopeM}` : "",
// 조작값이 정본에 실리므로 해시에도 넣는다 — 안 넣으면 저장 코리도가 안 만료된다.
revet
? Object.keys(revet)
.sort()
.map((role) => {
const value = revet[role];
return `${role}:${value.x},${value.d ?? ""},${value.h ?? ""},${value.m ?? ""}`;
})
.join("|")
: "",
counts ? `${counts.outlet},${counts.basin}` : "",
section.design?.revet_follow_grade === false ? "flatZ" : "",
// 다단 단별 구간값·연동 해제도 형상을 바꾼다 — 빠지면 저장 코리도가 안 만료돼
// 값만 바뀌고 3D는 옛 모양 그대로다(2026-08-30).
Object.entries(section.design?.extra_spans ?? {})
.sort(([a], [b]) => (a < b ? -1 : 1))
.map(([wall, span]) => `${wall}:${span.length_m},${span.before_m},${span.after_m}`)
.join("|"),
section.design?.revet_link_detached === true ? "detached" : "",
];
}
// 해시 요약은 `_Structures_Hash.ts` 에 있다(2026-09-02 분리) — 호출부 경로 유지를 위해 재수출.
export { structureHashParts } from "./B05_Profile_UI_Corridor_Structures_Hash";
@@ -0,0 +1,124 @@
/* =============================================================================
* B05_Profile_UI_Corridor_Structures_Hash.ts
* 저장 코리도 만료 판정용 **구조물 해시 요약**.
*
* `B05_Profile_UI_Corridor_Structures` 에서 떼어냈다(700줄 제한, 2026-09-02).
* 값이 바뀌면 저장본이 만료되도록 **구조물에 영향을 주는 값만** 이어 붙인다 —
* 빠뜨린 값은 곧 "고쳐도 3D가 안 바뀌는" 버그가 되므로 항목 추가에 주의할 것.
* 원래 파일이 그대로 다시 내보내므로 기존 import 경로는 유효하다.
* ========================================================================== */
import type { CrossSection, CulvertSideSpec } from "../B06_Section/B06_Section_Api_Fetch";
/** 해시 입력용 요약 — 구조물에 영향을 주는 값만 짧게 이어 붙인다(Corridor.ts가 쓴다). */
export function structureHashParts(section: CrossSection): Array<string | number> {
// 물넘이 파임·독립 기슭막이는 다른 세트와 함께 설 수 있으니 앞에 이어 붙인다 —
// 빼먹으면 값을 고쳐도 저장 코리도가 만료되지 않는다(2026-08-28).
const extras: Array<string | number> = [];
const fordPave = section.ford_pavement;
if (fordPave) {
extras.push("fp", fordPave.span_m, fordPave.depth_m ?? "", fordPave.slope_pct ?? "");
}
const ownRevet = section.revetment;
if (ownRevet) {
extras.push(
"rv",
ownRevet.start_m,
ownRevet.end_m,
ownRevet.anchor_m,
ownRevet.height_m ?? "",
ownRevet.side ?? "",
ownRevet.form ?? "",
ownRevet.tiers ?? "",
ownRevet.lift_m ?? "",
ownRevet.shift_m ?? "",
// 조정창 조작값이 바뀌면 저장 코리도를 만료시킨다.
JSON.stringify(section.design?.revet_adjust?.own ?? ""),
);
}
const box = section.box;
if (box) {
return [
...extras,
"bx",
box.inner_width_m,
box.inner_height_m,
box.span_m,
box.cover_m,
box.wing_in.slab_extend_m,
box.wing_in.height_m ?? "",
box.wing_out.slab_extend_m,
box.wing_out.height_m ?? "",
// 조작값이 정본에 실리므로 해시에도 넣는다 — 안 넣으면 저장 코리도가 안 만료된다.
...(["left", "right"] as const).map((side) => {
const value = section.design?.box_adjust?.[side];
return value ? `${side}:${value.lengthM},${value.riseM}` : "";
}),
];
}
const ford = section.ford;
if (ford) {
const adjust = section.design?.ford_adjust;
const wall = (role: "inlet" | "outlet"): string => {
const value = adjust?.[role];
return value ? `${value.heightM ?? ""},${value.lateralM},${value.slopeM}` : "";
};
return [
...extras,
"fd",
ford.diameter_m,
ford.pipe_count,
ford.span_m,
ford.wing_in.slab_extend_m,
ford.wing_out.slab_extend_m,
wall("inlet"),
wall("outlet"),
];
}
const culvert = section.culvert;
if (!culvert) return extras;
const side = (spec: CulvertSideSpec): string =>
[
spec.structure,
spec.revet_form ?? "",
spec.revet_height_m ?? "",
spec.revet_length_m ?? "",
spec.revet_before_m ?? "",
spec.revet_after_m ?? "",
spec.basin_length_m ?? "",
spec.basin_before_m ?? "",
spec.basin_after_m ?? "",
].join(",");
const basin = section.design?.basin_adjust;
const revet = section.design?.revet_adjust;
const counts = section.design?.extra_wall_counts;
return [
...extras,
"cv",
culvert.diameter_m,
culvert.pipe_kind ?? "",
side(culvert.inlet),
side(culvert.outlet),
section.design?.inlet_structure ?? "",
basin ? `${basin.innerWidthM},${basin.innerHeightM},${basin.lateralM},${basin.slopeM}` : "",
// 조작값이 정본에 실리므로 해시에도 넣는다 — 안 넣으면 저장 코리도가 안 만료된다.
revet
? Object.keys(revet)
.sort()
.map((role) => {
const value = revet[role];
return `${role}:${value.x},${value.d ?? ""},${value.h ?? ""},${value.m ?? ""}`;
})
.join("|")
: "",
counts ? `${counts.outlet},${counts.basin}` : "",
section.design?.revet_follow_grade === false ? "flatZ" : "",
// 다단 단별 구간값·연동 해제도 형상을 바꾼다 — 빠지면 저장 코리도가 안 만료돼
// 값만 바뀌고 3D는 옛 모양 그대로다(2026-08-30).
Object.entries(section.design?.extra_spans ?? {})
.sort(([a], [b]) => (a < b ? -1 : 1))
.map(([wall, span]) => `${wall}:${span.length_m},${span.before_m},${span.after_m}`)
.join("|"),
section.design?.revet_link_detached === true ? "detached" : "",
];
}
+4 -63
View File
@@ -1,5 +1,4 @@
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
import type { MapContextMenuItem } from "@ui/ui_template_context_menu";
import {
computeDetailBasins,
fetchDetailPipePoints,
@@ -8,7 +7,6 @@ import {
saveDetailPipePoints,
type DetailBasin,
type DetailBasinResponse,
type PipeFacility,
type PipeSource,
type VWorldMeta,
} from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
@@ -27,7 +25,7 @@ import type { FlowArrow } from "../B04_PreProcess/B04_PreProcess_UI_FlowArrows";
import { buildStrengthArray } from "../B04_PreProcess/B04_PreProcess_UI_FlowRamp";
import { resampleRoute } from "../B04_PreProcess/B04_PreProcess_UI_RouteSamples";
import { createPipeEditor } from "./B05_Profile_UI_Drainage_Pipes";
import { createFacilityStore, type FacilityAttributes } from "./B05_Profile_UI_Drainage_Facility";
import { createFacilityStore } from "./B05_Profile_UI_Drainage_Facility";
import { createDrainageChrome } from "./B05_Profile_UI_Drainage_Chrome";
import { bindDrainageInteractions } from "./B05_Profile_UI_Drainage_Interact";
import { drawDrainageScene } from "./B05_Profile_UI_Drainage_Render";
@@ -61,66 +59,9 @@ function L(key: keyof typeof ui_locales): string {
return ui_locales[key][currentLanguageIndex];
}
export interface DrainagePanel {
root: HTMLElement;
/** 프로젝트가 정해지면 배경지도·도엽 레이어를 불러온다. */
load: (projectId: string) => void;
/** 확정된 노선 평면 선형(사업지 좌표계 m)을 지도 위에 겹친다. */
setRoute: (points: ReadonlyArray<RoutePoint>) => void;
/** 현재 관 매설 누가거리 목록(종단 테이블의 "배관" 구조물 라인과 맞추는 데 쓴다). */
pipeChainages: () => number[];
/** 종단 테이블에서 배관 라인을 끌었을 때 — 그 자리로 옮기고 세부유역을 다시 나눈다. */
movePipe: (fromChainage: number, toChainage: number) => void;
/** 관 목록을 통째로 맞춘다(사이드바 구조물 폼 편집 등 밖에서 바뀐 경우).
* 현재 목록과 같으면 아무 일도 하지 않는다 — 되먹임 고리를 끊는 지점이다. */
setPipeChainages: (chainages: ReadonlyArray<number>) => void;
/** 종단 테이블 우클릭·통합 목록에서 계곡 통과 시설을 넣을 때.
* attributes에 시설 종류·구간·부속 옵션이 담긴다(생략 = 맨 배관). */
addPipe: (chainageM: number, attributes?: FacilityAttributes) => void;
/** 사이드 폼 [수정] — 기준점 이동·구간·부속 옵션을 정본에 반영하고 재계산한다. */
updatePipeFacility: (
fromChainageM: number,
toChainageM: number,
attributes: FacilityAttributes,
) => void;
removePipe: (chainageM: number) => void;
/** 경로 확정 시 관 매설 지점을 영구저장한다(B04 "모델 확정"과 같은 저장소). */
savePipes: () => Promise<number>;
/** 밖에서 유역을 고른다(그래프 측점선·사이드 패널 선택과 맞추기 위함). 이미 같으면 무시. */
selectBasinByChainage: (chainageM: number | null) => void;
/** 밖(리스트·그래프·3D)에서 관을 고른다 — 마커 선택·유역 강조를 맞춘다.
* 이미 같으면 무시(2026-08-17 전역 선택 동기화). */
selectPipeAtChainage: (chainageM: number | null) => void;
/** 측점 선택 마킹 — 계획선 위 해당 누가거리에 표식을 그린다(null이면 지움). */
markStation: (chainageM: number | null) => void;
dispose: () => void;
}
export interface DrainagePanelCallbacks {
/** 관 목록이 바뀔 때마다 누가거리 + 담당 유역의 배수 유효직경(mm) + 시설 종류를
* 넘긴다 — 종단 구조물 라인·통합 목록 동기화 및 관경 자동 지정(D800 기본)용. */
onPipesChanged?: (
pipes: Array<{
chainage_m: number;
effective_diameter_mm: number | null;
/** 담당 유역의 설계유량(㎥/s, 100년빈도·2.0배). 물넘이·세월교 개략 단면의 입력이다. */
design_flow_m3s?: number | null;
facility: PipeFacility;
start_m?: number;
end_m?: number;
source?: PipeSource;
options?: Record<string, string | number>;
}>,
) => void;
/** 유역을 고르거나 풀 때 그 관의 누가거리(없으면 null)를 넘긴다 — 그래프·사이드 패널 동기화용. */
onBasinSelected?: (chainageM: number | null) => void;
/** 관 마커 선택이 바뀔 때(클릭·해제) 누가거리를 알린다 — 유역이 없는 관도 전 화면
* (그래프·3D·리스트)이 같은 것을 가리키게 한다(2026-08-17 전역 선택 동기화). */
onPipeSelected?: (chainageM: number | null) => void;
/** 배수유역도 우클릭 빈 자리 메뉴 — 사이드 「구조물 배치」와 같은 구조물군 → 종류
* 2단 항목(2026-08-18 일원화). 선택 = 폼 자동 지정(addAt 경유). */
structureMenuItems?: (chainageM: number) => MapContextMenuItem[];
}
// 바깥 계약(창구·콜백)은 `_Types.ts` 에 있다(2026-09-02 분리) — 호출부 경로 유지를 위해 재수출.
export type { DrainagePanel, DrainagePanelCallbacks } from "./B05_Profile_UI_Drainage_Panel_Types";
import type { DrainagePanel, DrainagePanelCallbacks } from "./B05_Profile_UI_Drainage_Panel_Types";
export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): DrainagePanel {
const chrome = createDrainageChrome(
@@ -0,0 +1,74 @@
/* =============================================================================
* B05_Profile_UI_Drainage_Panel_Types.ts
* 배수유역 패널의 **바깥 계약** — 패널이 내주는 창구(`DrainagePanel`)와 페이지가
* 넘기는 콜백(`DrainagePanelCallbacks`).
*
* `B05_Profile_UI_Drainage_Panel` 에서 떼어냈다(700줄 제한, 2026-09-02). 패널이
* 그대로 다시 내보내므로 기존 import 경로는 유효하다.
* ========================================================================== */
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";
export interface DrainagePanel {
root: HTMLElement;
/** 프로젝트가 정해지면 배경지도·도엽 레이어를 불러온다. */
load: (projectId: string) => void;
/** 확정된 노선 평면 선형(사업지 좌표계 m)을 지도 위에 겹친다. */
setRoute: (points: ReadonlyArray<RoutePoint>) => void;
/** 현재 관 매설 누가거리 목록(종단 테이블의 "배관" 구조물 라인과 맞추는 데 쓴다). */
pipeChainages: () => number[];
/** 종단 테이블에서 배관 라인을 끌었을 때 — 그 자리로 옮기고 세부유역을 다시 나눈다. */
movePipe: (fromChainage: number, toChainage: number) => void;
/** 관 목록을 통째로 맞춘다(사이드바 구조물 폼 편집 등 밖에서 바뀐 경우).
* 현재 목록과 같으면 아무 일도 하지 않는다 — 되먹임 고리를 끊는 지점이다. */
setPipeChainages: (chainages: ReadonlyArray<number>) => void;
/** 종단 테이블 우클릭·통합 목록에서 계곡 통과 시설을 넣을 때.
* attributes에 시설 종류·구간·부속 옵션이 담긴다(생략 = 맨 배관). */
addPipe: (chainageM: number, attributes?: FacilityAttributes) => void;
/** 사이드 폼 [수정] — 기준점 이동·구간·부속 옵션을 정본에 반영하고 재계산한다. */
updatePipeFacility: (
fromChainageM: number,
toChainageM: number,
attributes: FacilityAttributes,
) => void;
removePipe: (chainageM: number) => void;
/** 경로 확정 시 관 매설 지점을 영구저장한다(B04 "모델 확정"과 같은 저장소). */
savePipes: () => Promise<number>;
/** 밖에서 유역을 고른다(그래프 측점선·사이드 패널 선택과 맞추기 위함). 이미 같으면 무시. */
selectBasinByChainage: (chainageM: number | null) => void;
/** 밖(리스트·그래프·3D)에서 관을 고른다 — 마커 선택·유역 강조를 맞춘다.
* 이미 같으면 무시(2026-08-17 전역 선택 동기화). */
selectPipeAtChainage: (chainageM: number | null) => void;
/** 측점 선택 마킹 — 계획선 위 해당 누가거리에 표식을 그린다(null이면 지움). */
markStation: (chainageM: number | null) => void;
dispose: () => void;
}
export interface DrainagePanelCallbacks {
/** 관 목록이 바뀔 때마다 누가거리 + 담당 유역의 배수 유효직경(mm) + 시설 종류를
* 넘긴다 — 종단 구조물 라인·통합 목록 동기화 및 관경 자동 지정(D800 기본)용. */
onPipesChanged?: (
pipes: Array<{
chainage_m: number;
effective_diameter_mm: number | null;
/** 담당 유역의 설계유량(㎥/s, 100년빈도·2.0배). 물넘이·세월교 개략 단면의 입력이다. */
design_flow_m3s?: number | null;
facility: PipeFacility;
start_m?: number;
end_m?: number;
source?: PipeSource;
options?: Record<string, string | number>;
}>,
) => void;
/** 유역을 고르거나 풀 때 그 관의 누가거리(없으면 null)를 넘긴다 — 그래프·사이드 패널 동기화용. */
onBasinSelected?: (chainageM: number | null) => void;
/** 관 마커 선택이 바뀔 때(클릭·해제) 누가거리를 알린다 — 유역이 없는 관도 전 화면
* (그래프·3D·리스트)이 같은 것을 가리키게 한다(2026-08-17 전역 선택 동기화). */
onPipeSelected?: (chainageM: number | null) => void;
/** 배수유역도 우클릭 빈 자리 메뉴 — 사이드 「구조물 배치」와 같은 구조물군 → 종류
* 2단 항목(2026-08-18 일원화). 선택 = 폼 자동 지정(addAt 경유). */
structureMenuItems?: (chainageM: number) => MapContextMenuItem[];
}