Skip to main content

emblema_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    // Walked over the contour's edges that have length, rather than over its vertices.
304    // A repeated point leaves a zero-length edge, and a zero-length edge turns through
305    // no angle -- so a walk over vertices reads a cross product of zero at the repeat
306    // *and* at the vertex after it, and the turn the contour actually made between the
307    // points either side is never examined. `[(50,70), (0,70), (0,0), (40,70), (40,70)]`
308    // passed as convex that way, and `fan_fill` covered 2100 where the non-zero rule
309    // fills 1400. Found by `property.rs`'s cross-check against the general route.
310    //
311    // Skipping the repeat rather than declining on it, because a contour can carry one
312    // and still be convex -- a rounded rectangle whose radius fills the side has four,
313    // and so does every round superellipse in `cost-baseline.txt`'s scene. Declining
314    // sent those to the general route, which is correct but costs a sweep where a fan
315    // would do. The turn that matters is the one between the edges with length, and
316    // this finds it: `(40,70)` above is where the counterexample doubles back, and
317    // examining that junction is what catches it.
318    for i in 0..n {
319        let a = points[i];
320        let b = points[(i + 1) % n];
321        // This edge has no length, so there is no turn at its far end to judge. The
322        // junction belongs to the last edge that did have length, and that iteration
323        // looked past this repeat to find it.
324        if a == b {
325            continue;
326        }
327        let incoming = b - a;
328        // The next point that is not `b`, which is the far end of the next edge with
329        // length. Only a contour carrying repeats advances this more than once.
330        let mut k = (i + 2) % n;
331        let mut skipped = 0;
332        while points[k] == b && skipped < n {
333            k = (k + 1) % n;
334            skipped += 1;
335        }
336        let c = points[k];
337        if c == b {
338            // Every point is this one. No area, and no turn to disagree about.
339            return Convexity::Convex;
340        }
341        let outgoing = c - b;
342        let cross = incoming.perp_dot(outgoing);
343        let advance = (quadrant(outgoing) - quadrant(incoming)).rem_euclid(4);
344        counter_clockwise += advance;
345        clockwise += (-advance).rem_euclid(4);
346        if cross.abs() <= f32::EPSILON {
347            continue;
348        }
349        let s = if cross > 0.0 { 1 } else { -1 };
350        if sign == 0 {
351            sign = s;
352        } else if sign != s {
353            return Convexity::Concave;
354        }
355    }
356    if sign == 0 {
357        // Every turn was collinear, so the polygon is a segment traversed out
358        // and back. Degenerate rather than self-crossing, and it covers no
359        // area either way.
360        return Convexity::Convex;
361    }
362    let turned = if sign > 0 {
363        counter_clockwise
364    } else {
365        clockwise
366    };
367    if turned > 4 {
368        return Convexity::Concave;
369    }
370    Convexity::Convex
371}
372
373/// Accumulates verbs and points into a [`Path`].
374#[derive(Debug, Clone, Default)]
375pub struct PathBuilder {
376    verbs: Vec<Verb>,
377    points: Vec<Vec2>,
378    fill_rule: FillRule,
379    /// Where the current subpath started, for `close`.
380    subpath_start: Option<Vec2>,
381    current: Option<Vec2>,
382    rounded_rect: Option<(Rect, f32)>,
383}
384
385impl PathBuilder {
386    pub fn new() -> Self {
387        Self::default()
388    }
389
390    pub fn with_fill_rule(mut self, rule: FillRule) -> Self {
391        self.fill_rule = rule;
392        self
393    }
394
395    pub fn move_to(&mut self, p: Vec2) -> &mut Self {
396        self.verbs.push(Verb::MoveTo);
397        self.points.push(p);
398        self.subpath_start = Some(p);
399        self.current = Some(p);
400        self
401    }
402
403    /// Add a line segment.
404    ///
405    /// A line before any `move_to` implicitly starts the subpath at the
406    /// origin, matching how path data from SVG and font outlines behaves when
407    /// it omits the opening move.
408    pub fn line_to(&mut self, p: Vec2) -> &mut Self {
409        self.ensure_started();
410        self.verbs.push(Verb::LineTo);
411        self.points.push(p);
412        self.current = Some(p);
413        self
414    }
415
416    pub fn quad_to(&mut self, ctrl: Vec2, to: Vec2) -> &mut Self {
417        self.ensure_started();
418        self.verbs.push(Verb::QuadTo);
419        self.points.push(ctrl);
420        self.points.push(to);
421        self.current = Some(to);
422        self
423    }
424
425    /// Append a rational quadratic -- a conic -- through one control point.
426    ///
427    /// The curve a circular arc actually is, and the one a font or an SVG
428    /// hands over. `weight` is the control point's pull: one is an ordinary
429    /// quadratic, less than one is elliptical, more is hyperbolic, and
430    /// `sqrt(2)/2` with the control at the corner of a square is exactly a
431    /// quarter circle.
432    ///
433    /// # Why this becomes quadratics here rather than surviving as a verb
434    ///
435    /// A conic of weight one *is* a quadratic, and splitting a conic in half
436    /// moves its weight toward one -- quadratically, so a handful of splits
437    /// puts it within a thousandth. So this subdivides until the weight is
438    /// near enough and emits ordinary quadratics, which every part of this
439    /// crate and the tessellator already understand.
440    ///
441    /// The alternative is a verb of its own, converted during flattening where
442    /// the transform's scale is known. That is what a renderer does when it
443    /// wants an absolute error in device pixels. What is bought here instead
444    /// is a *relative* error: the approximation is a fixed fraction of the
445    /// curve's own size, so it stays correct at every scale the path is later
446    /// drawn at, and nothing downstream has to learn a fifth verb.
447    ///
448    /// # Subdividing by the error rather than by the weight
449    ///
450    /// The criterion below stops when the *error* is small enough, and that is
451    /// worth spelling out because the obvious criterion -- stop when the
452    /// weight is near one -- is what it replaced, and it was expensive by a
453    /// factor of four.
454    ///
455    /// A weight criterion counts halvings of the weight's distance from one,
456    /// and each halving doubles the output. So a curve gets subdivided by how
457    /// hyperbolic it is rather than by how much the approximation misses. A
458    /// rounded superellipse octant emits conics at weights around 0.7 and 8.3;
459    /// under the weight criterion those became thirty-two and sixty-four
460    /// quadratics, and the corpus scene cost 1340 vertices where a circle
461    /// costs 40. It is 305 now, and the stroked one 274 against 922.
462    ///
463    /// The error itself is four lines: a rational quadratic and its weight-one
464    /// counterpart differ most at their midpoints, both midpoints have closed
465    /// forms, and the distance between them measured against the chord is a
466    /// relative bound -- the same quantity the weight criterion was a proxy
467    /// for, at the same tolerance of a thousandth.
468    ///
469    /// It is a little less accurate where the proxy over-subdivided, which is
470    /// most places. Measured against the old criterion: four pixels of the
471    /// filled corpus scene and eight of the stroked one, out of sixteen
472    /// thousand, and all of them at the shape's tangent extremes where the
473    /// outline lies along a pixel boundary and a sub-pixel shift moves every
474    /// sample in the pixel together. Every comparison in the workspace passes
475    /// unchanged, including the pin on upstream's boundary points, which is
476    /// what says the outline is still the shape upstream draws.
477    ///
478    /// A weight that is zero or negative or not finite describes no curve, and
479    /// gives the straight line between the ends.
480    pub fn conic_to(&mut self, ctrl: Vec2, to: Vec2, weight: f32) -> &mut Self {
481        self.ensure_started();
482        let from = self.current.unwrap_or(ctrl);
483        if !weight.is_finite() || weight <= 0.0 || !ctrl.is_finite() || !to.is_finite() {
484            return self.line_to(to);
485        }
486        self.push_conic(from, ctrl, to, weight, 0);
487        self
488    }
489
490    /// Split until the weight is near one, then emit a quadratic.
491    fn push_conic(&mut self, from: Vec2, ctrl: Vec2, to: Vec2, weight: f32, depth: u32) {
492        // A thousandth of the way from one is close enough that the quadratic
493        // and the conic differ by well under a thousandth of the curve's own
494        // extent, which no rasterizer this feeds can show.
495        //
496        // The depth cap is not expected to be reached: the weight halves its
497        // distance from one roughly every split, so even a wildly hyperbolic
498        // conic converges in a handful. It is here because a bound that cannot
499        // be reached costs nothing and a recursion without one is a stack
500        // overflow waiting for an input nobody thought of.
501        const NEAR_ONE: f32 = 1e-3;
502        const MAX_DEPTH: u32 = 6;
503        const RELATIVE_ERROR: f32 = 1e-3;
504        if (weight - 1.0).abs() <= NEAR_ONE || depth >= MAX_DEPTH {
505            self.quad_to(ctrl, to);
506            return;
507        }
508        // The weight is a proxy and this is the thing itself: how far the
509        // quadratic actually lies from the conic. The two are only loosely
510        // related, and the proxy is the expensive way round -- each halving of
511        // the weight's distance from one doubles the output, so a curve gets
512        // subdivided by how hyperbolic it is rather than by how much the
513        // approximation misses.
514        //
515        // Compared at the midpoints, which is where a rational quadratic and
516        // its weight-one counterpart differ most, and against the chord rather
517        // than an absolute length. That is what keeps the bound *relative* --
518        // the property the note above is about, and the reason this can be
519        // decided here rather than during flattening where the scale is known.
520        let conic_mid = (from + ctrl * (2.0 * weight) + to) / (2.0 * (1.0 + weight));
521        let quad_mid = (from + ctrl * 2.0 + to) * 0.25;
522        let chord = (to - from).length();
523        if chord > 0.0 && (conic_mid - quad_mid).length() <= RELATIVE_ERROR * chord {
524            self.quad_to(ctrl, to);
525            return;
526        }
527
528        // De Casteljau for a rational quadratic, at the halfway parameter. The
529        // denominators are the weights the same construction gives the control
530        // points, which is why the midpoint is not the average of three
531        // points.
532        let scale = 1.0 / (1.0 + weight);
533        let left_ctrl = (from + ctrl * weight) * scale;
534        let right_ctrl = (ctrl * weight + to) * scale;
535        let mid = (from + ctrl * (2.0 * weight) + to) * (0.5 * scale);
536        let split_weight = ((1.0 + weight) * 0.5).sqrt();
537
538        self.push_conic(from, left_ctrl, mid, split_weight, depth + 1);
539        self.push_conic(mid, right_ctrl, to, split_weight, depth + 1);
540    }
541
542    pub fn cubic_to(&mut self, c0: Vec2, c1: Vec2, to: Vec2) -> &mut Self {
543        self.ensure_started();
544        self.verbs.push(Verb::CubicTo);
545        self.points.push(c0);
546        self.points.push(c1);
547        self.points.push(to);
548        self.current = Some(to);
549        self
550    }
551
552    /// Append an elliptical arc, centered at `center` with the given radii.
553    ///
554    /// Angles are in radians, measured from the positive X axis toward positive
555    /// Y, and `sweep` may be negative to travel the other way. A sweep of a
556    /// full turn or more is clamped to one: going round twice draws the same
557    /// pixels as going round once, and the extra segments are cost without a
558    /// picture.
559    ///
560    /// If the path has a current point, a line joins it to the arc's start, the
561    /// way SVG and Skia both behave; otherwise the arc begins with a move. That
562    /// is what makes a pie slice `move_to(center)` then `arc(..)` then `close`,
563    /// and a progress ring just `arc(..)` on an empty builder.
564    ///
565    /// # Accuracy
566    ///
567    /// The arc is emitted as cubics of at most a quarter turn each, with
568    /// control points at `(4/3)·tan(θ/4)` of the radius for a segment spanning
569    /// `θ`. That expression is where the familiar 0.5523 comes from — it is
570    /// this evaluated at a right angle — and using the constant for any other
571    /// angle is the usual way to draw an arc that is visibly wrong near its
572    /// ends. At a quarter turn the worst radial error is under three parts in
573    /// ten thousand, so a circle a thousand pixels across is off by less than a
574    /// third of a pixel.
575    pub fn arc(&mut self, center: Vec2, radii: Vec2, start: f32, sweep: f32) -> &mut Self {
576        if !center.is_finite() || !radii.is_finite() || !start.is_finite() || !sweep.is_finite() {
577            return self;
578        }
579        let point_at = |angle: f32| {
580            Vec2::new(
581                center.x + radii.x * angle.cos(),
582                center.y + radii.y * angle.sin(),
583            )
584        };
585        let first = point_at(start);
586        match self.current {
587            Some(_) => {
588                self.line_to(first);
589            }
590            None => {
591                self.move_to(first);
592            }
593        }
594
595        let turn = std::f32::consts::TAU;
596        let sweep = sweep.clamp(-turn, turn);
597        if sweep == 0.0 {
598            return self;
599        }
600        // At most a quarter turn per cubic, which is where the error bound
601        // below holds. More segments would be more accurate and are not needed;
602        // fewer are visibly wrong.
603        let segments = (sweep.abs() / std::f32::consts::FRAC_PI_2).ceil().max(1.0);
604        let step = sweep / segments;
605        // The tangent handles are proportional to the *derivative* at the
606        // endpoints, which for an ellipse carries the radii, so each is scaled
607        // by its own axis rather than by a single radius.
608        let k = (4.0 / 3.0) * (step / 4.0).tan();
609        let mut angle = start;
610        for _ in 0..segments as u32 {
611            let next = angle + step;
612            let (from, to) = (point_at(angle), point_at(next));
613            // The derivative of the parameterization, which is the tangent
614            // direction scaled by the radii.
615            let d_from = Vec2::new(-radii.x * angle.sin(), radii.y * angle.cos());
616            let d_to = Vec2::new(-radii.x * next.sin(), radii.y * next.cos());
617            self.cubic_to(from + d_from * k, to - d_to * k, to);
618            angle = next;
619        }
620        self
621    }
622
623    /// Close the current subpath.
624    ///
625    /// Closing an empty builder is a no-op rather than an error, so callers
626    /// replaying arbitrary path data do not have to special-case it.
627    pub fn close(&mut self) -> &mut Self {
628        if self.verbs.is_empty() {
629            return self;
630        }
631        self.verbs.push(Verb::Close);
632        self.current = self.subpath_start;
633        self
634    }
635
636    /// Current pen position, if the path has started.
637    pub fn current_point(&self) -> Option<Vec2> {
638        self.current
639    }
640
641    fn ensure_started(&mut self) {
642        if self.verbs.is_empty() {
643            self.verbs.push(Verb::MoveTo);
644            self.points.push(Vec2::ZERO);
645            self.subpath_start = Some(Vec2::ZERO);
646            self.current = Some(Vec2::ZERO);
647        }
648    }
649
650    /// Record that what was laid down is this rounded rectangle.
651    ///
652    /// An assertion by the caller, not a check: nothing here reads the verbs
653    /// back to confirm it, because the only caller is the one that just wrote
654    /// them. A builder that marks a shape it did not draw gets that shape drawn
655    /// wherever a route prefers the shape to the outline -- so this is called
656    /// beside the drawing, never afterwards from somewhere that inferred it.
657    ///
658    /// A radius at or below zero is a plain rectangle and is recorded as such.
659    /// Anything not finite records nothing, since it describes no shape.
660    pub fn as_rounded_rect(&mut self, rect: Rect, radius: f32) -> &mut Self {
661        let finite = rect.min.x.is_finite()
662            && rect.min.y.is_finite()
663            && rect.max.x.is_finite()
664            && rect.max.y.is_finite()
665            && radius.is_finite();
666        self.rounded_rect = finite.then(|| (rect, radius.max(0.0)));
667        self
668    }
669
670    pub fn build(self) -> Path {
671        Path {
672            verbs: self.verbs,
673            points: self.points,
674            fill_rule: self.fill_rule,
675            rounded_rect: self.rounded_rect,
676        }
677    }
678}
679
680#[cfg(test)]
681mod tests {
682    use super::*;
683
684    #[test]
685    fn verb_point_counts_match_what_the_builder_pushes() {
686        let mut b = PathBuilder::new();
687        b.move_to(Vec2::ZERO)
688            .line_to(Vec2::new(1.0, 0.0))
689            .quad_to(Vec2::new(2.0, 1.0), Vec2::new(3.0, 0.0))
690            .cubic_to(
691                Vec2::new(4.0, 1.0),
692                Vec2::new(5.0, -1.0),
693                Vec2::new(6.0, 0.0),
694            )
695            .close();
696        let path = b.build();
697
698        let expected: usize = path.verbs().iter().map(|v| v.point_count()).sum();
699        // A mismatch here would desynchronize the parallel buffers and make
700        // every later segment read the wrong points.
701        assert_eq!(expected, path.points().len());
702    }
703
704    #[test]
705    fn segments_walk_verbs_and_points_in_step() {
706        let mut b = PathBuilder::new();
707        b.move_to(Vec2::ZERO)
708            .quad_to(Vec2::new(1.0, 1.0), Vec2::new(2.0, 0.0));
709        let path = b.build();
710
711        let collected: Vec<_> = path.segments().collect();
712        assert_eq!(collected.len(), 2);
713        assert_eq!(collected[0].0, Verb::MoveTo);
714        assert_eq!(collected[0].1, &[Vec2::ZERO]);
715        assert_eq!(collected[1].0, Verb::QuadTo);
716        assert_eq!(collected[1].1, &[Vec2::new(1.0, 1.0), Vec2::new(2.0, 0.0)]);
717    }
718
719    #[test]
720    fn line_without_move_starts_at_origin() {
721        let mut b = PathBuilder::new();
722        b.line_to(Vec2::new(1.0, 1.0));
723        let path = b.build();
724        assert_eq!(path.verbs()[0], Verb::MoveTo);
725        assert_eq!(path.points()[0], Vec2::ZERO);
726    }
727
728    #[test]
729    fn closing_an_empty_builder_is_a_no_op() {
730        let path = {
731            let mut b = PathBuilder::new();
732            b.close();
733            b.build()
734        };
735        assert!(path.is_empty());
736    }
737
738    #[test]
739    fn close_returns_the_pen_to_the_subpath_start() {
740        let mut b = PathBuilder::new();
741        b.move_to(Vec2::new(5.0, 5.0))
742            .line_to(Vec2::new(9.0, 5.0))
743            .close();
744        assert_eq!(b.current_point(), Some(Vec2::new(5.0, 5.0)));
745    }
746
747    #[test]
748    fn bounds_cover_every_control_point() {
749        let mut b = PathBuilder::new();
750        b.move_to(Vec2::new(0.0, 0.0)).cubic_to(
751            Vec2::new(0.0, 10.0),
752            Vec2::new(10.0, 10.0),
753            Vec2::new(10.0, 0.0),
754        );
755        let path = b.build();
756        let bounds = path.bounds();
757        for p in path.points() {
758            assert!(bounds.contains(*p), "bounds must contain {p:?}");
759        }
760        // Control-point hull, not a tight fit: the curve never reaches y=10.
761        assert_eq!(bounds.max.y, 10.0);
762    }
763
764    #[test]
765    fn empty_path_has_empty_bounds() {
766        let path = Path::default();
767        assert!(path.bounds().is_empty());
768        assert_eq!(path.bounds().width(), 0.0);
769    }
770
771    #[test]
772    fn square_is_convex_and_chevron_is_not() {
773        let square = [
774            Vec2::new(0.0, 0.0),
775            Vec2::new(1.0, 0.0),
776            Vec2::new(1.0, 1.0),
777            Vec2::new(0.0, 1.0),
778        ];
779        assert_eq!(polygon_convexity(&square), Convexity::Convex);
780
781        // A chevron reverses turn direction at the notch.
782        let chevron = [
783            Vec2::new(0.0, 0.0),
784            Vec2::new(2.0, 1.0),
785            Vec2::new(4.0, 0.0),
786            Vec2::new(2.0, 4.0),
787        ];
788        assert_eq!(polygon_convexity(&chevron), Convexity::Concave);
789    }
790
791    #[test]
792    fn collinear_points_do_not_break_convexity() {
793        // Generated geometry frequently contains collinear runs; treating a
794        // zero cross product as a reversal would reject valid fan-fill cases.
795        let with_collinear = [
796            Vec2::new(0.0, 0.0),
797            Vec2::new(1.0, 0.0),
798            Vec2::new(2.0, 0.0),
799            Vec2::new(2.0, 2.0),
800            Vec2::new(0.0, 2.0),
801        ];
802        assert_eq!(polygon_convexity(&with_collinear), Convexity::Convex);
803    }
804
805    #[test]
806    fn convexity_is_conservative_about_curves_and_subpaths() {
807        let mut b = PathBuilder::new();
808        b.move_to(Vec2::ZERO)
809            .quad_to(Vec2::new(1.0, 1.0), Vec2::new(2.0, 0.0));
810        // Curves are classified after flattening, so the unflattened answer
811        // errs toward the slow path rather than risking a wrong fast path.
812        assert_eq!(b.build().convexity(), Convexity::Concave);
813
814        let mut b = PathBuilder::new();
815        b.move_to(Vec2::ZERO)
816            .line_to(Vec2::new(1.0, 0.0))
817            .close()
818            .move_to(Vec2::new(5.0, 5.0))
819            .line_to(Vec2::new(6.0, 5.0));
820        assert_eq!(b.build().convexity(), Convexity::Concave);
821    }
822
823    #[test]
824    fn degenerate_polygons_are_trivially_convex() {
825        assert_eq!(polygon_convexity(&[]), Convexity::Convex);
826        assert_eq!(polygon_convexity(&[Vec2::ZERO]), Convexity::Convex);
827        assert_eq!(
828            polygon_convexity(&[Vec2::ZERO, Vec2::new(1.0, 1.0)]),
829            Convexity::Convex
830        );
831    }
832
833    #[test]
834    fn a_repeated_point_does_not_make_a_convex_contour_concave() {
835        // The other half, and the reason the repeat is skipped rather than declined.
836        // Declining was the first fix and it was conservative in the wrong direction: a
837        // rounded rectangle whose radius fills the side carries four repeats and is
838        // convex, as does every round superellipse in `cost-baseline.txt`'s scene, whose
839        // flattened contour ends with its closing point twice -- so stripping one leaves
840        // a zero-length edge at the wrap. Those went to the general route, which fills
841        // them correctly and sweeps where a fan would do.
842        let squared_off = [
843            Vec2::new(0.0, 0.0),
844            Vec2::new(10.0, 0.0),
845            Vec2::new(10.0, 0.0),
846            Vec2::new(10.0, 10.0),
847            Vec2::new(0.0, 10.0),
848            Vec2::new(0.0, 10.0),
849        ];
850        assert_eq!(polygon_convexity(&squared_off), Convexity::Convex);
851
852        // And at the wrap specifically, which is the superellipse's case: the last point
853        // repeats the first after the closing duplicate has been stripped.
854        let repeated_at_the_wrap = [
855            Vec2::new(0.0, 0.0),
856            Vec2::new(10.0, 0.0),
857            Vec2::new(10.0, 10.0),
858            Vec2::new(0.0, 10.0),
859            Vec2::new(0.0, 0.0),
860        ];
861        assert_eq!(polygon_convexity(&repeated_at_the_wrap), Convexity::Convex);
862    }
863
864    #[test]
865    fn a_repeated_point_does_not_hide_a_concavity() {
866        // Found by the generated cross-check in `tests/property.rs`, which caught it
867        // as a wrong fill rather than as a wrong classification: fanned, this contour
868        // covers 2,100 square units against the non-zero rule's 1,400. The repeated
869        // last point leaves a zero-length edge, and a zero-length edge turns through
870        // no angle -- so a convexity test that reads the sign of each turn sees
871        // nothing at that corner and never learns the contour doubled back.
872        //
873        // Both round superellipses in `cost-baseline.txt`'s scene carry such an edge
874        // and were classified convex, so this reached shipped shape code. Their
875        // pictures were right anyway, a fan being correct over a convex contour
876        // whatever degenerate points it carries; this one's is not.
877        let doubled_back = [
878            Vec2::new(50.0, 70.0),
879            Vec2::new(0.0, 70.0),
880            Vec2::new(0.0, 0.0),
881            Vec2::new(40.0, 70.0),
882            Vec2::new(40.0, 70.0),
883        ];
884        assert_eq!(polygon_convexity(&doubled_back), Convexity::Concave);
885    }
886
887    /// Every point on a flattened path, for checking a shape rather than a
888    /// vertex list.
889    fn points_of(path: &Path) -> Vec<Vec2> {
890        crate::flatten::flatten(path, 0.01)
891            .into_iter()
892            .flatten()
893            .collect()
894    }
895
896    #[test]
897    fn an_arc_stays_on_its_circle() {
898        // The property that matters, and the one the familiar constant gets
899        // wrong at any angle but a right one: every point the arc passes
900        // through is at the radius, not merely its ends.
901        for sweep in [
902            std::f32::consts::FRAC_PI_6,
903            std::f32::consts::FRAC_PI_2,
904            2.0,
905            std::f32::consts::PI,
906            -std::f32::consts::PI,
907            std::f32::consts::TAU,
908        ] {
909            let mut builder = PathBuilder::new();
910            builder.arc(Vec2::new(50.0, 50.0), Vec2::splat(40.0), 0.3, sweep);
911            let path = builder.build();
912            let worst = points_of(&path)
913                .iter()
914                .map(|p| ((*p - Vec2::new(50.0, 50.0)).length() - 40.0).abs())
915                .fold(0.0f32, f32::max);
916            assert!(
917                worst < 0.05,
918                "a sweep of {sweep} strays {worst} from a radius of forty"
919            );
920        }
921    }
922
923    #[test]
924    fn an_arc_begins_and_ends_where_it_was_asked_to() {
925        let center = Vec2::new(10.0, 20.0);
926        let radii = Vec2::new(30.0, 30.0);
927        let (start, sweep) = (0.5f32, 1.7f32);
928        let mut builder = PathBuilder::new();
929        builder.arc(center, radii, start, sweep);
930        let points = points_of(&builder.build());
931        let want_first = center + Vec2::new(radii.x * start.cos(), radii.y * start.sin());
932        let end = start + sweep;
933        let want_last = center + Vec2::new(radii.x * end.cos(), radii.y * end.sin());
934        assert!((points[0] - want_first).length() < 0.01, "{:?}", points[0]);
935        assert!(
936            (*points.last().unwrap() - want_last).length() < 0.01,
937            "{:?}",
938            points.last()
939        );
940    }
941
942    #[test]
943    fn a_negative_sweep_travels_the_other_way() {
944        let center = Vec2::ZERO;
945        let arc_of = |sweep: f32| {
946            let mut builder = PathBuilder::new();
947            builder.arc(center, Vec2::splat(10.0), 0.0, sweep);
948            points_of(&builder.build())
949        };
950        // A quarter turn forward passes through positive Y, backward through
951        // negative Y. Same endpoints in X, opposite in Y.
952        let forward = arc_of(std::f32::consts::FRAC_PI_2);
953        let backward = arc_of(-std::f32::consts::FRAC_PI_2);
954        assert!(forward.iter().all(|p| p.y >= -0.01), "forward dipped");
955        assert!(backward.iter().all(|p| p.y <= 0.01), "backward rose");
956    }
957
958    #[test]
959    fn an_elliptical_arc_follows_the_ellipse() {
960        // Different radii, so a circle-shaped approximation would fail this
961        // while passing every test above.
962        let (center, radii) = (Vec2::new(0.0, 0.0), Vec2::new(60.0, 20.0));
963        let mut builder = PathBuilder::new();
964        builder.arc(center, radii, 0.0, std::f32::consts::TAU);
965        let worst = points_of(&builder.build())
966            .iter()
967            .map(|p| {
968                let (x, y) = (p.x / radii.x, p.y / radii.y);
969                (x * x + y * y - 1.0).abs()
970            })
971            .fold(0.0f32, f32::max);
972        assert!(worst < 0.002, "off the ellipse by {worst}");
973    }
974
975    #[test]
976    fn an_arc_after_a_move_is_joined_by_a_line() {
977        // What makes a pie slice: the center, a line out to the arc, the arc,
978        // and a close. Without the joining line the slice would be a chorded
979        // segment with the center left dangling.
980        let mut builder = PathBuilder::new();
981        builder.move_to(Vec2::ZERO);
982        builder.arc(Vec2::ZERO, Vec2::splat(10.0), 0.0, 1.0);
983        builder.close();
984        let path = builder.build();
985        assert_eq!(path.verbs()[0], Verb::MoveTo);
986        assert_eq!(
987            path.verbs()[1],
988            Verb::LineTo,
989            "the arc did not join the current point"
990        );
991        assert!(points_of(&path).iter().any(|p| p.length() < 0.01));
992    }
993
994    #[test]
995    fn an_arc_on_an_empty_builder_starts_with_a_move() {
996        let mut builder = PathBuilder::new();
997        builder.arc(Vec2::ZERO, Vec2::splat(10.0), 0.0, 1.0);
998        assert_eq!(builder.build().verbs()[0], Verb::MoveTo);
999    }
1000
1001    #[test]
1002    fn a_degenerate_arc_adds_no_curve() {
1003        // A zero sweep still places the pen, which is what lets a caller emit
1004        // one unconditionally in a loop. A non-finite one does nothing at all,
1005        // since there is no position it describes.
1006        let mut builder = PathBuilder::new();
1007        builder.arc(Vec2::ZERO, Vec2::splat(10.0), 0.0, 0.0);
1008        let path = builder.build();
1009        assert_eq!(path.verbs(), &[Verb::MoveTo]);
1010
1011        let mut builder = PathBuilder::new();
1012        builder.arc(Vec2::ZERO, Vec2::splat(f32::NAN), 0.0, 1.0);
1013        assert!(builder.build().is_empty());
1014    }
1015
1016    #[test]
1017    fn a_sweep_past_a_full_turn_is_one_turn() {
1018        // Round and round draws the same pixels, and the extra segments are
1019        // cost without a picture -- but a self-overlapping path also changes
1020        // what a nonzero fill rule does, so this is about correctness as well.
1021        let count = |sweep| {
1022            let mut builder = PathBuilder::new();
1023            builder.arc(Vec2::ZERO, Vec2::splat(10.0), 0.0, sweep);
1024            builder.build().verbs().len()
1025        };
1026        assert_eq!(
1027            count(std::f32::consts::TAU),
1028            count(std::f32::consts::TAU * 3.0)
1029        );
1030    }
1031}
1032
1033#[cfg(test)]
1034mod conic_tests {
1035    use super::*;
1036
1037    fn points_of(path: &Path) -> Vec<Vec2> {
1038        crate::flatten::flatten(path, 0.01)
1039            .into_iter()
1040            .flatten()
1041            .collect()
1042    }
1043
1044    #[test]
1045    fn a_conic_of_weight_one_is_the_quadratic_it_already_was() {
1046        // The identity the whole conversion rests on. If these differ, the
1047        // subdivision is not preserving the curve it was given.
1048        let mut conic = PathBuilder::new();
1049        conic.move_to(Vec2::new(0.0, 0.0)).conic_to(
1050            Vec2::new(50.0, 100.0),
1051            Vec2::new(100.0, 0.0),
1052            1.0,
1053        );
1054        let mut quad = PathBuilder::new();
1055        quad.move_to(Vec2::new(0.0, 0.0))
1056            .quad_to(Vec2::new(50.0, 100.0), Vec2::new(100.0, 0.0));
1057
1058        assert_eq!(points_of(&conic.build()), points_of(&quad.build()));
1059    }
1060
1061    #[test]
1062    fn a_conic_of_the_circular_weight_traces_a_circle() {
1063        // The reason conics exist. A quadratic cannot be a circular arc and a
1064        // cubic can only approximate one; a conic of weight `sqrt(2)/2`, with
1065        // its control point at the corner of the square the arc spans, is one
1066        // exactly. So every flattened point has to sit on the circle, and how
1067        // far any of them strays is the whole error of this conversion.
1068        const R: f32 = 100.0;
1069        let mut b = PathBuilder::new();
1070        b.move_to(Vec2::new(R, 0.0)).conic_to(
1071            Vec2::new(R, R),
1072            Vec2::new(0.0, R),
1073            std::f32::consts::FRAC_1_SQRT_2,
1074        );
1075        let points = points_of(&b.build());
1076        assert!(points.len() > 8, "a quarter turn should not be two lines");
1077
1078        let worst = points
1079            .iter()
1080            .map(|p| (p.length() - R).abs())
1081            .fold(0.0f32, f32::max);
1082        // A hundredth of a pixel on a hundred-pixel radius, which is a part in
1083        // ten thousand and below what the flattening tolerance itself permits.
1084        assert!(
1085            worst < 0.05,
1086            "the arc strays {worst} from the circle it is supposed to be"
1087        );
1088    }
1089
1090    #[test]
1091    fn a_weight_that_describes_no_curve_gives_the_line_between_the_ends() {
1092        for weight in [0.0, -1.0, f32::NAN, f32::INFINITY] {
1093            let mut b = PathBuilder::new();
1094            b.move_to(Vec2::new(0.0, 0.0)).conic_to(
1095                Vec2::new(50.0, 100.0),
1096                Vec2::new(100.0, 0.0),
1097                weight,
1098            );
1099            let points = points_of(&b.build());
1100            assert_eq!(
1101                points,
1102                vec![Vec2::new(0.0, 0.0), Vec2::new(100.0, 0.0)],
1103                "a weight of {weight} should give a straight line"
1104            );
1105        }
1106    }
1107
1108    #[test]
1109    fn a_hyperbolic_conic_still_converges_and_stays_inside_its_hull() {
1110        // Weights above one pull the curve toward the control point rather
1111        // than away, and the subdivision has to converge from that side too. A
1112        // rational quadratic never leaves the triangle its three points make,
1113        // whatever the weight, which is what says the conversion did not
1114        // overshoot.
1115        let (a, c, e) = (
1116            Vec2::new(0.0, 0.0),
1117            Vec2::new(50.0, 100.0),
1118            Vec2::new(100.0, 0.0),
1119        );
1120        let mut b = PathBuilder::new();
1121        b.move_to(a).conic_to(c, e, 8.0);
1122        for p in points_of(&b.build()) {
1123            assert!(
1124                p.y >= -0.01 && p.y <= c.y + 0.01 && p.x >= -0.01 && p.x <= e.x + 0.01,
1125                "{p:?} is outside the triangle its own control points make"
1126            );
1127        }
1128    }
1129}