1414//! 4. Return non-intersecting cycles as separate arc sequences
1515
1616use togo:: prelude:: * ;
17- use std:: collections:: { HashMap , HashSet } ;
17+ use std:: collections:: HashMap ;
18+ use aabb:: HilbertRTree ;
1819
1920/// Tolerance for considering vertices as the same point
2021const VERTEX_TOLERANCE : f64 = 1e-8 ;
@@ -45,6 +46,9 @@ struct CycleGraph {
4546 adjacency : HashMap < VertexId , Vec < usize > > ,
4647 /// All edges in the graph
4748 edges : Vec < GraphEdge > ,
49+ /// Spatial index for fast vertex lookup
50+ #[ allow( dead_code) ]
51+ vertex_spatial_index : Option < HilbertRTree > ,
4852}
4953
5054impl CycleGraph {
@@ -54,19 +58,14 @@ impl CycleGraph {
5458 vertices : Vec :: new ( ) ,
5559 adjacency : HashMap :: new ( ) ,
5660 edges : Vec :: new ( ) ,
61+ vertex_spatial_index : None ,
5762 }
5863 }
5964
60- /// Add or find vertex for a given point, merging close points
61- fn add_vertex ( & mut self , point : Point ) -> VertexId {
62- // Check if point is close to existing vertex
63- for ( i, existing_point) in self . vertices . iter ( ) . enumerate ( ) {
64- if ( point - * existing_point) . norm ( ) < VERTEX_TOLERANCE {
65- return VertexId ( i) ;
66- }
67- }
68-
69- // Add new vertex
65+ /// Add vertex without merging - just collect all endpoints
66+ /// Merging happens later in build_graph using spatial index
67+ fn add_vertex_raw ( & mut self , point : Point ) -> VertexId {
68+ // Always add, no filtering - merging done post-build
7069 let vertex_id = VertexId ( self . vertices . len ( ) ) ;
7170 self . vertices . push ( point) ;
7271 self . adjacency . insert ( vertex_id, Vec :: new ( ) ) ;
@@ -75,8 +74,8 @@ impl CycleGraph {
7574
7675 /// Add an edge to the graph
7776 fn add_edge ( & mut self , arc : Arc ) {
78- let from = self . add_vertex ( arc. a ) ;
79- let to = self . add_vertex ( arc. b ) ;
77+ let from = self . add_vertex_raw ( arc. a ) ;
78+ let to = self . add_vertex_raw ( arc. b ) ;
8079
8180 let edge = GraphEdge {
8281 arc,
@@ -104,14 +103,95 @@ impl CycleGraph {
104103}
105104
106105/// Build graph from input arcs
106+ /// Build graph from input arcs with optional spatial-indexed vertex merging.
107+ /// Merging is done here to handle cases where find_non_intersecting_cycles is called
108+ /// without prior merge_close_endpoints, but the overhead is minimal when vertices are
109+ /// already merged (most vertices map to themselves).
107110fn build_graph ( arcs : & [ Arc ] ) -> CycleGraph {
108111 let mut graph = CycleGraph :: new ( ) ;
109112
113+ // Pass 1: Add all arcs and collect vertices
110114 for arc in arcs {
111115 graph. add_edge ( * arc) ;
112116 }
113117
114- graph
118+ if graph. vertices . is_empty ( ) {
119+ return graph;
120+ }
121+
122+ // Pass 2: Build spatial index of all vertices
123+ let mut spatial_index = HilbertRTree :: with_capacity ( graph. vertices . len ( ) ) ;
124+ for point in & graph. vertices {
125+ spatial_index. add_point ( point. x , point. y ) ;
126+ }
127+ spatial_index. build ( ) ;
128+
129+ // Pass 3: Find and merge close vertices using spatial queries
130+ let mut vertex_mapping: HashMap < usize , usize > = HashMap :: new ( ) ; // old_id -> new_id
131+ let mut merged_vertices: Vec < Point > = Vec :: with_capacity ( graph. vertices . len ( ) ) ;
132+ let mut used = vec ! [ false ; graph. vertices. len( ) ] ;
133+ let mut nearby_indices: Vec < usize > = Vec :: with_capacity ( graph. vertices . len ( ) / 8 ) ; // Preallocate reusable buffer
134+
135+ for i in 0 ..graph. vertices . len ( ) {
136+ if used[ i] {
137+ continue ;
138+ }
139+
140+ let point_i = graph. vertices [ i] ;
141+
142+ // Find all nearby vertices using spatial index
143+ nearby_indices. clear ( ) ;
144+ spatial_index. query_circle ( point_i. x , point_i. y , VERTEX_TOLERANCE , & mut nearby_indices) ;
145+
146+ // Keep the first one, merge others into it
147+ let new_vertex_id = merged_vertices. len ( ) ;
148+ merged_vertices. push ( point_i) ;
149+ vertex_mapping. insert ( i, new_vertex_id) ;
150+ used[ i] = true ;
151+
152+ // Merge nearby vertices (already filtered by query_circle, no need to check distance again)
153+ for & nearby_idx in & nearby_indices {
154+ if nearby_idx != i && !used[ nearby_idx] {
155+ vertex_mapping. insert ( nearby_idx, new_vertex_id) ;
156+ used[ nearby_idx] = true ;
157+ }
158+ }
159+ }
160+
161+ // Pass 4: Rebuild graph with merged vertices
162+ let mut new_graph = CycleGraph :: new ( ) ;
163+ new_graph. vertices = merged_vertices;
164+ new_graph. edges . reserve ( graph. edges . len ( ) ) ;
165+
166+ // Initialize adjacency lists for new vertices
167+ for i in 0 ..new_graph. vertices . len ( ) {
168+ new_graph. adjacency . insert ( VertexId ( i) , Vec :: new ( ) ) ;
169+ }
170+
171+ // Remap edges to use new vertex IDs
172+ for edge in & graph. edges {
173+ let old_from = edge. from . 0 ;
174+ let old_to = edge. to . 0 ;
175+
176+ // Look up merged vertex IDs - most vertices map to themselves if not merged
177+ let new_from_id = * vertex_mapping. get ( & old_from) . unwrap_or ( & old_from) ;
178+ let new_to_id = * vertex_mapping. get ( & old_to) . unwrap_or ( & old_to) ;
179+ let new_from = VertexId ( new_from_id) ;
180+ let new_to = VertexId ( new_to_id) ;
181+
182+ let remapped_edge = GraphEdge {
183+ arc : edge. arc ,
184+ from : new_from,
185+ to : new_to,
186+ id : new_graph. edges . len ( ) ,
187+ } ;
188+
189+ new_graph. adjacency . get_mut ( & new_from) . unwrap ( ) . push ( remapped_edge. id ) ;
190+ new_graph. adjacency . get_mut ( & new_to) . unwrap ( ) . push ( remapped_edge. id ) ;
191+ new_graph. edges . push ( remapped_edge) ;
192+ }
193+
194+ new_graph
115195}
116196
117197/// Find the next edge to follow from current vertex, avoiding the edge we came from
@@ -120,17 +200,18 @@ fn find_next_edge(
120200 graph : & CycleGraph ,
121201 current_vertex : VertexId ,
122202 came_from_edge : Option < usize > ,
123- used_edges : & HashSet < usize >
203+ used_edges : & [ bool ] ,
204+ cycle_edges : & [ usize ]
124205) -> Option < usize > {
125206 let adjacent_edges = graph. get_adjacent_edges ( current_vertex) ;
126207
127- // Filter out the edge we came from and already used edges
128- let available_edges: Vec < usize > = adjacent_edges. iter ( )
129- . copied ( )
130- . filter ( | & edge_id| {
131- Some ( edge_id ) != came_from_edge && !used_edges . contains ( & edge_id)
132- } )
133- . collect ( ) ;
208+ // Filter out the edge we came from, already used edges, and edges in current cycle
209+ let mut available_edges: Vec < usize > = Vec :: with_capacity ( adjacent_edges. len ( ) ) ;
210+ for & edge_id in adjacent_edges {
211+ if Some ( edge_id ) != came_from_edge && !used_edges [ edge_id ] && !cycle_edges . contains ( & edge_id) {
212+ available_edges . push ( edge_id) ;
213+ }
214+ }
134215
135216 if available_edges. is_empty ( ) {
136217 return None ;
@@ -165,7 +246,7 @@ fn choose_rightmost_edge(
165246 let incoming_direction = get_arc_direction_at_vertex ( & incoming_edge. arc , vertex_pos, true ) ;
166247
167248 // Calculate angles for all available outgoing edges
168- let mut edge_angles: Vec < ( usize , f64 ) > = Vec :: new ( ) ;
249+ let mut edge_angles: Vec < ( usize , f64 ) > = Vec :: with_capacity ( available_edges . len ( ) ) ;
169250
170251 for & edge_id in available_edges {
171252 let edge = & graph. edges [ edge_id] ;
@@ -251,9 +332,9 @@ fn get_arc_direction_at_vertex(arc: &Arc, vertex_pos: Point, incoming: bool) ->
251332fn find_cycle_from_edge (
252333 graph : & CycleGraph ,
253334 start_edge_id : usize ,
254- used_edges : & mut HashSet < usize >
335+ used_edges : & mut Vec < bool >
255336) -> Option < Vec < Arc > > {
256- if used_edges. contains ( & start_edge_id) {
337+ if used_edges[ start_edge_id] {
257338 return None ;
258339 }
259340
@@ -270,7 +351,7 @@ fn find_cycle_from_edge(
270351 if current_vertex == start_vertex {
271352 // Mark all edges in this cycle as used
272353 for edge_id in & cycle_edges {
273- used_edges. insert ( * edge_id) ;
354+ used_edges[ * edge_id] = true ;
274355 }
275356
276357 // Convert edge IDs to arcs
@@ -281,14 +362,8 @@ fn find_cycle_from_edge(
281362 return Some ( cycle_arcs) ;
282363 }
283364
284- // Create a temporary set of edges to avoid in this search (current path + permanently used)
285- let mut temp_used: HashSet < usize > = used_edges. clone ( ) ;
286- for & edge_id in & cycle_edges {
287- temp_used. insert ( edge_id) ;
288- }
289-
290- // Find next edge to follow
291- if let Some ( next_edge_id) = find_next_edge ( graph, current_vertex, Some ( current_edge_id) , & temp_used) {
365+ // Find next edge to follow (create temporary tracking on the fly)
366+ if let Some ( next_edge_id) = find_next_edge ( graph, current_vertex, Some ( current_edge_id) , used_edges, & cycle_edges) {
292367 let next_edge = & graph. edges [ next_edge_id] ;
293368
294369 // Determine which vertex to move to
@@ -316,7 +391,7 @@ pub fn find_non_intersecting_cycles(arcs: &[Arc]) -> Vec<Vec<Arc>> {
316391 let graph = build_graph ( arcs) ;
317392
318393 let mut cycles = Vec :: new ( ) ;
319- let mut used_edges = HashSet :: new ( ) ;
394+ let mut used_edges = vec ! [ false ; graph . edges . len ( ) ] ; // Use Vec<bool> instead of HashSet
320395
321396 // Try to find cycles starting from each edge
322397 for edge_id in 0 ..graph. edges . len ( ) {
0 commit comments