Skip to content

eagle vision: tweaks to zoom/cursor/edge scrolling#7836

Merged
mimecuvalo merged 22 commits intomainfrom
mime/eagle-vision-2
Feb 11, 2026
Merged

eagle vision: tweaks to zoom/cursor/edge scrolling#7836
mimecuvalo merged 22 commits intomainfrom
mime/eagle-vision-2

Conversation

@mimecuvalo
Copy link
Copy Markdown
Member

@mimecuvalo mimecuvalo commented Feb 4, 2026

couple tweaks to eagle vision 🦅

  • zoom to fit instead of 5% - feels like 5% is too high of a limit for a lot of boards, and then you can't actually see what you're zooming into, reduces the usability of the feature
  • using zoom to fit fixes the position being lost when going in/out of eagle vision
  • allow "quick peek" - don't updateBrush until the user has the intention to actually go to a different part of the board
  • brings back the logic to fix the cursor being (-) if alt key is being pressed, from original PR
  • adds edge scrolling

Change type

  • bugfix
  • improvement
  • feature
  • api
  • other

API changes

Added quickZoomPreservesScreenBounds to TldrawOptions (default true). When true, the quick-zoom brush maintains constant screen-pixel size as the user zooms the overview — zooming in shrinks the target viewport, zooming out expands it. Set to false to keep fixed page dimensions (original behavior).

<Tldraw options={{ quickZoomPreservesScreenBounds: false }} />

Release notes

  • eagle vision: tweaks to zoom/cursor/edge scrolling

Note

Medium Risk
Touches core viewport/camera behavior (quick-zoom camera math, brush sizing, and edge-scrolling conditions), so regressions could affect navigation/interaction feel across tools, though changes are well-covered by updated tests.

Overview
Quick zoom (zoom.zoom_quick) is reworked to compute a zoom-to-fit overview (instead of a fixed 5% zoom), preserve the page point under the cursor when zooming out, and only “commit” to moving/teleporting after crossing a drag threshold (idle vs moving), returning to the original viewport if released while idle.

Adds quickZoomPreservesScreenBounds to TldrawOptions (default true) to control whether the quick-zoom brush keeps a constant screen-pixel size as the user zooms the overview, plus UI/translation plumbing to show “Quick zoom” (shift+z) in the keyboard shortcuts dialog.

Edge scrolling is tightened to respect camera lock in EdgeScrollManager, and select-tool tick handlers now call edge scrolling only while actively dragging and not panning; ZoomTool cursor logic also avoids showing zoom-out while in quick-zoom even if Alt is held. Tests are updated/expanded to cover the new quick-zoom state machine, cursor/exit semantics, and the new option behavior.

Written by Cursor Bugbot for commit 44f0267. This will update automatically on new commits. Configure here.

@huppy-bot huppy-bot bot added the improvement Product improvement label Feb 4, 2026
@vercel
Copy link
Copy Markdown

vercel bot commented Feb 4, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
examples Ready Ready Preview Feb 11, 2026 11:29am
5 Skipped Deployments
Project Deployment Actions Updated (UTC)
analytics Ignored Ignored Preview Feb 11, 2026 11:29am
chat-template Ignored Ignored Preview Feb 11, 2026 11:29am
tldraw-docs Ignored Ignored Preview Feb 11, 2026 11:29am
tldraw-shader Ignored Ignored Preview Feb 11, 2026 11:29am
workflow-template Ignored Ignored Preview Feb 11, 2026 11:29am

Request Review

@steveruizok
Copy link
Copy Markdown
Collaborator

I still think the right zoom is minimum zoom (5%), preserving your cursor position, rather than zoom to fit. Its strange to zoom out and press this key and end up zooming back in, and on large rooms (where the whole canvas won't fit in 5% viewport) it feels strange to have the zoomed-out viewport centered on nothing. I would want to replicate the "zoomed out temporarily to minimum from my cursor location", though perhaps the right number isn't 5%?

@steveruizok
Copy link
Copy Markdown
Collaborator

Ok, more work on this... I think we were conflating the zoom brush and the mode of the interaction. Splitting this up so that we can show the zoom brush immediately, while still preserving the "require move before ending at the new bounds". Also switching from pointer moves to pointer position offsets to account for wheeling.

Rewrite ZoomQuick to use an internal 'idle'/'moving' state machine and preserve cursor position when zooming out. On enter, ZoomQuick now zooms to 10% of base zoom while keeping the pointer's page point stable, shows the zoom brush immediately, and only transitions to 'moving' after exceeding the drag threshold. While moving, the brush follows the cursor proportionally and edge-scrolling is enabled; exiting applies the appropriate zoom (returning to original viewport from idle or zooming to the new viewport from moving). EdgeScrollManager was adjusted to allow edge scrolling during zoom.zoom_quick. Tests (ZoomTool.test.ts) updated to match the new behavior and timing (added tick advances and many new assertions around idle/moving, brush sizing, exits, and cancel behavior).

Update EdgeScrollManager.ts
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages bot commented Feb 4, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
🔵 In progress
View logs
image-pipeline-template 44f0267 Commit Preview URL

Branch Preview URL
Feb 11 2026, 11:26 AM

Add validation to safely access zoomSteps[1], falling back to zoomSteps[0]
if the array only has one element. This prevents undefined access and NaN
zoom values when users configure a single-element zoomSteps array.

Co-authored-by: Steve Ruiz <[email protected]>
@mimecuvalo
Copy link
Copy Markdown
Member Author

I still think the right zoom is minimum zoom (5%), preserving your cursor position, rather than zoom to fit. Its strange to zoom out and press this key and end up zooming back in, and on large rooms (where the whole canvas won't fit in 5% viewport) it feels strange to have the zoomed-out viewport centered on nothing. I would want to replicate the "zoomed out temporarily to minimum from my cursor location", though perhaps the right number isn't 5%?

Sounds like we need to some design discussion/writer's room in person like I mentioned on Discord. I'm finding the 10%/5% choice unsatisfying in lots of cases, can show you when we meet.

Might be a HMR thing but I'm also finding that sometimes the tool gets stuck in Zoom mode — have you seen this?

@steveruizok
Copy link
Copy Markdown
Collaborator

Might be a HMR thing but I'm also finding that sometimes the tool gets stuck in Zoom mode — have you seen this?

I have, yeah.

Prevent unintended edge-scrolling and tidy related logic:

- Add early camera-lock check in EdgeScrollManager.updateEdgeScrolling to skip scrolling when camera is locked.
- Move the camera-lock check out of moveCameraWhenCloseToEdge and into the central update path (refactor).
- Add checks in SelectTool child states (Brushing, Resizing, Translating) to only call edgeScrollManager.updateEdgeScrolling when the user is dragging and not panning.
- Update ZoomQuick: adjust drag/move detection to account for zoom level and avoid edge-scrolling while camera is locked during 'moving' state.
- Remove redundant/obsolete tests in EdgeScrollManager.test.ts that asserted camera movement conditions.

These changes prevent camera movement when inappropriate (not dragging, panning, or camera-locked), fix zoom-sensitive movement detection, and consolidate the lock check to a single place.
Remove the camera lock check and the call to editor.edgeScrollManager.updateEdgeScrolling(elapsed) from the ZoomQuick 'moving' case. Edge-scrolling is no longer handled here (presumably moved or made redundant), simplifying the moving-state logic and avoiding duplicate or conflicting updates.
@steveruizok
Copy link
Copy Markdown
Collaborator

Removing edge scrolling from this feature, since we can't get the user's mouse position after it leaves the window (the user has not clicked so we can't capture the pointer). We could track the mouseleave and get an estimated exit position based on the last known position and the last known velocity but that feels like a separate system.

@steveruizok
Copy link
Copy Markdown
Collaborator

Sounds like we need to some design discussion/writer's room in person like I mentioned on Discord. I'm finding the 10%/5% choice unsatisfying in lots of cases, can show you when we meet.

My rationale here is:

  • I want to reproduce the same effect as "zooming out from the current pointer position", so its important to preserve the page position of the pointer. This gives a lot of control to the user about where the camera will be.
  • I want to allow the user to select new empty space

In practice, a user would probably zoom out to the level that lets them see everything on the screen and then stop there rather than going out to 5%. That would be a kind of expanded union of the common page shape bounds and the user's viewport, that would still preserve their pointer page position. :psyduck:

Introduce a new TldrawOptions flag quickZoomPreservesScreenBounds (default true) to control whether the quick-zoom brush preserves its screen-pixel size when the overview zoom changes. Update ZoomQuick to store an overviewZoom, compute a more robust target zoom that fits page bounds while preserving the cursor, and conditionally register a reactive updater to resize the brush when the option is enabled. Also add a cleanup hook for the reactor, adjust brush sizing in getNextVpb based on the option, and make small API/import/parameter refinements.
@huppy-bot
Copy link
Copy Markdown
Contributor

huppy-bot bot commented Feb 7, 2026

API Changes Check Passed

Great! The PR description now includes the required "### API changes" section. This helps reviewers and SDK users understand the impact of your changes.

@huppy-bot huppy-bot bot added the api API change label Feb 7, 2026
@steveruizok
Copy link
Copy Markdown
Collaborator

steveruizok commented Feb 7, 2026

Ok, some changes summarized. This should replicate the "zoom out from my mouse cursor until everything is on screen".

More realistic zoom level

The old ZoomQuick zoomed to a fixed 5% and used a small brush (1/4 screen size). The new version:

  • Zoom-to-fit overview: computes the minimum zoom needed to show the union of the current viewport and all page shapes, preserving the cursor's page position. Applies a 0.85x multiplier for breathing room, clamped to the minimum zoom step.
  • Brush matches original viewport size: instead of a small fixed brush, the brush dimensions equal the initial viewport page bounds, so you're choosing where to look at the same zoom level.
  • Cursor-proportional positioning: the brush offset within the viewport is normalized to the cursor's screen position, so the brush tracks naturally as you move.

Viewport while zooming / quickZoomPreservesScreenBounds option

New TldrawOptions field (default true). A reactive effect watches editor.getCamera().z and rescales the brush page dimensions by overviewZoom / currentZoom whenever the overview zoom changes (wheel, pinch, shortcuts). This keeps the brush the same pixel size on screen — zooming in on the overview shrinks the target viewport (higher return zoom), zooming out expands it. Set to false to keep fixed page dimensions regardless of overview zoom changes.

Edge scrolling cleanup

  • EdgeScrollManager.updateEdgeScrolling now bails immediately when the camera is locked, rather than checking isLocked deeper in moveCameraWhenCloseToEdge.
  • The isDragging/isPanning guard was moved out of moveCameraWhenCloseToEdge and into each caller (Brushing, Resizing, Translating onTick handlers), so edge scrolling only ticks while actually dragging — not while panning with spacebar.

ZoomTool cursor fix

ZoomTool.updateCursor no longer shows the alt "zoom-out" cursor while in zoom_quick state — it always shows "zoom-in" during quick zoom regardless of alt key state.

@steveruizok steveruizok added this pull request to the merge queue Feb 11, 2026
@steveruizok
Copy link
Copy Markdown
Collaborator

Landing this. The remaining issues are UX opinions but I'm reasonably happy with them, enough to learn more in the hand. My one thing is whether the zoom really should just be 10% / 5% in order to be more static and predictable, rather than dynamic based on where the cursor is on the screen.

@mimecuvalo mimecuvalo removed this pull request from the merge queue due to a manual request Feb 11, 2026
@mimecuvalo
Copy link
Copy Markdown
Member Author

we're discussing at the moment!

@mimecuvalo mimecuvalo enabled auto-merge February 11, 2026 10:58
@mimecuvalo mimecuvalo added this pull request to the merge queue Feb 11, 2026
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

this.cleanupZoomReactor = react('zoom change in quick zoom', () => {
editor.getZoomLevel()
this.updateBrush()
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reactor over-subscribes causing inconsistent brush-following behavior

Low Severity

The react() callback calls this.updateBrush(), which reads getCurrentPagePoint() and getCurrentScreenPoint() — both reactive atoms. This makes the reactor fire on every pointer move, not just zoom changes as the comment and name suggest. As a side effect, when quickZoomPreservesScreenBounds is true, the brush follows the cursor in idle state. But when false, no reactor exists and onPointerMove returns early in idle state (qzState !== 'moving'), so the brush freezes at its initial position until the drag threshold is crossed, then jumps. The quickZoomPreservesScreenBounds option unintentionally controls brush-following behavior in addition to brush-sizing.

Additional Locations (1)

Fix in Cursor Fix in Web

Merged via the queue into main with commit 90aa470 Feb 11, 2026
17 checks passed
@mimecuvalo mimecuvalo deleted the mime/eagle-vision-2 branch February 11, 2026 11:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api API change improvement Product improvement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants