From dce1ec5845800ec22040a144e89ac75a9e29e402 Mon Sep 17 00:00:00 2001 From: sayhiben Date: Mon, 19 Jan 2026 23:52:28 -0800 Subject: [PATCH 1/8] Add segment masks --- wled00/FX.h | 36 ++++++++++- wled00/FX_2Dfcn.cpp | 10 +++ wled00/FX_fcn.cpp | 148 +++++++++++++++++++++++++++++++++++++++++++ wled00/const.h | 11 ++++ wled00/data/index.js | 32 ++++++++++ wled00/json.cpp | 20 ++++++ wled00/set.cpp | 1 + wled00/wled.cpp | 1 + wled00/wled.h | 5 ++ 9 files changed, 263 insertions(+), 1 deletion(-) diff --git a/wled00/FX.h b/wled00/FX.h index ddfa3c8777..4fa4a5271c 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -454,6 +454,8 @@ typedef struct Segment { uint16_t startY; // start Y coodrinate 2D (top); there should be no more than 255 rows, but we cannot be sure. uint16_t stopY; // stop Y coordinate 2D (bottom); there should be no more than 255 rows, but we cannot be sure. char *name = nullptr; // WLEDMM initialize to nullptr + uint8_t maskId = 0; // segment mask ID (0 = none) + bool maskInvert = false; // runtime data unsigned long next_time; // millis() of next update @@ -469,6 +471,12 @@ typedef struct Segment { void *jMap = nullptr; //WLEDMM jMap private: + uint8_t *_mask = nullptr; + uint16_t _maskW = 0; + uint16_t _maskH = 0; + size_t _maskLen = 0; // number of mask bits (virtual pixels) + bool _maskValid = false; + union { uint8_t _capabilities; struct { @@ -563,6 +571,8 @@ typedef struct Segment { startY(0), stopY(1), name(nullptr), + maskId(0), + maskInvert(false), next_time(0), step(0), call(0), @@ -571,6 +581,11 @@ typedef struct Segment { data(nullptr), ledsrgb(nullptr), ledsrgbSize(0), //WLEDMM + _mask(nullptr), + _maskW(0), + _maskH(0), + _maskLen(0), + _maskValid(false), _capabilities(0), _dataLen(0), _t(nullptr) @@ -612,6 +627,7 @@ typedef struct Segment { if ((Segment::_globalLeds == nullptr) && !strip_uses_global_leds() && (ledsrgb != nullptr)) {free(ledsrgb); ledsrgb = nullptr;} // WLEDMM we need "!strip_uses_global_leds()" to avoid crashes (#104) if (name) { delete[] name; name = nullptr; } if (_t) { transitional = false; delete _t; _t = nullptr; } + clearMask(); deallocateData(); } @@ -666,6 +682,8 @@ typedef struct Segment { inline void markForReset(void) { reset = true; } // setOption(SEG_OPTION_RESET, true) inline void markForBlank(void) { needsBlank = true; } // WLEDMM serialize "blank" requests, avoid parallel drawing from different task void setUpLeds(void); // set up leds[] array for loseless getPixelColor() + bool setMask(uint8_t id); + void clearMask(); // transition functions void startTransition(uint16_t dur); // transition has to start before actual segment values change @@ -704,6 +722,21 @@ typedef struct Segment { #else inline uint16_t virtualLength(void) const {return _virtuallength;} #endif + inline bool hasMask(void) const { return _mask != nullptr; } + inline bool maskAllows(uint16_t i) const { + if (!_mask || !_maskValid) return true; + if (size_t(i) >= _maskLen) return false; + bool bit = (_mask[i >> 3] >> (i & 7)) & 0x01; + return maskInvert ? !bit : bit; + } + inline bool maskAllowsXY(int x, int y) const { + if (!_mask || !_maskValid) return true; + if (x < 0 || y < 0) return false; + size_t idx = size_t(x) + (size_t(y) * _maskW); + if (idx >= _maskLen) return false; + bool bit = (_mask[idx >> 3] >> (idx & 7)) & 0x01; + return maskInvert ? !bit : bit; + } void setPixelColor(int n, uint32_t c); // set relative pixel within segment with color inline void setPixelColor(int n, byte r, byte g, byte b, byte w = 0) { setPixelColor(n, RGBW32(r,g,b,w)); } // automatically inline inline void setPixelColor(int n, CRGB c) { setPixelColor(n, uint32_t(c) & 0x00FFFFFF); } // automatically inline @@ -1044,7 +1077,8 @@ class WS2812FX { // 96 bytes fixInvalidSegments(), show(void), setTargetFps(uint8_t fps), - enumerateLedmaps(); //WLEDMM (from fcn_declare) + enumerateLedmaps(), //WLEDMM (from fcn_declare) + enumerateSegmasks(); void setColor(uint8_t slot, uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0) { setColor(slot, RGBW32(r,g,b,w)); } void fill(uint32_t c) { for (int i = 0; i < getLengthTotal(); i++) setPixelColor(i, c); } // fill whole strip with color (inline) diff --git a/wled00/FX_2Dfcn.cpp b/wled00/FX_2Dfcn.cpp index d12153d5ff..95df582e36 100644 --- a/wled00/FX_2Dfcn.cpp +++ b/wled00/FX_2Dfcn.cpp @@ -234,6 +234,14 @@ void Segment::startFrame(void) { _2dWidth = _isValid2D ? calc_virtualWidth() : calc_virtualLength(); _virtuallength = calc_virtualLength(); #endif + + if (_mask) { + uint16_t vW = calc_virtualWidth(); + uint16_t vH = calc_virtualHeight(); + _maskValid = (_maskW == vW && _maskH == vH); + } else { + _maskValid = false; + } } // WLEDMM end @@ -245,6 +253,7 @@ void Segment::startFrame(void) { // * expects scaled color (final brightness) as additional input parameter, plus segment virtualWidth() and virtualHeight() void IRAM_ATTR __attribute__((hot)) Segment::setPixelColorXY_fast(int x, int y, uint32_t col, uint32_t scaled_col, int cols, int rows) const //WLEDMM { + if (!maskAllowsXY(x, y)) return; unsigned i = UINT_MAX; bool sameColor = false; if (ledsrgb) { // WLEDMM small optimization @@ -301,6 +310,7 @@ void IRAM_ATTR_YN Segment::setPixelColorXY(int x, int y, uint32_t col) //WLEDMM: const int_fast16_t rows = virtualHeight(); if (x<0 || y<0 || x >= cols || y >= rows) return; // if pixel would fall out of virtual segment just exit + if (!maskAllowsXY(x, y)) return; unsigned i = UINT_MAX; bool sameColor = false; diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 996bb57b60..3ced384300 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -95,6 +95,11 @@ Segment::Segment(const Segment &orig) { data = nullptr; _dataLen = 0; _t = nullptr; + _mask = nullptr; + _maskLen = 0; + _maskW = 0; + _maskH = 0; + _maskValid = false; if (ledsrgb && !Segment::_globalLeds) {ledsrgb = nullptr; ledsrgbSize = 0;} // WLEDMM if (orig.name) { name = new(std::nothrow) char[strlen(orig.name)+1]; if (name) strcpy(name, orig.name); } if (orig.data) { if (allocateData(orig._dataLen, true)) memcpy(data, orig.data, orig._dataLen); } @@ -142,6 +147,117 @@ void Segment::allocLeds() { } } +void Segment::clearMask() { + if (_mask) { + free(_mask); + _mask = nullptr; + } + _maskLen = 0; + _maskW = 0; + _maskH = 0; + _maskValid = false; +} + +bool Segment::setMask(uint8_t id) { + clearMask(); + maskId = id; + if (id >= WLED_MAX_SEGMASKS) { + maskId = 0; + return false; + } + if (id == 0) return true; + + char fileName[24] = {'\0'}; + snprintf_P(fileName, sizeof(fileName), PSTR("/segmask%d.json"), id); + if (!WLED_FS.exists(fileName)) { + USER_PRINTF("Segment mask missing: %s\n", fileName); + return false; + } + + File f = WLED_FS.open(fileName, "r"); + if (!f) return false; + + uint16_t w = 0; + uint16_t h = 0; + size_t bitLen = 0; + uint8_t* bits = nullptr; + bool inv = maskInvert; + bool ok = false; + + do { + char buf[32] = { '\0' }; + if (!f.find("\"w\":")) break; + f.readBytesUntil('\n', buf, sizeof(buf)-1); + w = atoi(cleanUpName(buf)); + + f.seek(0); + if (!f.find("\"h\":")) break; + memset(buf, 0, sizeof(buf)); + f.readBytesUntil('\n', buf, sizeof(buf)-1); + h = atoi(cleanUpName(buf)); + + if (w == 0 || h == 0) break; + bitLen = size_t(w) * size_t(h); + if (bitLen == 0 || bitLen > (size_t(Segment::maxWidth) * size_t(Segment::maxHeight))) break; + + size_t byteLen = (bitLen + 7) / 8; + bits = (uint8_t*)calloc(byteLen, 1); + if (!bits) { + errorFlag = ERR_LOW_MEM; // WLEDMM raise errorflag + break; + } + + f.seek(0); + if (f.find("\"inv\":")) { + String entry = f.readStringUntil(','); + int end = entry.indexOf('}'); + if (end >= 0) entry.remove(end); + entry.trim(); + if (entry == "true") inv = true; + else if (entry == "false") inv = false; + else break; + } + + f.seek(0); + if (!f.find("\"mask\":")) break; + f.readBytesUntil('[', buf, sizeof(buf)-1); + + size_t i = 0; + bool endOfArray = false; + bool parsedOk = true; + do { + String entry = f.readStringUntil(','); + int bracket = entry.indexOf(']'); + if (bracket >= 0) { + entry.remove(bracket); + endOfArray = true; + } + entry.trim(); + if (entry.length() == 0) { parsedOk = false; break; } + if (i >= bitLen) { parsedOk = false; break; } + if (entry == "1") bits[i >> 3] |= (0x01U << (i & 7)); + else if (entry != "0") { parsedOk = false; break; } + i++; + if (i > bitLen) { parsedOk = false; break; } + } while (f.available() && !endOfArray); + + if (!parsedOk || !endOfArray || i != bitLen) break; + + _mask = bits; + bits = nullptr; + _maskW = w; + _maskH = h; + _maskLen = bitLen; + maskInvert = inv; + _maskValid = (_maskW == calc_virtualWidth() && _maskH == calc_virtualHeight()); + ok = true; + } while (false); + + if (!ok && bits) free(bits); + f.close(); + return ok; +} + // move constructor --> moves everything (including buffer) from orig to this Segment::Segment(Segment &&orig) noexcept { DEBUG_PRINTLN(F("-- Move segment constructor --")); @@ -163,6 +279,11 @@ Segment::Segment(Segment &&orig) noexcept { orig.ledsrgb = nullptr; //WLEDMM orig.ledsrgbSize = 0; // WLEDMM orig.jMap = nullptr; //WLEDMM jMap + orig._mask = nullptr; + orig._maskLen = 0; + orig._maskW = 0; + orig._maskH = 0; + orig._maskValid = false; } // copy assignment --> overwrite segment with orig - deletes old buffers in "this", but does not change orig! @@ -173,6 +294,7 @@ Segment& Segment::operator= (const Segment &orig) { transitional = false; // copied segment cannot be in transition if (name) delete[] name; if (_t) delete _t; + clearMask(); CRGB* oldLeds = ledsrgb; size_t oldLedsSize = ledsrgbSize; if (ledsrgb && !Segment::_globalLeds) free(ledsrgb); @@ -191,6 +313,11 @@ Segment& Segment::operator= (const Segment &orig) { data = nullptr; _dataLen = 0; _t = nullptr; + _mask = nullptr; + _maskLen = 0; + _maskW = 0; + _maskH = 0; + _maskValid = false; //if (!Segment::_globalLeds) {ledsrgb = oldLeds; ledsrgbSize = oldLedsSize;}; // WLEDMM reuse leds instead of ledsrgb = nullptr; if (!Segment::_globalLeds) {ledsrgb = nullptr; ledsrgbSize = 0;}; // WLEDMM copy has no buffers (yet) // copy source data @@ -211,6 +338,7 @@ Segment& Segment::operator= (Segment &&orig) noexcept { transitional = false; // just temporary if (name) { delete[] name; name = nullptr; } // free old name deallocateData(); // free old runtime data + clearMask(); if (_t) { delete _t; _t = nullptr; } if (ledsrgb && !Segment::_globalLeds) free(ledsrgb); //WLEDMM: not needed anymore as we will use leds from copy. no need to nullify ledsrgb as it gets new value in memcpy @@ -230,6 +358,11 @@ Segment& Segment::operator= (Segment &&orig) noexcept { orig.ledsrgb = nullptr; //WLEDMM: do not free as moved to here orig.ledsrgbSize = 0; //WLEDMM orig.jMap = nullptr; //WLEDMM jMap + orig._mask = nullptr; + orig._maskLen = 0; + orig._maskW = 0; + orig._maskH = 0; + orig._maskValid = false; } return *this; } @@ -1001,6 +1134,8 @@ void IRAM_ATTR_YN WLED_O2_ATTR __attribute__((hot)) Segment::setPixelColor(int i i &= 0xFFFF; if (unsigned(i) >= virtualLength()) return; // if pixel would fall out of segment just exit //WLEDMM unsigned(i)>SEGLEN also catches "i<0" + if ((Segment::maxHeight == 1) && !maskAllows(i)) return; + #ifndef WLED_DISABLE_2D if (is2D()) { uint16_t vH = virtualHeight(); // segment height in logical pixels @@ -1397,6 +1532,8 @@ uint8_t Segment::differs(Segment& b) const { if (check3 != b.check3) d |= SEG_DIFFERS_FX; if (startY != b.startY) d |= SEG_DIFFERS_BOUNDS; if (stopY != b.stopY) d |= SEG_DIFFERS_BOUNDS; + if (maskId != b.maskId) d |= SEG_DIFFERS_OPT; + if (maskInvert != b.maskInvert) d |= SEG_DIFFERS_OPT; //bit pattern: (msb first) set:2, sound:1, mapping:3, transposed, mirrorY, reverseY, [transitional, reset,] paused, mirrored, on, reverse, [selected] if ((options & 0b1111111110011110U) != (b.options & 0b1111111110011110U)) d |= SEG_DIFFERS_OPT; @@ -1791,6 +1928,16 @@ void WS2812FX::enumerateLedmaps() { } } +// enumerate all segmaskX.json files on FS +void WS2812FX::enumerateSegmasks() { + segMasks = 0; + for (int i = 1; i < WLED_MAX_SEGMASKS; i++) { + char fileName[24] = {'\0'}; + snprintf_P(fileName, sizeof(fileName), PSTR("/segmask%d.json"), i); + if (WLED_FS.exists(fileName)) segMasks |= 1 << i; + } +} + //do not call this method from system context (network callback) void WS2812FX::finalizeInit(void) @@ -1806,6 +1953,7 @@ void WS2812FX::finalizeInit(void) // if we do it in json.cpp (serializeInfo()) we are getting flashes on LEDs // unfortunately this means we do not get updates after uploads enumerateLedmaps(); + enumerateSegmasks(); _hasWhiteChannel = _isOffRefreshRequired = false; diff --git a/wled00/const.h b/wled00/const.h index c81854dad0..389168e51c 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -91,6 +91,17 @@ #endif #endif +#if defined(WLED_MAX_SEGMASKS) && (WLED_MAX_SEGMASKS > 32 || WLED_MAX_SEGMASKS < 4) + #undef WLED_MAX_SEGMASKS +#endif +#ifndef WLED_MAX_SEGMASKS + #ifdef ESP8266 + #define WLED_MAX_SEGMASKS 10 + #else + #define WLED_MAX_SEGMASKS 16 + #endif +#endif + #ifndef WLED_MAX_SEGNAME_LEN #ifdef ESP8266 #define WLED_MAX_SEGNAME_LEN 32 diff --git a/wled00/data/index.js b/wled00/data/index.js index d1e367dd55..e0bbce5048 100644 --- a/wled00/data/index.js +++ b/wled00/data/index.js @@ -817,6 +817,23 @@ function populateSegments(s) ``+ ``+ ``; + let maskSel = ""; + let maskId = inst.mask || 0; + let maskInv = inst.minv || false; + let maskList = Array.isArray(li.masks) ? li.masks : []; + if (maskList.length > 0 || maskId > 0) { + let opts = ``; + for (const m of maskList) opts += ``; + maskSel = `
Mask
`+ + `
`+ + `
`+ + ``; + } //WLEDMM ARTIFX let fxName = eJson.find((o)=>{return o.id==selectedFx}).name; let cusEff = `
`; @@ -870,6 +887,7 @@ function populateSegments(s) `
`+ (!isMSeg ? rvXck : '') + (isMSeg&&stoY-staY>1&&stoX-staX>1 ? map2D : '') + + maskSel + (s.AudioReactive && s.AudioReactive.on ? "" : sndSim) + (s.ARTIFX && s.ARTIFX.on && fxName.includes("ARTI-FX") ? cusEff : "") + // `