// 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) }); } }