Skip to main content

valo_geometry/
path.rs

1use std::sync::Arc;
2
3use crate::{Matrix, Point, Rect};
4
5/// `FillRule` determines which regions of overlapping contours are filled.
6#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
7#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
8pub enum FillRule {
9    /// `NonZero` fills regions whose signed winding count is nonzero.
10    #[default]
11    NonZero,
12    /// `EvenOdd` fills regions crossed an odd number of times.
13    EvenOdd,
14}
15
16/// `Winding` selects the traversal direction of a closed contour.
17///
18/// Direction matters wherever traversal carries meaning: under the nonzero fill
19/// rule two overlapping contours cancel when their windings oppose and add
20/// when they agree, and dashing walks a contour in order.
21#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
22#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
23pub enum Winding {
24    /// `Clockwise` traverses in the clockwise direction on Valo's y-down plane.
25    #[default]
26    Clockwise,
27    /// `CounterClockwise` traverses in the counterclockwise direction.
28    CounterClockwise,
29}
30
31// Serialize ONLY (the serde feature is a debug dump): a deserializer would
32// let malformed verb/point/weight counts reach flatten() and index out of
33// bounds.
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35#[cfg_attr(feature = "serde", derive(serde::Serialize))]
36enum Verb {
37    Move,
38    Line,
39    Quad,
40    /// A rational quadratic: control point, end point, and a weight drawn
41    /// from the side channel `weights`. Weights are strictly positive and
42    /// finite — `conic_to` lowers everything else.
43    Conic,
44    Cubic,
45    Close,
46}
47
48/// `Contour` is one path contour flattened into a polyline.
49///
50/// Closed contours repeat their first point at the end so measurement and
51/// dashing include the closing edge. Closure remains explicit metadata rather
52/// than being inferred from coincident endpoints.
53#[derive(Clone, Debug, PartialEq)]
54pub struct Contour {
55    /// `points` contains the flattened polyline in traversal order.
56    pub points: Vec<Point>,
57    /// `closed` indicates whether the source contour ended with `close`.
58    pub closed: bool,
59    /// `has_segments` distinguishes drawn zero-length contours from a lone move.
60    ///
61    /// A close or explicit zero-length segment counts; a bare `move_to` does not.
62    pub has_segments: bool,
63}
64
65/// `PathElement` is one step of a [`Path`], as [`Path::elements`] walks it.
66///
67/// Points are absolute. A `Conic` carries its weight; feed the elements to a
68/// [`PathBuilder`] in order to rebuild an equal path.
69#[derive(Clone, Copy, Debug, PartialEq)]
70pub enum PathElement {
71    MoveTo(Point),
72    LineTo(Point),
73    QuadTo(Point, Point),
74    ConicTo(Point, Point, f32),
75    CubicTo(Point, Point, Point),
76    Close,
77}
78
79/// `Path` is an immutable collection of line and Bézier contours.
80///
81/// Build paths with [`PathBuilder`]. Display lists retain shared [`Arc`] handles,
82/// so recording and nesting do not copy path data.
83#[derive(Clone, Debug)]
84#[cfg_attr(feature = "serde", derive(serde::Serialize))]
85pub struct Path {
86    verbs: Vec<Verb>,
87    points: Vec<Point>,
88    /// One entry per `Verb::Conic`, consumed in verb order. Positive weights
89    /// keep the curve inside its control hull, so `bounds` stays sound.
90    weights: Vec<f32>,
91    /// Control-point bounds: conservative (curves stay inside their hull),
92    /// which is exactly what the record-time oracle wants.
93    bounds: Rect,
94}
95
96impl Path {
97    /// `elements` walks the path's verbs with their points, for a consumer that
98    /// serializes or re-encodes it rather than flattens it.
99    pub fn elements(&self) -> impl Iterator<Item = PathElement> + '_ {
100        let mut point = 0;
101        let mut weight = 0;
102        self.verbs.iter().map(move |verb| {
103            let element = match verb {
104                Verb::Move => PathElement::MoveTo(self.points[point]),
105                Verb::Line => PathElement::LineTo(self.points[point]),
106                Verb::Quad => PathElement::QuadTo(self.points[point], self.points[point + 1]),
107                Verb::Conic => {
108                    let element = PathElement::ConicTo(
109                        self.points[point],
110                        self.points[point + 1],
111                        self.weights[weight],
112                    );
113                    weight += 1;
114                    element
115                }
116                Verb::Cubic => PathElement::CubicTo(
117                    self.points[point],
118                    self.points[point + 1],
119                    self.points[point + 2],
120                ),
121                Verb::Close => PathElement::Close,
122            };
123            point += match verb {
124                Verb::Move | Verb::Line => 1,
125                Verb::Quad | Verb::Conic => 2,
126                Verb::Cubic => 3,
127                Verb::Close => 0,
128            };
129            element
130        })
131    }
132
133    /// `bounds` returns conservative control-point bounds.
134    pub fn bounds(&self) -> Rect {
135        self.bounds
136    }
137
138    /// `tight_bounds` returns exact axis-aligned curve bounds.
139    pub fn tight_bounds(&self) -> Rect {
140        let mut bounds = TightBounds::default();
141        let mut point_index = 0usize;
142        let mut weight_index = 0usize;
143        let mut cursor = Point::ZERO;
144        let mut contour_start = Point::ZERO;
145        for verb in &self.verbs {
146            match verb {
147                Verb::Move => {
148                    cursor = self.points[point_index];
149                    contour_start = cursor;
150                    point_index += 1;
151                    bounds.include(cursor);
152                }
153                Verb::Line => {
154                    cursor = self.points[point_index];
155                    point_index += 1;
156                    bounds.include(cursor);
157                }
158                Verb::Quad => {
159                    let control = self.points[point_index];
160                    let end = self.points[point_index + 1];
161                    point_index += 2;
162                    include_quadratic_extrema(&mut bounds, cursor, control, end);
163                    cursor = end;
164                }
165                Verb::Conic => {
166                    let control = self.points[point_index];
167                    let end = self.points[point_index + 1];
168                    let weight = self.weights[weight_index];
169                    point_index += 2;
170                    weight_index += 1;
171                    include_conic_extrema(&mut bounds, cursor, control, end, weight);
172                    cursor = end;
173                }
174                Verb::Cubic => {
175                    let first = self.points[point_index];
176                    let second = self.points[point_index + 1];
177                    let end = self.points[point_index + 2];
178                    point_index += 3;
179                    include_cubic_extrema(&mut bounds, cursor, first, second, end);
180                    cursor = end;
181                }
182                Verb::Close => {
183                    cursor = contour_start;
184                    bounds.include(cursor);
185                }
186            }
187        }
188        bounds.rect()
189    }
190
191    /// `is_empty` reports whether the path contains no commands.
192    pub fn is_empty(&self) -> bool {
193        self.verbs.is_empty()
194    }
195
196    /// `heap_bytes` returns an estimate of owned path storage.
197    pub fn heap_bytes(&self) -> usize {
198        self.points.len() * std::mem::size_of::<Point>()
199            + self.weights.len() * std::mem::size_of::<f32>()
200            + self.verbs.len()
201    }
202
203    /// `contains` reports whether a point lies inside the filled path.
204    ///
205    /// The query evaluates the original curves, implicitly closes open contours,
206    /// and treats points on the outline as inside.
207    pub fn contains(&self, point: Point, fill_rule: FillRule) -> bool {
208        if !self.bounds.contains_inclusive(point) {
209            return false;
210        }
211        let crossings = self.walk_crossings(point);
212        match fill_rule {
213            FillRule::NonZero => crossings.is_inside_non_zero(),
214            FillRule::EvenOdd => crossings.is_inside_even_odd(),
215        }
216    }
217
218    /// Ray-cast the whole path, one segment at a time.
219    fn walk_crossings(&self, point: Point) -> crate::winding::Crossings {
220        let mut crossings = crate::winding::Crossings::default();
221        let mut index = 0usize;
222        let mut weight_index = 0usize;
223        let mut cursor = Point::ZERO;
224        let mut contour_start = Point::ZERO;
225        let mut contour_open = false;
226        for verb in &self.verbs {
227            match verb {
228                Verb::Move => {
229                    // A new contour closes the previous one: fills always see
230                    // that last→first edge, whether or not Close was recorded.
231                    if contour_open {
232                        crossings.line(cursor, contour_start, point);
233                    }
234                    contour_open = true;
235                    contour_start = self.points[index];
236                    cursor = contour_start;
237                    index += 1;
238                }
239                Verb::Line => {
240                    crossings.line(cursor, self.points[index], point);
241                    cursor = self.points[index];
242                    index += 1;
243                }
244                Verb::Quad => {
245                    crossings.quad(cursor, self.points[index], self.points[index + 1], point);
246                    cursor = self.points[index + 1];
247                    index += 2;
248                }
249                Verb::Conic => {
250                    crossings.conic(
251                        cursor,
252                        self.points[index],
253                        self.points[index + 1],
254                        self.weights[weight_index],
255                        point,
256                    );
257                    cursor = self.points[index + 1];
258                    index += 2;
259                    weight_index += 1;
260                }
261                Verb::Cubic => {
262                    crossings.cubic(
263                        cursor,
264                        self.points[index],
265                        self.points[index + 1],
266                        self.points[index + 2],
267                        point,
268                    );
269                    cursor = self.points[index + 2];
270                    index += 3;
271                }
272                Verb::Close => {
273                    crossings.line(cursor, contour_start, point);
274                    cursor = contour_start;
275                    contour_open = false;
276                }
277            }
278        }
279        if contour_open {
280            crossings.line(cursor, contour_start, point);
281        }
282        crossings
283    }
284
285    /// `measure` returns an arc-length measurement for each nonempty contour.
286    ///
287    /// `tolerance` is the maximum flattening deviation in path coordinates.
288    pub fn measure(&self, tolerance: f32) -> Vec<crate::ContourMeasure> {
289        self.flatten(tolerance)
290            .iter()
291            .filter_map(crate::ContourMeasure::of)
292            .collect()
293    }
294
295    /// `flatten` approximates curves with polygonal contours.
296    ///
297    /// `tolerance` is the maximum deviation in path coordinates. Fill
298    /// operations implicitly close every contour; stroke operations use
299    /// [`Contour::closed`].
300    pub fn flatten(&self, tolerance: f32) -> Vec<Contour> {
301        let mut out = Flattener::new(tolerance.max(1e-4));
302        let mut i = 0usize;
303        let mut weight_index = 0usize;
304        for verb in &self.verbs {
305            match verb {
306                Verb::Move => {
307                    out.move_to(self.points[i]);
308                    i += 1;
309                }
310                Verb::Line => {
311                    out.line_to(self.points[i]);
312                    i += 1;
313                }
314                Verb::Quad => {
315                    out.quad_to(self.points[i], self.points[i + 1]);
316                    i += 2;
317                }
318                Verb::Conic => {
319                    out.conic_to(
320                        self.points[i],
321                        self.points[i + 1],
322                        self.weights[weight_index],
323                    );
324                    i += 2;
325                    weight_index += 1;
326                }
327                Verb::Cubic => {
328                    out.cubic_to(self.points[i], self.points[i + 1], self.points[i + 2]);
329                    i += 3;
330                }
331                Verb::Close => out.close(),
332            }
333        }
334        out.finish()
335    }
336}
337
338#[derive(Default)]
339struct TightBounds(Option<(f32, f32, f32, f32)>);
340
341impl TightBounds {
342    fn include(&mut self, point: Point) {
343        self.0 = Some(match self.0 {
344            Some((left, top, right, bottom)) => (
345                left.min(point.x),
346                top.min(point.y),
347                right.max(point.x),
348                bottom.max(point.y),
349            ),
350            None => (point.x, point.y, point.x, point.y),
351        });
352    }
353
354    fn rect(self) -> Rect {
355        self.0
356            .map_or_else(Rect::default, |(left, top, right, bottom)| {
357                Rect::from_ltrb(left, top, right, bottom)
358            })
359    }
360}
361
362fn include_quadratic_extrema(bounds: &mut TightBounds, start: Point, control: Point, end: Point) {
363    bounds.include(start);
364    bounds.include(end);
365    for (start_axis, control_axis, end_axis) in
366        [(start.x, control.x, end.x), (start.y, control.y, end.y)]
367    {
368        let denominator = start_axis as f64 - 2.0 * control_axis as f64 + end_axis as f64;
369        if denominator == 0.0 {
370            continue;
371        }
372        let parameter = ((start_axis as f64 - control_axis as f64) / denominator) as f32;
373        if parameter > 0.0 && parameter < 1.0 {
374            bounds.include(eval_quad(start, control, end, parameter));
375        }
376    }
377}
378
379fn include_cubic_extrema(
380    bounds: &mut TightBounds,
381    start: Point,
382    first: Point,
383    second: Point,
384    end: Point,
385) {
386    bounds.include(start);
387    bounds.include(end);
388    for (start_axis, first_axis, second_axis, end_axis) in [
389        (start.x, first.x, second.x, end.x),
390        (start.y, first.y, second.y, end.y),
391    ] {
392        for parameter in cubic_extrema(start_axis, first_axis, second_axis, end_axis)
393            .into_iter()
394            .flatten()
395        {
396            if parameter > 0.0 && parameter < 1.0 {
397                bounds.include(eval_cubic(start, first, second, end, parameter));
398            }
399        }
400    }
401}
402
403fn include_conic_extrema(
404    bounds: &mut TightBounds,
405    start: Point,
406    control: Point,
407    end: Point,
408    weight: f32,
409) {
410    bounds.include(start);
411    bounds.include(end);
412    for (start_axis, control_axis, end_axis) in
413        [(start.x, control.x, end.x), (start.y, control.y, end.y)]
414    {
415        for parameter in conic_derivative_roots(start_axis, control_axis, end_axis, weight) {
416            let p = eval_conic(start, control, end, weight, parameter);
417            // A positive-weight conic never leaves its control hull; a root
418            // that lands a float ulp inside an endpoint must not let eval
419            // noise widen the bounds past it.
420            let clamp = |v: f32, a: f32, b: f32, c: f32| v.clamp(a.min(b).min(c), a.max(b).max(c));
421            bounds.include(Point::new(
422                clamp(p.x, start.x, control.x, end.x),
423                clamp(p.y, start.y, control.y, end.y),
424            ));
425        }
426    }
427}
428
429/// Parameters in (0, 1) where one coordinate of the conic turns around.
430///
431/// With numerator `N(t) = At² + Bt + C` and denominator `D(t) = at² − at + 1`
432/// (`a = 2 − 2w`), the extremum condition `N′D − ND′ = 0` collapses — the
433/// cubic terms cancel — to the quadratic `−a(A+B)t² + 2(A − aC)t + (B + aC)`.
434pub(crate) fn conic_derivative_roots(
435    start: f32,
436    control: f32,
437    end: f32,
438    weight: f32,
439) -> impl Iterator<Item = f32> {
440    // In point DIFFERENCES, Skia's `conic_find_extrema` form: substituting
441    // p10 = control − start and p20 = end − start collapses the quadratic
442    // above (divided by 2) to the coefficients below. The absolute-coordinate
443    // form subtracts near-equal products and loses every significant digit
444    // for curves far from the origin; the difference form does not, and f64
445    // covers what remains.
446    let w = weight as f64;
447    let p10 = control as f64 - start as f64;
448    let p20 = end as f64 - start as f64;
449    let quadratic = (w - 1.0) * p20;
450    let linear = p20 - 2.0 * w * p10;
451    let constant = w * p10;
452
453    let mut roots = [None, None];
454    if quadratic == 0.0 {
455        roots[0] = unit_root(-constant, linear);
456    } else {
457        let discriminant = linear * linear - 4.0 * quadratic * constant;
458        if discriminant >= 0.0 && discriminant.is_finite() {
459            // The same stable pairing as `cubic_extrema`: never subtract two
460            // near-equal numbers.
461            let root = discriminant.sqrt();
462            let q = -0.5 * (linear + root.copysign(linear));
463            roots[0] = unit_root(q, quadratic);
464            roots[1] = unit_root(constant, q).filter(|value| roots[0] != Some(*value));
465        }
466    }
467    roots.into_iter().flatten()
468}
469
470/// The conic (rational quadratic) point at `t`.
471pub(crate) fn eval_conic(start: Point, control: Point, end: Point, weight: f32, t: f32) -> Point {
472    let u = 1.0 - t;
473    let b0 = u * u;
474    let b1 = 2.0 * weight * u * t;
475    let b2 = t * t;
476    let d = b0 + b1 + b2;
477    Point::new(
478        (b0 * start.x + b1 * control.x + b2 * end.x) / d,
479        (b0 * start.y + b1 * control.y + b2 * end.y) / d,
480    )
481}
482
483fn cubic_extrema(start: f32, first: f32, second: f32, end: f32) -> [Option<f32>; 2] {
484    let start = start as f64;
485    let first = first as f64;
486    let second = second as f64;
487    let end = end as f64;
488    let quadratic = -start + 3.0 * first - 3.0 * second + end;
489    let linear = 2.0 * (start - 2.0 * first + second);
490    let constant = first - start;
491    if quadratic == 0.0 {
492        return [unit_root(-constant, linear), None];
493    }
494    let discriminant = linear * linear - 4.0 * quadratic * constant;
495    if discriminant < 0.0 || !discriminant.is_finite() {
496        return [None, None];
497    }
498
499    // Numerical Recipes / Skia: Q/A and C/Q avoid the cancellation in the
500    // ordinary (-B ± sqrt(D)) / 2A formula when one root is much smaller.
501    let root = discriminant.sqrt();
502    let q = -0.5 * (linear + root.copysign(linear));
503    let first_root = unit_root(q, quadratic);
504    let second_root = unit_root(constant, q).filter(|value| Some(*value) != first_root);
505    [first_root, second_root]
506}
507
508fn unit_root(numerator: f64, denominator: f64) -> Option<f32> {
509    if denominator == 0.0 {
510        return None;
511    }
512    let value = numerator / denominator;
513    (value.is_finite() && value > 0.0 && value < 1.0).then_some(value as f32)
514}
515
516/// `PathBuilder` records commands used to create an immutable [`Path`].
517#[derive(Clone, Default)]
518pub struct PathBuilder {
519    verbs: Vec<Verb>,
520    points: Vec<Point>,
521    weights: Vec<f32>,
522    bounds: Option<Rect>,
523    /// Where a segment recorded after a `close` resumes.
524    ///
525    /// It OUTLIVES the close — that is the whole point. Without it
526    /// `M10,10 L30,10 Z L30,30` loses its diagonal, because the line would
527    /// start at its own destination. Skia does the same in `ensureMove`
528    /// (`moveTo(fPts[fLastMoveIndex])` when the last verb was a close), and
529    /// Impeller inherits it by building on `SkPathBuilder`.
530    ///
531    /// NOT always the contour's origin, which is why it is not called that.
532    /// For `close` it is. For `rect` and `roundRect` WHATWG names the point
533    /// separately — "create a new subpath with the point (x, y)" — and for a
534    /// rounded rectangle `(x, y)` is a bounding-box corner the outline never
535    /// touches, since the walk begins at the top-left tangent. The two
536    /// coincide only at radius zero.
537    resume_point: Option<Point>,
538    contour_open: bool,
539}
540
541impl PathBuilder {
542    /// `new` creates an empty path builder.
543    pub fn new() -> Self {
544        Self::default()
545    }
546
547    /// `move_to` starts a new contour at `p`.
548    pub fn move_to(&mut self, p: impl Into<Point>) -> &mut Self {
549        let p = p.into();
550        self.verbs.push(Verb::Move);
551        self.push_point(p);
552        self.resume_point = Some(p);
553        self.contour_open = true;
554        self
555    }
556
557    /// `line_to` adds a straight segment to `p`.
558    pub fn line_to(&mut self, p: impl Into<Point>) -> &mut Self {
559        let p = p.into();
560        self.ensure_contour(p);
561        self.verbs.push(Verb::Line);
562        self.push_point(p);
563        self
564    }
565
566    /// `quad_to` adds a quadratic Bézier through control point `c` to `p`.
567    pub fn quad_to(&mut self, c: impl Into<Point>, p: impl Into<Point>) -> &mut Self {
568        let (c, p) = (c.into(), p.into());
569        self.ensure_contour(c);
570        self.verbs.push(Verb::Quad);
571        self.push_point(c);
572        self.push_point(p);
573        self
574    }
575
576    /// `conic_to` adds a rational quadratic (a conic) through control point
577    /// `c` to `p`.
578    ///
579    /// A weight of 1 is an ordinary quadratic; `cos(θ/2)` traces a circular
580    /// arc of sweep `θ` exactly, which is what the arc and rounded-rect
581    /// helpers rely on. Stored weights are restricted to positive finite
582    /// values so every curve stays inside its control hull — the invariant
583    /// the record-time bounds rely on. Everything else lowers: a nonpositive
584    /// weight draws a line, a non-finite weight the control polygon (the
585    /// curve's limit as the weight grows). Skia, by contrast, records
586    /// negative weights as-is; nothing in valo's builders or Canvas2D
587    /// produces one.
588    pub fn conic_to(&mut self, c: impl Into<Point>, p: impl Into<Point>, weight: f32) -> &mut Self {
589        let (c, p) = (c.into(), p.into());
590        if !weight.is_finite() {
591            return self.line_to(c).line_to(p);
592        }
593        if weight <= 0.0 {
594            return self.line_to(p);
595        }
596        if weight == 1.0 {
597            return self.quad_to(c, p);
598        }
599        self.ensure_contour(c);
600        self.verbs.push(Verb::Conic);
601        self.push_point(c);
602        self.push_point(p);
603        self.weights.push(weight);
604        self
605    }
606
607    /// `cubic_to` adds a cubic Bézier through two control points to `p`.
608    pub fn cubic_to(
609        &mut self,
610        c1: impl Into<Point>,
611        c2: impl Into<Point>,
612        p: impl Into<Point>,
613    ) -> &mut Self {
614        let (c1, c2, p) = (c1.into(), c2.into(), p.into());
615        self.ensure_contour(c1);
616        self.verbs.push(Verb::Cubic);
617        self.push_point(c1);
618        self.push_point(c2);
619        self.push_point(p);
620        self
621    }
622
623    /// `close` adds a segment back to the current contour's starting point.
624    ///
625    /// It has no effect when no contour is open.
626    pub fn close(&mut self) -> &mut Self {
627        if self.contour_open {
628            self.verbs.push(Verb::Close);
629            self.contour_open = false;
630        }
631        self
632    }
633
634    // ── shape helpers (the common vocabulary) ──────────────────────────────
635
636    /// `rect` adds a closed rectangular contour.
637    pub fn rect(&mut self, r: Rect) -> &mut Self {
638        self.move_to((r.x, r.y))
639            .line_to((r.right(), r.y))
640            .line_to((r.right(), r.bottom()))
641            .line_to((r.x, r.bottom()))
642            .close();
643        // WHATWG's separate closing step: "create a new subpath with the
644        // point (x, y)". Stated here rather than inherited from the traversal
645        // above, so reordering the walk cannot move it.
646        self.resume_point = Some(Point::new(r.x, r.y));
647        self
648    }
649
650    /// `rrect` adds a closed rounded rectangle with one corner radius.
651    pub fn rrect(&mut self, r: Rect, radius: f32) -> &mut Self {
652        self.rrect_radii(r, [radius; 4])
653    }
654
655    /// `rrect_radii` adds a rounded rectangle with circular corner radii.
656    ///
657    /// `radii` is ordered clockwise from the top-left.
658    pub fn rrect_radii(&mut self, r: Rect, radii: [f32; 4]) -> &mut Self {
659        self.rrect_radii_elliptical(r, radii.map(|radius| [radius; 2]))
660    }
661
662    /// `rrect_radii_elliptical` adds per-corner elliptical radii.
663    ///
664    /// Each corner is `[x_radius, y_radius]`, starting at the top-left. Radii
665    /// are proportionally reduced when adjacent corners would overlap.
666    pub fn rrect_radii_elliptical(
667        &mut self,
668        r: impl Into<Rect>,
669        radii: [[f32; 2]; 4],
670    ) -> &mut Self {
671        self.rrect_radii_elliptical_wound(r, radii, Winding::Clockwise)
672    }
673
674    /// `rrect_radii_elliptical_wound` adds a rounded rectangle with explicit winding.
675    ///
676    /// Opposing contours cancel under [`FillRule::NonZero`], and dashing follows
677    /// this traversal order.
678    pub fn rrect_radii_elliptical_wound(
679        &mut self,
680        r: impl Into<Rect>,
681        radii: [[f32; 2]; 4],
682        winding: Winding,
683    ) -> &mut Self {
684        let r = r.into();
685        let [tl, tr, br, bl] = constrain_radii_elliptical(&r, radii);
686        let (l, t, rr, b) = (r.x, r.y, r.right(), r.bottom());
687        if [tl, tr, br, bl].iter().all(|[x, y]| *x == 0.0 && *y == 0.0) {
688            match winding {
689                Winding::Clockwise => self.rect(r),
690                Winding::CounterClockwise => self
691                    .move_to((l, t))
692                    .line_to((l, b))
693                    .line_to((rr, b))
694                    .line_to((rr, t))
695                    .close(),
696            };
697            self.resume_point = Some(Point::new(l, t));
698            return self;
699        }
700        // Each corner is one conic: a quarter ELLIPSE, exactly — the control
701        // point is the box corner and the weight is cos(45°). Impeller's
702        // rounded rect uses the identical construction (`round_rect.cc`).
703        let w = std::f32::consts::FRAC_1_SQRT_2;
704        match winding {
705            Winding::Clockwise => self
706                .move_to((l + tl[0], t))
707                .line_to((rr - tr[0], t))
708                .conic_to((rr, t), (rr, t + tr[1]), w)
709                .line_to((rr, b - br[1]))
710                .conic_to((rr, b), (rr - br[0], b), w)
711                .line_to((l + bl[0], b))
712                .conic_to((l, b), (l, b - bl[1]), w)
713                .line_to((l, t + tl[1]))
714                .conic_to((l, t), (l + tl[0], t), w)
715                .close(),
716            // The same anchors in reverse around the same control points —
717            // so the two directions are the identical outline and differ
718            // only in traversal.
719            Winding::CounterClockwise => self
720                .move_to((l + tl[0], t))
721                .conic_to((l, t), (l, t + tl[1]), w)
722                .line_to((l, b - bl[1]))
723                .conic_to((l, b), (l + bl[0], b), w)
724                .line_to((rr - br[0], b))
725                .conic_to((rr, b), (rr, b - br[1]), w)
726                .line_to((rr, t + tr[1]))
727                .conic_to((rr, t), (rr - tr[0], t), w)
728                .line_to((l + tl[0], t))
729                .close(),
730        };
731        // WHATWG step 14, SEPARATE from the outline that step 12 walks:
732        // "create a new subpath with the point (x, y)". For a rounded
733        // rectangle that corner is not on the outline at all — the walk
734        // begins at the top-left tangent — so this cannot be inherited from
735        // the traversal the way `close`'s resumption point is. Blink does the
736        // same explicitly, chaining `.MoveTo(x, y)` after its rounded-rect
737        // builder (`canvas_path.cc`).
738        //
739        // Verified against the spec text and current Blink source rather than
740        // by probing a browser. This corner of Canvas2D has already produced
741        // two places where the prose and every implementation disagree, so
742        // that distinction is worth keeping in view.
743        self.resume_point = Some(Point::new(l, t));
744        self
745    }
746
747    /// `rsuperellipse` adds a rounded superellipse with one corner radius.
748    ///
749    /// The corner profile is the iOS "squircle": circular near the diagonal,
750    /// blending into the straight edges along a superellipse instead of
751    /// meeting them abruptly the way a circular [`Self::rrect`] corner does.
752    pub fn rsuperellipse(&mut self, r: Rect, radius: f32) -> &mut Self {
753        self.rsuperellipse_radii(r, [[radius; 2]; 4])
754    }
755
756    /// `rsuperellipse_radii` adds a rounded superellipse with per-corner
757    /// elliptical radii.
758    ///
759    /// Each corner is `[x_radius, y_radius]`, clockwise from the top-left.
760    /// Radii are proportionally reduced when adjacent corners would overlap.
761    pub fn rsuperellipse_radii(&mut self, r: Rect, radii: [[f32; 2]; 4]) -> &mut Self {
762        crate::superellipse::RoundSuperellipse::new(r, radii).emit(self);
763        self.resume_point = Some(Point::new(r.x, r.y));
764        self
765    }
766
767    /// `arc` adds a circular arc.
768    ///
769    /// Angles are radians clockwise from +x in Valo's y-down coordinates.
770    /// Sweeps are limited to one full turn.
771    pub fn arc(
772        &mut self,
773        center: impl Into<Point>,
774        radius: f32,
775        start_angle: f32,
776        sweep_angle: f32,
777    ) -> &mut Self {
778        self.ellipse(center, [radius; 2], 0.0, start_angle, sweep_angle)
779    }
780
781    /// `ellipse` adds an elliptical arc.
782    ///
783    /// `radii` are the x and y half-extents, `x_axis_rotation` turns the
784    /// ellipse, and angles are radians clockwise from +x. An active contour is
785    /// connected to the arc's first point. Sweeps are limited to one full turn.
786    ///
787    /// Non-finite input is ignored. Negative radii trigger a debug assertion
788    /// and are ignored in release builds.
789    pub fn ellipse(
790        &mut self,
791        center: impl Into<Point>,
792        radii: [f32; 2],
793        x_axis_rotation: f32,
794        start_angle: f32,
795        sweep_angle: f32,
796    ) -> &mut Self {
797        let center = center.into();
798        let [radius_x, radius_y] = radii;
799        // Every input, not just the radii: a NaN centre flows into NaN points,
800        // and `f32::min`/`max` drop those from the bounds accumulator without
801        // complaint — an under-reported box silently breaks culling and
802        // hit-testing later.
803        let finite = center.x.is_finite()
804            && center.y.is_finite()
805            && radius_x.is_finite()
806            && radius_y.is_finite()
807            && x_axis_rotation.is_finite()
808            && start_angle.is_finite()
809            && sweep_angle.is_finite();
810        debug_assert!(
811            radius_x >= 0.0 && radius_y >= 0.0,
812            "negative radii draw nothing; Canvas2D throws here"
813        );
814        if !finite || radius_x < 0.0 || radius_y < 0.0 {
815            return self;
816        }
817
818        // Canvas2D stops at one full turn, and Skia routes full sweeps to an
819        // oval. Without this, `sweep = 1e20` passes the finite check above and
820        // asks for ~1e19 cubic pieces — an allocation the process does not
821        // survive, reachable by any embedder forwarding user input.
822        let full_turn = std::f32::consts::TAU;
823        let sweep_angle = sweep_angle.clamp(-full_turn, full_turn);
824
825        let unit_circle_to_ellipse = unit_circle_map(center, radii, x_axis_rotation);
826        let first = unit_circle_to_ellipse.map_point(unit_circle_point(start_angle));
827        // Canvas2D runs a straight line in to the arc's start when a contour
828        // is live. A CLOSED contour still counts as live for this: it resumes
829        // at its origin and then runs the line, so an arc after `closePath`
830        // stays connected to the seam. Only a path with no contour at all
831        // starts at the arc.
832        if self.contour_open || self.resume_point.is_some() {
833            self.ensure_contour(first);
834            self.line_to(first);
835        } else {
836            self.move_to(first);
837        }
838        if sweep_angle != 0.0 {
839            self.push_arc_conics(&unit_circle_to_ellipse, start_angle, sweep_angle);
840        }
841        // A whole turn ends where it began: close it, so the stroker joins the
842        // seam instead of capping it (Skia's full sweeps produce a closed oval).
843        if sweep_angle.abs() >= full_turn {
844            self.close();
845        }
846        self
847    }
848
849    /// `arc_to` rounds the corner between the current point, `corner`, and `next`.
850    ///
851    /// Zero or negative radius, coincident points, and straight-through corners
852    /// fall back to a line ending at `corner`.
853    pub fn arc_to(
854        &mut self,
855        corner: impl Into<Point>,
856        next: impl Into<Point>,
857        radius: f32,
858    ) -> &mut Self {
859        let (corner, next) = (corner.into(), next.into());
860        self.ensure_contour(corner);
861        let start = *self.points.last().expect("ensure_contour opened a contour");
862
863        // Skia's construction, in f64: the tangent length follows from the
864        // half-angle at the corner, and the centre sits one radius along the
865        // inward normal of the incoming edge.
866        let incoming = normalize(
867            corner.x as f64 - start.x as f64,
868            corner.y as f64 - start.y as f64,
869        );
870        let outgoing = normalize(
871            next.x as f64 - corner.x as f64,
872            next.y as f64 - corner.y as f64,
873        );
874        let (Some(incoming), Some(outgoing)) = (incoming, outgoing) else {
875            return self.line_to(corner);
876        };
877        let cosine = incoming.0 * outgoing.0 + incoming.1 * outgoing.1;
878        let sine = incoming.0 * outgoing.1 - incoming.1 * outgoing.0;
879        if radius <= 0.0 || !radius.is_finite() || sine.abs() < 1.0 / (1 << 12) as f64 {
880            return self.line_to(corner);
881        }
882
883        let tangent_length = (radius as f64 * (1.0 - cosine) / sine).abs();
884        let entry = Point::new(
885            corner.x - (tangent_length * incoming.0) as f32,
886            corner.y - (tangent_length * incoming.1) as f32,
887        );
888        // The turn's sign puts the centre on the side the arc bends toward.
889        let turn = sine.signum() as f32;
890        let center = Point::new(
891            entry.x + radius * turn * -(incoming.1 as f32),
892            entry.y + radius * turn * incoming.0 as f32,
893        );
894        let exit = Point::new(
895            corner.x + (tangent_length * outgoing.0) as f32,
896            corner.y + (tangent_length * outgoing.1) as f32,
897        );
898
899        let start_angle = (entry.y - center.y).atan2(entry.x - center.x);
900        let end_angle = (exit.y - center.y).atan2(exit.x - center.x);
901        let sweep = shortest_sweep(start_angle, end_angle, turn);
902
903        self.line_to(entry);
904        let map = unit_circle_map(center, [radius; 2], 0.0);
905        self.push_arc_conics(&map, start_angle, sweep);
906        self
907    }
908
909    /// `circle` adds a closed circular contour.
910    ///
911    /// Four conic quarters trace the circle exactly; the control points are
912    /// the bounding box's corners.
913    pub fn circle(&mut self, center: impl Into<Point>, radius: f32) -> &mut Self {
914        let c = center.into();
915        let r = radius;
916        let w = std::f32::consts::FRAC_1_SQRT_2;
917        self.move_to((c.x + r, c.y))
918            .conic_to((c.x + r, c.y + r), (c.x, c.y + r), w)
919            .conic_to((c.x - r, c.y + r), (c.x - r, c.y), w)
920            .conic_to((c.x - r, c.y - r), (c.x, c.y - r), w)
921            .conic_to((c.x + r, c.y - r), (c.x + r, c.y), w)
922            .close()
923    }
924
925    /// `append` adds a transformed copy of another path.
926    ///
927    /// The appended path's final contour becomes the current contour for
928    /// subsequent commands.
929    pub fn append(&mut self, path: &Path, transform: &Matrix) -> &mut Self {
930        if path.verbs.is_empty() {
931            // Appending nothing must change nothing — in particular it must
932            // not close this builder's open contour.
933            return self;
934        }
935        let mut point = path.points.iter();
936        let mut weight = path.weights.iter();
937        let mut cursor = Point::ZERO;
938        let mut contour_start = Point::ZERO;
939        for verb in &path.verbs {
940            let count = match verb {
941                Verb::Move | Verb::Line => 1,
942                Verb::Quad | Verb::Conic => 2,
943                Verb::Cubic => 3,
944                Verb::Close => 0,
945            };
946            self.verbs.push(*verb);
947            for _ in 0..count {
948                let Some(&p) = point.next() else {
949                    return self;
950                };
951                cursor = transform.map_point(p);
952                self.push_point(cursor);
953            }
954            if matches!(verb, Verb::Conic) {
955                // Weights survive affine transforms unchanged. (A projective
956                // transform would need to reweigh them — but it would also
957                // need more than control-point mapping for quads and cubics,
958                // so every verb shares that limitation.)
959                let Some(&w) = weight.next() else {
960                    return self;
961                };
962                self.weights.push(w);
963            }
964            match verb {
965                Verb::Move => contour_start = cursor,
966                Verb::Close => cursor = contour_start,
967                _ => {}
968            }
969        }
970        // WHATWG's `Path2D.addPath` ends by "creating a new subpath with the
971        // last point in path", which is what lets a following `line_to`
972        // continue from where the source stopped. A source ending mid-contour
973        // already leaves this builder there; one ending in `close` does not,
974        // and without the reopen the next segment would start at its own
975        // endpoint and the connecting edge would vanish.
976        //
977        // The reopen leaves a lone-point contour when nothing follows. That
978        // costs no pixels here: it sits exactly on the closed contour's seam,
979        // which the fill and the stroke's join already cover.
980        if matches!(path.verbs.last(), Some(Verb::Close)) {
981            self.move_to(cursor);
982        } else {
983            // The appended contour is this builder's contour now, origin and
984            // all — leaving the receiver's own origin in place would send a
985            // later `close` + segment back to the wrong seam.
986            self.resume_point = Some(contour_start);
987            self.contour_open = true;
988        }
989        self
990    }
991
992    /// `build` consumes the builder and returns a shared immutable path.
993    pub fn build(self) -> Arc<Path> {
994        Arc::new(Path {
995            verbs: self.verbs,
996            points: self.points,
997            weights: self.weights,
998            bounds: self.bounds.unwrap_or_default(),
999        })
1000    }
1001
1002    // ── internals ──────────────────────────────────────────────────────────
1003
1004    /// `push_arc_conics` traces an arc with conic pieces of at most 90°.
1005    ///
1006    /// Each piece is exact: a conic of weight `cos(θ/2)` whose control point
1007    /// sits where the endpoint tangents meet IS the circular arc of sweep
1008    /// `θ`. The current point must already be at the arc's start.
1009    fn push_arc_conics(&mut self, map: &Matrix, start_angle: f32, sweep_angle: f32) {
1010        let piece_count = (sweep_angle.abs() / std::f32::consts::FRAC_PI_2)
1011            .ceil()
1012            .max(1.0);
1013        let step = sweep_angle / piece_count;
1014        let weight = (step / 2.0).cos();
1015
1016        let mut angle = start_angle;
1017        for _ in 0..piece_count as u32 {
1018            let mid = unit_circle_point(angle + step / 2.0);
1019            let control = Point::new(mid.x / weight, mid.y / weight);
1020            let to = unit_circle_point(angle + step);
1021            self.conic_to(map.map_point(control), map.map_point(to), weight);
1022            angle += step;
1023        }
1024    }
1025
1026    /// `ensure_contour` guarantees an open contour before recording a segment.
1027    ///
1028    /// After a `close` the path resumes at the CLOSED contour's origin — the
1029    /// spec's "new subpath with the last point", and Skia's `ensureMove`.
1030    /// Resuming at the incoming point instead silently deletes the segment
1031    /// from the seam, which is the whole bug this exists to prevent.
1032    ///
1033    /// A path that never had a contour starts at the incoming point: Skia's
1034    /// implicit `moveTo(0, 0)` there is a footgun valo does not copy.
1035    fn ensure_contour(&mut self, p: Point) {
1036        if self.contour_open {
1037            return;
1038        }
1039        self.move_to(self.resume_point.unwrap_or(p));
1040    }
1041
1042    fn push_point(&mut self, p: Point) {
1043        self.points.push(p);
1044        // Plain min/max accumulation — a zero-size seed rect is a valid
1045        // bound, wherever it sits (a rect-union "empty = identity" rule
1046        // would drop a first point at the origin).
1047        self.bounds = Some(match self.bounds {
1048            Some(b) => Rect::from_ltrb(
1049                b.x.min(p.x),
1050                b.y.min(p.y),
1051                b.right().max(p.x),
1052                b.bottom().max(p.y),
1053            ),
1054            None => Rect::new(p.x, p.y, 0.0, 0.0),
1055        });
1056    }
1057}
1058
1059/// `unit_circle_point` returns the point at `angle` on the unit circle.
1060fn unit_circle_point(angle: f32) -> Point {
1061    let (sine, cosine) = angle.sin_cos();
1062    Point::new(cosine, sine)
1063}
1064
1065/// `unit_circle_map` creates the transform from a unit circle to an ellipse.
1066fn unit_circle_map(center: Point, radii: [f32; 2], rotation: f32) -> Matrix {
1067    let [radius_x, radius_y] = radii;
1068    let (sine, cosine) = rotation.sin_cos();
1069    Matrix::from_affine(
1070        radius_x * cosine,
1071        radius_x * sine,
1072        -radius_y * sine,
1073        radius_y * cosine,
1074        center.x,
1075        center.y,
1076    )
1077}
1078
1079/// `shortest_sweep` returns the sub-turn sweep matching `direction`.
1080fn shortest_sweep(start: f32, end: f32, direction: f32) -> f32 {
1081    let mut sweep = end - start;
1082    let turn = std::f32::consts::TAU;
1083    while sweep > 0.0 && direction < 0.0 {
1084        sweep -= turn;
1085    }
1086    while sweep < 0.0 && direction > 0.0 {
1087        sweep += turn;
1088    }
1089    sweep
1090}
1091
1092/// `normalize` returns a unit vector or `None` when no finite direction exists.
1093fn normalize(x: f64, y: f64) -> Option<(f64, f64)> {
1094    let length = (x * x + y * y).sqrt();
1095    (length.is_finite() && length > 0.0).then(|| (x / length, y / length))
1096}
1097
1098/// `constrain_radii` proportionally reduces circular radii to fit a rectangle.
1099///
1100/// Radii are ordered clockwise from the top-left. Negative values become zero.
1101pub fn constrain_radii(r: &Rect, radii: [f32; 4]) -> [f32; 4] {
1102    constrain_radii_elliptical(r, radii.map(|v| [v; 2])).map(|[x, _]| x)
1103}
1104
1105/// `constrain_radii_elliptical` proportionally reduces elliptical radii to fit.
1106///
1107/// Corners are ordered clockwise from the top-left as `[x_radius, y_radius]`.
1108/// Negative components become zero.
1109pub fn constrain_radii_elliptical(r: &Rect, radii: [[f32; 2]; 4]) -> [[f32; 2]; 4] {
1110    let [tl, tr, br, bl] = radii.map(|[x, y]| [x.max(0.0), y.max(0.0)]);
1111    let fit = |side: f32, a: f32, b: f32| if a + b <= side { 1.0 } else { side / (a + b) };
1112    let f = fit(r.width, tl[0], tr[0])
1113        .min(fit(r.width, bl[0], br[0]))
1114        .min(fit(r.height, tl[1], bl[1]))
1115        .min(fit(r.height, tr[1], br[1]));
1116    [tl, tr, br, bl].map(|[x, y]| [x * f, y * f])
1117}
1118
1119/// `Flattener` approximates curves with uniformly parameterized line segments.
1120struct Flattener {
1121    tolerance: f32,
1122    contours: Vec<Contour>,
1123    current: Vec<Point>,
1124    /// Whether a segment verb has landed since the last `move_to`.
1125    has_segments: bool,
1126}
1127
1128impl Flattener {
1129    fn new(tolerance: f32) -> Self {
1130        Self {
1131            tolerance,
1132            contours: Vec::new(),
1133            current: Vec::new(),
1134            has_segments: false,
1135        }
1136    }
1137
1138    fn move_to(&mut self, p: Point) {
1139        self.flush(false);
1140        self.current.push(p);
1141        self.has_segments = false;
1142    }
1143
1144    fn line_to(&mut self, p: Point) {
1145        self.current.push(p);
1146        self.has_segments = true;
1147    }
1148
1149    fn quad_to(&mut self, c: Point, p: Point) {
1150        let Some(&start) = self.current.last() else {
1151            return;
1152        };
1153        self.has_segments = true;
1154        let dev = second_difference(start, c, p);
1155        let n = segment_count((dev / (8.0 * self.tolerance)).sqrt());
1156        for i in 1..=n {
1157            let t = i as f32 / n as f32;
1158            self.current.push(eval_quad(start, c, p, t));
1159        }
1160    }
1161
1162    fn conic_to(&mut self, c: Point, p: Point, w: f32) {
1163        let Some(&start) = self.current.last() else {
1164            return;
1165        };
1166        self.has_segments = true;
1167        let n = segment_count(conic_segment_estimate(start, c, p, w, self.tolerance));
1168        for i in 1..=n {
1169            let t = i as f32 / n as f32;
1170            self.current.push(eval_conic(start, c, p, w, t));
1171        }
1172    }
1173
1174    fn cubic_to(&mut self, c1: Point, c2: Point, p: Point) {
1175        let Some(&start) = self.current.last() else {
1176            return;
1177        };
1178        self.has_segments = true;
1179        let dev = second_difference(start, c1, c2).max(second_difference(c1, c2, p));
1180        let n = segment_count((3.0 * dev / (4.0 * self.tolerance)).sqrt());
1181        for i in 1..=n {
1182            let t = i as f32 / n as f32;
1183            self.current.push(eval_cubic(start, c1, c2, p, t));
1184        }
1185    }
1186
1187    fn close(&mut self) {
1188        // Emit the closing edge back to the contour's start (unless the
1189        // last curve already landed there exactly).
1190        if let (Some(&first), Some(&last)) = (self.current.first(), self.current.last()) {
1191            if self.current.len() >= 2 && (first.x, first.y) != (last.x, last.y) {
1192                self.current.push(first);
1193            }
1194        }
1195        // Closing is itself a drawing command: `move_to(p)` then `close()` is
1196        // an explicit zero-length SUBPATH, which strokes exactly like an
1197        // explicit zero-length segment. Impeller says so directly — its
1198        // `Close()` calls `SegmentEncountered()` — and Skia turns move+close
1199        // into a zero-length line for every non-butt cap.
1200        if !self.current.is_empty() {
1201            self.has_segments = true;
1202        }
1203        self.flush(true);
1204    }
1205
1206    fn finish(mut self) -> Vec<Contour> {
1207        self.flush(false);
1208        self.contours
1209    }
1210
1211    /// `flush` keeps every contour, even lone points.
1212    ///
1213    /// Fills fan nothing from <3 points,
1214    /// but the stroker draws 2-point lines and caps EXPLICIT zero-length
1215    /// subpaths. A move-only contour is kept too, carrying `has_segments:
1216    /// false` so the stroker can tell the two apart.
1217    fn flush(&mut self, closed: bool) {
1218        if !self.current.is_empty() {
1219            self.contours.push(Contour {
1220                points: std::mem::take(&mut self.current),
1221                closed,
1222                has_segments: self.has_segments,
1223            });
1224        }
1225        self.has_segments = false;
1226    }
1227}
1228
1229/// Wang's formula extended to conics — the same estimate Impeller and Skia's
1230/// GPU tessellators use ("Wang's formula for rational quadratics", Skia
1231/// `wangs_formula::conic`), with `precision = 1 / tolerance`.
1232fn conic_segment_estimate(start: Point, control: Point, end: Point, w: f32, tolerance: f32) -> f32 {
1233    let precision = 1.0 / tolerance;
1234    // Recentring on the bounding box improves translation invariance.
1235    let cx = 0.5 * (start.x.min(control.x).min(end.x) + start.x.max(control.x).max(end.x));
1236    let cy = 0.5 * (start.y.min(control.y).min(end.y) + start.y.max(control.y).max(end.y));
1237    let (p0x, p0y) = (start.x - cx, start.y - cy);
1238    let (p1x, p1y) = (control.x - cx, control.y - cy);
1239    let (p2x, p2y) = (end.x - cx, end.y - cy);
1240    let max_len_sq = (p0x * p0x + p0y * p0y)
1241        .max(p1x * p1x + p1y * p1y)
1242        .max(p2x * p2x + p2y * p2y);
1243    let (dpx, dpy) = (p0x - 2.0 * w * p1x + p2x, p0y - 2.0 * w * p1y + p2y);
1244    let dw = (2.0 - 2.0 * w).abs();
1245    let rp_minus_1 = (max_len_sq.sqrt() * precision - 1.0).max(0.0);
1246    let numer = (dpx * dpx + dpy * dpy).sqrt() * precision + rp_minus_1 * dw;
1247    let denom = 4.0 * w.min(1.0);
1248    (numer / denom).sqrt()
1249}
1250
1251fn second_difference(a: Point, b: Point, c: Point) -> f32 {
1252    let dx = a.x - 2.0 * b.x + c.x;
1253    let dy = a.y - 2.0 * b.y + c.y;
1254    (dx * dx + dy * dy).sqrt()
1255}
1256
1257fn segment_count(estimate: f32) -> u32 {
1258    (estimate.ceil() as u32).clamp(1, 64)
1259}
1260
1261fn eval_quad(p0: Point, c: Point, p1: Point, t: f32) -> Point {
1262    let u = 1.0 - t;
1263    Point::new(
1264        u * u * p0.x + 2.0 * u * t * c.x + t * t * p1.x,
1265        u * u * p0.y + 2.0 * u * t * c.y + t * t * p1.y,
1266    )
1267}
1268
1269fn eval_cubic(p0: Point, c1: Point, c2: Point, p1: Point, t: f32) -> Point {
1270    let u = 1.0 - t;
1271    let (uu, tt) = (u * u, t * t);
1272    Point::new(
1273        u * uu * p0.x + 3.0 * uu * t * c1.x + 3.0 * u * tt * c2.x + t * tt * p1.x,
1274        u * uu * p0.y + 3.0 * uu * t * c1.y + 3.0 * u * tt * c2.y + t * tt * p1.y,
1275    )
1276}
1277
1278/// `local_tolerance` returns local curve tolerance for quarter-pixel device error.
1279pub fn local_tolerance(transform: &Matrix) -> f32 {
1280    0.25 / transform.max_scale().max(1e-3)
1281}
1282
1283#[cfg(test)]
1284mod tests {
1285    use super::*;
1286
1287    /// The observable meaning of winding: under the NON-ZERO rule two
1288    /// overlapping contours cancel when their directions oppose and reinforce
1289    /// when they agree. This is the property Chrome exhibits for a
1290    /// `roundRect` given with a negative width, and the reason normalizing
1291    /// the box without carrying the direction is wrong — the second rectangle
1292    /// would add instead of subtract.
1293    #[test]
1294    fn opposed_windings_cancel_under_the_non_zero_rule() {
1295        let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
1296        let radii = [[12.0, 12.0]; 4];
1297        let inside = Point::new(50.0, 50.0);
1298
1299        let mut opposed = PathBuilder::new();
1300        opposed.rrect_radii_elliptical_wound(rect, radii, Winding::Clockwise);
1301        opposed.rrect_radii_elliptical_wound(rect, radii, Winding::CounterClockwise);
1302        assert!(
1303            !opposed.build().contains(inside, FillRule::NonZero),
1304            "opposed windings must cancel"
1305        );
1306
1307        let mut agreeing = PathBuilder::new();
1308        agreeing.rrect_radii_elliptical_wound(rect, radii, Winding::Clockwise);
1309        agreeing.rrect_radii_elliptical_wound(rect, radii, Winding::Clockwise);
1310        assert!(
1311            agreeing.build().contains(inside, FillRule::NonZero),
1312            "agreeing windings must reinforce"
1313        );
1314    }
1315
1316    /// Direction must not move the OUTLINE, only the traversal. A reversed
1317    /// corner whose control points were not swapped with it would bulge the
1318    /// wrong way and show up here.
1319    #[test]
1320    fn winding_reverses_the_walk_without_moving_the_outline() {
1321        let rect = Rect::new(10.0, 20.0, 80.0, 60.0);
1322        let radii = [[8.0, 14.0], [4.0, 4.0], [20.0, 6.0], [0.0, 0.0]];
1323        let wound = |winding| {
1324            let mut path = PathBuilder::new();
1325            path.rrect_radii_elliptical_wound(rect, radii, winding);
1326            path.build()
1327        };
1328        let clockwise = wound(Winding::Clockwise);
1329        let counter = wound(Winding::CounterClockwise);
1330        assert_eq!(clockwise.tight_bounds(), counter.tight_bounds());
1331        // Sample across the shape, including just inside and outside each
1332        // rounded corner.
1333        for point in [
1334            Point::new(50.0, 50.0),
1335            Point::new(14.0, 30.0),
1336            Point::new(86.0, 24.0),
1337            Point::new(74.0, 76.0),
1338            Point::new(12.0, 78.0),
1339            Point::new(5.0, 15.0),
1340            Point::new(95.0, 85.0),
1341        ] {
1342            assert_eq!(
1343                clockwise.contains(point, FillRule::NonZero),
1344                counter.contains(point, FillRule::NonZero),
1345                "the two directions disagree about {point:?}"
1346            );
1347        }
1348    }
1349
1350    /// WHATWG leaves a one-point subpath at the closed contour's origin, so a
1351    /// segment recorded after `closePath` starts from the SEAM.
1352    ///
1353    /// The bug this pins is invisible to any test that paints immediately
1354    /// after the close — the closed shape looks right and the missing
1355    /// diagonal is a segment that was never recorded at all. That is exactly
1356    /// why the conformance fuzzer never caught it.
1357    #[test]
1358    fn a_segment_after_close_resumes_at_the_contour_origin() {
1359        let mut path = PathBuilder::new();
1360        path.move_to((10.0, 10.0));
1361        path.line_to((30.0, 10.0));
1362        path.close();
1363        path.line_to((30.0, 30.0));
1364        let path = path.build();
1365
1366        // The diagonal runs (10,10) → (30,30); its midpoint is (20,20).
1367        let contours = path.flatten(0.05);
1368        let resumed = contours.last().expect("the path continues after the close");
1369        assert_eq!(
1370            resumed.points.first().copied(),
1371            Some(Point::new(10.0, 10.0)),
1372            "the segment after close must start at the contour origin, not its own end"
1373        );
1374        assert!(crate::stroke_contains(
1375            &contours,
1376            &crate::Stroke::new(6.0),
1377            0.05,
1378            Point::new(20.0, 20.0)
1379        ));
1380    }
1381
1382    /// `rect` and `roundRect` resume at `(x, y)` — the bounding box's corner,
1383    /// which is a SEPARATE spec step from the outline they walk.
1384    ///
1385    /// For a rounded rectangle that corner is not on the outline at all: the
1386    /// walk starts at the top-left tangent, `(18, 10)` here. The two points
1387    /// coincide only at radius zero, which is exactly why a rect-only test
1388    /// would miss this.
1389    #[test]
1390    fn a_segment_after_a_shape_helper_resumes_at_the_box_corner() {
1391        let box_corner = Point::new(10.0, 10.0);
1392        for corner in [0.0f32, 8.0] {
1393            let mut path = PathBuilder::new();
1394            if corner == 0.0 {
1395                path.rect(Rect::new(10.0, 10.0, 40.0, 40.0));
1396            } else {
1397                path.rrect_radii_elliptical(Rect::new(10.0, 10.0, 40.0, 40.0), [[corner; 2]; 4]);
1398            }
1399            path.line_to((90.0, 90.0));
1400
1401            let contours = path.build().flatten(0.05);
1402            let resumed = contours.last().expect("the path continues after the shape");
1403            assert_eq!(
1404                resumed.points.first().copied(),
1405                Some(box_corner),
1406                "corner radius {corner}: the trailing segment starts at (x, y)"
1407            );
1408        }
1409
1410        // The same thing said in ink, which is how the divergence was found:
1411        // the diagonal from (10,10) is stroked and the one from the tangent
1412        // (18,10) is not.
1413        let mut path = PathBuilder::new();
1414        path.rrect_radii_elliptical(Rect::new(10.0, 10.0, 40.0, 40.0), [[8.0; 2]; 4]);
1415        path.line_to((90.0, 90.0));
1416        let contours = path.build().flatten(0.05);
1417        let stroke = crate::Stroke::new(4.0);
1418        assert!(
1419            crate::stroke_contains(&contours, &stroke, 0.05, Point::new(50.0, 50.0)),
1420            "the diagonal from (10,10) must be stroked"
1421        );
1422        assert!(
1423            !crate::stroke_contains(&contours, &stroke, 0.05, Point::new(54.0, 50.0)),
1424            "the diagonal from the tangent (18,10) must not be"
1425        );
1426    }
1427
1428    /// `closePath` keeps the contour-origin rule — the shape helpers' `(x, y)`
1429    /// override must not have leaked into it.
1430    #[test]
1431    fn close_still_resumes_at_the_contour_origin() {
1432        let mut path = PathBuilder::new();
1433        path.move_to((10.0, 10.0));
1434        path.line_to((30.0, 10.0));
1435        path.line_to((30.0, 30.0));
1436        path.close();
1437        path.line_to((90.0, 90.0));
1438        let contours = path.build().flatten(0.05);
1439        assert_eq!(
1440            contours
1441                .last()
1442                .and_then(|contour| contour.points.first())
1443                .copied(),
1444            Some(Point::new(10.0, 10.0)),
1445            "close resumes where the contour began, not at any box corner"
1446        );
1447    }
1448
1449    /// A path that never opened a contour still starts where it is told —
1450    /// Skia's implicit `moveTo(0, 0)` is deliberately not copied.
1451    #[test]
1452    fn a_first_segment_with_no_contour_starts_at_its_own_point() {
1453        let mut path = PathBuilder::new();
1454        path.line_to((30.0, 30.0));
1455        assert_eq!(
1456            path.build().bounds(),
1457            Rect::from_ltrb(30.0, 30.0, 30.0, 30.0)
1458        );
1459    }
1460
1461    #[test]
1462    fn append_carries_verbs_through_the_transform() {
1463        let mut source = PathBuilder::new();
1464        source.rect(Rect::new(0.0, 0.0, 10.0, 10.0));
1465        let source = source.build();
1466
1467        let mut target = PathBuilder::new();
1468        target.rect(Rect::new(0.0, 0.0, 4.0, 4.0));
1469        target.append(&source, &Matrix::translation(100.0, 50.0));
1470        let target = target.build();
1471
1472        assert_eq!(target.bounds(), Rect::from_ltrb(0.0, 0.0, 110.0, 60.0));
1473        assert!(target.contains(Point::new(105.0, 55.0), FillRule::NonZero));
1474        assert!(!target.contains(Point::new(5.0, 5.0), FillRule::NonZero));
1475    }
1476
1477    /// The reopen after a closed source is what keeps the next segment
1478    /// connected. Without it the `line_to` below starts a fresh contour at
1479    /// its own endpoint and the edge from the seam disappears.
1480    #[test]
1481    fn appending_a_closed_contour_reopens_at_its_seam() {
1482        let mut source = PathBuilder::new();
1483        source.move_to((10.0, 10.0));
1484        source.line_to((20.0, 10.0));
1485        source.close();
1486        let source = source.build();
1487
1488        let mut target = PathBuilder::new();
1489        target.append(&source, &Matrix::IDENTITY);
1490        target.line_to((10.0, 40.0));
1491        let built = target.build();
1492
1493        // The seam is (10, 10); the new edge runs from there to (10, 40).
1494        assert_eq!(built.bounds(), Rect::from_ltrb(10.0, 10.0, 20.0, 40.0));
1495        assert!(built.contains(Point::new(10.0, 25.0), FillRule::NonZero));
1496    }
1497
1498    /// An appended OPEN contour becomes the receiver's contour, origin
1499    /// included. Keeping the receiver's own origin would send a later
1500    /// `close` + segment back to the wrong seam.
1501    #[test]
1502    fn appending_an_open_contour_hands_over_its_origin() {
1503        let mut source = PathBuilder::new();
1504        source.move_to((50.0, 50.0));
1505        source.line_to((60.0, 50.0));
1506        let source = source.build();
1507
1508        let mut target = PathBuilder::new();
1509        target.move_to((0.0, 0.0));
1510        target.line_to((10.0, 0.0));
1511        target.append(&source, &Matrix::IDENTITY);
1512        target.close();
1513        target.line_to((90.0, 90.0));
1514
1515        let contours = target.build().flatten(0.05);
1516        let resumed = contours.last().expect("the path continues after the close");
1517        assert_eq!(
1518            resumed.points.first().copied(),
1519            Some(Point::new(50.0, 50.0)),
1520            "the resumed segment must start at the APPENDED contour's origin"
1521        );
1522    }
1523
1524    #[test]
1525    fn appending_nothing_leaves_an_open_contour_open() {
1526        let empty = PathBuilder::new().build();
1527        let mut target = PathBuilder::new();
1528        target.move_to((0.0, 0.0));
1529        target.line_to((10.0, 0.0));
1530        target.append(&empty, &Matrix::IDENTITY);
1531        target.line_to((10.0, 10.0));
1532        assert_eq!(
1533            target.build().bounds(),
1534            Rect::from_ltrb(0.0, 0.0, 10.0, 10.0)
1535        );
1536    }
1537
1538    #[test]
1539    fn appending_an_open_contour_leaves_it_open() {
1540        let mut source = PathBuilder::new();
1541        source.move_to((0.0, 0.0));
1542        source.line_to((10.0, 0.0));
1543        let source = source.build();
1544
1545        let mut target = PathBuilder::new();
1546        target.append(&source, &Matrix::IDENTITY);
1547        // Without the contour-open handoff this would restart at the origin
1548        // and the bounds would be unchanged by the new point.
1549        target.line_to((10.0, 10.0));
1550        assert_eq!(
1551            target.build().bounds(),
1552            Rect::from_ltrb(0.0, 0.0, 10.0, 10.0)
1553        );
1554    }
1555
1556    #[test]
1557    fn tight_bounds_use_curve_extrema_not_control_points() {
1558        let mut path = PathBuilder::new();
1559        path.move_to((0.0, 0.0));
1560        path.quad_to((100.0, 100.0), (200.0, 0.0));
1561        let path = path.build();
1562        assert_eq!(path.bounds(), Rect::new(0.0, 0.0, 200.0, 100.0));
1563        assert_eq!(path.tight_bounds(), Rect::new(0.0, 0.0, 200.0, 50.0));
1564    }
1565
1566    #[test]
1567    fn tight_bounds_keep_extrema_below_f32_epsilon() {
1568        let mut path = PathBuilder::new();
1569        path.move_to((0.0, 0.0));
1570        path.quad_to((0.0, 1.0e-8), (0.0, 0.0));
1571        let bounds = path.build().tight_bounds();
1572        assert!((bounds.height - 5.0e-9).abs() < 1.0e-12);
1573    }
1574
1575    #[test]
1576    fn cubic_extrema_preserve_the_small_root() {
1577        let roots = cubic_extrema(0.0, 1.0e-8, -0.5, -0.5);
1578        assert!(roots
1579            .into_iter()
1580            .flatten()
1581            .any(|root| (root - 1.0e-8).abs() < 1.0e-10));
1582    }
1583
1584    #[test]
1585    fn radii_constrain_together() {
1586        let r = Rect::new(0.0, 0.0, 100.0, 40.0);
1587        // tl+bl = 80 > height 40 → everything scales by 0.5.
1588        let out = constrain_radii(&r, [40.0, 10.0, 10.0, 40.0]);
1589        assert_eq!(out, [20.0, 5.0, 5.0, 20.0]);
1590        // Already fitting radii pass through untouched.
1591        assert_eq!(constrain_radii(&r, [8.0, 8.0, 8.0, 8.0]), [8.0; 4]);
1592    }
1593
1594    #[test]
1595    fn per_corner_rrect_stays_in_rect() {
1596        let r = Rect::new(10.0, 10.0, 100.0, 60.0);
1597        let mut b = PathBuilder::new();
1598        b.rrect_radii(r, [30.0, 0.0, 16.0, 8.0]);
1599        assert_eq!(b.build().bounds(), r);
1600    }
1601
1602    #[test]
1603    fn bounds_cover_control_points() {
1604        let mut b = PathBuilder::new();
1605        b.move_to((10.0, 10.0)).quad_to((50.0, -20.0), (90.0, 10.0));
1606        let p = b.build();
1607        assert_eq!(p.bounds(), Rect::from_ltrb(10.0, -20.0, 90.0, 10.0));
1608    }
1609
1610    #[test]
1611    fn circle_flattens_to_radius() {
1612        let mut b = PathBuilder::new();
1613        b.circle((0.0, 0.0), 100.0);
1614        let contours = b.build().flatten(0.1);
1615        assert_eq!(contours.len(), 1);
1616        assert!(contours[0].closed, "circle closes its contour");
1617        for p in &contours[0].points {
1618            let r = (p.x * p.x + p.y * p.y).sqrt();
1619            assert!((r - 100.0).abs() < 0.5, "point off circle: r={r}");
1620        }
1621    }
1622
1623    #[test]
1624    fn finer_tolerance_means_more_segments() {
1625        let path = {
1626            let mut b = PathBuilder::new();
1627            b.circle((0.0, 0.0), 100.0);
1628            b.build()
1629        };
1630        let coarse = path.flatten(2.0)[0].points.len();
1631        let fine = path.flatten(0.05)[0].points.len();
1632        assert!(fine > coarse, "fine {fine} vs coarse {coarse}");
1633    }
1634
1635    #[test]
1636    fn small_contours_survive_for_the_stroker() {
1637        let mut b = PathBuilder::new();
1638        b.move_to((0.0, 0.0)).line_to((10.0, 0.0)); // a stroked line segment
1639        b.move_to((50.0, 50.0)); // a lone point (caps render it)
1640        let contours = b.build().flatten(0.1);
1641        assert_eq!(contours.len(), 2);
1642        assert_eq!(contours[0].points.len(), 2);
1643        assert!(!contours[0].closed);
1644        assert_eq!(contours[1].points.len(), 1);
1645    }
1646
1647    #[test]
1648    fn close_emits_the_closing_edge_and_marks_the_contour() {
1649        let mut b = PathBuilder::new();
1650        b.move_to((0.0, 0.0))
1651            .line_to((10.0, 0.0))
1652            .line_to((10.0, 10.0))
1653            .close();
1654        let contours = b.build().flatten(0.1);
1655        assert!(contours[0].closed);
1656        assert_eq!(contours[0].points.len(), 4, "closing edge in the polyline");
1657        assert_eq!(contours[0].points[3], Point::new(0.0, 0.0));
1658    }
1659
1660    #[test]
1661    fn bounds_keep_a_first_point_at_the_origin() {
1662        let mut b = PathBuilder::new();
1663        b.move_to((0.0, 0.0)).line_to((50.0, 80.0));
1664        assert_eq!(b.build().bounds(), Rect::from_ltrb(0.0, 0.0, 50.0, 80.0));
1665
1666        let mut b = PathBuilder::new();
1667        b.move_to((0.0, 0.0)).line_to((100.0, 0.0)); // zero-height line
1668        assert_eq!(b.build().bounds(), Rect::from_ltrb(0.0, 0.0, 100.0, 0.0));
1669    }
1670
1671    #[test]
1672    fn curve_without_move_starts_contour() {
1673        let mut b = PathBuilder::new();
1674        b.line_to((10.0, 0.0))
1675            .line_to((10.0, 10.0))
1676            .line_to((0.0, 10.0));
1677        let contours = b.build().flatten(0.1);
1678        assert_eq!(contours.len(), 1);
1679        assert_eq!(contours[0].points.len(), 4);
1680    }
1681
1682    /// The circular constructor must be EXACTLY the rx == ry case of the
1683    /// elliptical one — same constraint order, same cubics — so every
1684    /// existing rrect golden also pins the elliptical code path.
1685    #[test]
1686    fn circular_rrect_is_the_equal_axes_elliptical_case() {
1687        let r = Rect::new(10.0, 20.0, 120.0, 80.0);
1688        let radii = [24.0, 8.0, 30.0, 0.0];
1689        let mut circular = PathBuilder::new();
1690        circular.rrect_radii(r, radii);
1691        let mut elliptical = PathBuilder::new();
1692        elliptical.rrect_radii_elliptical(r, radii.map(|v| [v; 2]));
1693        assert_eq!(
1694            circular.build().flatten(0.1)[0].points,
1695            elliptical.build().flatten(0.1)[0].points,
1696        );
1697    }
1698
1699    #[test]
1700    fn elliptical_radii_constrain_per_axis() {
1701        // A 100×40 rect with tall corner ellipses: the HEIGHT edges force
1702        // the scale (20 + 30 > 40 → f = 0.8); x components ride along.
1703        let r = Rect::new(0.0, 0.0, 100.0, 40.0);
1704        let out = constrain_radii_elliptical(
1705            &r,
1706            [[10.0, 20.0], [10.0, 20.0], [10.0, 30.0], [10.0, 30.0]],
1707        );
1708        assert_eq!(out[0], [8.0, 16.0]);
1709        assert_eq!(out[2], [8.0, 24.0]);
1710        // Negative radii clamp to zero before constraining.
1711        let out = constrain_radii_elliptical(&r, [[-5.0, 10.0], [0.0; 2], [0.0; 2], [0.0; 2]]);
1712        assert_eq!(out[0], [0.0, 10.0]);
1713    }
1714
1715    #[test]
1716    fn elliptical_corner_lands_on_axis_extremes() {
1717        // One elliptical corner (rx 40, ry 10): the arc must start 40 in
1718        // from the corner on x and end 10 down on y.
1719        let r = Rect::new(0.0, 0.0, 200.0, 100.0);
1720        let mut b = PathBuilder::new();
1721        b.rrect_radii_elliptical(r, [[0.0; 2], [40.0, 10.0], [0.0; 2], [0.0; 2]]);
1722        let points = &b.build().flatten(0.05)[0].points;
1723        // The top edge stops at x = 160 (200 - rx) and the right edge
1724        // starts at y = 10 (ry) — both points must be on the outline.
1725        assert!(points
1726            .iter()
1727            .any(|p| (p.x - 160.0).abs() < 0.5 && p.y.abs() < 0.5));
1728        assert!(points
1729            .iter()
1730            .any(|p| (p.x - 200.0).abs() < 0.5 && (p.y - 10.0).abs() < 0.5));
1731    }
1732
1733    // ── arcs ────────────────────────────────────────────────────────────────
1734
1735    /// Every point of a swept circle sits on the circle, to well under a
1736    /// tenth of a pixel — the cubic approximation's whole claim.
1737    #[test]
1738    fn swept_arc_stays_on_its_circle() {
1739        let (center, radius) = (Point::new(50.0, 60.0), 40.0);
1740        let mut b = PathBuilder::new();
1741        b.arc(center, radius, 0.0, std::f32::consts::TAU);
1742        for point in &b.build().flatten(0.01)[0].points {
1743            let offset = (point.x - center.x).hypot(point.y - center.y);
1744            assert!(
1745                (offset - radius).abs() < 0.05,
1746                "point {point:?} is {offset} from the centre, not {radius}"
1747            );
1748        }
1749    }
1750
1751    /// A quarter turn ends exactly where trigonometry says it does.
1752    #[test]
1753    fn quarter_arc_ends_where_it_should() {
1754        let mut b = PathBuilder::new();
1755        b.arc((0.0, 0.0), 100.0, 0.0, std::f32::consts::FRAC_PI_2);
1756        let points = &b.build().flatten(0.01)[0].points;
1757        let (first, last) = (points[0], *points.last().unwrap());
1758        assert!(
1759            (first.x - 100.0).abs() < 0.01 && first.y.abs() < 0.01,
1760            "{first:?}"
1761        );
1762        assert!(
1763            last.x.abs() < 0.05 && (last.y - 100.0).abs() < 0.05,
1764            "{last:?}"
1765        );
1766    }
1767
1768    /// An ellipse reaches its own half-extents on each axis.
1769    #[test]
1770    fn ellipse_reaches_both_radii() {
1771        let mut b = PathBuilder::new();
1772        b.ellipse((0.0, 0.0), [80.0, 20.0], 0.0, 0.0, std::f32::consts::TAU);
1773        let points = &b.build().flatten(0.01)[0].points;
1774        let widest = points.iter().fold(0.0f32, |m, p| m.max(p.x.abs()));
1775        let tallest = points.iter().fold(0.0f32, |m, p| m.max(p.y.abs()));
1776        assert!((widest - 80.0).abs() < 0.1, "widest {widest}");
1777        assert!((tallest - 20.0).abs() < 0.1, "tallest {tallest}");
1778    }
1779
1780    /// The rotation turns the ellipse: a 90° turn swaps which axis is long.
1781    #[test]
1782    fn ellipse_rotation_swaps_the_axes() {
1783        let mut b = PathBuilder::new();
1784        b.ellipse(
1785            (0.0, 0.0),
1786            [80.0, 20.0],
1787            std::f32::consts::FRAC_PI_2,
1788            0.0,
1789            std::f32::consts::TAU,
1790        );
1791        let points = &b.build().flatten(0.01)[0].points;
1792        let widest = points.iter().fold(0.0f32, |m, p| m.max(p.x.abs()));
1793        let tallest = points.iter().fold(0.0f32, |m, p| m.max(p.y.abs()));
1794        assert!((widest - 20.0).abs() < 0.1, "widest {widest}");
1795        assert!((tallest - 80.0).abs() < 0.1, "tallest {tallest}");
1796    }
1797
1798    /// A right-angle `arc_to` with radius r touches down r before the corner
1799    /// and leaves r after it, and every point between is r from the centre
1800    /// the two tangents share.
1801    #[test]
1802    fn arc_to_rounds_a_right_angle() {
1803        let radius = 20.0f32;
1804        let mut b = PathBuilder::new();
1805        b.move_to((0.0, 0.0))
1806            .arc_to((100.0, 0.0), (100.0, 100.0), radius);
1807        let points = &b.build().flatten(0.01)[0].points;
1808
1809        let entry = Point::new(100.0 - radius, 0.0);
1810        let exit = Point::new(100.0, radius);
1811        assert!(points
1812            .iter()
1813            .any(|p| (p.x - entry.x).abs() < 0.1 && (p.y - entry.y).abs() < 0.1));
1814        assert!(points
1815            .iter()
1816            .any(|p| (p.x - exit.x).abs() < 0.1 && (p.y - exit.y).abs() < 0.1));
1817
1818        let center = Point::new(100.0 - radius, radius);
1819        for point in points.iter().filter(|p| p.x > entry.x - 0.01) {
1820            let offset = (point.x - center.x).hypot(point.y - center.y);
1821            assert!(
1822                (offset - radius).abs() < 0.1,
1823                "{point:?} is {offset} from the centre"
1824            );
1825        }
1826    }
1827
1828    /// Collinear points and a zero radius both degenerate to a plain line,
1829    /// which is what the Canvas2D algorithm prescribes.
1830    #[test]
1831    fn degenerate_arc_to_falls_back_to_a_line() {
1832        for (corner, next, radius) in [
1833            ((50.0, 0.0), (100.0, 0.0), 20.0), // straight through
1834            ((50.0, 0.0), (50.0, 50.0), 0.0),  // no radius
1835        ] {
1836            let mut b = PathBuilder::new();
1837            b.move_to((0.0, 0.0)).arc_to(corner, next, radius);
1838            let points = &b.build().flatten(0.01)[0].points;
1839            assert_eq!(points.len(), 2, "expected a bare line, got {points:?}");
1840            assert!((points[1].x - corner.0).abs() < 0.01 && (points[1].y - corner.1).abs() < 0.01);
1841        }
1842    }
1843
1844    // ── containment ─────────────────────────────────────────────────────────
1845
1846    #[test]
1847    fn rect_contains_what_it_covers() {
1848        let mut b = PathBuilder::new();
1849        b.rect(Rect::new(10.0, 10.0, 80.0, 60.0));
1850        let path = b.build();
1851        assert!(path.contains(Point::new(50.0, 40.0), FillRule::NonZero));
1852        assert!(!path.contains(Point::new(5.0, 40.0), FillRule::NonZero));
1853        assert!(!path.contains(Point::new(50.0, 80.0), FillRule::NonZero));
1854        // Exactly on the outline counts as inside — on EVERY edge. The far
1855        // two are the ones a half-open bounds check silently loses.
1856        for on_outline in [
1857            Point::new(10.0, 40.0), // left
1858            Point::new(50.0, 10.0), // top
1859            Point::new(90.0, 40.0), // right
1860            Point::new(50.0, 70.0), // bottom
1861            Point::new(90.0, 70.0), // the far corner
1862        ] {
1863            assert!(
1864                path.contains(on_outline, FillRule::NonZero),
1865                "{on_outline:?} is on the outline and must count as inside"
1866            );
1867        }
1868    }
1869
1870    /// Canvas2D caps an arc at one turn. Without the clamp a huge sweep asks
1871    /// for billions of cubic pieces, which is an allocation the process does
1872    /// not survive — so this test is a crash guard, not a geometry check.
1873    #[test]
1874    fn an_enormous_sweep_stays_one_turn() {
1875        let mut b = PathBuilder::new();
1876        b.arc((0.0, 0.0), 50.0, 0.0, 1e20);
1877        let path = b.build();
1878        let contours = path.flatten(0.1);
1879        assert_eq!(contours.len(), 1);
1880        // One turn at this tolerance is a few hundred points, never millions.
1881        assert!(
1882            contours[0].points.len() < 1_000,
1883            "a clamped turn should stay small, got {}",
1884            contours[0].points.len()
1885        );
1886        assert!(contours[0].closed, "a full turn closes its contour");
1887    }
1888
1889    #[test]
1890    fn a_negative_sweep_turns_the_other_way() {
1891        let quarter = std::f32::consts::FRAC_PI_2;
1892        let mut clockwise = PathBuilder::new();
1893        clockwise.arc((0.0, 0.0), 50.0, 0.0, quarter);
1894        let mut anticlockwise = PathBuilder::new();
1895        anticlockwise.arc((0.0, 0.0), 50.0, 0.0, -quarter);
1896
1897        // y-down: a positive sweep from +x heads towards +y, a negative one
1898        // towards -y. Both start at the same point.
1899        let forward = clockwise.build().bounds();
1900        let backward = anticlockwise.build().bounds();
1901        assert!(forward.bottom() > 40.0, "positive sweep reaches +y");
1902        assert!(backward.y < -40.0, "negative sweep reaches -y");
1903    }
1904
1905    /// Containment runs on the CURVE, so it agrees with the true circle at
1906    /// every angle — a flattened test would drift inside the chords.
1907    #[test]
1908    fn circle_containment_is_exact_all_the_way_round() {
1909        let (center, radius) = (Point::new(0.0, 0.0), 100.0f32);
1910        let mut b = PathBuilder::new();
1911        b.circle(center, radius);
1912        let path = b.build();
1913        for step in 0..64 {
1914            let angle = step as f32 / 64.0 * std::f32::consts::TAU;
1915            let (sine, cosine) = angle.sin_cos();
1916            let inside = Point::new(cosine * radius * 0.99, sine * radius * 0.99);
1917            let outside = Point::new(cosine * radius * 1.01, sine * radius * 1.01);
1918            assert!(
1919                path.contains(inside, FillRule::NonZero),
1920                "{inside:?} should be in"
1921            );
1922            assert!(
1923                !path.contains(outside, FillRule::NonZero),
1924                "{outside:?} should be out"
1925            );
1926        }
1927    }
1928
1929    /// The two fill rules disagree exactly where they should: a hole wound
1930    /// the same way as its parent is solid under non-zero, empty under
1931    /// even-odd.
1932    #[test]
1933    fn fill_rules_disagree_about_a_same_wound_hole() {
1934        let mut b = PathBuilder::new();
1935        b.rect(Rect::new(0.0, 0.0, 100.0, 100.0));
1936        b.rect(Rect::new(25.0, 25.0, 50.0, 50.0));
1937        let path = b.build();
1938        let middle = Point::new(50.0, 50.0);
1939        assert!(path.contains(middle, FillRule::NonZero));
1940        assert!(!path.contains(middle, FillRule::EvenOdd));
1941        // Between the rings both rules agree it is filled.
1942        let ring = Point::new(10.0, 50.0);
1943        assert!(path.contains(ring, FillRule::NonZero));
1944        assert!(path.contains(ring, FillRule::EvenOdd));
1945    }
1946
1947    /// An unclosed contour still fills, so it must still contain.
1948    #[test]
1949    fn open_contour_closes_implicitly() {
1950        let mut b = PathBuilder::new();
1951        b.move_to((0.0, 0.0))
1952            .line_to((100.0, 0.0))
1953            .line_to((100.0, 100.0));
1954        let path = b.build();
1955        assert!(path.contains(Point::new(80.0, 40.0), FillRule::NonZero));
1956        assert!(!path.contains(Point::new(20.0, 60.0), FillRule::NonZero));
1957    }
1958
1959    /// Curved segments contribute their real crossings, not a chord's.
1960    #[test]
1961    fn containment_handles_curves_that_double_back() {
1962        let mut b = PathBuilder::new();
1963        b.move_to((0.0, 0.0))
1964            .cubic_to((120.0, 120.0), (-20.0, 120.0), (100.0, 0.0))
1965            .close();
1966        let path = b.build();
1967        assert!(path.contains(Point::new(50.0, 40.0), FillRule::NonZero));
1968        assert!(!path.contains(Point::new(50.0, -10.0), FillRule::NonZero));
1969        assert!(!path.contains(Point::new(-30.0, 40.0), FillRule::NonZero));
1970    }
1971
1972    // ── conic segments ─────────────────────────────────────────────────────
1973
1974    /// A conic quarter with weight cos(45°) IS the circle: every flattened
1975    /// point sits on it exactly, not within the old cubic approximation's
1976    /// 0.027%-of-radius bulge. Ignoring the weight (treating the segment as
1977    /// a plain quad) would miss by ~6% of the radius.
1978    #[test]
1979    fn circle_flattens_onto_the_exact_circle() {
1980        let mut b = PathBuilder::new();
1981        b.circle((10.0, -20.0), 100.0);
1982        let contours = b.build().flatten(0.1);
1983        let points: Vec<Point> = contours.iter().flat_map(|c| c.points.clone()).collect();
1984        assert!(points.len() > 20);
1985        for p in &points {
1986            let distance = (p.x - 10.0).hypot(p.y + 20.0);
1987            assert!(
1988                (distance - 100.0).abs() < 2e-3,
1989                "{p:?} is off the circle by {}",
1990                (distance - 100.0).abs()
1991            );
1992        }
1993    }
1994
1995    /// `ellipse` runs on conics too: sampled points must satisfy the ellipse
1996    /// equation, including through a rotation.
1997    #[test]
1998    fn ellipse_arc_flattens_onto_the_exact_ellipse() {
1999        let mut b = PathBuilder::new();
2000        let rotation = 0.5f32;
2001        b.ellipse((5.0, 7.0), [80.0, 30.0], rotation, 0.3, 4.0);
2002        let contours = b.build().flatten(0.05);
2003        let (sin_r, cos_r) = rotation.sin_cos();
2004        for p in contours.iter().flat_map(|c| &c.points) {
2005            let (dx, dy) = (p.x - 5.0, p.y - 7.0);
2006            // Undo the rotation, then normalize by the radii.
2007            let (ux, uy) = (dx * cos_r + dy * sin_r, -dx * sin_r + dy * cos_r);
2008            let value = (ux / 80.0).powi(2) + (uy / 30.0).powi(2);
2009            assert!((value - 1.0).abs() < 1e-3, "{p:?} deviates: {value}");
2010        }
2011    }
2012
2013    /// The conic's rational midpoint reaches the true circle edge, so the
2014    /// tight bounds of a circle are its exact box — no cubic overshoot, and
2015    /// no undershoot from treating control points as reachable.
2016    #[test]
2017    fn conic_circle_tight_bounds_are_exact() {
2018        let mut b = PathBuilder::new();
2019        b.circle((0.0, 0.0), 50.0);
2020        let tight = b.build().tight_bounds();
2021        for (got, want) in [
2022            (tight.x, -50.0),
2023            (tight.y, -50.0),
2024            (tight.right(), 50.0),
2025            (tight.bottom(), 50.0),
2026        ] {
2027            assert!((got - want).abs() < 1e-3, "bound {got} should be {want}");
2028        }
2029        // The arc helper's extremum lies between piece endpoints: an arc
2030        // through the top must still reach exactly -50 in y.
2031        let mut arc = PathBuilder::new();
2032        arc.arc((0.0, 0.0), 50.0, -3.0, 2.5);
2033        let tight = arc.build().tight_bounds();
2034        assert!((tight.y + 50.0).abs() < 1e-3, "arc top: {}", tight.y);
2035    }
2036
2037    /// Analytic containment solves the conic itself: probes 0.05 units off a
2038    /// 100-unit circle land on the right side at every angle.
2039    #[test]
2040    fn conic_containment_is_exact_on_the_curve() {
2041        let mut b = PathBuilder::new();
2042        b.circle((0.0, 0.0), 100.0);
2043        let path = b.build();
2044        for i in 0..64 {
2045            let angle = i as f32 / 64.0 * std::f32::consts::TAU;
2046            let (s, c) = angle.sin_cos();
2047            let inside = Point::new(c * 99.95, s * 99.95);
2048            let outside = Point::new(c * 100.05, s * 100.05);
2049            assert!(
2050                path.contains(inside, FillRule::NonZero),
2051                "{inside:?} at angle {i} should be inside"
2052            );
2053            assert!(
2054                !path.contains(outside, FillRule::NonZero),
2055                "{outside:?} at angle {i} should be outside"
2056            );
2057        }
2058    }
2059
2060    /// Conic circles wind like any other contour: an opposing inner circle
2061    /// carves a hole under the non-zero rule.
2062    #[test]
2063    fn conic_windings_cancel_under_the_non_zero_rule() {
2064        let mut b = PathBuilder::new();
2065        b.circle((0.0, 0.0), 100.0);
2066        // The same circle traversed the other way: swap the conic sweep
2067        // direction by walking the quarters in reverse.
2068        let w = std::f32::consts::FRAC_1_SQRT_2;
2069        b.move_to((60.0, 0.0))
2070            .conic_to((60.0, -60.0), (0.0, -60.0), w)
2071            .conic_to((-60.0, -60.0), (-60.0, 0.0), w)
2072            .conic_to((-60.0, 60.0), (0.0, 60.0), w)
2073            .conic_to((60.0, 60.0), (60.0, 0.0), w)
2074            .close();
2075        let path = b.build();
2076        assert!(!path.contains(Point::new(0.0, 0.0), FillRule::NonZero));
2077        assert!(path.contains(Point::new(80.0, 0.0), FillRule::NonZero));
2078    }
2079
2080    /// `append` carries conic weights through its transform: a rotated
2081    /// circle is still a circle.
2082    #[test]
2083    fn append_preserves_conic_weights() {
2084        let mut source = PathBuilder::new();
2085        source.circle((0.0, 0.0), 40.0);
2086        let source = source.build();
2087        let mut b = PathBuilder::new();
2088        b.append(&source, &Matrix::rotation(0.7));
2089        for p in b.build().flatten(0.05).iter().flat_map(|c| &c.points) {
2090            let distance = p.x.hypot(p.y);
2091            assert!((distance - 40.0).abs() < 1e-3, "{p:?} off by {distance}");
2092        }
2093    }
2094
2095    /// Degenerate weights lower instead of recording: nonpositive to a line,
2096    /// exactly 1 to a quad, non-finite to the control polygon.
2097    #[test]
2098    fn degenerate_conic_weights_lower_to_simpler_verbs() {
2099        let mut zero = PathBuilder::new();
2100        zero.move_to((0.0, 0.0))
2101            .conic_to((50.0, 50.0), (100.0, 0.0), 0.0);
2102        let contour = &zero.build().flatten(0.1)[0];
2103        assert_eq!(contour.points.len(), 2, "weight 0 must be a straight line");
2104
2105        let mut quad = PathBuilder::new();
2106        quad.move_to((0.0, 0.0))
2107            .conic_to((50.0, 50.0), (100.0, 0.0), 1.0);
2108        let mut plain = PathBuilder::new();
2109        plain
2110            .move_to((0.0, 0.0))
2111            .quad_to((50.0, 50.0), (100.0, 0.0));
2112        assert_eq!(
2113            quad.build().flatten(0.1),
2114            plain.build().flatten(0.1),
2115            "weight 1 must match the plain quadratic"
2116        );
2117
2118        let mut infinite = PathBuilder::new();
2119        infinite
2120            .move_to((0.0, 0.0))
2121            .conic_to((50.0, 50.0), (100.0, 0.0), f32::INFINITY);
2122        let contour = &infinite.build().flatten(0.1)[0];
2123        assert_eq!(
2124            contour.points,
2125            vec![
2126                Point::new(0.0, 0.0),
2127                Point::new(50.0, 50.0),
2128                Point::new(100.0, 0.0)
2129            ],
2130            "an unbounded weight collapses onto the control polygon"
2131        );
2132    }
2133
2134    /// Extremum-finding must survive coordinates far from the origin: the
2135    /// quadratic's coefficients are built from point DIFFERENCES (Skia's
2136    /// `conic_find_extrema` form) in f64, because the absolute-coordinate
2137    /// form cancels catastrophically in f32. Audit repro: this curve near
2138    /// y=442028 has a real interior maximum that the naive form misses.
2139    #[test]
2140    fn conic_extrema_survive_large_coordinates() {
2141        let p0 = Point::new(0.0, 442_027.38);
2142        let c = Point::new(50.0, 442_028.47);
2143        let p2 = Point::new(100.0, 442_027.03);
2144        let w = 20.189_045;
2145        let mut b = PathBuilder::new();
2146        b.move_to(p0).conic_to(c, p2, w).close();
2147        let path = b.build();
2148        let bottom = path.tight_bounds().bottom();
2149        assert!(
2150            (bottom - 442_028.4).abs() < 0.05,
2151            "tight bottom {bottom} should reach the true maximum ~442028.41"
2152        );
2153        assert!(
2154            path.contains(Point::new(49.660_536, 442_028.0), FillRule::NonZero),
2155            "an interior point above the chord must be inside"
2156        );
2157    }
2158
2159    /// Flattening error stays within the requested tolerance for a strongly
2160    /// hyperbolic weight (the superellipse emits weights past 8): the chord
2161    /// midpoints of consecutive flattened points may deviate from the curve
2162    /// by at most the tolerance.
2163    #[test]
2164    fn conic_flattening_respects_tolerance_for_large_weights() {
2165        let (p0, c, p2, w) = (
2166            Point::new(0.0, 100.0),
2167            Point::new(35.0, 100.0),
2168            Point::new(40.0, 90.0),
2169            8.3f32,
2170        );
2171        let tolerance = 0.05f32;
2172        let mut b = PathBuilder::new();
2173        b.move_to(p0).conic_to(c, p2, w);
2174        let contour = &b.build().flatten(tolerance)[0];
2175        // Dense reference polyline on the true curve.
2176        let reference: Vec<Point> = (0..=4096)
2177            .map(|i| eval_conic(p0, c, p2, w, i as f32 / 4096.0))
2178            .collect();
2179        for pair in contour.points.windows(2) {
2180            let mid = Point::new((pair[0].x + pair[1].x) / 2.0, (pair[0].y + pair[1].y) / 2.0);
2181            let distance = reference
2182                .iter()
2183                .map(|r| (r.x - mid.x).hypot(r.y - mid.y))
2184                .fold(f32::MAX, f32::min);
2185            assert!(
2186                distance <= tolerance * 1.1,
2187                "chord midpoint {mid:?} is {distance} off the curve"
2188            );
2189        }
2190    }
2191}
2192
2193#[cfg(test)]
2194mod element_tests {
2195    use super::*;
2196
2197    #[test]
2198    fn elements_rebuild_an_equal_path() {
2199        let mut builder = PathBuilder::new();
2200        builder
2201            .move_to((0.0, 0.0))
2202            .line_to((10.0, 0.0))
2203            .quad_to((10.0, 5.0), (10.0, 10.0))
2204            .conic_to((5.0, 10.0), (0.0, 10.0), 0.7)
2205            .cubic_to((0.0, 7.0), (0.0, 3.0), (0.0, 0.0))
2206            .close();
2207        let path = builder.build();
2208        let mut rebuilt = PathBuilder::new();
2209        for element in path.elements() {
2210            match element {
2211                PathElement::MoveTo(p) => rebuilt.move_to(p),
2212                PathElement::LineTo(p) => rebuilt.line_to(p),
2213                PathElement::QuadTo(c, p) => rebuilt.quad_to(c, p),
2214                PathElement::ConicTo(c, p, w) => rebuilt.conic_to(c, p, w),
2215                PathElement::CubicTo(a, b, p) => rebuilt.cubic_to(a, b, p),
2216                PathElement::Close => rebuilt.close(),
2217            };
2218        }
2219        let rebuilt = rebuilt.build();
2220        assert_eq!(path.elements().count(), 6);
2221        assert_eq!(rebuilt.bounds(), path.bounds());
2222        assert!(path.elements().eq(rebuilt.elements()));
2223    }
2224}