Skip to content

feat(core): Controller rubberBand option - #10481

Merged
Pessimistress merged 6 commits into
masterfrom
codex/orthographic-maxbounds-rubber-band-inertia
Aug 8, 2026
Merged

feat(core): Controller rubberBand option#10481
Pessimistress merged 6 commits into
masterfrom
codex/orthographic-maxbounds-rubber-band-inertia

Conversation

@ibgreen-openai

@ibgreen-openai ibgreen-openai commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator

Background

maxBounds keeps orthographic content inside the viewport, but a hard stop can make a drag or fling feel abrupt. This RFC proposes opt-in, bounds-specific elasticity: panning can briefly overshoot under increasing resistance, then smoothly settle back inside the valid bounds.

Existing behavior remains unchanged unless the new option is explicitly enabled.

Proposed API

<OrthographicView
  controller={{
    maxBounds: contentBounds,
    maxBoundsRubberBand: true
  }}
/>
  • maxBoundsRubberBand?: boolean; default false.
  • Applies only to orthographic panning with maxBounds; it does not change minimum or maximum zoom.
  • Calculates resistance separately for each viewport axis using zoomX and zoomY.
  • Supports remapped pointer and enabled two-finger panning, native in-bounds inertia, and smooth inward releases.
  • Uses a fixed 300 ms edge rebound while preserving configured native inertia for in-bounds flings.
  • Preserves hard bounds for programmatic updates, keyboard navigation, disabled panning, and final settled view state.

Infographic

Video: actual orthographic example

The existing /examples/orthographic-view example enables maxBoundsRubberBand alongside maxBounds and can demonstrate horizontal, vertical, and diagonal overscroll with real pointer input.

Response to Xiaoji's review

Xiaoji identified the key architectural issue:

This callback reflects the user input, not user intention. The semantic action is controllerState.panEnd().

A physical pointer event is not the same thing as a semantic action: deck.gl can map ordinary dragging, modifier keys, and two-finger input to the same panStart/pan/panEnd operations. This revision therefore makes rubber-banding a property of orthographic state, not of a particular input callback.

  1. Release belongs to OrthographicState.panEnd(). The state detects elastic overscroll, computes the exact bounded endpoint, and returns the 300 ms rebound transition. It retains the actual semantic pan target before inherited inertia projects another position, so inward and diagonal releases return directly without snapping or changing axes.

  2. All input mappings use inherited controller behavior. The orthographic _onPanMoveEnd override and custom controllerState getter are gone. Ordinary pointer actions, Shift-remapped panning, and multi-touch panning all reach the same state-owned release through deck.gl's existing dispatch.

  3. Constraint behavior lives in applyConstraints(). Independent viewport axes, progressive resistance, exact-fit and non-fitting bounds, zoom constraints, and transient-frame normalization are handled by OrthographicState. A shared per-axis settled-target calculation prevents a centered non-fitting axis from being mistaken for overscroll.

  4. No persistent global bookkeeping. The earlier global WeakSet is gone. Rebound identity is private state, and a transient Symbol exists only while normalizing a real rebound frame; neither appears in emitted view state or serialization.

  5. Ordinary transitions remain native. OrthographicController again defaults to the standard LinearInterpolator. Only an actually overscrolled panEnd() selects the private rebound interpolator, so opt-out, unbounded, programmatic, keyboard, and in-bounds transitions retain their existing behavior and configured duration.

  6. Interaction state follows the semantic rebound. A one-shot state signal keeps isPanning: true while the 300 ms return is active, then normal transition completion or interruption cleans up isDragging, isPanning, and inTransition. This is consumed generically by Controller.updateViewport; no input callback needs to know about rubber-banding.

  7. Native fling semantics are preserved. The event-specific 0.3 px/ms cutoff was removed because raw pointer velocity is deliberately unavailable to the semantic, zero-argument panEnd() action. In-bounds releases keep deck.gl's configured inertia; only a view that is actually overscrolled uses the fixed 300 ms rebound.

Why the small interpolator remains

The private LinearInterpolator subclass is selected by OrthographicState.panEnd() only for a real rebound. It preserves temporary overscroll through transition-state normalization, returns an exact bounded endpoint, and recognizes a zero-duration gesture update as an interruption. The normal orthographic default remains deck.gl's standard interpolator, and the private subclass and phase symbol are absent from generated public declarations.

The RFC remains an opt-in, five-file change: one documented controller option, the orthographic state implementation, focused regression coverage, and the existing website example.

Validation

  • Rebased directly onto current master (a02590afdc).
  • Focused controller and view-state suites: 64/64 passed, including direct OrthographicState.panEnd(), Shift-remapped pointer actions, multi-touch release, interruption, active interaction state, diagonal and inward rebounds, exact-fit and non-fitting axes, native slow/fast inertia, animated programmatic updates, and private-metadata cleanup.
  • yarn --frozen-lockfile: passed; no lockfile changes.
  • yarn lint: passed.
  • yarn build: passed, including TypeScript declarations and all bundles.
  • yarn test-website: passed, including the package rebuild, Docusaurus client/server production build, static generation, and final example build.
  • yarn test-headless: 930 passed, 7 skipped, 2 failed across 210 files; all 59 controller tests passed. The CARTO raster-tile failure passed on focused rerun. The remaining WebGPU heatmap timeout reproduces unchanged on the exact current master commit above.
  • Fresh TypeScript 6 declaration emit passed. Public declarations expose maxBoundsRubberBand without the private phase symbol, rebound interpolator, or controller action overrides.
  • git diff --check: passed.

Regression coverage includes default hard clamping, all four edges, diagonal resistance, independent-axis zoom, exact-fit and undersized content, semantic and remapped pan actions, pointer and touch interruption, fixed-duration spring-back, native configured inertia, programmatic and keyboard constraints, disabled panning, unbounded views, and metadata-free public view state.

@coveralls

coveralls commented Jul 25, 2026

Copy link
Copy Markdown

Coverage Status

coverage: 83.982% (+0.08%) from 83.903% — codex/orthographic-maxbounds-rubber-band-inertia into master

@Pessimistress

Copy link
Copy Markdown
Collaborator

This implementation seems unnecessarily complicated - I especially dislike the fact that the rubberBand setting affects multiple layers of abstraction: gesture (drag, multiTouchDrag), action (pan), and normalization. Should at least push for containing the logic in applyConstraints().

The persistent statefulness in a global singleton also looks suspicious.

If the name of this option doesn't contain "maxBounds", it's reasonable to expect it to also apply to all constraints, such as maxZoom/minZoom?

@ibgreen-openai

Copy link
Copy Markdown
Collaborator Author

@Pessimistress Thanks for quick review

This implementation seems unnecessarily complicated - I especially dislike the fact that the rubberBand setting affects multiple layers of abstraction: gesture (drag, multiTouchDrag), action (pan), and normalization. Should at least push for containing the logic in applyConstraints().

Done with small exception where required as described above

The persistent statefulness in a global singleton also looks suspicious.

Removed the global state.

If the name of this option doesn't contain "maxBounds", it's reasonable to expect it to also apply to all constraints, such as maxZoom/minZoom?

Renamed to maxBoundsRubberBand we can extend later.

@ibgreen-openai

Copy link
Copy Markdown
Collaborator Author

I pushed a follow-up addressing two reproducible interaction issues in maxBoundsRubberBand.

  1. At fully zoomed-out content fit, Math.log2(viewportSize / boundsSize) and 2 ** zoom can produce a slightly inverted legal-target interval. Treating a finite collapsed or inverted interval as having one centered resting target preserves progressive rubber-band feedback even when all content is already visible. Keyboard and programmatic updates remain hard-clamped.
  2. A nonzero panend velocity is not, by itself, a flick. Once a drag has already exceeded its bounds, projecting that velocity farther outward adds an unnecessary second motion. The follow-up uses one direct, monotonic 300 ms rebound with deck.gl’s native quadratic ease-out; genuine in-bounds flings still use their longer configured inertia and are recognized only above the strict dominant-axis 0.3 px/ms threshold.

The regression coverage exercises exact and floating-point-inverted fits, undersized content, both axes and diagonals, slow and threshold-speed releases, uninterrupted spring frames, preserved gesture interruption, the complete 300 ms return, and genuine long-distance flings. The controller documentation now distinguishes rebound duration from fling inertia.

Validation: yarn vitest run --project headless test/modules/core/controllers/controllers.spec.ts (56 tests passed); yarn lint (passed).

Comment thread modules/core/src/controllers/orthographic-controller.ts Outdated

Copy link
Copy Markdown
Collaborator Author

@Pessimistress Final follow-up on the concerns above and the blocking inline review:

  • Abstraction boundary: resistance, independent-axis bounds, exact-fit handling, and temporary overscroll normalization now live in OrthographicState.applyConstraints(). Release behavior lives in the semantic OrthographicState.panEnd() action.
  • Input remapping: the orthographic _onPanMoveEnd override and custom controllerState getter are gone. Ordinary pointer, Shift-remapped, and two-finger pans all use the inherited controller dispatch.
  • Global state: the global WeakSet is gone. The remaining private Symbol exists only on transient state during normalization and does not escape through onViewStateChange, serialization, or generated declarations.
  • API scope: the option is now maxBoundsRubberBand; it applies only to orthographic maxBounds panning and does not relax min/max zoom or other controller constraints.
  • Inertia: one correction to my earlier progress comment: the 0.3 px/ms event-level cutoff was removed because raw event velocity is not part of the semantic zero-argument panEnd() action. In-bounds releases now retain deck.gl’s native configured inertia, while actual overscroll returns directly in 300 ms.

The state-layer revision is in 8b045203d7, and the direct inline response is here: #10481 (comment).

Coverage now includes direct state calls, Shift-remapped and multi-touch actions, all edges and diagonals, exact-fit/non-fitting axes, inward release, interruption, native slow/fast inertia, programmatic and keyboard hard bounds, and private-metadata cleanup. All 63 focused tests, lint, build, and website checks pass. The full headless run passes 846 tests; its sole loading-widget failure reproduces on clean master.

Could you take another look when convenient?

@ibgreen ibgreen added this to the v9.4 milestone Aug 2, 2026
@ibgreen-openai
ibgreen-openai force-pushed the codex/orthographic-maxbounds-rubber-band-inertia branch from 8b04520 to 46d1687 Compare August 7, 2026 00:40
@Pessimistress
Pessimistress force-pushed the codex/orthographic-maxbounds-rubber-band-inertia branch from 46d1687 to 62a2df6 Compare August 8, 2026 00:23
@Pessimistress Pessimistress changed the title RFC/POC: View maxBounds rubber band inertia feat(core): Controller rubberBand option Aug 8, 2026
@Pessimistress
Pessimistress marked this pull request as ready for review August 8, 2026 00:26
@Pessimistress
Pessimistress merged commit 8e66c2f into master Aug 8, 2026
5 checks passed
@Pessimistress
Pessimistress deleted the codex/orthographic-maxbounds-rubber-band-inertia branch August 8, 2026 00:53
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.

4 participants