Merge remote-tracking branch 'origin/sub_laptop_2' into main_laptop_1 — 586-1 GL 바탕 시험 · test_582 같은 줄 양쪽 같음(주석 살림)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
* 캔버스 두 장(582) — 아래 = 위성 · 바탕 겹(`base`) · 위 = 그은 선 · 재기 · 누름 받음.
|
||||
* 마우스 움직임 = 위만 다시 그림 · 화면이 바뀌면(옮김 · 확대 · 돌림 · 겹) 둘 다 · 한 화면 한 번(rAF).
|
||||
* TM 겹(그은 선 · 재기 · 그림 겹)은 경위도로 바꿔 바탕과 같은 자리(586-0 · `localTmFrame`) — 좌표계를 모르면 옛 꼴.
|
||||
* GL 바탕 시험(586-1 · `setGl`) — 맨 아래 MapLibre 가 이 화면을 따라감 · 캔버스 겹은 그 투영으로 그림.
|
||||
* ========================================================================== */
|
||||
|
||||
import { createMapTools, mapToolButton } from "@ui/ui_template_map_tools";
|
||||
@@ -29,6 +30,7 @@ import {
|
||||
import { bindRouteEditNavigation } from "../B05_Profile/B05_Profile_UI_RouteEdit_Input";
|
||||
import { createMapRotation } from "../B05_Profile/B05_Profile_UI_RouteEdit_Rotate";
|
||||
import { fitCanvas, screenToMetric } from "../B05_Profile/B05_Profile_UI_RouteEdit_View";
|
||||
import type { GlBase } from "./D03_PlanAnalysis_UI_MapGl";
|
||||
import type { Vertex } from "./logic/D03_PlanAnalysis_Plan";
|
||||
import { localTmFrame, tmParams } from "./logic/D03_PlanAnalysis_Tm";
|
||||
|
||||
@@ -93,6 +95,12 @@ export interface PlanMapLayer {
|
||||
leave?: () => void;
|
||||
}
|
||||
|
||||
/** 경위도 ↔ 그림 px(돌리기 전). */
|
||||
interface LonLatFrame {
|
||||
project: (lon: number, lat: number) => [number, number];
|
||||
unproject: (px: number, py: number) => [number, number];
|
||||
}
|
||||
|
||||
export interface PlanMapEvents {
|
||||
onUndo: () => void;
|
||||
onRedo: () => void;
|
||||
@@ -119,6 +127,8 @@ export interface PlanMap {
|
||||
redraw: () => void;
|
||||
/** 바탕 겹까지 다시 그림(겹 켬끔 · 받음). */
|
||||
redrawBase: () => void;
|
||||
/** GL 바탕 켬(MapLibre 를 맨 아래에) · null = 끔(지금 캔버스). */
|
||||
setGl: (gl: GlBase | null) => void;
|
||||
/** 맞출 점이 들어오게 다시 맞춤. */
|
||||
fit: () => void;
|
||||
/** 검증 창구 — 작업 좌표계 점 → 화면에 보이는 px(돌린 뒤 · 그리기 전이면 null). */
|
||||
@@ -221,6 +231,7 @@ export function createPlanMap(events: PlanMapEvents): PlanMap {
|
||||
let layers: PlanMapLayer[] = [];
|
||||
let meta: VWorldMeta | null = null;
|
||||
let fitted = false;
|
||||
let gl: GlBase | null = null;
|
||||
let view: ViewState = {
|
||||
width: 0,
|
||||
height: 0,
|
||||
@@ -235,6 +246,71 @@ export function createPlanMap(events: PlanMapEvents): PlanMap {
|
||||
onChange: () => draw(),
|
||||
});
|
||||
|
||||
/** 경위도 ↔ 그림 px(돌리기 전) — 캔버스 = 위성 직사각형에 폄. */
|
||||
const canvasLonLat = (meta: VWorldMeta): LonLatFrame => {
|
||||
const normalizer = createNormalizer(meta);
|
||||
const [x0, y0] = normalizedToScreen(view, 0, 0);
|
||||
const [x1, y1] = normalizedToScreen(view, 1, 1);
|
||||
return {
|
||||
project: (lon, lat) => lonLatToScreen(normalizer, view, lon, lat),
|
||||
unproject: (px, py) => [
|
||||
normalizer.lonMin + ((px - x0) / (x1 - x0)) * normalizer.lonRange,
|
||||
normalizer.latMin + (1 - (py - y0) / (y1 - y0)) * normalizer.latRange,
|
||||
],
|
||||
};
|
||||
};
|
||||
/** 지금 바탕의 경위도 ↔ 그림 px — GL 이면 MapLibre 투영(돌림 되돌림). */
|
||||
const lonLat = (meta: VWorldMeta): LonLatFrame => {
|
||||
const glMap = gl?.map();
|
||||
if (!glMap) return canvasLonLat(meta);
|
||||
return {
|
||||
project: (lon, lat) => {
|
||||
const point = glMap.project([lon, lat]);
|
||||
return rotation.unrotate(point.x, point.y);
|
||||
},
|
||||
unproject: (px, py) => {
|
||||
const at = glMap.unproject(rotation.rerotate(px, py));
|
||||
return [at.lng, at.lat];
|
||||
},
|
||||
};
|
||||
};
|
||||
/** GL 바탕일 때 캔버스 경위도 겹이 쓸 틀 — MapLibre 투영을 화면 가운데에서 1차로 맞춤. */
|
||||
const glView = (meta: VWorldMeta): ViewState => {
|
||||
const frame = lonLat(meta);
|
||||
const normalizer = createNormalizer(meta);
|
||||
const [lon, lat] = frame.unproject(view.width / 2, view.height / 2);
|
||||
const step = 1e-4;
|
||||
const [cx, cy] = frame.project(lon, lat);
|
||||
const [ex] = frame.project(lon + step, lat);
|
||||
const [wx] = frame.project(lon - step, lat);
|
||||
const [, ny] = frame.project(lon, lat + step);
|
||||
const [, sy] = frame.project(lon, lat - step);
|
||||
const ax = ((ex - wx) / (2 * step)) * normalizer.lonRange;
|
||||
const ay = -((ny - sy) / (2 * step)) * normalizer.latRange;
|
||||
const u = (lon - normalizer.lonMin) / normalizer.lonRange;
|
||||
const v = 1 - (lat - normalizer.latMin) / normalizer.latRange;
|
||||
return {
|
||||
...view,
|
||||
scale: 1,
|
||||
offsetX: cx - ax * u,
|
||||
offsetY: cy - ay * v,
|
||||
mapRect: { x: 0, y: 0, width: ax, height: ay },
|
||||
};
|
||||
};
|
||||
/** GL 카메라를 지금 화면(가운데 · 확대 · 돌림)에 맞춤. */
|
||||
function glCamera(): void {
|
||||
if (!gl?.map() || !scene.meta || !view.width) return;
|
||||
const frame = canvasLonLat(scene.meta);
|
||||
const [x0] = normalizedToScreen(view, 0, 0);
|
||||
const [x1] = normalizedToScreen(view, 1, 1);
|
||||
const pxPerDegree = (x1 - x0) / (scene.meta.lon_max - scene.meta.lon_min || 1);
|
||||
gl.camera(
|
||||
frame.unproject(view.width / 2, view.height / 2),
|
||||
Math.log2((pxPerDegree * 360) / 512),
|
||||
-(rotation.radians() * 180) / Math.PI,
|
||||
);
|
||||
}
|
||||
|
||||
/** TM ↔ 그림 px — 경위도 바탕에 맞춤(586-0) · 가짜 도엽 · 모르는 좌표계 = 옛 「같은 직사각형」 꼴. */
|
||||
const metricFrame = (): Pick<ScreenMap, "toScreen" | "toMetric" | "pxPerMeter"> => {
|
||||
const params = tmParams(scene.epsg);
|
||||
@@ -246,19 +322,8 @@ export function createPlanMap(events: PlanMapEvents): PlanMap {
|
||||
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,
|
||||
);
|
||||
const { project, unproject } = lonLat(scene.meta);
|
||||
const frame = localTmFrame(params, project, unproject, view.width / 2, view.height / 2);
|
||||
return {
|
||||
toScreen: (point) => frame.toScreen(point[0], point[1]),
|
||||
toMetric: (px, py) => frame.toMetric(px, py),
|
||||
@@ -269,7 +334,7 @@ export function createPlanMap(events: PlanMapEvents): PlanMap {
|
||||
const screenMap = (): ScreenMap => {
|
||||
return {
|
||||
...metricFrame(),
|
||||
view,
|
||||
view: gl?.map() && scene.meta ? glView(scene.meta) : view,
|
||||
...marks,
|
||||
uprightRad: rotation.uprightRad(),
|
||||
upright: (ctx, x, y, paint) => {
|
||||
@@ -314,7 +379,7 @@ export function createPlanMap(events: PlanMapEvents): PlanMap {
|
||||
ctx.translate(view.width / 2, view.height / 2);
|
||||
ctx.rotate(rotation.radians());
|
||||
ctx.translate(-view.width / 2, -view.height / 2);
|
||||
if (base && scene.image?.complete && scene.image.naturalWidth && scene.meta) {
|
||||
if (base && !gl && scene.image?.complete && scene.image.naturalWidth && scene.meta) {
|
||||
const [x0, y0] = normalizedToScreen(view, 0, 0);
|
||||
const [x1, y1] = normalizedToScreen(view, 1, 1);
|
||||
ctx.drawImage(scene.image, x0, y0, x1 - x0, y1 - y0);
|
||||
@@ -336,6 +401,7 @@ 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);
|
||||
baseDirty = topDirty = false;
|
||||
@@ -355,6 +421,7 @@ export function createPlanMap(events: PlanMapEvents): PlanMap {
|
||||
if (!width || !height) return;
|
||||
view = { ...view, width, height, mapRect: computeMapRect(meta, width, height) };
|
||||
if (!fitted) refit();
|
||||
gl?.resize();
|
||||
// 크기를 바꾸면 캔버스가 비므로 같은 화면에 바로 그림(빈 화면 한 번 없음)
|
||||
baseDirty = topDirty = true;
|
||||
flush();
|
||||
@@ -458,6 +525,14 @@ export function createPlanMap(events: PlanMapEvents): PlanMap {
|
||||
},
|
||||
redraw: drawTop,
|
||||
redrawBase: draw,
|
||||
setGl(next) {
|
||||
if (next === gl) return;
|
||||
gl?.enable(null);
|
||||
gl = next;
|
||||
gl?.enable(root);
|
||||
gl?.resize();
|
||||
draw();
|
||||
},
|
||||
fit() {
|
||||
refit();
|
||||
draw();
|
||||
@@ -467,12 +542,11 @@ 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));
|
||||
},
|
||||
screenOfLonLat: (lon, lat) =>
|
||||
scene.meta ? rotation.rerotate(...lonLat(scene.meta).project(lon, lat)) : null,
|
||||
rotationDeg: () => (rotation.radians() * 180) / Math.PI,
|
||||
dispose: () => {
|
||||
gl?.enable(null);
|
||||
observer.disconnect();
|
||||
cancelAnimationFrame(frame);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user