Skip to content

Conversation

@Halvance
Copy link

@Halvance Halvance commented Dec 17, 2025

Camera Control Enhancement:

  • Added camera rotation using Alt + Middle Mouse Button
  • Works only when auto-tilt is disabled (user manual camera mode)
  • Smooth rotation with mouse drag sensitivity
  • Extended horizontal angle range to full -180..180 degrees (was -45..45)

Implementation:

  • New CameraRotationState struct to track rotation session
  • Stores initial mouse position and camera angles
  • Calculates delta movement for smooth camera updates
  • SizeAll cursor feedback during rotation
  • Configuration update for extended angle range

User Experience:

  • Intuitive drag-to-rotate camera control
  • Complements existing camera navigation tools
  • Enables full 360-degree horizontal rotation
  • Vertical angle remains clamped to safe range (0-90 degrees)

This gives users more precise control over the 3D camera view.

Summary by Sourcery

Add manual camera rotation controls and improve map canvas texture and batch handling.

New Features:

  • Enable camera rotation with Alt + Middle Mouse when auto-tilt is disabled, updating camera angles as the mouse is dragged.
  • Add background image support to the map canvas, including storage for background textures and image path resolution.
  • Introduce slots to reload textures on demand and when the game season changes, refreshing map visuals accordingly.

Enhancements:

  • Refactor cursor handling into a dedicated helper to consistently restore the cursor based on the current canvas mouse mode.
  • Add a map batches change hook to reset and refresh rendered batches when underlying data changes.

Camera Control Enhancement:
- Added camera rotation using Alt + Middle Mouse Button
- Works only when auto-tilt is disabled (user manual camera mode)
- Smooth rotation with mouse drag sensitivity
- Extended horizontal angle range to full -180..180 degrees (was -45..45)

Implementation:
- New CameraRotationState struct to track rotation session
- Stores initial mouse position and camera angles
- Calculates delta movement for smooth camera updates
- SizeAll cursor feedback during rotation
- Configuration update for extended angle range

User Experience:
- Intuitive drag-to-rotate camera control
- Complements existing camera navigation tools
- Enables full 360-degree horizontal rotation
- Vertical angle remains clamped to safe range (0-90 degrees)

This gives users more precise control over the 3D camera view.
@sourcery-ai
Copy link

sourcery-ai bot commented Dec 17, 2025

Reviewer's Guide

Implements Alt+Middle Mouse camera rotation in manual camera mode, refactors cursor handling for canvas mouse modes, and adds texture/season handling and batch reset hooks to MapCanvas.

Sequence diagram for Alt+Middle Mouse camera rotation interaction

sequenceDiagram
    actor User
    participant QtEventLoop
    participant MapCanvas
    participant MapCanvasConfig
    participant Configuration as CanvasConfiguration

    User->>QtEventLoop: Alt + Middle mouse press
    QtEventLoop->>MapCanvas: mousePressEvent(event)
    MapCanvas->>MapCanvas: read buttons/modifiers
    MapCanvas->>MapCanvasConfig: isAutoTilt()
    MapCanvasConfig-->>MapCanvas: autoTiltDisabled
    alt Alt+Middle and auto tilt disabled
        MapCanvas->>CanvasConfiguration: setConfig().canvas.advanced
        MapCanvas->>MapCanvas: create CameraRotationState
        MapCanvas->>MapCanvas: store initialMousePos and camera angles
        MapCanvas->>MapCanvas: setCursor(SizeAllCursor)
        MapCanvas-->>QtEventLoop: event->accept()
    else other mouse handling
        MapCanvas->>MapCanvas: existing press handling
    end

    loop while middle button held and mouse moves
        User->>QtEventLoop: mouse drag
        QtEventLoop->>MapCanvas: mouseMoveEvent(event)
        MapCanvas->>MapCanvas: isCameraRotating() and m_mouseMiddlePressed
        alt camera rotating
            MapCanvas->>MapCanvas: compute delta from initialMousePos
            MapCanvas->>CanvasConfiguration: update horizontalAngle
            MapCanvas->>CanvasConfiguration: update verticalAngle
            MapCanvas->>MapCanvas: slot_requestUpdate()
            MapCanvas-->>QtEventLoop: event->accept()
        else normal move handling
            MapCanvas->>MapCanvas: existing move handling
        end
    end

    User->>QtEventLoop: middle button release
    QtEventLoop->>MapCanvas: mouseReleaseEvent(event)
    MapCanvas->>MapCanvas: isCameraRotating()
    alt releasing camera rotation
        MapCanvas->>MapCanvas: m_cameraRotation.reset()
        MapCanvas->>MapCanvas: m_mouseMiddlePressed = false
        MapCanvas->>MapCanvas: restoreCursorForMouseMode()
        MapCanvas-->>QtEventLoop: event->accept()
    else other release handling
        MapCanvas->>MapCanvas: existing release handling
    end
Loading

Class diagram for updated MapCanvas camera and texture features

classDiagram
    class MapCanvas {
        // Existing responsibilities omitted
        +slot_setCanvasMouseMode(mode: CanvasMouseModeEnum) void
        +slot_onSeasonChanged(newSeason: MumeSeasonEnum) void
        +slot_reloadTextures() void
        +slot_rebuildMeshes() void
        +infomarksChanged() void
        +mapBatchesChanged() void
        +layerChanged() void
        +slot_mapChanged() void
        +slot_requestUpdate() void

        // Mouse handling
        -mousePressEvent(event: QMouseEvent*) void
        -mouseMoveEvent(event: QMouseEvent*) void
        -mouseReleaseEvent(event: QMouseEvent*) void
        -restoreCursorForMouseMode() void

        // Rendering and textures
        -actuallyPaintGL() void
        -renderBackgroundImage() void
        -loadBackgroundImageIfNeeded() void
        -paintMap() void
        -renderMapBatches() void
        -paintBatchedInfomarks() void

        // Data members
        -Batches m_batches
        -MapCanvasTextures m_textures
        -SharedMMTexture m_backgroundTexture
        -QString m_backgroundImagePath
        -CanvasMouseModeEnum m_canvasMouseMode
        -bool m_mouseLeftPressed
        -bool m_mouseRightPressed
        -bool m_mouseMiddlePressed
        -optional~CameraRotationState~ m_cameraRotation
    }

    class CameraRotationState {
        +CameraRotationState(initialMousePos: glm_vec2, initialVerticalAngle: float, initialHorizontalAngle: float)
        +glm_vec2 initialMousePos
        +float initialVerticalAngle
        +float initialHorizontalAngle
    }

    class MapCanvasConfig {
        +isAutoTilt() bool
    }

    class CanvasConfigurationAdvanced {
        +FloatOption verticalAngle
        +FloatOption horizontalAngle
        +setFloat(value: float) void
        +getFloat() float
    }

    class Batches {
        +resetExistingMeshesButKeepPendingRemesh() void
        +mapBatches: MapBatchContainer
    }

    class MapDiff {
        +resetExistingMeshesAndIgnorePendingRemesh() void
    }

    class MapCanvasTextures {
        +destroyAll() void
    }

    MapCanvas o-- CameraRotationState : uses
    MapCanvas --> MapCanvasConfig : queries_auto_tilt
    MapCanvas --> CanvasConfigurationAdvanced : updates_camera_angles
    MapCanvas --> Batches : manages_meshes
    MapCanvas --> MapDiff : manages_mesh_diff
    MapCanvas --> MapCanvasTextures : manages_textures
Loading

Flow diagram for seasonal texture reload and batch resetting

flowchart TD
    A[Texture or season change detected] --> B{Change source}
    B -->|Season change event| C["slot_onSeasonChanged(newSeason)"]
    B -->|Texture settings changed| D["slot_reloadTextures()"]

    C --> E[Check config.canvas.enableSeasonalTextures]
    E -->|disabled| F[Return without reload]
    E -->|enabled| G["setCurrentSeason(newSeason)"]
    G --> H["makeCurrent()"] --> I["m_textures.destroyAll()"] --> J["initTextures()"] --> K["doneCurrent()"]
    K --> L["m_diff.resetExistingMeshesAndIgnorePendingRemesh()"]
    L --> M["slot_requestUpdate()"]

    D --> N["makeCurrent()"] --> O["m_textures.destroyAll()"] --> P["initTextures()"] --> Q["doneCurrent()"]
    Q --> R["m_batches.resetExistingMeshesButKeepPendingRemesh()"]
    R --> S["m_diff.resetExistingMeshesAndIgnorePendingRemesh()"]
    S --> T["graphicsSettingsChanged()"]
Loading

File-Level Changes

Change Details Files
Add Alt+Middle Mouse drag-based camera rotation when auto-tilt is disabled.
  • Track middle mouse press state and Alt modifier in mousePressEvent.
  • Introduce CameraRotationState to capture initial mouse position and camera angles at rotation start.
  • On mouse move with active rotation, compute mouse delta, apply sensitivity factor, and update advanced.horizontalAngle and advanced.verticalAngle in configuration, then request a redraw.
  • On middle button release, clear rotation state, reset middle-press flag, and restore the cursor based on the current canvas mouse mode.
  • Use a SizeAll cursor during camera rotation for visual feedback.
src/display/mapcanvas.cpp
src/display/mapcanvas.h
Refactor cursor management for canvas mouse modes to be reusable after transient modes like camera rotation.
  • Extract cursor-setting logic from slot_setCanvasMouseMode into a new restoreCursorForMouseMode helper that switches on m_canvasMouseMode.
  • Update slot_setCanvasMouseMode to set m_canvasMouseMode then call restoreCursorForMouseMode instead of inlined cursor logic.
  • Call restoreCursorForMouseMode after camera rotation ends to restore the appropriate cursor.
src/display/mapcanvas.cpp
src/display/mapcanvas.h
Add season-change and texture-reload handlers to refresh OpenGL textures and map batches.
  • Add slot_onSeasonChanged to short-circuit when seasonal textures are disabled, update current season, destroy and reinitialize textures, reset diff meshes, and request an update with logging.
  • Add slot_reloadTextures to destroy and reinitialize textures on configuration changes, reset batch and diff meshes appropriately, notify graphics settings change, and log progress.
  • Expose mapBatchesChanged to reset map batches and trigger repaint.
  • Include needed headers and forward declarations for season enum and file/texture utilities.
src/display/mapcanvas.cpp
src/display/mapcanvas.h
Prepare MapCanvas for background image rendering and texture management extensions.
  • Add background texture and image path members (SharedMMTexture m_backgroundTexture and QString m_backgroundImagePath) to MapCanvas.
  • Declare renderBackgroundImage and loadBackgroundImageIfNeeded for later implementation.
  • Wire up new members with existing MapCanvas texture/batch infrastructure.
src/display/mapcanvas.h

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey there - I've reviewed your changes - here's some feedback:

  • In mouseMoveEvent, the vertical rotation comment says the motion is inverted (moving the mouse down should decrease the pitch), but the implementation adds delta.y to initialVerticalAngle; either invert the sign or adjust the comment so behavior and documentation match.
  • The camera rotation path only checks MapCanvasConfig::isAutoTilt() and the Alt modifier on mouse press; consider re-validating these conditions in mouseMoveEvent (and possibly cancelling rotation if they change mid-drag) to avoid leaving the user in a rotation state that no longer matches the current mode/modifiers.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `mouseMoveEvent`, the vertical rotation comment says the motion is inverted (moving the mouse down should decrease the pitch), but the implementation adds `delta.y` to `initialVerticalAngle`; either invert the sign or adjust the comment so behavior and documentation match.
- The camera rotation path only checks `MapCanvasConfig::isAutoTilt()` and the Alt modifier on mouse press; consider re-validating these conditions in `mouseMoveEvent` (and possibly cancelling rotation if they change mid-drag) to avoid leaving the user in a rotation state that no longer matches the current mode/modifiers.

## Individual Comments

### Comment 1
<location> `src/display/mapcanvas.cpp:479-478` </location>
<code_context>
     } else if (event->button() == Qt::BackButton) {
         slot_layerDown();
         return event->accept();
+    } else if (hasMiddleButton && hasAlt && !MapCanvasConfig::isAutoTilt()) {
+        // Alt + Middle Mouse: Camera rotation (only when auto-tilt is disabled)
+        auto &advanced = setConfig().canvas.advanced;
+        m_cameraRotation.emplace(CameraRotationState{getMouseCoords(event),
+                                                     advanced.verticalAngle.getFloat(),
+                                                     advanced.horizontalAngle.getFloat()});
+        setCursor(Qt::SizeAllCursor);
+        return event->accept();
     } else if (!m_mouseLeftPressed && m_mouseRightPressed) {
         if (m_canvasMouseMode == CanvasMouseModeEnum::MOVE && hasSel1()) {
</code_context>

<issue_to_address>
**issue (bug_risk):** Guard camera-rotation start on the actual middle-button press rather than any event while middle is held.

This branch should check `event->button() == Qt::MiddleButton` instead of `hasMiddleButton` (`event->buttons()`). As written, if the middle button is already held and the user presses another button with Alt down, we can incorrectly re-enter the camera-rotation path on a non-middle-button event and reinitialize `m_cameraRotation`. Using the specific button check avoids this unintended re-entrancy.
</issue_to_address>

### Comment 2
<location> `src/display/mapcanvas.cpp:653-654` </location>
<code_context>
+        const glm::vec2 currentMousePos = getMouseCoords(event);
+        const glm::vec2 delta = currentMousePos - m_cameraRotation->initialMousePos;
+
+        // Camera rotation sensitivity: 1.0 degree per pixel
+        constexpr float ROTATION_SENSITIVITY = 0.3f;
+
+        auto &advanced = setConfig().canvas.advanced;
</code_context>

<issue_to_address>
**nitpick:** Fix the discrepancy between the sensitivity comment and the actual constant value.

The comment states `1.0 degree per pixel`, but the constant is `0.3f`. Please either update the comment to match the current value or change the constant to `1.0f` so they stay consistent for future tuning.
</issue_to_address>

### Comment 3
<location> `src/display/mapcanvas.cpp:663-667` </location>
<code_context>
+                                         + (delta.x * ROTATION_SENSITIVITY);
+        advanced.horizontalAngle.setFloat(newHorizontalAngle);
+
+        // Vertical mouse movement controls vertical angle (pitch)
+        // Inverted: moving mouse down should decrease pitch angle
+        const float newVerticalAngle = m_cameraRotation->initialVerticalAngle
+                                       + (delta.y * ROTATION_SENSITIVITY);
+        advanced.verticalAngle.setFloat(newVerticalAngle);
+
+        slot_requestUpdate();
</code_context>

<issue_to_address>
**issue (bug_risk):** Invert the vertical delta or adjust the comment to match the actual pitch behavior.

Given Qt’s coordinate system (Y increases downward), moving the mouse down makes `delta.y` positive, so `initialVerticalAngle + (delta.y * ROTATION_SENSITIVITY)` actually *increases* the pitch angle. If the control is meant to be inverted as the comment states, this should be `initialVerticalAngle - (delta.y * ROTATION_SENSITIVITY)`. Otherwise, update the comment to describe the current non-inverted behavior accurately.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

return event->accept();
} else if (event->button() == Qt::BackButton) {
slot_layerDown();
return event->accept();
Copy link

Choose a reason for hiding this comment

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

issue (bug_risk): Guard camera-rotation start on the actual middle-button press rather than any event while middle is held.

This branch should check event->button() == Qt::MiddleButton instead of hasMiddleButton (event->buttons()). As written, if the middle button is already held and the user presses another button with Alt down, we can incorrectly re-enter the camera-rotation path on a non-middle-button event and reinitialize m_cameraRotation. Using the specific button check avoids this unintended re-entrancy.

Comment on lines +653 to +654
// Camera rotation sensitivity: 1.0 degree per pixel
constexpr float ROTATION_SENSITIVITY = 0.3f;
Copy link

Choose a reason for hiding this comment

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

nitpick: Fix the discrepancy between the sensitivity comment and the actual constant value.

The comment states 1.0 degree per pixel, but the constant is 0.3f. Please either update the comment to match the current value or change the constant to 1.0f so they stay consistent for future tuning.

Comment on lines +663 to +667
// Vertical mouse movement controls vertical angle (pitch)
// Inverted: moving mouse down should decrease pitch angle
const float newVerticalAngle = m_cameraRotation->initialVerticalAngle
+ (delta.y * ROTATION_SENSITIVITY);
advanced.verticalAngle.setFloat(newVerticalAngle);
Copy link

Choose a reason for hiding this comment

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

issue (bug_risk): Invert the vertical delta or adjust the comment to match the actual pitch behavior.

Given Qt’s coordinate system (Y increases downward), moving the mouse down makes delta.y positive, so initialVerticalAngle + (delta.y * ROTATION_SENSITIVITY) actually increases the pitch angle. If the control is meant to be inverted as the comment states, this should be initialVerticalAngle - (delta.y * ROTATION_SENSITIVITY). Otherwise, update the comment to describe the current non-inverted behavior accurately.

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.

2 participants