Merge remote-tracking branch 'origin/dev' into sub_laptop_6
This commit is contained in:
@@ -128,6 +128,9 @@ export interface StructureInstance {
|
||||
summary_item?: SummaryItem | null;
|
||||
/** 옵션 그룹마다 고른 집계 항목 `{그룹 id: 참조}`(PLAN 46-3) — 없으면 그룹 미지정. */
|
||||
group_items?: GroupItems | null;
|
||||
/** 땅 위 자리(344-2 · 410) — 서버가 채움 · 누가거리만 옮기면 낡음(따라감 · 저장이 노선 위로 다시). */
|
||||
x?: number | null;
|
||||
y?: number | null;
|
||||
}
|
||||
|
||||
interface StructureTypesResponse {
|
||||
|
||||
@@ -59,6 +59,7 @@ import {
|
||||
basinIndexOfPipe,
|
||||
pipeMarkerColor,
|
||||
adoptDraftPipes,
|
||||
followEditorPipes,
|
||||
reconcilePipes,
|
||||
WIDTH_KEY,
|
||||
type DrainageLayer,
|
||||
@@ -569,6 +570,7 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra
|
||||
if (projectId && meta) void run(loadSaved);
|
||||
},
|
||||
pipeChainages: () => pipeEditor.chainages(),
|
||||
followPipes: (pipes) => followEditorPipes(pipeEditor, facilityStore, pipes),
|
||||
movePipe(fromChainage, toChainage) {
|
||||
const index = pipeEditor
|
||||
.pipes()
|
||||
|
||||
@@ -25,6 +25,8 @@ export interface DrainagePanel {
|
||||
setRoute: (points: ReadonlyArray<RoutePoint>) => void;
|
||||
/** 현재 관 매설 누가거리 목록(종단 테이블의 "배관" 구조물 라인과 맞추는 데 쓴다). */
|
||||
pipeChainages: () => number[];
|
||||
/** 노선 초안 follow 가 다시 매긴 관 자리를 받음(짝 = 관 id · 재계산 없음 · 410). */
|
||||
followPipes: (pipes: ReadonlyArray<{ id?: string | null; chainage_m: number }>) => void;
|
||||
/** 종단 테이블에서 배관 라인을 끌었을 때 — 그 자리로 옮기고 세부유역을 다시 나눈다. */
|
||||
movePipe: (fromChainage: number, toChainage: number) => void;
|
||||
/** 관 목록을 통째로 맞춘다(사이드바 구조물 폼 편집 등 밖에서 바뀐 경우).
|
||||
@@ -64,6 +66,8 @@ export interface DrainagePanel {
|
||||
/** 관 하나의 보고 — 누가거리 + 담당 유역의 배수 유효직경(mm) + 시설 종류 · 구간 · 옵션 · 집계 항목. */
|
||||
export interface DrainagePipeReport {
|
||||
chainage_m: number;
|
||||
/** 관 고정 id(서버 · 새 관은 없음) — 따라감 · 옮김 짝(411-B). */
|
||||
id?: string;
|
||||
effective_diameter_mm: number | null;
|
||||
/** 담당 유역의 설계유량(㎥/s, 100년빈도·2.0배). 물넘이·세월교 개략 단면의 입력이다. */
|
||||
design_flow_m3s?: number | null;
|
||||
|
||||
@@ -22,6 +22,7 @@ import { normalizeStrength, rampColor } from "../B04_PreProcess/B04_PreProcess_U
|
||||
import type { MapContextMenu, MapContextMenuItem } from "@ui/ui_template_context_menu";
|
||||
import type { DetailBasin } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
||||
import type { PipeEditor, PipePoint } from "./B05_Profile_UI_Drainage_Pipes";
|
||||
import type { FacilityStore } from "./B05_Profile_UI_Drainage_Facility";
|
||||
import { PIPE_MATCH_M } from "../A00_Common/design/design_selection";
|
||||
import { viewOf } from "../common_util/common_util_view_window";
|
||||
|
||||
@@ -341,6 +342,33 @@ export function mountDrainageToggles(
|
||||
);
|
||||
}
|
||||
|
||||
/** 노선 초안 follow 가 다시 매긴 관 자리(가게 관 초안)를 편집기에 얹음(410) — 짝 = 관 id · 시설 속성 ·
|
||||
* 고름도 따라감 · 재계산 · 초안 쓰기 없음(가게가 이미 그 값). 편집기가 옛 노선 누가거리로 초안을 덮지 않게. */
|
||||
export function followEditorPipes(
|
||||
editor: PipeEditor,
|
||||
facilities: FacilityStore,
|
||||
followed: ReadonlyArray<{ id?: string | null; chainage_m: number }>,
|
||||
): void {
|
||||
const to = new Map(followed.flatMap((pipe) => (pipe.id ? [[pipe.id, pipe.chainage_m]] : [])));
|
||||
const before = editor.pipes();
|
||||
const moves = before.flatMap((pipe) => {
|
||||
const at = pipe.id ? to.get(pipe.id) : undefined;
|
||||
return at === undefined || Math.abs(at - pipe.chainage_m) < 0.005
|
||||
? []
|
||||
: [{ pipe, at, attributes: facilities.get(pipe.chainage_m) }];
|
||||
});
|
||||
if (!moves.length) return;
|
||||
const picked = before[editor.selected() ?? -1];
|
||||
moves.forEach(({ pipe }) => facilities.set(pipe.chainage_m, null)); // 다 뺀 뒤 얹음 — 자리 겹침 막음
|
||||
moves.forEach(({ at, attributes }) => attributes && facilities.set(at, attributes));
|
||||
const moved = new Map(moves.map(({ pipe, at }) => [pipe, at]));
|
||||
editor.setPipes(
|
||||
before.map((pipe) => ({ ...pipe, chainage_m: moved.get(pipe) ?? pipe.chainage_m })),
|
||||
);
|
||||
const pickedAt = picked && (moved.get(picked) ?? picked.chainage_m);
|
||||
if (picked) editor.select(editor.pipes().findIndex((pipe) => pipe.chainage_m === pickedAt));
|
||||
}
|
||||
|
||||
/** 밖에서 바뀐 관 목록을 현재 목록과 맞춘다. 같으면 null — 되먹임 고리를 끊는 지점이다.
|
||||
* 다르면 새 목록을 돌려주되, 원래 생성 사유는 자리로 맞춰 이어 붙인다(기본/자동/수동 표시 보존). */
|
||||
/** 되돌리기(369) — 되살린 걸음의 관 초안(세션)이 `next` 와 같은 자리면 그 초안(속성 정본) · id 도
|
||||
|
||||
@@ -15,7 +15,7 @@ import { PIPE_MATCH_M } from "../A00_Common/design/design_selection";
|
||||
* 시설 종류·구간·부속 옵션·출처(source)도 함께 — 통합 목록·사이드 폼이
|
||||
* 이 목록만으로 편집 화면을 채운다(2026-08-17 단일 폼 통합). */
|
||||
export function buildPipeReports(
|
||||
pipes: ReadonlyArray<{ chainage_m: number; reason?: string }>,
|
||||
pipes: ReadonlyArray<{ chainage_m: number; reason?: string; id?: string }>,
|
||||
basins: ReadonlyArray<DetailBasin>,
|
||||
facilityStore: ReturnType<typeof createFacilityStore>,
|
||||
): DrainagePipeReport[] {
|
||||
@@ -25,6 +25,7 @@ export function buildPipeReports(
|
||||
const attributes = facilityStore.get(chainage);
|
||||
return {
|
||||
chainage_m: chainage,
|
||||
...(pipe.id ? { id: pipe.id } : {}), // 관 고정 id — 옮겨도 짝(411-B)
|
||||
effective_diameter_mm:
|
||||
basin && !basin.bridge_required ? (basin.pipe_diameter_mm ?? null) : null,
|
||||
// 설계유량은 세월교 검토 유역에도 붙인다 — 물넘이·세월교 단면이 이 값에서 나온다.
|
||||
|
||||
@@ -18,6 +18,8 @@ export interface RouteProfilePanelCallbacks {
|
||||
onPipesChanged?: (
|
||||
pipes: Array<{
|
||||
chainage_m: number;
|
||||
/** 관 고정 id(서버 · 새 관은 없음) — 따라감 · 옮김 짝(411-B). */
|
||||
id?: string;
|
||||
effective_diameter_mm: number | null;
|
||||
/** 담당 유역의 설계유량(㎥/s) — 물넘이·세월교 개략 단면의 입력. */
|
||||
design_flow_m3s?: number | null;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createButton } from "@ui/ui_template_elements";
|
||||
import { createSidePanels, type SidePanels } from "@ui/ui_template_side_panels";
|
||||
import { bindFootprintToggle } from "./B05_Profile_UI_RouteEdit_Footprint";
|
||||
import { bindMarkToggle } from "./B05_Profile_UI_RouteEdit_Marks";
|
||||
import { infoLiftPx } from "./logic/B05_Profile_RouteEdit_InfoLift";
|
||||
|
||||
/* =============================================================================
|
||||
* B05_Profile_UI_RouteEdit_Chrome.ts
|
||||
@@ -205,6 +206,24 @@ export function createRouteEditChrome(
|
||||
setInfo(infoCollapsed);
|
||||
infoToggle.addEventListener("click", () => setInfo(!infoCollapsed));
|
||||
|
||||
// 뜬 종단 판(이웃 칸)이 지도 아래를 덮으면 상태 · 범례를 그 위로(408-2) — 판 높이 끌기 · 접기 ·
|
||||
// 창 분할(판이 다른 창으로 감 = 못 찾음)을 판 크기 관찰로 따라감.
|
||||
let watchedProfile: Element | null = null;
|
||||
const liftInfo = (): void => {
|
||||
if (!overlay.isConnected) return; // 붙기 전 첫 알림 · 걷힌 뒤 — 판 관찰은 판과 함께 사라짐
|
||||
const profile =
|
||||
overlay.closest(".design-page__main")?.querySelector(".b05-route-profile") ?? null;
|
||||
if (profile && profile !== watchedProfile) lift.observe((watchedProfile = profile));
|
||||
const map = overlay.getBoundingClientRect();
|
||||
const top = profile?.isConnected ? profile.getBoundingClientRect().top : null;
|
||||
overlay.style.setProperty(
|
||||
"--b05-profile-cover",
|
||||
`${infoLiftPx(map.bottom, map.height, top)}px`,
|
||||
);
|
||||
};
|
||||
const lift = new ResizeObserver(liftInfo);
|
||||
lift.observe(overlay);
|
||||
|
||||
const measureCloseButton = createButton({ label: "✕", variant: "ghost" });
|
||||
measureCloseButton.classList.add("b05-routeedit__measure-close");
|
||||
measureCloseButton.setAttribute("aria-label", "닫기");
|
||||
|
||||
@@ -88,10 +88,10 @@
|
||||
display: grid;
|
||||
}
|
||||
|
||||
/* 상태 · 범례 — 지도 왼쪽 아래(사용자 지시 ⑫) · 뜬 종단 판이 덮으면 가려진 채(349-0).
|
||||
/* 상태 · 범례 — 지도 왼쪽 아래(사용자 지시 ⑫) · 뜬 종단 판이 덮으면 그 위로(408-2 · `_Chrome` 이 적음).
|
||||
두 줄 안 · 좁은 폭(345-5 덧 3) — 왼쪽 상태 글(두 줄에서 자름) · 가운데 범례 두 줄 · 오른쪽 ▾. */
|
||||
.b05-routeedit__info {
|
||||
bottom: var(--spacing-12);
|
||||
bottom: calc(var(--spacing-12) + var(--b05-profile-cover, 0px));
|
||||
left: var(--spacing-12);
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||
@@ -184,13 +184,14 @@
|
||||
}
|
||||
|
||||
/* 오른쪽 덮는 판 — 패널 양식(382-1 · `ui_template_side_panels` · 손잡이 · 머리 · 폭 끌기) 을 지도 위에 얹음 ·
|
||||
위아래 반씩 **지금 횡단** · **이전 횡단**(352-1) · 아래 종단 판(z 3)이 그 위를 덮음 · 빈 자리는 지도 조작을 통과. */
|
||||
위아래 반씩 **지금 횡단** · **이전 횡단**(352-1) · 뜬 종단 판이 덮으면 그 위까지(408-3 · 손잡이 · 아래 반 보임) ·
|
||||
빈 자리는 지도 조작을 통과. */
|
||||
.b05-routeedit__side-wrap {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: calc(var(--spacing-16) + 34px + var(--spacing-8)); /* 도구줄 · 회전 단추 줄 아래 */
|
||||
right: var(--spacing-12);
|
||||
bottom: var(--spacing-12);
|
||||
bottom: calc(var(--spacing-12) + var(--b05-profile-cover, 0px));
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
pointer-events: none;
|
||||
@@ -364,11 +365,11 @@
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* ㉔ 잰 값 — 지도 오른쪽 아래 작은 창(뜬 종단 판이 덮으면 가려진 채 · 349-0). 닫으면 잰 것이 지워진다. */
|
||||
/* ㉔ 잰 값 — 지도 오른쪽 아래 작은 창(뜬 종단 판이 덮으면 그 위로 · 408-3). 닫으면 잰 것이 지워진다. */
|
||||
.b05-routeedit__measure {
|
||||
position: absolute;
|
||||
right: calc(var(--spacing-12) + var(--b05-side-open));
|
||||
bottom: var(--spacing-12);
|
||||
bottom: calc(var(--spacing-12) + var(--b05-profile-cover, 0px));
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/* =============================================================================
|
||||
* B05_Profile/logic/B05_Profile_Pipeline_Follow.ts
|
||||
* 노선이 움직인 뒤 관 목록 화면 값(PLAN 349-0d) — 배수유역도가 알린 관 목록(옛 누가거리)을 가게 관 초안
|
||||
* (계산 묶음 follow 가 좌표로 다시 매긴 누가거리)에 맞춤. 짝 = 누가거리 차례(따라감은 좌표 투영이라
|
||||
* 차례가 거의 그대로) · 관 수가 다르면 그대로(배수유역도가 아직 안 받은 고침).
|
||||
* (계산 묶음 follow 가 좌표로 다시 매긴 누가거리)에 맞춤. 짝 = 관 고정 id(411-B · 옮겨 차례가 바뀌어도
|
||||
* 그 관) · id 없는 관(막 놓은 관)끼리는 누가거리 차례 · 관 수가 다르면 그대로(배수유역도가 아직 안 받은 고침).
|
||||
* 순수 계산(DOM · fetch · 세션 없음).
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -10,6 +10,7 @@ interface Span {
|
||||
chainage_m: number;
|
||||
start_m?: number | null;
|
||||
end_m?: number | null;
|
||||
id?: string | null;
|
||||
}
|
||||
|
||||
/** 관 알림 목록의 측점 · 구간을 가게 관 초안 쪽으로 — 초안이 없으면(저장분) 그대로. */
|
||||
@@ -19,9 +20,15 @@ export function followPipeReports<R extends Span>(
|
||||
): R[] {
|
||||
if (!pipes || pipes.length !== reports.length) return [...reports];
|
||||
const byChainage = (a: Span, b: Span): number => a.chainage_m - b.chainage_m;
|
||||
const to = [...pipes].sort(byChainage);
|
||||
return [...reports].sort(byChainage).map((report, index) => {
|
||||
const pipe = to[index];
|
||||
const byId = new Map(pipes.flatMap((pipe) => (pipe.id ? [[pipe.id, pipe]] : [])));
|
||||
const paired = (report: R): boolean => Boolean(report.id && byId.has(report.id));
|
||||
const loose = [...pipes].filter((pipe) => !reports.some((r) => r.id && r.id === pipe.id));
|
||||
const to = loose.sort(byChainage);
|
||||
const sorted = [...reports].sort(byChainage);
|
||||
let next = 0;
|
||||
return sorted.map((report) => {
|
||||
const pipe = paired(report) ? byId.get(report.id!)! : to[next++];
|
||||
if (!pipe) return report;
|
||||
const chainage = Math.round(pipe.chainage_m * 100) / 100; // 알림 목록 자릿수(`buildPipeReports`)
|
||||
const shift = chainage - report.chainage_m;
|
||||
const moved = (own: number | null | undefined, from: number | null | undefined) =>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
/* =============================================================================
|
||||
* B05_Profile/logic/B05_Profile_RouteEdit_InfoLift.ts
|
||||
* 평면 상태 · 범례 올림(408-2) — 뜬 종단 판이 지도 아래를 덮은 높이만큼 위로.
|
||||
* 파이썬 짝 아님. 순수 계산 — 화면 · fetch · sessionStorage 없음. 부르는 쪽은 `_UI_RouteEdit_Chrome`.
|
||||
* ========================================================================== */
|
||||
|
||||
/** 상태 글판이 이 높이(px)는 지도 위에 남게 — 판을 끝까지 올려도 지도 밖으로 안 나감. */
|
||||
const KEEP_PX = 120;
|
||||
|
||||
/** 올릴 높이(px) — `profileTop` = 종단 판 윗변(없음 · 접힘 · 창 분할 = null) · 지도 칸 아래 · 높이. */
|
||||
export function infoLiftPx(
|
||||
mapBottom: number,
|
||||
mapHeight: number,
|
||||
profileTop: number | null,
|
||||
): number {
|
||||
if (profileTop === null) return 0;
|
||||
const cover = Math.max(0, mapBottom - profileTop);
|
||||
return Math.round(Math.min(cover, Math.max(0, mapHeight - KEEP_PX)));
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
/* =============================================================================
|
||||
* B05_Profile_Structure_Follow.ts
|
||||
* 노선 초안 동안 종단 알약 자리(PLAN 346-2 덧 3) — 계산 묶음 follow 가 좌표로 따라간 관 · 구조물의 새
|
||||
* 누가거리를 알약 목록에 얹음. 구조물 = structure_id 로 짝 · 관 알약 id = 세션 id(`pipeIdAt`) 라 묶음 관 id
|
||||
* (서버 고정 id)와 다르면 누가거리 차례로 짝.
|
||||
* 누가거리를 알약 목록에 얹음. 구조물 = structure_id 로 짝 · 관 알약 id = 세션 id(`pipeIdAt` · 자리 장부)라
|
||||
* 묶음 관 id(서버 고정 id)와 다르면 같은 자리(장부가 따라간 자리 = 그 관 · 411-B) → 남은 것끼리 누가거리 차례.
|
||||
*
|
||||
* 순수 — 화면 · fetch · 세션 없음.
|
||||
* ========================================================================== */
|
||||
@@ -22,6 +22,9 @@ interface Moved extends Placed {
|
||||
structure_id?: string | null;
|
||||
}
|
||||
|
||||
/** 같은 관으로 볼 자리 차이(m) — `PIPE_MATCH_M`(design_selection) 같은 값. */
|
||||
const SAME_PIPE_M = 0.51;
|
||||
|
||||
const list = (value: unknown): Moved[] => (Array.isArray(value) ? (value as Moved[]) : []);
|
||||
const at = (item: Placed): number => item.chainage_m ?? item.start_m ?? 0;
|
||||
|
||||
@@ -35,14 +38,21 @@ export function followedMarks<T extends Mark>(
|
||||
for (const item of list(structures)) if (item.structure_id) byId.set(item.structure_id, item);
|
||||
const movedPipes = list(pipes).filter((pipe) => typeof pipe.chainage_m === "number");
|
||||
for (const pipe of movedPipes) if (pipe.id) byId.set(pipe.id, pipe);
|
||||
// ponytail: 차례 짝 — 노선을 고쳐 관 차례가 뒤바뀌면 틀림 · 알약이 서버 관 id 를 들면 id 짝으로
|
||||
const loose = marks
|
||||
.filter((mark) => mark.structure_id?.startsWith("pipe-") && !byId.has(mark.structure_id))
|
||||
.sort((a, b) => at(a) - at(b));
|
||||
const ids = new Set(marks.map((mark) => mark.structure_id));
|
||||
const free = movedPipes.filter((pipe) => !ids.has(pipe.id)).sort((a, b) => at(a) - at(b));
|
||||
let free = movedPipes.filter((pipe) => !ids.has(pipe.id)).sort((a, b) => at(a) - at(b));
|
||||
const paired = new Map<T, Moved>();
|
||||
if (loose.length === free.length) loose.forEach((mark, index) => paired.set(mark, free[index]));
|
||||
for (const mark of loose) {
|
||||
const same = free.find((pipe) => Math.abs(at(pipe) - at(mark)) < SAME_PIPE_M);
|
||||
if (!same) continue;
|
||||
paired.set(mark, same);
|
||||
free = free.filter((pipe) => pipe !== same);
|
||||
}
|
||||
// ponytail: 남은 것은 차례 짝 — 알약이 옛 자리인 채 노선 고쳐 관 차례가 뒤바뀌면 틀림
|
||||
const rest = loose.filter((mark) => !paired.has(mark));
|
||||
if (rest.length === free.length) rest.forEach((mark, index) => paired.set(mark, free[index]));
|
||||
return marks.map((mark) => {
|
||||
const moved = (mark.structure_id ? byId.get(mark.structure_id) : undefined) ?? paired.get(mark);
|
||||
if (!moved) return mark;
|
||||
|
||||
Reference in New Issue
Block a user