Skip to main content

impeller_geometry/
path.rs

1//! Path representation.
2//!
3//! Paths are stored as a verb stream with a parallel point buffer rather than
4//! as an enum-per-segment vector. Tessellation walks both linearly, and
5//! keeping points contiguous means the walk touches one cache line per few
6//! segments instead of chasing a pointer per segment.
7
8use glam::Vec2;
9
10/// How overlapping subpaths combine when filling.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
12pub enum FillRule {
13    /// A point is inside when the signed crossing count is nonzero.
14    #[default]
15    NonZero,
16    /// A point is inside when the crossing count is odd.
17    EvenOdd,
18}
19
20/// A path segment operator.
21///
22/// Each verb consumes a fixed number of points from the point buffer, given by
23/// [`Verb::point_count`].
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum Verb {
26    MoveTo,
27    LineTo,
28    QuadTo,
29    CubicTo,
30    Close,
31}
32
33impl Verb {
34    /// Points this verb consumes from the point buffer.
35    pub const fn point_count(self) -> usize {
36        match self {
37            Self::MoveTo | Self::LineTo => 1,
38            Self::QuadTo => 2,
39            Self::CubicTo => 3,
40            Self::Close => 0,
41        }
42    }
43}
44
45/// An axis-aligned bounding box.
46#[derive(Debug, Clone, Copy, PartialEq)]
47pub struct Rect {
48    pub min: Vec2,
49    pub max: Vec2,
50}
51
52impl Rect {
53    pub fn new(min: Vec2, max: Vec2) -> Self {
54        Self { min, max }
55    }
56
57    /// The empty rect, which absorbs any point when unioned.
58    pub fn empty() -> Self {
59        Self {
60            min: Vec2::splat(f32::INFINITY),
61            max: Vec2::splat(f32::NEG_INFINITY),
62        }
63    }
64
65    pub fn is_empty(&self) -> bool {
66        self.min.x > self.max.x || self.min.y > self.max.y
67    }
68
69    pub fn union_point(&mut self, p: Vec2) {
70        self.min = self.min.min(p);
71        self.max = self.max.max(p);
72    }
73
74    pub fn contains(&self, p: Vec2) -> bool {
75        p.x >= self.min.x && p.x <= self.max.x && p.y >= self.min.y && p.y <= self.max.y
76    }
77
78    pub fn width(&self) -> f32 {
79        (self.max.x - self.min.x).max(0.0)
80    }
81
82    pub fn height(&self) -> f32 {
83        (self.max.y - self.min.y).max(0.0)
84    }
85}
86
87/// Whether a subpath is convex, which selects the fill strategy.
88///
89/// Convex paths take a fan-fill fast path that skips both tessellation and the
90/// stencil pass, so detection is worth doing — but a wrong `Convex` answer
91/// renders incorrectly, while a wrong `Concave` answer only costs speed.
92/// Detection is therefore conservative: anything it cannot prove convex is
93/// reported [`Convexity::Concave`].
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub enum Convexity {
96    Convex,
97    Concave,
98}
99
100/// A path: a verb stream plus the points those verbs consume.
101#[derive(Debug, Clone, Default, PartialEq)]
102pub struct Path {
103    verbs: Vec<Verb>,
104    points: Vec<Vec2>,
105    fill_rule: FillRule,
106    /// The rounded rectangle this path was built from, if it was one.
107    ///
108    /// A path is a verb stream, and once a shape has been written into one
109    /// there is no way back: a rounded rectangle and a hand-drawn outline that
110    /// happens to match it are the same path. Some routes want the shape rather
111    /// than the outline -- the blurred rounded rectangle is evaluated in the
112    /// fragment stage and needs a rect and a radius, not eight cubics -- so
113    /// what built it is recorded here rather than reconstructed by inspection,
114    /// which is the version of this that misfires on a shape it merely
115    /// resembles.
116    ///
117    /// Upstream keeps the same thing on `DlPath`, which can still answer
118    /// whether it is a round rect after being consumed as a path.
119    ///
120    /// Set only by a builder that actually laid down that shape. It is part of
121    /// equality because two paths differing in it are two different claims
122    /// about the same outline, and nothing here compares whole paths anyway.
123    rounded_rect: Option<(Rect, f32)>,
124}
125
126/// The largest coordinate magnitude the tessellator will accept.
127///
128/// Two to the twenty-fourth, which is the largest integer an `f32` represents
129/// exactly. See [`Path::is_within_tessellation_range`] for why it is here and
130/// why this is the value.
131pub const MAX_COORDINATE: f32 = 16_777_216.0;
132
133impl Path {
134    pub fn builder() -> PathBuilder {
135        PathBuilder::new()
136    }
137
138    /// The rounded rectangle this was built from, if it was built from one.
139    ///
140    /// A radius of zero is a plain rectangle and is reported as such; `None`
141    /// means only that nothing recorded a shape, never that the outline is not
142    /// one.
143    pub fn as_rounded_rect(&self) -> Option<(Rect, f32)> {
144        self.rounded_rect
145    }
146
147    pub fn verbs(&self) -> &[Verb] {
148        &self.verbs
149    }
150
151    pub fn points(&self) -> &[Vec2] {
152        &self.points
153    }
154
155    /// Whether every point in this path is a real location.
156    ///
157    /// A path carrying a NaN or an infinity is not a shape. It arrives when a
158    /// caller's own arithmetic has already gone wrong -- a division by a zero
159    /// extent, an inverted degenerate transform -- and there is no picture it
160    /// asks for, so the tessellator refuses one rather than inventing it.
161    ///
162    /// Worth having as a question rather than a debug assertion because lyon
163    /// asserts on a non-finite coordinate, and an assertion in a dependency
164    /// takes the process down. A library given a bad number should decline to
165    /// draw, not abort the application holding it.
166    pub fn is_finite(&self) -> bool {
167        self.points.iter().all(|p| p.is_finite())
168    }
169
170    /// Whether every coordinate is small enough to tessellate.
171    ///
172    /// Finite is not the same as usable, and the gap between them is where the
173    /// tessellator's cost stops being bounded. `f32::MAX` is finite; so is
174    /// `1e15`, and a path with three verbs at that magnitude strokes to
175    /// thirty-one million vertices -- six hundred megabytes of position and
176    /// index from a cubic and a close. The fill route is safe from it because
177    /// this crate's own flattener caps at [`MAX_SEGMENTS`], but a stroke hands
178    /// its curves to lyon intact, deliberately and for the reason
179    /// `Tessellator::stroke` gives, and lyon subdivides by its own arithmetic
180    /// with no such cap. The output grows linearly in the coordinate, which
181    /// makes it a caller-controlled allocation with nothing at the top of it.
182    ///
183    /// [`MAX_COORDINATE`] is where that stops, and the limit is the same one
184    /// two different arguments arrive at. A float past two to the twenty-fourth
185    /// has an interval above one between it and its neighbor, so a coordinate
186    /// there cannot name a pixel and no picture depends on it. And measured, a
187    /// curve at that magnitude strokes to about twenty thousand vertices,
188    /// which is a shape rather than an allocation.
189    ///
190    /// [`MAX_SEGMENTS`]: crate::flatten::MAX_SEGMENTS
191    pub fn is_within_tessellation_range(&self) -> bool {
192        self.points
193            .iter()
194            .all(|p| p.x.abs() <= MAX_COORDINATE && p.y.abs() <= MAX_COORDINATE)
195    }
196
197    pub fn fill_rule(&self) -> FillRule {
198        self.fill_rule
199    }
200
201    pub fn is_empty(&self) -> bool {
202        self.verbs.is_empty()
203    }
204
205    /// Bounds of the control points.
206    ///
207    /// This is a bound, not a tight fit: a curve lies within the convex hull of
208    /// its control points, so a curve that bends away from its handles reports
209    /// a larger box than it occupies. That is the right trade for culling,
210    /// where a conservative overestimate is safe and cheap while a tight fit
211    /// costs a solve per curve.
212    pub fn bounds(&self) -> Rect {
213        let mut r = Rect::empty();
214        for p in &self.points {
215            r.union_point(*p);
216        }
217        r
218    }
219
220    /// Walk the path as `(verb, points)` pairs.
221    pub fn segments(&self) -> impl Iterator<Item = (Verb, &[Vec2])> {
222        let mut cursor = 0usize;
223        self.verbs.iter().map(move |verb| {
224            let n = verb.point_count();
225            let slice = &self.points[cursor..cursor + n];
226            cursor += n;
227            (*verb, slice)
228        })
229    }
230
231    /// Whether the path is provably convex.
232    ///
233    /// Answers [`Convexity::Concave`] for anything with more than one subpath,
234    /// or containing curves, rather than analyzing further. Curved paths are
235    /// classified after flattening, where the question is a polygon question.
236    pub fn convexity(&self) -> Convexity {
237        if self
238            .verbs
239            .iter()
240            .any(|v| matches!(v, Verb::QuadTo | Verb::CubicTo))
241        {
242            return Convexity::Concave;
243        }
244        if self.verbs.iter().filter(|v| **v == Verb::MoveTo).count() > 1 {
245            return Convexity::Concave;
246        }
247        polygon_convexity(&self.points)
248    }
249}
250
251/// Which quadrant a direction points into, as a number that increases
252/// counter-clockwise.
253///
254/// Zero-length directions land in the first quadrant, which is harmless: a
255/// repeated point contributes no rotation and the count below is of net
256/// advance, not of steps.
257fn quadrant(v: Vec2) -> i32 {
258    match (v.x >= 0.0, v.y >= 0.0) {
259        (true, true) => 0,
260        (false, true) => 1,
261        (false, false) => 2,
262        (true, false) => 3,
263    }
264}
265
266/// Classify a polygon by the consistency of its turn directions.
267///
268/// A simple polygon is convex when every turn goes the same way. Collinear
269/// points contribute a zero cross product and are skipped rather than treated
270/// as a reversal, since they are common in generated geometry and do not
271/// affect convexity.
272///
273/// Turn agreement alone is not enough, because a self-crossing polygon can
274/// have every turn agree: a pentagram turns the same way at all five points
275/// and is not remotely convex. What separates them is how far the polygon
276/// turns in total. Walking a simple closed polygon comes back to the start
277/// having turned through exactly one full circle; walking a pentagram turns
278/// through two.
279///
280/// That total is counted rather than measured. Once the turns are known to
281/// agree the direction rotates monotonically, so each step advances through
282/// zero, one or two quadrants in the one direction and never doubles back --
283/// and summing those advances gives four per revolution exactly. The
284/// alternative, accumulating the turn angles with `atan2`, computes the same
285/// number and measured six times slower than the whole rest of this function,
286/// on a path that runs once per filled path per frame.
287///
288/// This matters well beyond classification. The caller uses `Convex` to take a
289/// fan fill, which triangulates from one vertex and applies no fill rule at
290/// all -- so a self-crossing polygon called convex is filled by a routine that
291/// cannot express what filling it means, and its fill rule is silently
292/// discarded.
293pub fn polygon_convexity(points: &[Vec2]) -> Convexity {
294    if points.len() < 3 {
295        return Convexity::Convex;
296    }
297    let n = points.len();
298    let mut sign = 0i32;
299    // Both readings of the advance, since which one counts depends on the
300    // direction of travel and that is not settled until the walk finishes.
301    // Two running sums rather than a list of steps, so this allocates nothing.
302    let (mut counter_clockwise, mut clockwise) = (0i32, 0i32);
303    for i in 0..n {
304        let a = points[i];
305        let b = points[(i + 1) % n];
306        let c = points[(i + 2) % n];
307        let incoming = b - a;
308        let outgoing = c - b;
309        let cross = incoming.perp_dot(outgoing);
310        let advance = (quadrant(outgoing) - quadrant(incoming)).rem_euclid(4);
311        counter_clockwise += advance;
312        clockwise += (-advance).rem_euclid(4);
313        if cross.abs() <= f32::EPSILON {
314            continue;
315        }
316        let s = if cross > 0.0 { 1 } else { -1 };
317        if sign == 0 {
318            sign = s;
319        } else if sign != s {
320            return Convexity::Concave;
321        }
322    }
323    if sign == 0 {
324        // Every turn was collinear, so the polygon is a segment traversed out
325        // and back. Degenerate rather than self-crossing, and it covers no
326        // area either way.
327        return Convexity::Convex;
328    }
329    let turned = if sign > 0 {
330        counter_clockwise
331    } else {
332        clockwise
333    };
334    if turned > 4 {
335        return Convexity::Concave;
336    }
337    Convexity::Convex
338}
339
340/// Accumulates verbs and points into a [`Path`].
341#[derive(Debug, Clone, Default)]
342pub struct PathBuilder {
343    verbs: Vec<Verb>,
344    points: Vec<Vec2>,
345    fill_rule: FillRule,
346    /// Where the current subpath started, for `close`.
347    subpath_start: Option<Vec2>,
348    current: Option<Vec2>,
349    rounded_rect: Option<(Rect, f32)>,
350}
351
352impl PathBuilder {
353    pub fn new() -> Self {
354        Self::default()
355    }
356
357    pub fn with_fill_rule(mut self, rule: FillRule) -> Self {
358        self.fill_rule = rule;
359        self
360    }
361
362    pub fn move_to(&mut self, p: Vec2) -> &mut Self {
363        self.verbs.push(Verb::MoveTo);
364        self.points.push(p);
365        self.subpath_start = Some(p);
366        self.current = Some(p);
367        self
368    }
369
370    /// Add a line segment.
371    ///
372    /// A line before any `move_to` implicitly starts the subpath at the
373    /// origin, matching how path data from SVG and font outlines behaves when
374    /// it omits the opening move.
375    pub fn line_to(&mut self, p: Vec2) -> &mut Self {
376        self.ensure_started();
377        self.verbs.push(Verb::LineTo);
378        self.points.push(p);
379        self.current = Some(p);
380        self
381    }
382
383    pub fn quad_to(&mut self, ctrl: Vec2, to: Vec2) -> &mut Self {
384        self.ensure_started();
385        self.verbs.push(Verb::QuadTo);
386        self.points.push(ctrl);
387        self.points.push(to);
388        self.current = Some(to);
389        self
390    }
391
392    /// Append a rational quadratic -- a conic -- through one control point.
393    ///
394    /// The curve a circular arc actually is, and the one a font or an SVG
395    /// hands over. `weight` is the control point's pull: one is an ordinary
396    /// quadratic, less than one is elliptical, more is hyperbolic, and
397    /// `sqrt(2)/2` with the control at the corner of a square is exactly a
398    /// quarter circle.
399    ///
400    /// # Why this becomes quadratics here rather than surviving as a verb
401    ///
402    /// A conic of weight one *is* a quadratic, and splitting a conic in half
403    /// moves its weight toward one -- quadratically, so a handful of splits
404    /// puts it within a thousandth. So this subdivides until the weight is
405    /// near enough and emits ordinary quadratics, which every part of this
406    /// crate and the tessellator already understand.
407    ///
408    /// The alternative is a verb of its own, converted during flattening where
409    /// the transform's scale is known. That is what a renderer does when it
410    /// wants an absolute error in device pixels. What is bought here instead
411    /// is a *relative* error: the approximation is a fixed fraction of the
412    /// curve's own size, so it stays correct at every scale the path is later
413    /// drawn at, and nothing downstream has to learn a fifth verb.
414    ///
415    /// # Subdividing by the error rather than by the weight
416    ///
417    /// The criterion below stops when the *error* is small enough, and that is
418    /// worth spelling out because the obvious criterion -- stop when the
419    /// weight is near one -- is what it replaced, and it was expensive by a
420    /// factor of four.
421    ///
422    /// A weight criterion counts halvings of the weight's distance from one,
423    /// and each halving doubles the output. So a curve gets subdivided by how
424    /// hyperbolic it is rather than by how much the approximation misses. A
425    /// rounded superellipse octant emits conics at weights around 0.7 and 8.3;
426    /// under the weight criterion those became thirty-two and sixty-four
427    /// quadratics, and the corpus scene cost 1340 vertices where a circle
428    /// costs 40. It is 305 now, and the stroked one 274 against 922.
429    ///
430    /// The error itself is four lines: a rational quadratic and its weight-one
431    /// counterpart differ most at their midpoints, both midpoints have closed
432    /// forms, and the distance between them measured against the chord is a
433    /// relative bound -- the same quantity the weight criterion was a proxy
434    /// for, at the same tolerance of a thousandth.
435    ///
436    /// It is a little less accurate where the proxy over-subdivided, which is
437    /// most places. Measured against the old criterion: four pixels of the
438    /// filled corpus scene and eight of the stroked one, out of sixteen
439    /// thousand, and all of them at the shape's tangent extremes where the
440    /// outline lies along a pixel boundary and a sub-pixel shift moves every
441    /// sample in the pixel together. Every comparison in the workspace passes
442    /// unchanged, including the pin on upstream's boundary points, which is
443    /// what says the outline is still the shape upstream draws.
444    ///
445    /// A weight that is zero or negative or not finite describes no curve, and
446    /// gives the straight line between the ends.
447    pub fn conic_to(&mut self, ctrl: Vec2, to: Vec2, weight: f32) -> &mut Self {
448        self.ensure_started();
449        let from = self.current.unwrap_or(ctrl);
450        if !weight.is_finite() || weight <= 0.0 || !ctrl.is_finite() || !to.is_finite() {
451            return self.line_to(to);
452        }
453        self.push_conic(from, ctrl, to, weight, 0);
454        self
455    }
456
457    /// Split until the weight is near one, then emit a quadratic.
458    fn push_conic(&mut self, from: Vec2, ctrl: Vec2, to: Vec2, weight: f32, depth: u32) {
459        // A thousandth of the way from one is close enough that the quadratic
460        // and the conic differ by well under a thousandth of the curve's own
461        // extent, which no rasterizer this feeds can show.
462        //
463        // The depth cap is not expected to be reached: the weight halves its
464        // distance from one roughly every split, so even a wildly hyperbolic
465        // conic converges in a handful. It is here because a bound that cannot
466        // be reached costs nothing and a recursion without one is a stack
467        // overflow waiting for an input nobody thought of.
468        const NEAR_ONE: f32 = 1e-3;
469        const MAX_DEPTH: u32 = 6;
470        const RELATIVE_ERROR: f32 = 1e-3;
471        if (weight - 1.0).abs() <= NEAR_ONE || depth >= MAX_DEPTH {
472            self.quad_to(ctrl, to);
473            return;
474        }
475        // The weight is a proxy and this is the thing itself: how far the
476        // quadratic actually lies from the conic. The two are only loosely
477        // related, and the proxy is the expensive way round -- each halving of
478        // the weight's distance from one doubles the output, so a curve gets
479        // subdivided by how hyperbolic it is rather than by how much the
480        // approximation misses.
481        //
482        // Compared at the midpoints, which is where a rational quadratic and
483        // its weight-one counterpart differ most, and against the chord rather
484        // than an absolute length. That is what keeps the bound *relative* --
485        // the property the note above is about, and the reason this can be
486        // decided here rather than during flattening where the scale is known.
487        let conic_mid = (from + ctrl * (2.0 * weight) + to) / (2.0 * (1.0 + weight));
488        let quad_mid = (from + ctrl * 2.0 + to) * 0.25;
489        let chord = (to - from).length();
490        if chord > 0.0 && (conic_mid - quad_mid).length() <= RELATIVE_ERROR * chord {
491            self.quad_to(ctrl, to);
492            return;
493        }
494
495        // De Casteljau for a rational quadratic, at the halfway parameter. The
496        // denominators are the weights the same construction gives the control
497        // points, which is why the midpoint is not the average of three
498        // points.
499        let scale = 1.0 / (1.0 + weight);
500        let left_ctrl = (from + ctrl * weight) * scale;
501        let right_ctrl = (ctrl * weight + to) * scale;
502        let mid = (from + ctrl * (2.0 * weight) + to) * (0.5 * scale);
503        let split_weight = ((1.0 + weight) * 0.5).sqrt();
504
505        self.push_conic(from, left_ctrl, mid, split_weight, depth + 1);
506        self.push_conic(mid, right_ctrl, to, split_weight, depth + 1);
507    }
508
509    pub fn cubic_to(&mut self, c0: Vec2, c1: Vec2, to: Vec2) -> &mut Self {
510        self.ensure_started();
511        self.verbs.push(Verb::CubicTo);
512        self.points.push(c0);
513        self.points.push(c1);
514        self.points.push(to);
515        self.current = Some(to);
516        self
517    }
518
519    /// Append an elliptical arc, centered at `center` with the given radii.
520    ///
521    /// Angles are in radians, measured from the positive X axis toward positive
522    /// Y, and `sweep` may be negative to travel the other way. A sweep of a
523    /// full turn or more is clamped to one: going round twice draws the same
524    /// pixels as going round once, and the extra segments are cost without a
525    /// picture.
526    ///
527    /// If the path has a current point, a line joins it to the arc's start, the
528    /// way SVG and Skia both behave; otherwise the arc begins with a move. That
529    /// is what makes a pie slice `move_to(center)` then `arc(..)` then `close`,
530    /// and a progress ring just `arc(..)` on an empty builder.
531    ///
532    /// # Accuracy
533    ///
534    /// The arc is emitted as cubics of at most a quarter turn each, with
535    /// control points at `(4/3)·tan(θ/4)` of the radius for a segment spanning
536    /// `θ`. That expression is where the familiar 0.5523 comes from — it is
537    /// this evaluated at a right angle — and using the constant for any other
538    /// angle is the usual way to draw an arc that is visibly wrong near its
539    /// ends. At a quarter turn the worst radial error is under three parts in
540    /// ten thousand, so a circle a thousand pixels across is off by less than a
541    /// third of a pixel.
542    pub fn arc(&mut self, center: Vec2, radii: Vec2, start: f32, sweep: f32) -> &mut Self {
543        if !center.is_finite() || !radii.is_finite() || !start.is_finite() || !sweep.is_finite() {
544            return self;
545        }
546        let point_at = |angle: f32| {
547            Vec2::new(
548                center.x + radii.x * angle.cos(),
549                center.y + radii.y * angle.sin(),
550            )
551        };
552        let first = point_at(start);
553        match self.current {
554            Some(_) => {
555                self.line_to(first);
556            }
557            None => {
558                self.move_to(first);
559            }
560        }
561
562        let turn = std::f32::consts::TAU;
563        let sweep = sweep.clamp(-turn, turn);
564        if sweep == 0.0 {
565            return self;
566        }
567        // At most a quarter turn per cubic, which is where the error bound
568        // below holds. More segments would be more accurate and are not needed;
569        // fewer are visibly wrong.
570        let segments = (sweep.abs() / std::f32::consts::FRAC_PI_2).ceil().max(1.0);
571        let step = sweep / segments;
572        // The tangent handles are proportional to the *derivative* at the
573        // endpoints, which for an ellipse carries the radii, so each is scaled
574        // by its own axis rather than by a single radius.
575        let k = (4.0 / 3.0) * (step / 4.0).tan();
576        let mut angle = start;
577        for _ in 0..segments as u32 {
578            let next = angle + step;
579            let (from, to) = (point_at(angle), point_at(next));
580            // The derivative of the parameterization, which is the tangent
581            // direction scaled by the radii.
582            let d_from = Vec2::new(-radii.x * angle.sin(), radii.y * angle.cos());
583            let d_to = Vec2::new(-radii.x * next.sin(), radii.y * next.cos());
584            self.cubic_to(from + d_from * k, to - d_to * k, to);
585            angle = next;
586        }
587        self
588    }
589
590    /// Close the current subpath.
591    ///
592    /// Closing an empty builder is a no-op rather than an error, so callers
593    /// replaying arbitrary path data do not have to special-case it.
594    pub fn close(&mut self) -> &mut Self {
595        if self.verbs.is_empty() {
596            return self;
597        }
598        self.verbs.push(Verb::Close);
599        self.current = self.subpath_start;
600        self
601    }
602
603    /// Current pen position, if the path has started.
604    pub fn current_point(&self) -> Option<Vec2> {
605        self.current
606    }
607
608    fn ensure_started(&mut self) {
609        if self.verbs.is_empty() {
610            self.verbs.push(Verb::MoveTo);
611            self.points.push(Vec2::ZERO);
612            self.subpath_start = Some(Vec2::ZERO);
613            self.current = Some(Vec2::ZERO);
614        }
615    }
616
617    /// Record that what was laid down is this rounded rectangle.
618    ///
619    /// An assertion by the caller, not a check: nothing here reads the verbs
620    /// back to confirm it, because the only caller is the one that just wrote
621    /// them. A builder that marks a shape it did not draw gets that shape drawn
622    /// wherever a route prefers the shape to the outline -- so this is called
623    /// beside the drawing, never afterwards from somewhere that inferred it.
624    ///
625    /// A radius at or below zero is a plain rectangle and is recorded as such.
626    /// Anything not finite records nothing, since it describes no shape.
627    pub fn as_rounded_rect(&mut self, rect: Rect, radius: f32) -> &mut Self {
628        let finite = rect.min.x.is_finite()
629            && rect.min.y.is_finite()
630            && rect.max.x.is_finite()
631            && rect.max.y.is_finite()
632            && radius.is_finite();
633        self.rounded_rect = finite.then(|| (rect, radius.max(0.0)));
634        self
635    }
636
637    pub fn build(self) -> Path {
638        Path {
639            verbs: self.verbs,
640            points: self.points,
641            fill_rule: self.fill_rule,
642            rounded_rect: self.rounded_rect,
643        }
644    }
645}
646
647#[cfg(test)]
648mod tests {
649    use super::*;
650
651    #[test]
652    fn verb_point_counts_match_what_the_builder_pushes() {
653        let mut b = PathBuilder::new();
654        b.move_to(Vec2::ZERO)
655            .line_to(Vec2::new(1.0, 0.0))
656            .quad_to(Vec2::new(2.0, 1.0), Vec2::new(3.0, 0.0))
657            .cubic_to(
658                Vec2::new(4.0, 1.0),
659                Vec2::new(5.0, -1.0),
660                Vec2::new(6.0, 0.0),
661            )
662            .close();
663        let path = b.build();
664
665        let expected: usize = path.verbs().iter().map(|v| v.point_count()).sum();
666        // A mismatch here would desynchronize the parallel buffers and make
667        // every later segment read the wrong points.
668        assert_eq!(expected, path.points().len());
669    }
670
671    #[test]
672    fn segments_walk_verbs_and_points_in_step() {
673        let mut b = PathBuilder::new();
674        b.move_to(Vec2::ZERO)
675            .quad_to(Vec2::new(1.0, 1.0), Vec2::new(2.0, 0.0));
676        let path = b.build();
677
678        let collected: Vec<_> = path.segments().collect();
679        assert_eq!(collected.len(), 2);
680        assert_eq!(collected[0].0, Verb::MoveTo);
681        assert_eq!(collected[0].1, &[Vec2::ZERO]);
682        assert_eq!(collected[1].0, Verb::QuadTo);
683        assert_eq!(collected[1].1, &[Vec2::new(1.0, 1.0), Vec2::new(2.0, 0.0)]);
684    }
685
686    #[test]
687    fn line_without_move_starts_at_origin() {
688        let mut b = PathBuilder::new();
689        b.line_to(Vec2::new(1.0, 1.0));
690        let path = b.build();
691        assert_eq!(path.verbs()[0], Verb::MoveTo);
692        assert_eq!(path.points()[0], Vec2::ZERO);
693    }
694
695    #[test]
696    fn closing_an_empty_builder_is_a_no_op() {
697        let path = {
698            let mut b = PathBuilder::new();
699            b.close();
700            b.build()
701        };
702        assert!(path.is_empty());
703    }
704
705    #[test]
706    fn close_returns_the_pen_to_the_subpath_start() {
707        let mut b = PathBuilder::new();
708        b.move_to(Vec2::new(5.0, 5.0))
709            .line_to(Vec2::new(9.0, 5.0))
710            .close();
711        assert_eq!(b.current_point(), Some(Vec2::new(5.0, 5.0)));
712    }
713
714    #[test]
715    fn bounds_cover_every_control_point() {
716        let mut b = PathBuilder::new();
717        b.move_to(Vec2::new(0.0, 0.0)).cubic_to(
718            Vec2::new(0.0, 10.0),
719            Vec2::new(10.0, 10.0),
720            Vec2::new(10.0, 0.0),
721        );
722        let path = b.build();
723        let bounds = path.bounds();
724        for p in path.points() {
725            assert!(bounds.contains(*p), "bounds must contain {p:?}");
726        }
727        // Control-point hull, not a tight fit: the curve never reaches y=10.
728        assert_eq!(bounds.max.y, 10.0);
729    }
730
731    #[test]
732    fn empty_path_has_empty_bounds() {
733        let path = Path::default();
734        assert!(path.bounds().is_empty());
735        assert_eq!(path.bounds().width(), 0.0);
736    }
737
738    #[test]
739    fn square_is_convex_and_chevron_is_not() {
740        let square = [
741            Vec2::new(0.0, 0.0),
742            Vec2::new(1.0, 0.0),
743            Vec2::new(1.0, 1.0),
744            Vec2::new(0.0, 1.0),
745        ];
746        assert_eq!(polygon_convexity(&square), Convexity::Convex);
747
748        // A chevron reverses turn direction at the notch.
749        let chevron = [
750            Vec2::new(0.0, 0.0),
751            Vec2::new(2.0, 1.0),
752            Vec2::new(4.0, 0.0),
753            Vec2::new(2.0, 4.0),
754        ];
755        assert_eq!(polygon_convexity(&chevron), Convexity::Concave);
756    }
757
758    #[test]
759    fn collinear_points_do_not_break_convexity() {
760        // Generated geometry frequently contains collinear runs; treating a
761        // zero cross product as a reversal would reject valid fan-fill cases.
762        let with_collinear = [
763            Vec2::new(0.0, 0.0),
764            Vec2::new(1.0, 0.0),
765            Vec2::new(2.0, 0.0),
766            Vec2::new(2.0, 2.0),
767            Vec2::new(0.0, 2.0),
768        ];
769        assert_eq!(polygon_convexity(&with_collinear), Convexity::Convex);
770    }
771
772    #[test]
773    fn convexity_is_conservative_about_curves_and_subpaths() {
774        let mut b = PathBuilder::new();
775        b.move_to(Vec2::ZERO)
776            .quad_to(Vec2::new(1.0, 1.0), Vec2::new(2.0, 0.0));
777        // Curves are classified after flattening, so the unflattened answer
778        // errs toward the slow path rather than risking a wrong fast path.
779        assert_eq!(b.build().convexity(), Convexity::Concave);
780
781        let mut b = PathBuilder::new();
782        b.move_to(Vec2::ZERO)
783            .line_to(Vec2::new(1.0, 0.0))
784            .close()
785            .move_to(Vec2::new(5.0, 5.0))
786            .line_to(Vec2::new(6.0, 5.0));
787        assert_eq!(b.build().convexity(), Convexity::Concave);
788    }
789
790    #[test]
791    fn degenerate_polygons_are_trivially_convex() {
792        assert_eq!(polygon_convexity(&[]), Convexity::Convex);
793        assert_eq!(polygon_convexity(&[Vec2::ZERO]), Convexity::Convex);
794        assert_eq!(
795            polygon_convexity(&[Vec2::ZERO, Vec2::new(1.0, 1.0)]),
796            Convexity::Convex
797        );
798    }
799
800    /// Every point on a flattened path, for checking a shape rather than a
801    /// vertex list.
802    fn points_of(path: &Path) -> Vec<Vec2> {
803        crate::flatten::flatten(path, 0.01)
804            .into_iter()
805            .flatten()
806            .collect()
807    }
808
809    #[test]
810    fn an_arc_stays_on_its_circle() {
811        // The property that matters, and the one the familiar constant gets
812        // wrong at any angle but a right one: every point the arc passes
813        // through is at the radius, not merely its ends.
814        for sweep in [
815            std::f32::consts::FRAC_PI_6,
816            std::f32::consts::FRAC_PI_2,
817            2.0,
818            std::f32::consts::PI,
819            -std::f32::consts::PI,
820            std::f32::consts::TAU,
821        ] {
822            let mut builder = PathBuilder::new();
823            builder.arc(Vec2::new(50.0, 50.0), Vec2::splat(40.0), 0.3, sweep);
824            let path = builder.build();
825            let worst = points_of(&path)
826                .iter()
827                .map(|p| ((*p - Vec2::new(50.0, 50.0)).length() - 40.0).abs())
828                .fold(0.0f32, f32::max);
829            assert!(
830                worst < 0.05,
831                "a sweep of {sweep} strays {worst} from a radius of forty"
832            );
833        }
834    }
835
836    #[test]
837    fn an_arc_begins_and_ends_where_it_was_asked_to() {
838        let center = Vec2::new(10.0, 20.0);
839        let radii = Vec2::new(30.0, 30.0);
840        let (start, sweep) = (0.5f32, 1.7f32);
841        let mut builder = PathBuilder::new();
842        builder.arc(center, radii, start, sweep);
843        let points = points_of(&builder.build());
844        let want_first = center + Vec2::new(radii.x * start.cos(), radii.y * start.sin());
845        let end = start + sweep;
846        let want_last = center + Vec2::new(radii.x * end.cos(), radii.y * end.sin());
847        assert!((points[0] - want_first).length() < 0.01, "{:?}", points[0]);
848        assert!(
849            (*points.last().unwrap() - want_last).length() < 0.01,
850            "{:?}",
851            points.last()
852        );
853    }
854
855    #[test]
856    fn a_negative_sweep_travels_the_other_way() {
857        let center = Vec2::ZERO;
858        let arc_of = |sweep: f32| {
859            let mut builder = PathBuilder::new();
860            builder.arc(center, Vec2::splat(10.0), 0.0, sweep);
861            points_of(&builder.build())
862        };
863        // A quarter turn forward passes through positive Y, backward through
864        // negative Y. Same endpoints in X, opposite in Y.
865        let forward = arc_of(std::f32::consts::FRAC_PI_2);
866        let backward = arc_of(-std::f32::consts::FRAC_PI_2);
867        assert!(forward.iter().all(|p| p.y >= -0.01), "forward dipped");
868        assert!(backward.iter().all(|p| p.y <= 0.01), "backward rose");
869    }
870
871    #[test]
872    fn an_elliptical_arc_follows_the_ellipse() {
873        // Different radii, so a circle-shaped approximation would fail this
874        // while passing every test above.
875        let (center, radii) = (Vec2::new(0.0, 0.0), Vec2::new(60.0, 20.0));
876        let mut builder = PathBuilder::new();
877        builder.arc(center, radii, 0.0, std::f32::consts::TAU);
878        let worst = points_of(&builder.build())
879            .iter()
880            .map(|p| {
881                let (x, y) = (p.x / radii.x, p.y / radii.y);
882                (x * x + y * y - 1.0).abs()
883            })
884            .fold(0.0f32, f32::max);
885        assert!(worst < 0.002, "off the ellipse by {worst}");
886    }
887
888    #[test]
889    fn an_arc_after_a_move_is_joined_by_a_line() {
890        // What makes a pie slice: the center, a line out to the arc, the arc,
891        // and a close. Without the joining line the slice would be a chorded
892        // segment with the center left dangling.
893        let mut builder = PathBuilder::new();
894        builder.move_to(Vec2::ZERO);
895        builder.arc(Vec2::ZERO, Vec2::splat(10.0), 0.0, 1.0);
896        builder.close();
897        let path = builder.build();
898        assert_eq!(path.verbs()[0], Verb::MoveTo);
899        assert_eq!(
900            path.verbs()[1],
901            Verb::LineTo,
902            "the arc did not join the current point"
903        );
904        assert!(points_of(&path).iter().any(|p| p.length() < 0.01));
905    }
906
907    #[test]
908    fn an_arc_on_an_empty_builder_starts_with_a_move() {
909        let mut builder = PathBuilder::new();
910        builder.arc(Vec2::ZERO, Vec2::splat(10.0), 0.0, 1.0);
911        assert_eq!(builder.build().verbs()[0], Verb::MoveTo);
912    }
913
914    #[test]
915    fn a_degenerate_arc_adds_no_curve() {
916        // A zero sweep still places the pen, which is what lets a caller emit
917        // one unconditionally in a loop. A non-finite one does nothing at all,
918        // since there is no position it describes.
919        let mut builder = PathBuilder::new();
920        builder.arc(Vec2::ZERO, Vec2::splat(10.0), 0.0, 0.0);
921        let path = builder.build();
922        assert_eq!(path.verbs(), &[Verb::MoveTo]);
923
924        let mut builder = PathBuilder::new();
925        builder.arc(Vec2::ZERO, Vec2::splat(f32::NAN), 0.0, 1.0);
926        assert!(builder.build().is_empty());
927    }
928
929    #[test]
930    fn a_sweep_past_a_full_turn_is_one_turn() {
931        // Round and round draws the same pixels, and the extra segments are
932        // cost without a picture -- but a self-overlapping path also changes
933        // what a nonzero fill rule does, so this is about correctness as well.
934        let count = |sweep| {
935            let mut builder = PathBuilder::new();
936            builder.arc(Vec2::ZERO, Vec2::splat(10.0), 0.0, sweep);
937            builder.build().verbs().len()
938        };
939        assert_eq!(
940            count(std::f32::consts::TAU),
941            count(std::f32::consts::TAU * 3.0)
942        );
943    }
944}
945
946#[cfg(test)]
947mod conic_tests {
948    use super::*;
949
950    fn points_of(path: &Path) -> Vec<Vec2> {
951        crate::flatten::flatten(path, 0.01)
952            .into_iter()
953            .flatten()
954            .collect()
955    }
956
957    #[test]
958    fn a_conic_of_weight_one_is_the_quadratic_it_already_was() {
959        // The identity the whole conversion rests on. If these differ, the
960        // subdivision is not preserving the curve it was given.
961        let mut conic = PathBuilder::new();
962        conic.move_to(Vec2::new(0.0, 0.0)).conic_to(
963            Vec2::new(50.0, 100.0),
964            Vec2::new(100.0, 0.0),
965            1.0,
966        );
967        let mut quad = PathBuilder::new();
968        quad.move_to(Vec2::new(0.0, 0.0))
969            .quad_to(Vec2::new(50.0, 100.0), Vec2::new(100.0, 0.0));
970
971        assert_eq!(points_of(&conic.build()), points_of(&quad.build()));
972    }
973
974    #[test]
975    fn a_conic_of_the_circular_weight_traces_a_circle() {
976        // The reason conics exist. A quadratic cannot be a circular arc and a
977        // cubic can only approximate one; a conic of weight `sqrt(2)/2`, with
978        // its control point at the corner of the square the arc spans, is one
979        // exactly. So every flattened point has to sit on the circle, and how
980        // far any of them strays is the whole error of this conversion.
981        const R: f32 = 100.0;
982        let mut b = PathBuilder::new();
983        b.move_to(Vec2::new(R, 0.0)).conic_to(
984            Vec2::new(R, R),
985            Vec2::new(0.0, R),
986            std::f32::consts::FRAC_1_SQRT_2,
987        );
988        let points = points_of(&b.build());
989        assert!(points.len() > 8, "a quarter turn should not be two lines");
990
991        let worst = points
992            .iter()
993            .map(|p| (p.length() - R).abs())
994            .fold(0.0f32, f32::max);
995        // A hundredth of a pixel on a hundred-pixel radius, which is a part in
996        // ten thousand and below what the flattening tolerance itself permits.
997        assert!(
998            worst < 0.05,
999            "the arc strays {worst} from the circle it is supposed to be"
1000        );
1001    }
1002
1003    #[test]
1004    fn a_weight_that_describes_no_curve_gives_the_line_between_the_ends() {
1005        for weight in [0.0, -1.0, f32::NAN, f32::INFINITY] {
1006            let mut b = PathBuilder::new();
1007            b.move_to(Vec2::new(0.0, 0.0)).conic_to(
1008                Vec2::new(50.0, 100.0),
1009                Vec2::new(100.0, 0.0),
1010                weight,
1011            );
1012            let points = points_of(&b.build());
1013            assert_eq!(
1014                points,
1015                vec![Vec2::new(0.0, 0.0), Vec2::new(100.0, 0.0)],
1016                "a weight of {weight} should give a straight line"
1017            );
1018        }
1019    }
1020
1021    #[test]
1022    fn a_hyperbolic_conic_still_converges_and_stays_inside_its_hull() {
1023        // Weights above one pull the curve toward the control point rather
1024        // than away, and the subdivision has to converge from that side too. A
1025        // rational quadratic never leaves the triangle its three points make,
1026        // whatever the weight, which is what says the conversion did not
1027        // overshoot.
1028        let (a, c, e) = (
1029            Vec2::new(0.0, 0.0),
1030            Vec2::new(50.0, 100.0),
1031            Vec2::new(100.0, 0.0),
1032        );
1033        let mut b = PathBuilder::new();
1034        b.move_to(a).conic_to(c, e, 8.0);
1035        for p in points_of(&b.build()) {
1036            assert!(
1037                p.y >= -0.01 && p.y <= c.y + 0.01 && p.x >= -0.01 && p.x <= e.x + 0.01,
1038                "{p:?} is outside the triangle its own control points make"
1039            );
1040        }
1041    }
1042}