Skip to main content

telar_renderer_core/
path.rs

1use geometry_core::{Point, Rect};
2use std::sync::atomic::{AtomicU64, Ordering};
3
4static NEXT_PATH_ID: AtomicU64 = AtomicU64::new(1);
5
6#[derive(Debug, Clone, PartialEq)]
7pub enum PathVerb {
8    MoveTo(Point),
9    LineTo(Point),
10    QuadTo {
11        ctrl: Point,
12        to: Point,
13    },
14    CubicTo {
15        ctrl1: Point,
16        ctrl2: Point,
17        to: Point,
18    },
19    Close,
20}
21
22#[derive(Debug, Clone)]
23pub struct PathData {
24    pub id: u64,
25    pub(crate) verbs: Vec<PathVerb>,
26    // OnceLock is both Send and Sync, required for Arc<PathData>: Send when crossing thread boundaries.
27    bounds_cache: std::sync::OnceLock<Option<Rect>>,
28}
29
30// Equal when the geometry (verbs) matches, ignoring the per-instance `id` and the lazily-filled bounds cache. Two structurally-identical paths rebuilt across frames must compare equal so dirty-tracking (scroll-blit, dirty-rect) treats them as unchanged — otherwise every rebuild's fresh `id` would force a full-screen repaint.
31impl PartialEq for PathData {
32    fn eq(&self, other: &Self) -> bool {
33        self.verbs == other.verbs
34    }
35}
36
37impl Default for PathData {
38    fn default() -> Self {
39        Self {
40            id: NEXT_PATH_ID.fetch_add(1, Ordering::Relaxed),
41            verbs: Vec::new(),
42            bounds_cache: std::sync::OnceLock::new(),
43        }
44    }
45}
46
47impl PathData {
48    pub fn new() -> Self {
49        Self::default()
50    }
51
52    pub fn verbs(&self) -> &[PathVerb] {
53        &self.verbs
54    }
55
56    pub fn move_to(mut self, p: Point) -> Self {
57        self.verbs.push(PathVerb::MoveTo(p));
58        self.bounds_cache = std::sync::OnceLock::new();
59        self
60    }
61
62    pub fn line_to(mut self, p: Point) -> Self {
63        self.verbs.push(PathVerb::LineTo(p));
64        self.bounds_cache = std::sync::OnceLock::new();
65        self
66    }
67
68    pub fn quad_to(mut self, ctrl: Point, to: Point) -> Self {
69        self.verbs.push(PathVerb::QuadTo { ctrl, to });
70        self.bounds_cache = std::sync::OnceLock::new();
71        self
72    }
73
74    pub fn cubic_to(mut self, ctrl1: Point, ctrl2: Point, to: Point) -> Self {
75        self.verbs.push(PathVerb::CubicTo { ctrl1, ctrl2, to });
76        self.bounds_cache = std::sync::OnceLock::new();
77        self
78    }
79
80    pub fn close(mut self) -> Self {
81        self.verbs.push(PathVerb::Close);
82        self.bounds_cache = std::sync::OnceLock::new();
83        self
84    }
85
86    /// Re-fits every point by `p' = p * s + (dx, dy)` (uniform scale + translation), returning a new path.
87    ///
88    /// Baking transforms points into path coordinates (not a matrix) so lyon never facets curves; this applies the runtime letterbox fit the same way, keeping the baked path equivalent to the dynamic one.
89    pub fn refit(&self, s: f32, dx: f32, dy: f32) -> Self {
90        self.refit_xy(s, s, dx, dy)
91    }
92
93    /// Non-uniform variant of `refit`: `p' = (p.x * sx + dx, p.y * sy + dy)`. `object-fit: fill` uses `sx != sy` to stretch the path to the box (distorting the aspect ratio).
94    pub fn refit_xy(&self, sx: f32, sy: f32, dx: f32, dy: f32) -> Self {
95        let map = |p: Point| Point::new(p.x * sx + dx, p.y * sy + dy);
96        let mut out = Self::new();
97        for verb in &self.verbs {
98            out = match verb {
99                PathVerb::MoveTo(p) => out.move_to(map(*p)),
100                PathVerb::LineTo(p) => out.line_to(map(*p)),
101                PathVerb::QuadTo { ctrl, to } => out.quad_to(map(*ctrl), map(*to)),
102                PathVerb::CubicTo { ctrl1, ctrl2, to } => {
103                    out.cubic_to(map(*ctrl1), map(*ctrl2), map(*to))
104                }
105                PathVerb::Close => out.close(),
106            };
107        }
108        out
109    }
110
111    /// A closed polygon through `points` (first point is the start; the path is closed back to it).
112    pub fn polygon(points: &[Point]) -> Self {
113        let mut path = Self::new();
114        let Some((first, rest)) = points.split_first() else {
115            return path;
116        };
117        path = path.move_to(*first);
118        for p in rest {
119            path = path.line_to(*p);
120        }
121        path.close()
122    }
123
124    pub fn bounds(&self) -> Option<Rect> {
125        *self.bounds_cache.get_or_init(|| {
126            let mut min_x = f32::INFINITY;
127            let mut min_y = f32::INFINITY;
128            let mut max_x = f32::NEG_INFINITY;
129            let mut max_y = f32::NEG_INFINITY;
130            let mut has_geometry = false;
131
132            for verb in &self.verbs {
133                match verb {
134                    PathVerb::MoveTo(p) | PathVerb::LineTo(p) => {
135                        min_x = min_x.min(p.x);
136                        min_y = min_y.min(p.y);
137                        max_x = max_x.max(p.x);
138                        max_y = max_y.max(p.y);
139                        has_geometry = true;
140                    }
141                    PathVerb::QuadTo { ctrl, to } => {
142                        // Bézier curves are bounded by their control polygon (convex hull property).
143                        for p in &[ctrl, to] {
144                            min_x = min_x.min(p.x);
145                            min_y = min_y.min(p.y);
146                            max_x = max_x.max(p.x);
147                            max_y = max_y.max(p.y);
148                        }
149                        has_geometry = true;
150                    }
151                    PathVerb::CubicTo { ctrl1, ctrl2, to } => {
152                        for p in &[ctrl1, ctrl2, to] {
153                            min_x = min_x.min(p.x);
154                            min_y = min_y.min(p.y);
155                            max_x = max_x.max(p.x);
156                            max_y = max_y.max(p.y);
157                        }
158                        has_geometry = true;
159                    }
160                    PathVerb::Close => {}
161                }
162            }
163
164            if has_geometry {
165                Some(Rect::new(min_x, min_y, max_x - min_x, max_y - min_y))
166            } else {
167                None
168            }
169        })
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[test]
178    fn path_data_new_is_empty() {
179        let path = PathData::new();
180        assert!(path.verbs().is_empty());
181    }
182
183    #[test]
184    fn path_data_move_to_adds_verb() {
185        let path = PathData::new().move_to(Point::new(0.0, 0.0));
186        assert_eq!(path.verbs().len(), 1);
187        assert!(matches!(path.verbs()[0], PathVerb::MoveTo(_)));
188    }
189
190    #[test]
191    fn path_data_line_to_adds_verb() {
192        let path = PathData::new()
193            .move_to(Point::new(0.0, 0.0))
194            .line_to(Point::new(1.0, 1.0));
195        assert_eq!(path.verbs().len(), 2);
196        assert!(matches!(path.verbs()[1], PathVerb::LineTo(_)));
197    }
198
199    #[test]
200    fn path_data_close_adds_verb() {
201        let path = PathData::new().move_to(Point::new(0.0, 0.0)).close();
202        assert!(matches!(path.verbs().last().unwrap(), PathVerb::Close));
203    }
204
205    #[test]
206    fn path_data_quad_to_adds_verb() {
207        let path = PathData::new().quad_to(Point::new(1.0, 0.0), Point::new(2.0, 0.0));
208        assert!(matches!(path.verbs()[0], PathVerb::QuadTo { .. }));
209    }
210
211    #[test]
212    fn path_data_cubic_to_adds_verb() {
213        let path = PathData::new().cubic_to(
214            Point::new(1.0, 0.0),
215            Point::new(2.0, 0.0),
216            Point::new(3.0, 0.0),
217        );
218        assert!(matches!(path.verbs()[0], PathVerb::CubicTo { .. }));
219    }
220
221    #[test]
222    fn path_data_builder_accumulates_verbs() {
223        let path = PathData::new()
224            .move_to(Point::new(0.0, 0.0))
225            .line_to(Point::new(1.0, 0.0))
226            .line_to(Point::new(1.0, 1.0))
227            .close();
228        assert_eq!(path.verbs().len(), 4);
229    }
230}