1use crate::{
19 Point, Rect,
20 float::{all_finite, at_least, within},
21};
22
23#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
25pub enum StrokeCap {
26 #[default]
28 Butt,
29 Round,
31 Square,
33}
34
35#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
37pub enum StrokeJoin {
38 #[default]
40 Miter,
41 Round,
43 Bevel,
45}
46
47#[derive(Clone, Copy, Debug, PartialEq)]
52pub struct Stroke {
53 pub width: f32,
56 pub cap: StrokeCap,
57 pub join: StrokeJoin,
58}
59
60impl Stroke {
61 pub const fn new(width: f32) -> Self {
63 Self {
64 width,
65 cap: StrokeCap::Butt,
66 join: StrokeJoin::Miter,
67 }
68 }
69
70 pub const fn with_width(mut self, width: f32) -> Self {
71 self.width = width;
72 self
73 }
74
75 pub const fn with_cap(mut self, cap: StrokeCap) -> Self {
76 self.cap = cap;
77 self
78 }
79
80 pub const fn with_join(mut self, join: StrokeJoin) -> Self {
81 self.join = join;
82 self
83 }
84
85 pub fn half_width(&self) -> f32 {
89 if self.width.is_finite() {
90 at_least(self.width * 0.5, 0.0)
91 } else {
92 0.0
93 }
94 }
95
96 pub fn is_visible(&self) -> bool {
98 self.width.to_bits().wrapping_sub(1) < f32::MAX.to_bits()
103 }
104
105 pub fn scaled(&self, scale: f32) -> Self {
107 Self {
108 width: self.width * scale,
109 ..*self
110 }
111 }
112}
113
114impl Default for Stroke {
115 fn default() -> Self {
116 Self::new(1.0)
117 }
118}
119
120pub const TAU: f32 = std::f32::consts::PI * 2.0;
122
123#[derive(Clone, Copy, Debug, PartialEq)]
136pub struct ArcGeometry {
137 pub center: Point,
138 pub inner_radius: f32,
139 pub outer_radius: f32,
140 pub start_angle: f32,
142 pub sweep_angle: f32,
144 pub cap: StrokeCap,
145}
146
147#[inline]
153fn exact_floor(x: f32) -> f32 {
154 if x == 0.0 {
155 return x;
156 }
157 if x.abs() < 8_388_608.0 {
158 let truncated = x as i32 as f32;
159 truncated - ((x < truncated) as i32 as f32)
160 } else {
161 x
162 }
163}
164
165#[inline]
171fn wrap_angle_tau(x: f32) -> f32 {
172 if (0.0..TAU).contains(&x) {
173 return x;
174 }
175 let wrapped = x - exact_floor(x * (1.0 / TAU)) * TAU;
176 if wrapped >= TAU {
177 wrapped - TAU
178 } else if wrapped < 0.0 {
179 0.0
180 } else {
181 wrapped
182 }
183}
184
185#[inline]
191fn fast_sin_cos(angle: f32) -> (f32, f32) {
192 use std::f32::consts::{FRAC_PI_2, PI};
193 #[inline]
194 fn fold_sin(x: f32) -> f32 {
195 const B: f32 = 4.0 / PI;
196 const C: f32 = -4.0 / (PI * PI);
197 let y = B * x + C * x * x.abs();
198 0.225 * (y * y.abs() - y) + y
199 }
200 let x = wrap_angle_tau(angle);
201 let x = if x > PI { x - TAU } else { x };
202 let mut c = x + FRAC_PI_2;
203 if c > PI {
204 c -= TAU;
205 }
206 (fold_sin(x), fold_sin(c))
207}
208
209const FAST_TRIG_ERR: f32 = 1.3e-3;
213
214impl ArcGeometry {
215 #[inline]
217 pub fn new(
218 center: Point,
219 inner_radius: f32,
220 outer_radius: f32,
221 start_angle: f32,
222 sweep_angle: f32,
223 cap: StrokeCap,
224 ) -> Self {
225 if !all_finite([
226 center.x,
227 center.y,
228 inner_radius,
229 outer_radius,
230 start_angle,
231 sweep_angle,
232 ]) {
233 return Self::DEGENERATE;
234 }
235 let outer = at_least(outer_radius, 0.0);
236 let inner = within(inner_radius, 0.0, outer);
237 Self::with_angles(center, inner, outer, start_angle, sweep_angle, cap)
238 }
239
240 #[inline]
243 pub(crate) fn of_band(
244 center: Point,
245 (inner, outer, cap): (f32, f32, StrokeCap),
246 start_angle: f32,
247 sweep_angle: f32,
248 ) -> Self {
249 if !all_finite([center.x, center.y, inner, outer, start_angle, sweep_angle]) {
250 return Self::DEGENERATE;
251 }
252 Self::with_angles(center, inner, outer, start_angle, sweep_angle, cap)
253 }
254
255 #[inline(always)]
258 fn with_angles(
259 center: Point,
260 inner: f32,
261 outer: f32,
262 start_angle: f32,
263 sweep_angle: f32,
264 cap: StrokeCap,
265 ) -> Self {
266 if sweep_angle.to_bits().wrapping_sub(1) < TAU.to_bits() - 1
271 && start_angle.to_bits() < TAU.to_bits()
272 {
273 return Self {
274 center,
275 inner_radius: inner,
276 outer_radius: outer,
277 start_angle,
278 sweep_angle,
279 cap,
280 };
281 }
282 Self::normalizing_angles(center, inner, outer, start_angle, sweep_angle, cap)
283 }
284
285 #[inline]
287 fn normalizing_angles(
288 center: Point,
289 inner: f32,
290 outer: f32,
291 start_angle: f32,
292 sweep_angle: f32,
293 cap: StrokeCap,
294 ) -> Self {
295 let (mut start, mut sweep) = if sweep_angle < 0.0 {
296 (start_angle + sweep_angle, -sweep_angle)
297 } else {
298 (start_angle, sweep_angle)
299 };
300 if sweep >= TAU {
301 sweep = TAU;
302 start = 0.0;
303 }
304 start = wrap_angle_tau(start);
305 if !start.is_finite() {
306 start = 0.0;
307 }
308 let cap = if sweep >= TAU { StrokeCap::Round } else { cap };
309
310 Self {
311 center,
312 inner_radius: inner,
313 outer_radius: outer,
314 start_angle: start,
315 sweep_angle: sweep,
316 cap,
317 }
318 }
319
320 const DEGENERATE: Self = Self {
321 center: Point::ZERO,
322 inner_radius: 0.0,
323 outer_radius: 0.0,
324 start_angle: 0.0,
325 sweep_angle: 0.0,
326 cap: StrokeCap::Butt,
327 };
328
329 pub fn mid_radius(&self) -> f32 {
331 (self.inner_radius + self.outer_radius) * 0.5
332 }
333
334 pub fn half_thickness(&self) -> f32 {
337 (self.outer_radius - self.inner_radius) * 0.5
338 }
339
340 pub fn is_degenerate(&self) -> bool {
342 !(self.outer_radius > 0.0
343 && self.outer_radius > self.inner_radius
344 && self.sweep_angle > 0.0)
345 }
346
347 pub fn contains_angle(&self, angle: f32) -> bool {
349 if self.sweep_angle >= TAU {
350 return true;
351 }
352 let delta = wrap_angle_tau(angle - self.start_angle);
353 delta <= self.sweep_angle + 1e-6
354 }
355
356 pub fn scaled_about(&self, center: Point, scale: f32) -> Self {
359 Self {
360 center,
361 inner_radius: self.inner_radius * scale,
362 outer_radius: self.outer_radius * scale,
363 ..*self
364 }
365 }
366
367 pub fn bounds(&self) -> Rect {
379 if self.is_degenerate() {
380 return Rect {
381 x: self.center.x,
382 y: self.center.y,
383 width: 0.0,
384 height: 0.0,
385 };
386 }
387
388 if self.sweep_angle >= TAU && self.cap != StrokeCap::Square {
389 let r = self.outer_radius;
390 return Rect {
391 x: self.center.x - r,
392 y: self.center.y - r,
393 width: r + r,
394 height: r + r,
395 };
396 }
397
398 let mut min_x = f32::INFINITY;
399 let mut min_y = f32::INFINITY;
400 let mut max_x = f32::NEG_INFINITY;
401 let mut max_y = f32::NEG_INFINITY;
402 let mut include = |x: f32, y: f32| {
403 min_x = min_x.min(x);
404 min_y = min_y.min(y);
405 max_x = max_x.max(x);
406 max_y = max_y.max(y);
407 };
408
409 let rb = self.half_thickness();
410 let ra = self.mid_radius();
411 let end_angle = self.start_angle + self.sweep_angle;
412
413 for (angle, outward) in [(self.start_angle, -1.0f32), (end_angle, 1.0f32)] {
414 let (sin, cos) = fast_sin_cos(angle);
415 match self.cap {
416 StrokeCap::Butt => {
417 include(
418 self.center.x + cos * self.inner_radius,
419 self.center.y + sin * self.inner_radius,
420 );
421 include(
422 self.center.x + cos * self.outer_radius,
423 self.center.y + sin * self.outer_radius,
424 );
425 }
426 StrokeCap::Square => {
427 let tx = -sin * rb * outward;
428 let ty = cos * rb * outward;
429 include(
430 self.center.x + cos * self.inner_radius + tx,
431 self.center.y + sin * self.inner_radius + ty,
432 );
433 include(
434 self.center.x + cos * self.outer_radius + tx,
435 self.center.y + sin * self.outer_radius + ty,
436 );
437 }
438 StrokeCap::Round => {
439 let cx = self.center.x + cos * ra;
440 let cy = self.center.y + sin * ra;
441 include(cx - rb, cy - rb);
442 include(cx + rb, cy + rb);
443 }
444 }
445 }
446
447 const AXIS_DIRECTIONS: [(f32, f32); 4] = [(0.0, 1.0), (1.0, 0.0), (0.0, -1.0), (-1.0, 0.0)];
448 for (quadrant, (sin, cos)) in AXIS_DIRECTIONS.into_iter().enumerate() {
449 let angle = quadrant as f32 * std::f32::consts::FRAC_PI_2;
450 if self.contains_angle(angle) {
451 include(
452 self.center.x + cos * self.outer_radius,
453 self.center.y + sin * self.outer_radius,
454 );
455 }
456 }
457
458 let pad = (self.outer_radius + rb) * FAST_TRIG_ERR + 0.02;
459 Rect {
460 x: min_x - pad,
461 y: min_y - pad,
462 width: (max_x - min_x + pad + pad).max(0.0),
463 height: (max_y - min_y + pad + pad).max(0.0),
464 }
465 }
466}
467
468#[inline]
479pub fn arc_band(radius: f32, inner_radius: f32, stroke: Option<Stroke>) -> (f32, f32, StrokeCap) {
480 match stroke {
481 Some(stroke) => {
482 if !radius.is_finite() || !stroke.is_visible() {
483 return (0.0, 0.0, stroke.cap);
484 }
485 let half = stroke.half_width();
486 let radius = at_least(radius, 0.0);
487 (at_least(radius - half, 0.0), radius + half, stroke.cap)
488 }
489 None => {
490 if !radius.is_finite() || !inner_radius.is_finite() {
491 return (0.0, 0.0, StrokeCap::Butt);
492 }
493 let outer = at_least(radius, 0.0);
494 let inner = within(inner_radius, 0.0, outer);
495 (inner, outer, StrokeCap::Butt)
496 }
497 }
498}
499
500pub fn inflate_rect(rect: Rect, amount: f32) -> Rect {
502 if !amount.is_finite() || amount <= 0.0 {
503 return rect;
504 }
505 Rect {
506 x: rect.x - amount,
507 y: rect.y - amount,
508 width: (rect.width + amount * 2.0).max(0.0),
509 height: (rect.height + amount * 2.0).max(0.0),
510 }
511}
512
513#[derive(Clone, Copy, Debug, PartialEq)]
517pub struct LineGeometry {
518 pub start: Point,
519 pub end: Point,
520 pub half_width: f32,
521 pub cap: StrokeCap,
522}
523
524#[derive(Clone, Copy, Debug, PartialEq)]
527pub struct LineFrame {
528 pub center: Point,
529 pub direction: Point,
530 pub half_length: f32,
531}
532
533impl LineGeometry {
534 pub fn new(start: Point, end: Point, stroke: Stroke) -> Self {
536 Self {
537 start,
538 end,
539 half_width: stroke.half_width(),
540 cap: stroke.cap,
541 }
542 }
543
544 pub fn is_degenerate(&self) -> bool {
548 !all_finite([self.start.x, self.start.y, self.end.x, self.end.y])
549 || !(self.half_width > 0.0 && self.half_width.is_finite())
550 || (self.cap == StrokeCap::Butt && self.start == self.end)
551 }
552
553 pub fn frame(&self) -> LineFrame {
557 let (dx, dy) = (self.end.x - self.start.x, self.end.y - self.start.y);
558 let length = (dx * dx + dy * dy).sqrt();
559 let direction = if length > 0.0 {
560 Point::new(dx / length, dy / length)
561 } else {
562 Point::new(1.0, 0.0)
563 };
564 LineFrame {
565 center: Point::new(
566 (self.start.x + self.end.x) * 0.5,
567 (self.start.y + self.end.y) * 0.5,
568 ),
569 direction,
570 half_length: length * 0.5,
571 }
572 }
573
574 pub fn cap_reach(&self) -> f32 {
577 if self.cap == StrokeCap::Butt {
578 0.0
579 } else {
580 self.half_width
581 }
582 }
583
584 pub fn end_bounds(&self) -> Rect {
586 let min_x = self.start.x.min(self.end.x);
587 let min_y = self.start.y.min(self.end.y);
588 Rect {
589 x: min_x,
590 y: min_y,
591 width: self.start.x.max(self.end.x) - min_x,
592 height: self.start.y.max(self.end.y) - min_y,
593 }
594 }
595
596 pub fn reach(&self) -> Point {
601 if self.cap == StrokeCap::Round {
602 return Point::new(self.half_width, self.half_width);
603 }
604 let frame = self.frame();
605 let (along_x, along_y) = (frame.direction.x.abs(), frame.direction.y.abs());
606 let cap = self.cap_reach();
607 Point::new(
608 along_x * cap + along_y * self.half_width,
609 along_y * cap + along_x * self.half_width,
610 )
611 }
612
613 pub fn bounds(&self) -> Rect {
616 let ends = self.end_bounds();
617 let reach = self.reach();
618 Rect {
619 x: ends.x - reach.x,
620 y: ends.y - reach.y,
621 width: ends.width + reach.x * 2.0,
622 height: ends.height + reach.y * 2.0,
623 }
624 }
625
626 pub fn placed(&self, start: Point, end: Point, scale: f32) -> Self {
629 Self {
630 start,
631 end,
632 half_width: self.half_width * scale,
633 ..*self
634 }
635 }
636
637 pub fn coverage(&self, point: Point) -> f32 {
643 let frame = self.frame();
644 let (dx, dy) = (point.x - frame.center.x, point.y - frame.center.y);
645 let along = (dx * frame.direction.x + dy * frame.direction.y).abs();
646 let across = (dx * frame.direction.y - dy * frame.direction.x).abs();
647 let across_coverage = (self.half_width + 0.5 - across).clamp(0.0, 1.0);
648 if self.cap == StrokeCap::Round {
649 if along <= frame.half_length {
650 return across_coverage;
651 }
652 let (past, side) = (along - frame.half_length, across);
653 let distance = (past * past + side * side).sqrt() - self.half_width;
654 return (0.5 - distance).clamp(0.0, 1.0);
655 }
656 let reach = frame.half_length + self.cap_reach();
657 across_coverage * (reach + 0.5 - along).clamp(0.0, 1.0)
658 }
659}
660
661#[cfg(test)]
662#[path = "tests/stroke_tests.rs"]
663mod tests;