use togo::prelude::*;
use std::collections::HashMap;
use aabb::HilbertRTree;
const VERTEX_TOLERANCE: f64 = 1e-8;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct VertexId(usize);
#[derive(Debug, Clone)]
struct GraphEdge {
arc: Arc,
from: VertexId,
to: VertexId,
id: usize,
}
#[derive(Debug)]
struct CycleGraph {
vertices: Vec<Point>,
adjacency: HashMap<VertexId, Vec<usize>>,
edges: Vec<GraphEdge>,
#[allow(dead_code)]
vertex_spatial_index: Option<HilbertRTree>,
}
impl CycleGraph {
fn new() -> Self {
Self {
vertices: Vec::new(),
adjacency: HashMap::new(),
edges: Vec::new(),
vertex_spatial_index: None,
}
}
fn add_vertex_raw(&mut self, point: Point) -> VertexId {
let vertex_id = VertexId(self.vertices.len());
self.vertices.push(point);
self.adjacency.insert(vertex_id, Vec::new());
vertex_id
}
fn add_edge(&mut self, arc: Arc) {
let from = self.add_vertex_raw(arc.a);
let to = self.add_vertex_raw(arc.b);
let edge = GraphEdge {
arc,
from,
to,
id: self.edges.len(),
};
self.adjacency.get_mut(&from).unwrap().push(edge.id);
self.adjacency.get_mut(&to).unwrap().push(edge.id);
self.edges.push(edge);
}
fn get_adjacent_edges(&self, vertex: VertexId) -> &[usize] {
self.adjacency.get(&vertex).map(|v| v.as_slice()).unwrap_or(&[])
}
fn get_vertex_position(&self, vertex: VertexId) -> Point {
self.vertices[vertex.0]
}
}
fn build_graph(arcs: &[Arc]) -> CycleGraph {
let mut graph = CycleGraph::new();
for arc in arcs {
graph.add_edge(*arc);
}
if graph.vertices.is_empty() {
return graph;
}
let mut spatial_index = HilbertRTree::with_capacity(graph.vertices.len());
for point in &graph.vertices {
spatial_index.add_point(point.x, point.y);
}
spatial_index.build();
let mut vertex_mapping: HashMap<usize, usize> = HashMap::new(); let mut merged_vertices: Vec<Point> = Vec::with_capacity(graph.vertices.len());
let mut used = vec![false; graph.vertices.len()];
let mut nearby_indices: Vec<usize> = Vec::with_capacity(graph.vertices.len() / 8);
for i in 0..graph.vertices.len() {
if used[i] {
continue;
}
let point_i = graph.vertices[i];
nearby_indices.clear();
spatial_index.query_circle(point_i.x, point_i.y, VERTEX_TOLERANCE, &mut nearby_indices);
let new_vertex_id = merged_vertices.len();
merged_vertices.push(point_i);
vertex_mapping.insert(i, new_vertex_id);
used[i] = true;
for &nearby_idx in &nearby_indices {
if nearby_idx != i && !used[nearby_idx] {
vertex_mapping.insert(nearby_idx, new_vertex_id);
used[nearby_idx] = true;
}
}
}
let mut new_graph = CycleGraph::new();
new_graph.vertices = merged_vertices;
new_graph.edges.reserve(graph.edges.len());
for i in 0..new_graph.vertices.len() {
new_graph.adjacency.insert(VertexId(i), Vec::new());
}
for edge in &graph.edges {
let old_from = edge.from.0;
let old_to = edge.to.0;
let new_from_id = *vertex_mapping.get(&old_from).unwrap_or(&old_from);
let new_to_id = *vertex_mapping.get(&old_to).unwrap_or(&old_to);
let new_from = VertexId(new_from_id);
let new_to = VertexId(new_to_id);
let remapped_edge = GraphEdge {
arc: edge.arc,
from: new_from,
to: new_to,
id: new_graph.edges.len(),
};
new_graph.adjacency.get_mut(&new_from).unwrap().push(remapped_edge.id);
new_graph.adjacency.get_mut(&new_to).unwrap().push(remapped_edge.id);
new_graph.edges.push(remapped_edge);
}
new_graph
}
fn find_next_edge(
graph: &CycleGraph,
current_vertex: VertexId,
came_from_edge: Option<usize>,
used_edges: &[bool],
cycle_edges: &[usize]
) -> Option<usize> {
let adjacent_edges = graph.get_adjacent_edges(current_vertex);
let mut available_edges: Vec<usize> = Vec::with_capacity(adjacent_edges.len());
for &edge_id in adjacent_edges {
if Some(edge_id) != came_from_edge && !used_edges[edge_id] && !cycle_edges.contains(&edge_id) {
available_edges.push(edge_id);
}
}
if available_edges.is_empty() {
return None;
}
if available_edges.len() == 1 {
return Some(available_edges[0]);
}
if let Some(from_edge_id) = came_from_edge {
return choose_rightmost_edge(graph, current_vertex, from_edge_id, &available_edges);
}
Some(available_edges[0])
}
fn choose_rightmost_edge(
graph: &CycleGraph,
vertex: VertexId,
incoming_edge_id: usize,
available_edges: &[usize]
) -> Option<usize> {
let vertex_pos = graph.get_vertex_position(vertex);
let incoming_edge = &graph.edges[incoming_edge_id];
let incoming_direction = get_arc_direction_at_vertex(&incoming_edge.arc, vertex_pos, true);
let mut edge_angles: Vec<(usize, f64)> = Vec::with_capacity(available_edges.len());
for &edge_id in available_edges {
let edge = &graph.edges[edge_id];
let outgoing_direction = get_arc_direction_at_vertex(&edge.arc, vertex_pos, false);
let cross = incoming_direction.x * outgoing_direction.y - incoming_direction.y * outgoing_direction.x;
let dot = incoming_direction.x * outgoing_direction.x + incoming_direction.y * outgoing_direction.y;
let angle = cross.atan2(dot);
edge_angles.push((edge_id, angle));
}
edge_angles.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
let rightmost_edge = edge_angles.iter()
.find(|(_, angle)| *angle > 0.0) .or_else(|| edge_angles.last()) .map(|(edge_id, _)| *edge_id);
rightmost_edge
}
fn get_arc_direction_at_vertex(arc: &Arc, vertex_pos: Point, incoming: bool) -> Point {
let tangents = arc.tangents();
let tolerance = 1e-10;
let is_at_start = (vertex_pos - arc.a).norm() < tolerance;
let is_at_end = (vertex_pos - arc.b).norm() < tolerance;
if is_at_start {
if incoming {
Point::new(-tangents[0].x, -tangents[0].y)
} else {
Point::new(tangents[0].x, tangents[0].y)
}
} else if is_at_end {
if incoming {
Point::new(tangents[1].x, tangents[1].y)
} else {
Point::new(-tangents[1].x, -tangents[1].y)
}
} else {
if incoming {
let (dir, _) = (vertex_pos - arc.a).normalize(false);
if (vertex_pos - arc.a).norm() < (vertex_pos - arc.b).norm() {
dir
} else {
let (dir, _) = (vertex_pos - arc.b).normalize(false);
dir
}
} else {
let (dir, _) = (arc.b - vertex_pos).normalize(false);
if (vertex_pos - arc.a).norm() < (vertex_pos - arc.b).norm() {
dir
} else {
let (dir, _) = (arc.a - vertex_pos).normalize(false);
dir
}
}
}
}
fn find_cycle_from_edge(
graph: &CycleGraph,
start_edge_id: usize,
used_edges: &mut Vec<bool>
) -> Option<Vec<Arc>> {
if used_edges[start_edge_id] {
return None;
}
let mut cycle_edges = Vec::new();
let mut current_edge_id = start_edge_id;
let start_vertex = graph.edges[start_edge_id].from;
let mut current_vertex = graph.edges[start_edge_id].to;
loop {
cycle_edges.push(current_edge_id);
if current_vertex == start_vertex {
for edge_id in &cycle_edges {
used_edges[*edge_id] = true;
}
let cycle_arcs: Vec<Arc> = cycle_edges.iter()
.map(|&edge_id| graph.edges[edge_id].arc)
.collect();
return Some(cycle_arcs);
}
if let Some(next_edge_id) = find_next_edge(graph, current_vertex, Some(current_edge_id), used_edges, &cycle_edges) {
let next_edge = &graph.edges[next_edge_id];
current_vertex = if next_edge.from == current_vertex {
next_edge.to
} else {
next_edge.from
};
current_edge_id = next_edge_id;
} else {
return None;
}
}
}
pub fn find_non_intersecting_cycles(arcs: &[Arc]) -> Vec<Vec<Arc>> {
if arcs.is_empty() {
return Vec::new();
}
let graph = build_graph(arcs);
let mut cycles = Vec::new();
let mut used_edges = vec![false; graph.edges.len()];
for edge_id in 0..graph.edges.len() {
if let Some(cycle) = find_cycle_from_edge(&graph, edge_id, &mut used_edges) {
cycles.push(cycle);
}
}
cycles
}
#[cfg(test)]
mod test_find_cycles_basic {
use super::*;
#[test]
fn test_empty_input() {
let result = find_non_intersecting_cycles(&[]);
assert!(result.is_empty());
}
#[test]
fn test_single_arc() {
let arcs = vec![
arcseg(point(0.0, 0.0), point(1.0, 0.0)),
];
let result = find_non_intersecting_cycles(&arcs);
assert!(result.is_empty());
}
#[test]
fn test_simple_triangle() {
let arcs = vec![
arcseg(point(0.0, 0.0), point(1.0, 0.0)),
arcseg(point(1.0, 0.0), point(0.5, 1.0)),
arcseg(point(0.5, 1.0), point(0.0, 0.0)),
];
let result = find_non_intersecting_cycles(&arcs);
assert_eq!(result.len(), 1);
assert_eq!(result[0].len(), 3);
}
#[test]
fn test_simple_square() {
let arcs = vec![
arcseg(point(0.0, 0.0), point(1.0, 0.0)),
arcseg(point(1.0, 0.0), point(1.0, 1.0)),
arcseg(point(1.0, 1.0), point(0.0, 1.0)),
arcseg(point(0.0, 1.0), point(0.0, 0.0)),
];
let result = find_non_intersecting_cycles(&arcs);
assert_eq!(result.len(), 1);
assert_eq!(result[0].len(), 4);
}
#[test]
fn test_build_graph() {
let arcs = vec![
arcseg(point(0.0, 0.0), point(1.0, 0.0)),
arcseg(point(1.0, 0.0), point(1.0, 1.0)),
];
let graph = build_graph(&arcs);
assert_eq!(graph.vertices.len(), 3); assert_eq!(graph.edges.len(), 2);
}
#[test]
fn test_vertex_merging() {
let arcs = vec![
arcseg(point(0.0, 0.0), point(1.0, 0.0)),
arcseg(point(1.0, 1e-10), point(2.0, 0.0)), ];
let graph = build_graph(&arcs);
assert_eq!(graph.vertices.len(), 3);
}
#[test]
fn test_figure_eight() {
let arcs = vec![
arcseg(point(0.0, 0.0), point(1.0, 0.0)),
arcseg(point(1.0, 0.0), point(1.0, 1.0)),
arcseg(point(1.0, 1.0), point(0.0, 1.0)),
arcseg(point(0.0, 1.0), point(0.0, 0.0)),
arcseg(point(1.0, 0.0), point(2.0, 0.0)),
arcseg(point(2.0, 0.0), point(2.0, 1.0)),
arcseg(point(2.0, 1.0), point(1.0, 1.0)),
];
let result = find_non_intersecting_cycles(&arcs);
assert!(!result.is_empty());
for cycle in &result {
assert!(cycle.len() >= 3);
}
}
#[test]
fn test_x_intersection() {
let center = point(0.5, 0.5);
let arcs = vec![
arcseg(point(0.0, 0.0), center),
arcseg(center, point(1.0, 1.0)),
arcseg(point(0.0, 1.0), center),
arcseg(center, point(1.0, 0.0)),
];
let result = find_non_intersecting_cycles(&arcs);
assert!(result.is_empty());
}
#[test]
fn test_double_edges() {
let p1 = point(0.0, 0.0);
let p2 = point(2.0, 0.0);
let bulge1 = 1.0; let bulge2 = 1.0;
let arcs = vec![
arc_from_bulge(p1, p2, bulge1),
arc_from_bulge(p2, p1, bulge2),
];
let result = find_non_intersecting_cycles(&arcs);
assert_eq!(result.len(), 1);
assert_eq!(result[0].len(), 2);
}
#[test]
fn test_multiple_separate_cycles() {
let arcs = vec![
arcseg(point(0.0, 0.0), point(1.0, 0.0)),
arcseg(point(1.0, 0.0), point(0.5, 1.0)),
arcseg(point(0.5, 1.0), point(0.0, 0.0)),
arcseg(point(3.0, 0.0), point(4.0, 0.0)),
arcseg(point(4.0, 0.0), point(3.5, 1.0)),
arcseg(point(3.5, 1.0), point(3.0, 0.0)),
];
let result = find_non_intersecting_cycles(&arcs);
assert_eq!(result.len(), 2);
for cycle in &result {
assert_eq!(cycle.len(), 3);
}
}
#[test]
fn test_mixed_arc_types() {
let arcs = vec![
arcseg(point(0.0, 0.0), point(1.0, 0.0)), arc_from_bulge(point(1.0, 0.0), point(0.0, 1.0), 0.5), arcseg(point(0.0, 1.0), point(0.0, 0.0)), ];
let result = find_non_intersecting_cycles(&arcs);
assert_eq!(result.len(), 1);
assert_eq!(result[0].len(), 3);
}
#[test]
fn test_choose_rightmost_edge() {
let arcs = vec![
arcseg(point(0.0, 0.0), point(1.0, 1.0)),
arcseg(point(1.0, 1.0), point(2.0, 1.0)), arcseg(point(1.0, 1.0), point(1.0, 2.0)), arcseg(point(1.0, 1.0), point(0.0, 2.0)), ];
let graph = build_graph(&arcs);
let vertex_id = VertexId(1); let incoming_edge = 0;
let available_edges = vec![1, 2, 3];
let chosen = choose_rightmost_edge(&graph, vertex_id, incoming_edge, &available_edges);
assert!(chosen.is_some());
}
#[test]
fn test_complex_graph_with_branches() {
let arcs = vec![
arcseg(point(0.0, 0.0), point(2.0, 0.0)),
arcseg(point(2.0, 0.0), point(2.0, 2.0)),
arcseg(point(2.0, 2.0), point(0.0, 2.0)),
arcseg(point(0.0, 2.0), point(0.0, 0.0)),
arcseg(point(1.0, 0.0), point(1.0, 2.0)),
arcseg(point(0.0, 1.0), point(2.0, 1.0)),
];
let result = find_non_intersecting_cycles(&arcs);
assert!(!result.is_empty());
for cycle in &result {
assert!(!cycle.is_empty());
}
}
mod integration_tests {
use super::*;
use crate::graph::merge_ends::merge_close_endpoints;
#[test]
fn test_integration_square_with_close_endpoints() {
let mut arcs = vec![
arcseg(
Point::new(0.0, 0.0),
Point::new(1.0, 0.0001), ),
arcseg(
Point::new(1.0, -0.0001), Point::new(1.0, 1.0),
),
arcseg(
Point::new(1.0, 1.0),
Point::new(0.0001, 1.0), ),
arcseg(
Point::new(-0.0001, 1.0), Point::new(0.0, 0.0),
),
];
merge_close_endpoints(&mut arcs, 0.001);
let cycles = find_non_intersecting_cycles(&arcs);
assert_eq!(cycles.len(), 1);
assert_eq!(cycles[0].len(), 4); }
#[test]
fn test_integration_disconnected_arcs_before_merge() {
let mut arcs = vec![
arcseg(
Point::new(0.0, 0.0),
Point::new(1.0, 0.0),
),
arcseg(
Point::new(1.0002, 0.0), Point::new(1.0, 1.0),
),
arcseg(
Point::new(1.0, 1.0),
Point::new(0.0, 1.0002), ),
arcseg(
Point::new(0.0, 0.9998), Point::new(0.0001, 0.0), ),
];
let cycles_before = find_non_intersecting_cycles(&arcs);
assert!(cycles_before.is_empty());
merge_close_endpoints(&mut arcs, 0.001);
let cycles_after = find_non_intersecting_cycles(&arcs);
assert_eq!(cycles_after.len(), 1);
}
#[test]
fn test_integration_multiple_cycles_with_merging() {
let mut arcs = vec![
arcseg(Point::new(0.0, 0.0), Point::new(1.0, 0.0)),
arcseg(Point::new(1.0001, 0.0), Point::new(1.0, 1.0)),
arcseg(Point::new(1.0, 1.0), Point::new(0.0, 1.0)),
arcseg(Point::new(-0.0001, 1.0), Point::new(0.0, 0.0)),
arcseg(Point::new(2.0, 0.0), Point::new(3.0, 0.0)),
arcseg(Point::new(3.0001, 0.0), Point::new(3.0, 1.0)),
arcseg(Point::new(3.0, 1.0), Point::new(2.0, 1.0)),
arcseg(Point::new(1.9999, 1.0), Point::new(2.0, 0.0)),
];
merge_close_endpoints(&mut arcs, 0.001);
let cycles = find_non_intersecting_cycles(&arcs);
assert_eq!(cycles.len(), 2);
for cycle in &cycles {
assert_eq!(cycle.len(), 4); }
}
#[test]
fn test_integration_small_arcs_elimination() {
let mut arcs = vec![
arcseg(
Point::new(0.0, 0.0),
Point::new(1.0, 0.0),
),
arcseg(
Point::new(1.0, 0.0),
Point::new(1.00001, 0.0), ),
arcseg(
Point::new(1.00001, 0.0),
Point::new(0.5, 1.0),
),
arcseg(
Point::new(0.5, 1.0),
Point::new(0.0, 0.0),
),
];
let original_count = arcs.len();
merge_close_endpoints(&mut arcs, 0.001);
assert!(arcs.len() < original_count);
let cycles = find_non_intersecting_cycles(&arcs);
assert_eq!(cycles.len(), 1);
}
#[test]
fn test_integration_circular_arcs_with_gaps() {
let mut arcs = vec![
arcseg(
Point::new(0.0, 0.0),
Point::new(1.0001, 0.0), ),
arcseg(
Point::new(0.9999, 0.0), Point::new(0.5, 1.0),
),
arcseg(
Point::new(0.5, 1.0),
Point::new(0.0001, 0.0), ),
];
merge_close_endpoints(&mut arcs, 0.01);
let cycles = find_non_intersecting_cycles(&arcs);
assert_eq!(cycles.len(), 1);
}
}
}