/* ============================================================================= * 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]; }, }; }