#pragma once /** * @file corkscrew.h * @brief Corkscrew LED strip projection and rendering * * The Corkscrew class provides a complete solution for drawing to densely-wrapped * helical LED strips. It maps a cylindrical coordinate system to a linear LED * strip, allowing you to draw on a rectangular surface and have it correctly * projected onto the corkscrew topology. * * Usage: * 1. Create a Corkscrew with the number of turns and LEDs * 2. Draw patterns on the input surface using surface() * 3. Call draw() to map the surface to LED pixels * 4. Access the final LED data via rawData() * * The class handles: * - Automatic cylindrical dimension calculation * - Pixel storage (external span or internal allocation) * - Multi-sampling for smooth projections * - Gap compensation for non-continuous wrapping * - Iterator interface for advanced coordinate access * * Parameters: * - totalTurns: Number of helical turns around the cylinder * - numLeds: Total number of LEDs in the strip * - invert: Reverse the mapping direction (default: false) * - gapParams: Optional gap compensation for solder points in a strip. */ #include "fl/allocator.h" #include "fl/geometry.h" #include "fl/math.h" #include "fl/math_macros.h" #include "fl/pair.h" #include "fl/tile2x2.h" #include "fl/vector.h" #include "fl/shared_ptr.h" #include "fl/variant.h" #include "fl/span.h" #include "crgb.h" #include "fl/int.h" namespace fl { // Forward declarations class Leds; class ScreenMap; template class Grid; // Simple constexpr functions for compile-time corkscrew dimension calculation constexpr fl::u16 calculateCorkscrewWidth(float totalTurns, fl::u16 numLeds) { return static_cast(ceil_constexpr(static_cast(numLeds) / totalTurns)); } constexpr fl::u16 calculateCorkscrewHeight(float totalTurns, fl::u16 numLeds) { return (calculateCorkscrewWidth(totalTurns, numLeds) * static_cast(ceil_constexpr(totalTurns)) > numLeds) ? static_cast(ceil_constexpr(static_cast(numLeds) / static_cast(calculateCorkscrewWidth(totalTurns, numLeds)))) : static_cast(ceil_constexpr(totalTurns)); } /** * Struct representing gap parameters for corkscrew mapping */ struct Gap { int num_leds = 0; // Number of LEDs after which gap is activated, 0 = no gap float gap = 0.0f; // Gap value from 0 to 1, represents percentage of width unit to add Gap() = default; Gap(float g) : num_leds(0), gap(g) {} // Backwards compatibility constructor Gap(int n, float g) : num_leds(n), gap(g) {} // New constructor with num_leds // Rule of 5 for POD data Gap(const Gap &other) = default; Gap &operator=(const Gap &other) = default; Gap(Gap &&other) noexcept = default; Gap &operator=(Gap &&other) noexcept = default; }; // Maps a Corkscrew defined by the input to a cylindrical mapping for rendering // a densly wrapped LED corkscrew. class Corkscrew { public: // Pixel storage variants - can hold either external span or owned vector using PixelStorage = fl::Variant, fl::vector>>; // Iterator class moved from CorkscrewState class iterator { public: using value_type = vec2f; using difference_type = fl::i32; using pointer = vec2f *; using reference = vec2f &; iterator(const Corkscrew *corkscrew, fl::size position) : corkscrew_(corkscrew), position_(position) {} vec2f operator*() const; iterator &operator++() { ++position_; return *this; } iterator operator++(int) { iterator temp = *this; ++position_; return temp; } iterator &operator--() { --position_; return *this; } iterator operator--(int) { iterator temp = *this; --position_; return temp; } bool operator==(const iterator &other) const { return position_ == other.position_; } bool operator!=(const iterator &other) const { return position_ != other.position_; } difference_type operator-(const iterator &other) const { return static_cast(position_) - static_cast(other.position_); } private: const Corkscrew *corkscrew_; fl::size position_; }; // Constructors that integrate input parameters directly // Primary constructor with default values for invert and gapParams Corkscrew(float totalTurns, fl::u16 numLeds, bool invert = false, const Gap& gapParams = Gap()); // Constructor with external pixel buffer - these pixels will be drawn to directly Corkscrew(float totalTurns, fl::span dstPixels, bool invert = false, const Gap& gapParams = Gap()); Corkscrew(const Corkscrew &) = default; Corkscrew(Corkscrew &&) = default; // Caching control void setCachingEnabled(bool enabled); // Essential API - Core functionality fl::u16 cylinderWidth() const { return mWidth; } fl::u16 cylinderHeight() const { return mHeight; } // Enhanced surface handling with shared_ptr // Note: Input surface will be created on first call fl::shared_ptr>& getOrCreateInputSurface(); // Draw like a regular rectangle surface - access input surface directly fl::Grid& surface(); // Draw the corkscrew by reading from the internal surface and populating LED pixels void draw(bool use_multi_sampling = true); // Pixel storage access - works with both external and owned pixels // This represents the pixels that will be drawn after draw() is called CRGB* rawData(); // Returns span of pixels that will be written to when draw() is called fl::span data(); fl::size pixelCount() const; // Create and return a fully constructed ScreenMap for this corkscrew // Each LED index will be mapped to its exact position on the cylindrical surface fl::ScreenMap toScreenMap(float diameter = 0.5f) const; // STL-style container interface fl::size size() const; iterator begin() { return iterator(this, 0); } iterator end() { return iterator(this, size()); } // Non-essential API - Lower level access vec2f at_no_wrap(fl::u16 i) const; vec2f at_exact(fl::u16 i) const; Tile2x2_u8_wrap at_wrap(float i) const; // Clear all buffers and free memory void clear(); // Fill the input surface with a color void fillInputSurface(const CRGB& color); private: // For internal use. Splats the pixel on the surface which // extends past the width. This extended Tile2x2 is designed // to be wrapped around with a Tile2x2_u8_wrap. Tile2x2_u8 at_splat_extrapolate(float i) const; // Read from fl::Grid object and populate our internal rectangular buffer // by sampling from the XY coordinates mapped to each corkscrew LED position // use_multi_sampling = true will use multi-sampling to sample from the source grid, // this will give a little bit better accuracy and the screenmap will be more accurate. void readFrom(const fl::Grid& source_grid, bool use_multi_sampling = true); // Read from rectangular buffer using multi-sampling and store in target grid // Uses Tile2x2_u8_wrap for sub-pixel accurate sampling with proper blending void readFromMulti(const fl::Grid& target_grid) const; // Initialize the rectangular buffer if not already done void initializeBuffer() const; // Initialize the cache if not already done and caching is enabled void initializeCache() const; // Calculate the tile at position i without using cache Tile2x2_u8_wrap calculateTileAtWrap(float i) const; // Core corkscrew parameters (moved from CorkscrewInput) float mTotalTurns = 19.0f; // Total turns of the corkscrew fl::u16 mNumLeds = 144; // Number of LEDs Gap mGapParams; // Gap parameters for gap accounting bool mInvert = false; // If true, reverse the mapping order // Cylindrical mapping dimensions (moved from CorkscrewState) fl::u16 mWidth = 0; // Width of cylindrical map (circumference of one turn) fl::u16 mHeight = 0; // Height of cylindrical map (total vertical segments) // Enhanced pixel storage - variant supports both external and owned pixels PixelStorage mPixelStorage; bool mOwnsPixels = false; // Track whether we own the pixel data // Input surface for drawing operations fl::shared_ptr> mInputSurface; // Caching for Tile2x2_u8_wrap objects mutable fl::vector mTileCache; mutable bool mCacheInitialized = false; bool mCachingEnabled = true; // Default to enabled }; } // namespace fl