Files
Aislo/B05_Profile/B05_Profile_UI_RouteEdit_Rotate.ts
T
eomsangdonandClaude Opus 5.5 207c2e40eb feat(B05): 계획노선 편집 창 — 측점 사이 기울기 라벨 · 1m 등고선 켬끔 · 보던 화면 기억
- 측점 사이 종단기울기(지반고 %) 라벨 — 측점 간격이 화면 90px 이상일 때만 · 측점 라벨 맞은편 · 회전해도 글 바로 섬 · 끄는 동안 안 물음
- 「1m 등고선」 단추(기본 꺼짐 · LAS 없으면 잠김) — 1m 선 얇고 옅게 · 켰을 때 처음 한 번만 준비 · 5m 선은 한 단 짙게(--map-contour-major)
- 회전 · 가운데 · 배율을 화면 취향(routeedit-view · 프로젝트마다 · 계정에도 올라감)에 적고 다시 열면 그대로
- 700줄 한도 — 화면 셈·기억은 _View, 재기 배선은 _Measure, 바탕 읽기는 _Contour, 노드 끼우기·빼기는 _Edits 로 나눔
- 시험 test_b05_routeedit_view.py (창 크기 달라도 같은 자리 · 저장소 왕복 · 노드 편집값 자리)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015sCeZz8z8YnbovsKgn7HK7
2026-09-29 09:33:57 +09:00

89 lines
3.9 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;
/** 기억해 둔 각으로 되돌린다(계획서 18 ③). 다시 그리기는 부르는 쪽 몫. */
set: (radians: number) => void;
}
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),
set: (next) => {
radians = Number.isFinite(next) ? next : 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];
},
};
}