84 lines
3.7 KiB
TypeScript
84 lines
3.7 KiB
TypeScript
/* =============================================================================
|
|
* B05_Profile_UI_RouteEdit_Rotate.ts
|
|
* 지도 회전 — 반시계·시계 단추 하나씩, 누를 때마다 **그림 전체**가 한 칸 돈다
|
|
* (계획서 0-9 ⑯, 2026-09-12 사용자 지시).
|
|
*
|
|
* 축은 **캔버스 한가운데**다. 그리기는 캔버스 변환으로 한 번에 돌리고(글자도 함께 돈다 —
|
|
* CAD 도면과 같은 방식), 집기는 들어온 화면 자리를 **거꾸로 돌려** 안 돌린 좌표로 바꿔 쓴다.
|
|
* 그래야 눈에 보이는 자리와 잡히는 자리가 같다.
|
|
* ========================================================================== */
|
|
|
|
/** 단추 한 번에 도는 각(도) — 15°씩 스물넷이면 한 바퀴. 90°씩이면 맞출 자리가 넷뿐이고,
|
|
* 5°씩이면 한 바퀴에 일흔두 번이라 성가시다. */
|
|
const ROTATE_STEP_DEG = 15;
|
|
|
|
/** **글자를 눈높이로 세울지**(계획서 0-9 ㉚, 2026-09-12 사용자 지시).
|
|
*
|
|
* 그림만 돌고 숫자는 늘 바로 서게 한다 — 180° 로 돌리면 글자가 뒤집혀 안 읽히기 때문이다.
|
|
* 비용은 라벨 하나에 변환 한 번뿐이라 그림을 다시 그리는 값에 묻힌다.
|
|
*
|
|
* ⚠ **되돌리려면 이 값을 `false` 로만 바꾸면 된다** — 그러면 글자도 그림과 함께 돈다
|
|
* (CAD 도면과 같은 방식). 사용자가 화면을 보고 판단할 수 있게 한 자리에 모아 두었다. */
|
|
export const UPRIGHT_LABELS = true;
|
|
|
|
export interface MapRotationParams {
|
|
/** 단추가 들어 있는 모달 — `[data-act="rotate-ccw"]`·`rotate-cw` 를 찾는다. */
|
|
overlay: HTMLElement;
|
|
/** 지금 캔버스 크기 — 회전축(한가운데)을 잡는 데 쓴다. */
|
|
size: () => { width: number; height: number };
|
|
/** 각이 바뀌었다 — 호출부가 다시 그린다. */
|
|
onChange: () => void;
|
|
}
|
|
|
|
export interface MapRotation {
|
|
/** 지금 돌린 각(라디안). 그리기가 캔버스 변환에 그대로 쓴다. */
|
|
radians: () => number;
|
|
/** 화면에 보이는 자리 → 그리기 좌표(돌리기 전). */
|
|
unrotate: (px: number, py: number) => [number, number];
|
|
/** 그리기 좌표 → 화면에 보이는 자리. 떠 있는 패널을 노드 옆에 붙일 때 쓴다. */
|
|
rerotate: (px: number, py: number) => [number, number];
|
|
/** 화면에서 민 만큼(dx, dy) → 그림 좌표의 만큼. 팬·휠 확대 보정용. */
|
|
unrotateDelta: (dx: number, dy: number) => [number, number];
|
|
/** 글자를 세울 각(라디안) — 그리기가 라벨마다 이만큼 되돌린다. 안 세우면 0. */
|
|
uprightRad: () => number;
|
|
}
|
|
|
|
export function createMapRotation(params: MapRotationParams): MapRotation {
|
|
let radians = 0;
|
|
|
|
const spin = (px: number, py: number, angle: number): [number, number] => {
|
|
if (!angle) return [px, py];
|
|
const { width, height } = params.size();
|
|
const cx = width / 2;
|
|
const cy = height / 2;
|
|
const cos = Math.cos(angle);
|
|
const sin = Math.sin(angle);
|
|
const dx = px - cx;
|
|
const dy = py - cy;
|
|
return [cx + dx * cos - dy * sin, cy + dx * sin + dy * cos];
|
|
};
|
|
|
|
for (const [act, sign] of [
|
|
["rotate-ccw", -1],
|
|
["rotate-cw", 1],
|
|
] as const) {
|
|
params.overlay.querySelector(`[data-act="${act}"]`)?.addEventListener("click", () => {
|
|
radians += (sign * ROTATE_STEP_DEG * Math.PI) / 180;
|
|
params.onChange();
|
|
});
|
|
}
|
|
|
|
return {
|
|
radians: () => radians,
|
|
unrotate: (px, py) => spin(px, py, -radians),
|
|
rerotate: (px, py) => spin(px, py, radians),
|
|
uprightRad: () => (UPRIGHT_LABELS ? -radians : 0),
|
|
unrotateDelta: (dx, dy) => {
|
|
if (!radians) return [dx, dy];
|
|
const cos = Math.cos(-radians);
|
|
const sin = Math.sin(-radians);
|
|
return [dx * cos - dy * sin, dx * sin + dy * cos];
|
|
},
|
|
};
|
|
}
|