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