worldmap-viewer

git clone https://git.godosa.eu/worldmap-viewer

master

raw · 4030 bytes

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