// globecontrols.js — mouse input for the globe camera. Left-drag pans (the grabbed ground follows the pointer);
// the compass heading stays put while panning; right-drag or Shift+drag turns (sideways) and tilts (up = toward the horizon); the wheel zooms toward the target.
// Touch: one finger pans; two fingers pinch (zoom), twist (turn) and move up/down together (tilt).
import { rotatePose, keepHeading } from './camera.js';
import { Touches } from './gestures.js';
export const TURN_DEG_PER_PX = 0.25;
export const TILT_DEG_PER_PX = 0.2;
const WHEEL_PER_PX = 0.001;
export class GlobeControls {
constructor(view, el) {
Object.assign(this, { view, el, enabled: false, drag: null, touches: new Touches() });
el.addEventListener('pointerdown', e => this.#down(e));
el.addEventListener('pointermove', e => this.#move(e));
el.addEventListener('pointerup', e => this.#up(e));
el.addEventListener('pointercancel', e => this.#up(e));
el.addEventListener('wheel', e => this.#wheel(e), { passive: false });
el.addEventListener('contextmenu', e => { if (this.enabled) e.preventDefault(); }); // right-drag turns the globe
}
#at(e) {
const r = this.el.getBoundingClientRect();
return { x: e.clientX - r.left, y: e.clientY - r.top };
}
#down(e) {
if (!this.enabled) return;
const at = this.#at(e), turn = e.button === 2 || (e.button === 0 && e.shiftKey);
if (e.pointerType === 'touch') {
this.touches.down(e.pointerId, at);
try { this.el.setPointerCapture?.(e.pointerId); } catch { /* a synthetic or already-ended pointer */ }
if (this.touches.size >= 2) { this.drag = null; return; } // a second finger: a gesture, not a pan
}
if (e.button !== 0 && !turn) return;
this.#grab(at, e.pointerId, turn);
}
#grab(at, id, turn = false) {
const grab = turn ? null : this.view.pickVec(at.x, at.y);
if (!turn && !grab) return; // pressed on space or sky: nothing to hold
this.drag = { turn, grab, r: grab && Math.hypot(grab[0], grab[1], grab[2]), x: at.x, y: at.y, id };
try { this.el.setPointerCapture?.(id); } catch { /* a synthetic or already-ended pointer */ }
}
#move(e) {
if (!this.enabled) return;
if (e.pointerType === 'touch') {
const g = this.touches.move(e.pointerId, this.#at(e));
if (g) return this.#gesture(g);
}
const d = this.drag;
if (!d || e.pointerId !== d.id) return; // one pointer drives
const at = this.#at(e), p = this.view.pose;
if (d.turn) {
this.view.setPose({ ...p, heading: p.heading + (at.x - d.x) * -TURN_DEG_PER_PX, pitch: p.pitch - (at.y - d.y) * TILT_DEG_PER_PX });
Object.assign(d, at);
return;
}
for (let i = 0; i < 4; i++) { // a rigid turn would swing north around: keep the heading, re-aim, repeat (converges fast)
const cur = this.view.pickVec(at.x, at.y, d.r); // the pointer ray at the grabbed ground's height
if (!cur) break;
this.view.setPose(keepHeading(p, rotatePose(this.view.pose, cur, d.grab)));
}
}
#gesture(g) { // pinch = zoom, twist = turn with the fingers, both up/down = tilt
const p = this.view.pose;
this.view.setPose({ ...p, range: p.range / g.scale, heading: p.heading - g.rot, pitch: p.pitch - g.tilt * TILT_DEG_PER_PX });
}
#up(e) {
try { if (this.drag) this.el.releasePointerCapture?.(this.drag.id); } catch { /* not captured */ }
this.drag = null;
if (e?.pointerType !== 'touch') return;
const wasPair = this.touches.size >= 2;
this.touches.up(e.pointerId);
const rest = wasPair && this.touches.size === 1 ? this.touches.other(e.pointerId) : null;
if (rest && this.enabled) this.#grab(rest.at, rest.id); // one finger left: it pans on from where it is
}
#wheel(e) {
if (!this.enabled) return;
e.preventDefault();
const px = e.deltaY * (e.deltaMode === 1 ? 33 : e.deltaMode === 2 ? 800 : 1), p = this.view.pose;
this.view.setPose({ ...p, range: p.range * Math.exp(px * WHEEL_PER_PX) });
}
}