diff --git a/D03_PlanAnalysis/D03_PlanAnalysis_UI_Cross.ts b/D03_PlanAnalysis/D03_PlanAnalysis_UI_Cross.ts index d8e7db7e4..00571394c 100644 --- a/D03_PlanAnalysis/D03_PlanAnalysis_UI_Cross.ts +++ b/D03_PlanAnalysis/D03_PlanAnalysis_UI_Cross.ts @@ -4,18 +4,28 @@ * `generate_sections` · 격자 = 그 연도 DEM 평활본) · 그림 = B05 `buildCrossSvg` 지반만. * ========================================================================== */ +import "../B05_Profile/B05_Profile_UI_Style_RouteEdit.css"; import { createWorkflowPanelHandle } from "@ui/ui_template_overlay"; import { stationLabel } from "../common_util/common_util_station"; import type { DtmGrid } from "../common_util/common_util_surface_sampler"; import { buildCrossSvg } from "../B05_Profile/B05_Profile_UI_RouteEdit_Cross_Draw"; -import { sectionsGround } from "../B05_Profile/logic/B05_Profile_Sections_Ground"; +import { + sectionsGround, + type StationGround, +} from "../B05_Profile/logic/B05_Profile_Sections_Ground"; import type { Vertex } from "./logic/D03_PlanAnalysis_Plan"; import { PLAN_STATION_INTERVAL_M } from "./D03_PlanAnalysis_UI_Map"; export interface PlanCross { root: HTMLElement; - /** `chainage` = 등록 누가 · `localM` = 그 연도 노선 위 누가. */ - show: (chainage: number, year: number, line: Vertex[], localM: number, grid: DtmGrid) => void; + /** `chainage` = 등록 누가 · `localM` = 그 연도 노선 위 누가 · 돌려줌 = 그린 측점 지반(못 읽으면 null). */ + show: ( + chainage: number, + year: number, + line: Vertex[], + localM: number, + grid: DtmGrid, + ) => StationGround | null; clear: (text: string) => void; } @@ -48,7 +58,10 @@ export function createPlanCross(): PlanCross { clear, show(chainage, year, line, localM, grid) { const station = sectionsGround(line, [localM], grid).stations[0]; - if (!station) return clear("이 측점의 지반을 읽지 못했습니다."); + if (!station) { + clear("이 측점의 지반을 읽지 못했습니다."); + return null; + } const name = stationLabel(chainage, PLAN_STATION_INTERVAL_M, 1); const z = station.center_z === null ? "—" : `${station.center_z.toFixed(2)} m`; head.textContent = `${year}년 · ${name} · 중심 지반고 ${z}`; @@ -71,6 +84,7 @@ export function createPlanCross(): PlanCross { true, ); body.replaceChildren(plot.svg); + return station; }, }; } diff --git a/D03_PlanAnalysis/D03_PlanAnalysis_UI_Map.ts b/D03_PlanAnalysis/D03_PlanAnalysis_UI_Map.ts index 219589be2..e5e59f21e 100644 --- a/D03_PlanAnalysis/D03_PlanAnalysis_UI_Map.ts +++ b/D03_PlanAnalysis/D03_PlanAnalysis_UI_Map.ts @@ -289,7 +289,8 @@ export function createPlanMap(events: PlanMapEvents): PlanMap { }; // 지점 끌기 — 지도 옮기기보다 먼저 듣고 지점을 집었으면 옮기기에 안 넘김 - let dragging: { index: number; id: number } | null = null; + // 집은 자리와 지점 가운데 차(px)를 지킴 · 손 떨림(CLICK_SLOP_PX) 안이면 끌기 아님(누름) + let dragging: { index: number; id: number; dx: number; dy: number; moved: boolean } | null = null; let press: [number, number] | null = null; canvas.addEventListener("pointerdown", (event) => { press = [event.clientX, event.clientY]; @@ -297,7 +298,9 @@ export function createPlanMap(events: PlanMapEvents): PlanMap { const index = nodeAt(...at(event)); if (index < 0) return; event.stopImmediatePropagation(); - dragging = { index, id: event.pointerId }; + const [px, py] = at(event); + const [sx, sy] = toScreen(scene.nodes[index]); + dragging = { index, id: event.pointerId, dx: sx - px, dy: sy - py, moved: false }; canvas.setPointerCapture(event.pointerId); canvas.style.cursor = "grabbing"; }); @@ -307,14 +310,19 @@ export function createPlanMap(events: PlanMapEvents): PlanMap { canvas.style.cursor = nodeAt(...at(event)) >= 0 ? "grab" : ""; return; } - events.onDrag(dragging.index, screenToMetric(meta, view, ...at(event))); + if (!press) return; + const far = Math.hypot(event.clientX - press[0], event.clientY - press[1]) > CLICK_SLOP_PX; + if (!dragging.moved && !far) return; + dragging.moved = true; + const [px, py] = at(event); + events.onDrag(dragging.index, screenToMetric(meta, view, px + dragging.dx, py + dragging.dy)); }); const endDrag = (event: PointerEvent): void => { if (!dragging || event.pointerId !== dragging.id) return; - const index = dragging.index; + const { index, moved } = dragging; dragging = null; canvas.style.cursor = ""; - events.onDragEnd(index); + if (moved) events.onDragEnd(index); }; canvas.addEventListener("pointerup", endDrag); canvas.addEventListener("pointercancel", endDrag); diff --git a/D03_PlanAnalysis/D03_PlanAnalysis_UI_Page.ts b/D03_PlanAnalysis/D03_PlanAnalysis_UI_Page.ts index ebbe66146..c2d77ff32 100644 --- a/D03_PlanAnalysis/D03_PlanAnalysis_UI_Page.ts +++ b/D03_PlanAnalysis/D03_PlanAnalysis_UI_Page.ts @@ -36,6 +36,7 @@ import { } from "../B04_PreProcess/B04_PreProcess_UI_MapRender"; import { prepareLayer } from "../B04_PreProcess/logic/B04_PreProcess_UI_MapRender_Prepare"; import { stationLabel } from "../common_util/common_util_station"; +import type { StationGround } from "../B05_Profile/logic/B05_Profile_Sections_Ground"; import { fetchPlanAnalysis, fetchPlanPoints, @@ -139,6 +140,7 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise { let selected: number | null = null; let editable = false; let busy: "terrain" | "analyze" | null = null; + let lastCross: StationGround | null = null; const moved = (): boolean => !sameNodes(nodes, savedNodes); const sentPoints = () => (moved() ? pointsOf(years, nodes) : null); @@ -171,7 +173,9 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise { routes: {}, }, ); - rows = chainRows(results, choices, startM); + // 고름 = 저장된 갈래(서버 이은 누가) 위에 캐시 고름 + const saved = Object.fromEntries((analysis?.chain ?? []).map((row) => [row.year, row.mode])); + rows = chainRows(results, { ...saved, ...choices }, startM); const line = joinedLine(results, rows); const others: Vertex[][] = results.flatMap((result, i) => { const routes = PLAN_MODES.filter((mode) => mode !== rows[i].mode) @@ -264,6 +268,9 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise { i === index - 1 || i === index ? { ...entry, analysis: null } : entry, ), }; + selected = null; + lastCross = null; + cross.clear("지점을 옮겼습니다 — [분석] 뒤 측점을 누르세요."); keepDraft(); refresh(); } @@ -275,7 +282,7 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise { const result = results.find((entry) => entry.year === row.year)!; selected = chainage; map.update({ selected }); - cross.show( + lastCross = cross.show( chainage, row.year, result.routes[row.mode].planned ?? [], @@ -356,9 +363,14 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise { } await sleep(POLL_MS); } + } catch (error) { + // 진행을 못 읽음(세션 끊김 등) — 서버 일은 그대로 돎 · 다시 열면 이어 지켜봄 + showToast(`진행을 읽지 못했습니다 — ${errorText(error)}`, "error"); + return null; } finally { busy = null; bar.hidden = true; + refresh(); } } @@ -449,6 +461,7 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise { rows: () => rows, nodes: () => nodes, pick: onStation, + cross: () => lastCross, }; try {