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;
149 }
150 if x.abs() < 8_388_608.0 {
151 let truncated = x as i32 as f32;
152 truncated - ((x < truncated) as i32 as f32)
153 } else {
154 x
155 }
156}
157
158#[inline]
164fn wrap_angle_tau(x: f32) -> f32 {
165 let wrapped = x - exact_floor(x * (1.0 / TAU)) * TAU;
166 if wrapped >= TAU {
167 wrapped - TAU
168 } else if wrapped < 0.0 {
169 0.0
170 } else {
171 wrapped
172 }
173}
174
175#[inline]
181fn fast_sin_cos(angle: f32) -> (f32, f32) {
182 use std::f32::consts::{FRAC_PI_2, PI};
183 #[inline]
184 fn fold_sin(x: f32) -> f32 {
185 const B: f32 = 4.0 / PI;
186 const C: f32 = -4.0 / (PI * PI);
187 let y = B * x + C * x * x.abs();
188 0.225 * (y * y.abs() - y) + y
189 }
190 let x = wrap_angle_tau(angle);
191 let x = if x > PI { x - TAU } else { x };
192 let mut c = x + FRAC_PI_2;
193 if c > PI {
194 c -= TAU;
195 }
196 (fold_sin(x), fold_sin(c))
197}
198
199const FAST_TRIG_ERR: f32 = 1.3e-3;
203
204impl ArcGeometry {
205 pub fn new(
207 center: Point,
208 inner_radius: f32,
209 outer_radius: f32,
210 start_angle: f32,
211 sweep_angle: f32,
212 cap: StrokeCap,
213 ) -> Self {
214 let finite = center.x.is_finite()
215 && center.y.is_finite()
216 && inner_radius.is_finite()
217 && outer_radius.is_finite()
218 && start_angle.is_finite()
219 && sweep_angle.is_finite();
220 if !finite {
221 return Self::DEGENERATE;
222 }
223
224 let outer = outer_radius.max(0.0);
225 let inner = inner_radius.clamp(0.0, outer);
226
227 let (mut start, mut sweep) = if sweep_angle < 0.0 {
228 (start_angle + sweep_angle, -sweep_angle)
229 } else {
230 (start_angle, sweep_angle)
231 };
232 if sweep >= TAU {
233 sweep = TAU;
234 start = 0.0;
235 }
236 start = wrap_angle_tau(start);
237 if !start.is_finite() {
238 start = 0.0;
239 }
240 let cap = if sweep >= TAU { StrokeCap::Round } else { cap };
241
242 Self {
243 center,
244 inner_radius: inner,
245 outer_radius: outer,
246 start_angle: start,
247 sweep_angle: sweep,
248 cap,
249 }
250 }
251
252 const DEGENERATE: Self = Self {
253 center: Point::ZERO,
254 inner_radius: 0.0,
255 outer_radius: 0.0,
256 start_angle: 0.0,
257 sweep_angle: 0.0,
258 cap: StrokeCap::Butt,
259 };
260
261 pub fn mid_radius(&self) -> f32 {
263 (self.inner_radius + self.outer_radius) * 0.5
264 }
265
266 pub fn half_thickness(&self) -> f32 {
269 (self.outer_radius - self.inner_radius) * 0.5
270 }
271
272 pub fn is_degenerate(&self) -> bool {
274 !(self.outer_radius > 0.0
275 && self.outer_radius > self.inner_radius
276 && self.sweep_angle > 0.0)
277 }
278
279 pub fn contains_angle(&self, angle: f32) -> bool {
281 if self.sweep_angle >= TAU {
282 return true;
283 }
284 let delta = wrap_angle_tau(angle - self.start_angle);
285 delta <= self.sweep_angle + 1e-6
286 }
287
288 pub fn scaled_about(&self, center: Point, scale: f32) -> Self {
291 Self {
292 center,
293 inner_radius: self.inner_radius * scale,
294 outer_radius: self.outer_radius * scale,
295 ..*self
296 }
297 }
298
299 pub fn bounds(&self) -> Rect {
311 if self.is_degenerate() {
312 return Rect {
313 x: self.center.x,
314 y: self.center.y,
315 width: 0.0,
316 height: 0.0,
317 };
318 }
319
320 if self.sweep_angle >= TAU && self.cap != StrokeCap::Square {
321 let r = self.outer_radius;
322 return Rect {
323 x: self.center.x - r,
324 y: self.center.y - r,
325 width: r + r,
326 height: r + r,
327 };
328 }
329
330 let mut min_x = f32::INFINITY;
331 let mut min_y = f32::INFINITY;
332 let mut max_x = f32::NEG_INFINITY;
333 let mut max_y = f32::NEG_INFINITY;
334 let mut include = |x: f32, y: f32| {
335 min_x = min_x.min(x);
336 min_y = min_y.min(y);
337 max_x = max_x.max(x);
338 max_y = max_y.max(y);
339 };
340
341 let rb = self.half_thickness();
342 let ra = self.mid_radius();
343 let end_angle = self.start_angle + self.sweep_angle;
344
345 for (angle, outward) in [(self.start_angle, -1.0f32), (end_angle, 1.0f32)] {
346 let (sin, cos) = fast_sin_cos(angle);
347 match self.cap {
348 StrokeCap::Butt => {
349 include(
350 self.center.x + cos * self.inner_radius,
351 self.center.y + sin * self.inner_radius,
352 );
353 include(
354 self.center.x + cos * self.outer_radius,
355 self.center.y + sin * self.outer_radius,
356 );
357 }
358 StrokeCap::Square => {
359 let tx = -sin * rb * outward;
360 let ty = cos * rb * outward;
361 include(
362 self.center.x + cos * self.inner_radius + tx,
363 self.center.y + sin * self.inner_radius + ty,
364 );
365 include(
366 self.center.x + cos * self.outer_radius + tx,
367 self.center.y + sin * self.outer_radius + ty,
368 );
369 }
370 StrokeCap::Round => {
371 let cx = self.center.x + cos * ra;
372 let cy = self.center.y + sin * ra;
373 include(cx - rb, cy - rb);
374 include(cx + rb, cy + rb);
375 }
376 }
377 }
378
379 const AXIS_DIRECTIONS: [(f32, f32); 4] = [(0.0, 1.0), (1.0, 0.0), (0.0, -1.0), (-1.0, 0.0)];
380 for (quadrant, (sin, cos)) in AXIS_DIRECTIONS.into_iter().enumerate() {
381 let angle = quadrant as f32 * std::f32::consts::FRAC_PI_2;
382 if self.contains_angle(angle) {
383 include(
384 self.center.x + cos * self.outer_radius,
385 self.center.y + sin * self.outer_radius,
386 );
387 }
388 }
389
390 let pad = (self.outer_radius + rb) * FAST_TRIG_ERR + 0.02;
391 Rect {
392 x: min_x - pad,
393 y: min_y - pad,
394 width: (max_x - min_x + pad + pad).max(0.0),
395 height: (max_y - min_y + pad + pad).max(0.0),
396 }
397 }
398}
399
400pub fn arc_band(radius: f32, inner_radius: f32, stroke: Option<Stroke>) -> (f32, f32, StrokeCap) {
411 match stroke {
412 Some(stroke) => {
413 if !radius.is_finite() || !stroke.is_visible() {
414 return (0.0, 0.0, stroke.cap);
415 }
416 let half = stroke.half_width();
417 let radius = radius.max(0.0);
418 ((radius - half).max(0.0), radius + half, stroke.cap)
419 }
420 None => {
421 if !radius.is_finite() || !inner_radius.is_finite() {
422 return (0.0, 0.0, StrokeCap::Butt);
423 }
424 let outer = radius.max(0.0);
425 let inner = inner_radius.clamp(0.0, outer);
426 (inner, outer, StrokeCap::Butt)
427 }
428 }
429}
430
431pub fn inflate_rect(rect: Rect, amount: f32) -> Rect {
433 if !amount.is_finite() || amount <= 0.0 {
434 return rect;
435 }
436 Rect {
437 x: rect.x - amount,
438 y: rect.y - amount,
439 width: (rect.width + amount * 2.0).max(0.0),
440 height: (rect.height + amount * 2.0).max(0.0),
441 }
442}
443
444#[cfg(test)]
445mod tests {
446 use std::f32::consts::{FRAC_PI_2, PI};
447
448 use super::*;
449
450 fn approx(a: f32, b: f32) -> bool {
451 (a - b).abs() < 0.15
452 }
453
454 #[test]
455 fn scaling_an_arc_moves_its_centre_and_its_radii_and_nothing_else() {
456 let arc = ArcGeometry::new(
457 Point { x: 10.0, y: 20.0 },
458 4.0,
459 10.0,
460 FRAC_PI_2,
461 PI,
462 StrokeCap::Round,
463 );
464 let moved = arc.scaled_about(Point { x: 100.0, y: 200.0 }, 2.5);
465
466 assert_eq!(moved.center, Point { x: 100.0, y: 200.0 });
467 assert_eq!(moved.inner_radius, 10.0);
468 assert_eq!(moved.outer_radius, 25.0);
469 assert_eq!(moved.start_angle, arc.start_angle);
470 assert_eq!(moved.sweep_angle, arc.sweep_angle);
471 assert_eq!(moved.cap, arc.cap);
472
473 let same = arc.scaled_about(arc.center, 1.0);
474 assert_eq!(same, arc);
475 }
476
477 #[test]
478 fn exact_floor_is_bit_equal_to_floorf() {
479 let mut probes: Vec<f32> = vec![
480 0.0,
481 -0.0,
482 0.5,
483 -0.5,
484 1.0,
485 -1.0,
486 8_388_607.5,
487 -8_388_607.5,
488 8_388_608.0,
489 -8_388_608.0,
490 1.0e30,
491 -1.0e30,
492 f32::INFINITY,
493 f32::NEG_INFINITY,
494 f32::MIN_POSITIVE,
495 -f32::MIN_POSITIVE,
496 ];
497 for i in -4000..4000 {
498 probes.push(i as f32 * 0.01737);
499 probes.push(i as f32 * PI);
500 }
501 for x in probes {
502 assert_eq!(
503 exact_floor(x).to_bits(),
504 x.floor().to_bits(),
505 "exact_floor({x}) diverged from floorf"
506 );
507 }
508 assert!(exact_floor(f32::NAN).is_nan());
509 }
510
511 #[test]
512 fn stroke_builders_compose() {
513 let stroke = Stroke::new(4.0)
514 .with_cap(StrokeCap::Round)
515 .with_join(StrokeJoin::Bevel);
516 assert_eq!(stroke.width, 4.0);
517 assert_eq!(stroke.cap, StrokeCap::Round);
518 assert_eq!(stroke.join, StrokeJoin::Bevel);
519 assert_eq!(stroke.half_width(), 2.0);
520 assert!(stroke.is_visible());
521 assert_eq!(Stroke::default(), Stroke::new(1.0));
522 assert_eq!(Stroke::new(4.0).with_width(6.0).width, 6.0);
523 }
524
525 #[test]
526 fn stroke_rejects_non_positive_and_non_finite_widths() {
527 assert!(!Stroke::new(0.0).is_visible());
528 assert!(!Stroke::new(-3.0).is_visible());
529 assert!(!Stroke::new(f32::NAN).is_visible());
530 assert!(!Stroke::new(f32::INFINITY).is_visible());
531 assert_eq!(Stroke::new(f32::NAN).half_width(), 0.0);
532 assert_eq!(Stroke::new(-3.0).half_width(), 0.0);
533 }
534
535 #[test]
536 fn arc_geometry_normalizes_negative_sweeps() {
537 let arc = ArcGeometry::new(Point::ZERO, 1.0, 2.0, PI, -FRAC_PI_2, StrokeCap::Butt);
538 assert!(approx(arc.start_angle, PI - FRAC_PI_2));
539 assert!(approx(arc.sweep_angle, FRAC_PI_2));
540 }
541
542 #[test]
543 fn arc_geometry_clamps_full_turns_and_forces_round_caps() {
544 let arc = ArcGeometry::new(Point::ZERO, 1.0, 2.0, 0.3, TAU * 3.0, StrokeCap::Butt);
545 assert_eq!(arc.sweep_angle, TAU);
546 assert_eq!(
547 arc.cap,
548 StrokeCap::Round,
549 "a closed ring must not clip its (invisible) caps"
550 );
551 assert!(arc.contains_angle(0.0));
552 assert!(arc.contains_angle(PI));
553 }
554
555 #[test]
556 fn arc_geometry_sanitizes_non_finite_input() {
557 for arc in [
558 ArcGeometry::new(
559 Point::new(f32::NAN, 0.0),
560 1.0,
561 2.0,
562 0.0,
563 1.0,
564 StrokeCap::Butt,
565 ),
566 ArcGeometry::new(Point::ZERO, f32::NAN, 2.0, 0.0, 1.0, StrokeCap::Butt),
567 ArcGeometry::new(Point::ZERO, 1.0, f32::INFINITY, 0.0, 1.0, StrokeCap::Butt),
568 ArcGeometry::new(Point::ZERO, 1.0, 2.0, f32::NAN, 1.0, StrokeCap::Butt),
569 ArcGeometry::new(Point::ZERO, 1.0, 2.0, 0.0, f32::NAN, StrokeCap::Butt),
570 ] {
571 assert!(arc.is_degenerate());
572 let bounds = arc.bounds();
573 for value in [bounds.x, bounds.y, bounds.width, bounds.height] {
574 assert!(value.is_finite(), "degenerate arc bounds must stay finite");
575 }
576 }
577 }
578
579 #[test]
580 fn approximate_bounds_contain_the_exact_box_within_documented_slack() {
581 for radius in [2.0f32, 10.0, 57.0, 204.0] {
582 for cap in [StrokeCap::Butt, StrokeCap::Round, StrokeCap::Square] {
583 for step in 0..48 {
584 let start = step as f32 * (TAU / 48.0) * 1.031;
585 for sweep in [0.05f32, 0.9, FRAC_PI_2, 3.6] {
586 let arc = ArcGeometry::new(
587 Point::new(11.0, -7.0),
588 radius * 0.55,
589 radius,
590 start,
591 sweep,
592 cap,
593 );
594 if arc.is_degenerate() {
595 continue;
596 }
597 let bounds = arc.bounds();
598 let exact = exact_bounds(&arc);
599 let slack =
600 (arc.outer_radius + arc.half_thickness()) * FAST_TRIG_ERR * 2.0 + 0.05;
601 assert!(
602 bounds.x <= exact.x + 1e-3
603 && bounds.y <= exact.y + 1e-3
604 && bounds.x + bounds.width >= exact.x + exact.width - 1e-3
605 && bounds.y + bounds.height >= exact.y + exact.height - 1e-3,
606 "approximate box lost containment: {bounds:?} vs exact {exact:?} \
607 (radius {radius}, start {start}, sweep {sweep}, cap {cap:?})"
608 );
609 assert!(
610 (bounds.x - exact.x).abs() <= slack
611 && (bounds.y - exact.y).abs() <= slack
612 && (bounds.width - exact.width).abs() <= 2.0 * slack
613 && (bounds.height - exact.height).abs() <= 2.0 * slack,
614 "approximate box drifted past its slack: {bounds:?} vs exact \
615 {exact:?} slack {slack} (radius {radius}, start {start}, sweep \
616 {sweep}, cap {cap:?})"
617 );
618 }
619 }
620 }
621 }
622 }
623
624 fn exact_bounds(arc: &ArcGeometry) -> Rect {
625 let mut min_x = f32::INFINITY;
626 let mut min_y = f32::INFINITY;
627 let mut max_x = f32::NEG_INFINITY;
628 let mut max_y = f32::NEG_INFINITY;
629 let mut include = |x: f32, y: f32| {
630 min_x = min_x.min(x);
631 min_y = min_y.min(y);
632 max_x = max_x.max(x);
633 max_y = max_y.max(y);
634 };
635 let rb = arc.half_thickness();
636 let ra = arc.mid_radius();
637 let end_angle = arc.start_angle + arc.sweep_angle;
638 for (angle, outward) in [(arc.start_angle, -1.0f32), (end_angle, 1.0f32)] {
639 let (sin, cos) = angle.sin_cos();
640 match arc.cap {
641 StrokeCap::Butt => {
642 include(
643 arc.center.x + cos * arc.inner_radius,
644 arc.center.y + sin * arc.inner_radius,
645 );
646 include(
647 arc.center.x + cos * arc.outer_radius,
648 arc.center.y + sin * arc.outer_radius,
649 );
650 }
651 StrokeCap::Square => {
652 let tx = -sin * rb * outward;
653 let ty = cos * rb * outward;
654 include(
655 arc.center.x + cos * arc.inner_radius + tx,
656 arc.center.y + sin * arc.inner_radius + ty,
657 );
658 include(
659 arc.center.x + cos * arc.outer_radius + tx,
660 arc.center.y + sin * arc.outer_radius + ty,
661 );
662 }
663 StrokeCap::Round => {
664 let cx = arc.center.x + cos * ra;
665 let cy = arc.center.y + sin * ra;
666 include(cx - rb, cy - rb);
667 include(cx + rb, cy + rb);
668 }
669 }
670 }
671 const AXIS_DIRECTIONS: [(f32, f32); 4] = [(0.0, 1.0), (1.0, 0.0), (0.0, -1.0), (-1.0, 0.0)];
672 for (quadrant, (sin, cos)) in AXIS_DIRECTIONS.into_iter().enumerate() {
673 let angle = quadrant as f32 * FRAC_PI_2;
674 if arc.contains_angle(angle) {
675 include(
676 arc.center.x + cos * arc.outer_radius,
677 arc.center.y + sin * arc.outer_radius,
678 );
679 }
680 }
681 Rect {
682 x: min_x,
683 y: min_y,
684 width: (max_x - min_x).max(0.0),
685 height: (max_y - min_y).max(0.0),
686 }
687 }
688
689 #[test]
690 fn arc_geometry_flags_degenerate_bands() {
691 assert!(ArcGeometry::new(Point::ZERO, 5.0, 5.0, 0.0, 1.0, StrokeCap::Butt).is_degenerate());
692 assert!(ArcGeometry::new(Point::ZERO, 9.0, 5.0, 0.0, 1.0, StrokeCap::Butt).is_degenerate());
693 assert!(ArcGeometry::new(Point::ZERO, 1.0, 5.0, 0.0, 0.0, StrokeCap::Butt).is_degenerate());
694 assert!(ArcGeometry::new(Point::ZERO, 0.0, 0.0, 0.0, 1.0, StrokeCap::Butt).is_degenerate());
695 }
696
697 #[test]
698 fn arc_bounds_quarter_sweep_hugs_the_quadrant() {
699 let arc = ArcGeometry::new(
700 Point::new(100.0, 100.0),
701 0.0,
702 10.0,
703 0.0,
704 FRAC_PI_2,
705 StrokeCap::Butt,
706 );
707 let bounds = arc.bounds();
708 assert!(approx(bounds.x, 100.0), "{bounds:?}");
709 assert!(approx(bounds.y, 100.0), "{bounds:?}");
710 assert!(approx(bounds.width, 10.0), "{bounds:?}");
711 assert!(approx(bounds.height, 10.0), "{bounds:?}");
712 }
713
714 #[test]
715 fn arc_bounds_three_quarter_sweep_spans_every_axis_it_crosses() {
716 let arc = ArcGeometry::new(
717 Point::new(0.0, 0.0),
718 0.0,
719 10.0,
720 0.0,
721 3.0 * FRAC_PI_2,
722 StrokeCap::Butt,
723 );
724 let bounds = arc.bounds();
725 assert!(approx(bounds.x, -10.0), "{bounds:?}");
726 assert!(approx(bounds.y, -10.0), "{bounds:?}");
727 assert!(approx(bounds.width, 20.0), "{bounds:?}");
728 assert!(approx(bounds.height, 20.0), "{bounds:?}");
729 }
730
731 #[test]
732 fn arc_bounds_include_inner_endpoints_when_no_axis_is_crossed() {
733 let arc = ArcGeometry::new(
734 Point::ZERO,
735 8.0,
736 10.0,
737 std::f32::consts::FRAC_PI_4,
738 FRAC_PI_2,
739 StrokeCap::Butt,
740 );
741 let bounds = arc.bounds();
742 let sqrt2_2 = std::f32::consts::FRAC_1_SQRT_2;
743 assert!(approx(bounds.y, 8.0 * sqrt2_2), "{bounds:?}");
744 assert!(approx(bounds.y + bounds.height, 10.0), "{bounds:?}");
745 assert!(approx(bounds.x, -10.0 * sqrt2_2), "{bounds:?}");
746 assert!(approx(bounds.width, 20.0 * sqrt2_2), "{bounds:?}");
747 }
748
749 #[test]
750 fn arc_bounds_negative_sweep_matches_equivalent_positive_sweep() {
751 let forward = ArcGeometry::new(Point::ZERO, 4.0, 6.0, 0.0, FRAC_PI_2, StrokeCap::Butt);
752 let backward = ArcGeometry::new(
753 Point::ZERO,
754 4.0,
755 6.0,
756 FRAC_PI_2,
757 -FRAC_PI_2,
758 StrokeCap::Butt,
759 );
760 assert_eq!(forward.bounds(), backward.bounds());
761 }
762
763 #[test]
764 fn arc_bounds_full_turn_is_the_outer_circle() {
765 let arc = ArcGeometry::new(Point::new(5.0, 7.0), 3.0, 9.0, 1.1, TAU, StrokeCap::Butt);
766 let bounds = arc.bounds();
767 assert!(approx(bounds.x, -4.0), "{bounds:?}");
768 assert!(approx(bounds.y, -2.0), "{bounds:?}");
769 assert!(approx(bounds.width, 18.0), "{bounds:?}");
770 assert!(approx(bounds.height, 18.0), "{bounds:?}");
771 }
772
773 #[test]
774 fn arc_bounds_round_caps_bulge_past_the_radial_ends() {
775 let butt = ArcGeometry::new(Point::ZERO, 8.0, 12.0, 0.0, FRAC_PI_2, StrokeCap::Butt);
776 let round = ArcGeometry::new(Point::ZERO, 8.0, 12.0, 0.0, FRAC_PI_2, StrokeCap::Round);
777 let butt_bounds = butt.bounds();
778 let round_bounds = round.bounds();
779 assert!(approx(butt_bounds.y, 0.0), "{butt_bounds:?}");
780 assert!(approx(round_bounds.y, -2.0), "{round_bounds:?}");
781 assert!(round_bounds.width >= butt_bounds.width);
782 assert!(round_bounds.height >= butt_bounds.height);
783 }
784
785 #[test]
786 fn arc_bounds_square_caps_project_along_the_tangent() {
787 let square = ArcGeometry::new(Point::ZERO, 8.0, 12.0, 0.0, FRAC_PI_2, StrokeCap::Square);
788 let bounds = square.bounds();
789 assert!(approx(bounds.y, -2.0), "{bounds:?}");
790 assert!(approx(bounds.x + bounds.width, 12.0), "{bounds:?}");
791 }
792
793 #[test]
794 fn arc_band_resolves_stroked_and_filled_forms() {
795 let (inner, outer, cap) =
796 arc_band(10.0, 0.0, Some(Stroke::new(4.0).with_cap(StrokeCap::Round)));
797 assert_eq!((inner, outer), (8.0, 12.0));
798 assert_eq!(cap, StrokeCap::Round);
799
800 let (inner, outer, cap) = arc_band(10.0, 6.0, None);
801 assert_eq!((inner, outer), (6.0, 10.0));
802 assert_eq!(cap, StrokeCap::Butt);
803
804 let (inner, outer, _) = arc_band(10.0, 40.0, None);
805 assert_eq!((inner, outer), (10.0, 10.0));
806
807 let (inner, outer, _) = arc_band(1.0, 0.0, Some(Stroke::new(10.0)));
808 assert_eq!((inner, outer), (0.0, 6.0));
809 }
810
811 #[test]
812 fn full_ring_bounds_shortcut_matches_the_endpoint_walk() {
813 let ring = ArcGeometry::new(Point::new(10.0, -4.0), 6.0, 9.0, 1.3, TAU, StrokeCap::Butt);
814 assert_eq!(
815 ring.bounds(),
816 Rect {
817 x: 1.0,
818 y: -13.0,
819 width: 18.0,
820 height: 18.0
821 }
822 );
823
824 let square = ArcGeometry {
825 cap: StrokeCap::Square,
826 start_angle: TAU - (1.5f32 / 9.0).atan(),
827 ..ring
828 };
829 let bounds = square.bounds();
830 assert!(bounds.x + bounds.width > square.center.x + square.outer_radius);
831 }
832
833 #[test]
834 fn inflate_rect_ignores_non_positive_amounts() {
835 let rect = Rect {
836 x: 1.0,
837 y: 2.0,
838 width: 3.0,
839 height: 4.0,
840 };
841 assert_eq!(inflate_rect(rect, 0.0), rect);
842 assert_eq!(inflate_rect(rect, -1.0), rect);
843 assert_eq!(inflate_rect(rect, f32::NAN), rect);
844 assert_eq!(
845 inflate_rect(rect, 1.0),
846 Rect {
847 x: 0.0,
848 y: 1.0,
849 width: 5.0,
850 height: 6.0
851 }
852 );
853 }
854}