Skip to content

feat(paint): Slice A — gradient ramp brushes (#544) - #915

Merged
fernandotonon merged 6 commits into
masterfrom
feat/paint-v2-slice-a-gradient-544
Jul 23, 2026
Merged

feat(paint): Slice A — gradient ramp brushes (#544)#915
fernandotonon merged 6 commits into
masterfrom
feat/paint-v2-slice-a-gradient-544

Conversation

@fernandotonon

@fernandotonon fernandotonon commented Jul 20, 2026

Copy link
Copy Markdown
Owner

Summary

Implements Paint v2 Slice A (#544) from epic #543: paint with gradient ramps instead of a single solid colour.

  • GradientRamp — stop list + linear/stepped interpolator, 6 bundled CC0 presets (Greyscale, Hue, Gold to Rust, Sunset, Ocean, Skin Tones), JSON save/load under <AppData>/paint/ramps/
  • BrushEngine — colour source solid | gradient with linear (stroke path length), radial, and angular modes + optional per-stroke phase jitter
  • TexturePaintBuffer — per-pixel colour callback overload for radial/angular stamps
  • TexturePaintController — QML properties, FG/BG quick ramp, eyedropper-from-texture seeding, ramp editor window, Sentry paint.brush.gradient breadcrumbs; strokes stay single undo commands
  • QML — Solid/Gradient picker, mode buttons, ramp combo, jitter, Edit… → GradientRampEditor.qml

Test plan

  • UnitTests --gtest_filter='GradientRamp*:BrushEngine*' — 10/10 passed
  • TexturePaintBuffer* suite still passes
  • GUI: enable Texture paint → Brush Color → Gradient → Linear/Radial/Angular on a mesh
  • Switch bundled ramps; toggle FG/BG; Edit… → add/move/recolour stops → Save Ramp; restart and confirm custom ramp loads
  • Confirm one stroke = one undo; Sentry breadcrumb paint.brush.gradient fires with mode

Made with Cursor

Summary by CodeRabbit

  • New Features
    • Added Paint v2 gradient-ramp brush support with Solid vs Gradient sources and Linear/Radial/Angular modes.
    • Added built-in presets, stepped ramps, FG/BG ramp option, and ramp jitter.
    • Introduced an interactive gradient ramp editor to add, drag, recolor, delete, and save ramp stops, including a preview.
    • Enabled custom ramp persistence and texture-based ramp sampling.
  • Bug Fixes
    • Improved color picker by using a non-native dialog with alpha support.
  • Documentation
    • Updated Paint section help text to direct users to the dedicated brush/paint controls.
  • Tests
    • Added automated coverage for gradient sampling, brush behavior, and ramp serialization.

Add BrushEngine colour sources (solid | gradient) with linear / radial /
angular modes, six bundled CC0 ramps, FG/BG quick mode, custom ramp
editor with AppData save/load, and paint.brush.gradient breadcrumbs.

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@fernandotonon, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 23 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8dbcb771-1a72-4146-910c-95c38e0a299f

📥 Commits

Reviewing files that changed from the base of the PR and between dc49931 and 21a07cd.

📒 Files selected for processing (7)
  • qml/GradientRampEditor.qml
  • src/AppSettingsKeys.h
  • src/GradientRamp.cpp
  • src/TexturePaintBuffer.cpp
  • src/TexturePaintController.cpp
  • src/TexturePaintController.h
  • src/mainwindow.cpp
📝 Walkthrough

Walkthrough

Adds gradient ramp data and presets, linear/radial/angular brush sampling, custom ramp persistence and editing, controller settings and stroke integration, and QML controls for selecting and editing gradient ramps.

Changes

Gradient Ramp Brush

Layer / File(s) Summary
Ramp model and persistence
src/GradientRamp.*, src/GradientRamp_test.cpp, src/CMakeLists.txt, tests/CMakeLists.txt
Defines ramp stops, interpolation, presets, JSON conversion, custom ramp storage, and tests for these behaviors.
Brush sampling and per-pixel painting
src/BrushEngine.*, src/TexturePaintBuffer.*, src/GradientRamp_test.cpp
Adds gradient sampling modes and callback-based per-pixel brush colors while retaining solid-color painting.
Controller state and gradient strokes
src/TexturePaintController.*, src/AppSettingsKeys.h
Exposes gradient settings and ramp operations, persists preferences, tracks stroke state, applies sampled colors, and manages manual paint-texture fallback.
Gradient controls and ramp editor
src/mainwindow.cpp, qml/GradientRampEditor.qml, qml/PropertiesPanel.qml, src/qml_resources.qrc
Adds gradient controls, previews, interactive ramp editing, color-dialog updates, and QML resource packaging.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant MainWindow
  participant TexturePaintController
  participant BrushEngine
  participant TexturePaintBuffer
  User->>MainWindow: Select gradient source and mode
  MainWindow->>TexturePaintController: Update gradient settings
  User->>TexturePaintController: Paint stroke
  TexturePaintController->>BrushEngine: Sample gradient color
  BrushEngine-->>TexturePaintController: Return RGBA result
  TexturePaintController->>TexturePaintBuffer: Paint callback-generated colors
  TexturePaintBuffer-->>User: Update painted texture
Loading

Possibly related issues

  • Issue 544 — The changes implement gradient ramp brushes, modes, presets, custom editing, persistence, and jitter.
  • Issue 543 — The changes implement the Paint v2 Slice A gradient-ramp functionality.

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers summary and test plan, but it omits the template’s Technical Details, Features, and Bugfixes sections. Add the missing template sections, especially Technical Details plus explicit Features and Bugfixes bullets; include PS1 runtime rip items if relevant.
Docstring Coverage ⚠️ Warning Docstring coverage is 7.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately describes the main change: gradient ramp brushes for Paint v2 Slice A.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/paint-v2-slice-a-gradient-544

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.

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

Copy link
Copy Markdown

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: a89747b0bc

ℹ️ 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".

Comment thread src/TexturePaintController.cpp Outdated
Comment thread src/TexturePaintController.cpp Outdated
Native QColorDialog blocks against the Ogre render surface on Linux;
DontUseNativeDialog matches the existing background-color picker path.

Co-authored-by: Cursor <cursoragent@cursor.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/GradientRamp.cpp`:
- Around line 245-308: Prevent sanitized-stem collisions in saveCustom and
deleteCustom by reading any existing JSON file at the computed path and
verifying that it belongs to the requested ramp/name before overwriting or
removing it. If ownership cannot be confirmed, make saveCustom fail without
modifying the file and make deleteCustom return false; preserve normal behavior
when the file is absent or matches the requested ramp.

In `@src/TexturePaintController.cpp`:
- Around line 297-303: Update the vertexPaintChanged connection in
TexturePaintController to call reloadActiveRamp() only when m_useFgBgRamp is
true. Remove the m_colorSource == ColorGradient condition so static/custom ramps
are not reloaded during repeated brush-setting changes.
- Around line 781-785: Remove the duplicate
qmlRegisterSingletonType<TexturePaintController> call, relying on the existing
PropertiesPanel registration in mainwindow.cpp. Also update the surrounding load
signal connection so handled is not captured by reference beyond its lifetime,
or disconnect the connection before load returns.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8e6a23e2-5752-42f0-90c4-6fad8a23a940

📥 Commits

Reviewing files that changed from the base of the PR and between b9ef5dc and 3ec4371.

📒 Files selected for processing (16)
  • qml/GradientRampEditor.qml
  • qml/PropertiesPanel.qml
  • src/AppSettingsKeys.h
  • src/BrushEngine.cpp
  • src/BrushEngine.h
  • src/CMakeLists.txt
  • src/GradientRamp.cpp
  • src/GradientRamp.h
  • src/GradientRamp_test.cpp
  • src/TexturePaintBuffer.cpp
  • src/TexturePaintBuffer.h
  • src/TexturePaintController.cpp
  • src/TexturePaintController.h
  • src/mainwindow.cpp
  • src/qml_resources.qrc
  • tests/CMakeLists.txt

Comment thread src/GradientRamp.cpp
Comment thread src/TexturePaintController.cpp
Comment thread src/TexturePaintController.cpp
fernandotonon and others added 2 commits July 20, 2026 16:30
Keep Solid/Gradient, mode, ramp picker, FG/BG, stepped, and jitter next
to radius/strength/falloff on the paint tool menu; drop the inspector copy.

Co-authored-by: Cursor <cursoragent@cursor.com>
When the CPU buffer could not load the model texture (blank fallback),
in-place blit still targeted the original GPU texture at a different
resolution and could crash the driver. Skip in-place upload on mismatch,
drop the original handle on blank sessions, and create the texture
session eagerly when switching to texture paint.

Co-authored-by: Cursor <cursoragent@cursor.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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/mainwindow.cpp (1)

1918-2110: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding SentryReporter::addBreadcrumb for the new gradient controls.

None of the new source/mode/ramp/FG-BG/stepped/jitter/edit controls record a breadcrumb. As per coding guidelines, "Track all user-facing actions and significant operations with SentryReporter::addBreadcrumb; use ui.action for toolbar/menu actions". Sibling sliders (radius/strength/falloff/shape) in this same popup have the same gap, so this isn't a new pattern, but it's worth closing for the new controls while touching this area.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mainwindow.cpp` around lines 1918 - 2110, Add
SentryReporter::addBreadcrumb calls for each new gradient control action in the
setup around syncGradientUi: source selection, gradient mode, ramp
selection/editing, FG/BG, stepped, and jitter changes. Use the ui.action
category for the user-facing menu controls and include meaningful action
details, while preserving the existing controller updates.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/mainwindow.cpp`:
- Around line 1924-1935: The color-source buttons currently share an implicit
exclusive group with the shape buttons through their common parent. In the
paint-settings setup around srcSolid and srcGradient, create a dedicated
QButtonGroup parented to paintSettings, add both color-source buttons to it, and
preserve their checkable and exclusive behavior independently of shapeRound and
shapeSquare.

---

Nitpick comments:
In `@src/mainwindow.cpp`:
- Around line 1918-2110: Add SentryReporter::addBreadcrumb calls for each new
gradient control action in the setup around syncGradientUi: source selection,
gradient mode, ramp selection/editing, FG/BG, stepped, and jitter changes. Use
the ui.action category for the user-facing menu controls and include meaningful
action details, while preserving the existing controller updates.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 84656a8a-dd54-4683-9b66-56604c354ee4

📥 Commits

Reviewing files that changed from the base of the PR and between 3ec4371 and e6d4887.

📒 Files selected for processing (4)
  • qml/PropertiesPanel.qml
  • src/TexturePaintBuffer.cpp
  • src/TexturePaintController.cpp
  • src/mainwindow.cpp
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/TexturePaintBuffer.cpp
  • src/TexturePaintController.cpp

Comment thread src/mainwindow.cpp Outdated
fernandotonon and others added 2 commits July 20, 2026 19:15
Rebind the viewport to the paint texture at session start (not on first
stroke) and stop requiring direct entity selection — node/sub-entity
picks were skipping rebind so pixels only updated in the 2D preview.
Use dirty-rect GPU uploads on Linux/Windows and batch flushes at 33ms.

Co-authored-by: Cursor <cursoragent@cursor.com>
Improve stroke visibility with tiled GPU uploads and hit-cache sampling,
restore solid as the default colour source, and fix ramp persistence:
custom ramps win over bundled presets, stepped mode loads from saved JSON,
FG/BG-only reload on colour changes, hash-suffixed custom filenames, and
isolated color-source button groups with Sentry breadcrumbs.

Co-authored-by: Cursor <cursoragent@cursor.com>
@sonarqubecloud

Copy link
Copy Markdown

@fernandotonon

Copy link
Copy Markdown
Owner Author

Addressed review feedback in 21a07cd:

  • Custom vs bundled ramps: reloadActiveRamp() now checks saved custom JSON before bundled presets, so edited preset names round-trip correctly.
  • Stepped interpolation: loaded ramps restore gradientStepped from the ramp's saved interpolate field instead of forcing linear.
  • FG/BG reload scope: vertexPaintChanged only reloads when FG/BG mode is active (not on every radius/strength/falloff drag).
  • Filename collisions: custom ramp files use a hash suffix; delete falls back to JSON name scan for legacy files.
  • Color source buttons: dedicated QButtonGroup separate from shape round/square.
  • Sentry: gradient FG/BG, stepped, and jitter changes now breadcrumb via controller setters.

CI on this commit is green (Linux/macOS/Windows builds, unit tests, SonarCloud).

@fernandotonon
fernandotonon merged commit 3f95309 into master Jul 23, 2026
21 checks passed
@fernandotonon
fernandotonon deleted the feat/paint-v2-slice-a-gradient-544 branch July 23, 2026 16:37
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