Merge remote-tracking branch 'origin/sub_laptop_2' into main_laptop_1 — 582 두 장 캔버스 · UI_Map 충돌(sub1 누름 잡기 · 놓음 + sub2 위만 다시 그림) 양쪽 살림
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,8 @@
|
||||
* 회전 `createMapRotation`(B05 그대로 · 단추 `[data-act="rotate-ccw|cw"]` · Ctrl+휠).
|
||||
* 왼쪽 단추 = 겹 몫(점 찍기 · 마크 · 재기) — 왼쪽 끌기 옮기기 없음(설정 창 규칙 = 가운데 단추 옮김 · 휠 방향).
|
||||
* 겹은 돌리기 전 그림 좌표로 그리고 받음 — 누른 자리는 `unrotate` 로 되돌려 넘김.
|
||||
* 캔버스 두 장(582) — 아래 = 위성 · 바탕 겹(`base`) · 위 = 그은 선 · 재기 · 누름 받음.
|
||||
* 마우스 움직임 = 위만 다시 그림 · 화면이 바뀌면(옮김 · 확대 · 돌림 · 겹) 둘 다 · 한 화면 한 번(rAF).
|
||||
* ========================================================================== */
|
||||
|
||||
import { createMapTools, mapToolButton } from "@ui/ui_template_map_tools";
|
||||
@@ -66,6 +68,8 @@ export interface ScreenMap {
|
||||
|
||||
/** 평면 위 겹 — 그리기 · 누름(참 = 받음 · 0 왼쪽 · 2 오른쪽) · 마우스 곁(왼쪽 끌기 중도) · 놓음 · 나감. */
|
||||
export interface PlanMapLayer {
|
||||
/** 바탕 겹(아래 캔버스 · 마우스 움직임엔 안 다시 그림). */
|
||||
base?: boolean;
|
||||
draw: (context: CanvasRenderingContext2D, map: ScreenMap) => void;
|
||||
press?: (px: number, py: number, map: ScreenMap, button: 0 | 2) => boolean;
|
||||
hover?: (px: number, py: number, map: ScreenMap) => void;
|
||||
@@ -93,8 +97,10 @@ export interface PlanMap {
|
||||
update: (patch: Partial<PlanMapScene>) => void;
|
||||
setLayers: (layers: PlanMapLayer[]) => void;
|
||||
setTools: (tools: Partial<PlanTools>) => void;
|
||||
/** 다시 그림(겹 값이 바뀜). */
|
||||
/** 위 겹 다시 그림(그은 선 · 재기 값이 바뀜). */
|
||||
redraw: () => void;
|
||||
/** 바탕 겹까지 다시 그림(겹 켬끔 · 받음). */
|
||||
redrawBase: () => void;
|
||||
/** 맞출 점이 들어오게 다시 맞춤. */
|
||||
fit: () => void;
|
||||
/** 검증 창구 — 작업 좌표계 점 → 화면에 보이는 px(돌린 뒤 · 그리기 전이면 null). */
|
||||
@@ -131,6 +137,8 @@ function fakeMeta(points: Vertex[]): VWorldMeta | null {
|
||||
export function createPlanMap(events: PlanMapEvents): PlanMap {
|
||||
const root = document.createElement("div");
|
||||
root.className = "d03-plan__map";
|
||||
const baseCanvas = document.createElement("canvas");
|
||||
baseCanvas.className = "d03-plan__base";
|
||||
const canvas = document.createElement("canvas");
|
||||
let marks = { stations: true, slopes: true };
|
||||
const markButton = (act: "stations" | "slopes", title: string): HTMLButtonElement => {
|
||||
@@ -140,7 +148,7 @@ export function createPlanMap(events: PlanMapEvents): PlanMap {
|
||||
onClick: () => {
|
||||
marks = { ...marks, [act]: !marks[act] };
|
||||
button.setAttribute("aria-pressed", String(marks[act]));
|
||||
draw();
|
||||
drawTop();
|
||||
},
|
||||
});
|
||||
return button;
|
||||
@@ -175,7 +183,8 @@ export function createPlanMap(events: PlanMapEvents): PlanMap {
|
||||
});
|
||||
const toolbar = tools.root;
|
||||
toolbar.classList.add("d03-plan__tools");
|
||||
root.append(canvas, toolbar);
|
||||
root.append(baseCanvas, canvas, toolbar);
|
||||
const baseContext = baseCanvas.getContext("2d")!;
|
||||
const context = canvas.getContext("2d")!;
|
||||
let scene: PlanMapScene = { meta: null, image: null, fit: [] };
|
||||
let layers: PlanMapLayer[] = [];
|
||||
@@ -236,33 +245,58 @@ export function createPlanMap(events: PlanMapEvents): PlanMap {
|
||||
fitted = true;
|
||||
}
|
||||
|
||||
function draw(): void {
|
||||
context.clearRect(0, 0, view.width, view.height);
|
||||
/** 한 장 그림 — 아래 = 위성 + 바탕 겹 · 위 = 나머지 겹. */
|
||||
function paint(ctx: CanvasRenderingContext2D, base: boolean): void {
|
||||
ctx.clearRect(0, 0, view.width, view.height);
|
||||
if (!meta) return;
|
||||
context.save();
|
||||
context.translate(view.width / 2, view.height / 2);
|
||||
context.rotate(rotation.radians());
|
||||
context.translate(-view.width / 2, -view.height / 2);
|
||||
if (scene.image?.complete && scene.image.naturalWidth && scene.meta) {
|
||||
ctx.save();
|
||||
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) {
|
||||
const [x0, y0] = normalizedToScreen(view, 0, 0);
|
||||
const [x1, y1] = normalizedToScreen(view, 1, 1);
|
||||
context.drawImage(scene.image, x0, y0, x1 - x0, y1 - y0);
|
||||
ctx.drawImage(scene.image, x0, y0, x1 - x0, y1 - y0);
|
||||
}
|
||||
const map = screenMap();
|
||||
for (const layer of layers) {
|
||||
context.save();
|
||||
layer.draw(context, map);
|
||||
context.restore();
|
||||
if (!layer.base !== !base) continue;
|
||||
ctx.save();
|
||||
layer.draw(ctx, map);
|
||||
ctx.restore();
|
||||
}
|
||||
context.restore();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
let frame = 0;
|
||||
let baseDirty = false;
|
||||
let topDirty = false;
|
||||
/** 모인 다시 그림을 지금 그림. */
|
||||
function flush(): void {
|
||||
cancelAnimationFrame(frame);
|
||||
frame = 0;
|
||||
if (baseDirty) paint(baseContext, true);
|
||||
if (topDirty) paint(context, false);
|
||||
baseDirty = topDirty = false;
|
||||
}
|
||||
/** 다음 화면에 한 번 — 바탕까지(화면 · 겹이 바뀜) 또는 위만(마우스). */
|
||||
function schedule(base: boolean): void {
|
||||
baseDirty ||= base;
|
||||
topDirty = true;
|
||||
if (!frame) frame = requestAnimationFrame(flush);
|
||||
}
|
||||
const draw = (): void => schedule(true);
|
||||
const drawTop = (): void => schedule(false);
|
||||
|
||||
function resize(): void {
|
||||
fitCanvas(baseCanvas, baseContext);
|
||||
const { width, height } = fitCanvas(canvas, context);
|
||||
if (!width || !height) return;
|
||||
view = { ...view, width, height, mapRect: computeMapRect(meta, width, height) };
|
||||
if (!fitted) refit();
|
||||
draw();
|
||||
// 크기를 바꾸면 캔버스가 비므로 같은 화면에 바로 그림(빈 화면 한 번 없음)
|
||||
baseDirty = topDirty = true;
|
||||
flush();
|
||||
}
|
||||
|
||||
/** 누른 화면 자리 → 그림 px(돌리기 전). */
|
||||
@@ -277,31 +311,31 @@ export function createPlanMap(events: PlanMapEvents): PlanMap {
|
||||
const map = screenMap();
|
||||
if (layers.some((layer) => layer.press?.(px, py, map, 0))) {
|
||||
canvas.setPointerCapture(event.pointerId); // 끌기가 캔버스 밖으로 나가도 놓음을 받음
|
||||
draw();
|
||||
drawTop();
|
||||
}
|
||||
});
|
||||
canvas.addEventListener("pointerup", (event) => {
|
||||
if (event.button !== 0) return;
|
||||
for (const layer of layers) layer.release?.();
|
||||
draw();
|
||||
drawTop();
|
||||
});
|
||||
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);
|
||||
draw();
|
||||
drawTop();
|
||||
});
|
||||
canvas.addEventListener("pointerleave", () => {
|
||||
for (const layer of layers) layer.leave?.();
|
||||
draw();
|
||||
drawTop();
|
||||
});
|
||||
canvas.addEventListener("contextmenu", (event) => {
|
||||
event.preventDefault();
|
||||
if (!meta) return;
|
||||
const [px, py] = at(event);
|
||||
const map = screenMap();
|
||||
if (layers.some((layer) => layer.press?.(px, py, map, 2))) draw();
|
||||
if (layers.some((layer) => layer.press?.(px, py, map, 2))) drawTop();
|
||||
});
|
||||
bindRouteEditNavigation({
|
||||
canvas,
|
||||
@@ -335,7 +369,8 @@ export function createPlanMap(events: PlanMapEvents): PlanMap {
|
||||
if (tools.measure !== undefined)
|
||||
measureButton.setAttribute("aria-pressed", String(tools.measure));
|
||||
},
|
||||
redraw: draw,
|
||||
redraw: drawTop,
|
||||
redrawBase: draw,
|
||||
fit() {
|
||||
refit();
|
||||
draw();
|
||||
@@ -346,6 +381,9 @@ export function createPlanMap(events: PlanMapEvents): PlanMap {
|
||||
return rotation.rerotate(x, y);
|
||||
},
|
||||
rotationDeg: () => (rotation.radians() * 180) / Math.PI,
|
||||
dispose: () => observer.disconnect(),
|
||||
dispose: () => {
|
||||
observer.disconnect();
|
||||
cancelAnimationFrame(frame);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user