FastLED 3.10.8
Loading...
Searching...
No Matches
screenmap.cpp.hpp
Go to the documentation of this file.
1/* Screenmap maps strip indexes to x,y coordinates. This is used for FastLED Web
2 * to map the 1D strip to a 2D grid. Note that the strip can have arbitrary
3 * size. this was first motivated during the (attempted? Oct. 19th 2024) port of
4 * the Chromancer project to FastLED Web.
5 */
6
7#include "fl/math/screenmap.h"
8
9// Heavy includes moved from header to reduce compilation time
10#include "fl/math/lut.h" // Full LUT definitions needed for implementation
11#include "fl/stl/json.h" // 61.1ms - only needed for ParseJson/toJson implementations
12#include "fl/stl/string.h" // 129.4ms - only needed for string parameters in implementations
13#include "fl/stl/flat_map.h" // 12.4ms - only needed for flat_map parameters in implementations
14// IWYU pragma: begin_keep
15#include "fl/stl/function.h"
16// IWYU pragma: end_keep // ~5ms - only needed for function<> constructor implementation
18
19// Other implementation dependencies
20#include "fl/math/math.h"
21#include "fl/math/math.h"
22#include "fl/stl/vector.h"
23#include "fl/log/log.h"
24#include "fl/stl/noexcept.h"
25
26
27namespace fl {
28
29namespace {
30
31// Every value that reaches `mDiameter` from a caller passes through here.
32//
33// The class accepts any `float` -- three constructors and `setDiameter` all
34// take one unfiltered -- but only two kinds of value mean anything: a
35// positive size, and the -1 sentinel for "unset". A caller who stores 0.0f
36// or -2.0f has named a diameter no LED has. The emitter cannot publish
37// those (a consumer reading `"diameter": -2` learns nothing it can use) and
38// the parsers read an absent key back as -1, so before this normalization a
39// stored 0.0f came back from a round trip as -1.0f: silently different from
40// what was set, and different in a way `getDiameter()` did not show until
41// the file had been through JSON.
42//
43// Folding them to the sentinel at the boundary makes the two agree. A
44// non-positive diameter now reads back as -1 immediately, from the setter
45// onward, so the round trip is the identity on every value the class holds.
46float normalizedDiameter(float diameter) FL_NO_EXCEPT {
47 return diameter > 0.0f ? diameter : -1.0f;
48}
49
50} // namespace
51
52// Default constructor and destructor - must be in .cpp for proper smart_ptr handling
54ScreenMap::~ScreenMap() FL_NO_EXCEPT = default;
55
56// Helper function to extract a vector of floats from a JSON array
57fl::vector<float> jsonArrayToFloatVector(const fl::json& jsonArray) FL_NO_EXCEPT {
59
60 if (!jsonArray.has_value() || !jsonArray.is_array()) {
61 return result;
62 }
63 auto begin_float = jsonArray.begin_array<float>();
64 auto end_float = jsonArray.end_array<float>();
65
66 using T = decltype(*begin_float);
67 FL_STATIC_ASSERT(fl::is_same<T, fl::parse_result<float>>::value, "Value type must be parse_result<float>");
68
69 // Use explicit array iterator style as demonstrated in FEATURE.md
70 // DO NOT CHANGE THIS CODE. FIX THE IMPLIMENTATION IF NECESSARY.
71 for (auto it = begin_float; it != end_float; ++it) {
72 // assert that the value type is parse_result<float>
73
74 // get the name of the type
75 auto parseResult = *it;
76 if (!parseResult.has_error()) {
77 result.push_back(parseResult.get_value());
78 } else {
79 FL_WARN("jsonArrayToFloatVector: parse_result<float> has error: " << parseResult.get_error().message);
80 }
81 }
82
83 return result;
84}
85
86ScreenMap ScreenMap::Circle(int numLeds, float cm_between_leds,
87 float cm_led_diameter, float completion) FL_NO_EXCEPT {
88 ScreenMap screenMap(numLeds);
89
90 // radius from LED spacing
91 float circumference = numLeds * cm_between_leds;
92 float radius = circumference / (2 * FL_PI);
93
94 // how big an arc we light vs leave dark
95 float totalAngle = completion * 2 * FL_PI;
96 float gapAngle = 2 * FL_PI - totalAngle;
97
98 // shift so the dark gap is centered at the bottom (–π/2)
99 float startAngle = -FL_PI / 2 + gapAngle / 2.0f;
100
101 // if partial, land last LED exactly at startAngle+totalAngle
102 float divisor =
103 (completion < 1.0f && numLeds > 1) ? (numLeds - 1) : numLeds;
104
105 for (int i = 0; i < numLeds; ++i) {
106 float angle = startAngle + (i * totalAngle) / divisor;
107 float x = radius * cos(angle) * 2;
108 float y = radius * sin(angle) * 2;
109 screenMap[i] = {x, y};
110 }
111
112 screenMap.setDiameter(cm_led_diameter);
113 return screenMap;
114}
115
116ScreenMap ScreenMap::DefaultStrip(int numLeds, float cm_between_leds,
117 float cm_led_diameter, float completion) FL_NO_EXCEPT {
118 return Circle(numLeds, cm_between_leds, cm_led_diameter, completion);
119}
120
121// Helper: parse a v2 screenmap document into the flat segmentMaps.
122// v2 shape (see ledmapper #92):
123// {
124// "version": 2, // optional, explicit
125// "groups": { "<name>": { "color": "...", ... } }, // optional, ignored by firmware
126// "segments": [
127// { "id": "<unique>", "pin": <int|str>, "group": "<name>",
128// "x": [...], "y": [...], "z": [...], // z optional
129// "parent": "<id>", "offset": <int|null> } // parent+offset on forks only
130// ]
131// }
132//
133// Flatten policy: each segment becomes one ScreenMap entry keyed by `id`.
134// `pin`, `group`, `parent`, `offset` are not represented in the firmware-side
135// flat_map; they're UI/wiring metadata the editor and video tools care about.
136// `z` is dropped (firmware-side ScreenMap is 2D today).
137static bool parseV2SegmentArray(const fl::json& segmentsArr,
139 string *err) FL_NO_EXCEPT {
140 if (!segmentsArr.has_value() || !segmentsArr.is_array()) {
141 *err = "v2 'segments' is not an array";
142 return false;
143 }
144
145 auto arrPtr = segmentsArr.as_array();
146 if (!arrPtr) {
147 *err = "v2 'segments' array could not be read";
148 return false;
149 }
150
151 for (const auto& elem : *arrPtr) {
152 if (!elem) {
153 *err = "v2 segment is null";
154 return false;
155 }
156 fl::json segVal(elem);
157 if (!segVal.has_value() || !segVal.is_object()) {
158 *err = "v2 segment is not an object";
159 return false;
160 }
161
162 // Required: id
163 if (!segVal.contains("id") || !segVal["id"].has_value()) {
164 *err = "v2 segment missing 'id'";
165 return false;
166 }
167 auto idOpt = segVal["id"].as_string();
168 if (!idOpt) {
169 *err = "v2 segment 'id' is not a string";
170 return false;
171 }
172 string id = *idOpt;
173
174 // Required: x
175 if (!segVal.contains("x") || !segVal["x"].has_value() || !segVal["x"].is_array()) {
176 *err = "v2 segment '" + id + "' missing or invalid 'x' array";
177 return false;
178 }
179 fl::vector<float> x_array = jsonArrayToFloatVector(segVal["x"]);
180
181 // Required: y
182 if (!segVal.contains("y") || !segVal["y"].has_value() || !segVal["y"].is_array()) {
183 *err = "v2 segment '" + id + "' missing or invalid 'y' array";
184 return false;
185 }
186 fl::vector<float> y_array = jsonArrayToFloatVector(segVal["y"]);
187
188 string type = "led_strip";
189 if (segVal.contains("type") && segVal["type"].has_value()) {
190 auto typeOpt = segVal["type"].as_string();
191 if (!typeOpt) { *err = "v2 segment '" + id + "' has invalid 'type'"; return false; }
192 type = *typeOpt;
193 }
194 const bool isWire = type == "el_wire";
195 const bool isPanel = type == "el_panel";
196 if (type != "led_strip" && !isWire && !isPanel) {
197 *err = "v2 segment '" + id + "' has unsupported type '" + type + "'";
198 return false;
199 }
200 if (x_array.size() != y_array.size()) {
201 *err = "v2 segment '" + id + "' has mismatched x/y lengths";
202 return false;
203 }
204 const size_t minVertices = isWire ? 2 : isPanel ? 3 : 0;
205 if ((isWire || isPanel) && x_array.size() < minVertices) {
206 *err = "v2 segment '" + id + "' has too few vertices";
207 return false;
208 }
209 float thickness = 0.0f;
210 if (isWire) {
211 if (!segVal.contains("thickness") || !segVal["thickness"].has_value()) {
212 *err = "v2 el_wire segment '" + id + "' missing thickness";
213 return false;
214 }
215 auto thicknessOpt = segVal["thickness"].as_float();
216 if (!thicknessOpt || *thicknessOpt <= 0.0f) {
217 *err = "v2 el_wire segment '" + id + "' requires positive thickness";
218 return false;
219 }
220 thickness = static_cast<float>(*thicknessOpt);
221 } else if (segVal.contains("thickness")) {
222 *err = "v2 segment '" + id + "' thickness is only valid for el_wire";
223 return false;
224 }
225
226 // Optional: diameter (not in canonical v2 but accepted as a backward-compat
227 // hint when present; otherwise the per-group `diameter` could be wired here
228 // in a future iteration).
229 float diameter = -1.0f;
230 if (segVal.contains("diameter") && segVal["diameter"].has_value()) {
231 auto diameterOpt = segVal["diameter"].as_float();
232 if (diameterOpt) {
233 diameter = static_cast<float>(*diameterOpt);
234 }
235 }
236
237 auto n = fl::min(x_array.size(), y_array.size());
238 ScreenMap segment_map(isWire || isPanel ? 1 : static_cast<u32>(n), diameter);
239 if (isWire || isPanel) {
240 fl::vector<vec2f> vertices;
241 vertices.reserve(n);
242 for (size_t i = 0; i < n; i++) vertices.push_back({x_array[i], y_array[i]});
243 segment_map.setShape(0, isWire ? ScreenMap::Shape::EL_WIRE : ScreenMap::Shape::EL_PANEL, vertices.data(), static_cast<u32>(n), thickness);
244 }
245 if (!isWire && !isPanel) {
246 for (size_t i = 0; i < n; i++) {
247 segment_map.set(static_cast<u16>(i), vec2f{x_array[i], y_array[i]});
248 }
249 }
250 (*segmentMaps)[id] = fl::move(segment_map);
251 }
252 return true;
253}
254
255bool ScreenMap::ParseJson(const char *jsonStrScreenMap,
256 fl::flat_map<string, ScreenMap> *segmentMaps, string *err) FL_NO_EXCEPT {
257
258#if FASTLED_NO_JSON
259 FL_UNUSED(jsonStrScreenMap);
260 FL_UNUSED(segmentMaps);
261 FL_UNUSED(err);
262 FL_WARN("ScreenMap::ParseJson called with FASTLED_NO_JSON");
263 if (err) {
264 *err = "JSON is not supported in this build";
265 }
266 return false;
267#else
268 //FL_WARN_SCREENMAP("ParseJson called with JSON: " << jsonStrScreenMap);
269
270 string _err;
271 if (!err) {
272 err = &_err;
273 }
274
275 auto jsonDoc = fl::json::parse(jsonStrScreenMap);
276 if (!jsonDoc.has_value()) {
277 *err = "Failed to parse JSON";
278 FL_WARN("Failed to parse JSON");
279 return false;
280 }
281
282 if (!jsonDoc.is_object()) {
283 *err = "JSON root is not an object";
284 FL_WARN("JSON root is not an object");
285 return false;
286 }
287
288 // ── v2 dispatch ──────────────────────────────────────────────────────
289 // v2 if: explicit "version": 2 OR has top-level "segments" array.
290 // v1 if: explicit "version": 1 OR has top-level "map" object.
291 bool explicitV2 = false;
292 bool explicitV1 = false;
293 if (jsonDoc.contains("version") && jsonDoc["version"].has_value()) {
294 auto versionOpt = jsonDoc["version"].as_int();
295 if (versionOpt) {
296 int v = static_cast<int>(*versionOpt);
297 if (v == 2) explicitV2 = true;
298 else if (v == 1) explicitV1 = true;
299 }
300 }
301 bool hasSegments = jsonDoc.contains("segments") && jsonDoc["segments"].has_value()
302 && jsonDoc["segments"].is_array();
303 bool hasMap = jsonDoc.contains("map") && jsonDoc["map"].has_value()
304 && jsonDoc["map"].is_object();
305
306 if (explicitV2 || (!explicitV1 && hasSegments && !hasMap)) {
307 return parseV2SegmentArray(jsonDoc["segments"], segmentMaps, err);
308 }
309
310 // Fall through to v1 path.
311 // Check if "map" key exists and is an object
312 if (!jsonDoc.contains("map")) {
313 *err = "Missing 'map' key in JSON";
314 FL_WARN("Missing 'map' key in JSON");
315 return false;
316 }
317
318 // Get the map object
319 auto mapObj = jsonDoc["map"];
320 if (!mapObj.has_value() || !mapObj.is_object()) {
321 *err = "Invalid 'map' object in JSON";
322 FL_WARN("Invalid 'map' object in JSON");
323 return false;
324 }
325
326 auto jsonMapPtr = mapObj.as_object();
327 if (!jsonMapPtr || jsonMapPtr->empty()) {
328 *err = "Failed to parse map from JSON or map is empty";
329 FL_WARN("Failed to parse map from JSON or map is empty");
330 return false;
331 }
332
333 auto& jsonMap = *jsonMapPtr;
334
335
336 for (const auto& kv : jsonMap) {
337 auto name = kv.first;
338
339
340 // Check that the value is not null before creating json object
341 if (!kv.second) {
342 *err = "Null value for segment " + name;
343 return false;
344 }
345
346 // Create json object directly from shared_ptr
347 fl::json val(kv.second);
348 if (!val.has_value()) {
349 *err = "Invalid value for segment " + name;
350 return false;
351 }
352
353 if (!val.is_object()) {
354 *err = "Segment value for " + name + " is not an object";
355 return false;
356 }
357
358 // Check if x array exists and is actually an array
359 if (!val.contains("x")) {
360 *err = "Missing x array for " + name;
361 return false;
362 }
363
364 if (!val["x"].has_value() || !val["x"].is_array()) {
365 *err = "Invalid x array for " + name;
366 return false;
367 }
368
369 // Extract x array using our helper function
370 fl::vector<float> x_array = jsonArrayToFloatVector(val["x"]);
371
372 // Check if y array exists and is actually an array
373 if (!val.contains("y")) {
374 *err = "Missing y array for " + name;
375 return false;
376 }
377
378 if (!val["y"].has_value() || !val["y"].is_array()) {
379 *err = "Invalid y array for " + name;
380 return false;
381 }
382
383 // Extract y array using our helper function
384 fl::vector<float> y_array = jsonArrayToFloatVector(val["y"]);
385
386 // Get diameter (optional) with default value
387 float diameter = -1.0f; // default value
388 if (val.contains("diameter") && val["diameter"].has_value()) {
389 auto diameterOpt = val["diameter"].as_float();
390 if (diameterOpt) {
391 diameter = static_cast<float>(*diameterOpt);
392 }
393 }
394
395 auto n = fl::min(x_array.size(), y_array.size());
396 if (n != x_array.size() || n != y_array.size()) {
397 if (n != x_array.size()) {
398 }
399 if (n != y_array.size()) {
400 }
401 }
402
403 ScreenMap segment_map(n, diameter);
404 for (size_t i = 0; i < n; i++) {
405 segment_map.set(i, vec2f{x_array[i], y_array[i]});
406 }
407 (*segmentMaps)[name] = fl::move(segment_map);
408 }
409 return true;
410#endif
411}
412
413bool ScreenMap::ParseJson(const char *jsonStrScreenMap,
414 const char *screenMapName, ScreenMap *screenmap,
415 string *err) FL_NO_EXCEPT {
416
418 bool ok = ParseJson(jsonStrScreenMap, &segmentMaps, err);
419 if (!ok) {
420 return false;
421 }
422 if (segmentMaps.size() == 0) {
423 return false;
424 }
425 if (segmentMaps.contains(screenMapName)) {
426 *screenmap = segmentMaps[screenMapName];
427 return true;
428 }
429 string _err = "ScreenMap not found: ";
430 _err.append(screenMapName);
431 if (err) {
432 *err = _err;
433 }
434
435 return false;
436}
437
439 fl::json *doc) FL_NO_EXCEPT {
440
441#if FASTLED_NO_JSON
442 FL_WARN("ScreenMap::toJson called with FASTLED_NO_JSON");
443 return;
444#else
445 if (!doc) {
446 FL_WARN("ScreenMap::toJson called with nullptr doc");
447 return;
448 }
449
450 // Emits the v2 screenmap shape (issue ledmapper#143):
451 // { "version": 2,
452 // "groups": { "<name>": { "color": "#hex" } },
453 // "segments": [ { "id": "<name>", "group": "<name>",
454 // "x": [...], "y": [...], "diameter": ... } ] }
455 //
456 // `pin` is part of the v2 shape but is not emitted here -- see the note
457 // at the segment object below. Writing the example value from a comment
458 // as data is how it came to be there.
459 // Bilingual readers (`ScreenMap::ParseJson`, ledmapper) accept both v1
460 // and v2, so any existing on-disk v1 JSON keeps loading. v1 emission
461 // is no longer supported.
462 *doc = fl::json::object();
463
464 fl::json groupsObj = fl::json::object();
465 fl::json segmentsArr = fl::json::array();
466
467 // Distinct palette so each strip lights up differently in the editor
468 // preview without the user having to pick a colour. Cycle on overflow.
469 static const char *const kPalette[] = {
470 "#3b82f6", "#10b981", "#f59e0b", "#ef4444",
471 "#a855f7", "#06b6d4", "#ec4899", "#84cc16",
472 };
473 constexpr size_t kPaletteSize = sizeof(kPalette) / sizeof(kPalette[0]);
474
475 size_t idx = 0;
476 for (const auto& kv : segmentMaps) {
477 if (kv.second.getLength() == 0) {
478 FL_WARN("ScreenMap::toJson called with empty segment: " << (fl::string(kv.first)));
479 continue;
480 }
481
482 const auto& name = kv.first;
483 const auto& segment = kv.second;
484 const float diameter = segment.getDiameter();
485
486 fl::json xArray = fl::json::array();
487 for (u16 i = 0; i < segment.getLength(); i++) {
488 xArray.push_back(fl::json(segment[i].x));
489 }
490 fl::json yArray = fl::json::array();
491 for (u16 i = 0; i < segment.getLength(); i++) {
492 yArray.push_back(fl::json(segment[i].y));
493 }
494
495 fl::json groupObj = fl::json::object();
496 groupObj.set("color", fl::json(fl::string(kPalette[idx % kPaletteSize])));
497 groupsObj.set(name, groupObj);
498
499 fl::json segmentObj = fl::json::object();
500 segmentObj.set("id", fl::json(fl::string(name)));
501 // No `pin`. The v2 shape carries one and the parser above drops it --
502 // it is wiring metadata with nowhere to live on this side -- so there
503 // is nothing here to write. What used to be written was the literal
504 // `"pin1"` from the shape example in the comment above, copied into
505 // the emitter as data.
506 //
507 // That is worse than omitting it, because it is wrong rather than
508 // absent. A three-segment file wired to pin1/pin2/pin3 came back with
509 // all three on pin1, and a numeric `"pin": 7` came back as the string
510 // `"pin1"`. A consumer reading that mis-wires a strip and has no way
511 // to tell; a consumer reading a missing optional key can see that it
512 // is missing.
513 //
514 // Carrying the value through instead is FastLED #3322, which needs
515 // somewhere on `ScreenMap` to put it and a decision about the
516 // `<int|str>` union. Until then this loses the field visibly rather
517 // than rewriting it silently.
518 segmentObj.set("group", fl::json(fl::string(name)));
519 segmentObj.set("x", xArray);
520 segmentObj.set("y", yArray);
521 // Only when there is one. `mDiameter` defaults to -1 as a sentinel
522 // meaning "unset", and writing that out publishes an impossible
523 // physical size: no LED is -1 units across. The parser already reads
524 // an absent key back as -1 (both the v1 and v2 paths default it), so
525 // omitting round-trips to exactly the same value inside FastLED and
526 // stops a consumer from having to know the sentinel.
527 //
528 // The field's own comment in screenmap.h asked for this and had the
529 // sense inverted -- "Only serialized if it's not > 0.0f" -- which is
530 // presumably how the writer came to do the opposite.
531 if (diameter > 0.0f) {
532 segmentObj.set("diameter", fl::json(diameter));
533 }
534 segmentsArr.push_back(segmentObj);
535
536 idx++;
537 }
538
539 doc->set("version", fl::json(i64(2)));
540 doc->set("groups", groupsObj);
541 doc->set("segments", segmentsArr);
542
543 fl::string debugStr = doc->to_string();
544 FL_WARN("ScreenMap::toJson generated JSON: " << debugStr);
545#endif
546}
547
549 string *jsonBuffer) FL_NO_EXCEPT {
550 fl::json doc;
551 toJson(segmentMaps, &doc);
552 *jsonBuffer = doc.to_string();
553}
554
556 : length(length), mDiameter(normalizedDiameter(mDiameter)) {
557 if (length > 0) {
559 LUTXYFLOAT &lut = *mLookUpTable.get();
560 vec2f *data = lut.getDataMutable();
561 for (u32 x = 0; x < length; x++) {
562 data[x] = {0, 0};
563 }
564 }
565}
566
567ScreenMap::ScreenMap(const vec2f *lut, u32 length, float diameter) FL_NO_EXCEPT
568 : length(length), mDiameter(normalizedDiameter(diameter)) {
570 LUTXYFLOAT &lut16xy = *mLookUpTable.get();
571 vec2f *data = lut16xy.getDataMutable();
572 for (u32 x = 0; x < length; x++) {
573 data[x] = lut[x];
574 }
575}
576
577ScreenMap::ScreenMap(int count, float diameter, fl::function<void(int, vec2f& pt_out)> func) FL_NO_EXCEPT
578 : length(count), mDiameter(normalizedDiameter(diameter)) {
579 if (count > 0) {
581 LUTXYFLOAT &lut = *mLookUpTable.get();
582 vec2f *data = lut.getDataMutable();
583 for (int i = 0; i < count; i++) {
584 func(i, data[i]);
585 }
586 }
587}
588
590 mDiameter = other.mDiameter;
591 length = other.length;
592 mLookUpTable = other.mLookUpTable;
593 mSourceXYMap = other.mSourceXYMap;
594 mShapes = other.mShapes;
595}
596
598 mDiameter = other.mDiameter;
599 length = other.length;
600 fl::swap(mLookUpTable, other.mLookUpTable);
601 fl::swap(mSourceXYMap, other.mSourceXYMap);
602 fl::swap(mShapes, other.mShapes);
603 other.mLookUpTable.reset();
604 other.mSourceXYMap.reset();
605}
606
607void ScreenMap::set(u16 index, const vec2f &p) FL_NO_EXCEPT {
608 if (mLookUpTable) {
609 LUTXYFLOAT &lut = *mLookUpTable.get();
610 auto *data = lut.getDataMutable();
611 data[index] = p;
612 }
613}
614
616 mDiameter = normalizedDiameter(diameter);
617}
618
620 if (x >= length || !mLookUpTable) {
621 return {0, 0};
622 }
623 LUTXYFLOAT &lut = *mLookUpTable.get();
624 vec2f screen_coords = lut[x];
625 return screen_coords;
626}
627
629
631
633
634 if (length == 0 || !mLookUpTable) {
635 return {0, 0};
636 }
637
638 LUTXYFLOAT &lut = *mLookUpTable.get();
639
640 fl::vec2f *data = lut.getDataMutable();
641 // float minX = lut[0].x;
642 // float maxX = lut[0].x;
643 // float minY = lut[0].y;
644 // float maxY = lut[0].y;
645 float minX = data[0].x;
646 float maxX = data[0].x;
647 float minY = data[0].y;
648 float maxY = data[0].y;
649
650 for (u32 i = 1; i < length; i++) {
651 const vec2f &p = lut[i];
652 minX = fl::min(minX, p.x);
653 maxX = fl::max(maxX, p.x);
654 minY = fl::min(minY, p.y);
655 maxY = fl::max(maxY, p.y);
656 }
657
658 return {maxX - minX, maxY - minY};
659}
660
661void ScreenMap::setShape(u32 index, Shape::Type type, const vec2f *vertices, u32 count,
662 float thickness) FL_NO_EXCEPT {
663 if (!vertices || count == 0) return;
664 if (index >= mShapes.size()) mShapes.resize(index + 1);
665 Shape &shape = mShapes[index];
666 shape.type = type;
667 shape.thickness = thickness;
668 shape.vertices.clear();
669 shape.vertices.reserve(count);
670 for (u32 i = 0; i < count; ++i) shape.vertices.push_back(vertices[i]);
671}
672
673bool ScreenMap::hasShapes() const FL_NO_EXCEPT { return !mShapes.empty(); }
674u32 ScreenMap::getShapeCount() const FL_NO_EXCEPT { return static_cast<u32>(mShapes.size()); }
675
677 static const Shape emptyShape = [] {
678 Shape shape;
679 shape.type = Shape::EL_PANEL;
680 return shape;
681 }();
682 return index < mShapes.size() ? mShapes[index] : emptyShape;
683}
684
686 static const vec2f s_empty = vec2f(0, 0); // okay static in header
687 return s_empty;
688}
689
691 if (x >= length || !mLookUpTable) {
692 return empty(); // better than crashing.
693 }
694 LUTXYFLOAT &lut = *mLookUpTable.get();
695 return lut[x];
696}
697
699 if (x >= length || !mLookUpTable) {
700 return const_cast<vec2f &>(empty()); // better than crashing.
701 }
702 LUTXYFLOAT &lut = *mLookUpTable.get();
703 auto *data = lut.getDataMutable();
704 return data[x];
705}
706
708 if (this != &other) {
709 mDiameter = other.mDiameter;
710 length = other.length;
711 mLookUpTable = other.mLookUpTable;
712 mSourceXYMap = other.mSourceXYMap;
713 mShapes = other.mShapes;
714 }
715 return *this;
716}
717
719 if (this != &other) {
720 mDiameter = other.mDiameter;
721 length = other.length;
722 mLookUpTable = fl::move(other.mLookUpTable);
723 mSourceXYMap = fl::move(other.mSourceXYMap);
724 mShapes = fl::move(other.mShapes);
725 other.length = 0;
726 other.mDiameter = -1.0f;
727 }
728 return *this;
729}
730
734
738
740 return mSourceXYMap.get();
741}
742
744 return mSourceXYMap != nullptr;
745}
746
748 vec2f *data = mLookUpTable->getDataMutable();
749 for (u32 i = 0; i < length; i++) {
750 vec2f &curr = data[i];
751 curr.x += p.x;
752 curr.y += p.y;
753 }
754}
755
756ScreenMap& ScreenMap::addOffsetX(float x) FL_NO_EXCEPT { addOffset({x, 0}); return *this; }
757ScreenMap& ScreenMap::addOffsetY(float y) FL_NO_EXCEPT { addOffset({0, y}); return *this; }
758
759} // namespace fl
XYMap xymap
fl::ScreenMap screenmap
T * getDataMutable() FL_NO_EXCEPT
Definition lut.h:53
void addOffset(const vec2f &p) FL_NO_EXCEPT
float mDiameter
Definition screenmap.h:142
static void toJsonStr(const fl::flat_map< string, ScreenMap > &, string *jsonBuffer) FL_NO_EXCEPT
static ScreenMap DefaultStrip(int numLeds, float cm_between_leds=1.5f, float cm_led_diameter=0.2f, float completion=.9f) FL_NO_EXCEPT
static ScreenMap Circle(int numLeds, float cm_between_leds=1.5f, float cm_led_diameter=0.5f, float completion=1.0f) FL_NO_EXCEPT
bool hasShapes() const FL_NO_EXCEPT
void setSourceXYMap(const fl::shared_ptr< XYMap > &xymap) FL_NO_EXCEPT
Set the source XYMap (used for pixel transformation during encoding)
const XYMap * getXYMap() const FL_NO_EXCEPT
Get the source XYMap as a raw const pointer.
ScreenMap & addOffsetY(float y) FL_NO_EXCEPT
ScreenMap & addOffsetX(float x) FL_NO_EXCEPT
fl::vector< Shape > mShapes
Definition screenmap.h:145
ScreenMap() FL_NO_EXCEPT
const vec2f & operator[](u32 x) const FL_NO_EXCEPT
void setShape(u32 index, Shape::Type type, const vec2f *vertices, u32 count, float thickness=0.0f) FL_NO_EXCEPT
static bool ParseJson(const char *jsonStrScreenMap, fl::flat_map< string, ScreenMap > *segmentMaps, string *err=nullptr) FL_NO_EXCEPT
ScreenMap & operator=(const ScreenMap &other) FL_NO_EXCEPT
float getDiameter() const FL_NO_EXCEPT
LUTXYFLOATPtr mLookUpTable
Definition screenmap.h:143
vec2f mapToIndex(u32 x) const FL_NO_EXCEPT
static const vec2f & empty() FL_NO_EXCEPT
u32 getShapeCount() const FL_NO_EXCEPT
void setDiameter(float diameter) FL_NO_EXCEPT
bool hasSourceXYMap() const FL_NO_EXCEPT
Check if source XYMap is available.
void set(u16 index, const vec2f &p) FL_NO_EXCEPT
u32 getLength() const FL_NO_EXCEPT
const Shape & getShape(u32 index) const FL_NO_EXCEPT
XYMapPtr mSourceXYMap
Definition screenmap.h:144
vec2f getBounds() const FL_NO_EXCEPT
static void toJson(const fl::flat_map< string, ScreenMap > &, fl::json *doc) FL_NO_EXCEPT
const XYMapPtr & getSourceXYMapPtr() const FL_NO_EXCEPT
Get the source XYMap shared pointer if available.
bool contains(const Key &key) const FL_NO_EXCEPT
Definition flat_map.h:176
size_type size() const FL_NO_EXCEPT
Definition flat_map.h:98
bool is_object() const FL_NO_EXCEPT
Definition json.h:391
fl::string to_string() const FL_NO_EXCEPT
Definition json.h:836
fl::optional< fl::string > as_string() const FL_NO_EXCEPT
Definition json.h:443
void set(const fl::string &key, const json &value) FL_NO_EXCEPT
Definition json.h:866
bool has_value() const FL_NO_EXCEPT
Definition json.h:819
fl::optional< float > as_float() const FL_NO_EXCEPT
Definition json.h:409
bool is_array() const FL_NO_EXCEPT
Definition json.h:389
void push_back(const json &value) FL_NO_EXCEPT
Definition json.h:918
bool contains(size_t idx) const FL_NO_EXCEPT
Definition json.h:790
static json parse(const fl::string &txt) FL_NO_EXCEPT
Definition json.h:842
static json object() FL_NO_EXCEPT
Definition json.h:857
static json array() FL_NO_EXCEPT
Definition json.h:853
string & append(const bitset_fixed< N > &bs) FL_NO_EXCEPT
Definition string.h:293
fl::size size() const FL_NO_EXCEPT
void reserve(fl::size n) FL_NO_EXCEPT
Definition vector.h:591
T * data() FL_NO_EXCEPT
Definition vector.h:619
void push_back(const T &value) FL_NO_EXCEPT
Definition vector.h:624
#define FL_UNUSED(x)
fl::UISlider length("Length", 1.0f, 0.0f, 1.0f, 0.01f)
fl::ScreenMap screenMap
Definition Corkscrew.h:101
FastLED's Elegant JSON Library: fl::json
#define FL_WARN(...)
Definition log.h:437
Centralized logging categories for FastLED hardware interfaces and subsystems.
#define FL_PI
Definition math.h:26
constexpr remove_reference< T >::type && move(T &&t) FL_NO_EXCEPT
Definition move.h:30
shared_ptr< T > make_shared(Args &&... args) FL_NO_EXCEPT
Definition shared_ptr.h:423
float normalizedDiameter(float diameter) FL_NO_EXCEPT
constexpr common_type_t< T, U > max(T a, U b) FL_NO_EXCEPT
Definition math.h:75
enable_if< is_fixed_point< T >::value, T >::type sin(T angle) FL_NO_EXCEPT
void swap(array< T, N > &lhs, array< T, N > &rhs) FL_NO_EXCEPT
Definition array.h:209
u8 u8 fract8 blur_amount FL_NO_EXCEPT
Definition blur.h:186
constexpr int type_rank< T >::value
enable_if< is_fixed_point< T >::value, T >::type cos(T angle) FL_NO_EXCEPT
FL_DISABLE_WARNING_PUSH U constexpr common_type_t< T, U > min(T a, U b) FL_NO_EXCEPT
Definition math.h:71
vec2< float > vec2f
Definition geometry.h:333
shared_ptr< XYMap > XYMapPtr
Definition screenmap.h:34
fl::vector< float > jsonArrayToFloatVector(const fl::json &jsonArray) FL_NO_EXCEPT
expected< T, E > result
Alias for expected (Rust-style naming)
Definition result.h:31
LUT< vec2f > LUTXYFLOAT
Definition lut.h:26
FL_STATIC_ASSERT(static_cast< fl::u8 >(Bus::FLEX_IO)==8, "Bus changed: update busName() and busDriverName()")
static bool parseV2SegmentArray(const fl::json &segmentsArr, fl::flat_map< string, ScreenMap > *segmentMaps, string *err) FL_NO_EXCEPT
Base definition for an LED controller.
Definition crgb.hpp:179
#define FL_NO_EXCEPT
Portable compile-time assertion wrapper.
fl::i64 i64
Definition stdint.h:221
enum fl::ScreenMap::Shape::Type type
fl::vector< vec2f > vertices
Definition screenmap.h:43
value_type y
Definition geometry.h:191
value_type x
Definition geometry.h:190