#include "fl/corkscrew.h" #include "fl/algorithm.h" #include "fl/assert.h" #include "fl/math.h" #include "fl/splat.h" #include "fl/warn.h" #include "fl/tile2x2.h" #include "fl/math_macros.h" #include "fl/unused.h" #include "fl/map_range.h" #include "fl/leds.h" #include "fl/grid.h" #include "fl/screenmap.h" #include "fl/memory.h" #include "fl/int.h" namespace fl { namespace { // New helper function to calculate individual LED position vec2f calculateLedPositionExtended(fl::u16 ledIndex, fl::u16 numLeds, float totalTurns, const Gap& gapParams, fl::u16 width, fl::u16 height) { FL_UNUSED(height); FL_UNUSED(totalTurns); // Check if gap feature is active AND will actually be triggered bool gapActive = (gapParams.num_leds > 0 && gapParams.gap > 0.0f && numLeds > static_cast(gapParams.num_leds)); if (!gapActive) { // Original behavior when no gap or gap never triggers const float ledProgress = static_cast(ledIndex) / static_cast(numLeds - 1); const fl::u16 row = ledIndex / width; const fl::u16 remainder = ledIndex % width; const float alpha = static_cast(remainder) / static_cast(width); const float width_pos = ledProgress * numLeds; const float height_pos = static_cast(row) + alpha; return vec2f(width_pos, height_pos); } // Simplified gap calculation based on user expectation // User wants: LED0=0, LED1=3, LED2=6(wraps to 0) with width=3 // This suggests they want regular spacing of width units per LED // Simple spacing: each LED is separated by exactly width units float width_pos = static_cast(ledIndex) * static_cast(width); // For height, divide by width to get turn progress float height_pos = width_pos / static_cast(width); return vec2f(width_pos, height_pos); } void calculateDimensions(float totalTurns, fl::u16 numLeds, const Gap& gapParams, fl::u16 *width, fl::u16 *height) { FL_UNUSED(gapParams); // Calculate optimal width and height float ledsPerTurn = static_cast(numLeds) / totalTurns; fl::u16 calc_width = static_cast(fl::ceil(ledsPerTurn)); fl::u16 height_from_turns = static_cast(fl::ceil(totalTurns)); fl::u16 calc_height; // If the grid would have more pixels than LEDs, adjust height to better match if (calc_width * height_from_turns > numLeds) { // Calculate height that better matches LED count calc_height = static_cast(fl::ceil(static_cast(numLeds) / static_cast(calc_width))); } else { calc_height = height_from_turns; } *width = calc_width; *height = calc_height; } } // namespace // New primary constructor Corkscrew::Corkscrew(float totalTurns, fl::u16 numLeds, bool invert, const Gap& gapParams) : mTotalTurns(totalTurns), mNumLeds(numLeds), mGapParams(gapParams), mInvert(invert) { fl::calculateDimensions(mTotalTurns, mNumLeds, mGapParams, &mWidth, &mHeight); mOwnsPixels = false; } // Constructor with external pixel buffer Corkscrew::Corkscrew(float totalTurns, fl::span dstPixels, bool invert, const Gap& gapParams) : mTotalTurns(totalTurns), mNumLeds(static_cast(dstPixels.size())), mGapParams(gapParams), mInvert(invert) { fl::calculateDimensions(mTotalTurns, mNumLeds, mGapParams, &mWidth, &mHeight); mPixelStorage = dstPixels; mOwnsPixels = false; // External span } vec2f Corkscrew::at_no_wrap(fl::u16 i) const { if (i >= mNumLeds) { // Handle out-of-bounds access, possibly by returning a default value return vec2f(0, 0); } // Compute position on-the-fly vec2f position = calculateLedPositionExtended(i, mNumLeds, mTotalTurns, mGapParams, mWidth, mHeight); // // Apply inversion if requested // if (mInvert) { // fl::u16 invertedIndex = mNumLeds - 1 - i; // position = calculateLedPositionExtended(invertedIndex, mNumLeds, mTotalTurns, // mGapParams, mState.width, mState.height); // } // now wrap the x-position //position.x = fmodf(position.x, static_cast(mState.width)); return position; } vec2f Corkscrew::at_exact(fl::u16 i) const { // Get the unwrapped position vec2f position = at_no_wrap(i); // Apply cylindrical wrapping to the x-position (like at_wrap does) position.x = fmodf(position.x, static_cast(mWidth)); return position; } Tile2x2_u8 Corkscrew::at_splat_extrapolate(float i) const { if (i >= mNumLeds) { // Handle out-of-bounds access, possibly by returning a default // Tile2x2_u8 FASTLED_ASSERT(false, "Out of bounds access in Corkscrew at_splat: " << i << " size: " << mNumLeds); return Tile2x2_u8(); } // Use the splat function to convert the vec2f to a Tile2x2_u8 float i_floor = floorf(i); float i_ceil = ceilf(i); if (ALMOST_EQUAL_FLOAT(i_floor, i_ceil)) { // If the index is the same, just return the splat of that index vec2f position = at_no_wrap(static_cast(i_floor)); return splat(position); } else { // Interpolate between the two points and return the splat of the result vec2f pos1 = at_no_wrap(static_cast(i_floor)); vec2f pos2 = at_no_wrap(static_cast(i_ceil)); float t = i - i_floor; vec2f interpolated_pos = map_range(t, 0.0f, 1.0f, pos1, pos2); return splat(interpolated_pos); } } fl::size Corkscrew::size() const { return mNumLeds; } Tile2x2_u8_wrap Corkscrew::at_wrap(float i) const { if (mCachingEnabled) { // Use cache if enabled initializeCache(); // Convert float index to integer for cache lookup fl::size cache_index = static_cast(i); if (cache_index < mTileCache.size()) { return mTileCache[cache_index]; } } // Fall back to dynamic calculation if cache disabled or index out of bounds return calculateTileAtWrap(i); } Tile2x2_u8_wrap Corkscrew::calculateTileAtWrap(float i) const { // This is a splatted pixel, but wrapped around the cylinder. // This is useful for rendering the corkscrew in a cylindrical way. Tile2x2_u8 tile = at_splat_extrapolate(i); Tile2x2_u8_wrap::Entry data[2][2]; vec2 origin = tile.origin(); for (fl::u8 x = 0; x < 2; x++) { for (fl::u8 y = 0; y < 2; y++) { // For each pixel in the tile, map it to the cylinder so that each subcomponent // is mapped to the correct position on the cylinder. vec2 pos = origin + vec2(x, y); // now wrap the x-position pos.x = fmodf(pos.x, static_cast(mWidth)); data[x][y] = {pos, tile.at(x, y)}; } } return Tile2x2_u8_wrap(data); } void Corkscrew::setCachingEnabled(bool enabled) { if (!enabled && mCachingEnabled) { // Caching was enabled, now disabling - clear the cache mTileCache.clear(); mCacheInitialized = false; } mCachingEnabled = enabled; } void Corkscrew::initializeCache() const { if (!mCacheInitialized && mCachingEnabled) { // Initialize cache with tiles for each LED position mTileCache.resize(mNumLeds); // Populate cache lazily for (fl::size i = 0; i < mNumLeds; ++i) { mTileCache[i] = calculateTileAtWrap(static_cast(i)); } mCacheInitialized = true; } } CRGB* Corkscrew::rawData() { // Use variant storage if available, otherwise fall back to input surface if (!mPixelStorage.empty()) { if (mPixelStorage.template is>()) { return mPixelStorage.template get>().data(); } else if (mPixelStorage.template is>>()) { return mPixelStorage.template get>>().data(); } } // Fall back to input surface data auto surface = getOrCreateInputSurface(); return surface->data(); } fl::span Corkscrew::data() { // Use variant storage if available, otherwise fall back to input surface if (!mPixelStorage.empty()) { if (mPixelStorage.template is>()) { return mPixelStorage.template get>(); } else if (mPixelStorage.template is>>()) { auto& vec = mPixelStorage.template get>>(); return fl::span(vec.data(), vec.size()); } } // Fall back to input surface data as span auto surface = getOrCreateInputSurface(); return fl::span(surface->data(), surface->size()); } void Corkscrew::readFrom(const fl::Grid& source_grid, bool use_multi_sampling) { if (use_multi_sampling) { readFromMulti(source_grid); return; } // Get or create the input surface auto target_surface = getOrCreateInputSurface(); // Clear surface first target_surface->clear(); // Iterate through each LED in the corkscrew for (fl::size led_idx = 0; led_idx < mNumLeds; ++led_idx) { // Get the rectangular coordinates for this corkscrew LED vec2f rect_pos = at_no_wrap(static_cast(led_idx)); // Convert to integer coordinates for indexing vec2i16 coord(static_cast(rect_pos.x + 0.5f), static_cast(rect_pos.y + 0.5f)); // Clamp coordinates to grid bounds coord.x = MAX(0, MIN(coord.x, static_cast(source_grid.width()) - 1)); coord.y = MAX(0, MIN(coord.y, static_cast(source_grid.height()) - 1)); // Sample from the source fl::Grid using its at() method CRGB sampled_color = source_grid.at(coord.x, coord.y); // Store the sampled color directly in the target surface if (led_idx < target_surface->size()) { target_surface->data()[led_idx] = sampled_color; } } } void Corkscrew::clear() { // Clear input surface if it exists if (mInputSurface) { mInputSurface->clear(); mInputSurface.reset(); // Free the shared_ptr memory } // Clear pixel storage if we own it (vector variant) if (!mPixelStorage.empty()) { if (mPixelStorage.template is>>()) { auto& vec = mPixelStorage.template get>>(); vec.clear(); // Note: fl::vector doesn't have shrink_to_fit(), but clear() frees the memory } // Note: Don't clear external spans as we don't own that memory } // Clear tile cache mTileCache.clear(); // Note: fl::vector doesn't have shrink_to_fit(), but clear() frees the memory mCacheInitialized = false; } void Corkscrew::fillInputSurface(const CRGB& color) { auto target_surface = getOrCreateInputSurface(); for (fl::size i = 0; i < target_surface->size(); ++i) { target_surface->data()[i] = color; } } void Corkscrew::draw(bool use_multi_sampling) { // The draw method should map from the rectangular surface to the LED pixel data // This is the reverse of readFrom - we read from our surface and populate LED data auto source_surface = getOrCreateInputSurface(); // Make sure we have pixel storage if (mPixelStorage.empty()) { // If no pixel storage is configured, there's nothing to draw to return; } CRGB* led_data = rawData(); if (!led_data) return; if (use_multi_sampling) { // Use multi-sampling to get better accuracy for (fl::size led_idx = 0; led_idx < mNumLeds; ++led_idx) { // Get the wrapped tile for this LED position Tile2x2_u8_wrap tile = at_wrap(static_cast(led_idx)); // Accumulate color from the 4 sample points with their weights fl::u32 r_accum = 0, g_accum = 0, b_accum = 0; fl::u32 total_weight = 0; // Sample from each of the 4 corners of the tile for (fl::u8 x = 0; x < 2; x++) { for (fl::u8 y = 0; y < 2; y++) { const auto& entry = tile.at(x, y); vec2 pos = entry.first; fl::u8 weight = entry.second; // Bounds check for the source surface if (pos.x < source_surface->width() && pos.y < source_surface->height()) { // Sample from the source surface CRGB sample_color = source_surface->at(pos.x, pos.y); // Accumulate weighted color components r_accum += static_cast(sample_color.r) * weight; g_accum += static_cast(sample_color.g) * weight; b_accum += static_cast(sample_color.b) * weight; total_weight += weight; } } } // Calculate final color by dividing by total weight CRGB final_color = CRGB::Black; if (total_weight > 0) { final_color.r = static_cast(r_accum / total_weight); final_color.g = static_cast(g_accum / total_weight); final_color.b = static_cast(b_accum / total_weight); } // Store the result in the LED data led_data[led_idx] = final_color; } } else { // Simple non-multi-sampling version for (fl::size led_idx = 0; led_idx < mNumLeds; ++led_idx) { // Get the rectangular coordinates for this corkscrew LED vec2f rect_pos = at_no_wrap(static_cast(led_idx)); // Convert to integer coordinates for indexing vec2i16 coord(static_cast(rect_pos.x + 0.5f), static_cast(rect_pos.y + 0.5f)); // Clamp coordinates to surface bounds coord.x = MAX(0, MIN(coord.x, static_cast(source_surface->width()) - 1)); coord.y = MAX(0, MIN(coord.y, static_cast(source_surface->height()) - 1)); // Sample from the source surface CRGB sampled_color = source_surface->at(coord.x, coord.y); // Store the sampled color in the LED data led_data[led_idx] = sampled_color; } } } void Corkscrew::readFromMulti(const fl::Grid& source_grid) const { // Get the target surface and clear it auto target_surface = const_cast(this)->getOrCreateInputSurface(); target_surface->clear(); const u16 width = static_cast(source_grid.width()); const u16 height = static_cast(source_grid.height()); // Iterate through each LED in the corkscrew for (fl::size led_idx = 0; led_idx < mNumLeds; ++led_idx) { // Get the wrapped tile for this LED position Tile2x2_u8_wrap tile = at_wrap(static_cast(led_idx)); // Accumulate color from the 4 sample points with their weights fl::u32 r_accum = 0, g_accum = 0, b_accum = 0; fl::u32 total_weight = 0; // Sample from each of the 4 corners of the tile for (fl::u8 x = 0; x < 2; x++) { for (fl::u8 y = 0; y < 2; y++) { const auto& entry = tile.at(x, y); vec2 pos = entry.first; // position is the first element of the pair fl::u8 weight = entry.second; // weight is the second element of the pair // Bounds check for the source grid if (pos.x >= 0 && pos.x < width && pos.y >= 0 && pos.y < height) { // Sample from the source grid CRGB sample_color = source_grid.at(pos.x, pos.y); // Accumulate weighted color components r_accum += static_cast(sample_color.r) * weight; g_accum += static_cast(sample_color.g) * weight; b_accum += static_cast(sample_color.b) * weight; total_weight += weight; } } } // Calculate final color by dividing by total weight CRGB final_color = CRGB::Black; if (total_weight > 0) { final_color.r = static_cast(r_accum / total_weight); final_color.g = static_cast(g_accum / total_weight); final_color.b = static_cast(b_accum / total_weight); } // Store the result in the target surface at the LED index position auto target_surface = const_cast(this)->getOrCreateInputSurface(); if (led_idx < target_surface->size()) { target_surface->data()[led_idx] = final_color; } } } // Iterator implementation vec2f Corkscrew::iterator::operator*() const { return corkscrew_->at_no_wrap(static_cast(position_)); } fl::ScreenMap Corkscrew::toScreenMap(float diameter) const { // Create a ScreenMap with the correct number of LEDs fl::ScreenMap screenMap(mNumLeds, diameter); // For each LED index, calculate its position and set it in the ScreenMap for (fl::u16 i = 0; i < mNumLeds; ++i) { // Get the wrapped 2D position for this LED index in the cylindrical mapping vec2f position = at_exact(i); // Set the wrapped position in the ScreenMap screenMap.set(i, position); } return screenMap; } // Enhanced surface handling methods fl::shared_ptr>& Corkscrew::getOrCreateInputSurface() { if (!mInputSurface) { // Create a new Grid with cylinder dimensions using PSRAM allocation mInputSurface = fl::make_shared>(mWidth, mHeight); } return mInputSurface; } fl::Grid& Corkscrew::surface() { return *getOrCreateInputSurface(); } fl::size Corkscrew::pixelCount() const { // Use variant storage if available, otherwise fall back to legacy buffer size if (!mPixelStorage.empty()) { if (mPixelStorage.template is>()) { return mPixelStorage.template get>().size(); } else if (mPixelStorage.template is>>()) { return mPixelStorage.template get>>().size(); } } // Fall back to input size return mNumLeds; } } // namespace fl