From 5e1c6a84ceb4dc8075eedac393d10603d6b6a4f3 Mon Sep 17 00:00:00 2001 From: Sam Carlberg Date: Fri, 29 Nov 2024 00:25:54 -0500 Subject: [PATCH] [wpilibj, wpilibc] Fix LED patterns not offsetting reads (#6948) Was causing bugs when combined with patterns that need to read back from the buffer (eg masks and overlays) Co-authored-by: Joseph Eng --- wpilibc/src/main/native/cpp/LEDPattern.cpp | 68 +++--- .../src/main/native/include/frc/LEDPattern.h | 56 +++-- .../src/test/native/cpp/LEDPatternTest.cpp | 214 ++++++++++++++++++ .../edu/wpi/first/wpilibj/LEDPattern.java | 108 +++++---- .../edu/wpi/first/wpilibj/LEDPatternTest.java | 172 ++++++++++++++ 5 files changed, 529 insertions(+), 89 deletions(-) diff --git a/wpilibc/src/main/native/cpp/LEDPattern.cpp b/wpilibc/src/main/native/cpp/LEDPattern.cpp index d06d1be5268..363bd545fe4 100644 --- a/wpilibc/src/main/native/cpp/LEDPattern.cpp +++ b/wpilibc/src/main/native/cpp/LEDPattern.cpp @@ -18,33 +18,46 @@ using namespace frc; -LEDPattern::LEDPattern(LEDPatternFn impl) : m_impl(std::move(impl)) {} +LEDPattern::LEDPattern(std::function)> + impl) + : m_impl(std::move(impl)) {} + +void LEDPattern::ApplyTo(LEDPattern::LEDReader reader, + std::function writer) const { + m_impl(reader, writer); +} void LEDPattern::ApplyTo(std::span data, - LEDWriterFn writer) const { - m_impl(data, writer); + std::function writer) const { + ApplyTo(LEDPattern::LEDReader{[=](size_t i) { return data[i]; }, data.size()}, + writer); } void LEDPattern::ApplyTo(std::span data) const { ApplyTo(data, [&](int index, Color color) { data[index].SetLED(color); }); } -LEDPattern LEDPattern::Reversed() { - return LEDPattern{[self = *this](auto data, auto writer) { - self.ApplyTo(data, [&](int i, Color color) { - writer((data.size() - 1) - i, color); - }); +LEDPattern LEDPattern::MapIndex( + std::function indexMapper) { + return LEDPattern{[self = *this, indexMapper](auto data, auto writer) { + size_t bufLen = data.size(); + self.ApplyTo( + LEDPattern::LEDReader{ + [=](auto i) { return data[indexMapper(bufLen, i)]; }, bufLen}, + [&](int i, Color color) { writer(indexMapper(bufLen, i), color); }); }}; } +LEDPattern LEDPattern::Reversed() { + return MapIndex([](size_t bufLen, size_t i) { return bufLen - 1 - i; }); +} + LEDPattern LEDPattern::OffsetBy(int offset) { - return LEDPattern{[=, self = *this](auto data, auto writer) { - self.ApplyTo(data, [&data, &writer, offset](int i, Color color) { - int shiftedIndex = - frc::FloorMod(i + offset, static_cast(data.size())); - writer(shiftedIndex, color); - }); - }}; + return MapIndex([offset](size_t bufLen, size_t i) { + return frc::FloorMod(static_cast(i) + offset, + static_cast(bufLen)); + }); } LEDPattern LEDPattern::ScrollAtRelativeSpeed(units::hertz_t velocity) { @@ -53,8 +66,7 @@ LEDPattern LEDPattern::ScrollAtRelativeSpeed(units::hertz_t velocity) { // Invert and multiply by 1,000,000 to get microseconds double periodMicros = 1e6 / velocity.value(); - return LEDPattern{[=, self = *this](auto data, auto writer) { - auto bufLen = data.size(); + return MapIndex([=](size_t bufLen, size_t i) { auto now = wpi::Now(); // index should move by (bufLen) / (period) @@ -62,12 +74,9 @@ LEDPattern LEDPattern::ScrollAtRelativeSpeed(units::hertz_t velocity) { (now % static_cast(std::floor(periodMicros))) / periodMicros; int offset = static_cast(std::floor(t * bufLen)); - self.ApplyTo(data, [=](int i, Color color) { - // floorMod so if the offset is negative, we still get positive outputs - int shiftedIndex = frc::FloorMod(i + offset, static_cast(bufLen)); - writer(shiftedIndex, color); - }); - }}; + return frc::FloorMod(static_cast(i) + offset, + static_cast(bufLen)); + }); } LEDPattern LEDPattern::ScrollAtAbsoluteSpeed( @@ -77,8 +86,7 @@ LEDPattern LEDPattern::ScrollAtAbsoluteSpeed( auto microsPerLed = static_cast(std::floor((ledSpacing / velocity).value() * 1e6)); - return LEDPattern{[=, self = *this](auto data, auto writer) { - auto bufLen = data.size(); + return MapIndex([=](size_t bufLen, size_t i) { auto now = wpi::Now(); // every step in time that's a multiple of microsPerLED will increment @@ -87,13 +95,9 @@ LEDPattern LEDPattern::ScrollAtAbsoluteSpeed( // offset values for negative velocities auto offset = static_cast(now) / microsPerLed; - self.ApplyTo(data, [=, &writer](int i, Color color) { - // FloorMod so if the offset is negative, we still get positive outputs - int shiftedIndex = frc::FloorMod(i + offset, static_cast(bufLen)); - - writer(shiftedIndex, color); - }); - }}; + return frc::FloorMod(static_cast(i) + offset, + static_cast(bufLen)); + }); } LEDPattern LEDPattern::Blink(units::second_t onTime, units::second_t offTime) { diff --git a/wpilibc/src/main/native/include/frc/LEDPattern.h b/wpilibc/src/main/native/include/frc/LEDPattern.h index f2dcf28485c..0b8cfcbd0b7 100644 --- a/wpilibc/src/main/native/include/frc/LEDPattern.h +++ b/wpilibc/src/main/native/include/frc/LEDPattern.h @@ -18,21 +18,36 @@ namespace frc { -/** - * Sets the LED at the given index to the given color. - */ -using LEDWriterFn = std::function; - -/** - * Accepts a data buffer (1st argument) and a callback (2nd argument) for - * writing data. - */ -using LEDPatternFn = - std::function, LEDWriterFn)>; - class LEDPattern { public: - explicit LEDPattern(LEDPatternFn impl); + /** + * A wrapper around a length and an arbitrary reader function that accepts an + * LED index and returns data for the LED at that index. This configuration + * allows us to abstract over different container types without templating. + */ + class LEDReader { + public: + LEDReader(std::function impl, + size_t size) + : m_impl{std::move(impl)}, m_size{size} {} + + frc::AddressableLED::LEDData operator[](size_t index) const { + return m_impl(index); + } + + size_t size() const { return m_size; } + + private: + std::function m_impl; + size_t m_size; + }; + + explicit LEDPattern(std::function)> + impl); + + void ApplyTo(LEDReader reader, + std::function writer) const; /** * Writes the pattern to an LED buffer. Dynamic animations should be called @@ -48,7 +63,7 @@ class LEDPattern { * @param writer data writer for setting new LED colors on the LED strip */ void ApplyTo(std::span data, - LEDWriterFn writer) const; + std::function writer) const; /** * Writes the pattern to an LED buffer. Dynamic animations should be called @@ -64,6 +79,15 @@ class LEDPattern { */ void ApplyTo(std::span data) const; + /** + * Creates a pattern with remapped indices. + * + * @param indexMapper the index mapper + * @return the mapped pattern + */ + [[nodiscard]] + LEDPattern MapIndex(std::function indexMapper); + /** * Creates a pattern that displays this one in reverse. Scrolling patterns * will scroll in the opposite direction (but at the same speed). It will @@ -373,6 +397,8 @@ class LEDPattern { static LEDPattern Rainbow(int saturation, int value); private: - LEDPatternFn m_impl; + std::function)> + m_impl; }; } // namespace frc diff --git a/wpilibc/src/test/native/cpp/LEDPatternTest.cpp b/wpilibc/src/test/native/cpp/LEDPatternTest.cpp index cd9e7d76b7e..467b4bbd813 100644 --- a/wpilibc/src/test/native/cpp/LEDPatternTest.cpp +++ b/wpilibc/src/test/native/cpp/LEDPatternTest.cpp @@ -813,6 +813,220 @@ TEST(LEDPatternTest, ClippingBrightness) { AssertIndexColor(buffer, 0, Color::kWhite); } +TEST(LEDPatternTest, ReverseMask) { + std::array buffer; + + std::array, 4> colorSteps{ + std::pair{0.0, Color::kRed}, std::pair{0.25, Color::kBlue}, + std::pair{0.5, Color::kYellow}, std::pair{0.75, Color::kGreen}}; + std::array, 2> maskSteps{ + std::pair{0, Color::kWhite}, std::pair{0.5, Color::kBlack}}; + + auto pattern = LEDPattern::Steps(colorSteps) + .Mask(LEDPattern::Steps(maskSteps)) + .Reversed(); + + pattern.ApplyTo(buffer); + + AssertIndexColor(buffer, 7, Color::kRed); + AssertIndexColor(buffer, 6, Color::kRed); + AssertIndexColor(buffer, 5, Color::kBlue); + AssertIndexColor(buffer, 4, Color::kBlue); + AssertIndexColor(buffer, 3, Color::kBlack); + AssertIndexColor(buffer, 2, Color::kBlack); + AssertIndexColor(buffer, 1, Color::kBlack); + AssertIndexColor(buffer, 0, Color::kBlack); +} + +TEST(LEDPatternTest, OffsetMask) { + std::array buffer; + + std::array, 4> colorSteps{ + std::pair{0.0, Color::kRed}, std::pair{0.25, Color::kBlue}, + std::pair{0.5, Color::kYellow}, std::pair{0.75, Color::kGreen}}; + std::array, 2> maskSteps{ + std::pair{0, Color::kWhite}, std::pair{0.5, Color::kBlack}}; + + auto pattern = LEDPattern::Steps(colorSteps) + .Mask(LEDPattern::Steps(maskSteps)) + .OffsetBy(4); + + pattern.ApplyTo(buffer); + + AssertIndexColor(buffer, 0, Color::kBlack); + AssertIndexColor(buffer, 1, Color::kBlack); + AssertIndexColor(buffer, 2, Color::kBlack); + AssertIndexColor(buffer, 3, Color::kBlack); + AssertIndexColor(buffer, 4, Color::kRed); + AssertIndexColor(buffer, 5, Color::kRed); + AssertIndexColor(buffer, 6, Color::kBlue); + AssertIndexColor(buffer, 7, Color::kBlue); +} + +TEST(LEDPatternTest, RelativeScrollingMask) { + std::array buffer; + + std::array, 4> colorSteps{ + std::pair{0.0, Color::kRed}, std::pair{0.25, Color::kBlue}, + std::pair{0.5, Color::kYellow}, std::pair{0.75, Color::kGreen}}; + std::array, 2> maskSteps{ + std::pair{0, Color::kWhite}, std::pair{0.5, Color::kBlack}}; + + auto pattern = LEDPattern::Steps(colorSteps) + .Mask(LEDPattern::Steps(maskSteps)) + .ScrollAtRelativeSpeed(units::hertz_t{1e6 / 8.0}); + + pattern.ApplyTo(buffer); + + static uint64_t now = 0ull; + WPI_SetNowImpl([] { return now; }); + + { + now = 0ull; // start + SCOPED_TRACE(fmt::format("Time {}", now)); + + pattern.ApplyTo(buffer); + + AssertIndexColor(buffer, 0, Color::kRed); + AssertIndexColor(buffer, 1, Color::kRed); + AssertIndexColor(buffer, 2, Color::kBlue); + AssertIndexColor(buffer, 3, Color::kBlue); + AssertIndexColor(buffer, 4, Color::kBlack); + AssertIndexColor(buffer, 5, Color::kBlack); + AssertIndexColor(buffer, 6, Color::kBlack); + AssertIndexColor(buffer, 7, Color::kBlack); + } + { + now = 1ull; + SCOPED_TRACE(fmt::format("Time {}", now)); + + pattern.ApplyTo(buffer); + + AssertIndexColor(buffer, 0, Color::kBlack); + AssertIndexColor(buffer, 1, Color::kRed); + AssertIndexColor(buffer, 2, Color::kRed); + AssertIndexColor(buffer, 3, Color::kBlue); + AssertIndexColor(buffer, 4, Color::kBlue); + AssertIndexColor(buffer, 5, Color::kBlack); + AssertIndexColor(buffer, 6, Color::kBlack); + AssertIndexColor(buffer, 7, Color::kBlack); + } + { + now = 2ull; + SCOPED_TRACE(fmt::format("Time {}", now)); + + pattern.ApplyTo(buffer); + + AssertIndexColor(buffer, 0, Color::kBlack); + AssertIndexColor(buffer, 1, Color::kBlack); + AssertIndexColor(buffer, 2, Color::kRed); + AssertIndexColor(buffer, 3, Color::kRed); + AssertIndexColor(buffer, 4, Color::kBlue); + AssertIndexColor(buffer, 5, Color::kBlue); + AssertIndexColor(buffer, 6, Color::kBlack); + AssertIndexColor(buffer, 7, Color::kBlack); + } + { + now = 3ull; + SCOPED_TRACE(fmt::format("Time {}", now)); + + pattern.ApplyTo(buffer); + + AssertIndexColor(buffer, 0, Color::kBlack); + AssertIndexColor(buffer, 1, Color::kBlack); + AssertIndexColor(buffer, 2, Color::kBlack); + AssertIndexColor(buffer, 3, Color::kRed); + AssertIndexColor(buffer, 4, Color::kRed); + AssertIndexColor(buffer, 5, Color::kBlue); + AssertIndexColor(buffer, 6, Color::kBlue); + AssertIndexColor(buffer, 7, Color::kBlack); + } + + WPI_SetNowImpl(nullptr); // cleanup +} + +TEST(LEDPatternTest, AbsoluteScrollingMask) { + std::array buffer; + + std::array, 4> colorSteps{ + std::pair{0.0, Color::kRed}, std::pair{0.25, Color::kBlue}, + std::pair{0.5, Color::kYellow}, std::pair{0.75, Color::kGreen}}; + std::array, 2> maskSteps{ + std::pair{0, Color::kWhite}, std::pair{0.5, Color::kBlack}}; + + auto pattern = LEDPattern::Steps(colorSteps) + .Mask(LEDPattern::Steps(maskSteps)) + .ScrollAtAbsoluteSpeed(1_mps, 1_m); + + pattern.ApplyTo(buffer); + + static uint64_t now = 0ull; + WPI_SetNowImpl([] { return now; }); + + { + now = 0ull; // start + SCOPED_TRACE(fmt::format("Time {}", now)); + + pattern.ApplyTo(buffer); + + AssertIndexColor(buffer, 0, Color::kRed); + AssertIndexColor(buffer, 1, Color::kRed); + AssertIndexColor(buffer, 2, Color::kBlue); + AssertIndexColor(buffer, 3, Color::kBlue); + AssertIndexColor(buffer, 4, Color::kBlack); + AssertIndexColor(buffer, 5, Color::kBlack); + AssertIndexColor(buffer, 6, Color::kBlack); + AssertIndexColor(buffer, 7, Color::kBlack); + } + { + now = 1000000ull; + SCOPED_TRACE(fmt::format("Time {}", now)); + + pattern.ApplyTo(buffer); + + AssertIndexColor(buffer, 0, Color::kBlack); + AssertIndexColor(buffer, 1, Color::kRed); + AssertIndexColor(buffer, 2, Color::kRed); + AssertIndexColor(buffer, 3, Color::kBlue); + AssertIndexColor(buffer, 4, Color::kBlue); + AssertIndexColor(buffer, 5, Color::kBlack); + AssertIndexColor(buffer, 6, Color::kBlack); + AssertIndexColor(buffer, 7, Color::kBlack); + } + { + now = 2000000ull; + SCOPED_TRACE(fmt::format("Time {}", now)); + + pattern.ApplyTo(buffer); + + AssertIndexColor(buffer, 0, Color::kBlack); + AssertIndexColor(buffer, 1, Color::kBlack); + AssertIndexColor(buffer, 2, Color::kRed); + AssertIndexColor(buffer, 3, Color::kRed); + AssertIndexColor(buffer, 4, Color::kBlue); + AssertIndexColor(buffer, 5, Color::kBlue); + AssertIndexColor(buffer, 6, Color::kBlack); + AssertIndexColor(buffer, 7, Color::kBlack); + } + { + now = 3000000ull; + SCOPED_TRACE(fmt::format("Time {}", now)); + + pattern.ApplyTo(buffer); + + AssertIndexColor(buffer, 0, Color::kBlack); + AssertIndexColor(buffer, 1, Color::kBlack); + AssertIndexColor(buffer, 2, Color::kBlack); + AssertIndexColor(buffer, 3, Color::kRed); + AssertIndexColor(buffer, 4, Color::kRed); + AssertIndexColor(buffer, 5, Color::kBlue); + AssertIndexColor(buffer, 6, Color::kBlue); + AssertIndexColor(buffer, 7, Color::kBlack); + } + + WPI_SetNowImpl(nullptr); // cleanup +} + void AssertIndexColor(std::span data, int index, Color color) { frc::Color8Bit color8bit{color}; diff --git a/wpilibj/src/main/java/edu/wpi/first/wpilibj/LEDPattern.java b/wpilibj/src/main/java/edu/wpi/first/wpilibj/LEDPattern.java index 88bb1a755b7..902a54c0319 100644 --- a/wpilibj/src/main/java/edu/wpi/first/wpilibj/LEDPattern.java +++ b/wpilibj/src/main/java/edu/wpi/first/wpilibj/LEDPattern.java @@ -93,6 +93,19 @@ */ @FunctionalInterface public interface LEDPattern { + /** A functional interface for index mapping functions. */ + @FunctionalInterface + interface IndexMapper { + /** + * Maps the index. + * + * @param bufLen Length of the buffer + * @param index The index to map + * @return The mapped index + */ + int apply(int bufLen, int index); + } + /** * Writes the pattern to an LED buffer. Dynamic animations should be called periodically (such as * with a command or with a periodic method) to refresh the buffer over time. @@ -129,6 +142,41 @@ default void applyTo(T readWriter) { applyTo(readWriter, readWriter); } + /** + * Creates a pattern with remapped indices. + * + * @param indexMapper the index mapper + * @return the mapped pattern + */ + default LEDPattern mapIndex(IndexMapper indexMapper) { + return (reader, writer) -> { + int bufLen = reader.getLength(); + applyTo( + new LEDReader() { + @Override + public int getLength() { + return reader.getLength(); + } + + @Override + public int getRed(int index) { + return reader.getRed(indexMapper.apply(bufLen, index)); + } + + @Override + public int getGreen(int index) { + return reader.getGreen(indexMapper.apply(bufLen, index)); + } + + @Override + public int getBlue(int index) { + return reader.getBlue(indexMapper.apply(bufLen, index)); + } + }, + (i, r, g, b) -> writer.setRGB(indexMapper.apply(bufLen, i), r, g, b)); + }; + } + /** * Creates a pattern that displays this one in reverse. Scrolling patterns will scroll in the * opposite direction (but at the same speed). It will treat the end of an LED strip as the start, @@ -143,10 +191,7 @@ default void applyTo(T readWriter) { * @see AddressableLEDBufferView#reversed() */ default LEDPattern reversed() { - return (reader, writer) -> { - int bufLen = reader.getLength(); - applyTo(reader, (i, r, g, b) -> writer.setRGB((bufLen - 1) - i, r, g, b)); - }; + return mapIndex((length, index) -> length - 1 - index); } /** @@ -157,15 +202,7 @@ default LEDPattern reversed() { * @return the offset pattern */ default LEDPattern offsetBy(int offset) { - return (reader, writer) -> { - int bufLen = reader.getLength(); - applyTo( - reader, - (i, r, g, b) -> { - int shiftedIndex = Math.floorMod(i + offset, bufLen); - writer.setRGB(shiftedIndex, r, g, b); - }); - }; + return mapIndex((length, index) -> Math.floorMod(index + offset, length)); } /** @@ -189,23 +226,16 @@ default LEDPattern offsetBy(int offset) { default LEDPattern scrollAtRelativeSpeed(Frequency velocity) { final double periodMicros = velocity.asPeriod().in(Microseconds); - return (reader, writer) -> { - int bufLen = reader.getLength(); - long now = RobotController.getTime(); + return mapIndex( + (bufLen, index) -> { + long now = RobotController.getTime(); - // index should move by (buf.length) / (period) - double t = (now % (long) periodMicros) / periodMicros; - int offset = (int) (t * bufLen); + // index should move by (buf.length) / (period) + double t = (now % (long) periodMicros) / periodMicros; + int offset = (int) (t * bufLen); - applyTo( - reader, - (i, r, g, b) -> { - // floorMod so if the offset is negative, we still get positive outputs - int shiftedIndex = Math.floorMod(i + offset, bufLen); - - writer.setRGB(shiftedIndex, r, g, b); - }); - }; + return Math.floorMod(index + offset, bufLen); + }); } /** @@ -240,22 +270,16 @@ default LEDPattern scrollAtAbsoluteSpeed(LinearVelocity velocity, Distance ledSp var metersPerMicro = velocity.in(Meters.per(Microsecond)); var microsPerLED = (int) (ledSpacing.in(Meters) / metersPerMicro); - return (reader, writer) -> { - int bufLen = reader.getLength(); - long now = RobotController.getTime(); + return mapIndex( + (bufLen, index) -> { + long now = RobotController.getTime(); - // every step in time that's a multiple of microsPerLED will increment the offset by 1 - var offset = now / microsPerLED; + // every step in time that's a multiple of microsPerLED will increment the offset by 1 + var offset = (int) (now / microsPerLED); - applyTo( - reader, - (i, r, g, b) -> { - // floorMod so if the offset is negative, we still get positive outputs - int shiftedIndex = Math.floorMod(i + offset, bufLen); - - writer.setRGB(shiftedIndex, r, g, b); - }); - }; + // floorMod so if the offset is negative, we still get positive outputs + return Math.floorMod(index + offset, bufLen); + }); } /** diff --git a/wpilibj/src/test/java/edu/wpi/first/wpilibj/LEDPatternTest.java b/wpilibj/src/test/java/edu/wpi/first/wpilibj/LEDPatternTest.java index 453fa5710e7..a3c70849132 100644 --- a/wpilibj/src/test/java/edu/wpi/first/wpilibj/LEDPatternTest.java +++ b/wpilibj/src/test/java/edu/wpi/first/wpilibj/LEDPatternTest.java @@ -5,6 +5,7 @@ package edu.wpi.first.wpilibj; import static edu.wpi.first.units.Units.Centimeters; +import static edu.wpi.first.units.Units.Meters; import static edu.wpi.first.units.Units.MetersPerSecond; import static edu.wpi.first.units.Units.Microsecond; import static edu.wpi.first.units.Units.Microseconds; @@ -15,6 +16,7 @@ import static edu.wpi.first.wpilibj.LEDPattern.GradientType.kDiscontinuous; import static edu.wpi.first.wpilibj.util.Color.kBlack; import static edu.wpi.first.wpilibj.util.Color.kBlue; +import static edu.wpi.first.wpilibj.util.Color.kGreen; import static edu.wpi.first.wpilibj.util.Color.kLime; import static edu.wpi.first.wpilibj.util.Color.kMagenta; import static edu.wpi.first.wpilibj.util.Color.kMidnightBlue; @@ -815,6 +817,176 @@ void clippingBrightness() { assertColorEquals(kWhite, buffer.getLED(0)); } + @Test + void reverseMask() { + var pattern = + LEDPattern.steps(Map.of(0, kRed, 0.25, kBlue, 0.5, kYellow, 0.75, kGreen)) + .mask(LEDPattern.steps(Map.of(0, kWhite, 0.5, kBlack))) + .reversed(); + var buffer = new AddressableLEDBuffer(8); + + pattern.applyTo(buffer); + + assertColorEquals(kRed, buffer.getLED(7)); + assertColorEquals(kRed, buffer.getLED(6)); + assertColorEquals(kBlue, buffer.getLED(5)); + assertColorEquals(kBlue, buffer.getLED(4)); + assertColorEquals(kBlack, buffer.getLED(3)); + assertColorEquals(kBlack, buffer.getLED(2)); + assertColorEquals(kBlack, buffer.getLED(1)); + assertColorEquals(kBlack, buffer.getLED(0)); + } + + @Test + void offsetMask() { + var pattern = + LEDPattern.steps(Map.of(0, kRed, 0.25, kBlue, 0.5, kYellow, 0.75, kGreen)) + .mask(LEDPattern.steps(Map.of(0, kWhite, 0.5, kBlack))) + .offsetBy(4); + var buffer = new AddressableLEDBuffer(8); + + pattern.applyTo(buffer); + + assertColorEquals(kBlack, buffer.getLED(0)); + assertColorEquals(kBlack, buffer.getLED(1)); + assertColorEquals(kBlack, buffer.getLED(2)); + assertColorEquals(kBlack, buffer.getLED(3)); + assertColorEquals(kRed, buffer.getLED(4)); + assertColorEquals(kRed, buffer.getLED(5)); + assertColorEquals(kBlue, buffer.getLED(6)); + assertColorEquals(kBlue, buffer.getLED(7)); + } + + @Test + void relativeScrollingMask() { + // [red, red, blue, blue, yellow, yellow, green, green] + // under a mask of first 50% on, last 50% off + // [red, red, blue, blue, black, black, black, black] + // all scrolling at 1 LED per microsecond + var pattern = + LEDPattern.steps(Map.of(0, kRed, 0.25, kBlue, 0.5, kYellow, 0.75, kGreen)) + .mask(LEDPattern.steps(Map.of(0, kWhite, 0.5, kBlack))) + .scrollAtRelativeSpeed(Percent.per(Microsecond).of(12.5)); + var buffer = new AddressableLEDBuffer(8); + + { + m_mockTime = 0; // start + pattern.applyTo(buffer); + assertColorEquals(kRed, buffer.getLED(0)); + assertColorEquals(kRed, buffer.getLED(1)); + assertColorEquals(kBlue, buffer.getLED(2)); + assertColorEquals(kBlue, buffer.getLED(3)); + assertColorEquals(kBlack, buffer.getLED(4)); + assertColorEquals(kBlack, buffer.getLED(5)); + assertColorEquals(kBlack, buffer.getLED(6)); + assertColorEquals(kBlack, buffer.getLED(7)); + } + + { + m_mockTime = 1; + pattern.applyTo(buffer); + assertColorEquals(kBlack, buffer.getLED(0)); + assertColorEquals(kRed, buffer.getLED(1)); + assertColorEquals(kRed, buffer.getLED(2)); + assertColorEquals(kBlue, buffer.getLED(3)); + assertColorEquals(kBlue, buffer.getLED(4)); + assertColorEquals(kBlack, buffer.getLED(5)); + assertColorEquals(kBlack, buffer.getLED(6)); + assertColorEquals(kBlack, buffer.getLED(7)); + } + + { + m_mockTime = 2; + pattern.applyTo(buffer); + assertColorEquals(kBlack, buffer.getLED(0)); + assertColorEquals(kBlack, buffer.getLED(1)); + assertColorEquals(kRed, buffer.getLED(2)); + assertColorEquals(kRed, buffer.getLED(3)); + assertColorEquals(kBlue, buffer.getLED(4)); + assertColorEquals(kBlue, buffer.getLED(5)); + assertColorEquals(kBlack, buffer.getLED(6)); + assertColorEquals(kBlack, buffer.getLED(7)); + } + + { + m_mockTime = 3; + pattern.applyTo(buffer); + assertColorEquals(kBlack, buffer.getLED(0)); + assertColorEquals(kBlack, buffer.getLED(1)); + assertColorEquals(kBlack, buffer.getLED(2)); + assertColorEquals(kRed, buffer.getLED(3)); + assertColorEquals(kRed, buffer.getLED(4)); + assertColorEquals(kBlue, buffer.getLED(5)); + assertColorEquals(kBlue, buffer.getLED(6)); + assertColorEquals(kBlack, buffer.getLED(7)); + } + } + + @Test + void absoluteScrollingMask() { + // [red, red, blue, blue, yellow, yellow, green, green] + // under a mask of first 50% on, last 50% off + // [red, red, blue, blue, black, black, black, black] + // all scrolling at 1 LED per microsecond + var pattern = + LEDPattern.steps(Map.of(0, kRed, 0.25, kBlue, 0.5, kYellow, 0.75, kGreen)) + .mask(LEDPattern.steps(Map.of(0, kWhite, 0.5, kBlack))) + .scrollAtAbsoluteSpeed(Meters.per(Microsecond).of(1), Meters.one()); + var buffer = new AddressableLEDBuffer(8); + + { + m_mockTime = 0; // start + pattern.applyTo(buffer); + assertColorEquals(kRed, buffer.getLED(0)); + assertColorEquals(kRed, buffer.getLED(1)); + assertColorEquals(kBlue, buffer.getLED(2)); + assertColorEquals(kBlue, buffer.getLED(3)); + assertColorEquals(kBlack, buffer.getLED(4)); + assertColorEquals(kBlack, buffer.getLED(5)); + assertColorEquals(kBlack, buffer.getLED(6)); + assertColorEquals(kBlack, buffer.getLED(7)); + } + + { + m_mockTime = 1; + pattern.applyTo(buffer); + assertColorEquals(kBlack, buffer.getLED(0)); + assertColorEquals(kRed, buffer.getLED(1)); + assertColorEquals(kRed, buffer.getLED(2)); + assertColorEquals(kBlue, buffer.getLED(3)); + assertColorEquals(kBlue, buffer.getLED(4)); + assertColorEquals(kBlack, buffer.getLED(5)); + assertColorEquals(kBlack, buffer.getLED(6)); + assertColorEquals(kBlack, buffer.getLED(7)); + } + + { + m_mockTime = 2; + pattern.applyTo(buffer); + assertColorEquals(kBlack, buffer.getLED(0)); + assertColorEquals(kBlack, buffer.getLED(1)); + assertColorEquals(kRed, buffer.getLED(2)); + assertColorEquals(kRed, buffer.getLED(3)); + assertColorEquals(kBlue, buffer.getLED(4)); + assertColorEquals(kBlue, buffer.getLED(5)); + assertColorEquals(kBlack, buffer.getLED(6)); + assertColorEquals(kBlack, buffer.getLED(7)); + } + + { + m_mockTime = 3; + pattern.applyTo(buffer); + assertColorEquals(kBlack, buffer.getLED(0)); + assertColorEquals(kBlack, buffer.getLED(1)); + assertColorEquals(kBlack, buffer.getLED(2)); + assertColorEquals(kRed, buffer.getLED(3)); + assertColorEquals(kRed, buffer.getLED(4)); + assertColorEquals(kBlue, buffer.getLED(5)); + assertColorEquals(kBlue, buffer.getLED(6)); + assertColorEquals(kBlack, buffer.getLED(7)); + } + } + void assertColorEquals(Color expected, Color actual) { assertEquals(new Color8Bit(expected), new Color8Bit(actual)); }