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