- 조작 설명을 지도 좌상단 2열 오버레이로, 상태·범례를 좌하단 오버레이로 (계획서 0-9 ⑩⑫) - 단추를 제목행 오른쪽으로 옮기고 공용 `.ui-btn` 규격을 씀, 하단 정보행 삭제 (⑪⑬) - LAS 등고선을 도엽과 같은 5m 단위로 그림 (⑭) - 바탕이 LAS 면 세류선도 등고선 범위 안쪽만 그림 (⑮) - 지도 오른쪽 위 반시계·시계 단추로 15°씩 그림 전체 회전, 집기도 따라감 (⑯) - R·L 하한을 5m 로 채움. 다만 L 하한은 칸 입력만 막음 — 기하에 걸면 거의 곧은 자리에서 R 이 부풀어 아무것도 안 고쳤는데 노선이 바뀜 (⑰) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012jsXWphgRUHAG2mFupSKPX
72 lines
3.0 KiB
TypeScript
72 lines
3.0 KiB
TypeScript
/* =============================================================================
|
|
* B05_Profile_UI_RouteEdit_Rotate.ts
|
|
* 지도 회전 — 반시계·시계 단추 하나씩, 누를 때마다 **그림 전체**가 한 칸 돈다
|
|
* (계획서 0-9 ⑯, 2026-09-12 사용자 지시).
|
|
*
|
|
* 축은 **캔버스 한가운데**다. 그리기는 캔버스 변환으로 한 번에 돌리고(글자도 함께 돈다 —
|
|
* CAD 도면과 같은 방식), 집기는 들어온 화면 자리를 **거꾸로 돌려** 안 돌린 좌표로 바꿔 쓴다.
|
|
* 그래야 눈에 보이는 자리와 잡히는 자리가 같다.
|
|
* ========================================================================== */
|
|
|
|
/** 단추 한 번에 도는 각(도) — 15°씩 스물넷이면 한 바퀴. 90°씩이면 맞출 자리가 넷뿐이고,
|
|
* 5°씩이면 한 바퀴에 일흔두 번이라 성가시다. */
|
|
const ROTATE_STEP_DEG = 15;
|
|
|
|
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];
|
|
}
|
|
|
|
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),
|
|
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];
|
|
},
|
|
};
|
|
}
|