Skip to content

Improve camera zoom and pan for smoother navigation#198

Merged
fernandotonon merged 1 commit intomasterfrom
feature/smooth-camera-controls
Mar 17, 2026
Merged

Improve camera zoom and pan for smoother navigation#198
fernandotonon merged 1 commit intomasterfrom
feature/smooth-camera-controls

Conversation

@fernandotonon
Copy link
Owner

@fernandotonon fernandotonon commented Mar 16, 2026

Summary

  • Proportional zoom: Each scroll tick moves 15% of current distance instead of a fixed amount — consistent feel regardless of model size
  • Trackpad two-finger swipe = pan: Vertical swipe no longer zooms; both axes pan the camera
  • Pinch = zoom: Uses float deltas for fine-grained control (was truncating to int)
  • WASD pan proportional to distance: Slower when zoomed in, faster when far away
  • Minimum distance clamp: Prevents zooming through the target

Controls summary

Input Action
Trackpad pinch Zoom
Trackpad two-finger swipe Pan (both axes)
Mouse scroll wheel Zoom
Ctrl + scroll Zoom
WASD Pan
Arrow keys Rotate (arcball)
Middle mouse drag Rotate
Right mouse drag Pan

Test plan

  • Tested pinch zoom on macOS trackpad — smooth, fine control
  • Tested two-finger swipe — pans in all directions
  • Tested WASD — proportional speed, comfortable
  • Test mouse scroll wheel zoom on Linux/Windows
  • Test with very small and very large models

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • Improvements
    • Camera zoom now applies distance-proportional scaling for more intuitive control
    • Camera pan movement updated with distance-based proportional adjustment
    • Enhanced input handling to better differentiate gesture and scroll interactions

Zoom:
- Proportional zoom: each scroll tick moves 15% of current distance,
  so zooming feels consistent regardless of model size
- Minimum distance clamp prevents zooming through the target
- Pinch gesture uses float delta for fine-grained control

Pan:
- WASD pan is now proportional to camera distance (slower when close)
- Two-finger trackpad swipe always pans (both axes), no longer zooms
  on vertical swipe — uses Qt::MouseEventSynthesizedBySystem to
  distinguish trackpad from mouse wheel
- Mouse scroll wheel still zooms (Ctrl+scroll also zooms)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Mar 16, 2026

📝 Walkthrough

Walkthrough

Zoom and pan functionality refactored from fixed integer deltas to real-valued, distance-aware motion calculations. Input handling branches on source (Ctrl modifier, system events, mouse wheel) with proportional scaling. Gesture handling simplified for consistency.

Changes

Cohort / File(s) Summary
Method Signature Updates
src/SpaceCamera.h, src/SpaceCamera.cpp
Parameter type changed from int to Ogre::Real for zoomByDelta() and zoom() methods to support real-valued delta calculations.
Input Handling and Motion Logic
src/SpaceCamera.cpp, src/OgreWidget.cpp
Wheel event processing now branches on input source (Ctrl-modified, system-synthesized, regular scroll). Zoom now uses distance-aware proportional stepping with clamped minimum distance. Pan applies distance-based proportional scaling. Gesture handling simplified to fixed scaling factor (5.0 instead of 200 × camera speed).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A rabbit adjusts the lens with care,
From rigid integers to values fair,
Each pinch and scroll now scales by sight,
Distance-aware, the camera's flight!
No more fixed steps—proportional grace,
Keeps the target in its proper place. ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: improved zoom and pan mechanics for smoother camera navigation, which aligns with the proportional zoom, distance-aware pan, and gesture handling modifications across all three modified files.
Description check ✅ Passed The description follows the template with a clear summary section detailing all major changes (proportional zoom, trackpad two-finger swipe behavior, pinch precision, WASD proportional pan, and minimum distance clamp). The controls table and test plan provide comprehensive context, though some test items remain pending.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/smooth-camera-controls
📝 Coding Plan
  • Generate coding plan for human review comments

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

Tip

You can customize the tone of the review comments and chat replies.

Configure the tone_instructions setting to customize the tone of the review comments and chat replies. For example, you can set the tone to Act like a strict teacher, Act like a pirate and more.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/OgreWidget.cpp (1)

271-273: Consider replacing pinch scale magic number with a named constant.

Line 272 uses 5.0 inline; promoting it to a named constant (or shared camera-input tuning value) will make future sensitivity tuning safer.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/OgreWidget.cpp` around lines 271 - 273, Replace the magic literal 5.0
used when computing pinch delta with a named constant (e.g., PINCH_SCALE or
kPinchScale) and use that constant in the computation of delta in the method
where gestureEvent is handled (the code calling mCamera->zoomByDelta). Define
the constant in a visible scope for tuning (file-level static const,
anonymous-namespace const, or a static/class member on OgreWidget) so it can be
adjusted centrally, keep the same static_cast to Ogre::Real when multiplying
gestureEvent->value(), and update any references to use the new named constant.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/SpaceCamera.cpp`:
- Around line 229-234: The touchpad detection using event->source() ==
Qt::MouseEventSynthesizedBySystem is unreliable; change the logic in the scroll
handling branch (the block that currently calls pan(Ogre::Vector2(xDelta *
panScale, yDelta * panScale)) and later falls through to zoom) to also check
event->pixelDelta() (Qt 6 high-resolution scrolling) as a fallback: treat
non-zero pixelDelta() (or a sufficiently large pixelDelta magnitude) as a
touchpad/trackpad scroll and call pan(...) with the scaled pixel deltas,
otherwise fall back to angleDelta() behavior (wheel steps) and call zoom(...).
Update references around MouseEventSynthesizedBySystem, pixelDelta(),
angleDelta(), pan(), and zoom() accordingly so touchpad scrolling consistently
triggers panning across platforms.

---

Nitpick comments:
In `@src/OgreWidget.cpp`:
- Around line 271-273: Replace the magic literal 5.0 used when computing pinch
delta with a named constant (e.g., PINCH_SCALE or kPinchScale) and use that
constant in the computation of delta in the method where gestureEvent is handled
(the code calling mCamera->zoomByDelta). Define the constant in a visible scope
for tuning (file-level static const, anonymous-namespace const, or a
static/class member on OgreWidget) so it can be adjusted centrally, keep the
same static_cast to Ogre::Real when multiplying gestureEvent->value(), and
update any references to use the new named constant.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5c1bd35a-d566-499a-9aca-ddedf137ab7f

📥 Commits

Reviewing files that changed from the base of the PR and between 17c5fb0 and c71a244.

📒 Files selected for processing (3)
  • src/OgreWidget.cpp
  • src/SpaceCamera.cpp
  • src/SpaceCamera.h

Comment on lines +229 to +234
else if (event->source() == Qt::MouseEventSynthesizedBySystem)
{
// Trackpad two-finger swipe = pan in both axes
Ogre::Real panScale = 3.0f;
pan(Ogre::Vector2(xDelta * panScale, yDelta * panScale));
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In Qt (5 and 6), is QWheelEvent::source() == Qt::MouseEventSynthesizedBySystem a reliable way to detect two-finger touchpad scroll across macOS, Windows, and Linux? What is the recommended cross-platform detection signal (e.g., pixelDelta, pointingDevice type, phase)?

💡 Result:

No. QWheelEvent::source() == Qt::MouseEventSynthesizedBySystem is not a reliable (or portable) “two‑finger touchpad scroll” detector across macOS/Windows/Linux (and not even reliably on macOS): Qt explicitly warns that it was “tempting and wrong” to equate “synthesized” mouse events with “from touch”, and in practice both mouse-wheel and touchpad wheel events can report MouseEventSynthesizedBySystem. [1], [2]

Recommended cross‑platform approach (Qt 5 & Qt 6): don’t detect “touchpad”, detect high‑resolution scrolling

Use the wheel event properties that describe how the scroll is delivered, and handle both smoothly:

  1. Prefer pixelDelta() when non-null (high‑resolution / touch-like scrolling); otherwise fall back to angleDelta() (traditional wheel “steps”). [3]
  2. Use phase() (when provided) to know whether you’re starting/updating/ending a scroll sequence; for non‑phase‑aware mice it is NoScrollPhase. [4]
  3. Use inverted() to respect “natural scrolling” direction (trackpads, some mice). (Documented alongside wheel event construction in Qt docs.) [3]

Qt 6: if you truly need device-based detection, use the pointing device (not source())

In Qt 6, QWheelEvent carries a QPointingDevice* device, and Qt documents that for touchpad-originated wheel events device()->type() will be QInputDevice::DeviceType::Touchpad. [3]
That’s the closest Qt provides to an explicit “this was a touchpad” classification; availability/accuracy still depends on platform backend/device discovery, but it’s the intended API in Qt 6—not source(). [1], [3]

Practical rule of thumb (best portability):

  • Treat pixelDelta()!=QPoint(0,0) as “smooth/pixel scrolling” (often touchpad / precision scrolling), and otherwise treat it as a wheel. [3]
  • Use device()->type()==Touchpad only as an optional refinement in Qt 6. [3]

Sources:
[1] Qt blog (“Input Events in Qt 6”) on why QMouseEvent::source() was misleading for inferring touch origin (qt.io)
[2] StackOverflow report that both real mouse wheel and touchpad can show MouseEventSynthesizedBySystem (stackoverflow.com)
[3] QWheelEvent docs (Qt 6) including pixelDelta/angleDelta/phase/source/device() and note about device()->type()==Touchpad (doc.qt.io)
[4] Qt 5.7 changelog note: QWheelEvent::phase() returns NoScrollPhase for non-phase-aware mice (sources.debian.org)

Citations:


🏁 Script executed:

# Find and examine SpaceCamera.cpp around lines 229-234
fd SpaceCamera.cpp --type f

Repository: fernandotonon/QtMeshEditor

Length of output: 89


🏁 Script executed:

# Get the size and examine the wheel event handling context
wc -l src/SpaceCamera.cpp

Repository: fernandotonon/QtMeshEditor

Length of output: 93


🏁 Script executed:

# Read the wheel event handling context
cat -n src/SpaceCamera.cpp | sed -n '200,260p'

Repository: fernandotonon/QtMeshEditor

Length of output: 1976


🏁 Script executed:

# Check Qt version and includes to understand which Qt version is being used
rg "QT_VERSION|include.*Qt.*Core|QWheelEvent" src/SpaceCamera.cpp -A 2

Repository: fernandotonon/QtMeshEditor

Length of output: 226


🏁 Script executed:

# Check Qt version in CMakeLists.txt or project configuration
fd "CMakeLists.txt|\.pro|\.pri|conanfile" --type f | head -5

Repository: fernandotonon/QtMeshEditor

Length of output: 187


🏁 Script executed:

# Check Qt version in CMakeLists.txt
rg "find_package.*Qt|QT_VERSION|qt_version" CMakeLists.txt -i

Repository: fernandotonon/QtMeshEditor

Length of output: 1502


Touchpad detection via MouseEventSynthesizedBySystem is unreliable; use pixel-delta as fallback.

Qt's MouseEventSynthesizedBySystem flag is not reliable for distinguishing touchpad events across macOS, Windows, and Linux—both real mouse wheel and touchpad scroll can report it, and some platforms may not set it at all. This causes touchpad scrolling to incorrectly fall through to zoom (line 238) instead of panning.

Harden detection by checking for high-resolution scrolling:

Fix: Add pixel-delta fallback
-    else if (event->source() == Qt::MouseEventSynthesizedBySystem)
+    else if (event->source() == Qt::MouseEventSynthesizedBySystem
+             || !event->pixelDelta().isNull())
     {
         // Trackpad two-finger swipe = pan in both axes
         Ogre::Real panScale = 3.0f;
         pan(Ogre::Vector2(xDelta * panScale, yDelta * panScale));
     }

Qt 6 (which this project uses) documents that pixelDelta() indicates high-resolution/touch-like scrolling (typically trackpad), while angleDelta() is traditional wheel "steps." Falling back to pixel-delta detection ensures portable touchpad handling.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/SpaceCamera.cpp` around lines 229 - 234, The touchpad detection using
event->source() == Qt::MouseEventSynthesizedBySystem is unreliable; change the
logic in the scroll handling branch (the block that currently calls
pan(Ogre::Vector2(xDelta * panScale, yDelta * panScale)) and later falls through
to zoom) to also check event->pixelDelta() (Qt 6 high-resolution scrolling) as a
fallback: treat non-zero pixelDelta() (or a sufficiently large pixelDelta
magnitude) as a touchpad/trackpad scroll and call pan(...) with the scaled pixel
deltas, otherwise fall back to angleDelta() behavior (wheel steps) and call
zoom(...). Update references around MouseEventSynthesizedBySystem, pixelDelta(),
angleDelta(), pan(), and zoom() accordingly so touchpad scrolling consistently
triggers panning across platforms.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c71a24485e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// Proportional zoom: step size scales with distance to target,
// so zooming feels consistent regardless of model size.
Ogre::Real distance = std::abs(mCameraNode->getPosition().z);
Ogre::Real minDistance = 0.01f;

Choose a reason for hiding this comment

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

P2 Badge Clamp zoom minimum to camera near clip distance

Using a hardcoded minDistance = 0.01f lets the camera stop much closer to the target than the configured near clip plane (setNearClipDistance(1.0f) in SpaceCamera::SpaceCamera), so zooming in can place the focal region inside the near plane and make geometry disappear/clamp oddly at close range. This regression comes from the new minimum-distance clamp; it should be derived from the active near clip distance (or another scene-scale-aware lower bound) instead of 0.01.

Useful? React with 👍 / 👎.

@sonarqubecloud
Copy link

@fernandotonon fernandotonon merged commit 62f4159 into master Mar 17, 2026
17 checks passed
@fernandotonon fernandotonon deleted the feature/smooth-camera-controls branch March 17, 2026 00:32
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