1use crate::{Point, Rect};
19
20#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
22pub enum StrokeCap {
23 #[default]
25 Butt,
26 Round,
28 Square,
30}
31
32#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
34pub enum StrokeJoin {
35 #[default]
37 Miter,
38 Round,
40 Bevel,
42}
43
44#[derive(Clone, Copy, Debug, PartialEq)]
49pub struct Stroke {
50 pub width: f32,
53 pub cap: StrokeCap,
54 pub join: StrokeJoin,
55}
56
57impl Stroke {
58 pub const fn new(width: f32) -> Self {
60 Self {
61 width,
62 cap: StrokeCap::Butt,
63 join: StrokeJoin::Miter,
64 }
65 }
66
67 pub const fn with_width(mut self, width: f32) -> Self {
68 self.width = width;
69 self
70 }
71
72 pub const fn with_cap(mut self, cap: StrokeCap) -> Self {
73 self.cap = cap;
74 self
75 }
76
77 pub const fn with_join(mut self, join: StrokeJoin) -> Self {
78 self.join = join;
79 self
80 }
81
82 pub fn half_width(&self) -> f32 {
86 if self.width.is_finite() {
87 (self.width * 0.5).max(0.0)
88 } else {
89 0.0
90 }
91 }
92
93 pub fn is_visible(&self) -> bool {
95 self.width.is_finite() && self.width > 0.0
96 }
97
98 pub fn scaled(&self, scale: f32) -> Self {
100 Self {
101 width: self.width * scale,
102 ..*self
103 }
104 }
105}
106
107impl Default for Stroke {
108 fn default() -> Self {
109 Self::new(1.0)
110 }
111}
112
113pub const TAU: f32 = std::f32::consts::PI * 2.0;
115
116#[derive(Clone, Copy, Debug, PartialEq)]
129pub struct ArcGeometry {
130 pub center: Point,
131 pub inner_radius: f32,
132 pub outer_radius: f32,
133 pub start_angle: f32,
135 pub sweep_angle: f32,
137 pub cap: StrokeCap,
138}
139
140#[inline]
146fn exact_floor(x: f32) -> f32 {
147 if x == 0.0 {
148 return x;
150 }
151 if x.abs() < 8_388_608.0 {
152 let truncated = x as i32 as f32;
153 truncated - ((x < truncated) as i32 as f32)
154 } else {
155 x
156 }
157}
158
159#[inline]
165fn wrap_angle_tau(x: f32) -> f32 {
166 let wrapped = x - exact_floor(x * (1.0 / TAU)) * TAU;
167 if wrapped >= TAU {
168 wrapped - TAU
169 } else if wrapped < 0.0 {
170 0.0
171 } else {
172 wrapped
173 }
174}
175
176#[inline]
182fn fast_sin_cos(angle: f32) -> (f32, f32) {
183 use std::f32::consts::{FRAC_PI_2, PI};
184 #[inline]
185 fn fold_sin(x: f32) -> f32 {
186 const B: f32 = 4.0 / PI;
187 const C: f32 = -4.0 / (PI * PI);
188 let y = B * x + C * x * x.abs();
189 0.225 * (y * y.abs() - y) + y
190 }
191 let x = wrap_angle_tau(angle);
192 let x = if x > PI { x - TAU } else { x };
193 let mut c = x + FRAC_PI_2;
194 if c > PI {
195 c -= TAU;
196 }
197 (fold_sin(x), fold_sin(c))
198}
199
200const FAST_TRIG_ERR: f32 = 1.3e-3;
204
205impl ArcGeometry {
206 pub fn new(
208 center: Point,
209 inner_radius: f32,
210 outer_radius: f32,
211 start_angle: f32,
212 sweep_angle: f32,
213 cap: StrokeCap,
214 ) -> Self {
215 let finite = center.x.is_finite()
216 && center.y.is_finite()
217 && inner_radius.is_finite()
218 && outer_radius.is_finite()
219 && start_angle.is_finite()
220 && sweep_angle.is_finite();
221 if !finite {
222 return Self::DEGENERATE;
223 }
224
225 let outer = outer_radius.max(0.0);
226 let inner = inner_radius.clamp(0.0, outer);
227
228 let (mut start, mut sweep) = if sweep_angle < 0.0 {
231 (start_angle + sweep_angle, -sweep_angle)
232 } else {
233 (start_angle, sweep_angle)
234 };
235 if sweep >= TAU {
236 sweep = TAU;
239 start = 0.0;
240 }
241 start = wrap_angle_tau(start);
242 if !start.is_finite() {
243 start = 0.0;
244 }
245 let cap = if sweep >= TAU { StrokeCap::Round } else { cap };
246
247 Self {
248 center,
249 inner_radius: inner,
250 outer_radius: outer,
251 start_angle: start,
252 sweep_angle: sweep,
253 cap,
254 }
255 }
256
257 const DEGENERATE: Self = Self {
258 center: Point::ZERO,
259 inner_radius: 0.0,
260 outer_radius: 0.0,
261 start_angle: 0.0,
262 sweep_angle: 0.0,
263 cap: StrokeCap::Butt,
264 };
265
266 pub fn mid_radius(&self) -> f32 {
268 (self.inner_radius + self.outer_radius) * 0.5
269 }
270
271 pub fn half_thickness(&self) -> f32 {
274 (self.outer_radius - self.inner_radius) * 0.5
275 }
276
277 pub fn is_degenerate(&self) -> bool {
279 !(self.outer_radius > 0.0
280 && self.outer_radius > self.inner_radius
281 && self.sweep_angle > 0.0)
282 }
283
284 pub fn contains_angle(&self, angle: f32) -> bool {
286 if self.sweep_angle >= TAU {
287 return true;
288 }
289 let delta = wrap_angle_tau(angle - self.start_angle);
290 delta <= self.sweep_angle + 1e-6
291 }
292
293 pub fn scaled_about(&self, center: Point, scale: f32) -> Self {
296 Self {
297 center,
298 inner_radius: self.inner_radius * scale,
299 outer_radius: self.outer_radius * scale,
300 ..*self
301 }
302 }
303
304 pub fn bounds(&self) -> Rect {
316 if self.is_degenerate() {
317 return Rect {
318 x: self.center.x,
319 y: self.center.y,
320 width: 0.0,
321 height: 0.0,
322 };
323 }
324
325 if self.sweep_angle >= TAU && self.cap != StrokeCap::Square {
333 let r = self.outer_radius;
334 return Rect {
335 x: self.center.x - r,
336 y: self.center.y - r,
337 width: r + r,
338 height: r + r,
339 };
340 }
341
342 let mut min_x = f32::INFINITY;
343 let mut min_y = f32::INFINITY;
344 let mut max_x = f32::NEG_INFINITY;
345 let mut max_y = f32::NEG_INFINITY;
346 let mut include = |x: f32, y: f32| {
347 min_x = min_x.min(x);
348 min_y = min_y.min(y);
349 max_x = max_x.max(x);
350 max_y = max_y.max(y);
351 };
352
353 let rb = self.half_thickness();
354 let ra = self.mid_radius();
355 let end_angle = self.start_angle + self.sweep_angle;
356
357 for (angle, outward) in [(self.start_angle, -1.0f32), (end_angle, 1.0f32)] {
358 let (sin, cos) = fast_sin_cos(angle);
359 match self.cap {
360 StrokeCap::Butt => {
361 include(
362 self.center.x + cos * self.inner_radius,
363 self.center.y + sin * self.inner_radius,
364 );
365 include(
366 self.center.x + cos * self.outer_radius,
367 self.center.y + sin * self.outer_radius,
368 );
369 }
370 StrokeCap::Square => {
371 let tx = -sin * rb * outward;
373 let ty = cos * rb * outward;
374 include(
375 self.center.x + cos * self.inner_radius + tx,
376 self.center.y + sin * self.inner_radius + ty,
377 );
378 include(
379 self.center.x + cos * self.outer_radius + tx,
380 self.center.y + sin * self.outer_radius + ty,
381 );
382 }
383 StrokeCap::Round => {
384 let cx = self.center.x + cos * ra;
386 let cy = self.center.y + sin * ra;
387 include(cx - rb, cy - rb);
388 include(cx + rb, cy + rb);
389 }
390 }
391 }
392
393 const AXIS_DIRECTIONS: [(f32, f32); 4] = [(0.0, 1.0), (1.0, 0.0), (0.0, -1.0), (-1.0, 0.0)];
397 for (quadrant, (sin, cos)) in AXIS_DIRECTIONS.into_iter().enumerate() {
398 let angle = quadrant as f32 * std::f32::consts::FRAC_PI_2;
399 if self.contains_angle(angle) {
400 include(
401 self.center.x + cos * self.outer_radius,
402 self.center.y + sin * self.outer_radius,
403 );
404 }
405 }
406
407 let pad = (self.outer_radius + rb) * FAST_TRIG_ERR + 0.02;
414 Rect {
415 x: min_x - pad,
416 y: min_y - pad,
417 width: (max_x - min_x + pad + pad).max(0.0),
418 height: (max_y - min_y + pad + pad).max(0.0),
419 }
420 }
421}
422
423pub fn arc_band(radius: f32, inner_radius: f32, stroke: Option<Stroke>) -> (f32, f32, StrokeCap) {
434 match stroke {
435 Some(stroke) => {
436 if !radius.is_finite() || !stroke.is_visible() {
437 return (0.0, 0.0, stroke.cap);
438 }
439 let half = stroke.half_width();
440 let radius = radius.max(0.0);
441 ((radius - half).max(0.0), radius + half, stroke.cap)
442 }
443 None => {
444 if !radius.is_finite() || !inner_radius.is_finite() {
445 return (0.0, 0.0, StrokeCap::Butt);
446 }
447 let outer = radius.max(0.0);
448 let inner = inner_radius.clamp(0.0, outer);
449 (inner, outer, StrokeCap::Butt)
450 }
451 }
452}
453
454pub fn inflate_rect(rect: Rect, amount: f32) -> Rect {
456 if !amount.is_finite() || amount <= 0.0 {
457 return rect;
458 }
459 Rect {
460 x: rect.x - amount,
461 y: rect.y - amount,
462 width: (rect.width + amount * 2.0).max(0.0),
463 height: (rect.height + amount * 2.0).max(0.0),
464 }
465}
466
467#[cfg(test)]
468mod tests {
469 use std::f32::consts::{FRAC_PI_2, PI};
470
471 use super::*;
472
473 fn approx(a: f32, b: f32) -> bool {
478 (a - b).abs() < 0.15
479 }
480
481 #[test]
485 fn scaling_an_arc_moves_its_centre_and_its_radii_and_nothing_else() {
486 let arc = ArcGeometry::new(
487 Point { x: 10.0, y: 20.0 },
488 4.0,
489 10.0,
490 FRAC_PI_2,
491 PI,
492 StrokeCap::Round,
493 );
494 let moved = arc.scaled_about(Point { x: 100.0, y: 200.0 }, 2.5);
495
496 assert_eq!(moved.center, Point { x: 100.0, y: 200.0 });
497 assert_eq!(moved.inner_radius, 10.0);
498 assert_eq!(moved.outer_radius, 25.0);
499 assert_eq!(moved.start_angle, arc.start_angle);
500 assert_eq!(moved.sweep_angle, arc.sweep_angle);
501 assert_eq!(moved.cap, arc.cap);
502
503 let same = arc.scaled_about(arc.center, 1.0);
505 assert_eq!(same, arc);
506 }
507
508 #[test]
509 fn exact_floor_is_bit_equal_to_floorf() {
510 let mut probes: Vec<f32> = vec![
511 0.0,
512 -0.0,
513 0.5,
514 -0.5,
515 1.0,
516 -1.0,
517 8_388_607.5,
518 -8_388_607.5,
519 8_388_608.0,
520 -8_388_608.0,
521 1.0e30,
522 -1.0e30,
523 f32::INFINITY,
524 f32::NEG_INFINITY,
525 f32::MIN_POSITIVE,
526 -f32::MIN_POSITIVE,
527 ];
528 for i in -4000..4000 {
529 probes.push(i as f32 * 0.01737);
530 probes.push(i as f32 * PI);
531 }
532 for x in probes {
533 assert_eq!(
534 exact_floor(x).to_bits(),
535 x.floor().to_bits(),
536 "exact_floor({x}) diverged from floorf"
537 );
538 }
539 assert!(exact_floor(f32::NAN).is_nan());
540 }
541
542 #[test]
543 fn stroke_builders_compose() {
544 let stroke = Stroke::new(4.0)
545 .with_cap(StrokeCap::Round)
546 .with_join(StrokeJoin::Bevel);
547 assert_eq!(stroke.width, 4.0);
548 assert_eq!(stroke.cap, StrokeCap::Round);
549 assert_eq!(stroke.join, StrokeJoin::Bevel);
550 assert_eq!(stroke.half_width(), 2.0);
551 assert!(stroke.is_visible());
552 assert_eq!(Stroke::default(), Stroke::new(1.0));
553 assert_eq!(Stroke::new(4.0).with_width(6.0).width, 6.0);
554 }
555
556 #[test]
557 fn stroke_rejects_non_positive_and_non_finite_widths() {
558 assert!(!Stroke::new(0.0).is_visible());
559 assert!(!Stroke::new(-3.0).is_visible());
560 assert!(!Stroke::new(f32::NAN).is_visible());
561 assert!(!Stroke::new(f32::INFINITY).is_visible());
562 assert_eq!(Stroke::new(f32::NAN).half_width(), 0.0);
563 assert_eq!(Stroke::new(-3.0).half_width(), 0.0);
564 }
565
566 #[test]
567 fn arc_geometry_normalizes_negative_sweeps() {
568 let arc = ArcGeometry::new(Point::ZERO, 1.0, 2.0, PI, -FRAC_PI_2, StrokeCap::Butt);
569 assert!(approx(arc.start_angle, PI - FRAC_PI_2));
570 assert!(approx(arc.sweep_angle, FRAC_PI_2));
571 }
572
573 #[test]
574 fn arc_geometry_clamps_full_turns_and_forces_round_caps() {
575 let arc = ArcGeometry::new(Point::ZERO, 1.0, 2.0, 0.3, TAU * 3.0, StrokeCap::Butt);
576 assert_eq!(arc.sweep_angle, TAU);
577 assert_eq!(
578 arc.cap,
579 StrokeCap::Round,
580 "a closed ring must not clip its (invisible) caps"
581 );
582 assert!(arc.contains_angle(0.0));
583 assert!(arc.contains_angle(PI));
584 }
585
586 #[test]
587 fn arc_geometry_sanitizes_non_finite_input() {
588 for arc in [
589 ArcGeometry::new(
590 Point::new(f32::NAN, 0.0),
591 1.0,
592 2.0,
593 0.0,
594 1.0,
595 StrokeCap::Butt,
596 ),
597 ArcGeometry::new(Point::ZERO, f32::NAN, 2.0, 0.0, 1.0, StrokeCap::Butt),
598 ArcGeometry::new(Point::ZERO, 1.0, f32::INFINITY, 0.0, 1.0, StrokeCap::Butt),
599 ArcGeometry::new(Point::ZERO, 1.0, 2.0, f32::NAN, 1.0, StrokeCap::Butt),
600 ArcGeometry::new(Point::ZERO, 1.0, 2.0, 0.0, f32::NAN, StrokeCap::Butt),
601 ] {
602 assert!(arc.is_degenerate());
603 let bounds = arc.bounds();
604 for value in [bounds.x, bounds.y, bounds.width, bounds.height] {
605 assert!(value.is_finite(), "degenerate arc bounds must stay finite");
606 }
607 }
608 }
609
610 #[test]
614 fn approximate_bounds_contain_the_exact_box_within_documented_slack() {
615 for radius in [2.0f32, 10.0, 57.0, 204.0] {
616 for cap in [StrokeCap::Butt, StrokeCap::Round, StrokeCap::Square] {
617 for step in 0..48 {
618 let start = step as f32 * (TAU / 48.0) * 1.031;
619 for sweep in [0.05f32, 0.9, FRAC_PI_2, 3.6] {
620 let arc = ArcGeometry::new(
621 Point::new(11.0, -7.0),
622 radius * 0.55,
623 radius,
624 start,
625 sweep,
626 cap,
627 );
628 if arc.is_degenerate() {
629 continue;
630 }
631 let bounds = arc.bounds();
632 let exact = exact_bounds(&arc);
633 let slack =
634 (arc.outer_radius + arc.half_thickness()) * FAST_TRIG_ERR * 2.0 + 0.05;
635 assert!(
636 bounds.x <= exact.x + 1e-3
637 && bounds.y <= exact.y + 1e-3
638 && bounds.x + bounds.width >= exact.x + exact.width - 1e-3
639 && bounds.y + bounds.height >= exact.y + exact.height - 1e-3,
640 "approximate box lost containment: {bounds:?} vs exact {exact:?} \
641 (radius {radius}, start {start}, sweep {sweep}, cap {cap:?})"
642 );
643 assert!(
644 (bounds.x - exact.x).abs() <= slack
645 && (bounds.y - exact.y).abs() <= slack
646 && (bounds.width - exact.width).abs() <= 2.0 * slack
647 && (bounds.height - exact.height).abs() <= 2.0 * slack,
648 "approximate box drifted past its slack: {bounds:?} vs exact \
649 {exact:?} slack {slack} (radius {radius}, start {start}, sweep \
650 {sweep}, cap {cap:?})"
651 );
652 }
653 }
654 }
655 }
656 }
657
658 fn exact_bounds(arc: &ArcGeometry) -> Rect {
660 let mut min_x = f32::INFINITY;
661 let mut min_y = f32::INFINITY;
662 let mut max_x = f32::NEG_INFINITY;
663 let mut max_y = f32::NEG_INFINITY;
664 let mut include = |x: f32, y: f32| {
665 min_x = min_x.min(x);
666 min_y = min_y.min(y);
667 max_x = max_x.max(x);
668 max_y = max_y.max(y);
669 };
670 let rb = arc.half_thickness();
671 let ra = arc.mid_radius();
672 let end_angle = arc.start_angle + arc.sweep_angle;
673 for (angle, outward) in [(arc.start_angle, -1.0f32), (end_angle, 1.0f32)] {
674 let (sin, cos) = angle.sin_cos();
675 match arc.cap {
676 StrokeCap::Butt => {
677 include(
678 arc.center.x + cos * arc.inner_radius,
679 arc.center.y + sin * arc.inner_radius,
680 );
681 include(
682 arc.center.x + cos * arc.outer_radius,
683 arc.center.y + sin * arc.outer_radius,
684 );
685 }
686 StrokeCap::Square => {
687 let tx = -sin * rb * outward;
688 let ty = cos * rb * outward;
689 include(
690 arc.center.x + cos * arc.inner_radius + tx,
691 arc.center.y + sin * arc.inner_radius + ty,
692 );
693 include(
694 arc.center.x + cos * arc.outer_radius + tx,
695 arc.center.y + sin * arc.outer_radius + ty,
696 );
697 }
698 StrokeCap::Round => {
699 let cx = arc.center.x + cos * ra;
700 let cy = arc.center.y + sin * ra;
701 include(cx - rb, cy - rb);
702 include(cx + rb, cy + rb);
703 }
704 }
705 }
706 const AXIS_DIRECTIONS: [(f32, f32); 4] = [(0.0, 1.0), (1.0, 0.0), (0.0, -1.0), (-1.0, 0.0)];
707 for (quadrant, (sin, cos)) in AXIS_DIRECTIONS.into_iter().enumerate() {
708 let angle = quadrant as f32 * FRAC_PI_2;
709 if arc.contains_angle(angle) {
710 include(
711 arc.center.x + cos * arc.outer_radius,
712 arc.center.y + sin * arc.outer_radius,
713 );
714 }
715 }
716 Rect {
717 x: min_x,
718 y: min_y,
719 width: (max_x - min_x).max(0.0),
720 height: (max_y - min_y).max(0.0),
721 }
722 }
723
724 #[test]
725 fn arc_geometry_flags_degenerate_bands() {
726 assert!(ArcGeometry::new(Point::ZERO, 5.0, 5.0, 0.0, 1.0, StrokeCap::Butt).is_degenerate());
728 assert!(ArcGeometry::new(Point::ZERO, 9.0, 5.0, 0.0, 1.0, StrokeCap::Butt).is_degenerate());
729 assert!(ArcGeometry::new(Point::ZERO, 1.0, 5.0, 0.0, 0.0, StrokeCap::Butt).is_degenerate());
731 assert!(ArcGeometry::new(Point::ZERO, 0.0, 0.0, 0.0, 1.0, StrokeCap::Butt).is_degenerate());
733 }
734
735 #[test]
736 fn arc_bounds_quarter_sweep_hugs_the_quadrant() {
737 let arc = ArcGeometry::new(
738 Point::new(100.0, 100.0),
739 0.0,
740 10.0,
741 0.0,
742 FRAC_PI_2,
743 StrokeCap::Butt,
744 );
745 let bounds = arc.bounds();
746 assert!(approx(bounds.x, 100.0), "{bounds:?}");
747 assert!(approx(bounds.y, 100.0), "{bounds:?}");
748 assert!(approx(bounds.width, 10.0), "{bounds:?}");
749 assert!(approx(bounds.height, 10.0), "{bounds:?}");
750 }
751
752 #[test]
753 fn arc_bounds_three_quarter_sweep_spans_every_axis_it_crosses() {
754 let arc = ArcGeometry::new(
756 Point::new(0.0, 0.0),
757 0.0,
758 10.0,
759 0.0,
760 3.0 * FRAC_PI_2,
761 StrokeCap::Butt,
762 );
763 let bounds = arc.bounds();
764 assert!(approx(bounds.x, -10.0), "{bounds:?}");
765 assert!(approx(bounds.y, -10.0), "{bounds:?}");
766 assert!(approx(bounds.width, 20.0), "{bounds:?}");
767 assert!(approx(bounds.height, 20.0), "{bounds:?}");
768 }
769
770 #[test]
771 fn arc_bounds_include_inner_endpoints_when_no_axis_is_crossed() {
772 let arc = ArcGeometry::new(
775 Point::ZERO,
776 8.0,
777 10.0,
778 std::f32::consts::FRAC_PI_4,
779 FRAC_PI_2,
780 StrokeCap::Butt,
781 );
782 let bounds = arc.bounds();
783 let sqrt2_2 = std::f32::consts::FRAC_1_SQRT_2;
784 assert!(approx(bounds.y, 8.0 * sqrt2_2), "{bounds:?}");
785 assert!(approx(bounds.y + bounds.height, 10.0), "{bounds:?}");
786 assert!(approx(bounds.x, -10.0 * sqrt2_2), "{bounds:?}");
787 assert!(approx(bounds.width, 20.0 * sqrt2_2), "{bounds:?}");
788 }
789
790 #[test]
791 fn arc_bounds_negative_sweep_matches_equivalent_positive_sweep() {
792 let forward = ArcGeometry::new(Point::ZERO, 4.0, 6.0, 0.0, FRAC_PI_2, StrokeCap::Butt);
793 let backward = ArcGeometry::new(
794 Point::ZERO,
795 4.0,
796 6.0,
797 FRAC_PI_2,
798 -FRAC_PI_2,
799 StrokeCap::Butt,
800 );
801 assert_eq!(forward.bounds(), backward.bounds());
802 }
803
804 #[test]
805 fn arc_bounds_full_turn_is_the_outer_circle() {
806 let arc = ArcGeometry::new(Point::new(5.0, 7.0), 3.0, 9.0, 1.1, TAU, StrokeCap::Butt);
807 let bounds = arc.bounds();
808 assert!(approx(bounds.x, -4.0), "{bounds:?}");
809 assert!(approx(bounds.y, -2.0), "{bounds:?}");
810 assert!(approx(bounds.width, 18.0), "{bounds:?}");
811 assert!(approx(bounds.height, 18.0), "{bounds:?}");
812 }
813
814 #[test]
815 fn arc_bounds_round_caps_bulge_past_the_radial_ends() {
816 let butt = ArcGeometry::new(Point::ZERO, 8.0, 12.0, 0.0, FRAC_PI_2, StrokeCap::Butt);
817 let round = ArcGeometry::new(Point::ZERO, 8.0, 12.0, 0.0, FRAC_PI_2, StrokeCap::Round);
818 let butt_bounds = butt.bounds();
819 let round_bounds = round.bounds();
820 assert!(approx(butt_bounds.y, 0.0), "{butt_bounds:?}");
822 assert!(approx(round_bounds.y, -2.0), "{round_bounds:?}");
823 assert!(round_bounds.width >= butt_bounds.width);
824 assert!(round_bounds.height >= butt_bounds.height);
825 }
826
827 #[test]
828 fn arc_bounds_square_caps_project_along_the_tangent() {
829 let square = ArcGeometry::new(Point::ZERO, 8.0, 12.0, 0.0, FRAC_PI_2, StrokeCap::Square);
830 let bounds = square.bounds();
831 assert!(approx(bounds.y, -2.0), "{bounds:?}");
833 assert!(approx(bounds.x + bounds.width, 12.0), "{bounds:?}");
834 }
835
836 #[test]
837 fn arc_band_resolves_stroked_and_filled_forms() {
838 let (inner, outer, cap) =
839 arc_band(10.0, 0.0, Some(Stroke::new(4.0).with_cap(StrokeCap::Round)));
840 assert_eq!((inner, outer), (8.0, 12.0));
841 assert_eq!(cap, StrokeCap::Round);
842
843 let (inner, outer, cap) = arc_band(10.0, 6.0, None);
844 assert_eq!((inner, outer), (6.0, 10.0));
845 assert_eq!(cap, StrokeCap::Butt);
846
847 let (inner, outer, _) = arc_band(10.0, 40.0, None);
849 assert_eq!((inner, outer), (10.0, 10.0));
850
851 let (inner, outer, _) = arc_band(1.0, 0.0, Some(Stroke::new(10.0)));
853 assert_eq!((inner, outer), (0.0, 6.0));
854 }
855
856 #[test]
857 fn full_ring_bounds_shortcut_matches_the_endpoint_walk() {
858 let ring = ArcGeometry::new(Point::new(10.0, -4.0), 6.0, 9.0, 1.3, TAU, StrokeCap::Butt);
863 assert_eq!(
864 ring.bounds(),
865 Rect {
866 x: 1.0,
867 y: -13.0,
868 width: 18.0,
869 height: 18.0
870 }
871 );
872
873 let square = ArcGeometry {
879 cap: StrokeCap::Square,
880 start_angle: TAU - (1.5f32 / 9.0).atan(),
881 ..ring
882 };
883 let bounds = square.bounds();
884 assert!(bounds.x + bounds.width > square.center.x + square.outer_radius);
885 }
886
887 #[test]
888 fn inflate_rect_ignores_non_positive_amounts() {
889 let rect = Rect {
890 x: 1.0,
891 y: 2.0,
892 width: 3.0,
893 height: 4.0,
894 };
895 assert_eq!(inflate_rect(rect, 0.0), rect);
896 assert_eq!(inflate_rect(rect, -1.0), rect);
897 assert_eq!(inflate_rect(rect, f32::NAN), rect);
898 assert_eq!(
899 inflate_rect(rect, 1.0),
900 Rect {
901 x: 0.0,
902 y: 1.0,
903 width: 5.0,
904 height: 6.0
905 }
906 );
907 }
908}