From 251eefda117452136cd81daf5a27a155e5bd7007 Mon Sep 17 00:00:00 2001 From: radevgit Date: Sun, 26 Oct 2025 17:01:58 +0200 Subject: [PATCH] Spatial index implementation for prune --- benches/bench_offset_multiple1000.rs | 13 ++- benches/bench_offset_multiple200.rs | 13 ++- benches/bench_offset_multiple500.rs | 12 ++- examples/offset_arcline200.rs | 7 +- examples/offset_multi200.rs | 5 -- examples/offset_pline1.rs | 9 ++- src/offset_prune_invalid.rs | 115 ++++++++++++++++++++++++++- 7 files changed, 153 insertions(+), 21 deletions(-) diff --git a/benches/bench_offset_multiple1000.rs b/benches/bench_offset_multiple1000.rs index 3826693..06c2580 100644 --- a/benches/bench_offset_multiple1000.rs +++ b/benches/bench_offset_multiple1000.rs @@ -35,8 +35,15 @@ fn main() { /* > cargo bench --bench bench_offset_multiple1000 -Total time for 50 offset operations: 17.233370346s -Average time per operation: 344.667406ms -Operations per second: 2.9 +BRUTE-FORCE (USE_BRUTE_FORCE = true): +Total time for 50 offset operations: 19.007039989s +Average time per operation: 380.140799ms +Operations per second: 2.6 +SPATIAL INDEX (USE_BRUTE_FORCE = false): +Total time for 50 offset operations: 6.249249758s +Average time per operation: 124.984995ms +Operations per second: 8.0 + +SPEEDUP: 3.04x faster with spatial index */ diff --git a/benches/bench_offset_multiple200.rs b/benches/bench_offset_multiple200.rs index f73addc..e074d00 100644 --- a/benches/bench_offset_multiple200.rs +++ b/benches/bench_offset_multiple200.rs @@ -35,9 +35,16 @@ fn main() { /* > cargo bench --bench bench_offset_multiple200 -Total time for 198 offset operations: 3.386764598s -Average time per operation: 17.104871ms -Operations per second: 58.5 +Base +BRUTE-FORCE (USE_BRUTE_FORCE = true): +Total time for 198 offset operations: 3.431741022s +Average time per operation: 17.332025ms +Operations per second: 57.7 +SPATIAL INDEX (USE_BRUTE_FORCE = false): +Total time for 198 offset operations: 1.314992506s +Average time per operation: 6.641376ms +Operations per second: 150.6 +SPEEDUP: 2.61x faster with spatial index */ diff --git a/benches/bench_offset_multiple500.rs b/benches/bench_offset_multiple500.rs index 7e7a297..e43871a 100644 --- a/benches/bench_offset_multiple500.rs +++ b/benches/bench_offset_multiple500.rs @@ -35,9 +35,15 @@ fn main() { /* > cargo bench --bench bench_offset_multiple500 -Total time for 198 offset operations: 14.064167675s -Average time per operation: 71.031149ms -Operations per second: 14.1 +BRUTE-FORCE (USE_BRUTE_FORCE = true): +Total time for 198 offset operations: 14.44784818s +Average time per operation: 72.96893ms +Operations per second: 13.7 +SPATIAL INDEX (USE_BRUTE_FORCE = false): +Total time for 198 offset operations: 6.224936148s +Average time per operation: 31.439071ms +Operations per second: 31.8 +SPEEDUP: 2.32x faster with spatial index */ diff --git a/examples/offset_arcline200.rs b/examples/offset_arcline200.rs index 23bf31c..1bec03b 100644 --- a/examples/offset_arcline200.rs +++ b/examples/offset_arcline200.rs @@ -6,13 +6,16 @@ fn main() { let mut svg = SVG::new(800.0, 800.0, Some("/tmp/arcline200.svg")); cfg.svg = Some(&mut svg); cfg.svg_orig = true; - cfg.svg_connect = true; + cfg.svg_final = true; let poly = arcline200(); - let _offset_polylines = offset_arcline_to_arcline(&poly, 5.0, &mut cfg); + let offset_polylines = offset_arcline_to_arcline(&poly, 5.0, &mut cfg); if let Some(svg) = cfg.svg.as_mut(){ // Write svg to file svg.write_stroke_width(0.1); } + + assert_eq!(offset_polylines.len(), 1, "Expected exactly 1 offset polyline"); + assert_eq!(offset_polylines[0].len(), 337); } diff --git a/examples/offset_multi200.rs b/examples/offset_multi200.rs index e3a3df9..1fdae3b 100644 --- a/examples/offset_multi200.rs +++ b/examples/offset_multi200.rs @@ -31,9 +31,4 @@ fn main() { // Write svg to file svg.write_stroke_width(0.1); } - - // assert!( - // offset_external.len() == 228, - // "Wrong number of offset arclines generated. Expected 228, got {}", offset_external.len() - // ); } diff --git a/examples/offset_pline1.rs b/examples/offset_pline1.rs index 5d06d4f..5322933 100644 --- a/examples/offset_pline1.rs +++ b/examples/offset_pline1.rs @@ -7,21 +7,24 @@ fn main() { let mut svg = SVG::new(300.0, 300.0, Some("/tmp/pline1.svg")); cfg.svg = Some(&mut svg); cfg.svg_orig = true; - cfg.svg_raw = true; - cfg.svg_connect = true; + cfg.svg_final = true; let poly_orig = pline_01()[0].clone(); // Translate to fit in the SVG viewport let poly = polyline_translate(&poly_orig, point(100.0, -50.0)); - let _offset_polylines = offset_polyline_to_polyline(&poly, 10.0, &mut cfg); + let offset_polylines = offset_polyline_to_polyline(&poly, 10.0, &mut cfg); // Internal offsetting // let poly = polyline_reverse(&poly); // let _offset_polylines = offset_polyline_to_polyline(&poly, 15.5600615, &mut cfg); //let _offset_polylines = offset_polyline_to_polyline(&poly, 16.0, &mut cfg); + if let Some(svg) = cfg.svg.as_mut(){ // Write svg to file svg.write_stroke_width(0.1); } + + assert_eq!(offset_polylines.len(), 1, "Expected exactly 1 offset polyline"); + assert_eq!(offset_polylines[0].len(), 27); } diff --git a/src/offset_prune_invalid.rs b/src/offset_prune_invalid.rs index 5bc627f..d5eb729 100644 --- a/src/offset_prune_invalid.rs +++ b/src/offset_prune_invalid.rs @@ -1,15 +1,98 @@ #![allow(dead_code)] use togo::prelude::*; +use togo::spatial::HilbertRTree; use crate::offsetraw::OffsetRaw; // Prune arcs that are close to any of the arcs in the polyline. const PRUNE_EPSILON: f64 = 1e-8; + +// Set to true to use brute-force algorithm (for testing/comparison) +const USE_BRUTE_FORCE: bool = false; + pub fn offset_prune_invalid( polyraws: &Vec>, offsets: &mut Vec, off: f64, +) -> Vec { + if USE_BRUTE_FORCE { + offset_prune_invalid_brute_force(polyraws, offsets, off) + } else { + offset_prune_invalid_spatial(polyraws, offsets, off) + } +} + +fn offset_prune_invalid_spatial( + polyraws: &Vec>, + offsets: &mut Vec, + off: f64, +) -> Vec { + let mut valid = Vec::new(); + let polyarcs: Vec = polyraws + .iter() + .flatten() + .map(|offset_raw| offset_raw.arc.clone()) + .filter(|arc| arc.is_valid(PRUNE_EPSILON)) + .collect(); + + // Build spatial index containing both polyarcs and offsets + let polyarc_count = polyarcs.len(); + let mut spatial_index = HilbertRTree::with_capacity(polyarc_count + offsets.len()); + + // Add polyarcs to index with offset + epsilon expansion (done once) + let search_radius = off + PRUNE_EPSILON; + for arc in polyarcs.iter() { + let (min_x, max_x, min_y, max_y) = arc_bounds_expanded(arc, search_radius); + spatial_index.add(min_x, max_x, min_y, max_y); + } + + // Add offsets to index + for arc in offsets.iter() { + let (min_x, max_x, min_y, max_y) = arc_bounds(arc); + spatial_index.add(min_x, max_x, min_y, max_y); + } + + spatial_index.build(); + + while offsets.len() > 0 { + let offset = offsets.pop().unwrap(); + valid.push(offset.clone()); + + // Query nearby arcs using spatial index + let (offset_min_x, offset_max_x, offset_min_y, offset_max_y) = + arc_bounds(&offset); + let mut nearby_indices = Vec::new(); + spatial_index.query_intersecting( + offset_min_x, + offset_max_x, + offset_min_y, + offset_max_y, + &mut nearby_indices, + ); + + // Check only nearby polyarcs for actual distance + for idx in nearby_indices { + if idx < polyarc_count { + let p = &polyarcs[idx]; + if p.id == offset.id { + continue; // skip self offsets + } + let dist = distance_element_element(p, &offset); + if dist < off - PRUNE_EPSILON { + valid.pop(); + break; + } + } + } + } + valid +} + +fn offset_prune_invalid_brute_force( + polyraws: &Vec>, + offsets: &mut Vec, + off: f64, ) -> Vec { let mut valid = Vec::new(); let polyarcs: Vec = polyraws @@ -18,14 +101,13 @@ pub fn offset_prune_invalid( .map(|offset_raw| offset_raw.arc.clone()) .filter(|arc| arc.is_valid(PRUNE_EPSILON)) .collect(); - let _zzz = polyarcs.len(); while offsets.len() > 0 { let offset = offsets.pop().unwrap(); valid.push(offset.clone()); for p in polyarcs.iter() { if p.id == offset.id { - continue; // skip self ofsets + continue; // skip self offsets } let dist = distance_element_element(&p, &offset); if dist < off - PRUNE_EPSILON { @@ -37,6 +119,35 @@ pub fn offset_prune_invalid( valid } +/// Get bounding box of an arc +fn arc_bounds(arc: &Arc) -> (f64, f64, f64, f64) { + if arc.is_seg() { + // For line segments, just return min/max of endpoints + let min_x = arc.a.x.min(arc.b.x); + let max_x = arc.a.x.max(arc.b.x); + let min_y = arc.a.y.min(arc.b.y); + let max_y = arc.a.y.max(arc.b.y); + (min_x, max_x, min_y, max_y) + } else { + // For arcs, return the bounding box of the circle (center ± radius) + let cx = arc.c.x; + let cy = arc.c.y; + let r = arc.r; + (cx - r, cx + r, cy - r, cy + r) + } +} + +/// Get expanded bounding box of an arc (for spatial queries) +fn arc_bounds_expanded(arc: &Arc, expansion: f64) -> (f64, f64, f64, f64) { + let (min_x, max_x, min_y, max_y) = arc_bounds(arc); + ( + min_x - expansion, + max_x + expansion, + min_y - expansion, + max_y + expansion, + ) +} + fn distance_element_element(seg0: &Arc, seg1: &Arc) -> f64 { let mut dist = std::f64::INFINITY; if seg0.is_seg() && seg1.is_seg() {