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