Skip to main content

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/// ![LED panel preview](https://raw.githubusercontent.com/CarlKCarlK/device-envoy/main/crates/device-envoy-core/docs/assets/led2d1.png)
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/// ![LED panel preview](https://raw.githubusercontent.com/CarlKCarlK/device-envoy/main/crates/device-envoy-core/docs/assets/led2d2.png)
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}