feat(b06): 종단 측점선·구조물 알약을 끌어 옮길 수 있게 함
B05 에서만 되던 끌어 옮기기를 B06 종단에도 붙임 — 측점선과 알약 어느 쪽을 끌어도 같은 길을 타고, 관은 예약 이동(저장·확정 때 정본과 배수유역 재분할), 구조물은 정본 이동임. 끌기 판별과 대상 찾기는 새 모듈(B06_Section_UI_Section_View_Menu)로 모아, 종단 화면 본체가 더 커지지 않게 둠. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fe1QWPTfw11PaKh2LjwXdR
This commit is contained in:
@@ -495,6 +495,11 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
||||
removeStructure: (structureId) => {
|
||||
structuresPanel.removeStructureById(structureId);
|
||||
},
|
||||
movePipe: (fromChainageM, toChainageM) =>
|
||||
structuresPanel.movePipeTo(fromChainageM, toChainageM),
|
||||
moveStructure: (structureId, toChainageM) => {
|
||||
structuresPanel.moveStructureTo(structureId, toChainageM);
|
||||
},
|
||||
});
|
||||
// 폼 → 횡단 캐시 반영은 따로 뗀 모듈이 맡는다(2026-09-02 분리).
|
||||
const pipeOptionsContext: PipeOptionsContext = {
|
||||
|
||||
@@ -111,6 +111,8 @@ export interface B06StructuresPanel {
|
||||
removeStructureById: (structureId: string) => boolean;
|
||||
addPipeAt: (chainageM: number) => void;
|
||||
removePipeAt: (chainageM: number) => void;
|
||||
movePipeTo: (fromChainageM: number, toChainageM: number) => void;
|
||||
moveStructureTo: (structureId: string, toChainageM: number) => boolean;
|
||||
/** 유입구 "구조"에 합쳐진 B06 유입측 형식 — 조정창 값과 맞춘다(2026-08-29 지시 5). */
|
||||
facility: {
|
||||
setInletStructure: (value: "auto" | "revet" | "I" | "L" | "U") => void;
|
||||
@@ -434,6 +436,26 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
|
||||
section.setPipeFacilities(pipeFacilities);
|
||||
}
|
||||
|
||||
/** 관 옮기기 — 폼의 측점 칸과 종단 끌기가 같은 길을 탄다. 재분할은 [저장]·[확정] 뒤. */
|
||||
function movePipeTo(fromChainageM: number, toChainageM: number): void {
|
||||
if (!deps.movePipe) {
|
||||
showToast(PIPE_MOVE_GUIDE, "error");
|
||||
return;
|
||||
}
|
||||
const to = Number(toChainageM.toFixed(2));
|
||||
deps.movePipe(fromChainageM, to);
|
||||
const hit = pipeFacilities.find(
|
||||
(entry) => Math.abs(entry.chainage_m - fromChainageM) < PIPE_MATCH_M,
|
||||
);
|
||||
if (hit) hit.chainage_m = to;
|
||||
if (currentChainageM !== null && Math.abs(currentChainageM - fromChainageM) < PIPE_MATCH_M)
|
||||
currentChainageM = to;
|
||||
pipeFacilities.sort((left, right) => left.chainage_m - right.chainage_m);
|
||||
section.setPipeFacilities(pipeFacilities);
|
||||
pushMarks();
|
||||
showToast(PIPE_MOVE_NOTICE, "success");
|
||||
}
|
||||
|
||||
/** 관 빼기 — 목록·폼·종단 우클릭이 같은 길을 탄다. 정본은 [저장]·[확정]에서 나간다. */
|
||||
function removePipeAt(chainageM: number): void {
|
||||
if (!deps.removePipe) {
|
||||
@@ -463,6 +485,8 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
|
||||
removeStructureById: (structureId) => section.removeById(structureId),
|
||||
addPipeAt: (chainageM) => section.addAt(chainageM, "pipe"),
|
||||
removePipeAt: (chainageM) => removePipeAt(chainageM),
|
||||
movePipeTo: (fromChainageM, toChainageM) => movePipeTo(fromChainageM, toChainageM),
|
||||
moveStructureTo: (structureId, toChainageM) => section.moveById(structureId, toChainageM),
|
||||
facility: {
|
||||
setInletStructure: (value) => section.facility.setInletStructure(value),
|
||||
onInletStructureChange: (handler) => section.facility.onInletStructureChange(handler),
|
||||
|
||||
@@ -52,6 +52,7 @@ import {
|
||||
} from "../B05_Profile/B05_Profile_UI_Structures_Marks";
|
||||
import {
|
||||
mountSectionStructureMenu,
|
||||
moveMarkById,
|
||||
type SectionStructureEdit,
|
||||
} from "./B06_Section_UI_Section_View_Menu";
|
||||
import {
|
||||
@@ -511,6 +512,14 @@ export function createSectionView(
|
||||
// 생겨 패널이 16,000px 까지 부푼다(2026-09-07 실측). 빼 두면 한 번에 수렴한다.
|
||||
const laneHeight = markStructures.length && markTypes.length ? STRUCTURE_LANE_HEIGHT_PX : 0;
|
||||
const heights = chartHeights(Math.max(lastChartAvailable - laneHeight, MIN_LONG_HEIGHT));
|
||||
const moveStationMark = (markId: string, toChainageM: number): void => {
|
||||
if (!structureEdit) return;
|
||||
moveMarkById(markId, toChainageM, {
|
||||
structures: markStructures,
|
||||
stations: detail.longitudinal.stations ?? [],
|
||||
edit: structureEdit,
|
||||
});
|
||||
};
|
||||
const minWidth = longitudinalMinimumWidth(detail.longitudinal, cachedStationInterval);
|
||||
const chartWidth = Math.max(renderWidth, minWidth);
|
||||
const chart = buildLongitudinalChart({
|
||||
@@ -524,6 +533,10 @@ export function createSectionView(
|
||||
minWidth,
|
||||
scrollLeft: keepScrollLeft,
|
||||
viewportWidth: chartWrap.clientWidth || chartWidth,
|
||||
// 측점선을 끌면 그 구조물이 옮겨 간다 — 관은 예약 이동, 구조물은 정본 이동.
|
||||
onDragStation: structureEdit
|
||||
? (stationId, toChainageM) => moveStationMark(stationId, toChainageM)
|
||||
: undefined,
|
||||
});
|
||||
const longAxis = chart.axis;
|
||||
const nodes: Element[] = [chart.node];
|
||||
@@ -555,8 +568,8 @@ export function createSectionView(
|
||||
}
|
||||
if (best) selectStation(best.id, true);
|
||||
},
|
||||
// 알약 끌기는 B05 몫이다(배수유역 재분할이 걸린다) — 여기서는 자리만 보여 준다.
|
||||
onMove: () => undefined,
|
||||
// 알약 끌기도 B05 와 같게 받는다(2026-09-12 일원화) — 정본은 [저장]·[확정]에서.
|
||||
onMove: (structureId, toChainageM) => moveStationMark(structureId, toChainageM),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ export interface LongitudinalChartInput {
|
||||
/** 지금 보이는 구간을 정하는 값 — 가로 스크롤 위치와 컨테이너 안쪽 폭. */
|
||||
scrollLeft: number;
|
||||
viewportWidth: number;
|
||||
/** 구조물(비정규) 측점선을 끌어 옮겼다. 안 넘기면 그 선은 못 잡는다. */
|
||||
onDragStation?: (stationId: string, toChainageM: number) => void;
|
||||
}
|
||||
|
||||
export interface LongitudinalChartResult {
|
||||
@@ -110,7 +112,8 @@ export function buildLongitudinalChart(input: LongitudinalChartInput): Longitudi
|
||||
axis = next;
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
// 구조물(비정규) 측점선 끌어 옮기기 — B05 와 같은 조작이다(2026-09-12 일원화).
|
||||
input.onDragStation,
|
||||
0,
|
||||
0,
|
||||
visibleElevationRange(detail, fromM, toM) ?? undefined,
|
||||
|
||||
@@ -25,6 +25,53 @@ export interface SectionStructureEdit {
|
||||
addStructureType: (chainageM: number, typeId: string) => void;
|
||||
/** 구조물(관 아님)을 뺀다. */
|
||||
removeStructure: (structureId: string) => void;
|
||||
/** 관을 다른 측점으로 옮긴다(측점선·알약 끌기). */
|
||||
movePipe: (fromChainageM: number, toChainageM: number) => void;
|
||||
/** 구조물(관 아님)을 다른 측점으로 옮긴다. */
|
||||
moveStructure: (structureId: string, toChainageM: number) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 측점선·알약을 끌어 놓았다 — 그래프 위 id 로 구조물을 찾아 같은 예약 경로로 보낸다.
|
||||
* 종단 측점선의 id 는 **종단 정본 측점 id** 라 알약(구조물 id)과 다를 수 있다. 둘 다 받아
|
||||
* 같은 자리(누가거리)로 맞춘다.
|
||||
*/
|
||||
export function moveMarkById(
|
||||
markId: string,
|
||||
toChainageM: number,
|
||||
input: {
|
||||
structures: ReadonlyArray<StructureInstance>;
|
||||
stations: ReadonlyArray<{ station_id: string; chainage_m: number }>;
|
||||
edit: SectionStructureEdit;
|
||||
},
|
||||
): void {
|
||||
const direct = input.structures.find((entry) => String(entry.structure_id) === markId);
|
||||
if (direct) {
|
||||
moveStructureMark(input.structures, input.edit, markId, toChainageM);
|
||||
return;
|
||||
}
|
||||
const station = input.stations.find((entry) => String(entry.station_id) === markId);
|
||||
if (!station) return;
|
||||
// 측점선 id 로 온 경우 — 그 측점 자리에 선 구조물을 찾는다(관 매칭과 같은 0.51m).
|
||||
const near = input.structures.find(
|
||||
(entry) => Math.abs(structureAnchorM(entry) - station.chainage_m) < 0.51,
|
||||
);
|
||||
if (near) moveStructureMark(input.structures, input.edit, String(near.structure_id), toChainageM);
|
||||
}
|
||||
|
||||
/** 측점선·알약을 끌었을 때 — 관인지 구조물인지 갈라 같은 예약 경로로 보낸다. */
|
||||
function moveStructureMark(
|
||||
structures: ReadonlyArray<StructureInstance>,
|
||||
edit: SectionStructureEdit,
|
||||
structureId: string,
|
||||
toChainageM: number,
|
||||
): void {
|
||||
const hit = structures.find((entry) => String(entry.structure_id) === structureId);
|
||||
if (!hit) return;
|
||||
const from = structureAnchorM(hit);
|
||||
if (Math.abs(from - toChainageM) < 0.005) return;
|
||||
if (isPipeMark(hit)) edit.movePipe(from, toChainageM);
|
||||
else edit.moveStructure(structureId, toChainageM);
|
||||
}
|
||||
|
||||
/** 알약·우클릭이 쓰는 「관인가」 판정 — 관은 정본이 달라 지우는 길도 다르다. */
|
||||
|
||||
Reference in New Issue
Block a user