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