Skip to main content

device_envoy_core/cyd/display/
draw_item.rs

1use crate::pixel_target::{
2    PixelTarget, PixelTargetAdapter, fill_ellipse_pixels, pixel_put, pixel_put_565,
3};
4use embedded_graphics::{
5    Drawable,
6    pixelcolor::{Rgb565, Rgb888, raw::RawU16},
7    prelude::{IntoStorage, Point, Size},
8    primitives::Rectangle,
9    primitives::{Circle, Line, Primitive, PrimitiveStyle},
10};
11
12/// A view into a statically-stored RGB565 bitmap, optionally cropped to a
13/// sub-rectangle.
14///
15/// For a full-image view use [`Image565Fixed::view`](super::tga::Image565Fixed::view);
16/// for a cropped view use
17/// [`Image565Fixed::view_rect`](super::tga::Image565Fixed::view_rect). `stride`
18/// is the full image width (row step in pixels); `source` is the crop
19/// rectangle in image coordinates.
20#[derive(Clone, Copy, Debug)]
21pub struct Image565View {
22    pixels: &'static [u16],
23    stride: u32,
24    source: Rectangle,
25}
26
27impl Image565View {
28    /// Full-image view from a raw pixel slice.
29    ///
30    /// Panics if `pixels.len() != size.width * size.height`.
31    #[must_use]
32    pub const fn new(pixels: &'static [u16], size: Size) -> Self {
33        assert!(
34            pixels.len() == size.width as usize * size.height as usize,
35            "Image565View pixels must match width * height"
36        );
37        Self {
38            pixels,
39            stride: size.width,
40            source: Rectangle::new(Point::zero(), size),
41        }
42    }
43
44    /// Cropped view — `source` is in image coordinates, `stride` is the full
45    /// image row width. Prefer [`Image565Fixed::view_rect`] at call sites.
46    #[must_use]
47    pub(crate) const fn new_cropped(
48        pixels: &'static [u16],
49        stride: u32,
50        source: Rectangle,
51    ) -> Self {
52        Self {
53            pixels,
54            stride,
55            source,
56        }
57    }
58
59    #[must_use]
60    pub const fn size(&self) -> Size {
61        self.source.size
62    }
63
64    /// Returns the pixel at `point`, where `point` is in view-local coordinates
65    /// (i.e. `(0, 0)` is the top-left of this view, not of the underlying image).
66    #[must_use]
67    pub fn pixel_at(&self, point: Point) -> Rgb565 {
68        assert!(
69            point.x >= 0 && point.y >= 0,
70            "Image565View pixel coordinate must be non-negative"
71        );
72        let vx = point.x as usize;
73        let vy = point.y as usize;
74        assert!(
75            vx < self.source.size.width as usize && vy < self.source.size.height as usize,
76            "Image565View pixel coordinate must be inside the view"
77        );
78        let source_x = self.source.top_left.x as usize + vx;
79        let source_y = self.source.top_left.y as usize + vy;
80        let index = source_y * self.stride as usize + source_x;
81        Rgb565::from(RawU16::new(self.pixels[index]))
82    }
83
84    /// Iterate over the view's pixels in row-major order as `Rgb565` values.
85    ///
86    /// Cropped views skip the pixels outside the view while preserving the
87    /// view's local row order.
88    pub fn rgb565_iter(&self) -> impl Iterator<Item = Rgb565> + '_ {
89        Image565ViewPixels {
90            view: *self,
91            index: 0,
92        }
93    }
94}
95
96struct Image565ViewPixels {
97    view: Image565View,
98    index: usize,
99}
100
101impl Iterator for Image565ViewPixels {
102    type Item = Rgb565;
103
104    fn next(&mut self) -> Option<Self::Item> {
105        let width = self.view.source.size.width as usize;
106        let height = self.view.source.size.height as usize;
107        if self.index >= width * height {
108            return None;
109        }
110
111        let view_x = self.index % width;
112        let view_y = self.index / width;
113        let source_x = self.view.source.top_left.x as usize + view_x;
114        let source_y = self.view.source.top_left.y as usize + view_y;
115        let source_index = source_y * self.view.stride as usize + source_x;
116        self.index += 1;
117        Some(Rgb565::from(RawU16::new(self.view.pixels[source_index])))
118    }
119}
120
121/// A pixel-space 2D draw item, ready to draw onto a [`PixelTarget`].
122///
123/// Construct one directly when you already have pixel-space geometry, or via
124/// linkage-blaze's CYD 3D adapters when projecting a 3D scene. All coordinates
125/// and sizes are in pixels. The `color` stays [`Rgb888`]; the target performs
126/// any conversion (for example to `Rgb565`) at its pixel boundary.
127#[derive(Clone, Copy, Debug)]
128pub enum DrawItem {
129    /// A line stroke from `start` to `end` with the given pixel width.
130    Stroke {
131        start: (f32, f32),
132        end: (f32, f32),
133        color: Rgb888,
134        pixel_width: f32,
135    },
136    /// A filled, possibly foreshortened, ellipse (a projected disk).
137    ///
138    /// The ellipse is the locus of `center + s·axis_a + t·axis_b` with `s²+t² ≤ 1`.
139    Ellipse {
140        center: (f32, f32),
141        axis_a: (f32, f32),
142        axis_b: (f32, f32),
143        color: Rgb888,
144    },
145    /// A filled circle (a projected sphere).
146    Circle {
147        center: (f32, f32),
148        pixel_radius: f32,
149        color: Rgb888,
150    },
151    /// A statically-stored RGB565 bitmap view placed at a screen position.
152    Bitmap { view: Image565View, top_left: Point },
153}
154
155impl DrawItem {
156    /// Draw this item onto a [`PixelTarget`].
157    ///
158    /// Strokes use the embedded-graphics [`Line`] primitive and circles use
159    /// [`Circle`]; the general projected ellipse is rasterized with
160    /// [`fill_ellipse_pixels`].
161    pub fn draw<T: PixelTarget>(&self, target: &mut T) {
162        match *self {
163            DrawItem::Stroke {
164                start,
165                end,
166                color,
167                pixel_width,
168            } => {
169                let width = ((pixel_width + 0.5) as u32).max(1);
170                Line::new(
171                    embedded_graphics::prelude::Point::new(start.0 as i32, start.1 as i32),
172                    embedded_graphics::prelude::Point::new(end.0 as i32, end.1 as i32),
173                )
174                .into_styled(PrimitiveStyle::with_stroke(color, width))
175                .draw(&mut PixelTargetAdapter(target))
176                .expect("drawing onto a PixelTargetAdapter is Infallible");
177            }
178            DrawItem::Ellipse {
179                center,
180                axis_a,
181                axis_b,
182                color,
183            } => {
184                fill_ellipse_pixels(center, axis_a, axis_b, |position_x, position_y| {
185                    pixel_put(target, position_x, position_y, color);
186                });
187            }
188            DrawItem::Circle {
189                center,
190                pixel_radius,
191                color,
192            } => {
193                let diameter = (((pixel_radius * 2.0) + 0.5) as u32).max(1);
194                Circle::with_center(
195                    embedded_graphics::prelude::Point::new(center.0 as i32, center.1 as i32),
196                    diameter,
197                )
198                .into_styled(PrimitiveStyle::with_fill(color))
199                .draw(&mut PixelTargetAdapter(target))
200                .expect("drawing onto a PixelTargetAdapter is Infallible");
201            }
202            DrawItem::Bitmap { view, top_left } => {
203                let size = view.size();
204                for dy in 0..size.height as i32 {
205                    for dx in 0..size.width as i32 {
206                        let view_point = Point::new(dx, dy);
207                        let target_point = top_left + view_point;
208                        pixel_put_565(
209                            target,
210                            target_point.x,
211                            target_point.y,
212                            view.pixel_at(view_point).into_storage(),
213                        );
214                    }
215                }
216            }
217        }
218    }
219}