Skip to content

ADR-062 — Sphere → flat ground patch transition at deep zoom

Status · Accepted Date · 2026-05-30 Closes into · src/lib/surface-scene/SurfaceFlatPatch.svelte (new component) + src/lib/surface-scene/SurfaceScene.svelte (transition trigger) Related · ADR-038 (per-body 2D projection), ADR-061 (regions via region_bounds), ADR-072 (shared SurfaceScene + drift consolidation), Issue #283 (Surface hotspots v2 epic — Slice 4) Triggered by · Honest-data redesign for surface hotspots: the sphere-bound circular/rectangular Tier-2 patches still misrepresent HiRISE / LROC-NAC footprints by 100× (a stylized callout, not true ground extent). True-scale rectangles need a flat-map view to be legible at any zoom

Context

The surface-hotspots v0.7 work (RFC-017, PRD-014) put high-resolution orbital imagery on the planet sphere as CircleGeometry Tier-2 patches sized at 1 unit² (≈ 100 km × 100 km in scene scale). Real HiRISE swaths are ~5 km wide; real LROC-NAC ROIs ~1 km. The patch was always a stylized "you are HERE" callout, never a true footprint.

ADR-061 / Slice 1 added region_bounds to surface sites and Slice 3a swapped the geometry from CircleGeometry to a stylized rectangle whose aspect matches the region's lon-extent vs lat-extent ratio. That's honest about shape; it's still dishonest about scale.

The user research baseline (12 mockup frames, ADR-072) shows that the redesign requires true-scale rectangles at deep zoom — and at true scale, HiRISE is sub-pixel inside CTX on a sphere-projected view. The only honest projection at that zoom is flat.

Decision

At a per-planet zoom threshold, fade the spherical scene out and materialize a flat 2D ground-patch view of the selected region. Implemented as a new src/lib/surface-scene/SurfaceFlatPatch.svelte component owned by SurfaceScene. Camera state maps both directions so the back gesture lands smoothly back on the sphere.

Trigger conditions

The flat patch becomes the active view when all three are true:

  1. A site has been selected (selected !== null).
  2. The selected site has region_bounds (Slice 1 backfilled all 35 surface sites).
  3. The camera distance camR has dropped below a per-planet threshold:
    • Moon: camR < 30.5 (just inside the Tier-2 detail-fade end at 30.5).
    • Mars: camR < 30.5 (same — Mars's Tier-2 ramp also ends at 30.5 post-consolidation).

The threshold is also gated by selection direction: zooming in past the threshold while a region is selected → flat patch appears. Manually deselecting (Esc, close-panel) or clicking the "Back to planet" button → flat patch hides, sphere returns. Wheel/pinch zoom-out past the threshold while in flat-patch mode does NOT auto-return — the user explicitly leaves via the back button to avoid jarring zoom-to-orbit mid-pan.

Animation timing

600 ms shared cubic-bezier(0.4, 0, 0.2, 1) (Material Design's "standard" ease-in-out) applied symmetrically to both layers. Same duration as Slice 2's fly-in tween (Drift 14) for consistency. During the transition:

  • Sphere opacity transitions 1 → 0 over the full 600 ms.
  • Flat-patch opacity transitions 0 → 1 over the full 600 ms.
  • Both run on the same easing curve simultaneously (single shared transition, no stagger / offset).
  • After the entering transition completes, the Three.js per-frame animation loop short-circuits its body when flatPatchPhase === 'visible' — RAF still queues (so it can resume cleanly on leaving) but no scene work runs.
  • Reverse-fade on leaving: same curve, opacities swap targets, sphere resumes per-frame work as soon as the leaving phase starts so it's rendered behind the fading-out flat patch.

Amendment from original spec: the first draft of this ADR called for an overlapping 60 % / 40 % fade with the sphere going first. In practice that read as a sequential hand-off (sphere disappears, then patch arrives) rather than a clean cross-fade. A single shared cubic-bezier without offset matches the intent better — both layers easing simultaneously creates the "you're in two views at once for a moment" feel ADR-072's design language calls for. Code reflects the shared-curve version; this paragraph documents the divergence.

Camera state mapping

  • Sphere → patch: the selected site's region_bounds centroid is the patch's centred lat/lon. The initial flat-patch zoom is set so the region fills ~60 % of the viewport diagonal (leaving room for HUD elements).
  • Patch → sphere: restore the pre-transition camR, camP, camT. Auto-spin resumes if it was on. Selection state stays put — the panel remains open with the same site.

Layer composition (rectangular & honest)

The flat patch renders four canvas 2D layers, drawn back-to-front:

  1. Base — solid dark background (matches Orrery's --color-bg).
  2. Regional — Murray Lab CTX V01 mosaic (Mars) or LROC NAC ROI mosaic (Moon) at its true ground extent, sized to region_bounds projected into screen pixels via latLonToEquirect. Toggleable via a layer chip.
  3. Detail — HiRISE (Mars) or LROC NAC closeup (Moon) at true ground extent — typically a small rectangle inside the regional layer. Toggleable.
  4. Vector overlays — traverse polyline (Mars rovers), waypoint markers (start / curated sample stops / current rover position), site label, north arrow. Markers stay at real-world scale per ADR-072 §"scale-aware marker rule" (Slice 5).

HUD

  • Scale bar bottom-left — km or mi based on locale (Orrery follows the locale's measurement system).
  • Lat/lon readout bottom-right — live, updated on pan, formatted to 4 decimals (≈ 11 m precision at planetary scales).
  • Back to planet button top-left — explicit affordance, Esc keyboard shortcut also fires it.
  • Layer toggles top-right — Regional / Detail / Traverse on/off chips, similar shape to the sphere's LayerChipRow.

Mobile

Same patch component, same layer composition. Pinch-zoom + single-finger pan replace mouse wheel + drag. The back gesture is the top-left button (no swipe-to-dismiss in v1 — that requires touch-state tracking the back gesture would interfere with the pan handler). Mobile-chromium e2e coverage in Slice 6.

Alternatives considered

  • Spherical quads following planet curvature — keep everything on the sphere but render HiRISE/CTX as curved quads at true scale. Rejected because at the scales involved (HiRISE swath ~5 km across a 3389 km Mars radius), the curved quad is visually indistinguishable from a planar one, AND the surrounding sphere texture is so much lower resolution that the HiRISE swath becomes a sub-pixel speck. The user can never get close enough to read the high-res imagery without dropping the sphere context anyway — so the flat patch is the honest move.
  • Inline flat patch as a Three.js orthographic camera over the sphere — keep one canvas, swap projections. Simpler integration but loses the ability to use canvas 2D drawing primitives (scale bar, lat/lon graticule, marker dots are all way cheaper as Canvas 2D than as Three.js objects). Rejected.
  • Full-screen overlay div with image tiles (Leaflet / OpenLayers / MapLibre) — would give us continuous zoom + tile streaming for free. Rejected because (a) we don't actually need tile streaming at v0.7 scope (only ~30 marquee regions to render), (b) the library footprint (~200 KB even minified) is large for a feature used at deep zoom on ~30 sites, (c) adding an external map library to the dependency graph crosses the bar set by AGENTS.md "no new dependencies without ADR." If #284 (base sphere texture LOD) ever justifies it, we can revisit.

Consequences

  • SurfaceScene gains state for flatPatchActive + transition progress + saved-sphere-camera. The animation loop checks flatPatchActive and skips per-frame sphere rendering when the flat patch owns the view.
  • New SurfaceFlatPatch.svelte lives next to SurfaceScene.svelte in src/lib/surface-scene/. Slim component — most logic is canvas 2D drawing.
  • Slice 5's "scale-aware marker primitive" lands inside SurfaceFlatPatch (markers shrink with zoom, never grow).
  • Slice 6 adds mobile e2e + i18n + full Playwright coverage.
  • The legacy disc-only rendering on the sphere (when region_bounds is absent on a hypothetical future site) is preserved as the fallback path — the flat-patch view is opt-in via region_bounds.
  • Future Earth-launchpad route (#285) inherits this flat-patch view for free once it adopts SurfaceScene — launchpads are inherently small regions where true scale matters.
  • Issue #284 (base sphere texture LOD) becomes lower priority — the soft mid-zoom sphere texture matters much less when the user can drop into the flat patch for real detail.

Orrery — architecture documentation · MIT · No tracking