{"version":3,"sources":["nav-input.mjs"],"sourcesContent":["// NAVIGATION INPUT RULES (the owner's Navigation Overhaul, confirmed Oct 7) — the pure half.\n//\n// Everything here is a rule about pointers, fingers, wheels and prices that the galaxy view (app.js) and the system view\n// (system-view.js) both have to obey the same way. It holds no DOM and no three.js, so tests/nav-input.test.mjs drives it\n// directly: a right-click, a pinch, a drag, a double tap and a trackpad's inertia are all sequences of plain numbers.\n//\n// What it decides:\n//   * pressTracker  — whether a release is a TAP (select) or not. A right or middle button, a second finger at any\n//                     point of the gesture, or movement past TAP_SLOP_PX make it not a tap. (fixes=now: a right-click\n//                     bought a trip, and a pinch's second finger reset the drag flag, so lifting it could travel.)\n//   * hitPx         — how far from a target a pointer may land and still pick it: one number per pointer kind, the\n//                     same in every view (hit=fixed: 24 px for a mouse, 44 px for a finger; planets still beat moons).\n//   * tripConfirm   — whether a trip asks first (confirm=threshold, the owner's note: \"make it ask only at more than\n//                     1/3 of energy\"). Measured against the energy you HAVE, not the tank's size: the question is how\n//                     much of what you are carrying the trip spends, so a nearly empty suit asks before small trips too.\n//   * makeDetent    — the soft stop at each level boundary (snap=detent): the push that arrives there is held, and\n//                     only a fresh push after DETENT_GAP_MS of quiet crosses, so a trackpad's inertia cannot.\n//   * makeTapDrag   — double-tap-and-drag, one-finger zoom on a phone (thumb=taphold, \"keep pinch and add double-tap\n//                     drag too\"); a double tap with no drag is a double-tap, which frames (dbl=frame).\n//   * inEdgeStrip   — the dead strip at the left and right screen edges on a phone (edge=guard).\n\n// ONE TAP THRESHOLD EVERYWHERE. It was 5 px for a click in both orbit views and 10 px for the orbit-mine drag cancel, so\n// the same small wobble was a click in one place and a drag in the next.\nexport const TAP_SLOP_PX = 8;\n\n// ONE ORBIT FEEL IN EVERY VIEW (feel=same): the system view's damping, now the galaxy's and the universe's too.\nexport const NAV_DAMPING = 0.08;\n\n// HIT RADII, in CSS pixels past the target's own drawn edge. A finger's contact patch is about 44 pt, the platform guideline\n// for a touch target; a mouse needs far less but more than the 11 px stars had.\nexport const HIT_PX = Object.freeze({ fine: 24, coarse: 44 });\nexport function hitPx(pointerType) {\n  return pointerType === 'touch' ? HIT_PX.coarse : HIT_PX.fine;\n}\n\n// A MOON BESIDE ITS PLANET: the system view prefers the planet when both are within the hit radius (today's rule, kept by\n// hit=fixed), but not when the pointer is ON the moon (0 px outside its silhouette) or clearly nearer it — by more than half\n// the hit radius — than the planet. `moonD` and `planetD` are pixels outside each body's drawn edge.\nexport function moonWins(moonD, planetD, hit) {\n  return moonD <= 0 || (planetD - moonD) > hit / 2;\n}\n\n// THE THRESHOLD FOR ASKING: more than a third of the energy you have.\nexport const CONFIRM_SHARE = 1 / 3;\nexport function tripConfirm(cost, have) {\n  const c = Number(cost), h = Number(have);\n  if (!(c > 0)) return null;                         // free, or not a trip: never asks\n  if (!(h > 0)) return null;                         // nothing to spend: the refusal says so, not a confirm\n  if (c <= h * CONFIRM_SHARE) return null;           // cheap enough to go straight away\n  const pct = Math.min(100, Math.round((c / h) * 100));\n  const n = Math.round(c * 10) / 10;\n  return { pct, text: `${n} energy is ${pct}% of your energy` };\n}\n\n// A RELEASE IS A TAP ONLY WHEN THE WHOLE GESTURE WAS ONE BUTTON-0 POINTER THAT STAYED PUT.\n// The tracker sees every pointerdown/move/up/cancel of a view (pointerId keyed). `maxMs` is optional: the system view\n// has always refused a press held past half a second (that is the orbit-mine hold); the galaxy has no hold, so none.\nexport function pressTracker({ slop = TAP_SLOP_PX, maxMs = Infinity } = {}) {\n  const down = new Map();          // pointerId -> { x, y, t }\n  let g = null;                    // the gesture: { id, button, x, y, t, multi, moved }\n  return {\n    down(e) {\n      if (!g || down.size === 0) g = { id: e.pointerId, button: e.button, x: e.clientX, y: e.clientY, t: e.t ?? 0, multi: false, moved: false };\n      else g.multi = true;          // a second pointer joined: this gesture is a pinch or a chord, never a tap\n      down.set(e.pointerId, { x: e.clientX, y: e.clientY });\n    },\n    move(e) {\n      if (!g || !down.has(e.pointerId) || e.pointerId !== g.id) return;\n      if (Math.hypot(e.clientX - g.x, e.clientY - g.y) > slop) g.moved = true;\n    },\n    // 'tap' or null. Read on the release of the pointer the gesture started with, when it is the last one up.\n    up(e) {\n      const had = down.delete(e.pointerId);\n      if (!g) return null;\n      const cur = g;\n      if (down.size === 0) g = null;     // the gesture ends when the last pointer lifts\n      if (!had || e.pointerId !== cur.id) return null;\n      if (cur.multi || down.size > 0) return null;\n      if (cur.button !== 0 || e.button !== 0) return null;\n      if (cur.moved || Math.hypot(e.clientX - cur.x, e.clientY - cur.y) > slop) return null;\n      if ((e.t ?? 0) - cur.t > maxMs) return null;\n      return 'tap';\n    },\n    // A cancelled pointer (the browser took the gesture, or a pointer left mid-press) spoils the gesture: no tap.\n    cancel(e) { down.delete(e.pointerId); if (g) { g.multi = true; if (down.size === 0) g = null; } },\n    get active() { return down.size; },\n    get dragging() { return !!(g && (g.moved || g.multi)); },\n  };\n}\n\n// SOFT DETENTS AT THE LEVEL BOUNDARIES. `push(now)` is called for every input that pushes against a boundary the camera is\n// held at. The first one arms the detent and is held; while inputs keep arriving less than DETENT_GAP_MS apart they are the\n// same push (a hand still scrolling, or a trackpad's inertia) and are held too; the first input after a quiet gap is a new\n// push, and crosses. reset() when the camera moves off the boundary, so the next arrival is held again.\nexport const DETENT_GAP_MS = 350;\nexport function makeDetent({ gapMs = DETENT_GAP_MS } = {}) {\n  let armedAt = null, last = -Infinity;\n  return {\n    push(now) {\n      if (armedAt === null) { armedAt = now; last = now; return 'hold'; }\n      const gap = now - last;\n      last = now;\n      return gap >= gapMs ? 'cross' : 'hold';\n    },\n    reset() { armedAt = null; last = -Infinity; },\n    get armed() { return armedAt !== null; },\n  };\n}\n\n// DOUBLE-TAP AND DRAG. A touch that lands within TAPDRAG_MS and TAPDRAG_PX of the end of a quick tap is captured: drag it\n// up or down and the view zooms, as in phone maps; lift it without dragging and it was a double tap. `tap` is the press\n// tracker's verdict on the first touch. Returns what the caller should do with each event.\nexport const TAPDRAG_MS = 300;\nexport const TAPDRAG_PX = 30;\nexport const TAPDRAG_WHEEL_PER_PX = 2.5;   // wheel deltaY per pixel of drag: a 40 px drag is one wheel notch\nexport function makeTapDrag({ ms = TAPDRAG_MS, px = TAPDRAG_PX, slop = TAP_SLOP_PX } = {}) {\n  let lastTap = null;   // { x, y, t } of the last quick single tap's release\n  let cap = null;       // the captured second touch: { id, x0, y0, y, dragged }\n  return {\n    // A touch went down. True when it is the second touch of a double tap and the caller should keep it from the camera.\n    down(e) {\n      if (e.pointerType !== 'touch') { lastTap = null; return false; }\n      if (cap) { cap = null; lastTap = null; return false; }      // a second finger: a pinch, which stays the camera's\n      if (lastTap && (e.t - lastTap.t) <= ms && Math.hypot(e.clientX - lastTap.x, e.clientY - lastTap.y) <= px) {\n        cap = { id: e.pointerId, x0: e.clientX, y0: e.clientY, y: e.clientY, dragged: false };\n        lastTap = null;\n        return true;\n      }\n      return false;\n    },\n    // The captured touch moved: { wheel } is the zoom to apply (a wheel deltaY, negative is in), or null.\n    move(e) {\n      if (!cap || e.pointerId !== cap.id) return null;\n      if (!cap.dragged && Math.abs(e.clientY - cap.y0) <= slop) return null;\n      cap.dragged = true;\n      const dy = e.clientY - cap.y;\n      cap.y = e.clientY;\n      return dy ? { wheel: -dy * TAPDRAG_WHEEL_PER_PX } : null;   // dragging DOWN zooms IN, as the maps do\n    },\n    // A touch lifted. 'doubletap' (frame it), 'dragend', or null. `tap` says whether this release was a quick tap.\n    up(e, tap) {\n      if (cap && e.pointerId === cap.id) { const r = cap.dragged ? 'dragend' : 'doubletap'; cap = null; return r; }\n      lastTap = (tap && e.pointerType === 'touch') ? { x: e.clientX, y: e.clientY, t: e.t } : null;\n      return null;\n    },\n    cancel() { cap = null; lastTap = null; },\n    get capturing() { return !!cap; },\n  };\n}\n\n// THE EDGE STRIP: a touch that starts this close to the left or right edge of the screen is not the camera's. On iOS a\n// drag from the left edge is Safari's Back gesture, which the game cannot prevent, only undo (app.js popstate).\nexport const EDGE_STRIP_PX = 20;\nexport function inEdgeStrip(x, width, strip = EDGE_STRIP_PX) {\n  return Number.isFinite(x) && Number.isFinite(width) && width > 0 && (x < strip || x > width - strip);\n}\n\n// THE NEAREST OF A SET OF SCREEN POINTS WITHIN A RADIUS, or null. `pts` is [{ x, y, ... }]; the item itself is returned.\nexport function nearestWithin(pts, x, y, r) {\n  let best = null, bestD = r;\n  for (const p of pts) {\n    if (!p) continue;\n    const d = Math.hypot(p.x - x, p.y - y);\n    if (d <= bestD) { bestD = d; best = p; }\n  }\n  return best;\n}\n"],"mappings":"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","names":[],"file":"nav-input.mjs"}