Skip to main content

scirs2_spatial/computational_geometry/
fortune_voronoi.rs

1//! Fortune's sweep line algorithm for Voronoi diagram construction
2//!
3//! This module implements Fortune's algorithm for computing Voronoi diagrams
4//! in 2D. Fortune's algorithm is a sweep line algorithm that processes events
5//! from left to right, maintaining a beach line of parabolic arcs.
6//!
7//! # Algorithm
8//!
9//! The algorithm maintains:
10//! - A **beach line**: a sequence of parabolic arcs defined by the sites to the
11//!   left of the sweep line
12//! - An **event queue**: site events (when the sweep line hits a new site) and
13//!   circle events (when three consecutive arcs converge)
14//!
15//! Time complexity: O(n log n) where n is the number of sites.
16//! Space complexity: O(n).
17//!
18//! # Examples
19//!
20//! ```
21//! use scirs2_spatial::computational_geometry::fortune_voronoi::{fortune_voronoi_2d, VoronoiVertex};
22//!
23//! let sites = vec![[0.0, 0.0], [1.0, 0.0], [0.5, 1.0]];
24//! let diagram = fortune_voronoi_2d(&sites).expect("Operation failed");
25//!
26//! assert!(!diagram.vertices.is_empty());
27//! assert!(!diagram.edges.is_empty());
28//! ```
29
30use crate::error::{SpatialError, SpatialResult};
31use std::collections::BinaryHeap;
32
33/// Tolerance for floating-point comparisons
34const EPSILON: f64 = 1e-10;
35
36/// A vertex in the Voronoi diagram
37#[derive(Debug, Clone, Copy)]
38pub struct VoronoiVertex {
39    /// X coordinate
40    pub x: f64,
41    /// Y coordinate
42    pub y: f64,
43}
44
45impl VoronoiVertex {
46    /// Create a new Voronoi vertex
47    pub fn new(x: f64, y: f64) -> Self {
48        Self { x, y }
49    }
50}
51
52/// An edge in the Voronoi diagram
53#[derive(Debug, Clone)]
54pub struct VoronoiEdge {
55    /// Index of the start vertex (or None if the edge extends to infinity)
56    pub start_vertex: Option<usize>,
57    /// Index of the end vertex (or None if the edge extends to infinity)
58    pub end_vertex: Option<usize>,
59    /// Index of the left site (the site to the left of this edge)
60    pub left_site: usize,
61    /// Index of the right site (the site to the right of this edge)
62    pub right_site: usize,
63    /// Direction of the edge (for unbounded edges pointing toward infinity)
64    pub direction: Option<[f64; 2]>,
65}
66
67/// A cell (region) in the Voronoi diagram
68#[derive(Debug, Clone)]
69pub struct VoronoiCell {
70    /// Index of the site that defines this cell
71    pub site_index: usize,
72    /// Indices of edges that bound this cell
73    pub edge_indices: Vec<usize>,
74    /// Indices of vertices that form this cell's boundary
75    pub vertex_indices: Vec<usize>,
76    /// Whether this cell is bounded (closed)
77    pub is_bounded: bool,
78}
79
80/// A complete Voronoi diagram
81#[derive(Debug, Clone)]
82pub struct VoronoiDiagram {
83    /// The input sites
84    pub sites: Vec<[f64; 2]>,
85    /// The Voronoi vertices
86    pub vertices: Vec<VoronoiVertex>,
87    /// The Voronoi edges
88    pub edges: Vec<VoronoiEdge>,
89    /// The Voronoi cells (regions), one per site
90    pub cells: Vec<VoronoiCell>,
91}
92
93impl VoronoiDiagram {
94    /// Get the number of sites
95    pub fn num_sites(&self) -> usize {
96        self.sites.len()
97    }
98
99    /// Get the number of Voronoi vertices
100    pub fn num_vertices(&self) -> usize {
101        self.vertices.len()
102    }
103
104    /// Get the number of Voronoi edges
105    pub fn num_edges(&self) -> usize {
106        self.edges.len()
107    }
108
109    /// Get the number of bounded edges
110    pub fn num_bounded_edges(&self) -> usize {
111        self.edges
112            .iter()
113            .filter(|e| e.start_vertex.is_some() && e.end_vertex.is_some())
114            .count()
115    }
116
117    /// Find the nearest site to a given point
118    pub fn nearest_site(&self, point: &[f64; 2]) -> usize {
119        let mut best_idx = 0;
120        let mut best_dist = f64::INFINITY;
121
122        for (i, site) in self.sites.iter().enumerate() {
123            let dx = point[0] - site[0];
124            let dy = point[1] - site[1];
125            let dist = dx * dx + dy * dy;
126            if dist < best_dist {
127                best_dist = dist;
128                best_idx = i;
129            }
130        }
131
132        best_idx
133    }
134}
135
136/// Arc in the beach line
137#[derive(Debug, Clone)]
138struct Arc {
139    /// Index of the site that defines this arc
140    site_idx: usize,
141    /// Index of the circle event that will remove this arc (if any)
142    circle_event: Option<usize>,
143}
144
145/// Event types for Fortune's algorithm
146#[derive(Debug, Clone)]
147enum EventKind {
148    /// A site event (sweep line reaches a new site)
149    Site(usize),
150    /// A circle event (three consecutive arcs converge)
151    Circle {
152        arc_idx: usize,
153        center_x: f64,
154        center_y: f64,
155        radius: f64,
156    },
157}
158
159/// An event in the priority queue
160#[derive(Debug, Clone)]
161struct Event {
162    /// X-coordinate of the event (determines priority)
163    x: f64,
164    /// The event kind
165    kind: EventKind,
166    /// Whether this event has been invalidated
167    valid: bool,
168    /// Unique ID for the event
169    id: usize,
170}
171
172impl PartialEq for Event {
173    fn eq(&self, other: &Self) -> bool {
174        self.id == other.id
175    }
176}
177
178impl Eq for Event {}
179
180impl PartialOrd for Event {
181    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
182        Some(self.cmp(other))
183    }
184}
185
186impl Ord for Event {
187    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
188        // Min-heap: smaller x values have higher priority (we negate for BinaryHeap)
189        other
190            .x
191            .partial_cmp(&self.x)
192            .unwrap_or(std::cmp::Ordering::Equal)
193    }
194}
195
196/// Internal builder for the Voronoi diagram
197struct FortuneBuilder {
198    sites: Vec<[f64; 2]>,
199    beach_line: Vec<Arc>,
200    events: BinaryHeap<Event>,
201    event_counter: usize,
202    // Circle event validity tracking
203    circle_event_valid: Vec<bool>,
204    // Output
205    vertices: Vec<VoronoiVertex>,
206    edges: Vec<VoronoiEdge>,
207    // Edge tracking for each pair of sites
208    half_edges: Vec<HalfEdgeRecord>,
209}
210
211/// A half-edge record used during construction
212#[derive(Debug, Clone)]
213struct HalfEdgeRecord {
214    left_site: usize,
215    right_site: usize,
216    start_vertex: Option<usize>,
217    end_vertex: Option<usize>,
218    direction: [f64; 2],
219}
220
221impl FortuneBuilder {
222    fn new(sites: Vec<[f64; 2]>) -> Self {
223        Self {
224            sites,
225            beach_line: Vec::new(),
226            events: BinaryHeap::new(),
227            event_counter: 0,
228            circle_event_valid: Vec::new(),
229            vertices: Vec::new(),
230            edges: Vec::new(),
231            half_edges: Vec::new(),
232        }
233    }
234
235    fn next_event_id(&mut self) -> usize {
236        let id = self.event_counter;
237        self.event_counter += 1;
238        id
239    }
240
241    fn build(mut self) -> SpatialResult<VoronoiDiagram> {
242        let n = self.sites.len();
243        if n < 2 {
244            return Err(SpatialError::ValueError(
245                "Need at least 2 sites for Voronoi diagram".to_string(),
246            ));
247        }
248
249        // Sort sites by x-coordinate
250        let mut site_order: Vec<usize> = (0..n).collect();
251        site_order.sort_by(|&a, &b| {
252            self.sites[a][0]
253                .partial_cmp(&self.sites[b][0])
254                .unwrap_or(std::cmp::Ordering::Equal)
255                .then_with(|| {
256                    self.sites[a][1]
257                        .partial_cmp(&self.sites[b][1])
258                        .unwrap_or(std::cmp::Ordering::Equal)
259                })
260        });
261
262        // Initialize event queue with site events
263        for &idx in &site_order {
264            let id = self.next_event_id();
265            self.events.push(Event {
266                x: self.sites[idx][0],
267                kind: EventKind::Site(idx),
268                valid: true,
269                id,
270            });
271        }
272
273        // Process events
274        let max_iterations = n * n * 4 + 100;
275        let mut iterations = 0;
276
277        while let Some(event) = self.events.pop() {
278            iterations += 1;
279            if iterations > max_iterations {
280                break;
281            }
282
283            if !event.valid {
284                continue;
285            }
286
287            match event.kind {
288                EventKind::Site(site_idx) => {
289                    self.handle_site_event(site_idx, event.x);
290                }
291                EventKind::Circle {
292                    arc_idx,
293                    center_x,
294                    center_y,
295                    ..
296                } => {
297                    // Check if the circle event is still valid
298                    if arc_idx < self.circle_event_valid.len() && self.circle_event_valid[arc_idx] {
299                        self.handle_circle_event(arc_idx, center_x, center_y);
300                    }
301                }
302            }
303        }
304
305        // Finalize: close remaining half-edges
306        self.finalize_edges();
307
308        // Build cells
309        let cells = self.build_cells();
310
311        Ok(VoronoiDiagram {
312            sites: self.sites,
313            vertices: self.vertices,
314            edges: self.edges,
315            cells,
316        })
317    }
318
319    fn handle_site_event(&mut self, site_idx: usize, _sweep_x: f64) {
320        if self.beach_line.is_empty() {
321            self.beach_line.push(Arc {
322                site_idx,
323                circle_event: None,
324            });
325            return;
326        }
327
328        let site = self.sites[site_idx];
329
330        // Find the arc directly above the new site
331        let arc_idx = self.find_arc_above(site[1], site[0]);
332
333        // If the found arc has a circle event, invalidate it
334        if let Some(ce_idx) = self.beach_line[arc_idx].circle_event {
335            if ce_idx < self.circle_event_valid.len() {
336                self.circle_event_valid[ce_idx] = false;
337            }
338        }
339
340        let existing_site = self.beach_line[arc_idx].site_idx;
341
342        // Create a half-edge between existing_site and site_idx
343        let dir = self.compute_edge_direction(existing_site, site_idx);
344        self.half_edges.push(HalfEdgeRecord {
345            left_site: existing_site,
346            right_site: site_idx,
347            start_vertex: None,
348            end_vertex: None,
349            direction: dir,
350        });
351
352        // Split the arc: replace arc[arc_idx] with three arcs
353        let old_arc = Arc {
354            site_idx: existing_site,
355            circle_event: None,
356        };
357        let new_arc = Arc {
358            site_idx,
359            circle_event: None,
360        };
361        let old_arc_copy = Arc {
362            site_idx: existing_site,
363            circle_event: None,
364        };
365
366        // Insert the new arcs
367        self.beach_line[arc_idx] = old_arc;
368        self.beach_line.insert(arc_idx + 1, new_arc);
369        self.beach_line.insert(arc_idx + 2, old_arc_copy);
370
371        // Pad circle_event_valid
372        while self.circle_event_valid.len() < self.beach_line.len() {
373            self.circle_event_valid.push(false);
374        }
375
376        // Check for new circle events
377        if arc_idx > 0 {
378            self.check_circle_event(arc_idx);
379        }
380        if arc_idx + 2 < self.beach_line.len() {
381            self.check_circle_event(arc_idx + 2);
382        }
383    }
384
385    fn handle_circle_event(&mut self, arc_idx: usize, center_x: f64, center_y: f64) {
386        if arc_idx >= self.beach_line.len() || arc_idx == 0 || arc_idx >= self.beach_line.len() - 1
387        {
388            return;
389        }
390
391        // Invalidate this arc's circle event
392        if arc_idx < self.circle_event_valid.len() {
393            self.circle_event_valid[arc_idx] = false;
394        }
395
396        // Add a Voronoi vertex at the circle center
397        let vertex_idx = self.vertices.len();
398        self.vertices.push(VoronoiVertex::new(center_x, center_y));
399
400        // Get the three sites involved
401        let left_site = self.beach_line[arc_idx - 1].site_idx;
402        let mid_site = self.beach_line[arc_idx].site_idx;
403        let right_site = self.beach_line[arc_idx + 1].site_idx;
404
405        // Complete half-edges that end at this vertex
406        for he in &mut self.half_edges {
407            if ((he.left_site == left_site && he.right_site == mid_site)
408                || (he.left_site == mid_site && he.right_site == left_site))
409                && he.end_vertex.is_none()
410            {
411                he.end_vertex = Some(vertex_idx);
412            }
413            if ((he.left_site == mid_site && he.right_site == right_site)
414                || (he.left_site == right_site && he.right_site == mid_site))
415                && he.end_vertex.is_none()
416            {
417                he.end_vertex = Some(vertex_idx);
418            }
419        }
420
421        // Create a new half-edge between left_site and right_site
422        let dir = self.compute_edge_direction(left_site, right_site);
423        self.half_edges.push(HalfEdgeRecord {
424            left_site,
425            right_site,
426            start_vertex: Some(vertex_idx),
427            end_vertex: None,
428            direction: dir,
429        });
430
431        // Invalidate circle events for neighboring arcs
432        if arc_idx > 0 {
433            if let Some(ce_idx) = self.beach_line[arc_idx - 1].circle_event {
434                if ce_idx < self.circle_event_valid.len() {
435                    self.circle_event_valid[ce_idx] = false;
436                }
437            }
438            self.beach_line[arc_idx - 1].circle_event = None;
439        }
440        if arc_idx + 1 < self.beach_line.len() {
441            if let Some(ce_idx) = self.beach_line[arc_idx + 1].circle_event {
442                if ce_idx < self.circle_event_valid.len() {
443                    self.circle_event_valid[ce_idx] = false;
444                }
445            }
446            self.beach_line[arc_idx + 1].circle_event = None;
447        }
448
449        // Remove the disappearing arc
450        self.beach_line.remove(arc_idx);
451        if arc_idx < self.circle_event_valid.len() {
452            self.circle_event_valid.remove(arc_idx);
453        }
454
455        // Check for new circle events
456        if arc_idx > 0 && arc_idx < self.beach_line.len() {
457            self.check_circle_event(arc_idx - 1);
458            if arc_idx < self.beach_line.len() {
459                self.check_circle_event(arc_idx);
460            }
461        }
462    }
463
464    fn find_arc_above(&self, y: f64, sweep_x: f64) -> usize {
465        if self.beach_line.len() <= 1 {
466            return 0;
467        }
468
469        // For each arc, compute the breakpoints with its neighbors
470        // and find which arc is above the given y at the sweep line position
471        for i in 0..self.beach_line.len() {
472            if i + 1 < self.beach_line.len() {
473                let site_a = self.sites[self.beach_line[i].site_idx];
474                let site_b = self.sites[self.beach_line[i + 1].site_idx];
475
476                // Compute breakpoint between arcs i and i+1
477                if let Some(bp_y) = self.compute_breakpoint(site_a, site_b, sweep_x) {
478                    if y <= bp_y {
479                        return i;
480                    }
481                }
482            } else {
483                return i;
484            }
485        }
486
487        self.beach_line.len() - 1
488    }
489
490    fn compute_breakpoint(&self, site_a: [f64; 2], site_b: [f64; 2], sweep_x: f64) -> Option<f64> {
491        let ax = site_a[0];
492        let ay = site_a[1];
493        let bx = site_b[0];
494        let by = site_b[1];
495
496        // Degenerate: both sites have same x
497        if (ax - bx).abs() < EPSILON {
498            return Some((ay + by) / 2.0);
499        }
500
501        // Degenerate: one site is on the sweep line
502        if (ax - sweep_x).abs() < EPSILON {
503            return Some(ay);
504        }
505        if (bx - sweep_x).abs() < EPSILON {
506            return Some(by);
507        }
508
509        // General case: solve for y where the two parabolas intersect
510        let da = ax - sweep_x;
511        let db = bx - sweep_x;
512
513        let a = 1.0 / da - 1.0 / db;
514        let b = -2.0 * (ay / da - by / db);
515        let c = (ay * ay + ax * ax - sweep_x * sweep_x) / da
516            - (by * by + bx * bx - sweep_x * sweep_x) / db;
517
518        if a.abs() < EPSILON {
519            if b.abs() < EPSILON {
520                return None;
521            }
522            return Some(-c / b);
523        }
524
525        let discriminant = b * b - 4.0 * a * c;
526        if discriminant < 0.0 {
527            return Some((ay + by) / 2.0);
528        }
529
530        let sqrt_disc = discriminant.sqrt();
531        let y1 = (-b - sqrt_disc) / (2.0 * a);
532        let y2 = (-b + sqrt_disc) / (2.0 * a);
533
534        // Return the breakpoint that is between the two site y-coordinates
535        if (ax < bx) == (y1 < y2) {
536            Some(y1)
537        } else {
538            Some(y2)
539        }
540    }
541
542    fn check_circle_event(&mut self, arc_idx: usize) {
543        if arc_idx == 0 || arc_idx >= self.beach_line.len() - 1 {
544            return;
545        }
546
547        let left = self.beach_line[arc_idx - 1].site_idx;
548        let mid = self.beach_line[arc_idx].site_idx;
549        let right = self.beach_line[arc_idx + 1].site_idx;
550
551        // Don't create circle event if two consecutive arcs belong to the same site
552        if left == right {
553            return;
554        }
555
556        let a = self.sites[left];
557        let b = self.sites[mid];
558        let c = self.sites[right];
559
560        // Compute circumcircle
561        if let Some((cx, cy, r)) = circumcircle(a, b, c) {
562            let event_x = cx + r;
563
564            // Only add if the event is to the right of the rightmost site
565            let max_x = a[0].max(b[0]).max(c[0]);
566            if event_x >= max_x - EPSILON {
567                // Ensure circle_event_valid is large enough
568                while self.circle_event_valid.len() <= arc_idx {
569                    self.circle_event_valid.push(false);
570                }
571
572                self.circle_event_valid[arc_idx] = true;
573
574                let id = self.next_event_id();
575                self.beach_line[arc_idx].circle_event = Some(arc_idx);
576
577                self.events.push(Event {
578                    x: event_x,
579                    kind: EventKind::Circle {
580                        arc_idx,
581                        center_x: cx,
582                        center_y: cy,
583                        radius: r,
584                    },
585                    valid: true,
586                    id,
587                });
588            }
589        }
590    }
591
592    fn compute_edge_direction(&self, site_a: usize, site_b: usize) -> [f64; 2] {
593        let a = self.sites[site_a];
594        let b = self.sites[site_b];
595
596        // The edge direction is perpendicular to the line connecting the two sites
597        let dx = b[0] - a[0];
598        let dy = b[1] - a[1];
599
600        // Perpendicular direction (rotate 90 degrees)
601        [-dy, dx]
602    }
603
604    fn finalize_edges(&mut self) {
605        // Convert half-edges into Voronoi edges
606        for he in &self.half_edges {
607            self.edges.push(VoronoiEdge {
608                start_vertex: he.start_vertex,
609                end_vertex: he.end_vertex,
610                left_site: he.left_site,
611                right_site: he.right_site,
612                direction: Some(he.direction),
613            });
614        }
615    }
616
617    fn build_cells(&self) -> Vec<VoronoiCell> {
618        let n = self.sites.len();
619        let mut cells = Vec::with_capacity(n);
620
621        for site_idx in 0..n {
622            let mut edge_indices = Vec::new();
623            let mut vertex_indices = Vec::new();
624            let mut is_bounded = true;
625
626            for (edge_idx, edge) in self.edges.iter().enumerate() {
627                if edge.left_site == site_idx || edge.right_site == site_idx {
628                    edge_indices.push(edge_idx);
629
630                    if let Some(v) = edge.start_vertex {
631                        if !vertex_indices.contains(&v) {
632                            vertex_indices.push(v);
633                        }
634                    } else {
635                        is_bounded = false;
636                    }
637
638                    if let Some(v) = edge.end_vertex {
639                        if !vertex_indices.contains(&v) {
640                            vertex_indices.push(v);
641                        }
642                    } else {
643                        is_bounded = false;
644                    }
645                }
646            }
647
648            cells.push(VoronoiCell {
649                site_index: site_idx,
650                edge_indices,
651                vertex_indices,
652                is_bounded,
653            });
654        }
655
656        cells
657    }
658}
659
660/// Compute the circumcircle of three points
661///
662/// Returns (center_x, center_y, radius) or None if points are collinear
663fn circumcircle(a: [f64; 2], b: [f64; 2], c: [f64; 2]) -> Option<(f64, f64, f64)> {
664    let ax = a[0];
665    let ay = a[1];
666    let bx = b[0];
667    let by = b[1];
668    let cx = c[0];
669    let cy = c[1];
670
671    let d = 2.0 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
672
673    if d.abs() < EPSILON {
674        return None; // Collinear
675    }
676
677    let ux = ((ax * ax + ay * ay) * (by - cy)
678        + (bx * bx + by * by) * (cy - ay)
679        + (cx * cx + cy * cy) * (ay - by))
680        / d;
681    let uy = ((ax * ax + ay * ay) * (cx - bx)
682        + (bx * bx + by * by) * (ax - cx)
683        + (cx * cx + cy * cy) * (bx - ax))
684        / d;
685
686    let r = ((ax - ux).powi(2) + (ay - uy).powi(2)).sqrt();
687
688    Some((ux, uy, r))
689}
690
691/// Compute a Voronoi diagram using Fortune's sweep line algorithm
692///
693/// # Arguments
694///
695/// * `sites` - A slice of 2D point coordinates [x, y]
696///
697/// # Returns
698///
699/// * `SpatialResult<VoronoiDiagram>` - The computed Voronoi diagram
700///
701/// # Examples
702///
703/// ```
704/// use scirs2_spatial::computational_geometry::fortune_voronoi::fortune_voronoi_2d;
705///
706/// let sites = vec![[0.0, 0.0], [1.0, 0.0], [0.5, 1.0]];
707/// let diagram = fortune_voronoi_2d(&sites).expect("Operation failed");
708///
709/// assert_eq!(diagram.num_sites(), 3);
710/// assert!(!diagram.vertices.is_empty());
711/// ```
712pub fn fortune_voronoi_2d(sites: &[[f64; 2]]) -> SpatialResult<VoronoiDiagram> {
713    if sites.len() < 2 {
714        return Err(SpatialError::ValueError(
715            "Need at least 2 sites for Voronoi diagram".to_string(),
716        ));
717    }
718
719    // Check for duplicate sites
720    for i in 0..sites.len() {
721        for j in (i + 1)..sites.len() {
722            if (sites[i][0] - sites[j][0]).abs() < EPSILON
723                && (sites[i][1] - sites[j][1]).abs() < EPSILON
724            {
725                return Err(SpatialError::ValueError(format!(
726                    "Duplicate sites at index {} and {}: [{}, {}]",
727                    i, j, sites[i][0], sites[i][1]
728                )));
729            }
730        }
731    }
732
733    let builder = FortuneBuilder::new(sites.to_vec());
734    builder.build()
735}
736
737/// Compute the Voronoi diagram from an ndarray of points
738///
739/// # Arguments
740///
741/// * `points` - A 2D array of points (n x 2)
742///
743/// # Returns
744///
745/// * `SpatialResult<VoronoiDiagram>` - The computed Voronoi diagram
746pub fn fortune_voronoi_from_array(
747    points: &scirs2_core::ndarray::ArrayView2<'_, f64>,
748) -> SpatialResult<VoronoiDiagram> {
749    if points.ncols() != 2 {
750        return Err(SpatialError::DimensionError(
751            "Points must be 2D for Voronoi diagram".to_string(),
752        ));
753    }
754
755    let sites: Vec<[f64; 2]> = (0..points.nrows())
756        .map(|i| [points[[i, 0]], points[[i, 1]]])
757        .collect();
758
759    fortune_voronoi_2d(&sites)
760}
761
762#[cfg(test)]
763mod tests {
764    use super::*;
765
766    #[test]
767    fn test_two_sites() {
768        let sites = vec![[0.0, 0.0], [2.0, 0.0]];
769        let diagram = fortune_voronoi_2d(&sites).expect("Operation failed");
770
771        assert_eq!(diagram.num_sites(), 2);
772        // Two sites produce one edge (the perpendicular bisector)
773        assert!(diagram.num_edges() >= 1);
774    }
775
776    #[test]
777    fn test_three_sites_triangle() {
778        let sites = vec![[0.0, 0.0], [2.0, 0.0], [1.0, 2.0]];
779        let diagram = fortune_voronoi_2d(&sites).expect("Operation failed");
780
781        assert_eq!(diagram.num_sites(), 3);
782        // Three non-collinear sites produce 1 Voronoi vertex (circumcenter) and 3 edges
783        assert!(diagram.num_vertices() >= 1);
784        assert!(diagram.num_edges() >= 3);
785    }
786
787    #[test]
788    fn test_four_sites_square() {
789        let sites = vec![[0.0, 0.0], [2.0, 0.0], [2.0, 2.0], [0.0, 2.0]];
790        let diagram = fortune_voronoi_2d(&sites).expect("Operation failed");
791
792        assert_eq!(diagram.num_sites(), 4);
793        assert!(!diagram.vertices.is_empty());
794        assert!(!diagram.edges.is_empty());
795        // Each site should have a cell
796        assert_eq!(diagram.cells.len(), 4);
797    }
798
799    #[test]
800    fn test_nearest_site() {
801        let sites = vec![[0.0, 0.0], [10.0, 0.0], [5.0, 10.0]];
802        let diagram = fortune_voronoi_2d(&sites).expect("Operation failed");
803
804        // Point near site 0
805        assert_eq!(diagram.nearest_site(&[0.1, 0.1]), 0);
806        // Point near site 1
807        assert_eq!(diagram.nearest_site(&[9.9, 0.1]), 1);
808        // Point near site 2
809        assert_eq!(diagram.nearest_site(&[5.0, 9.0]), 2);
810    }
811
812    #[test]
813    fn test_cells_created() {
814        let sites = vec![[0.0, 0.0], [1.0, 0.0], [0.5, 1.0]];
815        let diagram = fortune_voronoi_2d(&sites).expect("Operation failed");
816
817        // Each site should have a cell with at least one edge
818        for cell in &diagram.cells {
819            assert!(!cell.edge_indices.is_empty());
820        }
821    }
822
823    #[test]
824    fn test_collinear_sites() {
825        // Three collinear sites
826        let sites = vec![[0.0, 0.0], [1.0, 0.0], [2.0, 0.0]];
827        let diagram = fortune_voronoi_2d(&sites).expect("Operation failed");
828
829        assert_eq!(diagram.num_sites(), 3);
830        // Should produce 2 edges (perpendicular bisectors) but no finite vertex
831        assert!(diagram.num_edges() >= 2);
832    }
833
834    #[test]
835    fn test_duplicate_sites_error() {
836        let sites = vec![[0.0, 0.0], [0.0, 0.0], [1.0, 1.0]];
837        let result = fortune_voronoi_2d(&sites);
838        assert!(result.is_err());
839    }
840
841    #[test]
842    fn test_too_few_sites() {
843        let sites = vec![[0.0, 0.0]];
844        let result = fortune_voronoi_2d(&sites);
845        assert!(result.is_err());
846    }
847
848    #[test]
849    fn test_circumcircle() {
850        let a = [0.0, 0.0];
851        let b = [1.0, 0.0];
852        let c = [0.0, 1.0];
853
854        let result = circumcircle(a, b, c);
855        assert!(result.is_some());
856
857        let (cx, cy, r) = result.expect("Operation failed");
858        // Circumcenter of right triangle (0,0)-(1,0)-(0,1) is at (0.5, 0.5)
859        assert!((cx - 0.5).abs() < 1e-10);
860        assert!((cy - 0.5).abs() < 1e-10);
861        // Radius = distance from center to any vertex = sqrt(0.5) ~ 0.707
862        assert!((r - (0.5_f64).sqrt()).abs() < 1e-10);
863    }
864
865    #[test]
866    fn test_circumcircle_collinear() {
867        let a = [0.0, 0.0];
868        let b = [1.0, 0.0];
869        let c = [2.0, 0.0];
870
871        let result = circumcircle(a, b, c);
872        assert!(result.is_none());
873    }
874
875    #[test]
876    fn test_from_array() {
877        use scirs2_core::ndarray::array;
878
879        let points = array![[0.0, 0.0], [1.0, 0.0], [0.5, 1.0]];
880        let diagram = fortune_voronoi_from_array(&points.view()).expect("Operation failed");
881        assert_eq!(diagram.num_sites(), 3);
882    }
883
884    #[test]
885    fn test_many_sites() {
886        // 8 sites in a circle pattern
887        let mut sites = Vec::new();
888        for i in 0..8 {
889            let angle = 2.0 * std::f64::consts::PI * (i as f64) / 8.0;
890            sites.push([angle.cos(), angle.sin()]);
891        }
892
893        let diagram = fortune_voronoi_2d(&sites).expect("Operation failed");
894        assert_eq!(diagram.num_sites(), 8);
895        assert!(!diagram.vertices.is_empty());
896        assert!(!diagram.edges.is_empty());
897    }
898}