Skip to content

Color spaces implementation#34

Open
cone-forest wants to merge 3 commits intomasterfrom
feature/color-spaces
Open

Color spaces implementation#34
cone-forest wants to merge 3 commits intomasterfrom
feature/color-spaces

Conversation

@cone-forest
Copy link
Collaborator

@cone-forest cone-forest commented May 2, 2025

  • Rename mr::Color to mr::RGBAColor
  • Fix deprecated literal suffix syntax
  • First color spaces implementation for mr::Color

Summary by CodeRabbit

  • New Features

    • Introduced distinct color types (RGBA, HSLA, HSVA, CMYK, OKLAB) with unified operations and added color-space conversion support (including OKLAB).
  • Documentation

    • Updated usage examples to reference the new RGBA color type.
  • Style

    • Renamed color type in debugger visualizations for clarity.
  • Tests

    • Updated tests and test names to use the new RGBA color type and fixed minor comment/literal formatting.

@coderabbitai
Copy link

coderabbitai bot commented May 2, 2025

Walkthrough

Refactors the color subsystem: replaces the single Color type with multiple specific types (RGBAColor, HSLAColor, HSVAColor, CMYKColor, OKLABColor), adds a CRTP ColorBase for common ops, implements RGBA→OKLAB conversion, and updates docs, tests, and natvis to use RGBAColor.

Changes

Cohort / File(s) Change Summary
Core color implementation
include/mr-math/color.hpp
Introduced ColorBase CRTP template; replaced monolithic Color with RGBAColor (with constructors, component accessors, format methods), and added HSLAColor, HSVAColor, CMYKColor, OKLABColor. Added convert<RGBAColor, OKLABColor> specialization. Updated user-defined literal and structured-binding specializations.
Documentation
USAGE.md
Replaced uses of mr::Color with mr::RGBAColor in examples and code blocks.
Debugger visualizer
mr-math.natvis
Updated natvis type name from mr::math::Color to mr::math::RGBAColor.
Tests
tests/misc.cpp
Migrated tests from mr::Color to mr::RGBAColor, renamed test suite, fixed a comment typo and adjusted literals to the new type.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant User
    participant RGBAColor
    participant ColorBase
    participant OKLABColor

    Note right of RGBAColor #DDEEFF: Construction & access
    User->>RGBAColor: construct(from floats/ints/hex/Vec4)
    RGBAColor->>ColorBase: use common ops (tuple, math, stream)
    User->>RGBAColor: access/set components / format (argb/bgra/abgr)
    User->>RGBAColor: request conversion to OKLAB
    RGBAColor->>OKLABColor: convert<RGBAColor,OKLABColor>(rgba)
    OKLABColor-->>User: OKLABColor instance
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

I hopped through code with nimble paws,
Swapping Color for clearer laws.
RGBA leads the rainbow band,
OKLAB joins the rabbit band.
Tests and docs now sing in tune,
A hopping patch beneath the moon. 🐰🎨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.11% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title “Color spaces implementation” succinctly and accurately describes the primary change in this pull request, which introduces support for multiple color spaces and refactors the existing color type; it is clear, concise, and focused on the main enhancement.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/color-spaces

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


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 and usage tips.

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

🔭 Outside diff range comments (2)
mr-math.natvis (1)

107-116: 🧹 Nitpick (assertive)

Spurious closing brace in the display string
R={r()}, G={g()}, B={b()}, A={a()}} has one brace too many, so Visual Studio will show an Invalid Expression error for the whole visualiser.

-<DisplayString>R={r()}, G={g()}, B={b()}, A={a()}}</DisplayString>
+<DisplayString>R={r()}, G={g()}, B={b()}, A={a()}</DisplayString>
USAGE.md (1)

220-220: 🧹 Nitpick (assertive)

Typo: epsionepsilon

-float eps = mr::epsion<float>();
+float eps = mr::epsilon<float>();
📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f5efe00 and ae0812d.

📒 Files selected for processing (4)
  • USAGE.md (2 hunks)
  • include/mr-math/color.hpp (3 hunks)
  • mr-math.natvis (1 hunks)
  • tests/misc.cpp (3 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
include/mr-math/color.hpp (3)
include/mr-math/matr.hpp (18)
  • other (117-126)
  • other (117-117)
  • other (128-133)
  • other (128-128)
  • other (135-140)
  • other (135-135)
  • other (142-150)
  • other (142-142)
  • other (152-158)
  • other (152-152)
  • other (160-166)
  • other (160-160)
  • nodiscard (169-171)
  • nodiscard (173-175)
  • nodiscard (178-191)
  • _data (321-324)
  • _data (383-386)
  • _data (455-458)
include/mr-math/operators.hpp (2)
  • s (133-141)
  • s (134-134)
include/mr-math/def.hpp (2)
  • epsilon (35-37)
  • epsilon (35-35)
tests/misc.cpp (1)
include/mr-math/color.hpp (6)
  • RGBAColor (76-76)
  • RGBAColor (79-80)
  • RGBAColor (82-83)
  • RGBAColor (86-93)
  • RGBAColor (95-101)
  • RGBAColor (95-95)
🪛 markdownlint-cli2 (0.17.2)
USAGE.md

190-190: Fenced code blocks should be surrounded by blank lines
null

(MD031, blanks-around-fences)


192-192: Fenced code blocks should be surrounded by blank lines
null

(MD031, blanks-around-fences)


197-197: Fenced code blocks should be surrounded by blank lines
null

(MD031, blanks-around-fences)


199-199: Lists should be surrounded by blank lines
null

(MD032, blanks-around-lists)


199-199: Lists should be surrounded by blank lines
null

(MD032, blanks-around-lists)


200-200: Fenced code blocks should be surrounded by blank lines
null

(MD031, blanks-around-fences)


205-205: Fenced code blocks should be surrounded by blank lines
null

(MD031, blanks-around-fences)


206-206: Lists should be surrounded by blank lines
null

(MD032, blanks-around-lists)


206-206: Lists should be surrounded by blank lines
null

(MD032, blanks-around-lists)


207-207: Fenced code blocks should be surrounded by blank lines
null

(MD031, blanks-around-fences)

🪛 GitHub Actions: Benchmarks
include/mr-math/color.hpp

[error] 29-54: Syntax errors due to invalid use of 'this' keyword in method parameters and undeclared identifiers 'self', 'other', and 'eps'.


[error] 213-213: No matching function for call to 'mr::math::Vec<float, 3>::Vec(mr::math::RGBAColor&)'. Invalid static_cast from RGBAColor to Vec3f.

⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: build (windows-latest, Release, cl)
  • GitHub Check: build (windows-latest, Debug, cl)
  • GitHub Check: build (ubuntu-24.04, Debug, clang)
🔇 Additional comments (5)
include/mr-math/color.hpp (1)

213-230: convert<RGBAColor, OKLABColor> still fails without the implicit Vec3f conversion
After replacing the “deducing this” operators you must adjust this line:

auto v = static_cast<Vec3f>(rgba) * Matr3f{ … };

Because static_cast<Vec3f>(rgba) now works (see earlier fix) this part will compile, but note:

  • You need <cmath> for cbrtf.
  • Vec3f * Matr3f looks backwards – shouldn’t it be Matr3f * Vec3f? Double-check the intended order.

No code suggestion here, only a heads-up to verify the math once the build is green.

🧰 Tools
🪛 GitHub Actions: Benchmarks

[error] 213-213: No matching function for call to 'mr::math::Vec<float, 3>::Vec(mr::math::RGBAColor&)'. Invalid static_cast from RGBAColor to Vec3f.

tests/misc.cpp (4)

34-45: Tests updated to use mr::RGBAColor

All constructor tests have been correctly updated to reference the new RGBAColor type, including float, integer, and packed‐integer overloads and the user‐defined literal. The assertions align with the implementations in include/mr-math/color.hpp.


35-35: Verify default‐constructor behavior

The test assumes RGBAColor() yields (0,0,0,0). Please confirm that the default constructor zero‐initializes all channels (including alpha) rather than defaulting alpha to 1.0 or leaving uninitialized memory.


77-77: Setters test updated to RGBAColor

The setter test now correctly asserts the updated RGBA channel values against the RGBAColor instance. Good use of set(index, value) following r(…).


94-95: Clarify addition semantics and potential clamping

The test allows channel values to exceed 1.0 (e.g., alpha sums to 2.0). Please verify whether RGBAColor::operator+ is intended to produce unclamped results or should clamp channels to [0,1]. If clamping is desired, consider adding a saturating‐add function or adjust operator behavior.

Comment on lines +17 to +20
template <typename InputT, typename OutputT>
OutputT convert(InputT input) {
static_assert(false, "You have to specify this function for your particular types");
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

convert is currently unusable – the unconditional static_assert(false) aborts every build
With the present implementation the primary template can never be instantiated, so even a valid call (one that has a full specialization) will trigger the assertion before the compiler has a chance to pick the specialization. Make the assertion dependent on the template parameters:

-template <typename InputT, typename OutputT>
-  OutputT convert(InputT input) {
-    static_assert(false, "You have to specify this function for your particular types");
-  }
+template <typename...>
+inline constexpr bool _always_false_v = false;
+
+template <typename InputT, typename OutputT>
+constexpr OutputT convert(InputT /*unused*/) {
+  static_assert(_always_false_v<InputT, OutputT>,
+                "No `convert` specialization found for the supplied types");
+}

This stops the hard error, allows the compiler to look for specializations, and still provides a clear diagnostic when none are found.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
template <typename InputT, typename OutputT>
OutputT convert(InputT input) {
static_assert(false, "You have to specify this function for your particular types");
}
template <typename...>
inline constexpr bool _always_false_v = false;
template <typename InputT, typename OutputT>
constexpr OutputT convert(InputT /*unused*/) {
static_assert(_always_false_v<InputT, OutputT>,
"No `convert` specialization found for the supplied types");
}

Comment on lines +49 to +55
bool operator==(this const ColorDerived &self, const ColorDerived &other) noexcept {
return self._data == other._data;
}

bool equal(this const ColorDerived &self, const ColorDerived &other, ValueT eps = epsilon<ValueT>()) noexcept {
return self._data.equal(other._data, eps);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

operator== / equal suffer from the same “deducing this” issue
Rewrite using the conventional form and preserve const-correctness:

-bool operator==(this const ColorDerived &self, const ColorDerived &other) noexcept {
-  return self._data == other._data;
-}
-
-bool equal(this const ColorDerived &self, const ColorDerived &other, ValueT eps = epsilon<ValueT>()) noexcept {
-  return self._data.equal(other._data, eps);
-}
+constexpr bool operator==(const ColorDerived &other) const noexcept {
+  return static_cast<const ColorDerived &>(*this)._data == other._data;
+}
+
+constexpr bool equal(const ColorDerived &other,
+                     ValueT eps = epsilon<ValueT>()) const noexcept {
+  return static_cast<const ColorDerived &>(*this)._data.equal(other._data, eps);
+}

This removes the compilation errors and keeps behaviour identical.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
bool operator==(this const ColorDerived &self, const ColorDerived &other) noexcept {
return self._data == other._data;
}
bool equal(this const ColorDerived &self, const ColorDerived &other, ValueT eps = epsilon<ValueT>()) noexcept {
return self._data.equal(other._data, eps);
}
constexpr bool operator==(const ColorDerived &other) const noexcept {
return static_cast<const ColorDerived &>(*this)._data == other._data;
}
constexpr bool equal(const ColorDerived &other,
ValueT eps = epsilon<ValueT>()) const noexcept {
return static_cast<const ColorDerived &>(*this)._data.equal(other._data, eps);
}

Comment on lines +27 to +38
// structured binding support
template <size_t I> requires (I < 4)
ValueT get(this const ColorDerived &self) { return self._data[I]; }

operator Vec3f(this ColorDerived &self) noexcept {
return (Vec3f)self._data;
}

operator Vec4f(this ColorDerived &self) noexcept {
return self._data;
}

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

deducing this syntax isn’t yet supported by our tool-chain – replace it with ordinary methods
All the functions that use
operator Vec3f(this ColorDerived &self) / ValueT get(this const ColorDerived &self) etc. are rejected by both GCC 13 and Clang 17 (see CI errors 29-54). A drop-in, C++20-compatible rewrite is straightforward:

-// structured binding support
-template <size_t I> requires (I < 4)
-  ValueT get(this const ColorDerived &self) { return self._data[I]; }
-
-operator Vec3f(this ColorDerived &self) noexcept {
-  return (Vec3f)self._data;
-}
-
-operator Vec4f(this ColorDerived &self) noexcept {
-  return self._data;
-}
+// structured binding support
+template <size_t I> requires (I < 4)
+constexpr ValueT get() const noexcept {
+  static_assert(I < 4);
+  return static_cast<const ColorDerived &>(*this)._data[I];
+}
+
+/* implicit conversions */
+constexpr operator Vec3f() const noexcept {
+  const auto &d = static_cast<const ColorDerived &>(*this)._data;
+  return Vec3f{d.x(), d.y(), d.z()};          // avoid the “no Vec3f(Vec4f) ctor” error
+}
+constexpr operator Vec4f() const noexcept {
+  return static_cast<const ColorDerived &>(*this)._data;
+}

Apply the same pattern to every other member that currently relies on the P0847 “deducing this” feature.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// structured binding support
template <size_t I> requires (I < 4)
ValueT get(this const ColorDerived &self) { return self._data[I]; }
operator Vec3f(this ColorDerived &self) noexcept {
return (Vec3f)self._data;
}
operator Vec4f(this ColorDerived &self) noexcept {
return self._data;
}
// structured binding support
template <size_t I> requires (I < 4)
constexpr ValueT get() const noexcept {
static_assert(I < 4);
return static_cast<const ColorDerived &>(*this)._data[I];
}
/* implicit conversions */
constexpr operator Vec3f() const noexcept {
const auto &d = static_cast<const ColorDerived &>(*this)._data;
return Vec3f{d.x(), d.y(), d.z()}; // avoid the “no Vec3f(Vec4f) ctor” error
}
constexpr operator Vec4f() const noexcept {
return static_cast<const ColorDerived &>(*this)._data;
}

Comment on lines +57 to +62
friend std::ostream & operator<<(std::ostream &s, const ColorDerived &color) noexcept {
Vec4u comps = color._data * 255;
comps.clamp(0, 256);
s << '#' << std::hex << std::uppercase << comps[0] << comps[1] << comps[2] << comps[3] << std::nouppercase << std::dec;
return s;
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

operator<< prints incorrect hex and risks narrowing

  1. Multiplying a Vec4f by 255 gives floats; converting them to a Vec4u via implicit ctor may silently floor or truncate.
  2. << std::hex streams the decimal value of every component, not two-digit padded bytes, so #3F3F3F3F might appear as #3F3F3F3F or #3f3f3f3f depending on the values ‑ but e.g. component 10 becomes just A, losing the leading zero.

Consider formatting each channel explicitly:

-Vec4u comps = color._data * 255;
-comps.clamp(0, 256);
-s << '#' << std::hex << std::uppercase << comps[0] << comps[1] << comps[2] << comps[3] << std::nouppercase << std::dec;
+const auto comps = (color._data * 255.0f).clamped(0.0f, 255.0f).round(); // or explicit cast
+s << '#' << std::uppercase << std::hex
+  << std::setw(2) << std::setfill('0') << uint32_t(comps[0])
+  << std::setw(2) << std::setfill('0') << uint32_t(comps[1])
+  << std::setw(2) << std::setfill('0') << uint32_t(comps[2])
+  << std::setw(2) << std::setfill('0') << uint32_t(comps[3])
+  << std::dec << std::nouppercase;

Requires <iomanip> and a round()/clamped helper, but produces predictable 8-digit ARGB output.

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +44 to +48
ColorDerived &operator+=(const ColorDerived &other) noexcept {
static_cast<ColorDerived*>(this)->_data += other._data;
return *(ColorDerived*)this;
}

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Unsafe pointer casts & missing const qualification in operator+=
static_cast<ColorDerived*>(this)->_data += other._data; forces a mutable pointer even when *this is const.
Use a reference, keep qualifiers, and avoid C-style casts:

-ColorDerived &operator+=(const ColorDerived &other) noexcept {
-  static_cast<ColorDerived*>(this)->_data += other._data;
-  return *(ColorDerived*)this;
-}
+ColorDerived &operator+=(const ColorDerived &other) noexcept {
+  auto &self = static_cast<ColorDerived &>(*this);
+  self._data += other._data;
+  return self;
+}

This addresses UB and removes the noisy casts.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ColorDerived &operator+=(const ColorDerived &other) noexcept {
static_cast<ColorDerived*>(this)->_data += other._data;
return *(ColorDerived*)this;
}
ColorDerived &operator+=(const ColorDerived &other) noexcept {
auto &self = static_cast<ColorDerived &>(*this);
self._data += other._data;
return self;
}

Comment on lines +185 to 190
mr::RGBAColor c1; // same as Vec4f(0, 0, 0, 0)
mr::RGBAColor c2 = mr::Color(0.3, 0.47, 0.8); // == (0.3, 0.47, 0.8, 1.0)
mr::RGBAColor c3 = mr::Color(76, 119, 204, 255); // == (0.2980392156862745, 0.4666666666666667, 0.8, 1.0)
mr::RGBAColor c4 = mr::Color(0x4C'77'CC'FF); // == (0.2980392156862745, 0.4666666666666667, 0.8, 1.0)
mr::RGBAColor c5 = 0x4C'77'CC'FF_rgba; // == (0.2980392156862745, 0.4666666666666667, 0.8, 1.0)
```
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Examples still reference the removed mr::Color type
The refactor renamed the type to mr::RGBAColor, yet the docs mix both names:

-mr::RGBAColor c2 = mr::Color(0.3, 0.47, 0.8);
-mr::RGBAColor c3 = mr::Color(76, 119, 204, 255);
-mr::RGBAColor c4 = mr::Color(0x4C'77'CC'FF);
+mr::RGBAColor c2{0.3, 0.47, 0.8};
+mr::RGBAColor c3{76, 119, 204, 255};
+mr::RGBAColor c4{0x4C'77'CC'FF};

Do the same for the arithmetic example further down (mr::Color(1.0, …)mr::RGBAColor{…}).
Keeping the docs in sync prevents confusion for consumers.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
mr::RGBAColor c1; // same as Vec4f(0, 0, 0, 0)
mr::RGBAColor c2 = mr::Color(0.3, 0.47, 0.8); // == (0.3, 0.47, 0.8, 1.0)
mr::RGBAColor c3 = mr::Color(76, 119, 204, 255); // == (0.2980392156862745, 0.4666666666666667, 0.8, 1.0)
mr::RGBAColor c4 = mr::Color(0x4C'77'CC'FF); // == (0.2980392156862745, 0.4666666666666667, 0.8, 1.0)
mr::RGBAColor c5 = 0x4C'77'CC'FF_rgba; // == (0.2980392156862745, 0.4666666666666667, 0.8, 1.0)
```
mr::RGBAColor c1; // same as Vec4f(0, 0, 0, 0)
mr::RGBAColor c2{0.3, 0.47, 0.8}; // == (0.3, 0.47, 0.8, 1.0)
mr::RGBAColor c3{76, 119, 204, 255}; // == (0.2980392156862745, 0.4666666666666667, 0.8, 1.0)
mr::RGBAColor c4{0x4C'77'CC'FF}; // == (0.2980392156862745, 0.4666666666666667, 0.8, 1.0)
mr::RGBAColor c5 = 0x4C'77'CC'FF_rgba; // == (0.2980392156862745, 0.4666666666666667, 0.8, 1.0)
🧰 Tools
🪛 markdownlint-cli2 (0.17.2)

190-190: Fenced code blocks should be surrounded by blank lines
null

(MD031, blanks-around-fences)

Comment on lines +37 to 44
const mr::RGBAColor expected1{0.3, 0.47, 0.8, 1.0};
EXPECT_EQ(mr::RGBAColor(0.3, 0.47, 0.8), expected1);
EXPECT_EQ(mr::RGBAColor(mr::Vec4f(0.3, 0.47, 0.8, 1)), expected1);

const mr::Color expected2{0.2980392156862745, 0.4666666666666667, 0.8, 1.0};
EXPECT_EQ(mr::Color(76, 119, 204, 255), expected2);
EXPECT_EQ(mr::Color(0x4C'77'CC'FF), expected2);
const mr::RGBAColor expected2{0.2980392156862745, 0.4666666666666667, 0.8, 1.0};
EXPECT_EQ(mr::RGBAColor(76, 119, 204, 255), expected2);
EXPECT_EQ(mr::RGBAColor(0x4C'77'CC'FF), expected2);
EXPECT_EQ(0x4C'77'CC'FF_rgba, expected2);
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Consider using EXPECT_FLOAT_EQ for floating‐point constructor tests

The constructors accepting floats and vectors compare RGBAColor instances via EXPECT_EQ, which relies on operator==. If operator== uses exact float comparison, tiny rounding differences could cause spurious failures. Consider switching to EXPECT_TRUE(equal(...)) or EXPECT_FLOAT_EQ on each component for these tests to tolerate floating‐point precision.

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

♻️ Duplicate comments (5)
include/mr-math/color.hpp (5)

17-20: Unconditional static_assert still prevents all instantiations

This exact issue was raised in the previous review: the primary template aborts every build even when a matching specialization exists. Make the assertion dependent on the template parameters.


27-38: deducing this is the cause of the current CI syntax errors

GCC 13 / Clang 17 (our tool-chain) do not support the P0847 “deducing-this” syntax yet. Every use (get, the two conversion operators) fails to compile (self/other/eps undeclared).


44-48: operator+= still does UB-prone pointer casts

Casting away const and going through a pointer was already flagged; the unsafe pattern is unchanged.


49-55: Equality helpers repeat the unsupported “deducing-this” style

Again identical to the previously reported issue; the build stops here.


57-62: operator<< prints wrong hex & silently truncates ‑ nothing changed

The earlier suggestions (use std::setw, explicit rounding, avoid << std::hex on floats) are still applicable.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ae0812d and c4ddbf9.

📒 Files selected for processing (1)
  • include/mr-math/color.hpp (3 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
include/mr-math/color.hpp (4)
include/mr-math/matr.hpp (8)
  • nodiscard (169-171)
  • nodiscard (173-175)
  • nodiscard (178-191)
  • _data (321-324)
  • _data (383-386)
  • _data (455-458)
  • i (169-169)
  • i (173-173)
include/mr-math/operators.hpp (2)
  • s (133-141)
  • s (134-134)
include/mr-math/def.hpp (6)
  • epsilon (35-37)
  • epsilon (35-35)
  • value (93-93)
  • value (93-93)
  • value (116-116)
  • value (116-116)
include/mr-math/vec.hpp (7)
  • l (165-169)
  • l (172-175)
  • i (84-84)
  • i (84-84)
  • i (95-95)
  • y (86-86)
  • y (86-86)
🪛 GitHub Actions: Benchmarks
include/mr-math/color.hpp

[error] 29-54: Syntax errors due to incorrect use of 'this' keyword in method parameters and undeclared identifiers 'self', 'other', and 'eps'.


[error] 371-371: No matching function for call to 'mr::math::Vec<float, 3>::Vec(mr::math::RGBAColor&)'. Invalid static_cast from RGBAColor to Vec3f.

⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: build (windows-latest, Release, cl)
  • GitHub Check: build (windows-latest, Debug, cl)
  • GitHub Check: build (ubuntu-24.04, Release, clang)

Comment on lines +138 to +156
struct HSLAColor {
friend class ColorBase<HSLAColor>;

private:
Vec4f _data;

public:
using ValueT = float;

HSLAColor() = default;

template <std::floating_point T>
HSLAColor(T h, T s, T l, T a = 1) noexcept
: _data(h, s, l, a) {}

HSLAColor(Vec4f hsla) noexcept
: _data(hsla) {}

template <std::integral T>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

All new colour structs forget to inherit from ColorBase

HSLAColor, HSVAColor, CMYKColor, and OKLABColor only friend the base but do not derive from it, so they do not get:

  • operator+, operator+=
  • equality helpers
  • structured-binding support
  • stream output
    …and any future generic utilities.

This is a functional break (e.g. HSLAColor{} can’t be streamed, added, or compared).

Apply the pattern already used for RGBAColor:

-struct HSLAColor {
-  friend class ColorBase<HSLAColor>;
+struct HSLAColor : ColorBase<HSLAColor> {
+  friend class ColorBase<HSLAColor>;

Repeat the same one-liner change for HSVAColor, CMYKColor, and OKLABColor.

Also applies to: 196-214, 254-272, 312-330

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
mr-math.natvis (1)

107-116: Fix natvis: extra brace and better hex packing.

  • Remove the stray ‘}’ in DisplayString.
  • Prefer rounded bytes and bitwise OR.
-  <Type Name="mr::math::RGBAColor">
-    <DisplayString>R={r()}, G={g()}, B={b()}, A={a()}}</DisplayString>
+  <Type Name="mr::math::RGBAColor">
+    <DisplayString>R={r()}, G={g()}, B={b()}, A={a()}</DisplayString>
     <Expand>
       <Item Name="R">r()</Item>
       <Item Name="G">g()</Item>
       <Item Name="B">b()</Item>
       <Item Name="A">a()</Item>
-      <Item Name="Hex">(uint32_t(r() * 255) &lt;&lt; 24) + (uint32_t(g() * 255) &lt;&lt; 16) + (uint32_t(b() * 255) &lt;&lt; 8) + (uint32_t(a() * 255)), Xb</Item>
+      <Item Name="Hex">(uint32_t(r()*255 + 0.5f) &lt;&lt; 24) | (uint32_t(g()*255 + 0.5f) &lt;&lt; 16) | (uint32_t(b()*255 + 0.5f) &lt;&lt; 8) | uint32_t(a()*255 + 0.5f), Xb</Item>
     </Expand>
   </Type>
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c4ddbf9 and 2640318.

📒 Files selected for processing (4)
  • USAGE.md (2 hunks)
  • include/mr-math/color.hpp (3 hunks)
  • mr-math.natvis (1 hunks)
  • tests/misc.cpp (3 hunks)
🧰 Additional context used
🪛 GitHub Actions: Benchmarks
include/mr-math/color.hpp

[error] 29-54: Compilation errors in ColorBase definitions: use of 'this' in member function signatures appears invalid (e.g., 'ValueT get(this const ColorDerived &self)', 'operator Vec3f(this ColorDerived &self)'), plus missing declarations of 'self', 'other', and 'eps' in several methods.

🪛 markdownlint-cli2 (0.18.1)
USAGE.md

218-218: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


220-220: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


225-225: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


228-228: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


233-233: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


235-235: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build (windows-latest, Release, cl)
🔇 Additional comments (12)
tests/misc.cpp (3)

442-447: Setters test aligns with the API and expected semantics.


463-465: Define addition semantics (clamp/saturate vs. unbounded).

Test asserts unbounded sums; ensure this matches intended API (document or clamp in impl).


403-414: Constructors tests look good.

Consider EXPECT_TRUE(equal(lhs, rhs)) or component-wise EXPECT_FLOAT_EQ to avoid brittle exact-float compares.

include/mr-math/color.hpp (7)

17-21: Fix: make the primary convert SFINAE-friendly (current code hard-errors every instantiation).

The unconditional static_assert(false) prevents valid specializations from being found.

Apply:

-template <typename InputT, typename OutputT>
-  OutputT convert(InputT input) {
-    static_assert(false, "You have to specify this function for your particular types");
-  }
+template <typename...>
+inline constexpr bool _mr_convert_always_false_v = false;
+
+template <typename InputT, typename OutputT>
+constexpr OutputT convert(InputT /*unused*/) {
+  static_assert(_mr_convert_always_false_v<InputT, OutputT>,
+                "No `convert` specialization found for the supplied types");
+}

27-37: Replace unsupported “deducing this” with standard C++20 members.

These signatures won’t compile on our toolchain and are the source of the pipeline errors.

Apply:

-// structured binding support
-template <size_t I> requires (I < 4)
-  ValueT get(this const ColorDerived &self) { return self._data[I]; }
-
-operator Vec3f(this ColorDerived &self) noexcept {
-  return (Vec3f)self._data;
-}
-
-operator Vec4f(this ColorDerived &self) noexcept {
-  return self._data;
-}
+// structured binding support
+template <size_t I>
+  requires (I < 4)
+constexpr ValueT get() const noexcept {
+  static_assert(I < 4);
+  return static_cast<const ColorDerived &>(*this)._data[I];
+}
+
+// implicit conversions
+constexpr operator Vec3f() const noexcept {
+  const auto &d = static_cast<const ColorDerived &>(*this)._data;
+  return Vec3f{d.x(), d.y(), d.z()};
+}
+constexpr operator Vec4f() const noexcept {
+  return static_cast<const ColorDerived &>(*this)._data;
+}

Additionally (outside this hunk), expose ADL get for structured bindings:

namespace mr { inline namespace math {
  template <size_t I>
  constexpr auto get(const RGBAColor &c) noexcept -> RGBAColor::ValueT {
    return c.template get<I>();
  }
}}

44-47: Fix UB-prone casts in operator+= (use a reference, preserve cv-qualifiers).

-ColorDerived &operator+=(const ColorDerived &other) noexcept {
-  static_cast<ColorDerived*>(this)->_data += other._data;
-  return *(ColorDerived*)this;
-}
+ColorDerived &operator+=(const ColorDerived &other) noexcept {
+  auto &self = static_cast<ColorDerived &>(*this);
+  self._data += other._data;
+  return self;
+}

49-55: Rewrite equality helpers without “deducing this” and keep const-correctness.

-bool operator==(this const ColorDerived &self, const ColorDerived &other) noexcept {
-  return self._data == other._data;
-}
-
-bool equal(this const ColorDerived &self, const ColorDerived &other, ValueT eps = epsilon<ValueT>()) noexcept {
-  return self._data.equal(other._data, eps);
-}
+constexpr bool operator==(const ColorDerived &other) const noexcept {
+  return static_cast<const ColorDerived &>(*this)._data == other._data;
+}
+constexpr bool equal(const ColorDerived &other,
+                     ValueT eps = epsilon<ValueT>()) const noexcept {
+  return static_cast<const ColorDerived &>(*this)._data.equal(other._data, eps);
+}

57-62: Hex stream formatting is incorrect; also risks truncation.

Print zero‑padded bytes with rounding; avoid relying on implicit narrowing.

-friend std::ostream & operator<<(std::ostream &s, const ColorDerived &color) noexcept {
-  Vec4u comps = color._data * 255;
-  comps.clamp(0, 256);
-  s << '#' << std::hex << std::uppercase << comps[0] << comps[1] << comps[2] << comps[3] << std::nouppercase << std::dec;
-  return s;
-}
+friend std::ostream & operator<<(std::ostream &s, const ColorDerived &color) noexcept {
+  auto tmp = color._data * 255.0f;
+  tmp.clamp(0.0f, 255.0f);
+  const uint32_t r = static_cast<uint32_t>(tmp[0] + 0.5f);
+  const uint32_t g = static_cast<uint32_t>(tmp[1] + 0.5f);
+  const uint32_t b = static_cast<uint32_t>(tmp[2] + 0.5f);
+  const uint32_t a = static_cast<uint32_t>(tmp[3] + 0.5f);
+  s << '#' << std::uppercase << std::hex
+    << std::setw(2) << std::setfill('0') << r
+    << std::setw(2) << std::setfill('0') << g
+    << std::setw(2) << std::setfill('0') << b
+    << std::setw(2) << std::setfill('0') << a
+    << std::dec << std::nouppercase;
+  return s;
+}

Also add (outside this hunk):

#include <iomanip>

142-160: All non-RGBA colour structs don’t inherit from ColorBase (missing operators, streaming, etc.).

This breaks arithmetic, equality, and output for these types.

Apply the one-liner on each:

-struct HSLAColor {
+struct HSLAColor : ColorBase<HSLAColor> {
   friend class ColorBase<HSLAColor>;
-struct HSVAColor {
+struct HSVAColor : ColorBase<HSVAColor> {
   friend class ColorBase<HSVAColor>;
-struct CMYKColor {
+struct CMYKColor : ColorBase<CMYKColor> {
   friend class ColorBase<CMYKColor>;
-struct OKLABColor {
+struct OKLABColor : ColorBase<OKLABColor> {
   friend class ColorBase<OKLABColor>;

Also applies to: 200-256, 258-314, 316-371


373-392: Fix RGBA→OKLAB conversion: avoid invalid cast, ensure matrix×vector, use std::cbrtf

  • Change signature to convert<RGBAColor,OKLABColor>(const RGBAColor& rgba) and extract Vec3f rgb{rgba.r(),rgba.g(),rgba.b()}
  • Compute auto v = Matr3f{…} * rgb (avoid static_cast<Vec3f>(rgba) and reversed operand)
  • Replace cbrtf with std::cbrtf and add #include <cmath>
  • Confirm a Matr3f*Vec3f overload exists and that RGBAColor is linear‐RGB (add gamma→linear if needed)
USAGE.md (2)

213-218: Docs still reference removed mr::Color – switch to mr::RGBAColor with brace-init.

-mr::RGBAColor c1; // same as Vec4f(0, 0, 0, 0)
-mr::RGBAColor c2 = mr::Color(0.3, 0.47, 0.8); // == (0.3, 0.47, 0.8, 1.0)
-mr::RGBAColor c3 = mr::Color(76, 119, 204, 255); // == (0.2980392156862745, 0.4666666666666667, 0.8, 1.0)
-mr::RGBAColor c4 = mr::Color(0x4C'77'CC'FF); // == (0.2980392156862745, 0.4666666666666667, 0.8, 1.0)
-mr::RGBAColor c5 = 0x4C'77'CC'FF_rgba; // == (0.2980392156862745, 0.4666666666666667, 0.8, 1.0)
+mr::RGBAColor c1; // same as Vec4f(0, 0, 0, 0)
+mr::RGBAColor c2{0.3, 0.47, 0.8}; // == (0.3, 0.47, 0.8, 1.0)
+mr::RGBAColor c3{76, 119, 204, 255}; // == (0.298..., 0.466..., 0.8, 1.0)
+mr::RGBAColor c4{0x4C'77'CC'FF}; // == (0.298..., 0.466..., 0.8, 1.0)
+mr::RGBAColor c5 = 0x4C'77'CC'FF_rgba; // == (0.298..., 0.466..., 0.8, 1.0)

Also add blank lines around fenced code blocks to satisfy MD031.


236-236: Use RGBAColor in arithmetic example.

-mr::RGBAColor c11 = mr::Color(1.0, 0.0, 0.5, 1.0) + mr::Color(0.0, 1.0, 0.5, 1.0); // == mr::Color(1.0, 1.0, 1.0, 2.0));
+mr::RGBAColor c11 = mr::RGBAColor{1.0, 0.0, 0.5, 1.0} + mr::RGBAColor{0.0, 1.0, 0.5, 1.0}; // == mr::RGBAColor{1.0, 1.0, 1.0, 2.0}

Comment on lines +67 to +68
struct alignas(float) [[nodiscard]] RGBAColor : ColorBase<RGBAColor> {
friend class ColorBase<RGBAColor>;
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Drop redundant alignas(float) on RGBAColor.

It’s unnecessary and potentially misleading; member alignment dictates the type’s alignment anyway.

-struct alignas(float) [[nodiscard]] RGBAColor : ColorBase<RGBAColor> {
+struct [[nodiscard]] RGBAColor : ColorBase<RGBAColor> {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
struct alignas(float) [[nodiscard]] RGBAColor : ColorBase<RGBAColor> {
friend class ColorBase<RGBAColor>;
struct [[nodiscard]] RGBAColor : ColorBase<RGBAColor> {
friend class ColorBase<RGBAColor>;
🤖 Prompt for AI Agents
In include/mr-math/color.hpp around lines 67 to 68, the struct declaration uses
a redundant alignas(float) on RGBAColor; remove the alignas(float) specifier
from the RGBAColor declaration so the type uses natural member-driven alignment
instead, leaving the struct declaration as a plain struct alignement (and run a
quick build to ensure no intended ABI assumptions relied on that attribute).

Comment on lines +410 to 416
struct tuple_size<mr::RGBAColor>
: std::integral_constant<size_t, 4> {};

template <size_t I>
struct tuple_element<I, mr::Color> {
using type = mr::Color::ValueT;
struct tuple_element<I, mr::RGBAColor> {
using type = mr::RGBAColor::ValueT;
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Structured bindings need ADL get – provide free functions.

tuple_size/tuple_element isn’t enough by itself.

Add (outside this hunk):

namespace mr { inline namespace math {
  template <size_t I>
  constexpr auto get(const RGBAColor &c) noexcept -> RGBAColor::ValueT {
    return c.template get<I>();
  }
}}
🤖 Prompt for AI Agents
In include/mr-math/color.hpp around lines 410–416, tuple_size/tuple_element are
defined but structured bindings also require free ADL get functions; add inline
free get overloads in namespace mr (e.g., inline namespace math) that forward to
the member template get<I>() — provide at least a constexpr const& (or by-value
if ValueT is a scalar) noexcept template<size_t I> returning RGBAColor::ValueT
using c.template get<I>(), and also add non-const and rvalue overloads so
structured bindings and std::get work for const, mutable and rvalue RGBAColor
objects.

Comment on lines +221 to 225
mr::RGBAColor c6 = 0x4C'77'CC'FF_rgba;
mr::RGBAColor c7 = color.argb(); // == 0xFF'4C'77'CC_rgba
mr::RGBAColor c8 = color.bgra(); // == 0xCC'77'4c'FF_rgba
mr::RGBAColor c9 = color.abgr(); // == 0xFF'CC'77'4c_rgba
```
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Type of color.argb()/bgra()/abgr() in examples.

They return Vec4f; relying on the RGBAColor(Vec4f) ctor is fine, but prefer clarity.

-mr::RGBAColor c7 = color.argb(); // == 0xFF'4C'77'CC_rgba
-mr::RGBAColor c8 = color.bgra(); // == 0xCC'77'4c'FF_rgba
-mr::RGBAColor c9 = color.abgr(); // == 0xFF'CC'77'4c_rgba
+mr::RGBAColor c7{color.argb()}; // == 0xFF'4C'77'CC_rgba
+mr::RGBAColor c8{color.bgra()}; // == 0xCC'77'4c'FF_rgba
+mr::RGBAColor c9{color.abgr()}; // == 0xFF'CC'77'4c_rgba
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
mr::RGBAColor c6 = 0x4C'77'CC'FF_rgba;
mr::RGBAColor c7 = color.argb(); // == 0xFF'4C'77'CC_rgba
mr::RGBAColor c8 = color.bgra(); // == 0xCC'77'4c'FF_rgba
mr::RGBAColor c9 = color.abgr(); // == 0xFF'CC'77'4c_rgba
```
mr::RGBAColor c6 = 0x4C'77'CC'FF_rgba;
mr::RGBAColor c7{color.argb()}; // == 0xFF'4C'77'CC_rgba
mr::RGBAColor c8{color.bgra()}; // == 0xCC'77'4c'FF_rgba
mr::RGBAColor c9{color.abgr()}; // == 0xFF'CC'77'4c_rgba
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

225-225: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)

🤖 Prompt for AI Agents
In USAGE.md around lines 221 to 225, the examples call
color.argb()/bgra()/abgr() which return Vec4f; update the examples to explicitly
construct an mr::RGBAColor from those Vec4f results for clarity. Replace
assignments like "mr::RGBAColor c7 = color.argb();" with an explicit constructor
call such as "mr::RGBAColor c7 = mr::RGBAColor(color.argb());" and do the same
for c8 and c9 so the type conversion is obvious.

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