vello_common/
flatten.rs

1// Copyright 2025 the Vello Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Flattening filled and stroked paths.
5
6use crate::flatten_simd::Callback;
7use crate::kurbo::{self, Affine, PathEl, Stroke, StrokeCtx, StrokeOpts};
8use alloc::vec::Vec;
9use fearless_simd::{Level, Simd, dispatch};
10use log::warn;
11
12pub use crate::flatten_simd::FlattenCtx;
13
14/// The flattening tolerance.
15const TOL: f64 = 0.25;
16pub(crate) const TOL_2: f64 = TOL * TOL;
17
18/// A point.
19#[derive(Clone, Copy, Debug, PartialEq)]
20pub struct Point {
21    /// The x coordinate of the point.
22    pub x: f32,
23    /// The y coordinate of the point.
24    pub y: f32,
25}
26
27impl Point {
28    /// The point `(0, 0)`.
29    pub const ZERO: Self = Self::new(0., 0.);
30
31    /// Create a new point.
32    pub const fn new(x: f32, y: f32) -> Self {
33        Self { x, y }
34    }
35}
36
37impl core::ops::Add for Point {
38    type Output = Self;
39
40    fn add(self, rhs: Self) -> Self {
41        Self::new(self.x + rhs.x, self.y + rhs.y)
42    }
43}
44
45impl core::ops::Sub for Point {
46    type Output = Self;
47
48    fn sub(self, rhs: Self) -> Self {
49        Self::new(self.x - rhs.x, self.y - rhs.y)
50    }
51}
52
53impl core::ops::Mul<f32> for Point {
54    type Output = Self;
55
56    fn mul(self, rhs: f32) -> Self {
57        Self::new(self.x * rhs, self.y * rhs)
58    }
59}
60
61/// A line.
62#[derive(Clone, Copy, Debug)]
63pub struct Line {
64    /// The start point of the line.
65    pub p0: Point,
66    /// The end point of the line.
67    pub p1: Point,
68}
69
70impl Line {
71    /// Create a new line.
72    pub fn new(p0: Point, p1: Point) -> Self {
73        Self { p0, p1 }
74    }
75}
76
77/// Flatten a filled bezier path into line segments.
78pub fn fill(
79    level: Level,
80    path: impl IntoIterator<Item = PathEl>,
81    affine: Affine,
82    line_buf: &mut Vec<Line>,
83    ctx: &mut FlattenCtx,
84) {
85    dispatch!(level, simd => fill_impl(simd, path, affine, line_buf, ctx));
86}
87
88/// Flatten a filled bezier path into line segments.
89pub fn fill_impl<S: Simd>(
90    simd: S,
91    path: impl IntoIterator<Item = PathEl>,
92    affine: Affine,
93    line_buf: &mut Vec<Line>,
94    flatten_ctx: &mut FlattenCtx,
95) {
96    line_buf.clear();
97    let iter = path.into_iter().map(|el| affine * el);
98
99    let mut lb = FlattenerCallback {
100        line_buf,
101        start: kurbo::Point::default(),
102        p0: kurbo::Point::default(),
103        is_nan: false,
104        closed: false,
105    };
106
107    crate::flatten_simd::flatten(simd, iter, TOL, &mut lb, flatten_ctx);
108
109    if !lb.closed {
110        close_path(lb.start, lb.p0, lb.line_buf);
111    }
112
113    // A path that contains NaN is ill-defined, so ignore it.
114    if lb.is_nan {
115        warn!("A path contains NaN, ignoring it.");
116
117        line_buf.clear();
118    }
119}
120/// Flatten a stroked bezier path into line segments.
121pub fn stroke(
122    level: Level,
123    path: impl IntoIterator<Item = PathEl>,
124    style: &Stroke,
125    affine: Affine,
126    line_buf: &mut Vec<Line>,
127    flatten_ctx: &mut FlattenCtx,
128    stroke_ctx: &mut StrokeCtx,
129) {
130    // TODO: Temporary hack to ensure that strokes are scaled properly by the transform.
131    let tolerance = TOL
132        / affine.as_coeffs()[0]
133            .abs()
134            .max(affine.as_coeffs()[3].abs())
135            .max(1.);
136
137    expand_stroke(path, style, tolerance, stroke_ctx);
138    fill(level, stroke_ctx.output(), affine, line_buf, flatten_ctx);
139}
140
141/// Expand a stroked path to a filled path.
142pub fn expand_stroke(
143    path: impl IntoIterator<Item = PathEl>,
144    style: &Stroke,
145    tolerance: f64,
146    stroke_ctx: &mut StrokeCtx,
147) {
148    kurbo::stroke_with(path, style, &StrokeOpts::default(), tolerance, stroke_ctx);
149}
150
151struct FlattenerCallback<'a> {
152    line_buf: &'a mut Vec<Line>,
153    start: kurbo::Point,
154    p0: kurbo::Point,
155    is_nan: bool,
156    closed: bool,
157}
158
159impl Callback for FlattenerCallback<'_> {
160    #[inline(always)]
161    fn callback(&mut self, el: PathEl) {
162        self.is_nan |= el.is_nan();
163
164        match el {
165            kurbo::PathEl::MoveTo(p) => {
166                if !self.closed && self.p0 != self.start {
167                    close_path(self.start, self.p0, self.line_buf);
168                }
169
170                self.closed = false;
171                self.start = p;
172                self.p0 = p;
173            }
174            kurbo::PathEl::LineTo(p) => {
175                let pt0 = Point::new(self.p0.x as f32, self.p0.y as f32);
176                let pt1 = Point::new(p.x as f32, p.y as f32);
177                self.line_buf.push(Line::new(pt0, pt1));
178                self.p0 = p;
179            }
180            el @ (kurbo::PathEl::QuadTo(_, _) | kurbo::PathEl::CurveTo(_, _, _)) => {
181                unreachable!("Path has been flattened, so shouldn't contain {el:?}.")
182            }
183            kurbo::PathEl::ClosePath => {
184                self.closed = true;
185
186                close_path(self.start, self.p0, self.line_buf);
187            }
188        }
189    }
190}
191
192fn close_path(start: kurbo::Point, p0: kurbo::Point, line_buf: &mut Vec<Line>) {
193    let pt0 = Point::new(p0.x as f32, p0.y as f32);
194    let pt1 = Point::new(start.x as f32, start.y as f32);
195
196    if pt0 != pt1 {
197        line_buf.push(Line::new(pt0, pt1));
198    }
199}