Merge remote-tracking branch 'origin/sub_laptop_2' into main_laptop_1 — 586-0 겹 맞춤 · UI_Page 충돌(points + epsg) 양쪽 살림 · test_582 겹 수 낡은 단언(12 → 모두 바탕)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-29 18:46:48 +09:00
co-authored by Claude Opus 5.5
7 changed files with 360 additions and 9 deletions
+45 -5
View File
@@ -10,6 +10,7 @@
* 겹은 돌리기 전 그림 좌표로 그리고 받음 — 누른 자리는 `unrotate` 로 되돌려 넘김.
* 캔버스 두 장(582) — 아래 = 위성 · 바탕 겹(`base`) · 위 = 그은 선 · 재기 · 누름 받음.
* 마우스 움직임 = 위만 다시 그림 · 화면이 바뀌면(옮김 · 확대 · 돌림 · 겹) 둘 다 · 한 화면 한 번(rAF).
* TM 겹(그은 선 · 재기 · 그림 겹)은 경위도로 바꿔 바탕과 같은 자리(586-0 · `localTmFrame`) — 좌표계를 모르면 옛 꼴.
* ========================================================================== */
import { createMapTools, mapToolButton } from "@ui/ui_template_map_tools";
@@ -19,6 +20,8 @@ import {
computeMapRect,
computeRouteView,
createMetricProjector,
createNormalizer,
lonLatToScreen,
normalizedToScreen,
routeLineColor,
type ViewState,
@@ -27,6 +30,7 @@ import { bindRouteEditNavigation } from "../B05_Profile/B05_Profile_UI_RouteEdit
import { createMapRotation } from "../B05_Profile/B05_Profile_UI_RouteEdit_Rotate";
import { fitCanvas, screenToMetric } from "../B05_Profile/B05_Profile_UI_RouteEdit_View";
import type { Vertex } from "./logic/D03_PlanAnalysis_Plan";
import { localTmFrame, tmParams } from "./logic/D03_PlanAnalysis_Tm";
/** 측점 간격(m) — B 규칙 측점과 같음. */
export const PLAN_STATION_INTERVAL_M = 20;
@@ -48,6 +52,8 @@ export interface PlanMapScene {
image: HTMLImageElement | null;
/** 처음 맞출 점(시점 · 종점 · 그은 선). */
fit: Vertex[];
/** 작업 좌표계 — TM 겹을 경위도 바탕에 맞춤(586-0). */
epsg?: number | null;
}
/** 겹이 쓰는 좌표 — 그림 px(돌리기 전) ↔ 작업 좌표계 m · 글자 세우기. */
@@ -105,6 +111,8 @@ export interface PlanMap {
fit: () => void;
/** 검증 창구 — 작업 좌표계 점 → 화면에 보이는 px(돌린 뒤 · 그리기 전이면 null). */
screenOf: (point: Vertex) => [number, number] | null;
/** 검증 창구 — 경위도 → 화면 px(바탕 겹 꼴 · 586-0 맞춤 대조). */
screenOfLonLat: (lon: number, lat: number) => [number, number] | null;
/** 검증 창구 — 지금 돌린 각(도). */
rotationDeg: () => number;
dispose: () => void;
@@ -204,12 +212,40 @@ export function createPlanMap(events: PlanMapEvents): PlanMap {
onChange: () => draw(),
});
const screenMap = (): ScreenMap => {
const projector = createMetricProjector(meta!, view);
/** TM ↔ 그림 px — 경위도 바탕에 맞춤(586-0) · 가짜 도엽 · 모르는 좌표계 = 옛 「같은 직사각형」 꼴. */
const metricFrame = (): Pick<ScreenMap, "toScreen" | "toMetric" | "pxPerMeter"> => {
const params = tmParams(scene.epsg);
if (!params || !scene.meta || meta !== scene.meta) {
const projector = createMetricProjector(meta!, view);
return {
toScreen: (point) => projector.toScreen(point[0], point[1]),
toMetric: (px, py) => screenToMetric(meta!, view, px, py),
pxPerMeter: projector.pxPerMeter,
};
}
const normalizer = createNormalizer(scene.meta);
const [x0, y0] = normalizedToScreen(view, 0, 0);
const [x1, y1] = normalizedToScreen(view, 1, 1);
const frame = localTmFrame(
params,
(lon, lat) => lonLatToScreen(normalizer, view, lon, lat),
(px, py) => [
normalizer.lonMin + ((px - x0) / (x1 - x0)) * normalizer.lonRange,
normalizer.latMin + (1 - (py - y0) / (y1 - y0)) * normalizer.latRange,
],
view.width / 2,
view.height / 2,
);
return {
toScreen: (point) => projector.toScreen(point[0], point[1]),
toMetric: (px, py) => screenToMetric(meta!, view, px, py),
pxPerMeter: projector.pxPerMeter,
toScreen: (point) => frame.toScreen(point[0], point[1]),
toMetric: (px, py) => frame.toMetric(px, py),
pxPerMeter: frame.pxPerMeter,
};
};
const screenMap = (): ScreenMap => {
return {
...metricFrame(),
view,
...marks,
uprightRad: rotation.uprightRad(),
@@ -380,6 +416,10 @@ export function createPlanMap(events: PlanMapEvents): PlanMap {
const [x, y] = screenMap().toScreen(point);
return rotation.rerotate(x, y);
},
screenOfLonLat: (lon, lat) => {
if (!scene.meta) return null;
return rotation.rerotate(...lonLatToScreen(createNormalizer(scene.meta), view, lon, lat));
},
rotationDeg: () => (rotation.radians() * 180) / Math.PI,
dispose: () => {
observer.disconnect();