/* B04 지표면 3D 뷰어 방위 콤파스 — 바늘이 늘 도북(N)을 가리킨다. * * 뷰어 좌표 규약은 백엔드 scene_vertices와 같다: x, 높이, -y. 그래서 세계에서 북쪽은 * **-z** 다. 화면 위쪽은 카메라가 보는 수평 방향이므로, 카메라 오프셋(카메라 − 타깃)만 * 알면 북쪽이 화면에서 몇 도 돌아가 있는지 나온다 — `Math.atan2(offset.x, offset.z)`. * (기본 시점 offset=(0, d, d·tilt)면 0° = 북쪽이 화면 위, 카메라가 동쪽으로 가면 +90°.) * * 뷰어 파일이 이미 900줄을 넘어 여기로 뺐다(CLAUDE.md 4장 700줄 제한). */ export interface TerrainCompass { root: HTMLElement; /** 카메라 오프셋(카메라 위치 − 타깃)으로 바늘 각도를 맞춘다. */ update(offsetX: number, offsetZ: number): void; setVisible(visible: boolean): void; } const NEEDLE_SVG = ` `; export function createTerrainCompass(): TerrainCompass { const root = document.createElement("div"); root.className = "b04-surface__compass"; root.hidden = true; root.title = "도북(N)"; const dial = document.createElement("div"); dial.className = "b04-surface__compass-dial"; dial.innerHTML = NEEDLE_SVG; root.append(dial); // 프레임마다 style을 다시 쓰지 않도록 직전 각도를 들고 있는다. let lastHeading = Number.NaN; return { root, update(offsetX: number, offsetZ: number): void { const heading = (Math.atan2(offsetX, offsetZ) * 180) / Math.PI; if (Number.isFinite(lastHeading) && Math.abs(heading - lastHeading) < 0.5) return; lastHeading = heading; dial.style.transform = `rotate(${heading.toFixed(1)}deg)`; }, setVisible(visible: boolean): void { root.hidden = !visible; }, }; }