Skip to main content

embedded_gui/
pdc.rs

1//! Precise Draw Commands (PDC) vector graphics format and renderer.
2//!
3//! Compact, binary-efficient vector format supporting arbitrary paths, circles,
4//! and 13.3 fixed-point subpixel precision coordinates.
5
6use embedded_graphics_core::{draw_target::DrawTarget, geometry::Point, pixelcolor::Rgb565};
7use heapless::Vec;
8
9use crate::{
10    geometry::Rect,
11    render::{RenderCtx, StrokeStyle},
12};
13
14/// Type of Precise Draw Command.
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub enum PdcCommandType {
17    /// Arbitrary line/polygon path with standard integer coordinates.
18    Path,
19    /// Circle with center point and radius.
20    Circle,
21    /// Arbitrary path with 13.3 fixed-point subpixel precision (1/8th pixel).
22    PrecisePath,
23}
24
25/// Error indicating capacity limit exceeded during PDC construction.
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27pub struct PdcError;
28
29/// A 13.3 fixed-point coordinate point (1 unit = 1/8th pixel).
30#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
31pub struct PdcPrecisePoint {
32    pub x_fixed: i16,
33    pub y_fixed: i16,
34}
35
36impl PdcPrecisePoint {
37    pub const fn from_subpixels(x_fixed: i16, y_fixed: i16) -> Self {
38        Self { x_fixed, y_fixed }
39    }
40
41    pub const fn from_pixels(x: i16, y: i16) -> Self {
42        Self {
43            x_fixed: x << 3,
44            y_fixed: y << 3,
45        }
46    }
47
48    pub const fn to_pixel_point(self) -> Point {
49        Point::new((self.x_fixed >> 3) as i32, (self.y_fixed >> 3) as i32)
50    }
51
52    pub fn to_f32_point(self) -> (f32, f32) {
53        (self.x_fixed as f32 / 8.0, self.y_fixed as f32 / 8.0)
54    }
55}
56
57/// A single Precise Draw Command encoding stroke, fill, and geometry.
58#[derive(Clone, Debug, PartialEq)]
59pub struct PdcCommand<const MAX_POINTS: usize = 16> {
60    pub command_type: PdcCommandType,
61    pub stroke_color: Option<Rgb565>,
62    pub fill_color: Option<Rgb565>,
63    pub stroke_width: u8,
64    pub radius: u16,
65    pub is_closed: bool,
66    pub points: Vec<PdcPrecisePoint, MAX_POINTS>,
67}
68
69impl<const MAX_POINTS: usize> PdcCommand<MAX_POINTS> {
70    pub const fn new(command_type: PdcCommandType) -> Self {
71        Self {
72            command_type,
73            stroke_color: None,
74            fill_color: None,
75            stroke_width: 1,
76            radius: 0,
77            is_closed: false,
78            points: Vec::new(),
79        }
80    }
81
82    pub fn circle(
83        center: Point,
84        radius: u16,
85        stroke: Option<Rgb565>,
86        fill: Option<Rgb565>,
87        stroke_width: u8,
88    ) -> Self {
89        let mut cmd = Self::new(PdcCommandType::Circle);
90        cmd.stroke_color = stroke;
91        cmd.fill_color = fill;
92        cmd.stroke_width = stroke_width;
93        cmd.radius = radius;
94        let p = PdcPrecisePoint::from_pixels(center.x as i16, center.y as i16);
95        let _ = cmd.points.push(p);
96        cmd
97    }
98
99    pub fn add_point(&mut self, pt: Point) -> Result<(), PdcError> {
100        self.points
101            .push(PdcPrecisePoint::from_pixels(pt.x as i16, pt.y as i16))
102            .map_err(|_| PdcError)
103    }
104
105    pub fn add_subpixel_point(&mut self, x_fixed: i16, y_fixed: i16) -> Result<(), PdcError> {
106        self.points
107            .push(PdcPrecisePoint::from_subpixels(x_fixed, y_fixed))
108            .map_err(|_| PdcError)
109    }
110
111    /// Renders this vector command into the provided [`RenderCtx`].
112    pub fn render<D, C>(&self, ctx: &mut RenderCtx<'_, D, C>, offset: Point) -> Result<(), D::Error>
113    where
114        D: DrawTarget<Color = Rgb565>,
115        C: crate::render::Compositor<D>,
116    {
117        match self.command_type {
118            PdcCommandType::Circle => {
119                if let Some(center_pt) = self.points.first() {
120                    let p = center_pt.to_pixel_point();
121                    let cx = p.x + offset.x;
122                    let cy = p.y + offset.y;
123                    let r = self.radius as u32;
124
125                    if let Some(fill) = self.fill_color {
126                        ctx.fill_circle(cx, cy, r, fill)?;
127                    }
128                    if let Some(stroke) = self.stroke_color {
129                        if self.stroke_width > 0 {
130                            ctx.stroke_circle(cx, cy, r, stroke)?;
131                        }
132                    }
133                }
134            }
135            PdcCommandType::Path | PdcCommandType::PrecisePath => {
136                if self.points.len() < 2 {
137                    return Ok(());
138                }
139
140                // Render stroke lines connecting points
141                if let Some(stroke) = self.stroke_color {
142                    let mut prev = self.points[0].to_pixel_point();
143                    prev.x += offset.x;
144                    prev.y += offset.y;
145
146                    let mut i = 1;
147                    while i < self.points.len() {
148                        let mut curr = self.points[i].to_pixel_point();
149                        curr.x += offset.x;
150                        curr.y += offset.y;
151
152                        if self.stroke_width <= 1 {
153                            ctx.draw_line(prev.x, prev.y, curr.x, curr.y, stroke)?;
154                        } else {
155                            ctx.draw_line_styled(
156                                prev.x,
157                                prev.y,
158                                curr.x,
159                                curr.y,
160                                StrokeStyle::new(stroke).with_width(self.stroke_width),
161                            )?;
162                        }
163                        prev = curr;
164                        i += 1;
165                    }
166
167                    if self.is_closed && self.points.len() >= 3 {
168                        let mut first = self.points[0].to_pixel_point();
169                        first.x += offset.x;
170                        first.y += offset.y;
171                        if self.stroke_width <= 1 {
172                            ctx.draw_line(prev.x, prev.y, first.x, first.y, stroke)?;
173                        } else {
174                            ctx.draw_line_styled(
175                                prev.x,
176                                prev.y,
177                                first.x,
178                                first.y,
179                                StrokeStyle::new(stroke).with_width(self.stroke_width),
180                            )?;
181                        }
182                    }
183                }
184            }
185        }
186        Ok(())
187    }
188}
189
190/// A composite Precise Draw Command Image (PDC vector asset).
191#[derive(Clone, Debug, PartialEq)]
192pub struct PdcImage<const MAX_COMMANDS: usize = 8, const MAX_POINTS_PER_CMD: usize = 16> {
193    pub viewbox: Rect,
194    pub commands: Vec<PdcCommand<MAX_POINTS_PER_CMD>, MAX_COMMANDS>,
195}
196
197impl<const MAX_COMMANDS: usize, const MAX_POINTS_PER_CMD: usize> Default
198    for PdcImage<MAX_COMMANDS, MAX_POINTS_PER_CMD>
199{
200    fn default() -> Self {
201        Self::new(Rect::new(0, 0, 0, 0))
202    }
203}
204
205impl<const MAX_COMMANDS: usize, const MAX_POINTS_PER_CMD: usize>
206    PdcImage<MAX_COMMANDS, MAX_POINTS_PER_CMD>
207{
208    pub const fn new(viewbox: Rect) -> Self {
209        Self {
210            viewbox,
211            commands: Vec::new(),
212        }
213    }
214
215    pub fn push_command(&mut self, cmd: PdcCommand<MAX_POINTS_PER_CMD>) -> Result<(), PdcError> {
216        self.commands.push(cmd).map_err(|_| PdcError)
217    }
218
219    /// Renders the entire vector image at the given target origin.
220    pub fn draw<D, C>(&self, ctx: &mut RenderCtx<'_, D, C>, origin: Point) -> Result<(), D::Error>
221    where
222        D: DrawTarget<Color = Rgb565>,
223        C: crate::render::Compositor<D>,
224    {
225        for cmd in &self.commands {
226            cmd.render(ctx, origin)?;
227        }
228        Ok(())
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235    use crate::framebuffer::Framebuffer;
236    use embedded_graphics_core::pixelcolor::RgbColor;
237
238    #[test]
239    fn test_pdc_precise_points() {
240        let pt = PdcPrecisePoint::from_pixels(10, 20);
241        assert_eq!(pt.x_fixed, 80);
242        assert_eq!(pt.y_fixed, 160);
243        assert_eq!(pt.to_pixel_point(), Point::new(10, 20));
244
245        let sub_pt = PdcPrecisePoint::from_subpixels(84, 164);
246        assert_eq!(sub_pt.to_pixel_point(), Point::new(10, 20));
247        let (fx, fy) = sub_pt.to_f32_point();
248        assert!((fx - 10.5).abs() < 0.001);
249        assert!((fy - 20.5).abs() < 0.001);
250    }
251
252    #[test]
253    fn test_pdc_image_render() {
254        let mut img = PdcImage::<4, 8>::new(Rect::new(0, 0, 20, 20));
255        let circle = PdcCommand::circle(Point::new(10, 10), 4, Some(Rgb565::RED), None, 1);
256        assert!(img.push_command(circle).is_ok());
257
258        let mut fb = Framebuffer::<400>::new(20, 20);
259        let mut ctx = RenderCtx::new(&mut fb, Rect::new(0, 0, 20, 20));
260        img.draw(&mut ctx, Point::new(0, 0)).unwrap();
261    }
262}