diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Input.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Input.ts index 59e14f86a..be18299bc 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Input.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Input.ts @@ -47,6 +47,8 @@ export interface RouteEditNavigationParams { leftPan?: boolean; /** 왼쪽이 옮기기 버튼일 때 페이지가 그 누름을 잡았나(노드 · 마커) — 안 주면 포인터를 잡은 겹이 있으면 잡음 */ claimed?: () => boolean; + /** 배율 하한 — 안 주면 0.5(B05 그대로). 예정노선 지도만 한반도까지 빼려고 낮은 값을 줌(600-3). */ + minScale?: () => number; /** Ctrl+휠 = 돌리기(390-3) — 안 주면 Ctrl+휠도 확대(추천 지도). 손가락 · 트랙패드 벌림은 늘 확대. */ rotateWheel?: (sign: 1 | -1) => void; draw: () => void; @@ -81,7 +83,10 @@ export function bindRouteEditNavigation(params: RouteEditNavigationParams): void MAX_SCALE_FALLBACK, ); const factor = wheelZoomsIn(event) ? ZOOM_IN_FACTOR : ZOOM_OUT_FACTOR; - const scale = Math.min(maxScale, Math.max(MIN_SCALE, view.scale * factor)); + const scale = Math.min( + maxScale, + Math.max(params.minScale?.() ?? MIN_SCALE, view.scale * factor), + ); const ratio = scale / view.scale; const rect = canvas.getBoundingClientRect(); // 커서 자리를 **화면 중심 기준**으로 잡는다 — 그래야 그 지점이 제자리에 남는다. diff --git a/D03_PlanAnalysis/D03_PlanAnalysis_UI_Map.ts b/D03_PlanAnalysis/D03_PlanAnalysis_UI_Map.ts index fc69c07cd..54b93fc0a 100644 --- a/D03_PlanAnalysis/D03_PlanAnalysis_UI_Map.ts +++ b/D03_PlanAnalysis/D03_PlanAnalysis_UI_Map.ts @@ -16,6 +16,7 @@ * 모두 `view` 를 바꾸고 `draw()` → 한 화면 한 번 `glCamera` 로 MapLibre 를 맞춘 뒤 캔버스를 그림. * ========================================================================== */ +import { perfTime } from "@ui/ui_template_gl_map"; import { createMapTools, mapToolButton } from "@ui/ui_template_map_tools"; import { routeColor, themeColor } from "@ui/ui_template_palette"; import type { VWorldMeta } from "../B04_PreProcess/B04_PreProcess_Api_Fetch"; @@ -60,6 +61,9 @@ export function planColors(): { }; } +/** 줌아웃 한도(600-3) — 화면 폭 약 600 km(한반도 · 위성 z6). 휠 하한 배율 = 이 폭이 화면에 드는 값(B05 는 0.5 그대로). */ +export const MAP_MAX_VIEW_WIDTH_M = 600_000; + /** 위성이 없을 때 맞출 점 둘레로 둘 가짜 도엽 여유(m). */ const FAKE_MARGIN_M = 600; @@ -406,9 +410,11 @@ export function createPlanMap(events: PlanMapEvents): PlanMap { function flush(): void { cancelAnimationFrame(frame); frame = 0; - if (baseDirty) glCamera(); - if (baseDirty) paint(baseContext, true); - if (topDirty) paint(context, false); + perfTime("plan:flush", () => { + if (baseDirty) perfTime("plan:glCamera", glCamera); + if (baseDirty) perfTime("plan:paintBase", () => paint(baseContext, true)); + if (topDirty) perfTime("plan:paintTop", () => paint(context, false)); + }); baseDirty = topDirty = false; } /** 다음 화면에 한 번 — 바탕까지(화면 · 겹이 바뀜) 또는 위만(마우스). */ @@ -455,10 +461,12 @@ export function createPlanMap(events: PlanMapEvents): PlanMap { }); canvas.addEventListener("pointermove", (event) => { if (!meta || event.buttons & 4) return; // 가운데 단추 옮기는 중 - const [px, py] = at(event); - const map = screenMap(); - for (const layer of layers) layer.hover?.(px, py, map); - drawTop(); + perfTime("plan:pointermove", () => { + const [px, py] = at(event); + const map = screenMap(); + for (const layer of layers) layer.hover?.(px, py, map); + drawTop(); + }); }); canvas.addEventListener("pointerleave", () => { for (const layer of layers) layer.leave?.(); @@ -502,6 +510,13 @@ export function createPlanMap(events: PlanMapEvents): PlanMap { getMeta: () => meta, unrotateDelta: rotation.unrotateDelta, rotateWheel: rotation.wheel, + minScale: () => + meta && view.mapRect.width > 0 + ? Math.min( + 0.5, + (view.width * meta.width_meters) / (view.mapRect.width * MAP_MAX_VIEW_WIDTH_M), + ) + : 0.5, draw, }); diff --git a/ui_template/ui_template_gl_map.ts b/ui_template/ui_template_gl_map.ts index e61fe4e95..c3ec62d0b 100644 --- a/ui_template/ui_template_gl_map.ts +++ b/ui_template/ui_template_gl_map.ts @@ -65,6 +65,17 @@ export function satelliteTileSource(projectId: string): SourceSpecification { }; } +/** 느린 조작 잼(600-4) — `globalThis.__aisloPerf = true` 일 때만 `performance.measure(이름)` 으로 걸린 ms 를 남김(꺼져 있으면 그냥 실행). */ +export function perfTime(name: string, run: () => T): T { + if (!(globalThis as { __aisloPerf?: boolean }).__aisloPerf) return run(); + const start = performance.now(); + try { + return run(); + } finally { + performance.measure(name, { start, end: performance.now() }); + } +} + export function createGlMap(opts: { /** 겹 차례(아래 → 위)와 꼴 — 밝음 · 어두움이 바뀌면 다시 부름. */ specs: () => GlSpec[]; @@ -190,8 +201,8 @@ export function createGlMap(opts: { camera(center, zoom, bearingDeg) { if (!map) return; if (ready && themeKey() !== theme) apply(); - map.jumpTo({ center, zoom, bearing: bearingDeg }); - if (ready) map.redraw(); // 캔버스와 같은 화면(다음 화면으로 미루지 않음) + perfTime("gl:jumpTo", () => map!.jumpTo({ center, zoom, bearing: bearingDeg })); + if (ready) perfTime("gl:redraw", () => map!.redraw()); // 캔버스와 같은 화면(다음 화면으로 미루지 않음) }, resize: () => map?.resize(), };