1use egui::Color32;
34
35use crate::core::triangles::Triangles;
36
37#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40pub enum GridMajorOrder {
41 Row,
44 Column,
47}
48
49#[derive(Clone, Copy, Debug, PartialEq, Eq)]
52pub enum BinnedStatisticFunction {
53 Mean,
55 Count,
57 Sum,
59}
60
61#[derive(Clone, Debug, PartialEq, Eq)]
68pub struct Triangulation {
69 pub triangles: Vec<[usize; 3]>,
71}
72
73impl Triangulation {
74 #[must_use]
76 pub fn len(&self) -> usize {
77 self.triangles.len()
78 }
79
80 #[must_use]
82 pub fn is_empty(&self) -> bool {
83 self.triangles.is_empty()
84 }
85}
86
87fn orient2d(a: [f64; 2], b: [f64; 2], c: [f64; 2]) -> f64 {
90 (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])
91}
92
93fn in_circumcircle(a: [f64; 2], b: [f64; 2], c: [f64; 2], p: [f64; 2]) -> bool {
98 let ax = a[0] - p[0];
99 let ay = a[1] - p[1];
100 let bx = b[0] - p[0];
101 let by = b[1] - p[1];
102 let cx = c[0] - p[0];
103 let cy = c[1] - p[1];
104
105 let a2 = ax * ax + ay * ay;
106 let b2 = bx * bx + by * by;
107 let c2 = cx * cx + cy * cy;
108
109 let det = ax * (by * c2 - b2 * cy) - ay * (bx * c2 - b2 * cx) + a2 * (bx * cy - by * cx);
110 det > 0.0
111}
112
113#[must_use]
129pub fn delaunay(x: &[f64], y: &[f64]) -> Triangulation {
130 assert_eq!(x.len(), y.len(), "x and y must have the same length");
131
132 let pts: Vec<(usize, [f64; 2])> = x
135 .iter()
136 .zip(y)
137 .enumerate()
138 .filter(|&(_, (&xi, &yi))| xi.is_finite() && yi.is_finite())
139 .map(|(i, (&xi, &yi))| (i, [xi, yi]))
140 .collect();
141
142 if pts.len() < 3 {
143 return Triangulation { triangles: vec![] };
144 }
145
146 let p0 = pts[0].1;
149 let has_area = pts.iter().enumerate().any(|(i, &(_, pi))| {
150 pts[i + 1..]
151 .iter()
152 .any(|&(_, pj)| orient2d(p0, pi, pj).abs() > 0.0)
153 });
154 if !has_area {
155 return Triangulation { triangles: vec![] };
156 }
157
158 let (mut min_x, mut min_y, mut max_x, mut max_y) = (
160 f64::INFINITY,
161 f64::INFINITY,
162 f64::NEG_INFINITY,
163 f64::NEG_INFINITY,
164 );
165 for &(_, [px, py]) in &pts {
166 min_x = min_x.min(px);
167 min_y = min_y.min(py);
168 max_x = max_x.max(px);
169 max_y = max_y.max(py);
170 }
171 let dx = max_x - min_x;
172 let dy = max_y - min_y;
173 let d = dx.max(dy).max(f64::MIN_POSITIVE);
174 let mid_x = 0.5 * (min_x + max_x);
175 let mid_y = 0.5 * (min_y + max_y);
176 let st0 = [mid_x - 20.0 * d, mid_y - d];
179 let st1 = [mid_x, mid_y + 20.0 * d];
180 let st2 = [mid_x + 20.0 * d, mid_y - d];
181
182 let n = pts.len();
184 let mut verts: Vec<[f64; 2]> = pts.iter().map(|&(_, p)| p).collect();
185 verts.push(st0);
186 verts.push(st1);
187 verts.push(st2);
188 let s0 = n;
189 let s1 = n + 1;
190 let s2 = n + 2;
191
192 let mut tris: Vec<[usize; 3]> = vec![ccw(&verts, [s0, s1, s2])];
194
195 for ip in 0..n {
196 let p = verts[ip];
197
198 let mut bad: Vec<usize> = Vec::new();
200 for (ti, t) in tris.iter().enumerate() {
201 if in_circumcircle(verts[t[0]], verts[t[1]], verts[t[2]], p) {
202 bad.push(ti);
203 }
204 }
205
206 let mut boundary: Vec<[usize; 2]> = Vec::new();
209 for &bi in &bad {
210 let t = tris[bi];
211 for &(a, b) in &[(t[0], t[1]), (t[1], t[2]), (t[2], t[0])] {
212 let shared = bad
214 .iter()
215 .any(|&oi| oi != bi && triangle_has_edge(&tris[oi], a, b));
216 if !shared {
217 boundary.push([a, b]);
218 }
219 }
220 }
221
222 bad.sort_unstable();
224 for &bi in bad.iter().rev() {
225 tris.swap_remove(bi);
226 }
227
228 for [a, b] in boundary {
230 tris.push(ccw(&verts, [a, b, ip]));
231 }
232 }
233
234 let original: Vec<usize> = pts.iter().map(|&(i, _)| i).collect();
237 let triangles: Vec<[usize; 3]> = tris
238 .into_iter()
239 .filter(|t| t.iter().all(|&v| v < n))
240 .map(|t| [original[t[0]], original[t[1]], original[t[2]]])
241 .collect();
242
243 Triangulation { triangles }
244}
245
246fn ccw(verts: &[[f64; 2]], t: [usize; 3]) -> [usize; 3] {
248 if orient2d(verts[t[0]], verts[t[1]], verts[t[2]]) < 0.0 {
249 [t[0], t[2], t[1]]
250 } else {
251 t
252 }
253}
254
255fn triangle_has_edge(t: &[usize; 3], a: usize, b: usize) -> bool {
257 let edges = [(t[0], t[1]), (t[1], t[2]), (t[2], t[0])];
258 edges
259 .iter()
260 .any(|&(u, v)| (u == a && v == b) || (u == b && v == a))
261}
262
263#[must_use]
278pub fn solid_triangles(x: &[f64], y: &[f64], colors: &[Color32]) -> Option<Triangles> {
279 assert_eq!(x.len(), y.len(), "x and y must have the same length");
280 assert_eq!(
281 colors.len(),
282 x.len(),
283 "colors must have one entry per vertex"
284 );
285
286 let tri = delaunay(x, y);
287 if tri.is_empty() {
288 return None;
289 }
290
291 let indices: Vec<[u32; 3]> = tri
292 .triangles
293 .iter()
294 .map(|t| {
295 [
296 u32::try_from(t[0]).expect("vertex index fits in u32"),
297 u32::try_from(t[1]).expect("vertex index fits in u32"),
298 u32::try_from(t[2]).expect("vertex index fits in u32"),
299 ]
300 })
301 .collect();
302
303 Some(Triangles::new(
304 x.to_vec(),
305 y.to_vec(),
306 indices,
307 colors.to_vec(),
308 ))
309}
310
311fn quadrilateral_grid_coords(points: &[[f64; 2]], dim0: usize, dim1: usize) -> Vec<[f64; 2]> {
319 debug_assert!(dim0 >= 2 && dim1 >= 2);
320 debug_assert_eq!(points.len(), dim0 * dim1);
321 let gw = dim1 + 1; let mut grid = vec![[0.0_f64; 2]; (dim0 + 1) * gw];
323 let p = |r: usize, c: usize| points[r * dim1 + c];
324 let inner = |r: usize, c: usize| -> [f64; 2] {
327 let (a, b, d, e) = (p(r, c), p(r, c + 1), p(r + 1, c), p(r + 1, c + 1));
328 [
329 (a[0] + b[0] + d[0] + e[0]) / 4.0,
330 (a[1] + b[1] + d[1] + e[1]) / 4.0,
331 ]
332 };
333 for r in 0..dim0 - 1 {
334 for c in 0..dim1 - 1 {
335 grid[(r + 1) * gw + (c + 1)] = inner(r, c);
336 }
337 }
338 for r in 0..dim0 - 1 {
341 let il = inner(r, 0);
342 grid[(r + 1) * gw] = [p(r, 0)[0] + p(r + 1, 0)[0] - il[0], il[1]];
343 let ir = inner(r, dim1 - 2);
344 grid[(r + 1) * gw + dim1] = [p(r, dim1 - 1)[0] + p(r + 1, dim1 - 1)[0] - ir[0], ir[1]];
345 }
346 for c in 0..dim1 - 1 {
349 let it = inner(0, c);
350 grid[c + 1] = [it[0], p(0, c)[1] + p(0, c + 1)[1] - it[1]];
351 let ib = inner(dim0 - 2, c);
352 grid[dim0 * gw + (c + 1)] = [ib[0], p(dim0 - 1, c)[1] + p(dim0 - 1, c + 1)[1] - ib[1]];
353 }
354 let corner = |pr: usize, pc: usize, ir: usize, ic: usize| -> [f64; 2] {
356 let (pp, ii) = (p(pr, pc), inner(ir, ic));
357 [2.0 * pp[0] - ii[0], 2.0 * pp[1] - ii[1]]
358 };
359 grid[0] = corner(0, 0, 0, 0);
360 grid[dim1] = corner(0, dim1 - 1, 0, dim1 - 2);
361 grid[dim0 * gw + dim1] = corner(dim0 - 1, dim1 - 1, dim0 - 2, dim1 - 2);
362 grid[dim0 * gw] = corner(dim0 - 1, 0, dim0 - 2, 0);
363 grid
364}
365
366fn quadrilateral_grid_as_triangles(
373 points: &[[f64; 2]],
374 dim0: usize,
375 dim1: usize,
376) -> (Vec<[f64; 2]>, Vec<[u32; 3]>) {
377 let nbpoints = dim0 * dim1;
378 let grid = quadrilateral_grid_coords(points, dim0, dim1);
379 let gw = dim1 + 1;
380 let mut coords = vec![[0.0_f64; 2]; 4 * nbpoints];
381 for r in 0..dim0 {
382 for c in 0..dim1 {
383 let k = r * dim1 + c;
384 coords[4 * k] = grid[r * gw + c];
385 coords[4 * k + 1] = grid[(r + 1) * gw + c];
386 coords[4 * k + 2] = grid[r * gw + (c + 1)];
387 coords[4 * k + 3] = grid[(r + 1) * gw + (c + 1)];
388 }
389 }
390 let mut indices = Vec::with_capacity(2 * nbpoints);
391 for k in 0..nbpoints {
392 let b = u32::try_from(4 * k).expect("vertex index fits in u32");
393 indices.push([b, b + 1, b + 2]);
394 indices.push([b + 1, b + 2, b + 3]);
395 }
396 (coords, indices)
397}
398
399fn arrange_irregular_grid_points(
410 x: &[f64],
411 y: &[f64],
412) -> Option<(Vec<[f64; 2]>, usize, usize, bool)> {
413 let nbpoints = x.len();
414 if nbpoints < 2 {
415 return None;
416 }
417 let grid = detect_regular_grid(x, y)?;
418 let (mut s0, mut s1) = grid.shape; let order = grid.order;
420 if nbpoints != s0 * s1 {
422 match order {
423 GridMajorOrder::Row => s0 = nbpoints.div_ceil(s1),
424 GridMajorOrder::Column => s1 = nbpoints.div_ceil(s0),
425 }
426 }
427
428 if s0 < 2 || s1 < 2 {
430 let row_order = s0 == 1;
431 let line: Vec<[f64; 2]> = (0..nbpoints)
433 .map(|i| {
434 if row_order {
435 [x[i], y[i]]
436 } else {
437 [y[i], x[i]]
438 }
439 })
440 .collect();
441 let mut points = Vec::with_capacity(2 * nbpoints);
445 points.extend_from_slice(&line);
446 for i in 0..nbpoints {
447 let (dx, dy) = if i + 1 < nbpoints {
448 (line[i + 1][0] - line[i][0], line[i + 1][1] - line[i][1])
449 } else {
450 (line[i][0] - line[i - 1][0], line[i][1] - line[i - 1][1])
451 };
452 points.push([line[i][0] + dy, line[i][1] - dx]);
453 }
454 return Some((points, 2, nbpoints, !row_order));
455 }
456
457 let total = s0 * s1;
459 let mut points = vec![[0.0_f64; 2]; total];
460 match order {
461 GridMajorOrder::Row => {
462 for i in 0..nbpoints {
463 points[i] = [x[i], y[i]];
464 }
465 if nbpoints != total {
466 let index = (nbpoints / s1) * s1; let pad = total - nbpoints;
470 let last_y = y[nbpoints - 1];
471 for j in 0..pad {
472 points[nbpoints + j] = [x[index - pad + j], last_y];
473 }
474 }
475 Some((points, s0, s1, false))
476 }
477 GridMajorOrder::Column => {
478 for i in 0..nbpoints {
480 points[i] = [y[i], x[i]];
481 }
482 if nbpoints != total {
483 let index = (nbpoints / s0) * s0; let pad = total - nbpoints;
485 let last_x = x[nbpoints - 1];
486 for j in 0..pad {
487 points[nbpoints + j] = [y[index - pad + j], last_x];
488 }
489 }
490 Some((points, s1, s0, true))
491 }
492 }
493}
494
495#[must_use]
510pub fn irregular_grid_triangles(x: &[f64], y: &[f64], colors: &[Color32]) -> Option<Triangles> {
511 assert_eq!(x.len(), y.len(), "x and y must have the same length");
512 assert_eq!(
513 colors.len(),
514 x.len(),
515 "colors must have one entry per point"
516 );
517 let nbpoints = x.len();
518 let (points, dim0, dim1, swap_xy) = arrange_irregular_grid_points(x, y)?;
519 let (mut coords, mut indices) = quadrilateral_grid_as_triangles(&points, dim0, dim1);
520 coords.truncate(4 * nbpoints);
522 indices.truncate(2 * nbpoints);
523 let (vx, vy): (Vec<f64>, Vec<f64>) = if swap_xy {
524 coords.iter().map(|c| (c[1], c[0])).unzip()
525 } else {
526 coords.iter().map(|c| (c[0], c[1])).unzip()
527 };
528 let mut vcolors = Vec::with_capacity(4 * nbpoints);
530 for &c in colors {
531 vcolors.extend_from_slice(&[c; 4]);
532 }
533 Some(Triangles::new(vx, vy, indices, vcolors))
534}
535
536#[must_use]
545pub fn irregular_grid_pick(mesh: &Triangles, px: f64, py: f64) -> Option<usize> {
546 for (t, tri) in mesh.indices.iter().enumerate() {
547 let v = |i: usize| [mesh.x[i], mesh.y[i]];
548 let (a, b, c) = (v(tri[0] as usize), v(tri[1] as usize), v(tri[2] as usize));
549 if barycentric(a, b, c, [px, py]).is_some() {
550 return Some(t / 2);
551 }
552 }
553 None
554}
555
556fn barycentric(a: [f64; 2], b: [f64; 2], c: [f64; 2], p: [f64; 2]) -> Option<[f64; 3]> {
562 let det = orient2d(a, b, c);
563 if det == 0.0 {
564 return None;
565 }
566 let wa = orient2d(b, c, p) / det;
569 let wb = orient2d(c, a, p) / det;
570 let wc = orient2d(a, b, p) / det;
571 let eps = -1e-9;
573 if wa >= eps && wb >= eps && wc >= eps {
574 Some([wa, wb, wc])
575 } else {
576 None
577 }
578}
579
580#[must_use]
590pub fn interpolate(
591 tri: &Triangulation,
592 x: &[f64],
593 y: &[f64],
594 values: &[f64],
595 px: f64,
596 py: f64,
597) -> Option<f64> {
598 let p = [px, py];
599 for t in &tri.triangles {
600 let a = [x[t[0]], y[t[0]]];
601 let b = [x[t[1]], y[t[1]]];
602 let c = [x[t[2]], y[t[2]]];
603 if let Some([wa, wb, wc]) = barycentric(a, b, c, p) {
604 return Some(wa * values[t[0]] + wb * values[t[1]] + wc * values[t[2]]);
605 }
606 }
607 None
608}
609
610#[derive(Clone, Debug, PartialEq, Default)]
613pub struct ScatterLineProfile {
614 pub points: Vec<[f64; 2]>,
617 pub values: Vec<Option<f64>>,
620}
621
622impl ScatterLineProfile {
623 #[must_use]
631 pub fn distance_value_curve(&self) -> (Vec<f64>, Vec<f64>) {
632 let Some(&[x0, y0]) = self.points.first() else {
633 return (Vec::new(), Vec::new());
634 };
635 let distance = self
636 .points
637 .iter()
638 .map(|&[x, y]| (x - x0).hypot(y - y0))
639 .collect();
640 let value = self.values.iter().map(|v| v.unwrap_or(f64::NAN)).collect();
641 (distance, value)
642 }
643}
644
645pub fn scatter_line_profile(
660 x: &[f64],
661 y: &[f64],
662 values: &[f64],
663 start: (f64, f64),
664 end: (f64, f64),
665 n_points: usize,
666) -> ScatterLineProfile {
667 let tri = delaunay(x, y);
668 let mut points = Vec::with_capacity(n_points);
669 let mut profile = Vec::with_capacity(n_points);
670 for i in 0..n_points {
671 let t = if n_points <= 1 {
672 0.0
673 } else {
674 i as f64 / (n_points - 1) as f64
675 };
676 let px = start.0 + (end.0 - start.0) * t;
677 let py = start.1 + (end.1 - start.1) * t;
678 points.push([px, py]);
679 profile.push(interpolate(&tri, x, y, values, px, py));
680 }
681 ScatterLineProfile {
682 points,
683 values: profile,
684 }
685}
686
687#[derive(Clone, Debug, PartialEq)]
690pub struct GridImage {
691 pub data: Vec<f64>,
694 pub shape: (usize, usize),
696 pub origin: (f64, f64),
699 pub scale: (f64, f64),
701}
702
703impl GridImage {
704 #[must_use]
706 pub fn get(&self, r: usize, c: usize) -> Option<f64> {
707 if r < self.shape.0 && c < self.shape.1 {
708 Some(self.data[r * self.shape.1 + c])
709 } else {
710 None
711 }
712 }
713
714 #[must_use]
720 pub fn cell(&self, x: f64, y: f64) -> Option<(usize, usize)> {
721 grid_cell(self.shape, self.origin, self.scale, x, y)
722 }
723}
724
725fn grid_cell(
730 shape: (usize, usize),
731 origin: (f64, f64),
732 scale: (f64, f64),
733 x: f64,
734 y: f64,
735) -> Option<(usize, usize)> {
736 let (sx, sy) = scale;
737 if sx == 0.0 || sy == 0.0 {
738 return None;
739 }
740 let cf = (x - origin.0) / sx;
741 let rf = (y - origin.1) / sy;
742 if !cf.is_finite() || !rf.is_finite() || cf < 0.0 || rf < 0.0 {
743 return None;
744 }
745 let col = cf.floor() as usize;
746 let row = rf.floor() as usize;
747 if row < shape.0 && col < shape.1 {
748 Some((row, col))
749 } else {
750 None
751 }
752}
753
754#[must_use]
767pub fn regular_grid_pick(
768 image: &GridImage,
769 order: GridMajorOrder,
770 point_count: usize,
771 x: f64,
772 y: f64,
773) -> Option<usize> {
774 let (row, col) = image.cell(x, y)?;
775 let (rows, cols) = image.shape;
776 let index = match order {
777 GridMajorOrder::Row => row * cols + col,
778 GridMajorOrder::Column => row + col * rows,
779 };
780 if index < point_count {
781 Some(index)
782 } else {
783 None
784 }
785}
786
787#[must_use]
797pub fn irregular_grid_image(
798 x: &[f64],
799 y: &[f64],
800 values: &[f64],
801 rows: usize,
802 cols: usize,
803) -> Option<GridImage> {
804 assert_eq!(x.len(), y.len(), "x and y must have the same length");
805 assert_eq!(
806 values.len(),
807 x.len(),
808 "values must have one entry per point"
809 );
810 if rows == 0 || cols == 0 {
811 return None;
812 }
813
814 let tri = delaunay(x, y);
815 if tri.is_empty() {
816 return None;
817 }
818
819 let (mut min_x, mut min_y, mut max_x, mut max_y) = (
820 f64::INFINITY,
821 f64::INFINITY,
822 f64::NEG_INFINITY,
823 f64::NEG_INFINITY,
824 );
825 for (&xi, &yi) in x.iter().zip(y) {
826 if xi.is_finite() && yi.is_finite() {
827 min_x = min_x.min(xi);
828 min_y = min_y.min(yi);
829 max_x = max_x.max(xi);
830 max_y = max_y.max(yi);
831 }
832 }
833
834 let sx = if cols > 0 {
836 (max_x - min_x) / cols as f64
837 } else {
838 1.0
839 };
840 let sy = if rows > 0 {
841 (max_y - min_y) / rows as f64
842 } else {
843 1.0
844 };
845
846 let mut data = vec![f64::NAN; rows * cols];
847 for r in 0..rows {
848 let py = min_y + (r as f64 + 0.5) * sy;
850 for c in 0..cols {
851 let px = min_x + (c as f64 + 0.5) * sx;
852 if let Some(v) = interpolate(&tri, x, y, values, px, py) {
853 data[r * cols + c] = v;
854 }
855 }
856 }
857
858 Some(GridImage {
859 data,
860 shape: (rows, cols),
861 origin: (min_x, min_y),
862 scale: (sx, sy),
863 })
864}
865
866#[derive(Clone, Copy, Debug, PartialEq, Eq)]
873pub struct RegularGrid {
874 pub shape: (usize, usize),
876 pub order: GridMajorOrder,
878}
879
880fn get_z_line_length(array: &[f64]) -> usize {
889 if array.len() < 2 {
890 return 0;
891 }
892 let sign: Vec<i8> = array
893 .windows(2)
894 .map(|w| {
895 let d = w[1] - w[0];
896 if d > 0.0 {
897 1
898 } else if d < 0.0 {
899 -1
900 } else {
901 0
902 }
903 })
904 .collect();
905 if sign.is_empty() || sign[0] == 0 {
907 return 0;
908 }
909 let first = sign[0];
910 let beginnings: Vec<usize> = sign
913 .iter()
914 .enumerate()
915 .filter(|&(_, &s)| s == -first)
916 .map(|(i, _)| i + 1)
917 .collect();
918 if beginnings.is_empty() {
919 return 0;
920 }
921 let length = beginnings[0];
922 let uniform = beginnings.windows(2).all(|w| w[1] - w[0] == length);
924 if uniform { length } else { 0 }
925}
926
927fn guess_z_grid_shape(x: &[f64], y: &[f64]) -> Option<RegularGrid> {
934 let n = x.len();
935 let width = get_z_line_length(x);
936 if width != 0 {
937 let height = n.div_ceil(width);
938 return Some(RegularGrid {
939 shape: (height, width),
940 order: GridMajorOrder::Row,
941 });
942 }
943 let height = get_z_line_length(y);
944 if height != 0 {
945 let width = n.div_ceil(height);
946 return Some(RegularGrid {
947 shape: (height, width),
948 order: GridMajorOrder::Column,
949 });
950 }
951 None
952}
953
954fn is_monotonic(array: &[f64]) -> i8 {
957 if array.len() < 2 {
958 return 1;
960 }
961 let diffs: Vec<f64> = array.windows(2).map(|w| w[1] - w[0]).collect();
962 if diffs.iter().all(|&d| d >= 0.0) {
963 1
964 } else if diffs.iter().all(|&d| d <= 0.0) {
965 -1
966 } else {
967 0
968 }
969}
970
971#[must_use]
981pub fn detect_regular_grid(x: &[f64], y: &[f64]) -> Option<RegularGrid> {
982 assert_eq!(x.len(), y.len(), "x and y must have the same length");
983 if x.is_empty() {
984 return None;
985 }
986
987 if let Some(grid) = guess_z_grid_shape(x, y) {
988 return Some(grid);
989 }
990
991 let y_monotonic = is_monotonic(y) != 0;
993 let x_monotonic = is_monotonic(x) != 0;
994 if x_monotonic || y_monotonic {
995 let (x_min, x_max) = min_max(x);
996 let (y_min, y_max) = min_max(y);
997 let shape = if !y_monotonic || (x_max - x_min) >= (y_max - y_min) {
998 (1, x.len())
1000 } else {
1001 (y.len(), 1)
1003 };
1004 Some(RegularGrid {
1005 shape,
1006 order: GridMajorOrder::Row, })
1008 } else {
1009 None
1010 }
1011}
1012
1013fn min_max(array: &[f64]) -> (f64, f64) {
1016 let mut min = f64::INFINITY;
1017 let mut max = f64::NEG_INFINITY;
1018 for &v in array {
1019 if v.is_finite() {
1020 min = min.min(v);
1021 max = max.max(v);
1022 }
1023 }
1024 if min > max {
1025 (f64::NAN, f64::NAN)
1026 } else {
1027 (min, max)
1028 }
1029}
1030
1031#[derive(Clone, Debug, PartialEq)]
1041pub struct BinnedStatistic {
1042 pub mean: Vec<f64>,
1045 pub count: Vec<u64>,
1047 pub sum: Vec<f64>,
1049 pub shape: (usize, usize),
1051 pub origin: (f64, f64),
1054 pub scale: (f64, f64),
1056}
1057
1058impl BinnedStatistic {
1059 #[must_use]
1062 pub fn select(&self, func: BinnedStatisticFunction) -> Vec<f64> {
1063 match func {
1064 BinnedStatisticFunction::Mean => self.mean.clone(),
1065 BinnedStatisticFunction::Count => self.count.iter().map(|&c| c as f64).collect(),
1066 BinnedStatisticFunction::Sum => self.sum.clone(),
1067 }
1068 }
1069
1070 #[must_use]
1082 pub fn pick(&self, x: &[f64], y: &[f64], px: f64, py: f64) -> Option<Vec<usize>> {
1083 let (row, col) = grid_cell(self.shape, self.origin, self.scale, px, py)?;
1084 let (ox, oy) = self.origin;
1085 let (sx, sy) = self.scale;
1086 let x_lo = ox + sx * col as f64;
1087 let x_hi = ox + sx * (col + 1) as f64;
1088 let y_lo = oy + sy * row as f64;
1089 let y_hi = oy + sy * (row + 1) as f64;
1090 let indices: Vec<usize> = x
1091 .iter()
1092 .zip(y.iter())
1093 .enumerate()
1094 .filter(|&(_, (&xi, &yi))| xi >= x_lo && xi < x_hi && yi >= y_lo && yi < y_hi)
1095 .map(|(i, _)| i)
1096 .collect();
1097 if indices.is_empty() {
1098 None
1099 } else {
1100 Some(indices)
1101 }
1102 }
1103}
1104
1105#[must_use]
1119pub fn binned_statistic(
1120 x: &[f64],
1121 y: &[f64],
1122 values: &[f64],
1123 rows: usize,
1124 cols: usize,
1125) -> Option<BinnedStatistic> {
1126 assert_eq!(x.len(), y.len(), "x and y must have the same length");
1127 assert_eq!(
1128 values.len(),
1129 x.len(),
1130 "values must have one entry per point"
1131 );
1132 if rows == 0 || cols == 0 {
1133 return None;
1134 }
1135
1136 let (x_min, x_max) = min_max(x);
1137 let (y_min, y_max) = min_max(y);
1138 if !x_min.is_finite() || !y_min.is_finite() {
1139 return None; }
1141
1142 let sx = {
1144 let span = x_max - x_min;
1145 if span > 0.0 { span / cols as f64 } else { 1.0 }
1146 };
1147 let sy = {
1148 let span = y_max - y_min;
1149 if span > 0.0 { span / rows as f64 } else { 1.0 }
1150 };
1151
1152 let mut count = vec![0u64; rows * cols];
1153 let mut sum = vec![0.0f64; rows * cols];
1154
1155 for ((&xi, &yi), &vi) in x.iter().zip(y).zip(values) {
1156 if !xi.is_finite() || !yi.is_finite() || !vi.is_finite() {
1157 continue;
1158 }
1159 let mut c = ((xi - x_min) / sx).floor() as isize;
1161 let mut r = ((yi - y_min) / sy).floor() as isize;
1162 if c >= cols as isize {
1164 c = cols as isize - 1;
1165 }
1166 if r >= rows as isize {
1167 r = rows as isize - 1;
1168 }
1169 if c < 0 || r < 0 {
1170 continue; }
1172 let idx = r as usize * cols + c as usize;
1173 count[idx] += 1;
1174 sum[idx] += vi;
1175 }
1176
1177 let mean: Vec<f64> = count
1178 .iter()
1179 .zip(&sum)
1180 .map(|(&c, &s)| if c == 0 { f64::NAN } else { s / c as f64 })
1181 .collect();
1182
1183 Some(BinnedStatistic {
1184 mean,
1185 count,
1186 sum,
1187 shape: (rows, cols),
1188 origin: (x_min, y_min),
1189 scale: (sx, sy),
1190 })
1191}
1192
1193#[derive(Clone, Debug, PartialEq)]
1203pub struct PointsViz {
1204 pub x: Vec<f64>,
1206 pub y: Vec<f64>,
1208 pub values: Vec<f64>,
1210 pub colors: Vec<Color32>,
1212 pub alpha: Option<Vec<f64>>,
1215}
1216
1217impl PointsViz {
1218 #[must_use]
1221 pub fn new(x: Vec<f64>, y: Vec<f64>, values: Vec<f64>, colors: Vec<Color32>) -> Self {
1222 assert_eq!(x.len(), y.len(), "x and y must have the same length");
1223 assert_eq!(
1224 values.len(),
1225 x.len(),
1226 "values must have one entry per point"
1227 );
1228 assert_eq!(
1229 colors.len(),
1230 x.len(),
1231 "colors must have one entry per point"
1232 );
1233 Self {
1234 x,
1235 y,
1236 values,
1237 colors,
1238 alpha: None,
1239 }
1240 }
1241
1242 #[must_use]
1245 pub fn with_alpha(mut self, alpha: Vec<f64>) -> Self {
1246 assert_eq!(
1247 alpha.len(),
1248 self.x.len(),
1249 "alpha must have one entry per point"
1250 );
1251 self.alpha = Some(alpha.into_iter().map(|a| a.clamp(0.0, 1.0)).collect());
1252 self
1253 }
1254
1255 #[must_use]
1257 pub fn len(&self) -> usize {
1258 self.x.len()
1259 }
1260
1261 #[must_use]
1263 pub fn is_empty(&self) -> bool {
1264 self.x.is_empty()
1265 }
1266}
1267
1268#[cfg(test)]
1269mod tests {
1270 use super::*;
1271
1272 #[test]
1275 fn delaunay_three_points_one_triangle() {
1276 let tri = delaunay(&[0.0, 1.0, 0.0], &[0.0, 0.0, 1.0]);
1277 assert_eq!(tri.len(), 1);
1278 let mut refs = tri.triangles[0];
1280 refs.sort_unstable();
1281 assert_eq!(refs, [0, 1, 2]);
1282 }
1283
1284 #[test]
1285 fn delaunay_four_convex_points_two_triangles() {
1286 let x = [0.0, 1.0, 1.0, 0.0];
1288 let y = [0.0, 0.0, 1.0, 1.0];
1289 let tri = delaunay(&x, &y);
1290 assert_eq!(tri.len(), 2);
1291 let mut seen = [false; 4];
1293 for t in &tri.triangles {
1294 for &v in t {
1295 seen[v] = true;
1296 }
1297 }
1298 assert!(seen.iter().all(|&s| s), "every input point referenced");
1299 }
1300
1301 #[test]
1302 fn delaunay_collinear_points_empty() {
1303 let tri = delaunay(&[0.0, 1.0, 2.0, 3.0], &[0.0, 1.0, 2.0, 3.0]);
1305 assert!(tri.is_empty(), "collinear input -> empty triangulation");
1306 }
1307
1308 #[test]
1309 fn delaunay_fewer_than_three_points_empty() {
1310 assert!(delaunay(&[0.0, 1.0], &[0.0, 1.0]).is_empty());
1311 assert!(delaunay(&[], &[]).is_empty());
1312 }
1313
1314 #[test]
1315 fn delaunay_ignores_non_finite_points() {
1316 let x = [0.0, 1.0, 0.0, f64::NAN];
1317 let y = [0.0, 0.0, 1.0, 5.0];
1318 let tri = delaunay(&x, &y);
1319 assert_eq!(tri.len(), 1);
1321 for t in &tri.triangles {
1322 assert!(
1323 t.iter().all(|&v| v < 3),
1324 "no triangle references the NaN point"
1325 );
1326 }
1327 }
1328
1329 #[test]
1330 fn delaunay_property_no_point_inside_circumcircle() {
1331 let x = [0.0, 1.0, 2.0, 0.5, 1.5, 1.0];
1333 let y = [0.0, 0.2, 0.0, 1.0, 1.1, 2.0];
1334 let tri = delaunay(&x, &y);
1335 assert!(!tri.is_empty());
1336 for t in &tri.triangles {
1339 let a = [x[t[0]], y[t[0]]];
1340 let b = [x[t[1]], y[t[1]]];
1341 let c = [x[t[2]], y[t[2]]];
1342 let (a, b, c) = if orient2d(a, b, c) < 0.0 {
1343 (a, c, b)
1344 } else {
1345 (a, b, c)
1346 };
1347 for i in 0..x.len() {
1348 if i == t[0] || i == t[1] || i == t[2] {
1349 continue;
1350 }
1351 let p = [x[i], y[i]];
1352 assert!(
1353 !in_circumcircle(a, b, c, p),
1354 "point {i} inside circumcircle of triangle {t:?}"
1355 );
1356 }
1357 }
1358 }
1359
1360 #[test]
1363 fn solid_triangles_colors_each_vertex() {
1364 let x = [0.0, 1.0, 0.0];
1365 let y = [0.0, 0.0, 1.0];
1366 let colors = [Color32::RED, Color32::GREEN, Color32::BLUE];
1367 let t = solid_triangles(&x, &y, &colors).expect("triangulable");
1368 assert_eq!(t.indices.len(), 1);
1369 assert_eq!(t.colors, colors);
1370 assert_eq!(t.x, x);
1371 assert_eq!(t.y, y);
1372 }
1373
1374 #[test]
1375 fn solid_triangles_none_for_collinear() {
1376 let x = [0.0, 1.0, 2.0];
1377 let y = [0.0, 1.0, 2.0];
1378 let colors = [Color32::RED; 3];
1379 assert!(solid_triangles(&x, &y, &colors).is_none());
1380 }
1381
1382 #[test]
1385 fn interpolate_at_vertices_returns_vertex_value() {
1386 let x = [0.0, 1.0, 0.0];
1387 let y = [0.0, 0.0, 1.0];
1388 let values = [10.0, 20.0, 30.0];
1389 let tri = delaunay(&x, &y);
1390 for i in 0..3 {
1391 let v = interpolate(&tri, &x, &y, &values, x[i], y[i]).expect("inside");
1392 assert!((v - values[i]).abs() < 1e-9, "vertex {i}: {v}");
1393 }
1394 }
1395
1396 #[test]
1397 fn interpolate_at_centroid_returns_mean() {
1398 let x = [0.0, 1.0, 0.0];
1399 let y = [0.0, 0.0, 1.0];
1400 let values = [10.0, 20.0, 30.0];
1401 let tri = delaunay(&x, &y);
1402 let cx = (x[0] + x[1] + x[2]) / 3.0;
1403 let cy = (y[0] + y[1] + y[2]) / 3.0;
1404 let v = interpolate(&tri, &x, &y, &values, cx, cy).expect("inside");
1405 let mean = (10.0 + 20.0 + 30.0) / 3.0;
1406 assert!((v - mean).abs() < 1e-9, "centroid value {v} != mean {mean}");
1407 }
1408
1409 #[test]
1410 fn interpolate_outside_returns_none() {
1411 let x = [0.0, 1.0, 0.0];
1412 let y = [0.0, 0.0, 1.0];
1413 let values = [10.0, 20.0, 30.0];
1414 let tri = delaunay(&x, &y);
1415 assert!(interpolate(&tri, &x, &y, &values, 5.0, 5.0).is_none());
1417 assert!(interpolate(&tri, &x, &y, &values, -1.0, -1.0).is_none());
1418 }
1419
1420 #[test]
1421 fn scatter_line_profile_interpolates_affine_field_along_line() {
1422 let x = [0.0, 2.0, 0.0];
1426 let y = [0.0, 0.0, 2.0];
1427 let values = [0.0, 2.0, 4.0];
1428 let prof = scatter_line_profile(&x, &y, &values, (0.0, 0.0), (1.0, 1.0), 3);
1429 assert_eq!(prof.points, vec![[0.0, 0.0], [0.5, 0.5], [1.0, 1.0]]);
1430 let got: Vec<f64> = prof
1431 .values
1432 .iter()
1433 .map(|v| v.expect("inside hull"))
1434 .collect();
1435 for (g, want) in got.iter().zip([0.0, 1.5, 3.0]) {
1436 assert!((g - want).abs() < 1e-9, "got {g}, want {want}");
1437 }
1438 }
1439
1440 #[test]
1441 fn scatter_line_profile_outside_hull_is_none() {
1442 let x = [0.0, 2.0, 0.0];
1445 let y = [0.0, 0.0, 2.0];
1446 let values = [0.0, 2.0, 4.0];
1447 let prof = scatter_line_profile(&x, &y, &values, (5.0, 5.0), (9.0, 9.0), 4);
1448 assert!(prof.values.iter().all(Option::is_none), "{:?}", prof.values);
1449 }
1450
1451 #[test]
1452 fn scatter_line_profile_too_few_points_yields_no_values() {
1453 let x = [0.0, 1.0];
1455 let y = [0.0, 1.0];
1456 let values = [1.0, 2.0];
1457 let prof = scatter_line_profile(&x, &y, &values, (0.0, 0.0), (1.0, 1.0), 2);
1458 assert_eq!(prof.points.len(), 2);
1459 assert!(prof.values.iter().all(Option::is_none));
1460 }
1461
1462 #[test]
1463 fn distance_value_curve_is_distance_from_start_with_nan_gaps() {
1464 let prof = ScatterLineProfile {
1467 points: vec![[0.0, 0.0], [3.0, 4.0], [6.0, 8.0]],
1468 values: vec![Some(1.0), None, Some(2.0)],
1469 };
1470 let (distance, value) = prof.distance_value_curve();
1471 assert_eq!(distance, vec![0.0, 5.0, 10.0]);
1472 assert_eq!(value[0], 1.0);
1473 assert!(value[1].is_nan(), "out-of-hull sample maps to NaN");
1474 assert_eq!(value[2], 2.0);
1475 }
1476
1477 #[test]
1478 fn distance_value_curve_empty_profile_is_empty() {
1479 let prof = ScatterLineProfile::default();
1480 assert_eq!(prof.distance_value_curve(), (Vec::new(), Vec::new()));
1481 }
1482
1483 #[test]
1486 fn irregular_grid_image_interpolates_inside_nan_outside() {
1487 let x = [0.0, 4.0, 0.0];
1489 let y = [0.0, 0.0, 4.0];
1490 let values = [0.0, 4.0, 0.0];
1491 let img = irregular_grid_image(&x, &y, &values, 4, 4).expect("triangulable");
1492 assert_eq!(img.shape, (4, 4));
1493 let v = img.get(0, 0).unwrap();
1495 assert!((v - 0.5).abs() < 1e-9, "interior value {v}");
1496 let outside = img.get(3, 3).unwrap();
1498 assert!(
1499 outside.is_nan(),
1500 "exterior pixel should be NaN, got {outside}"
1501 );
1502 assert_eq!(img.origin, (0.0, 0.0));
1503 assert_eq!(img.scale, (1.0, 1.0));
1504 }
1505
1506 #[test]
1507 fn irregular_grid_image_none_for_degenerate() {
1508 assert!(
1509 irregular_grid_image(&[0.0, 1.0, 2.0], &[0.0, 1.0, 2.0], &[1.0, 2.0, 3.0], 4, 4)
1510 .is_none()
1511 );
1512 assert!(
1513 irregular_grid_image(&[0.0, 1.0, 0.0], &[0.0, 0.0, 1.0], &[1.0, 2.0, 3.0], 0, 4)
1514 .is_none()
1515 );
1516 }
1517
1518 fn grid_3x4_row_major() -> (Vec<f64>, Vec<f64>) {
1522 let (rows, cols) = (3usize, 4usize);
1523 let mut x = Vec::new();
1524 let mut y = Vec::new();
1525 for r in 0..rows {
1526 for c in 0..cols {
1527 x.push(c as f64);
1528 y.push(r as f64);
1529 }
1530 }
1531 (x, y)
1532 }
1533
1534 #[test]
1535 fn detect_regular_grid_row_major_3x4() {
1536 let (x, y) = grid_3x4_row_major();
1537 let grid = detect_regular_grid(&x, &y).expect("grid detected");
1538 assert_eq!(grid.shape, (3, 4));
1539 assert_eq!(grid.order, GridMajorOrder::Row);
1540 }
1541
1542 #[test]
1543 fn detect_regular_grid_column_major_3x4() {
1544 let (rows, cols) = (3usize, 4usize);
1546 let mut x = Vec::new();
1547 let mut y = Vec::new();
1548 for c in 0..cols {
1549 for r in 0..rows {
1550 x.push(c as f64);
1551 y.push(r as f64);
1552 }
1553 }
1554 let grid = detect_regular_grid(&x, &y).expect("grid detected");
1555 assert_eq!(grid.shape, (3, 4));
1556 assert_eq!(grid.order, GridMajorOrder::Column);
1557 }
1558
1559 #[test]
1560 fn detect_regular_grid_rejects_random_scatter() {
1561 let x = [0.0, 1.0, 0.5, 2.0, 3.0, 1.0];
1566 let y = [2.0, 0.0, 1.0, 3.0, 0.5, 4.0];
1567 assert!(detect_regular_grid(&x, &y).is_none());
1568 }
1569
1570 #[test]
1571 fn detect_regular_grid_single_line_along_x() {
1572 let x = [0.0, 1.0, 2.0, 3.0, 4.0, 5.0];
1576 let y = [2.0, 0.0, 1.0, 3.0, 0.5, 4.0];
1577 let grid = detect_regular_grid(&x, &y).expect("line detected");
1578 assert_eq!(grid.shape, (1, 6));
1579 }
1580
1581 #[test]
1584 fn binned_statistic_2x2_mean_count_sum() {
1585 let x = [0.0, 0.5, 2.0];
1594 let y = [0.0, 0.5, 2.0];
1595 let v = [10.0, 30.0, 7.0];
1596 let bs = binned_statistic(&x, &y, &v, 2, 2).expect("binned");
1597 assert_eq!(bs.shape, (2, 2));
1598
1599 assert_eq!(bs.count[0], 2);
1601 assert!((bs.sum[0] - 40.0).abs() < 1e-12);
1602 assert!((bs.mean[0] - 20.0).abs() < 1e-12);
1603
1604 assert_eq!(bs.count[1], 0);
1606 assert_eq!(bs.sum[1], 0.0);
1607 assert!(bs.mean[1].is_nan(), "empty bin mean is NaN");
1608
1609 assert_eq!(bs.count[2], 0);
1611
1612 assert_eq!(bs.count[3], 1);
1614 assert!((bs.sum[3] - 7.0).abs() < 1e-12);
1615 assert!((bs.mean[3] - 7.0).abs() < 1e-12);
1616
1617 assert_eq!(bs.origin, (0.0, 0.0));
1619 assert_eq!(bs.scale, (1.0, 1.0));
1620 }
1621
1622 #[test]
1623 fn binned_statistic_max_point_clamped_into_last_bin() {
1624 let x = [0.0, 2.0];
1626 let y = [0.0, 2.0];
1627 let v = [1.0, 2.0];
1628 let bs = binned_statistic(&x, &y, &v, 2, 2).expect("binned");
1629 assert_eq!(bs.count[3], 1);
1631 assert!((bs.sum[3] - 2.0).abs() < 1e-12);
1632 assert_eq!(bs.count[0], 1);
1634 }
1635
1636 #[test]
1637 fn binned_statistic_select_returns_chosen_grid() {
1638 let x = [0.2, 1.5];
1639 let y = [0.2, 1.5];
1640 let v = [10.0, 7.0];
1641 let bs = binned_statistic(&x, &y, &v, 2, 2).expect("binned");
1642 let counts = bs.select(BinnedStatisticFunction::Count);
1643 assert_eq!(counts, vec![1.0, 0.0, 0.0, 1.0]);
1644 let sums = bs.select(BinnedStatisticFunction::Sum);
1645 assert_eq!(sums, vec![10.0, 0.0, 0.0, 7.0]);
1646 let means = bs.select(BinnedStatisticFunction::Mean);
1647 assert!((means[0] - 10.0).abs() < 1e-12);
1648 assert!(means[1].is_nan());
1649 }
1650
1651 #[test]
1652 fn binned_statistic_none_for_empty_or_zero_shape() {
1653 assert!(binned_statistic(&[], &[], &[], 2, 2).is_none());
1654 assert!(binned_statistic(&[0.0], &[0.0], &[1.0], 0, 2).is_none());
1655 }
1656
1657 #[test]
1658 fn binned_statistic_skips_non_finite_value() {
1659 let x = [0.2, 0.4];
1660 let y = [0.2, 0.4];
1661 let v = [10.0, f64::NAN];
1662 let bs = binned_statistic(&x, &y, &v, 2, 2).expect("binned");
1663 assert_eq!(bs.count[0], 1);
1665 assert!((bs.sum[0] - 10.0).abs() < 1e-12);
1666 }
1667
1668 #[test]
1671 fn regular_grid_pick_row_major_maps_cell_to_index() {
1672 let image = GridImage {
1674 data: vec![0.0; 6],
1675 shape: (2, 3),
1676 origin: (0.0, 0.0),
1677 scale: (1.0, 1.0),
1678 };
1679 assert_eq!(
1681 regular_grid_pick(&image, GridMajorOrder::Row, 6, 2.5, 0.5),
1682 Some(2)
1683 );
1684 assert_eq!(
1686 regular_grid_pick(&image, GridMajorOrder::Row, 6, 0.5, 1.5),
1687 Some(3)
1688 );
1689 assert_eq!(
1691 regular_grid_pick(&image, GridMajorOrder::Row, 6, 3.5, 0.5),
1692 None
1693 );
1694 }
1695
1696 #[test]
1697 fn regular_grid_pick_column_major_inverts_transposed_placement() {
1698 let image = GridImage {
1700 data: vec![0.0; 6],
1701 shape: (2, 3),
1702 origin: (0.0, 0.0),
1703 scale: (1.0, 1.0),
1704 };
1705 assert_eq!(
1707 regular_grid_pick(&image, GridMajorOrder::Column, 6, 2.5, 1.5),
1708 Some(5)
1709 );
1710 assert_eq!(
1712 regular_grid_pick(&image, GridMajorOrder::Column, 6, 1.5, 0.5),
1713 Some(2)
1714 );
1715 }
1716
1717 #[test]
1718 fn regular_grid_pick_none_past_last_point() {
1719 let image = GridImage {
1722 data: vec![0.0; 6],
1723 shape: (2, 3),
1724 origin: (0.0, 0.0),
1725 scale: (1.0, 1.0),
1726 };
1727 assert_eq!(
1729 regular_grid_pick(&image, GridMajorOrder::Row, 5, 2.5, 1.5),
1730 None
1731 );
1732 assert_eq!(
1734 regular_grid_pick(&image, GridMajorOrder::Row, 5, 1.5, 1.5),
1735 Some(4)
1736 );
1737 }
1738
1739 #[test]
1740 fn regular_grid_pick_handles_negative_scale() {
1741 let image = GridImage {
1743 data: vec![0.0; 3],
1744 shape: (1, 3),
1745 origin: (11.0, -0.5),
1746 scale: (-2.0, 1.0),
1747 };
1748 assert_eq!(
1750 regular_grid_pick(&image, GridMajorOrder::Row, 3, 10.0, 0.0),
1751 Some(0)
1752 );
1753 assert_eq!(
1754 regular_grid_pick(&image, GridMajorOrder::Row, 3, 6.0, 0.0),
1755 Some(2)
1756 );
1757 assert_eq!(
1759 regular_grid_pick(&image, GridMajorOrder::Row, 3, 12.0, 0.0),
1760 None
1761 );
1762 }
1763
1764 #[test]
1765 fn binned_statistic_pick_returns_points_in_bin() {
1766 let x = [0.0, 0.5, 1.5, 2.0];
1768 let y = [0.0, 0.5, 1.5, 2.0];
1769 let v = [10.0, 30.0, 5.0, 7.0];
1770 let bs = binned_statistic(&x, &y, &v, 2, 2).expect("binned");
1771 assert_eq!(bs.origin, (0.0, 0.0));
1772 assert_eq!(bs.scale, (1.0, 1.0));
1773 assert_eq!(bs.pick(&x, &y, 0.5, 0.5), Some(vec![0, 1]));
1775 assert_eq!(bs.pick(&x, &y, 1.5, 1.5), Some(vec![2]));
1778 }
1779
1780 #[test]
1781 fn binned_statistic_pick_none_off_grid_or_empty_bin() {
1782 let x = [0.0, 0.5, 2.0];
1783 let y = [0.0, 0.5, 2.0];
1784 let v = [10.0, 30.0, 7.0];
1785 let bs = binned_statistic(&x, &y, &v, 2, 2).expect("binned");
1786 assert_eq!(bs.pick(&x, &y, 1.5, 0.5), None);
1788 assert_eq!(bs.pick(&x, &y, 2.5, 0.5), None);
1790 assert_eq!(bs.pick(&x, &y, -0.5, 0.5), None);
1791 }
1792
1793 #[test]
1796 fn points_viz_default_no_alpha() {
1797 let p = PointsViz::new(
1798 vec![0.0, 1.0],
1799 vec![0.0, 1.0],
1800 vec![5.0, 6.0],
1801 vec![Color32::RED, Color32::BLUE],
1802 );
1803 assert_eq!(p.len(), 2);
1804 assert!(p.alpha.is_none());
1805 }
1806
1807 #[test]
1808 fn points_viz_with_alpha_clamps() {
1809 let p = PointsViz::new(
1810 vec![0.0, 1.0],
1811 vec![0.0, 1.0],
1812 vec![5.0, 6.0],
1813 vec![Color32::RED, Color32::BLUE],
1814 )
1815 .with_alpha(vec![-0.5, 2.0]);
1816 assert_eq!(p.alpha, Some(vec![0.0, 1.0]));
1817 }
1818
1819 #[test]
1820 #[should_panic(expected = "alpha must have one entry per point")]
1821 fn points_viz_alpha_length_mismatch_panics() {
1822 let _ = PointsViz::new(
1823 vec![0.0, 1.0],
1824 vec![0.0, 1.0],
1825 vec![5.0, 6.0],
1826 vec![Color32::RED, Color32::BLUE],
1827 )
1828 .with_alpha(vec![0.5]);
1829 }
1830
1831 #[test]
1834 fn quadrilateral_grid_coords_unit_2x2_offsets_by_half() {
1835 let points = [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]];
1837 let grid = quadrilateral_grid_coords(&points, 2, 2);
1838 let expect = [
1840 [-0.5, -0.5],
1841 [0.5, -0.5],
1842 [1.5, -0.5],
1843 [-0.5, 0.5],
1844 [0.5, 0.5],
1845 [1.5, 0.5],
1846 [-0.5, 1.5],
1847 [0.5, 1.5],
1848 [1.5, 1.5],
1849 ];
1850 assert_eq!(grid.len(), 9);
1851 for (got, want) in grid.iter().zip(&expect) {
1852 assert!((got[0] - want[0]).abs() < 1e-12, "x: {got:?} vs {want:?}");
1853 assert!((got[1] - want[1]).abs() < 1e-12, "y: {got:?} vs {want:?}");
1854 }
1855 }
1856
1857 #[test]
1858 fn irregular_grid_triangles_builds_one_cell_per_point() {
1859 let x = [0.0, 1.0, 0.0, 1.0];
1860 let y = [0.0, 0.0, 1.0, 1.0];
1861 let colors = [Color32::RED, Color32::GREEN, Color32::BLUE, Color32::WHITE];
1862 let mesh = irregular_grid_triangles(&x, &y, &colors).expect("buildable grid");
1863 assert_eq!(mesh.x.len(), 16);
1865 assert_eq!(mesh.colors.len(), 16);
1866 assert_eq!(mesh.indices.len(), 8);
1867 for (k, &c) in colors.iter().enumerate() {
1869 for v in 0..4 {
1870 assert_eq!(mesh.colors[4 * k + v], c, "point {k} vertex {v}");
1871 }
1872 }
1873 }
1874
1875 #[test]
1876 fn irregular_grid_pick_maps_cursor_to_owning_cell() {
1877 let x = [0.0, 1.0, 0.0, 1.0];
1878 let y = [0.0, 0.0, 1.0, 1.0];
1879 let mesh = irregular_grid_triangles(&x, &y, &[Color32::RED; 4]).expect("buildable grid");
1880 assert_eq!(irregular_grid_pick(&mesh, 0.0, 0.0), Some(0));
1882 assert_eq!(irregular_grid_pick(&mesh, 1.0, 0.0), Some(1));
1883 assert_eq!(irregular_grid_pick(&mesh, 0.0, 1.0), Some(2));
1884 assert_eq!(irregular_grid_pick(&mesh, 1.0, 1.0), Some(3));
1885 assert_eq!(irregular_grid_pick(&mesh, 10.0, 10.0), None);
1887 }
1888
1889 #[test]
1890 fn irregular_grid_single_line_builds_one_cell_per_point_and_picks() {
1891 let x = [0.0, 1.0, 2.0, 3.0];
1893 let y = [0.0, 0.0, 0.0, 0.0];
1894 let mesh = irregular_grid_triangles(&x, &y, &[Color32::RED; 4]).expect("buildable line");
1895 assert_eq!(mesh.x.len(), 16, "4 points -> 16 vertices");
1896 assert_eq!(mesh.indices.len(), 8, "4 points -> 8 triangles");
1897 assert_eq!(irregular_grid_pick(&mesh, 1.0, 0.0), Some(1));
1899 assert_eq!(irregular_grid_pick(&mesh, 2.0, 0.0), Some(2));
1900 }
1901
1902 #[test]
1903 fn irregular_grid_triangles_none_for_too_few_points() {
1904 assert!(irregular_grid_triangles(&[0.0], &[0.0], &[Color32::RED]).is_none());
1906 }
1907}