Skip to content

fix(web): scroll-wheel zoom — overscale detent + clean floor stop#13

Merged
beetlebugorg merged 2 commits into
mainfrom
bugfix/overscale-zoom
Jun 25, 2026
Merged

fix(web): scroll-wheel zoom — overscale detent + clean floor stop#13
beetlebugorg merged 2 commits into
mainfrom
bugfix/overscale-zoom

Conversation

@beetlebugorg

Copy link
Copy Markdown
Owner

Replace MapLibre's native scrollZoom with a dedicated WheelZoom handler so the two overscale limits feel right when zooming with the wheel/trackpad:

  • Overscale detent: zoom-IN stops dead at the onset of overscale — the zoom whose displayed scale equals the covering chart's compilation scale (coverScale), the same boundary the HUD's "Overscale xN" warning uses. The whole scroll AND its momentum tail are absorbed, so a "throw" can't punch through; you continue into overscale by scrolling again (a fresh gesture releases it). Re-arms once you zoom back below.

  • Floor: the 1:MIN_DETAIL_SCALE scale floor is computed live per wheel event (getFloor) instead of read from map.getMaxZoom() — which is only re-applied on moveend, so mid-gesture it could be stale-high and let you sail past the floor and bounce back ~a second later on moveend. Now it stops dead at the real max, with a small elastic over-pull that springs back in ~one frame (latched, so a momentum tail can't hold the spring-back open).

Speed: each wheel step accumulates onto a private target and applies instantly (duration 0). Reading map.getZoom() instead crawled — easeTo({duration:0}) only updates the camera next frame, so multiple events per frame collapsed to one step.

HUD updateZoomCap no longer slams the band cap into setMaxZoom (the hard clamp that caused both the sticky feel and the bounce); it records the overscale- onset zoom as a detent exposed via getDetentZoom(). _applyScaleFloor sets maxZoom = floor + FLOOR_GIVE for the elastic headroom. own-ship drops its own wheel takeover and just registers a follow anchor with WheelZoom, so vessel- anchored zoom and the detent/floor behavior share one handler.

beetlebugorg and others added 2 commits June 25, 2026 12:07
Replace MapLibre's native scrollZoom with a dedicated WheelZoom handler so
the two overscale limits feel right when zooming with the wheel/trackpad:

- Overscale detent: zoom-IN stops dead at the onset of overscale — the zoom
  whose displayed scale equals the covering chart's compilation scale
  (coverScale), the same boundary the HUD's "Overscale xN" warning uses. The
  whole scroll AND its momentum tail are absorbed, so a "throw" can't punch
  through; you continue into overscale by scrolling again (a fresh gesture
  releases it). Re-arms once you zoom back below.

- Floor: the 1:MIN_DETAIL_SCALE scale floor is computed live per wheel event
  (getFloor) instead of read from map.getMaxZoom() — which is only re-applied
  on moveend, so mid-gesture it could be stale-high and let you sail past the
  floor and bounce back ~a second later on moveend. Now it stops dead at the
  real max, with a small elastic over-pull that springs back in ~one frame
  (latched, so a momentum tail can't hold the spring-back open).

Speed: each wheel step accumulates onto a private target and applies instantly
(duration 0). Reading map.getZoom() instead crawled — easeTo({duration:0})
only updates the camera next frame, so multiple events per frame collapsed to
one step.

HUD updateZoomCap no longer slams the band cap into setMaxZoom (the hard clamp
that caused both the sticky feel and the bounce); it records the overscale-
onset zoom as a detent exposed via getDetentZoom(). _applyScaleFloor sets
maxZoom = floor + FLOOR_GIVE for the elastic headroom. own-ship drops its own
wheel takeover and just registers a follow anchor with WheelZoom, so vessel-
anchored zoom and the detent/floor behavior share one handler.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…g it

WheelZoom no longer exposes a mutable setAnchorProvider() that plugins reach in
and call (single-slot, last-writer-wins, and it coupled own-ship to WheelZoom's
API). Anchoring is now an injected accessor — getAnchor() — symmetric with
getDetent/getFloor: WheelZoom just asks where to anchor.

The shell is the broker. ChartPlotter._zoomAnchor() composes the anchor from the
plugins that can contribute one (first non-null wins; null → cursor), and hands
that to WheelZoom. own-ship drops its WheelZoom dependency entirely and instead
exposes a zoomAnchor() capability (vessel while following a fix, else null) that
the shell reads. Future camera plugins slot into the broker without WheelZoom
changing, and multiple contributors compose instead of clobbering.

Behavior unchanged; verified the injected path end to end.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@beetlebugorg beetlebugorg merged commit c1651c0 into main Jun 25, 2026
4 checks passed
@beetlebugorg beetlebugorg deleted the bugfix/overscale-zoom branch June 25, 2026 16:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant