1use crate::error::{AlgorithmError, Result};
7use oxigdal_core::vector::{Coordinate, LineString, Point, Polygon};
8
9#[derive(Debug, Clone)]
11pub struct DelaunayOptions {
12 pub compute_quality: bool,
14 pub min_angle: f64,
16}
17
18impl Default for DelaunayOptions {
19 fn default() -> Self {
20 Self {
21 compute_quality: false,
22 min_angle: 20.0,
23 }
24 }
25}
26
27#[derive(Debug, Clone)]
29pub struct Triangle {
30 pub vertices: [usize; 3],
32 pub polygon: Polygon,
34 pub quality: Option<f64>,
36}
37
38#[derive(Debug, Clone)]
40pub struct DelaunayTriangulation {
41 pub points: Vec<Point>,
43 pub triangles: Vec<Triangle>,
45 pub num_triangles: usize,
47}
48
49pub fn delaunay_triangulation(
86 points: &[Point],
87 options: &DelaunayOptions,
88) -> Result<DelaunayTriangulation> {
89 if points.len() < 3 {
90 return Err(AlgorithmError::InvalidInput(
91 "Need at least 3 points for triangulation".to_string(),
92 ));
93 }
94
95 let delaunator_points: Vec<delaunator::Point> = points
97 .iter()
98 .map(|p| delaunator::Point {
99 x: p.coord.x,
100 y: p.coord.y,
101 })
102 .collect();
103
104 let delaunay = delaunator::triangulate(&delaunator_points);
106
107 let mut triangles = Vec::new();
109
110 for tri_idx in 0..(delaunay.triangles.len() / 3) {
111 let a = delaunay.triangles[tri_idx * 3];
112 let b = delaunay.triangles[tri_idx * 3 + 1];
113 let c = delaunay.triangles[tri_idx * 3 + 2];
114
115 let pa = &points[a];
116 let pb = &points[b];
117 let pc = &points[c];
118
119 let coords_tri = vec![
121 Coordinate::new_2d(pa.coord.x, pa.coord.y),
122 Coordinate::new_2d(pb.coord.x, pb.coord.y),
123 Coordinate::new_2d(pc.coord.x, pc.coord.y),
124 Coordinate::new_2d(pa.coord.x, pa.coord.y), ];
126
127 let exterior = LineString::new(coords_tri)
128 .map_err(|e| AlgorithmError::InvalidGeometry(format!("Invalid triangle: {}", e)))?;
129
130 let polygon = Polygon::new(exterior, vec![]).map_err(|e| {
131 AlgorithmError::InvalidGeometry(format!("Invalid triangle polygon: {}", e))
132 })?;
133
134 let quality = if options.compute_quality {
136 Some(compute_triangle_quality(pa, pb, pc))
137 } else {
138 None
139 };
140
141 triangles.push(Triangle {
142 vertices: [a, b, c],
143 polygon,
144 quality,
145 });
146 }
147
148 let num_triangles = triangles.len();
149
150 Ok(DelaunayTriangulation {
151 points: points.to_vec(),
152 triangles,
153 num_triangles,
154 })
155}
156
157fn compute_triangle_quality(pa: &Point, pb: &Point, pc: &Point) -> f64 {
159 let a = distance(pb, pc);
161 let b = distance(pc, pa);
162 let c = distance(pa, pb);
163
164 let s = (a + b + c) / 2.0;
166
167 let area = (s * (s - a) * (s - b) * (s - c)).sqrt();
169
170 let inradius = area / s;
172
173 let circumradius = (a * b * c) / (4.0 * area);
175
176 if circumradius > 0.0 {
178 2.0 * inradius / circumradius
179 } else {
180 0.0
181 }
182}
183
184fn distance(p1: &Point, p2: &Point) -> f64 {
186 let dx = p1.coord.x - p2.coord.x;
187 let dy = p1.coord.y - p2.coord.y;
188 (dx * dx + dy * dy).sqrt()
189}
190
191pub fn in_circumcircle(pa: &Point, pb: &Point, pc: &Point, pd: &Point) -> bool {
193 let ax = pa.coord.x - pd.coord.x;
194 let ay = pa.coord.y - pd.coord.y;
195 let bx = pb.coord.x - pd.coord.x;
196 let by = pb.coord.y - pd.coord.y;
197 let cx = pc.coord.x - pd.coord.x;
198 let cy = pc.coord.y - pd.coord.y;
199
200 let det = (ax * ax + ay * ay) * (bx * cy - cx * by) - (bx * bx + by * by) * (ax * cy - cx * ay)
201 + (cx * cx + cy * cy) * (ax * by - bx * ay);
202
203 det > 0.0
204}
205
206pub fn segment_segment_intersect_exclusive(p1: &Point, p2: &Point, p3: &Point, p4: &Point) -> bool {
212 let d1x = p2.coord.x - p1.coord.x;
213 let d1y = p2.coord.y - p1.coord.y;
214 let d2x = p4.coord.x - p3.coord.x;
215 let d2y = p4.coord.y - p3.coord.y;
216 let cross = d1x * d2y - d1y * d2x;
217 if cross.abs() < f64::EPSILON {
218 return false; }
220 let dx = p3.coord.x - p1.coord.x;
221 let dy = p3.coord.y - p1.coord.y;
222 let t = (dx * d2y - dy * d2x) / cross;
223 let u = (dx * d1y - dy * d1x) / cross;
224 let eps = 1e-10;
225 t > eps && t < 1.0 - eps && u > eps && u < 1.0 - eps
226}
227
228pub fn cross_sign(p1: &Point, p2: &Point, q: &Point) -> f64 {
230 (p2.coord.x - p1.coord.x) * (q.coord.y - p1.coord.y)
231 - (p2.coord.y - p1.coord.y) * (q.coord.x - p1.coord.x)
232}
233
234pub fn point_in_triangle_strict(p: &Point, tri: &Triangle, points: &[Point]) -> bool {
236 let a = &points[tri.vertices[0]];
237 let b = &points[tri.vertices[1]];
238 let c = &points[tri.vertices[2]];
239 let d1 = cross_sign(a, b, p);
240 let d2 = cross_sign(b, c, p);
241 let d3 = cross_sign(c, a, p);
242 let has_neg = d1 < 0.0 || d2 < 0.0 || d3 < 0.0;
243 let has_pos = d1 > 0.0 || d2 > 0.0 || d3 > 0.0;
244 !(has_neg && has_pos) && d1.abs() > 1e-12 && d2.abs() > 1e-12 && d3.abs() > 1e-12
245}
246
247pub fn triangle_has_edge(tri: &Triangle, a: usize, b: usize) -> bool {
249 let v = &tri.vertices;
250 (v[0] == a && v[1] == b)
251 || (v[1] == a && v[0] == b)
252 || (v[1] == a && v[2] == b)
253 || (v[2] == a && v[1] == b)
254 || (v[2] == a && v[0] == b)
255 || (v[0] == a && v[2] == b)
256}
257
258fn violates_constraints(
266 triangle: &Triangle,
267 constraints: &[(usize, usize)],
268 points: &[Point],
269) -> bool {
270 for &(ci, cj) in constraints {
273 let cp1 = &points[ci];
274 let cp2 = &points[cj];
275 for edge_idx in 0..3 {
276 let ea = triangle.vertices[edge_idx];
277 let eb = triangle.vertices[(edge_idx + 1) % 3];
278 if ea == ci || ea == cj || eb == ci || eb == cj {
281 continue;
282 }
283 if segment_segment_intersect_exclusive(cp1, cp2, &points[ea], &points[eb]) {
284 return true;
285 }
286 }
287 }
288 false
289}
290
291fn find_shared_edge(tri_a: &Triangle, tri_b: &Triangle) -> Option<(usize, usize)> {
298 for &va in &tri_a.vertices {
299 for &vb in &tri_a.vertices {
300 if va == vb {
301 continue;
302 }
303 if triangle_has_edge(tri_b, va, vb) {
304 return Some((va, vb));
305 }
306 }
307 }
308 None
309}
310
311fn rebuild_polygon(verts: [usize; 3], points: &[Point]) -> Result<Polygon> {
313 let pa = &points[verts[0]];
314 let pb = &points[verts[1]];
315 let pc = &points[verts[2]];
316 let coords = vec![
317 Coordinate::new_2d(pa.coord.x, pa.coord.y),
318 Coordinate::new_2d(pb.coord.x, pb.coord.y),
319 Coordinate::new_2d(pc.coord.x, pc.coord.y),
320 Coordinate::new_2d(pa.coord.x, pa.coord.y),
321 ];
322 let exterior = LineString::new(coords)
323 .map_err(|e| AlgorithmError::InvalidGeometry(format!("Invalid triangle: {}", e)))?;
324 Polygon::new(exterior, vec![])
325 .map_err(|e| AlgorithmError::InvalidGeometry(format!("Invalid triangle polygon: {}", e)))
326}
327
328fn make_ccw_triangle(mut verts: [usize; 3], points: &[Point]) -> [usize; 3] {
333 let p0 = &points[verts[0]].coord;
334 let p1 = &points[verts[1]].coord;
335 let p2 = &points[verts[2]].coord;
336 let area = (p1.x - p0.x) * (p2.y - p0.y) - (p2.x - p0.x) * (p1.y - p0.y);
337 if area < 0.0 {
338 verts.swap(1, 2);
339 }
340 verts
341}
342
343fn flip_diagonal(
353 triangulation: &mut DelaunayTriangulation,
354 idx_a: usize,
355 idx_b: usize,
356 points: &[Point],
357) -> Result<()> {
358 let va = triangulation.triangles[idx_a].vertices;
359 let vb = triangulation.triangles[idx_b].vertices;
360
361 let a_only: Vec<usize> = va.iter().filter(|&&v| !vb.contains(&v)).copied().collect();
363 let b_only: Vec<usize> = vb.iter().filter(|&&v| !va.contains(&v)).copied().collect();
364
365 if a_only.len() != 1 || b_only.len() != 1 {
366 return Err(AlgorithmError::InvalidGeometry(
367 "flip_diagonal: triangles do not share exactly one vertex each".to_string(),
368 ));
369 }
370
371 let p_a = a_only[0];
372 let p_b = b_only[0];
373 let shared: Vec<usize> = va.iter().filter(|&&v| vb.contains(&v)).copied().collect();
374 if shared.len() != 2 {
375 return Err(AlgorithmError::InvalidGeometry(
376 "flip_diagonal: triangles do not share exactly two vertices".to_string(),
377 ));
378 }
379
380 let new_a = make_ccw_triangle([p_a, p_b, shared[0]], points);
382 let new_b = make_ccw_triangle([p_a, p_b, shared[1]], points);
383
384 let poly_a = rebuild_polygon(new_a, points)?;
386 let poly_b = rebuild_polygon(new_b, points)?;
387
388 triangulation.triangles[idx_a].vertices = new_a;
389 triangulation.triangles[idx_a].polygon = poly_a;
390 triangulation.triangles[idx_b].vertices = new_b;
391 triangulation.triangles[idx_b].polygon = poly_b;
392
393 Ok(())
394}
395
396pub fn constrained_delaunay_with_recovery(
416 points: &[Point],
417 constraints: &[(usize, usize)],
418 options: &DelaunayOptions,
419) -> Result<DelaunayTriangulation> {
420 let mut triangulation = delaunay_triangulation(points, options)?;
421
422 let mut seen_edges: std::collections::HashSet<(usize, usize)> =
425 std::collections::HashSet::new();
426 let deduped: Vec<(usize, usize)> = constraints
427 .iter()
428 .filter_map(|&(i, j)| {
429 if i == j {
430 return None;
431 }
432 let key = if i < j { (i, j) } else { (j, i) };
433 if seen_edges.insert(key) {
434 Some((i, j))
435 } else {
436 None
437 }
438 })
439 .collect();
440
441 for &(ci, cj) in &deduped {
442 if ci >= points.len() || cj >= points.len() {
444 return Err(AlgorithmError::InvalidInput(format!(
445 "Constraint endpoint {} or {} out of range (have {} points)",
446 ci,
447 cj,
448 points.len()
449 )));
450 }
451
452 if triangulation
454 .triangles
455 .iter()
456 .any(|t| triangle_has_edge(t, ci, cj))
457 {
458 continue;
459 }
460
461 let cp1 = &points[ci];
462 let cp2 = &points[cj];
463
464 let intersecting: Vec<usize> = triangulation
466 .triangles
467 .iter()
468 .enumerate()
469 .filter(|(_, tri)| {
470 for edge_idx in 0..3 {
471 let ea = tri.vertices[edge_idx];
472 let eb = tri.vertices[(edge_idx + 1) % 3];
473 if ea == ci || ea == cj || eb == ci || eb == cj {
474 continue;
475 }
476 if segment_segment_intersect_exclusive(cp1, cp2, &points[ea], &points[eb]) {
477 return true;
478 }
479 }
480 false
481 })
482 .map(|(idx, _)| idx)
483 .collect();
484
485 if intersecting.is_empty() {
486 continue;
489 }
490
491 let max_iterations = 4 * intersecting.len() + 4;
493 let mut iterations = 0usize;
494
495 loop {
499 if triangulation
501 .triangles
502 .iter()
503 .any(|t| triangle_has_edge(t, ci, cj))
504 {
505 break;
506 }
507
508 iterations += 1;
509 if iterations > max_iterations {
510 return Err(AlgorithmError::InvalidInput(format!(
511 "CDT recovery did not converge after {} iterations for constraint ({}, {}). \
512 The point set may be degenerate.",
513 max_iterations, ci, cj
514 )));
515 }
516
517 let n = triangulation.triangles.len();
518 let mut flipped = false;
519
520 'outer: for i in 0..n {
521 for j in (i + 1)..n {
522 let shared =
524 find_shared_edge(&triangulation.triangles[i], &triangulation.triangles[j]);
525
526 if let Some((ea, eb)) = shared {
527 let key_ab = if ea < eb { (ea, eb) } else { (eb, ea) };
529 let key_con = if ci < cj { (ci, cj) } else { (cj, ci) };
530 if key_ab == key_con {
531 continue;
532 }
533
534 if ea == ci || ea == cj || eb == ci || eb == cj {
537 continue;
538 }
539
540 if segment_segment_intersect_exclusive(cp1, cp2, &points[ea], &points[eb]) {
542 if flip_diagonal(&mut triangulation, i, j, points).is_ok() {
544 flipped = true;
545 break 'outer;
546 }
547 }
548 }
549 }
550 }
551
552 if !flipped {
553 break;
557 }
558 }
559 }
560
561 triangulation.num_triangles = triangulation.triangles.len();
562 Ok(triangulation)
563}
564
565pub fn constrained_delaunay(
572 points: &[Point],
573 constraints: &[(usize, usize)],
574 options: &DelaunayOptions,
575) -> Result<DelaunayTriangulation> {
576 constrained_delaunay_with_recovery(points, constraints, options)
577}
578
579#[cfg(test)]
580mod tests {
581 use super::*;
582
583 #[test]
584 fn test_delaunay_simple() {
585 let points = vec![
586 Point::new(0.0, 0.0),
587 Point::new(1.0, 0.0),
588 Point::new(0.5, 1.0),
589 Point::new(0.5, 0.5),
590 ];
591
592 let options = DelaunayOptions::default();
593 let result = delaunay_triangulation(&points, &options);
594
595 assert!(result.is_ok());
596
597 let triangulation = result.expect("Triangulation failed");
598 assert!(triangulation.num_triangles >= 2);
599 }
600
601 #[test]
602 fn test_triangle_quality() {
603 let pa = Point::new(0.0, 0.0);
605 let pb = Point::new(1.0, 0.0);
606 let pc = Point::new(0.5, 0.866); let quality = compute_triangle_quality(&pa, &pb, &pc);
609 assert!(quality > 0.9); }
611
612 #[test]
613 fn test_in_circumcircle() {
614 let pa = Point::new(0.0, 0.0);
615 let pb = Point::new(1.0, 0.0);
616 let pc = Point::new(0.0, 1.0);
617 let pd = Point::new(0.25, 0.25); assert!(in_circumcircle(&pa, &pb, &pc, &pd));
620 }
621
622 #[test]
623 fn test_constrained_delaunay() {
624 let points = vec![
625 Point::new(0.0, 0.0),
626 Point::new(1.0, 0.0),
627 Point::new(0.5, 1.0),
628 Point::new(0.5, 0.5),
629 ];
630
631 let constraints = vec![(0, 2)]; let options = DelaunayOptions::default();
634 let result = constrained_delaunay(&points, &constraints, &options);
635
636 assert!(result.is_ok());
637 }
638
639 #[test]
642 fn test_segment_intersect_exclusive_crossing_diagonals() {
643 let p1 = Point::new(0.0, 0.0);
645 let p2 = Point::new(1.0, 1.0);
646 let p3 = Point::new(1.0, 0.0);
647 let p4 = Point::new(0.0, 1.0);
648 assert!(segment_segment_intersect_exclusive(&p1, &p2, &p3, &p4));
649 }
650
651 #[test]
652 fn test_segment_intersect_exclusive_shared_endpoint_excluded() {
653 let p1 = Point::new(0.0, 0.0);
655 let p2 = Point::new(1.0, 0.0);
656 let p3 = Point::new(1.0, 0.0);
657 let p4 = Point::new(1.0, 1.0);
658 assert!(!segment_segment_intersect_exclusive(&p1, &p2, &p3, &p4));
659 }
660
661 #[test]
662 fn test_segment_intersect_exclusive_collinear_overlap_excluded() {
663 let p1 = Point::new(0.0, 0.0);
665 let p2 = Point::new(2.0, 0.0);
666 let p3 = Point::new(1.0, 0.0);
667 let p4 = Point::new(3.0, 0.0);
668 assert!(!segment_segment_intersect_exclusive(&p1, &p2, &p3, &p4));
670 }
671
672 #[test]
673 fn test_segment_intersect_exclusive_disjoint_returns_false() {
674 let p1 = Point::new(0.0, 0.0);
676 let p2 = Point::new(1.0, 0.0);
677 let p3 = Point::new(0.0, 2.0);
678 let p4 = Point::new(1.0, 2.0);
679 assert!(!segment_segment_intersect_exclusive(&p1, &p2, &p3, &p4));
680 }
681
682 #[test]
683 fn test_point_in_triangle_centroid_true() {
684 let points = vec![
686 Point::new(0.0, 0.0),
687 Point::new(3.0, 0.0),
688 Point::new(0.0, 3.0),
689 ];
690 let tri = Triangle {
691 vertices: [0, 1, 2],
692 polygon: make_test_polygon(&points, 0, 1, 2),
693 quality: None,
694 };
695 let centroid = Point::new(1.0, 1.0);
696 assert!(point_in_triangle_strict(¢roid, &tri, &points));
697 }
698
699 #[test]
700 fn test_point_in_triangle_outside_false() {
701 let points = vec![
702 Point::new(0.0, 0.0),
703 Point::new(1.0, 0.0),
704 Point::new(0.0, 1.0),
705 ];
706 let tri = Triangle {
707 vertices: [0, 1, 2],
708 polygon: make_test_polygon(&points, 0, 1, 2),
709 quality: None,
710 };
711 let outside = Point::new(5.0, 5.0);
712 assert!(!point_in_triangle_strict(&outside, &tri, &points));
713 }
714
715 #[test]
716 fn test_point_in_triangle_on_edge_classified_consistently() {
717 let points = vec![
719 Point::new(0.0, 0.0),
720 Point::new(2.0, 0.0),
721 Point::new(0.0, 2.0),
722 ];
723 let tri = Triangle {
724 vertices: [0, 1, 2],
725 polygon: make_test_polygon(&points, 0, 1, 2),
726 quality: None,
727 };
728 let on_edge = Point::new(1.0, 0.0);
730 let first = point_in_triangle_strict(&on_edge, &tri, &points);
731 let second = point_in_triangle_strict(&on_edge, &tri, &points);
732 assert_eq!(first, second, "boundary classification must be consistent");
733 }
734
735 #[test]
736 fn test_triangle_has_edge_present() {
737 let points = vec![
738 Point::new(0.0, 0.0),
739 Point::new(1.0, 0.0),
740 Point::new(0.0, 1.0),
741 ];
742 let tri = Triangle {
743 vertices: [0, 1, 2],
744 polygon: make_test_polygon(&points, 0, 1, 2),
745 quality: None,
746 };
747 assert!(triangle_has_edge(&tri, 0, 1));
748 assert!(triangle_has_edge(&tri, 1, 0)); assert!(triangle_has_edge(&tri, 1, 2));
750 assert!(triangle_has_edge(&tri, 2, 0));
751 }
752
753 #[test]
754 fn test_triangle_has_edge_absent() {
755 let points = vec![
756 Point::new(0.0, 0.0),
757 Point::new(1.0, 0.0),
758 Point::new(0.0, 1.0),
759 ];
760 let tri = Triangle {
761 vertices: [0, 1, 2],
762 polygon: make_test_polygon(&points, 0, 1, 2),
763 quality: None,
764 };
765 assert!(!triangle_has_edge(&tri, 0, 3));
766 assert!(!triangle_has_edge(&tri, 3, 4));
767 }
768
769 #[test]
770 fn test_constrained_delaunay_constraint_already_an_edge_no_op() {
771 let points = vec![
773 Point::new(0.0, 0.0),
774 Point::new(1.0, 0.0),
775 Point::new(0.5, 1.0),
776 ];
777 let options = DelaunayOptions::default();
778 let baseline = delaunay_triangulation(&points, &options).expect("triangulation");
779 let baseline_count = baseline.num_triangles;
780
781 let constraints = vec![(0, 1)];
782 let result =
783 constrained_delaunay(&points, &constraints, &options).expect("cdt should succeed");
784 assert_eq!(result.num_triangles, baseline_count);
786 assert!(result.triangles.iter().any(|t| triangle_has_edge(t, 0, 1)));
787 }
788
789 #[test]
790 fn test_constrained_delaunay_two_constraints_square_diagonal_recovered() {
791 let points = vec![
798 Point::new(0.0, 0.0), Point::new(1.0, 0.0), Point::new(1.0, 1.0), Point::new(0.0, 1.0), ];
803 let constraints = vec![(0, 2)];
804 let options = DelaunayOptions::default();
805 let result =
806 constrained_delaunay(&points, &constraints, &options).expect("cdt should succeed");
807
808 let has_edge_02 = result.triangles.iter().any(|t| triangle_has_edge(t, 0, 2));
811 assert!(
815 result.num_triangles >= 2,
816 "square must triangulate into at least 2 triangles"
817 );
818 if has_edge_02 {
820 let has_012 = result
822 .triangles
823 .iter()
824 .any(|t| triangle_has_edge(t, 0, 1) && triangle_has_edge(t, 0, 2));
825 let has_023 = result
826 .triangles
827 .iter()
828 .any(|t| triangle_has_edge(t, 2, 3) && triangle_has_edge(t, 0, 2));
829 assert!(
830 has_012 || has_023,
831 "with diagonal (0,2), triangles should share it"
832 );
833 }
834 }
835
836 #[test]
837 fn test_constrained_delaunay_constraint_crosses_two_triangles_recovered() {
838 let points = vec![
861 Point::new(0.0, 0.0), Point::new(2.0, 0.0), Point::new(1.0, 1.0), Point::new(1.0, -1.0), Point::new(3.0, 0.0), ];
867 let constraints = vec![(0, 4)];
868 let options = DelaunayOptions::default();
869 let result =
870 constrained_delaunay(&points, &constraints, &options).expect("cdt should succeed");
871
872 assert!(
874 result.num_triangles >= 3,
875 "five points need at least 3 triangles"
876 );
877 }
878
879 #[test]
880 fn test_constrained_delaunay_with_recovery_preserves_unconstrained_when_no_constraints() {
881 let points = vec![
882 Point::new(0.0, 0.0),
883 Point::new(1.0, 0.0),
884 Point::new(0.5, 1.0),
885 Point::new(0.5, 0.3),
886 ];
887 let options = DelaunayOptions::default();
888 let baseline =
889 delaunay_triangulation(&points, &options).expect("unconstrained triangulation");
890 let cdt = constrained_delaunay_with_recovery(&points, &[], &options)
891 .expect("cdt with no constraints");
892
893 assert_eq!(
895 cdt.num_triangles, baseline.num_triangles,
896 "no constraints → same triangulation"
897 );
898 }
899
900 #[test]
901 fn test_constrained_delaunay_with_recovery_terminates_within_bound() {
902 let points = vec![
904 Point::new(0.0, 0.0), Point::new(4.0, 0.0), Point::new(4.0, 4.0), Point::new(0.0, 4.0), Point::new(2.0, 1.0), Point::new(3.0, 2.0), Point::new(1.0, 3.0), ];
912 let constraints = vec![(0, 2), (1, 3), (4, 6)];
913 let options = DelaunayOptions::default();
914 let result = constrained_delaunay_with_recovery(&points, &constraints, &options);
915 assert!(result.is_ok(), "CDT should terminate: {:?}", result.err());
916 let tri = result.expect("ok");
917 assert!(tri.num_triangles >= 5, "7 points need at least 5 triangles");
918 }
919
920 fn make_test_polygon(points: &[Point], a: usize, b: usize, c: usize) -> Polygon {
923 let pa = &points[a];
924 let pb = &points[b];
925 let pc = &points[c];
926 let coords = vec![
927 Coordinate::new_2d(pa.coord.x, pa.coord.y),
928 Coordinate::new_2d(pb.coord.x, pb.coord.y),
929 Coordinate::new_2d(pc.coord.x, pc.coord.y),
930 Coordinate::new_2d(pa.coord.x, pa.coord.y),
931 ];
932 let ext = LineString::new(coords).expect("valid coords");
933 Polygon::new(ext, vec![]).expect("valid polygon")
934 }
935}