diff --git a/wpilibc/src/main/native/cpp/util/Color.cpp b/wpilibc/src/main/native/cpp/util/Color.cpp index e3adaf2db3f..6923c9cbec1 100644 --- a/wpilibc/src/main/native/cpp/util/Color.cpp +++ b/wpilibc/src/main/native/cpp/util/Color.cpp @@ -4,8 +4,6 @@ #include "frc/util/Color.h" -#include - using namespace frc; std::string Color::HexString() const { diff --git a/wpilibc/src/main/native/cpp/util/Color8Bit.cpp b/wpilibc/src/main/native/cpp/util/Color8Bit.cpp index af200a294b5..e8e46567d70 100644 --- a/wpilibc/src/main/native/cpp/util/Color8Bit.cpp +++ b/wpilibc/src/main/native/cpp/util/Color8Bit.cpp @@ -4,8 +4,6 @@ #include "frc/util/Color8Bit.h" -#include - using namespace frc; std::string Color8Bit::HexString() const { diff --git a/wpilibc/src/main/native/include/frc/util/Color.h b/wpilibc/src/main/native/include/frc/util/Color.h index 87d8c12f497..b5855e23ce8 100644 --- a/wpilibc/src/main/native/include/frc/util/Color.h +++ b/wpilibc/src/main/native/include/frc/util/Color.h @@ -5,7 +5,12 @@ #pragma once #include +#include #include +#include + +#include +#include namespace frc { @@ -739,6 +744,9 @@ class Color { */ static const Color kYellowGreen; + /** + * Constructs a default color (black). + */ constexpr Color() = default; /** @@ -763,6 +771,33 @@ class Color { constexpr Color(int r, int g, int b) : Color(r / 255.0, g / 255.0, b / 255.0) {} + /** + * Constructs a Color from a hex string. + * + * @param hexString a string of the format \#RRGGBB + * @throws std::invalid_argument if the hex string is invalid. + */ + explicit constexpr Color(std::string_view hexString) { + if (hexString.length() != 7 || !hexString.starts_with("#") || + !wpi::isHexDigit(hexString[1]) || !wpi::isHexDigit(hexString[2]) || + !wpi::isHexDigit(hexString[3]) || !wpi::isHexDigit(hexString[4]) || + !wpi::isHexDigit(hexString[5]) || !wpi::isHexDigit(hexString[6])) { + throw std::invalid_argument( + fmt::format("Invalid hex string for Color \"{}\"", hexString)); + } + + int r = wpi::hexDigitValue(hexString[1]) * 16 + + wpi::hexDigitValue(hexString[2]); + int g = wpi::hexDigitValue(hexString[3]) * 16 + + wpi::hexDigitValue(hexString[4]); + int b = wpi::hexDigitValue(hexString[5]) * 16 + + wpi::hexDigitValue(hexString[6]); + + red = r / 255.0; + green = g / 255.0; + blue = b / 255.0; + } + constexpr bool operator==(const Color&) const = default; /** diff --git a/wpilibc/src/main/native/include/frc/util/Color8Bit.h b/wpilibc/src/main/native/include/frc/util/Color8Bit.h index 10afead93dc..77bdc9f0ee5 100644 --- a/wpilibc/src/main/native/include/frc/util/Color8Bit.h +++ b/wpilibc/src/main/native/include/frc/util/Color8Bit.h @@ -5,7 +5,12 @@ #pragma once #include +#include #include +#include + +#include +#include #include "Color.h" @@ -16,6 +21,9 @@ namespace frc { */ class Color8Bit { public: + /** + * Constructs a default color (black). + */ constexpr Color8Bit() = default; /** @@ -40,11 +48,59 @@ class Color8Bit { green(color.green * 255), blue(color.blue * 255) {} + /** + * Constructs a Color8Bit from a hex string. + * + * @param hexString a string of the format \#RRGGBB + * @throws std::invalid_argument if the hex string is invalid. + */ + explicit constexpr Color8Bit(std::string_view hexString) { + if (hexString.length() != 7 || !hexString.starts_with("#") || + !wpi::isHexDigit(hexString[1]) || !wpi::isHexDigit(hexString[2]) || + !wpi::isHexDigit(hexString[3]) || !wpi::isHexDigit(hexString[4]) || + !wpi::isHexDigit(hexString[5]) || !wpi::isHexDigit(hexString[6])) { + throw std::invalid_argument( + fmt::format("Invalid hex string for Color \"{}\"", hexString)); + } + + red = wpi::hexDigitValue(hexString[1]) * 16 + + wpi::hexDigitValue(hexString[2]); + green = wpi::hexDigitValue(hexString[3]) * 16 + + wpi::hexDigitValue(hexString[4]); + blue = wpi::hexDigitValue(hexString[5]) * 16 + + wpi::hexDigitValue(hexString[6]); + } + + constexpr bool operator==(const Color8Bit&) const = default; + constexpr operator Color() const { // NOLINT return Color(red / 255.0, green / 255.0, blue / 255.0); } - constexpr bool operator==(const Color8Bit&) const = default; + /** + * Create a Color8Bit from a hex string. + * + * @param hexString a string of the format \#RRGGBB + * @return Color8Bit object from hex string. + * @throws std::invalid_argument if the hex string is invalid. + */ + static constexpr Color8Bit FromHexString(std::string_view hexString) { + if (hexString.length() != 7 || !hexString.starts_with("#") || + !wpi::isHexDigit(hexString[1]) || !wpi::isHexDigit(hexString[2]) || + !wpi::isHexDigit(hexString[3]) || !wpi::isHexDigit(hexString[4]) || + !wpi::isHexDigit(hexString[5]) || !wpi::isHexDigit(hexString[6])) { + throw std::invalid_argument( + fmt::format("Invalid hex string for Color \"{}\"", hexString)); + } + + int r = wpi::hexDigitValue(hexString[0]) * 16 + + wpi::hexDigitValue(hexString[1]); + int g = wpi::hexDigitValue(hexString[2]) * 16 + + wpi::hexDigitValue(hexString[3]); + int b = wpi::hexDigitValue(hexString[4]) * 16 + + wpi::hexDigitValue(hexString[5]); + return Color8Bit{r, g, b}; + } /** * Return this color represented as a hex string. diff --git a/wpilibc/src/test/native/cpp/util/Color8BitTest.cpp b/wpilibc/src/test/native/cpp/util/Color8BitTest.cpp new file mode 100644 index 00000000000..e0b26dd5790 --- /dev/null +++ b/wpilibc/src/test/native/cpp/util/Color8BitTest.cpp @@ -0,0 +1,62 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#include + +#include "frc/util/Color8Bit.h" + +TEST(Color8BitTest, ConstructDefault) { + constexpr frc::Color8Bit color; + + EXPECT_EQ(0, color.red); + EXPECT_EQ(0, color.green); + EXPECT_EQ(0, color.blue); +} + +TEST(Color8BitTest, ConstructFromInts) { + constexpr frc::Color8Bit color{255, 128, 64}; + + EXPECT_EQ(255, color.red); + EXPECT_EQ(128, color.green); + EXPECT_EQ(64, color.blue); +} + +TEST(Color8BitTest, ConstructFromColor) { + constexpr frc::Color8Bit color{frc::Color{255, 128, 64}}; + + EXPECT_EQ(255, color.red); + EXPECT_EQ(128, color.green); + EXPECT_EQ(64, color.blue); +} + +TEST(Color8BitTest, ConstructFromHexString) { + constexpr frc::Color8Bit color{"#FF8040"}; + + EXPECT_EQ(255, color.red); + EXPECT_EQ(128, color.green); + EXPECT_EQ(64, color.blue); + + // No leading # + EXPECT_THROW(frc::Color8Bit{"112233"}, std::invalid_argument); + + // Too long + EXPECT_THROW(frc::Color8Bit{"#11223344"}, std::invalid_argument); + + // Invalid hex characters + EXPECT_THROW(frc::Color8Bit{"#$$$$$$"}, std::invalid_argument); +} + +TEST(Color8BitTest, ImplicitConversionToColor) { + frc::Color color = frc::Color8Bit{255, 128, 64}; + + EXPECT_NEAR(1.0, color.red, 1e-2); + EXPECT_NEAR(0.5, color.green, 1e-2); + EXPECT_NEAR(0.25, color.blue, 1e-2); +} + +TEST(Color8BitTest, ToHexString) { + constexpr frc::Color8Bit color{255, 128, 64}; + + EXPECT_EQ("#FF8040", color.HexString()); +} diff --git a/wpilibc/src/test/native/cpp/util/ColorTest.cpp b/wpilibc/src/test/native/cpp/util/ColorTest.cpp new file mode 100644 index 00000000000..c76c7f1da29 --- /dev/null +++ b/wpilibc/src/test/native/cpp/util/ColorTest.cpp @@ -0,0 +1,62 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#include + +#include "frc/util/Color.h" + +TEST(ColorTest, ConstructDefault) { + constexpr frc::Color color; + + EXPECT_DOUBLE_EQ(0.0, color.red); + EXPECT_DOUBLE_EQ(0.0, color.green); + EXPECT_DOUBLE_EQ(0.0, color.blue); +} + +TEST(ColorTest, ConstructFromDoubles) { + constexpr frc::Color color{1.0, 0.5, 0.25}; + + EXPECT_NEAR(1.0, color.red, 1e-2); + EXPECT_NEAR(0.5, color.green, 1e-2); + EXPECT_NEAR(0.25, color.blue, 1e-2); +} + +TEST(ColorTest, ConstructFromInts) { + constexpr frc::Color color{255, 128, 64}; + + EXPECT_NEAR(1.0, color.red, 1e-2); + EXPECT_NEAR(0.5, color.green, 1e-2); + EXPECT_NEAR(0.25, color.blue, 1e-2); +} + +TEST(ColorTest, ConstructFromHexString) { + constexpr frc::Color color{"#FF8040"}; + + EXPECT_NEAR(1.0, color.red, 1e-2); + EXPECT_NEAR(0.5, color.green, 1e-2); + EXPECT_NEAR(0.25, color.blue, 1e-2); + + // No leading # + EXPECT_THROW(frc::Color{"112233"}, std::invalid_argument); + + // Too long + EXPECT_THROW(frc::Color{"#11223344"}, std::invalid_argument); + + // Invalid hex characters + EXPECT_THROW(frc::Color{"#$$$$$$"}, std::invalid_argument); +} + +TEST(ColorTest, FromHSV) { + constexpr frc::Color color = frc::Color::FromHSV(90, 128, 64); + + EXPECT_DOUBLE_EQ(0.1256103515625, color.red); + EXPECT_DOUBLE_EQ(0.2510986328125, color.green); + EXPECT_DOUBLE_EQ(0.2510986328125, color.blue); +} + +TEST(ColorTest, ToHexString) { + constexpr frc::Color color{255, 128, 64}; + + EXPECT_EQ("#FF8040", color.HexString()); +} diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/util/Color.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/util/Color.java index 2defbd8ad36..706795dd701 100644 --- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/util/Color.java +++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/util/Color.java @@ -21,6 +21,13 @@ public class Color { public final double blue; private String m_name; + /** Constructs a default color (black). */ + public Color() { + red = 0.0; + green = 0.0; + blue = 0.0; + } + /** * Constructs a Color from doubles. * @@ -69,6 +76,22 @@ private Color(double red, double green, double blue, String name) { this.m_name = name; } + /** + * Constructs a Color from a hex string. + * + * @param hexString a string of the format #RRGGBB + * @throws IllegalArgumentException if the hex string is invalid. + */ + public Color(String hexString) { + if (hexString.length() != 7 || !hexString.startsWith("#")) { + throw new IllegalArgumentException("Invalid hex string \"" + hexString + "\""); + } + + this.red = Integer.valueOf(hexString.substring(1, 3), 16) / 255.0; + this.green = Integer.valueOf(hexString.substring(3, 5), 16) / 255.0; + this.blue = Integer.valueOf(hexString.substring(5, 7), 16) / 255.0; + } + /** * Creates a Color from HSV values. * diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/util/Color8Bit.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/util/Color8Bit.java index 32606731006..5d916e1941a 100644 --- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/util/Color8Bit.java +++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/util/Color8Bit.java @@ -14,6 +14,13 @@ public class Color8Bit { public final int green; public final int blue; + /** Constructs a default color (black). */ + public Color8Bit() { + red = 0; + green = 0; + blue = 0; + } + /** * Constructs a Color8Bit. * @@ -36,6 +43,22 @@ public Color8Bit(Color color) { this((int) (color.red * 255), (int) (color.green * 255), (int) (color.blue * 255)); } + /** + * Constructs a Color8Bit from a hex string. + * + * @param hexString a string of the format #RRGGBB + * @throws IllegalArgumentException if the hex string is invalid. + */ + public Color8Bit(String hexString) { + if (hexString.length() != 7 || !hexString.startsWith("#")) { + throw new IllegalArgumentException("Invalid hex string \"" + hexString + "\""); + } + + this.red = Integer.valueOf(hexString.substring(1, 3), 16); + this.green = Integer.valueOf(hexString.substring(3, 5), 16); + this.blue = Integer.valueOf(hexString.substring(5, 7), 16); + } + @Override public boolean equals(Object other) { if (this == other) { diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/util/Color8BitTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/util/Color8BitTest.java new file mode 100644 index 00000000000..499ecbde80c --- /dev/null +++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/util/Color8BitTest.java @@ -0,0 +1,65 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package edu.wpi.first.wpilibj.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +class Color8BitTest { + @Test + void testConstructDefault() { + var color = new Color8Bit(); + + assertEquals(0, color.red); + assertEquals(0, color.green); + assertEquals(0, color.blue); + } + + @Test + void testConstructFromInts() { + var color = new Color8Bit(255, 128, 64); + + assertEquals(255, color.red); + assertEquals(128, color.green); + assertEquals(64, color.blue); + } + + @Test + void testConstructFromColor() { + var color = new Color8Bit(new Color(255, 128, 64)); + + assertEquals(255, color.red); + assertEquals(128, color.green); + assertEquals(64, color.blue); + } + + @Test + void testConstructFromHexString() { + var color = new Color8Bit("#FF8040"); + + assertEquals(255, color.red); + assertEquals(128, color.green); + assertEquals(64, color.blue); + + // No leading # + assertThrows(IllegalArgumentException.class, () -> new Color8Bit("112233")); + + // Too long + assertThrows(IllegalArgumentException.class, () -> new Color8Bit("#11223344")); + + // Invalid hex characters + assertThrows(IllegalArgumentException.class, () -> new Color8Bit("#$$$$$$")); + } + + @Test + void testToHexString() { + var color = new Color8Bit(255, 128, 64); + + assertEquals("#FF8040", color.toHexString()); + assertEquals("#FF8040", color.toString()); + } +} diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/util/ColorTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/util/ColorTest.java new file mode 100644 index 00000000000..c6af4e35c21 --- /dev/null +++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/util/ColorTest.java @@ -0,0 +1,74 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package edu.wpi.first.wpilibj.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +class ColorTest { + @Test + void testConstructDefault() { + var color = new Color(); + + assertEquals(0.0, color.red); + assertEquals(0.0, color.green); + assertEquals(0.0, color.blue); + } + + @Test + void testConstructFromDoubles() { + var color = new Color(1.0, 0.5, 0.25); + + assertEquals(1.0, color.red, 1e-2); + assertEquals(0.5, color.green, 1e-2); + assertEquals(0.25, color.blue, 1e-2); + } + + @Test + void testConstructFromInts() { + var color = new Color(255, 128, 64); + + assertEquals(1.0, color.red, 1e-2); + assertEquals(0.5, color.green, 1e-2); + assertEquals(0.25, color.blue, 1e-2); + } + + @Test + void testConstructFromHexString() { + var color = new Color("#FF8040"); + + assertEquals(1.0, color.red, 1e-2); + assertEquals(0.5, color.green, 1e-2); + assertEquals(0.25, color.blue, 1e-2); + + // No leading # + assertThrows(IllegalArgumentException.class, () -> new Color("112233")); + + // Too long + assertThrows(IllegalArgumentException.class, () -> new Color("#11223344")); + + // Invalid hex characters + assertThrows(IllegalArgumentException.class, () -> new Color("#$$$$$$")); + } + + @Test + void testFromHSV() { + var color = Color.fromHSV(90, 128, 64); + + assertEquals(0.125732421875, color.red); + assertEquals(0.251220703125, color.green); + assertEquals(0.251220703125, color.blue); + } + + @Test + void testToHexString() { + var color = new Color(255, 128, 64); + + assertEquals("#FF8040", color.toHexString()); + assertEquals("#FF8040", color.toString()); + } +}