1#![allow(clippy::unwrap_used, clippy::disallowed_methods)]
2use serde::{Deserialize, Serialize};
30use std::f64::consts::PI;
31
32#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
34pub struct Point2D {
35 pub x: f64,
36 pub y: f64,
37}
38
39impl Point2D {
40 #[must_use]
42 pub const fn new(x: f64, y: f64) -> Self {
43 Self { x, y }
44 }
45
46 pub const ORIGIN: Self = Self { x: 0.0, y: 0.0 };
48
49 #[must_use]
51 pub fn distance(&self, other: &Self) -> f64 {
52 let dx = self.x - other.x;
53 let dy = self.y - other.y;
54 dx.hypot(dy)
55 }
56
57 #[must_use]
59 pub fn lerp(&self, other: &Self, t: f64) -> Self {
60 Self {
61 x: (other.x - self.x).mul_add(t, self.x),
62 y: (other.y - self.y).mul_add(t, self.y),
63 }
64 }
65}
66
67impl std::ops::Add for Point2D {
68 type Output = Self;
69
70 fn add(self, rhs: Self) -> Self::Output {
71 Self {
72 x: self.x + rhs.x,
73 y: self.y + rhs.y,
74 }
75 }
76}
77
78impl std::ops::Sub for Point2D {
79 type Output = Self;
80
81 fn sub(self, rhs: Self) -> Self::Output {
82 Self {
83 x: self.x - rhs.x,
84 y: self.y - rhs.y,
85 }
86 }
87}
88
89impl std::ops::Mul<f64> for Point2D {
90 type Output = Self;
91
92 fn mul(self, rhs: f64) -> Self::Output {
93 Self {
94 x: self.x * rhs,
95 y: self.y * rhs,
96 }
97 }
98}
99
100pub trait Interpolator {
102 fn interpolate(&self, x: f64) -> f64;
104
105 fn sample(&self, start: f64, end: f64, num_points: usize) -> Vec<Point2D> {
107 if num_points < 2 {
108 return vec![];
109 }
110 let step = (end - start) / (num_points - 1) as f64;
111 (0..num_points)
112 .map(|i| {
113 let x = (i as f64).mul_add(step, start);
114 Point2D::new(x, self.interpolate(x))
115 })
116 .collect()
117 }
118}
119
120#[derive(Debug, Clone)]
122pub struct LinearInterpolator {
123 points: Vec<Point2D>,
124}
125
126impl LinearInterpolator {
127 #[must_use]
129 pub fn from_points(points: &[Point2D]) -> Self {
130 let mut sorted = points.to_vec();
131 sorted.sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap_or(std::cmp::Ordering::Equal));
132 Self { points: sorted }
133 }
134
135 #[must_use]
137 pub fn from_xy(xs: &[f64], ys: &[f64]) -> Self {
138 let points: Vec<_> = xs
139 .iter()
140 .zip(ys.iter())
141 .map(|(&x, &y)| Point2D::new(x, y))
142 .collect();
143 Self::from_points(&points)
144 }
145
146 #[must_use]
148 pub fn points(&self) -> &[Point2D] {
149 &self.points
150 }
151
152 fn find_segment(&self, x: f64) -> Option<(usize, f64)> {
154 if self.points.len() < 2 {
155 return None;
156 }
157 for i in 0..self.points.len() - 1 {
158 let p1 = &self.points[i];
159 let p2 = &self.points[i + 1];
160 if x >= p1.x && x <= p2.x {
161 let t = if (p2.x - p1.x).abs() < 1e-10 {
162 0.0
163 } else {
164 (x - p1.x) / (p2.x - p1.x)
165 };
166 return Some((i, t));
167 }
168 }
169 if x < self.points[0].x {
171 Some((
172 0,
173 (x - self.points[0].x) / (self.points[1].x - self.points[0].x),
174 ))
175 } else {
176 let n = self.points.len();
177 Some((
178 n - 2,
179 (x - self.points[n - 2].x) / (self.points[n - 1].x - self.points[n - 2].x),
180 ))
181 }
182 }
183}
184
185impl Interpolator for LinearInterpolator {
186 fn interpolate(&self, x: f64) -> f64 {
187 if self.points.is_empty() {
188 return 0.0;
189 }
190 if self.points.len() == 1 {
191 return self.points[0].y;
192 }
193
194 if let Some((i, t)) = self.find_segment(x) {
195 let p1 = &self.points[i];
196 let p2 = &self.points[i + 1];
197 (p2.y - p1.y).mul_add(t, p1.y)
198 } else {
199 0.0
200 }
201 }
202}
203
204#[derive(Debug, Clone)]
206pub struct CubicSpline {
207 points: Vec<Point2D>,
208 y2: Vec<f64>,
210}
211
212impl CubicSpline {
213 #[must_use]
215 pub fn from_points(points: &[Point2D]) -> Self {
216 let mut sorted = points.to_vec();
217 sorted.sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap_or(std::cmp::Ordering::Equal));
218
219 let n = sorted.len();
220 if n < 3 {
221 return Self {
222 points: sorted,
223 y2: vec![0.0; n],
224 };
225 }
226
227 let mut y2 = vec![0.0; n];
229 let mut u = vec![0.0; n];
230
231 for i in 1..n - 1 {
233 let h_prev = sorted[i].x - sorted[i - 1].x;
234 let h_next = sorted[i + 1].x - sorted[i].x;
235
236 if h_prev.abs() < 1e-10 || h_next.abs() < 1e-10 {
237 continue;
238 }
239
240 let sig = h_prev / (h_prev + h_next);
241 let p = sig.mul_add(y2[i - 1], 2.0);
242 y2[i] = (sig - 1.0) / p;
243 u[i] =
244 (sorted[i + 1].y - sorted[i].y) / h_next - (sorted[i].y - sorted[i - 1].y) / h_prev;
245 u[i] = sig.mul_add(-u[i - 1], 6.0 * u[i] / (h_prev + h_next)) / p;
246 }
247
248 for k in (0..n - 1).rev() {
250 y2[k] = y2[k].mul_add(y2[k + 1], u[k]);
251 }
252
253 Self { points: sorted, y2 }
254 }
255
256 #[must_use]
258 pub fn from_xy(xs: &[f64], ys: &[f64]) -> Self {
259 let points: Vec<_> = xs
260 .iter()
261 .zip(ys.iter())
262 .map(|(&x, &y)| Point2D::new(x, y))
263 .collect();
264 Self::from_points(&points)
265 }
266
267 #[must_use]
269 pub fn points(&self) -> &[Point2D] {
270 &self.points
271 }
272}
273
274impl Interpolator for CubicSpline {
275 fn interpolate(&self, x: f64) -> f64 {
276 let n = self.points.len();
277 if n == 0 {
278 return 0.0;
279 }
280 if n == 1 {
281 return self.points[0].y;
282 }
283 if n == 2 {
284 let t = (x - self.points[0].x) / (self.points[1].x - self.points[0].x);
286 return (self.points[1].y - self.points[0].y).mul_add(t, self.points[0].y);
287 }
288
289 let mut lo = 0;
291 let mut hi = n - 1;
292 while hi - lo > 1 {
293 let mid = (hi + lo) / 2;
294 if self.points[mid].x > x {
295 hi = mid;
296 } else {
297 lo = mid;
298 }
299 }
300
301 let h = self.points[hi].x - self.points[lo].x;
302 if h.abs() < 1e-10 {
303 return self.points[lo].y;
304 }
305
306 let a = (self.points[hi].x - x) / h;
307 let b = (x - self.points[lo].x) / h;
308
309 a.mul_add(self.points[lo].y, b * self.points[hi].y)
310 + (a * a)
311 .mul_add(a, -a)
312 .mul_add(self.y2[lo], (b * b).mul_add(b, -b) * self.y2[hi])
313 * h
314 * h
315 / 6.0
316 }
317}
318
319#[derive(Debug, Clone)]
321pub struct CatmullRom {
322 points: Vec<Point2D>,
323 tension: f64,
325}
326
327impl CatmullRom {
328 #[must_use]
330 pub fn from_points(points: &[Point2D]) -> Self {
331 Self::with_tension(points, 0.5)
332 }
333
334 #[must_use]
336 pub fn with_tension(points: &[Point2D], tension: f64) -> Self {
337 let mut sorted = points.to_vec();
338 sorted.sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap_or(std::cmp::Ordering::Equal));
339 Self {
340 points: sorted,
341 tension: tension.clamp(0.0, 1.0),
342 }
343 }
344
345 #[must_use]
347 pub fn points(&self) -> &[Point2D] {
348 &self.points
349 }
350
351 #[must_use]
353 pub fn to_path(&self, segments_per_span: usize) -> Vec<Point2D> {
354 if self.points.len() < 2 {
355 return self.points.clone();
356 }
357 if self.points.len() == 2 {
358 return self.points.clone();
359 }
360
361 let mut path = Vec::new();
362 let n = self.points.len();
363
364 for i in 0..n - 1 {
365 let p0 = if i == 0 {
366 self.points[0]
367 } else {
368 self.points[i - 1]
369 };
370 let p1 = self.points[i];
371 let p2 = self.points[i + 1];
372 let p3 = if i + 2 < n {
373 self.points[i + 2]
374 } else {
375 self.points[n - 1]
376 };
377
378 for j in 0..segments_per_span {
379 let t = j as f64 / segments_per_span as f64;
380 let point = self.catmull_rom_point(p0, p1, p2, p3, t);
381 path.push(point);
382 }
383 }
384
385 path.push(self.points[n - 1]);
387 path
388 }
389
390 fn catmull_rom_point(
392 &self,
393 p0: Point2D,
394 p1: Point2D,
395 p2: Point2D,
396 p3: Point2D,
397 t: f64,
398 ) -> Point2D {
399 let t2 = t * t;
400 let t3 = t2 * t;
401
402 let tau = self.tension;
403
404 let c0 = tau.mul_add(-t, (-tau).mul_add(t3, 2.0 * tau * t2));
406 let c1 = (2.0 - tau).mul_add(t3, (tau - 3.0) * t2) + 1.0;
407 let c2 = tau.mul_add(t, (tau - 2.0).mul_add(t3, 2.0f64.mul_add(-tau, 3.0) * t2));
408 let c3 = tau.mul_add(t3, -(tau * t2));
409
410 Point2D::new(
411 c3.mul_add(p3.x, c0 * p0.x + c1 * p1.x + c2 * p2.x),
412 c3.mul_add(p3.y, c0 * p0.y + c1 * p1.y + c2 * p2.y),
413 )
414 }
415}
416
417impl Interpolator for CatmullRom {
418 fn interpolate(&self, x: f64) -> f64 {
419 if self.points.is_empty() {
421 return 0.0;
422 }
423 if self.points.len() == 1 {
424 return self.points[0].y;
425 }
426
427 let mut idx = 0;
429 for i in 0..self.points.len() - 1 {
430 if x >= self.points[i].x && x <= self.points[i + 1].x {
431 idx = i;
432 break;
433 }
434 if x < self.points[i].x {
435 idx = i.saturating_sub(1);
436 break;
437 }
438 idx = i;
439 }
440
441 let p1 = &self.points[idx];
442 let p2 = &self.points[(idx + 1).min(self.points.len() - 1)];
443
444 let t = if (p2.x - p1.x).abs() < 1e-10 {
445 0.0
446 } else {
447 ((x - p1.x) / (p2.x - p1.x)).clamp(0.0, 1.0)
448 };
449
450 let p0 = if idx == 0 { *p1 } else { self.points[idx - 1] };
451 let p3 = if idx + 2 < self.points.len() {
452 self.points[idx + 2]
453 } else {
454 *p2
455 };
456
457 self.catmull_rom_point(p0, *p1, *p2, p3, t).y
458 }
459}
460
461#[derive(Debug, Clone, Copy, PartialEq)]
463pub struct CubicBezier {
464 pub p0: Point2D,
466 pub p1: Point2D,
468 pub p2: Point2D,
470 pub p3: Point2D,
472}
473
474impl CubicBezier {
475 #[must_use]
477 pub const fn new(p0: Point2D, p1: Point2D, p2: Point2D, p3: Point2D) -> Self {
478 Self { p0, p1, p2, p3 }
479 }
480
481 #[must_use]
483 pub fn evaluate(&self, t: f64) -> Point2D {
484 let t = t.clamp(0.0, 1.0);
485 let mt = 1.0 - t;
486 let mt2 = mt * mt;
487 let mt3 = mt2 * mt;
488 let t2 = t * t;
489 let t3 = t2 * t;
490
491 Point2D::new(
492 (3.0 * mt * t2).mul_add(self.p2.x, mt3 * self.p0.x + 3.0 * mt2 * t * self.p1.x)
493 + t3 * self.p3.x,
494 (3.0 * mt * t2).mul_add(self.p2.y, mt3 * self.p0.y + 3.0 * mt2 * t * self.p1.y)
495 + t3 * self.p3.y,
496 )
497 }
498
499 #[must_use]
501 pub fn to_polyline(&self, segments: usize) -> Vec<Point2D> {
502 let segments = segments.max(1);
503 (0..=segments)
504 .map(|i| self.evaluate(i as f64 / segments as f64))
505 .collect()
506 }
507
508 #[must_use]
510 pub fn arc_length(&self, segments: usize) -> f64 {
511 let points = self.to_polyline(segments);
512 points.windows(2).map(|w| w[0].distance(&w[1])).sum()
513 }
514
515 #[must_use]
517 pub fn split(&self, t: f64) -> (Self, Self) {
518 let t = t.clamp(0.0, 1.0);
519
520 let p01 = self.p0.lerp(&self.p1, t);
522 let p12 = self.p1.lerp(&self.p2, t);
523 let p23 = self.p2.lerp(&self.p3, t);
524
525 let p012 = p01.lerp(&p12, t);
526 let p123 = p12.lerp(&p23, t);
527
528 let p0123 = p012.lerp(&p123, t);
529
530 let left = Self::new(self.p0, p01, p012, p0123);
531 let right = Self::new(p0123, p123, p23, self.p3);
532
533 (left, right)
534 }
535}
536
537#[derive(Debug, Clone)]
539pub struct HistogramBins {
540 pub edges: Vec<f64>,
542 pub counts: Vec<usize>,
544 pub densities: Vec<f64>,
546}
547
548impl HistogramBins {
549 #[must_use]
551 pub fn from_data(data: &[f64], num_bins: usize) -> Self {
552 if data.is_empty() || num_bins == 0 {
553 return Self {
554 edges: vec![],
555 counts: vec![],
556 densities: vec![],
557 };
558 }
559
560 let num_bins = num_bins.max(1);
561 let min = data.iter().copied().fold(f64::INFINITY, f64::min);
562 let max = data.iter().copied().fold(f64::NEG_INFINITY, f64::max);
563
564 Self::from_data_range(data, num_bins, min, max)
565 }
566
567 #[must_use]
569 pub fn from_data_range(data: &[f64], num_bins: usize, min: f64, max: f64) -> Self {
570 let num_bins = num_bins.max(1);
571 let range = (max - min).max(1e-10);
572 let bin_width = range / num_bins as f64;
573
574 let edges: Vec<f64> = (0..=num_bins)
576 .map(|i| (i as f64).mul_add(bin_width, min))
577 .collect();
578
579 let mut counts = vec![0usize; num_bins];
581 for &value in data {
582 let bin = ((value - min) / bin_width).floor() as usize;
583 let bin = bin.min(num_bins - 1); counts[bin] += 1;
585 }
586
587 let total = data.len() as f64;
589 let densities: Vec<f64> = counts
590 .iter()
591 .map(|&c| (c as f64) / (total * bin_width))
592 .collect();
593
594 Self {
595 edges,
596 counts,
597 densities,
598 }
599 }
600
601 #[must_use]
603 pub fn num_bins(&self) -> usize {
604 self.counts.len()
605 }
606
607 #[must_use]
609 pub fn bin_width(&self) -> f64 {
610 if self.edges.len() < 2 {
611 return 0.0;
612 }
613 self.edges[1] - self.edges[0]
614 }
615
616 #[must_use]
618 pub fn bin_center(&self, index: usize) -> Option<f64> {
619 if index >= self.counts.len() {
620 return None;
621 }
622 Some((self.edges[index] + self.edges[index + 1]) / 2.0)
623 }
624
625 #[must_use]
627 pub fn bin_range(&self, index: usize) -> Option<(f64, f64)> {
628 if index >= self.counts.len() {
629 return None;
630 }
631 Some((self.edges[index], self.edges[index + 1]))
632 }
633
634 #[must_use]
636 pub fn total_count(&self) -> usize {
637 self.counts.iter().sum()
638 }
639
640 #[must_use]
642 pub fn max_count(&self) -> usize {
643 self.counts.iter().copied().max().unwrap_or(0)
644 }
645}
646
647#[derive(Debug, Clone, Copy, PartialEq)]
649pub struct ArcGeometry {
650 pub center: Point2D,
652 pub radius: f64,
654 pub start_angle: f64,
656 pub end_angle: f64,
658}
659
660impl ArcGeometry {
661 #[must_use]
663 pub const fn new(center: Point2D, radius: f64, start_angle: f64, end_angle: f64) -> Self {
664 Self {
665 center,
666 radius,
667 start_angle,
668 end_angle,
669 }
670 }
671
672 #[must_use]
674 pub fn circle(center: Point2D, radius: f64) -> Self {
675 Self::new(center, radius, 0.0, 2.0 * PI)
676 }
677
678 #[must_use]
680 pub fn sweep(&self) -> f64 {
681 self.end_angle - self.start_angle
682 }
683
684 #[must_use]
686 pub fn point_at_angle(&self, angle: f64) -> Point2D {
687 Point2D::new(
688 self.radius.mul_add(angle.cos(), self.center.x),
689 self.radius.mul_add(angle.sin(), self.center.y),
690 )
691 }
692
693 #[must_use]
695 pub fn start_point(&self) -> Point2D {
696 self.point_at_angle(self.start_angle)
697 }
698
699 #[must_use]
701 pub fn end_point(&self) -> Point2D {
702 self.point_at_angle(self.end_angle)
703 }
704
705 #[must_use]
707 pub fn mid_point(&self) -> Point2D {
708 let mid_angle = (self.start_angle + self.end_angle) / 2.0;
709 self.point_at_angle(mid_angle)
710 }
711
712 #[must_use]
714 pub fn arc_length(&self) -> f64 {
715 self.radius * self.sweep().abs()
716 }
717
718 #[must_use]
720 pub fn to_polyline(&self, segments: usize) -> Vec<Point2D> {
721 let segments = segments.max(1);
722 let sweep = self.sweep();
723 (0..=segments)
724 .map(|i| {
725 let t = i as f64 / segments as f64;
726 let angle = self.start_angle + t * sweep;
727 self.point_at_angle(angle)
728 })
729 .collect()
730 }
731
732 #[must_use]
734 pub fn to_pie_slice(&self, segments: usize) -> Vec<Point2D> {
735 let mut points = vec![self.center];
736 points.extend(self.to_polyline(segments));
737 points.push(self.center);
738 points
739 }
740
741 #[must_use]
743 pub fn contains_angle(&self, angle: f64) -> bool {
744 let normalized = Self::normalize_angle(angle);
745 let start = Self::normalize_angle(self.start_angle);
746 let end = Self::normalize_angle(self.end_angle);
747
748 if start <= end {
749 normalized >= start && normalized <= end
750 } else {
751 normalized >= start || normalized <= end
752 }
753 }
754
755 fn normalize_angle(angle: f64) -> f64 {
757 let mut a = angle % (2.0 * PI);
758 if a < 0.0 {
759 a += 2.0 * PI;
760 }
761 a
762 }
763}
764
765#[derive(Debug, Clone, Copy)]
767pub struct DataNormalizer {
768 pub min: f64,
770 pub max: f64,
772}
773
774impl DataNormalizer {
775 #[must_use]
777 pub fn new(min: f64, max: f64) -> Self {
778 Self { min, max }
779 }
780
781 #[must_use]
783 pub fn from_data(data: &[f64]) -> Self {
784 if data.is_empty() {
785 return Self::new(0.0, 1.0);
786 }
787 let min = data.iter().copied().fold(f64::INFINITY, f64::min);
788 let max = data.iter().copied().fold(f64::NEG_INFINITY, f64::max);
789 Self::new(min, max)
790 }
791
792 #[must_use]
794 pub fn normalize(&self, value: f64) -> f64 {
795 let range = self.max - self.min;
796 if range.abs() < 1e-10 {
797 return 0.5;
798 }
799 (value - self.min) / range
800 }
801
802 #[must_use]
804 pub fn denormalize(&self, normalized: f64) -> f64 {
805 normalized.mul_add(self.max - self.min, self.min)
806 }
807
808 #[must_use]
810 pub fn normalize_all(&self, data: &[f64]) -> Vec<f64> {
811 data.iter().map(|&v| self.normalize(v)).collect()
812 }
813
814 #[must_use]
816 pub fn nice_bounds(&self) -> (f64, f64) {
817 let range = self.max - self.min;
818 if range.abs() < 1e-10 {
819 return (self.min - 1.0, self.max + 1.0);
820 }
821
822 let magnitude = 10.0_f64.powf(range.log10().floor());
823 let nice_min = (self.min / magnitude).floor() * magnitude;
824 let nice_max = (self.max / magnitude).ceil() * magnitude;
825
826 (nice_min, nice_max)
827 }
828}
829
830#[derive(Debug, Clone, Default)]
832pub struct PathTessellator {
833 pub tolerance: f64,
835 pub vertices: Vec<[f32; 2]>,
837 pub indices: Vec<u32>,
839}
840
841impl PathTessellator {
842 #[must_use]
844 pub fn new(tolerance: f64) -> Self {
845 Self {
846 tolerance: tolerance.max(0.001),
847 vertices: Vec::new(),
848 indices: Vec::new(),
849 }
850 }
851
852 #[must_use]
854 pub fn with_default_tolerance() -> Self {
855 Self::new(0.25)
856 }
857
858 pub fn clear(&mut self) {
860 self.vertices.clear();
861 self.indices.clear();
862 }
863
864 pub fn tessellate_polygon(&mut self, points: &[Point2D]) {
866 if points.len() < 3 {
867 return;
868 }
869
870 let base_idx = self.vertices.len() as u32;
871
872 for p in points {
874 self.vertices.push([p.x as f32, p.y as f32]);
875 }
876
877 for i in 1..points.len() as u32 - 1 {
879 self.indices.push(base_idx);
880 self.indices.push(base_idx + i);
881 self.indices.push(base_idx + i + 1);
882 }
883 }
884
885 pub fn tessellate_stroke(&mut self, points: &[Point2D], width: f64) {
887 if points.len() < 2 {
888 return;
889 }
890
891 let half_width = width / 2.0;
892
893 for window in points.windows(2) {
894 let p1 = window[0];
895 let p2 = window[1];
896
897 let dx = p2.x - p1.x;
899 let dy = p2.y - p1.y;
900 let len = dx.hypot(dy);
901 if len < 1e-10 {
902 continue;
903 }
904
905 let nx = -dy / len * half_width;
906 let ny = dx / len * half_width;
907
908 let base_idx = self.vertices.len() as u32;
909
910 self.vertices.push([(p1.x + nx) as f32, (p1.y + ny) as f32]);
912 self.vertices.push([(p1.x - nx) as f32, (p1.y - ny) as f32]);
913 self.vertices.push([(p2.x + nx) as f32, (p2.y + ny) as f32]);
914 self.vertices.push([(p2.x - nx) as f32, (p2.y - ny) as f32]);
915
916 self.indices.push(base_idx);
918 self.indices.push(base_idx + 1);
919 self.indices.push(base_idx + 2);
920
921 self.indices.push(base_idx + 1);
922 self.indices.push(base_idx + 3);
923 self.indices.push(base_idx + 2);
924 }
925 }
926
927 pub fn tessellate_circle(&mut self, center: Point2D, radius: f64, segments: usize) {
929 let segments = segments.max(8);
930 let base_idx = self.vertices.len() as u32;
931
932 self.vertices.push([center.x as f32, center.y as f32]);
934
935 for i in 0..segments {
937 let angle = 2.0 * PI * i as f64 / segments as f64;
938 let x = radius.mul_add(angle.cos(), center.x);
939 let y = radius.mul_add(angle.sin(), center.y);
940 self.vertices.push([x as f32, y as f32]);
941 }
942
943 for i in 0..segments as u32 {
945 self.indices.push(base_idx); self.indices.push(base_idx + 1 + i);
947 self.indices.push(base_idx + 1 + (i + 1) % segments as u32);
948 }
949 }
950
951 pub fn tessellate_rect(&mut self, x: f64, y: f64, width: f64, height: f64) {
953 let base_idx = self.vertices.len() as u32;
954
955 self.vertices.push([x as f32, y as f32]);
956 self.vertices.push([(x + width) as f32, y as f32]);
957 self.vertices
958 .push([(x + width) as f32, (y + height) as f32]);
959 self.vertices.push([x as f32, (y + height) as f32]);
960
961 self.indices.push(base_idx);
963 self.indices.push(base_idx + 1);
964 self.indices.push(base_idx + 2);
965
966 self.indices.push(base_idx);
967 self.indices.push(base_idx + 2);
968 self.indices.push(base_idx + 3);
969 }
970
971 #[must_use]
973 pub fn vertex_count(&self) -> usize {
974 self.vertices.len()
975 }
976
977 #[must_use]
979 pub fn index_count(&self) -> usize {
980 self.indices.len()
981 }
982
983 #[must_use]
985 pub fn triangle_count(&self) -> usize {
986 self.indices.len() / 3
987 }
988}
989
990#[derive(Debug, Clone, Default)]
992pub struct DrawBatch {
993 pub circles: Vec<[f32; 7]>,
995 pub rects: Vec<[f32; 8]>,
997 pub lines: Vec<[f32; 9]>,
999}
1000
1001impl DrawBatch {
1002 #[must_use]
1004 pub fn new() -> Self {
1005 Self::default()
1006 }
1007
1008 pub fn add_circle(&mut self, x: f32, y: f32, radius: f32, r: f32, g: f32, b: f32, a: f32) {
1010 self.circles.push([x, y, radius, r, g, b, a]);
1011 }
1012
1013 #[allow(clippy::too_many_arguments)]
1015 pub fn add_rect(&mut self, x: f32, y: f32, w: f32, h: f32, r: f32, g: f32, b: f32, a: f32) {
1016 self.rects.push([x, y, w, h, r, g, b, a]);
1017 }
1018
1019 #[allow(clippy::too_many_arguments)]
1021 pub fn add_line(
1022 &mut self,
1023 x1: f32,
1024 y1: f32,
1025 x2: f32,
1026 y2: f32,
1027 width: f32,
1028 r: f32,
1029 g: f32,
1030 b: f32,
1031 a: f32,
1032 ) {
1033 self.lines.push([x1, y1, x2, y2, width, r, g, b, a]);
1034 }
1035
1036 pub fn clear(&mut self) {
1038 self.circles.clear();
1039 self.rects.clear();
1040 self.lines.clear();
1041 }
1042
1043 #[must_use]
1045 pub fn unbatched_draw_calls(&self) -> usize {
1046 self.circles.len() + self.rects.len() + self.lines.len()
1047 }
1048
1049 #[must_use]
1051 pub fn batched_draw_calls(&self) -> usize {
1052 let mut calls = 0;
1053 if !self.circles.is_empty() {
1054 calls += 1;
1055 }
1056 if !self.rects.is_empty() {
1057 calls += 1;
1058 }
1059 if !self.lines.is_empty() {
1060 calls += 1;
1061 }
1062 calls
1063 }
1064}