From 3443c1c65e9f1753e1e656b35d08416c1fa298f2 Mon Sep 17 00:00:00 2001 From: godosa Date: Wed, 7 Oct 2026 00:14:38 +0200 Subject: worldmap-viewer: initial public history --- viewer/js/globecontrols.js | 88 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 viewer/js/globecontrols.js (limited to 'viewer/js/globecontrols.js') diff --git a/viewer/js/globecontrols.js b/viewer/js/globecontrols.js new file mode 100644 index 0000000..81a0807 --- /dev/null +++ b/viewer/js/globecontrols.js @@ -0,0 +1,88 @@ +// 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) }); + } +} -- cgit