device_envoy_core/led2d.rs
1#![cfg_attr(
2 feature = "doc-images",
3 doc = ::embed_doc_image::embed_image!("led2d1", "docs/assets/led2d1.png"),
4 doc = ::embed_doc_image::embed_image!("led2d2", "docs/assets/led2d2.png")
5)]
6//! Shared 2D LED panel building blocks used across all device-envoy platforms.
7//!
8//! This module provides platform-independent types for NeoPixel-style (WS2812) LED panel
9//! displays. See the platform crate (`device-envoy-rp` or `device-envoy-esp`) for the
10//! primary documentation and examples.
11//!
12//! [led2d1]: https://raw.githubusercontent.com/CarlKCarlK/device-envoy/main/crates/device-envoy-core/docs/assets/led2d1.png
13//! [led2d2]: https://raw.githubusercontent.com/CarlKCarlK/device-envoy/main/crates/device-envoy-core/docs/assets/led2d2.png
14
15pub mod layout;
16
17pub use embedded_graphics::geometry::Point;
18pub use embedded_graphics::geometry::Size;
19pub use layout::LedLayout;
20
21use core::{
22 borrow::Borrow,
23 convert::Infallible,
24 ops::{Deref, DerefMut, Index, IndexMut},
25};
26use embedded_graphics::pixelcolor::Rgb888;
27use embedded_graphics::{
28 draw_target::DrawTarget,
29 mono_font::{
30 DecorationDimensions, MonoFont,
31 ascii::{
32 FONT_4X6, FONT_5X7, FONT_5X8, FONT_6X9, FONT_6X10, FONT_6X12, FONT_6X13,
33 FONT_6X13_BOLD, FONT_6X13_ITALIC, FONT_7X13, FONT_7X13_BOLD, FONT_7X13_ITALIC,
34 FONT_7X14, FONT_7X14_BOLD, FONT_8X13, FONT_8X13_BOLD, FONT_8X13_ITALIC, FONT_9X15,
35 FONT_9X15_BOLD, FONT_9X18, FONT_9X18_BOLD, FONT_10X20,
36 },
37 mapping::StrGlyphMapping,
38 },
39 prelude::*,
40};
41use smart_leds::RGB8;
42
43use crate::led_strip::ToRgb888;
44use crate::led_strip::{Frame1d as StripFrame, LedStrip as LedStripTrait};
45
46/// Platform-agnostic LED panel device contract.
47///
48/// Platform crates implement this for their concrete LED panel types so shared logic can
49/// drive LED panels without knowing the underlying hardware backend.
50///
51/// This page serves as the definitive reference for what a generated LED panel type
52/// provides. For first-time readers, start with the `led2d` module documentation in your
53/// platform crate (`device-envoy-rp` or `device-envoy-esp`), then return here for a
54/// complete list of available methods and associated constants.
55///
56/// Design intent:
57///
58/// - Primitive operations are [`Led2d::write_frame`] and [`Led2d::animate`].
59/// - Convenience text operations ([`Led2d::write_text_to_frame`] and [`Led2d::write_text`])
60/// are default methods derived from primitives and associated constants.
61/// - This trait is intended for static dispatch on embedded targets.
62///
63/// The trait takes `const W` and `const H` so dimensions remain compile-time constants and
64/// can be used in frame types like [`Frame2d<W, H>`].
65///
66/// # Example: Write Text
67///
68/// In this example, we render text on a 12x4 panel.
69///
70/// 
71///
72/// ```rust,no_run
73/// use device_envoy_core::led2d::Led2d;
74/// use smart_leds::RGB8;
75///
76/// fn write_rust<const W: usize, const H: usize>(led2d: &impl Led2d<W, H>) {
77/// let colors = [
78/// RGB8::new(0, 255, 255),
79/// RGB8::new(255, 0, 0),
80/// RGB8::new(255, 255, 0),
81/// ];
82/// led2d.write_text("Rust", &colors);
83/// }
84///
85/// # use device_envoy_core::led2d::{Frame2d, Led2dFont};
86/// # struct Led12x4;
87/// # impl Led2d<12, 4> for Led12x4 {
88/// # const MAX_FRAMES: usize = 2;
89/// # const MAX_BRIGHTNESS: u8 = 22;
90/// # const FONT: Led2dFont = Led2dFont::Font3x4Trim;
91/// # fn write_frame(&self, _frame2d: Frame2d<12, 4>) {}
92/// # fn animate<I>(&self, _frames: I)
93/// # where
94/// # I: IntoIterator,
95/// # I::Item: core::borrow::Borrow<(Frame2d<12, 4>, embassy_time::Duration)>,
96/// # {
97/// # }
98/// # }
99/// # let led12x4 = Led12x4;
100/// # write_rust(&led12x4);
101/// ```
102///
103/// # Example: Animated Text
104///
105/// This example animates text on an LED panel.
106///
107/// 
108///
109/// ```rust,no_run
110/// use device_envoy_core::led2d::{Frame2d, Led2d};
111/// use smart_leds::colors;
112///
113/// fn animate_go_go<const W: usize, const H: usize>(led2d: &impl Led2d<W, H>) {
114/// let mut frame_0 = Frame2d::new();
115/// led2d.write_text_to_frame("Go", &[], &mut frame_0);
116///
117/// let mut frame_1 = Frame2d::new();
118/// led2d.write_text_to_frame("\nGo", &[colors::HOT_PINK, colors::LIME], &mut frame_1);
119///
120/// let frame_duration = embassy_time::Duration::from_secs(1);
121/// led2d.animate([(frame_0, frame_duration), (frame_1, frame_duration)]);
122/// }
123///
124/// # use device_envoy_core::led2d::Led2dFont;
125/// # struct Led8x12;
126/// # impl Led2d<8, 12> for Led8x12 {
127/// # const MAX_FRAMES: usize = 2;
128/// # const MAX_BRIGHTNESS: u8 = 22;
129/// # const FONT: Led2dFont = Led2dFont::Font4x6Trim;
130/// # fn write_frame(&self, _frame2d: Frame2d<8, 12>) {}
131/// # fn animate<I>(&self, _frames: I)
132/// # where
133/// # I: IntoIterator,
134/// # I::Item: core::borrow::Borrow<(Frame2d<8, 12>, embassy_time::Duration)>,
135/// # {
136/// # }
137/// # }
138/// # let led8x12 = Led8x12;
139/// # animate_go_go(&led8x12);
140/// ```
141pub trait Led2d<const W: usize, const H: usize> {
142 /// The width of the panel.
143 const WIDTH: usize = W;
144 /// The height of the panel.
145 const HEIGHT: usize = H;
146 /// Total LEDs in this panel (width × height).
147 const LEN: usize = W * H;
148 /// Panel dimensions as a [`Size`].
149 ///
150 /// For [`embedded-graphics`](https://docs.rs/embedded-graphics) drawing operations.
151 const SIZE: Size = Frame2d::<W, H>::SIZE;
152 /// Top-left corner coordinate as a [`Point`].
153 ///
154 /// For [`embedded-graphics`](https://docs.rs/embedded-graphics) drawing operations.
155 const TOP_LEFT: Point = Frame2d::<W, H>::TOP_LEFT;
156 /// Top-right corner coordinate as a [`Point`].
157 ///
158 /// For [`embedded-graphics`](https://docs.rs/embedded-graphics) drawing operations.
159 const TOP_RIGHT: Point = Frame2d::<W, H>::TOP_RIGHT;
160 /// Bottom-left corner coordinate as a [`Point`].
161 ///
162 /// For [`embedded-graphics`](https://docs.rs/embedded-graphics) drawing operations.
163 const BOTTOM_LEFT: Point = Frame2d::<W, H>::BOTTOM_LEFT;
164 /// Bottom-right corner coordinate as a [`Point`].
165 ///
166 /// For [`embedded-graphics`](https://docs.rs/embedded-graphics) drawing operations.
167 const BOTTOM_RIGHT: Point = Frame2d::<W, H>::BOTTOM_RIGHT;
168 /// Maximum number of animation frames allowed.
169 ///
170 /// Usually configured by the platform macro (for example `led2d!`).
171 const MAX_FRAMES: usize;
172 /// Maximum brightness level, automatically limited by the power budget.
173 ///
174 /// Many implementations assume each LED draws about 60 mA at full brightness and compute
175 /// a safe cap from power budget and LED count.
176 const MAX_BRIGHTNESS: u8;
177 /// The font used by default text helpers.
178 ///
179 /// Used by [`Led2d::write_text_to_frame`] and [`Led2d::write_text`].
180 const FONT: Led2dFont;
181
182 /// Write a frame to the LED panel.
183 ///
184 /// See your platform crate's led2d module docs for possible usage examples.
185 fn write_frame(&self, frame2d: Frame2d<W, H>);
186
187 /// Animate frames on the LED panel.
188 ///
189 /// The duration type is [`embassy_time::Duration`](https://docs.rs/embassy-time/latest/embassy_time/struct.Duration.html), and `frames` can be any iterator whose
190 /// items borrow `(Frame2d<W, H>, embassy_time::Duration)`.
191 ///
192 /// See the [Led2d trait documentation](Self) for usage examples.
193 fn animate<I>(&self, frames: I)
194 where
195 I: IntoIterator,
196 I::Item: Borrow<(Frame2d<W, H>, embassy_time::Duration)>;
197
198 /// Write text into a frame.
199 ///
200 /// This is a default helper built on [`render_text_to_frame`] plus associated constants.
201 ///
202 /// Behavior:
203 ///
204 /// - Text is drawn with [`Led2d::FONT`].
205 /// - `colors` cycles one color per character; an empty slice defaults to white.
206 /// - A `\n` character starts a new line.
207 /// - Characters beyond frame width are clipped.
208 ///
209 /// See the [Led2d trait documentation](Self) for usage examples.
210 fn write_text_to_frame(&self, text: &str, colors: &[RGB8], frame: &mut Frame2d<W, H>) {
211 render_text_to_frame(
212 frame,
213 &Self::FONT.to_font(),
214 text,
215 colors,
216 Self::FONT.spacing_reduction(),
217 );
218 }
219
220 /// Write text to the LED panel.
221 ///
222 /// This default helper is equivalent to:
223 ///
224 /// 1. Create `Frame2d::<W, H>::new()`.
225 /// 2. Call [`Led2d::write_text_to_frame`].
226 /// 3. Call [`Led2d::write_frame`].
227 ///
228 /// See the [Led2d trait documentation](Self) for usage examples.
229 fn write_text(&self, text: &str, colors: &[RGB8]) {
230 let mut frame = Frame2d::<W, H>::new();
231 self.write_text_to_frame(text, colors, &mut frame);
232 self.write_frame(frame);
233 }
234}
235
236/// Extension trait for strip-backed [`Led2d`] implementations.
237///
238/// This keeps the base [`Led2d`] trait backend-agnostic while still providing
239/// reusable default behavior for implementations that render via a [`crate::led_strip::LedStrip`].
240#[doc(hidden)] // Platform plumbing trait used by RP/ESP generated led2d wrappers.
241pub trait Led2dStripBacked<const N: usize> {
242 /// Concrete strip type used by this panel implementation.
243 type Strip: LedStripTrait<N> + ?Sized;
244
245 /// Return the underlying strip handle.
246 fn led_strip(&self) -> &Self::Strip;
247
248 /// Return the `(x, y) -> strip_index` mapping table.
249 fn mapping_by_xy(&self) -> &[u16; N];
250
251 /// Return the panel width used by [`Led2dStripBacked::xy_to_index`].
252 fn width(&self) -> usize;
253
254 /// Convert `(column, row)` to strip index using [`Led2dStripBacked::mapping_by_xy`].
255 #[must_use]
256 fn xy_to_index(&self, x_index: usize, y_index: usize) -> usize {
257 self.mapping_by_xy()[y_index * self.width() + x_index] as usize
258 }
259
260 /// Convert a 2D frame into a strip frame using the stored mapping.
261 fn convert_frame<const W: usize, const H: usize>(
262 &self,
263 frame_2d: Frame2d<W, H>,
264 ) -> StripFrame<N> {
265 let mut frame_1d = [RGB8::new(0, 0, 0); N];
266 for y_index in 0..H {
267 for x_index in 0..W {
268 let led_index = self.xy_to_index(x_index, y_index);
269 frame_1d[led_index] = frame_2d[(x_index, y_index)];
270 }
271 }
272 StripFrame::from(frame_1d)
273 }
274
275 /// Write a panel frame through the associated strip backend.
276 fn write_frame<const W: usize, const H: usize>(&self, frame: Frame2d<W, H>) {
277 let strip_frame = self.convert_frame(frame);
278 self.led_strip().write_frame(strip_frame);
279 }
280
281 /// Animate panel frames through the associated strip backend.
282 fn animate<const W: usize, const H: usize, I>(&self, frames: I)
283 where
284 I: IntoIterator,
285 I::Item: Borrow<(Frame2d<W, H>, embassy_time::Duration)>,
286 {
287 self.led_strip().animate(frames.into_iter().map(|frame| {
288 let (frame, duration) = *frame.borrow();
289 (self.convert_frame(frame), duration)
290 }));
291 }
292}
293
294/// Shared adapter that maps [`Frame2d`] panels onto a 1D LED strip device.
295///
296/// Platform crates can use this to build their `led2d` wrappers while keeping
297/// mapping and frame-conversion logic in `device-envoy-core`.
298#[doc(hidden)] // Platform plumbing adapter used by RP/ESP implementations.
299pub struct Led2dStripAdapter<'a, const N: usize, S>
300where
301 S: LedStripTrait<N> + ?Sized,
302{
303 led_strip: &'a S,
304 mapping_by_xy: [u16; N],
305 width: usize,
306}
307
308impl<'a, const N: usize, S> Led2dStripAdapter<'a, N, S>
309where
310 S: LedStripTrait<N> + ?Sized,
311{
312 /// Create a strip-backed LED panel adapter from a strip and panel layout.
313 #[must_use]
314 pub fn new<const W: usize, const H: usize>(
315 led_strip: &'a S,
316 led_layout: &LedLayout<N, W, H>,
317 ) -> Self {
318 assert_eq!(
319 W.checked_mul(H).expect("width * height must fit in usize"),
320 N,
321 "width * height must equal N"
322 );
323 Self {
324 led_strip,
325 mapping_by_xy: led_layout.xy_to_index(),
326 width: W,
327 }
328 }
329}
330
331impl<'a, const N: usize, S> Led2dStripBacked<N> for Led2dStripAdapter<'a, N, S>
332where
333 S: LedStripTrait<N> + ?Sized,
334{
335 type Strip = S;
336
337 fn led_strip(&self) -> &Self::Strip {
338 self.led_strip
339 }
340
341 fn mapping_by_xy(&self) -> &[u16; N] {
342 &self.mapping_by_xy
343 }
344
345 fn width(&self) -> usize {
346 self.width
347 }
348}
349
350// Packed bitmap for the internal 3x4 font (ASCII 0x20-0x7E).
351const BIT_MATRIX3X4_FONT_DATA: [u8; 144] = [
352 0x0a, 0xd5, 0x10, 0x4a, 0xa0, 0x01, 0x0a, 0xfe, 0x68, 0x85, 0x70, 0x02, 0x08, 0x74, 0x90, 0x86,
353 0xa5, 0xc4, 0x08, 0x5e, 0x68, 0x48, 0x08, 0x10, 0xeb, 0x7b, 0xe7, 0xfd, 0x22, 0x27, 0xb8, 0x9b,
354 0x39, 0xb4, 0x05, 0xd1, 0xa9, 0x3e, 0xea, 0x5d, 0x28, 0x0a, 0xff, 0xf3, 0xfc, 0xe4, 0x45, 0xd2,
355 0xff, 0x7d, 0xff, 0xbc, 0xd9, 0xff, 0xb7, 0xcb, 0xb4, 0xe8, 0xe9, 0xfd, 0xfe, 0xcb, 0x25, 0xaa,
356 0xd9, 0x7d, 0x97, 0x7d, 0xe7, 0xbf, 0xdf, 0x6f, 0xdf, 0x7f, 0x6d, 0xb7, 0xe0, 0xd0, 0xf7, 0xe5,
357 0x6d, 0x48, 0xc0, 0x68, 0xdf, 0x35, 0x6f, 0x49, 0x40, 0x40, 0x86, 0xf5, 0xd7, 0xab, 0xe0, 0xc7,
358 0x5f, 0x7d, 0xff, 0xbc, 0xd9, 0xff, 0x37, 0xcb, 0xb4, 0xe8, 0xe9, 0xfd, 0x1e, 0xcb, 0x25, 0xaa,
359 0xd9, 0x7d, 0x17, 0x7d, 0xe7, 0xbf, 0xdf, 0x6f, 0xdf, 0x7f, 0x6d, 0xb7, 0xb1, 0x80, 0xf7, 0xe5,
360 0x6d, 0x48, 0xa0, 0xa8, 0xdf, 0x35, 0x6f, 0x49, 0x20, 0x90, 0x86, 0xf5, 0xd7, 0xab, 0xb1, 0x80,
361];
362const BIT_MATRIX3X4_IMAGE_WIDTH: u32 = 48;
363const BIT_MATRIX3X4_GLYPH_MAPPING: StrGlyphMapping<'static> = StrGlyphMapping::new("\0 \u{7e}", 0);
364
365/// Monospace 3x4 font matching the internal `BIT_MATRIX3X4` bitmap data.
366#[must_use]
367pub fn bit_matrix3x4_font() -> MonoFont<'static> {
368 MonoFont {
369 image: embedded_graphics::image::ImageRaw::new(
370 &BIT_MATRIX3X4_FONT_DATA,
371 BIT_MATRIX3X4_IMAGE_WIDTH,
372 ),
373 glyph_mapping: &BIT_MATRIX3X4_GLYPH_MAPPING,
374 character_size: embedded_graphics::prelude::Size::new(3, 4),
375 character_spacing: 0,
376 baseline: 3,
377 underline: DecorationDimensions::new(3, 1),
378 strikethrough: DecorationDimensions::new(2, 1),
379 }
380}
381
382/// Render text into a frame using the provided font.
383///
384/// Text flows left-to-right within the frame width; a `\n` character advances to the next row.
385/// Characters that exceed the frame width are skipped (no wrapping). Colors cycle over the
386/// `colors` slice (one color per character); an empty slice defaults to white.
387///
388/// `spacing_reduction` is a `(width_reduction, height_reduction)` pair in pixels used by the
389/// trimmed [`Led2dFont`] variants to pack characters more tightly.
390pub fn render_text_to_frame<const W: usize, const H: usize>(
391 frame: &mut Frame2d<W, H>,
392 font: &embedded_graphics::mono_font::MonoFont<'static>,
393 text: &str,
394 colors: &[RGB8],
395 spacing_reduction: (i32, i32),
396) {
397 let glyph_width = font.character_size.width as i32;
398 let glyph_height = font.character_size.height as i32;
399 let advance_x = glyph_width - spacing_reduction.0;
400 let advance_y = glyph_height - spacing_reduction.1;
401 let width_limit = W as i32;
402 let height_limit = H as i32;
403 if height_limit <= 0 || width_limit <= 0 {
404 return;
405 }
406 let baseline = font.baseline as i32;
407 let mut x = 0i32;
408 let mut y = baseline;
409 let mut color_index: usize = 0;
410
411 for ch in text.chars() {
412 if ch == '\n' {
413 x = 0;
414 y += advance_y;
415 if y - baseline >= height_limit {
416 break;
417 }
418 continue;
419 }
420
421 // Clip characters that exceed width limit (no wrapping until explicit \n).
422 if x + advance_x > width_limit {
423 continue;
424 }
425
426 let color = if colors.is_empty() {
427 smart_leds::colors::WHITE
428 } else {
429 colors[color_index % colors.len()]
430 };
431 color_index = color_index.wrapping_add(1);
432
433 let mut buf = [0u8; 4];
434 let slice = ch.encode_utf8(&mut buf);
435 let style = embedded_graphics::mono_font::MonoTextStyle::new(font, color.to_rgb888());
436 let position = embedded_graphics::prelude::Point::new(x, y);
437 embedded_graphics::Drawable::draw(
438 &embedded_graphics::text::Text::new(slice, position, style),
439 frame,
440 )
441 .expect("drawing into frame cannot fail");
442
443 x += advance_x;
444 }
445}
446
447/// Fonts available for use with LED panel displays.
448///
449/// Fonts with `Trim` suffix remove blank spacing to pack text more tightly on small displays.
450#[derive(Clone, Copy, Debug)]
451pub enum Led2dFont {
452 /// 3x4 monospace font, trimmed (compact layout).
453 Font3x4Trim,
454 /// 4x6 monospace font.
455 Font4x6,
456 /// 3x5 monospace font, trimmed (compact layout).
457 Font3x5Trim,
458 /// 5x7 monospace font.
459 Font5x7,
460 /// 4x6 monospace font, trimmed (compact layout).
461 Font4x6Trim,
462 /// 5x8 monospace font.
463 Font5x8,
464 /// 4x7 monospace font, trimmed (compact layout).
465 Font4x7Trim,
466 /// 6x9 monospace font.
467 Font6x9,
468 /// 5x8 monospace font, trimmed (compact layout).
469 Font5x8Trim,
470 /// 6x10 monospace font.
471 Font6x10,
472 /// 5x9 monospace font, trimmed (compact layout).
473 Font5x9Trim,
474 /// 6x12 monospace font.
475 Font6x12,
476 /// 5x11 monospace font, trimmed (compact layout).
477 Font5x11Trim,
478 /// 6x13 monospace font.
479 Font6x13,
480 /// 5x12 monospace font, trimmed (compact layout).
481 Font5x12Trim,
482 /// 6x13 bold monospace font.
483 Font6x13Bold,
484 /// 5x12 bold monospace font, trimmed (compact layout).
485 Font5x12TrimBold,
486 /// 6x13 italic monospace font.
487 Font6x13Italic,
488 /// 5x12 italic monospace font, trimmed (compact layout).
489 Font5x12TrimItalic,
490 /// 7x13 monospace font.
491 Font7x13,
492 /// 6x12 monospace font, trimmed (compact layout).
493 Font6x12Trim,
494 /// 7x13 bold monospace font.
495 Font7x13Bold,
496 /// 6x12 bold monospace font, trimmed (compact layout).
497 Font6x12TrimBold,
498 /// 7x13 italic monospace font.
499 Font7x13Italic,
500 /// 6x12 italic monospace font, trimmed (compact layout).
501 Font6x12TrimItalic,
502 /// 7x14 monospace font.
503 Font7x14,
504 /// 6x13 monospace font, trimmed (compact layout).
505 Font6x13Trim,
506 /// 7x14 bold monospace font.
507 Font7x14Bold,
508 /// 6x13 bold monospace font, trimmed (compact layout).
509 Font6x13TrimBold,
510 /// 8x13 monospace font.
511 Font8x13,
512 /// 7x12 monospace font, trimmed (compact layout).
513 Font7x12Trim,
514 /// 8x13 bold monospace font.
515 Font8x13Bold,
516 /// 7x12 bold monospace font, trimmed (compact layout).
517 Font7x12TrimBold,
518 /// 8x13 italic monospace font.
519 Font8x13Italic,
520 /// 7x12 italic monospace font, trimmed (compact layout).
521 Font7x12TrimItalic,
522 /// 9x15 monospace font.
523 Font9x15,
524 /// 8x14 monospace font, trimmed (compact layout).
525 Font8x14Trim,
526 /// 9x15 bold monospace font.
527 Font9x15Bold,
528 /// 8x14 bold monospace font, trimmed (compact layout).
529 Font8x14TrimBold,
530 /// 9x18 monospace font.
531 Font9x18,
532 /// 8x17 monospace font, trimmed (compact layout).
533 Font8x17Trim,
534 /// 9x18 bold monospace font.
535 Font9x18Bold,
536 /// 8x17 bold monospace font, trimmed (compact layout).
537 Font8x17TrimBold,
538 /// 10x20 monospace font.
539 Font10x20,
540 /// 9x19 monospace font, trimmed (compact layout).
541 Font9x19Trim,
542}
543
544impl Led2dFont {
545 /// Return the `MonoFont` for this variant.
546 #[must_use]
547 pub fn to_font(self) -> MonoFont<'static> {
548 match self {
549 Self::Font3x4Trim => bit_matrix3x4_font(),
550 Self::Font4x6 | Self::Font3x5Trim => FONT_4X6,
551 Self::Font5x7 | Self::Font4x6Trim => FONT_5X7,
552 Self::Font5x8 | Self::Font4x7Trim => FONT_5X8,
553 Self::Font6x9 | Self::Font5x8Trim => FONT_6X9,
554 Self::Font6x10 | Self::Font5x9Trim => FONT_6X10,
555 Self::Font6x12 | Self::Font5x11Trim => FONT_6X12,
556 Self::Font6x13 | Self::Font5x12Trim => FONT_6X13,
557 Self::Font6x13Bold | Self::Font5x12TrimBold => FONT_6X13_BOLD,
558 Self::Font6x13Italic | Self::Font5x12TrimItalic => FONT_6X13_ITALIC,
559 Self::Font7x13 | Self::Font6x12Trim => FONT_7X13,
560 Self::Font7x13Bold | Self::Font6x12TrimBold => FONT_7X13_BOLD,
561 Self::Font7x13Italic | Self::Font6x12TrimItalic => FONT_7X13_ITALIC,
562 Self::Font7x14 | Self::Font6x13Trim => FONT_7X14,
563 Self::Font7x14Bold | Self::Font6x13TrimBold => FONT_7X14_BOLD,
564 Self::Font8x13 | Self::Font7x12Trim => FONT_8X13,
565 Self::Font8x13Bold | Self::Font7x12TrimBold => FONT_8X13_BOLD,
566 Self::Font8x13Italic | Self::Font7x12TrimItalic => FONT_8X13_ITALIC,
567 Self::Font9x15 | Self::Font8x14Trim => FONT_9X15,
568 Self::Font9x15Bold | Self::Font8x14TrimBold => FONT_9X15_BOLD,
569 Self::Font9x18 | Self::Font8x17Trim => FONT_9X18,
570 Self::Font9x18Bold | Self::Font8x17TrimBold => FONT_9X18_BOLD,
571 Self::Font10x20 | Self::Font9x19Trim => FONT_10X20,
572 }
573 }
574
575 /// Return spacing reduction for trimmed variants as `(width_reduction, height_reduction)`.
576 #[must_use]
577 pub const fn spacing_reduction(self) -> (i32, i32) {
578 match self {
579 Self::Font3x4Trim
580 | Self::Font4x6
581 | Self::Font5x7
582 | Self::Font5x8
583 | Self::Font6x9
584 | Self::Font6x10
585 | Self::Font6x12
586 | Self::Font6x13
587 | Self::Font6x13Bold
588 | Self::Font6x13Italic
589 | Self::Font7x13
590 | Self::Font7x13Bold
591 | Self::Font7x13Italic
592 | Self::Font7x14
593 | Self::Font7x14Bold
594 | Self::Font8x13
595 | Self::Font8x13Bold
596 | Self::Font8x13Italic
597 | Self::Font9x15
598 | Self::Font9x15Bold
599 | Self::Font9x18
600 | Self::Font9x18Bold
601 | Self::Font10x20 => (0, 0),
602 Self::Font3x5Trim
603 | Self::Font4x6Trim
604 | Self::Font4x7Trim
605 | Self::Font5x8Trim
606 | Self::Font5x9Trim
607 | Self::Font5x11Trim
608 | Self::Font5x12Trim
609 | Self::Font5x12TrimBold
610 | Self::Font5x12TrimItalic
611 | Self::Font6x12Trim
612 | Self::Font6x12TrimBold
613 | Self::Font6x12TrimItalic
614 | Self::Font6x13Trim
615 | Self::Font6x13TrimBold
616 | Self::Font7x12Trim
617 | Self::Font7x12TrimBold
618 | Self::Font7x12TrimItalic
619 | Self::Font8x14Trim
620 | Self::Font8x14TrimBold
621 | Self::Font8x17Trim
622 | Self::Font8x17TrimBold
623 | Self::Font9x19Trim => (1, 1),
624 }
625 }
626}
627
628/// 2D pixel array used for general graphics on LED panels.
629///
630/// - Coordinates are `(x, y)` with `(0, 0)` at the top-left. The x-axis increases to the
631/// right, and the y-axis increases downward.
632/// - Set pixels using tuple indexing: `frame[(x, y)] = colors::RED;`.
633/// - For shapes, lines, and text rendering, use the [`embedded-graphics`](https://docs.rs/embedded-graphics) crate.
634///
635/// ## Indexing and storage
636///
637/// `Frame2d` supports both:
638///
639/// - `(x, y)` tuple indexing: `frame[(x, y)]`
640/// - Row-major array indexing: `frame[y][x]`
641///
642/// Tuple indexing matches display coordinates. Array indexing matches the underlying storage.
643///
644/// # Example: Draw pixels both directly and with [`embedded-graphics`](https://docs.rs/embedded-graphics)
645///
646/// ```rust,no_run
647/// use device_envoy_core::{led2d::Frame2d, led_strip::ToRgb888};
648/// use embedded_graphics::{
649/// prelude::*,
650/// primitives::{Circle, PrimitiveStyle, Rectangle},
651/// };
652/// use smart_leds::colors;
653/// # use core::convert::Infallible;
654/// # fn example() -> Result<(), Infallible> {
655///
656/// type Frame = Frame2d<12, 8>;
657///
658/// /// Calculate the top-left corner position to center a shape within a bounding box.
659/// const fn centered_top_left(width: usize, height: usize, size: usize) -> Point {
660/// assert!(size <= width);
661/// assert!(size <= height);
662/// Point::new(((width - size) / 2) as i32, ((height - size) / 2) as i32)
663/// }
664///
665/// // Create a frame to draw on. This is just an in-memory 2D pixel buffer.
666/// let mut frame = Frame::new();
667///
668/// // Use the embedded-graphics crate to draw a red rectangle border around the edge of the frame.
669/// // We use `to_rgb888()` to convert from smart-leds RGB8 to embedded-graphics Rgb888.
670/// Rectangle::new(Frame::TOP_LEFT, Frame::SIZE)
671/// .into_styled(PrimitiveStyle::with_stroke(colors::RED.to_rgb888(), 1))
672/// .draw(&mut frame)
673/// ?;
674///
675/// // Direct pixel access: set the upper-left LED pixel (x = 0, y = 0).
676/// // Frame2d stores LED colors directly, so we write an LED color here.
677/// frame[(0, 0)] = colors::CYAN;
678///
679/// // Use the embedded-graphics crate to draw a green circle centered in the frame.
680/// const DIAMETER: u32 = 6;
681/// const CIRCLE_TOP_LEFT: Point = centered_top_left(Frame::WIDTH, Frame::HEIGHT, DIAMETER as usize);
682/// Circle::new(CIRCLE_TOP_LEFT, DIAMETER)
683/// .into_styled(PrimitiveStyle::with_stroke(colors::LIME.to_rgb888(), 1))
684/// .draw(&mut frame)
685/// ?;
686/// # Ok(())
687/// # }
688/// ```
689#[derive(Clone, Copy, Debug)]
690pub struct Frame2d<const W: usize, const H: usize>(pub [[RGB8; W]; H]);
691
692impl<const W: usize, const H: usize> Frame2d<W, H> {
693 /// The width of the frame.
694 pub const WIDTH: usize = W;
695 /// The height of the frame.
696 pub const HEIGHT: usize = H;
697 /// Total pixels in this frame (width × height).
698 pub const LEN: usize = W * H;
699 /// Frame dimensions as a [`Size`].
700 ///
701 /// For [`embedded-graphics`](https://docs.rs/embedded-graphics) drawing operations.
702 pub const SIZE: Size = Size::new(W as u32, H as u32);
703 /// Top-left corner coordinate as a [`Point`].
704 ///
705 /// For [`embedded-graphics`](https://docs.rs/embedded-graphics) drawing operations.
706 pub const TOP_LEFT: Point = Point::new(0, 0);
707 /// Top-right corner coordinate as a [`Point`].
708 ///
709 /// For [`embedded-graphics`](https://docs.rs/embedded-graphics) drawing operations.
710 pub const TOP_RIGHT: Point = Point::new((W - 1) as i32, 0);
711 /// Bottom-left corner coordinate as a [`Point`].
712 ///
713 /// For [`embedded-graphics`](https://docs.rs/embedded-graphics) drawing operations.
714 pub const BOTTOM_LEFT: Point = Point::new(0, (H - 1) as i32);
715 /// Bottom-right corner coordinate as a [`Point`].
716 ///
717 /// For [`embedded-graphics`](https://docs.rs/embedded-graphics) drawing operations.
718 pub const BOTTOM_RIGHT: Point = Point::new((W - 1) as i32, (H - 1) as i32);
719
720 /// Create a new blank (all black) frame.
721 #[must_use]
722 pub const fn new() -> Self {
723 Self([[RGB8::new(0, 0, 0); W]; H])
724 }
725
726 /// Create a frame filled with a single color.
727 #[must_use]
728 pub const fn filled(color: RGB8) -> Self {
729 Self([[color; W]; H])
730 }
731}
732
733impl<const W: usize, const H: usize> Deref for Frame2d<W, H> {
734 type Target = [[RGB8; W]; H];
735
736 fn deref(&self) -> &Self::Target {
737 &self.0
738 }
739}
740
741impl<const W: usize, const H: usize> DerefMut for Frame2d<W, H> {
742 fn deref_mut(&mut self) -> &mut Self::Target {
743 &mut self.0
744 }
745}
746
747impl<const W: usize, const H: usize> Index<(usize, usize)> for Frame2d<W, H> {
748 type Output = RGB8;
749
750 fn index(&self, (x_index, y_index): (usize, usize)) -> &Self::Output {
751 assert!(x_index < W, "x_index must be within width");
752 assert!(y_index < H, "y_index must be within height");
753 &self.0[y_index][x_index]
754 }
755}
756
757impl<const W: usize, const H: usize> IndexMut<(usize, usize)> for Frame2d<W, H> {
758 fn index_mut(&mut self, (x_index, y_index): (usize, usize)) -> &mut Self::Output {
759 assert!(x_index < W, "x_index must be within width");
760 assert!(y_index < H, "y_index must be within height");
761 &mut self.0[y_index][x_index]
762 }
763}
764
765impl<const W: usize, const H: usize> From<[[RGB8; W]; H]> for Frame2d<W, H> {
766 fn from(array: [[RGB8; W]; H]) -> Self {
767 Self(array)
768 }
769}
770
771impl<const W: usize, const H: usize> From<Frame2d<W, H>> for [[RGB8; W]; H] {
772 fn from(frame: Frame2d<W, H>) -> Self {
773 frame.0
774 }
775}
776
777impl<const W: usize, const H: usize> Default for Frame2d<W, H> {
778 fn default() -> Self {
779 Self::new()
780 }
781}
782
783impl<const W: usize, const H: usize> OriginDimensions for Frame2d<W, H> {
784 fn size(&self) -> Size {
785 Size::new(W as u32, H as u32)
786 }
787}
788
789impl<const W: usize, const H: usize> DrawTarget for Frame2d<W, H> {
790 type Color = Rgb888;
791 type Error = Infallible;
792
793 fn draw_iter<I>(&mut self, pixels: I) -> core::result::Result<(), Self::Error>
794 where
795 I: IntoIterator<Item = Pixel<Self::Color>>,
796 {
797 for Pixel(coord, color) in pixels {
798 let x_index = coord.x;
799 let y_index = coord.y;
800 if x_index >= 0 && x_index < W as i32 && y_index >= 0 && y_index < H as i32 {
801 self.0[y_index as usize][x_index as usize] =
802 RGB8::new(color.r(), color.g(), color.b());
803 }
804 }
805 Ok(())
806 }
807}