Skip to main content

dinamika_cpu/path/
mod.rs

1//! Vector contours: segments, [`Path`] and the convenient builder [`PathBuilder`].
2//!
3//! Bézier curves are flattened into polylines during rasterization; see [`Path::to_contours`].
4
5use core::fmt;
6use std::cell::RefCell;
7use std::sync::Arc;
8
9use crate::geometry::{Point, Rect, Transform};
10
11pub(crate) mod stroke;
12
13/// Fill rule.
14#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
15pub enum FillRule {
16    /// Non-zero (winding) — the standard rule.
17    #[default]
18    NonZero,
19    /// Even-odd.
20    EvenOdd,
21}
22
23/// A contour command.
24#[derive(Copy, Clone, Debug, PartialEq)]
25pub enum PathSegment {
26    MoveTo(Point),
27    LineTo(Point),
28    /// Quadratic Bézier curve: control point, end point.
29    QuadTo(Point, Point),
30    /// Cubic Bézier curve: two control points, end point.
31    CubicTo(Point, Point, Point),
32    Close,
33}
34
35/// An immutable set of contours.
36///
37/// Flattening the curves into polylines ([`Path::to_contours`]) is memoized: a
38/// path filled repeatedly with the same transform and tolerance — the common
39/// case for a shape redrawn every frame — reuses the cached polylines instead of
40/// re-flattening and re-allocating them. The cache uses interior mutability, so
41/// a `Path` is `Send` but not `Sync` (single-threaded use, as elsewhere in the
42/// crate); equality and `Debug` ignore it.
43#[derive(Default)]
44pub struct Path {
45    pub(crate) segments: Vec<PathSegment>,
46    bounds: Option<Rect>,
47    /// Memoized flattening for the last `(transform, tolerance)` it was asked
48    /// for. `None` until the first [`Path::to_contours`].
49    flatten_cache: RefCell<Option<FlattenCache>>,
50}
51
52/// The flattened polylines cached for one `(transform, tolerance)` pair.
53struct FlattenCache {
54    transform: Transform,
55    tolerance: f32,
56    contours: Arc<Vec<Contour>>,
57}
58
59impl Clone for Path {
60    fn clone(&self) -> Self {
61        // A clone starts with an empty cache: cheap, and avoids sharing mutable
62        // cache state. The flattening is rebuilt on first use if needed.
63        Path { segments: self.segments.clone(), bounds: self.bounds, flatten_cache: RefCell::new(None) }
64    }
65}
66
67impl PartialEq for Path {
68    fn eq(&self, other: &Self) -> bool {
69        // The flatten cache is a derived value — two paths are equal when their
70        // segments (and bounds) are.
71        self.segments == other.segments && self.bounds == other.bounds
72    }
73}
74
75impl fmt::Debug for Path {
76    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77        f.debug_struct("Path")
78            .field("segments", &self.segments)
79            .field("bounds", &self.bounds)
80            .finish()
81    }
82}
83
84/// A single contour after flattening the curves — a polyline.
85#[derive(Clone, Debug)]
86pub(crate) struct Contour {
87    pub points: Vec<Point>,
88    pub closed: bool,
89}
90
91impl Path {
92    /// The contour segments.
93    pub fn segments(&self) -> &[PathSegment] {
94        &self.segments
95    }
96
97    /// Whether the path is empty.
98    pub fn is_empty(&self) -> bool {
99        self.segments.is_empty()
100    }
101
102    /// Bounding box over the anchor points (without accounting for curve bending).
103    pub fn bounds(&self) -> Option<Rect> {
104        self.bounds
105    }
106
107    /// Converts the path into a set of polylines in screen coordinates.
108    ///
109    /// `transform` is applied to the anchor points before flattening, so
110    /// `tolerance` is specified in pixels of the final image.
111    ///
112    /// The result is memoized: calling again with the same `transform` and
113    /// `tolerance` returns the cached polylines (a cheap [`Arc`] clone) instead
114    /// of re-flattening.
115    pub(crate) fn to_contours(&self, transform: Transform, tolerance: f32) -> Arc<Vec<Contour>> {
116        let tol = tolerance.max(1e-3);
117        if let Some(cache) = self.flatten_cache.borrow().as_ref() {
118            if cache.transform == transform && cache.tolerance == tol {
119                return cache.contours.clone();
120            }
121        }
122        let contours = Arc::new(self.flatten(transform, tol));
123        *self.flatten_cache.borrow_mut() =
124            Some(FlattenCache { transform, tolerance: tol, contours: contours.clone() });
125        contours
126    }
127
128    /// Flattens the curves into polylines (the uncached worker behind
129    /// [`Path::to_contours`]). `tol` is already clamped to a sane minimum.
130    fn flatten(&self, transform: Transform, tol: f32) -> Vec<Contour> {
131        let mut contours: Vec<Contour> = Vec::new();
132        let mut current: Vec<Point> = Vec::new();
133        let mut start = Point::ZERO;
134        let mut pen = Point::ZERO;
135
136        let map = |p: Point| transform.map_point(p);
137
138        let flush = |contours: &mut Vec<Contour>, pts: &mut Vec<Point>, closed: bool| {
139            if pts.len() >= 2 {
140                contours.push(Contour { points: std::mem::take(pts), closed });
141            } else {
142                pts.clear();
143            }
144        };
145
146        for seg in &self.segments {
147            match *seg {
148                PathSegment::MoveTo(p) => {
149                    flush(&mut contours, &mut current, false);
150                    let p = map(p);
151                    start = p;
152                    pen = p;
153                    current.push(p);
154                }
155                PathSegment::LineTo(p) => {
156                    let p = map(p);
157                    if current.is_empty() {
158                        current.push(pen);
159                    }
160                    current.push(p);
161                    pen = p;
162                }
163                PathSegment::QuadTo(c, p) => {
164                    let c = map(c);
165                    let p = map(p);
166                    if current.is_empty() {
167                        current.push(pen);
168                    }
169                    flatten_quad(pen, c, p, tol, 0, &mut current);
170                    pen = p;
171                }
172                PathSegment::CubicTo(c1, c2, p) => {
173                    let c1 = map(c1);
174                    let c2 = map(c2);
175                    let p = map(p);
176                    if current.is_empty() {
177                        current.push(pen);
178                    }
179                    flatten_cubic(pen, c1, c2, p, tol, 0, &mut current);
180                    pen = p;
181                }
182                PathSegment::Close => {
183                    flush(&mut contours, &mut current, true);
184                    pen = start;
185                }
186            }
187        }
188        flush(&mut contours, &mut current, false);
189        contours
190    }
191}
192
193const MAX_FLATTEN_DEPTH: u8 = 16;
194
195/// Distance from point `p` to the line through `a`–`b` (unsigned).
196#[inline]
197fn dist_to_line(p: Point, a: Point, b: Point) -> f32 {
198    let ab = b - a;
199    let len = ab.length();
200    if len < 1e-6 {
201        (p - a).length()
202    } else {
203        ((p - a).cross(ab)).abs() / len
204    }
205}
206
207fn flatten_quad(p0: Point, p1: Point, p2: Point, tol: f32, depth: u8, out: &mut Vec<Point>) {
208    if depth >= MAX_FLATTEN_DEPTH || dist_to_line(p1, p0, p2) <= tol {
209        out.push(p2);
210        return;
211    }
212    // De Casteljau subdivision at the midpoint.
213    let p01 = p0.lerp(p1, 0.5);
214    let p12 = p1.lerp(p2, 0.5);
215    let mid = p01.lerp(p12, 0.5);
216    flatten_quad(p0, p01, mid, tol, depth + 1, out);
217    flatten_quad(mid, p12, p2, tol, depth + 1, out);
218}
219
220fn flatten_cubic(
221    p0: Point,
222    p1: Point,
223    p2: Point,
224    p3: Point,
225    tol: f32,
226    depth: u8,
227    out: &mut Vec<Point>,
228) {
229    let d = dist_to_line(p1, p0, p3).max(dist_to_line(p2, p0, p3));
230    if depth >= MAX_FLATTEN_DEPTH || d <= tol {
231        out.push(p3);
232        return;
233    }
234    let p01 = p0.lerp(p1, 0.5);
235    let p12 = p1.lerp(p2, 0.5);
236    let p23 = p2.lerp(p3, 0.5);
237    let p012 = p01.lerp(p12, 0.5);
238    let p123 = p12.lerp(p23, 0.5);
239    let mid = p012.lerp(p123, 0.5);
240    flatten_cubic(p0, p01, p012, mid, tol, depth + 1, out);
241    flatten_cubic(mid, p123, p23, p3, tol, depth + 1, out);
242}
243
244/// A path builder.
245#[derive(Clone, Debug, Default)]
246pub struct PathBuilder {
247    segments: Vec<PathSegment>,
248    start: Option<Point>,
249    pen: Option<Point>,
250    min: Option<Point>,
251    max: Option<Point>,
252}
253
254impl PathBuilder {
255    pub fn new() -> Self {
256        PathBuilder::default()
257    }
258
259    fn track(&mut self, p: Point) {
260        self.min = Some(match self.min {
261            Some(m) => Point::new(m.x.min(p.x), m.y.min(p.y)),
262            None => p,
263        });
264        self.max = Some(match self.max {
265            Some(m) => Point::new(m.x.max(p.x), m.y.max(p.y)),
266            None => p,
267        });
268    }
269
270    /// Begins a new contour.
271    pub fn move_to(&mut self, x: f32, y: f32) -> &mut Self {
272        let p = Point::new(x, y);
273        self.track(p);
274        self.start = Some(p);
275        self.pen = Some(p);
276        self.segments.push(PathSegment::MoveTo(p));
277        self
278    }
279
280    /// A straight segment to a point.
281    pub fn line_to(&mut self, x: f32, y: f32) -> &mut Self {
282        let p = Point::new(x, y);
283        if self.pen.is_none() {
284            return self.move_to(x, y);
285        }
286        self.track(p);
287        self.pen = Some(p);
288        self.segments.push(PathSegment::LineTo(p));
289        self
290    }
291
292    /// A quadratic Bézier curve.
293    pub fn quad_to(&mut self, cx: f32, cy: f32, x: f32, y: f32) -> &mut Self {
294        if self.pen.is_none() {
295            self.move_to(cx, cy);
296        }
297        let c = Point::new(cx, cy);
298        let p = Point::new(x, y);
299        self.track(c);
300        self.track(p);
301        self.pen = Some(p);
302        self.segments.push(PathSegment::QuadTo(c, p));
303        self
304    }
305
306    /// A cubic Bézier curve.
307    pub fn cubic_to(&mut self, c1x: f32, c1y: f32, c2x: f32, c2y: f32, x: f32, y: f32) -> &mut Self {
308        if self.pen.is_none() {
309            self.move_to(c1x, c1y);
310        }
311        let c1 = Point::new(c1x, c1y);
312        let c2 = Point::new(c2x, c2y);
313        let p = Point::new(x, y);
314        self.track(c1);
315        self.track(c2);
316        self.track(p);
317        self.pen = Some(p);
318        self.segments.push(PathSegment::CubicTo(c1, c2, p));
319        self
320    }
321
322    /// Closes the current contour.
323    pub fn close(&mut self) -> &mut Self {
324        if !self.segments.is_empty() {
325            self.segments.push(PathSegment::Close);
326            self.pen = self.start;
327        }
328        self
329    }
330
331    /// Adds a rectangular contour (clockwise in screen coordinates).
332    pub fn push_rect(&mut self, rect: Rect) -> &mut Self {
333        self.move_to(rect.left, rect.top)
334            .line_to(rect.right, rect.top)
335            .line_to(rect.right, rect.bottom)
336            .line_to(rect.left, rect.bottom)
337            .close()
338    }
339
340    /// Adds a rectangle with rounded corners (cubic arcs).
341    ///
342    /// `radius` is clamped to half of the shorter side. With a zero radius,
343    /// a plain rectangle is added.
344    pub fn push_round_rect(&mut self, rect: Rect, radius: f32) -> &mut Self {
345        let r = radius.min(rect.width() * 0.5).min(rect.height() * 0.5);
346        if !r.is_finite() || r <= 0.0 {
347            return self.push_rect(rect);
348        }
349        // Control point offset for approximating a quarter circle.
350        const K: f32 = 0.552_284_8;
351        let kr = r * K;
352        let (l, t, rt, b) = (rect.left, rect.top, rect.right, rect.bottom);
353        self.move_to(l + r, t)
354            .line_to(rt - r, t)
355            .cubic_to(rt - r + kr, t, rt, t + r - kr, rt, t + r)
356            .line_to(rt, b - r)
357            .cubic_to(rt, b - r + kr, rt - r + kr, b, rt - r, b)
358            .line_to(l + r, b)
359            .cubic_to(l + r - kr, b, l, b - r + kr, l, b - r)
360            .line_to(l, t + r)
361            .cubic_to(l, t + r - kr, l + r - kr, t, l + r, t)
362            .close()
363    }
364
365    /// Adds an ellipse inscribed in a rectangle using four cubic arcs.
366    pub fn push_oval(&mut self, rect: Rect) -> &mut Self {
367        const K: f32 = 0.552_284_8; // (4/3)·tan(π/8)
368        let cx = (rect.left + rect.right) * 0.5;
369        let cy = (rect.top + rect.bottom) * 0.5;
370        let rx = rect.width() * 0.5;
371        let ry = rect.height() * 0.5;
372        let ox = rx * K;
373        let oy = ry * K;
374        self.move_to(cx, rect.top)
375            .cubic_to(cx + ox, rect.top, rect.right, cy - oy, rect.right, cy)
376            .cubic_to(rect.right, cy + oy, cx + ox, rect.bottom, cx, rect.bottom)
377            .cubic_to(cx - ox, rect.bottom, rect.left, cy + oy, rect.left, cy)
378            .cubic_to(rect.left, cy - oy, cx - ox, rect.top, cx, rect.top)
379            .close()
380    }
381
382    /// Adds a circle.
383    pub fn push_circle(&mut self, cx: f32, cy: f32, r: f32) -> &mut Self {
384        if let Some(rect) = Rect::from_ltrb(cx - r, cy - r, cx + r, cy + r) {
385            self.push_oval(rect);
386        }
387        self
388    }
389
390    /// Finishes building and returns a [`Path`]. `None` if the path is empty.
391    pub fn finish(self) -> Option<Path> {
392        if self.segments.is_empty() {
393            return None;
394        }
395        let bounds = match (self.min, self.max) {
396            (Some(min), Some(max)) => Rect::from_ltrb(min.x, min.y, max.x, max.y),
397            _ => None,
398        };
399        Some(Path { segments: self.segments, bounds, flatten_cache: RefCell::new(None) })
400    }
401
402    /// Appends path `segments`, each mapped by `transform`.
403    ///
404    /// Used to assemble laid-out text from cached, unscaled glyph outlines: the
405    /// outline is built once per glyph (in font-design space) and re-emitted here
406    /// under the per-placement scale/translate, avoiding a fresh `ttf-parser`
407    /// outline walk on every draw.
408    pub(crate) fn push_path_transformed(
409        &mut self,
410        segments: &[PathSegment],
411        transform: &Transform,
412    ) -> &mut Self {
413        for seg in segments {
414            match *seg {
415                PathSegment::MoveTo(p) => {
416                    let p = transform.map_point(p);
417                    self.move_to(p.x, p.y);
418                }
419                PathSegment::LineTo(p) => {
420                    let p = transform.map_point(p);
421                    self.line_to(p.x, p.y);
422                }
423                PathSegment::QuadTo(c, p) => {
424                    let c = transform.map_point(c);
425                    let p = transform.map_point(p);
426                    self.quad_to(c.x, c.y, p.x, p.y);
427                }
428                PathSegment::CubicTo(c1, c2, p) => {
429                    let c1 = transform.map_point(c1);
430                    let c2 = transform.map_point(c2);
431                    let p = transform.map_point(p);
432                    self.cubic_to(c1.x, c1.y, c2.x, c2.y, p.x, p.y);
433                }
434                PathSegment::Close => {
435                    self.close();
436                }
437            }
438        }
439        self
440    }
441
442    /// Quick creation of a rectangular path.
443    pub fn from_rect(rect: Rect) -> Path {
444        let mut b = PathBuilder::new();
445        b.push_rect(rect);
446        b.finish().unwrap()
447    }
448
449    /// Quick creation of a circle path.
450    pub fn from_circle(cx: f32, cy: f32, r: f32) -> Option<Path> {
451        let mut b = PathBuilder::new();
452        b.push_circle(cx, cy, r);
453        b.finish()
454    }
455
456    /// Quick creation of a rounded rectangle path.
457    pub fn from_round_rect(rect: Rect, radius: f32) -> Path {
458        let mut b = PathBuilder::new();
459        b.push_round_rect(rect, radius);
460        b.finish().expect("a non-empty rounded rectangle contour")
461    }
462}
463
464#[cfg(test)]
465mod tests {
466    use super::*;
467
468    #[test]
469    fn builds_rect_contour() {
470        let path = PathBuilder::from_rect(Rect::from_xywh(1.0, 2.0, 10.0, 4.0).unwrap());
471        let contours = path.to_contours(Transform::identity(), 0.1);
472        assert_eq!(contours.len(), 1);
473        assert!(contours[0].closed);
474        // 4 corners (closing does not add an extra point)
475        assert_eq!(contours[0].points.len(), 4);
476    }
477
478    #[test]
479    fn round_rect_zero_radius_is_plain_rect() {
480        let rect = Rect::from_xywh(0.0, 0.0, 10.0, 10.0).unwrap();
481        let plain = PathBuilder::from_rect(rect);
482        let rounded = PathBuilder::from_round_rect(rect, 0.0);
483        assert_eq!(plain.segments(), rounded.segments());
484    }
485
486    #[test]
487    fn round_rect_radius_clamped_to_half() {
488        // A radius larger than half the side must not break the contour.
489        let rect = Rect::from_xywh(0.0, 0.0, 10.0, 10.0).unwrap();
490        let path = PathBuilder::from_round_rect(rect, 100.0);
491        let contours = path.to_contours(Transform::identity(), 0.1);
492        assert_eq!(contours.len(), 1);
493        assert!(contours[0].closed);
494    }
495
496    /// The flattening cache must hand back identical polylines for a repeated
497    /// `(transform, tolerance)` and rebuild them when either changes.
498    #[test]
499    fn flatten_cache_reuses_and_invalidates() {
500        let mut b = PathBuilder::new();
501        b.move_to(0.0, 0.0).cubic_to(0.0, 40.0, 40.0, 40.0, 40.0, 0.0);
502        let path = b.finish().unwrap();
503
504        let a = path.to_contours(Transform::identity(), 0.1);
505        let again = path.to_contours(Transform::identity(), 0.1);
506        // Same parameters: the very same allocation is returned (Arc reuse).
507        assert!(Arc::ptr_eq(&a, &again));
508
509        // A different transform invalidates the cache and reflattens.
510        let scaled = path.to_contours(Transform::from_scale(4.0, 4.0), 0.1);
511        assert!(!Arc::ptr_eq(&a, &scaled));
512        // Finer detail at a larger scale → more flattened points.
513        assert!(scaled[0].points.len() >= a[0].points.len());
514
515        // Cloning a path does not carry the cache (fresh, independent flatten).
516        let clone = path.clone();
517        let from_clone = clone.to_contours(Transform::identity(), 0.1);
518        assert!(!Arc::ptr_eq(&a, &from_clone));
519        assert_eq!(from_clone[0].points.len(), a[0].points.len());
520    }
521
522    #[test]
523    fn flattens_curve_into_many_points() {
524        let mut b = PathBuilder::new();
525        b.move_to(0.0, 0.0).cubic_to(0.0, 100.0, 100.0, 100.0, 100.0, 0.0);
526        let path = b.finish().unwrap();
527        let contours = path.to_contours(Transform::identity(), 0.1);
528        assert!(contours[0].points.len() > 5);
529    }
530}