Skip to content

Replay: stop the map at its edges instead of panning forever - #312

Merged
SunkenInTime merged 2 commits into
mainfrom
t3/replay-canvas-bounds
Oct 16, 2026
Merged

SunkenInTime merged 2 commits into
mainfrom
t3/replay-canvas-bounds

Conversation

@SunkenInTime

@SunkenInTime SunkenInTime commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

Before and after: Split at zoom 1, map dragged as far down-right as it goes

In the replay viewer you could drag the map off the screen and keep going. The dot grid stopped at a hard line and past it there was nothing. Dara's note: "the half way method at which we are doing this sucks". This PR stops the map at its edges, the way the strategy editor's canvas does, so the replay feels like the editor people already know.

What changed

The canvas camera now holds the box the dot grid and map are drawn in (the 16:9 "world" box, the same one the editor uses) over the part of the canvas the roster leaves clear. Along each axis the box either covers that area, or sits centred in it when it is smaller, as on a very wide window. Nothing past the dot grid is reachable at any zoom.

The clamp lives in the camera itself (_MapCamera, a TransformationController whose setter runs replayCameraClamped). Pans, wheel and pinch zoom, pan fling and the watch-a-player camera all write through that one setter, so none of them can get around it. The InteractiveViewer keeps its infinite boundaryMargin on purpose. Its own bounds would treat the space under the roster as room, and its minimum-scale rule would force a zoom past 1 whenever the canvas is wider than the world box.

Watching a player still centres them, as far as the map's edge allows. Near an edge the camera stops at the edge and the player sits off-centre but on screen. The camera slides between the two with no jump.

The infinite margin came in with the first viewer commit (a784cce), before the watch camera existed, so nothing depended on it.

Seen in the release build

Windows release build, real replays from the 13.00 corpus, real mouse and keyboard.

Panning at zoom 1, then about 2x, pushing into every edge:

Panning into the edges at zoom 1 and zoomed in

Watching Sova (key 9), who starts at Split's top edge, while the round plays, then Cypher (key 2) at the bottom edge:

Following players at the top and bottom edges

Same moment, before and after. Before, the camera centred Sova and showed the blank band above the dot grid. After, the map's top edge stops at the top of the view.

Before and after: watching Sova near the top edge

Window sizes:

  • 1920x640, where the world box is narrower than the clear area: the map sits centred and dragging at zoom 1 doesn't move it. Zoomed in, it pans and stops at the edges.
  • 1264x851, the narrowest the app allows: at zoom 1 the map slides about 240 px each way from centre and no further. Zoomed in, it stops at every edge.

1920x640 window after dragging down-right

A call you might want to undo

The clamp holds the world box, not the map art inside it. That matches the editor, and the dot grid always fills the view. It also means that zoomed in to 3x you can pan into a corner of the world box that is mostly empty dot grid with a sliver of map, like this:

3x zoom, dragged into the top-left corner

Clamping to the map art's own box instead would stop at the map's outline. That's a small change to replayCameraClamped (the map box instead of the world box), but the replay would then behave differently from the editor.

One place the replay and editor differ: on a canvas wider than 16:9, InteractiveViewer's own bounds should make the editor jump past zoom 1 on the first scroll. I read that in Flutter's source and didn't check it in the app. The replay keeps the map centred there instead. I left the editor alone.

Tests

test/replay_watch_test.dart:

  • replayCameraClamped with hand-worked numbers: edge to edge, zoomed in, and a world box narrower than the view.
  • Dragging past the edge, and setting the camera from code past the edge, both stop at the edge.
  • Following a player who walks to the map's top edge: the map never leaves the top of the view, the player stays on screen and horizontally centred, and the camera ends held at the edge.

The last two fail on main and pass here. flutter test on all replay tests passes (183), and flutter analyze is clean on the touched files.

ci: analyze test

🤖 Generated with Claude Code

The replay canvas panned without limit, so a drag could push the map and
its dot grid off screen into empty space. The camera now holds the world
box over the part of the canvas the roster leaves clear, as the editor's
canvas does, and the follow camera goes through the same clamp, so a
player at the map edge stays in view, as centred as the edge allows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 2e8537df-4708-4e5f-9e2d-b8ebf43fb0c3
📥 Commits

Reviewing files that changed from the base of the PR and between 765054e and c74c7b4.

📒 Files selected for processing: 2
  • lib/widgets/replay/replay_canvas.dart
  • test/replay_watch_test.dart

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

📝 Walkthrough

The replay camera now clamps movement to map bounds and centers maps that are smaller than the viewport. Tests cover camera movement by dragging, zoomed transforms, and player following near a map edge.

Changes: Replay camera bounds

Layer / File(s) Summary
Camera clamping
lib/widgets/replay/replay_canvas.dart
replayCameraClamped centers content when its scaled size is no larger than the viewport. Otherwise, it clamps translation to keep the viewport within the content. _MapCamera applies the clamp when its value changes.
Canvas integration and tests
lib/widgets/replay/replay_canvas.dart, test/replay_watch_test.dart
The canvas supplies world and viewport dimensions to the camera controller. Tests check map-edge clamping, smaller-map centering, out-of-bounds transforms, and player following near the top map edge.

Priority: ⬇️ Low

Estimated code review effort: 2 (Simple) | ~12 minutes

Change: Bug fix

Merge Risk: ⚪ Minimal · up to c74c7

The replay map now stops at its edges, and tests cover the main cases. No merge-blocking risk is identified.

Pre-merge checks: Passed 5
Check name Status Explanation
Docstring Coverage Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 0…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the main change: preventing the replay map from panning beyond its edges.
Full details: Docstring Coverage

Explanation

No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 0 files. (2 skipped: 2 unsupported.)

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…unds stay off

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@SunkenInTime
SunkenInTime merged commit 91a1539 into main Oct 16, 2026
14 checks passed
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