Skip to content

Commit 4b9fe51

Browse files
authored
Merge pull request #5 from radevgit/spatial_index
Spatial index implementation for prune
2 parents 8962ff9 + 251eefd commit 4b9fe51

File tree

7 files changed

+153
-21
lines changed

7 files changed

+153
-21
lines changed

benches/bench_offset_multiple1000.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,15 @@ fn main() {
3535
/*
3636
> cargo bench --bench bench_offset_multiple1000
3737
38-
Total time for 50 offset operations: 17.233370346s
39-
Average time per operation: 344.667406ms
40-
Operations per second: 2.9
38+
BRUTE-FORCE (USE_BRUTE_FORCE = true):
39+
Total time for 50 offset operations: 19.007039989s
40+
Average time per operation: 380.140799ms
41+
Operations per second: 2.6
4142
43+
SPATIAL INDEX (USE_BRUTE_FORCE = false):
44+
Total time for 50 offset operations: 6.249249758s
45+
Average time per operation: 124.984995ms
46+
Operations per second: 8.0
47+
48+
SPEEDUP: 3.04x faster with spatial index
4249
*/

benches/bench_offset_multiple200.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,16 @@ fn main() {
3535
/*
3636
> cargo bench --bench bench_offset_multiple200
3737
38-
Total time for 198 offset operations: 3.386764598s
39-
Average time per operation: 17.104871ms
40-
Operations per second: 58.5
38+
Base
39+
BRUTE-FORCE (USE_BRUTE_FORCE = true):
40+
Total time for 198 offset operations: 3.431741022s
41+
Average time per operation: 17.332025ms
42+
Operations per second: 57.7
4143
44+
SPATIAL INDEX (USE_BRUTE_FORCE = false):
45+
Total time for 198 offset operations: 1.314992506s
46+
Average time per operation: 6.641376ms
47+
Operations per second: 150.6
4248
49+
SPEEDUP: 2.61x faster with spatial index
4350
*/

benches/bench_offset_multiple500.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,15 @@ fn main() {
3535
/*
3636
> cargo bench --bench bench_offset_multiple500
3737
38-
Total time for 198 offset operations: 14.064167675s
39-
Average time per operation: 71.031149ms
40-
Operations per second: 14.1
38+
BRUTE-FORCE (USE_BRUTE_FORCE = true):
39+
Total time for 198 offset operations: 14.44784818s
40+
Average time per operation: 72.96893ms
41+
Operations per second: 13.7
4142
43+
SPATIAL INDEX (USE_BRUTE_FORCE = false):
44+
Total time for 198 offset operations: 6.224936148s
45+
Average time per operation: 31.439071ms
46+
Operations per second: 31.8
4247
48+
SPEEDUP: 2.32x faster with spatial index
4349
*/

examples/offset_arcline200.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,16 @@ fn main() {
66
let mut svg = SVG::new(800.0, 800.0, Some("/tmp/arcline200.svg"));
77
cfg.svg = Some(&mut svg);
88
cfg.svg_orig = true;
9-
cfg.svg_connect = true;
9+
cfg.svg_final = true;
1010

1111
let poly = arcline200();
12-
let _offset_polylines = offset_arcline_to_arcline(&poly, 5.0, &mut cfg);
12+
let offset_polylines = offset_arcline_to_arcline(&poly, 5.0, &mut cfg);
1313

1414
if let Some(svg) = cfg.svg.as_mut(){
1515
// Write svg to file
1616
svg.write_stroke_width(0.1);
1717
}
18+
19+
assert_eq!(offset_polylines.len(), 1, "Expected exactly 1 offset polyline");
20+
assert_eq!(offset_polylines[0].len(), 337);
1821
}

examples/offset_multi200.rs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,4 @@ fn main() {
3131
// Write svg to file
3232
svg.write_stroke_width(0.1);
3333
}
34-
35-
// assert!(
36-
// offset_external.len() == 228,
37-
// "Wrong number of offset arclines generated. Expected 228, got {}", offset_external.len()
38-
// );
3934
}

examples/offset_pline1.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,24 @@ fn main() {
77
let mut svg = SVG::new(300.0, 300.0, Some("/tmp/pline1.svg"));
88
cfg.svg = Some(&mut svg);
99
cfg.svg_orig = true;
10-
cfg.svg_raw = true;
11-
cfg.svg_connect = true;
10+
cfg.svg_final = true;
1211

1312
let poly_orig = pline_01()[0].clone();
1413
// Translate to fit in the SVG viewport
1514
let poly = polyline_translate(&poly_orig, point(100.0, -50.0));
1615

17-
let _offset_polylines = offset_polyline_to_polyline(&poly, 10.0, &mut cfg);
16+
let offset_polylines = offset_polyline_to_polyline(&poly, 10.0, &mut cfg);
1817
// Internal offsetting
1918
// let poly = polyline_reverse(&poly);
2019
// let _offset_polylines = offset_polyline_to_polyline(&poly, 15.5600615, &mut cfg);
2120
//let _offset_polylines = offset_polyline_to_polyline(&poly, 16.0, &mut cfg);
2221

22+
2323
if let Some(svg) = cfg.svg.as_mut(){
2424
// Write svg to file
2525
svg.write_stroke_width(0.1);
2626
}
27+
28+
assert_eq!(offset_polylines.len(), 1, "Expected exactly 1 offset polyline");
29+
assert_eq!(offset_polylines[0].len(), 27);
2730
}

src/offset_prune_invalid.rs

Lines changed: 113 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,98 @@
11
#![allow(dead_code)]
22

33
use togo::prelude::*;
4+
use togo::spatial::HilbertRTree;
45

56
use crate::offsetraw::OffsetRaw;
67

78
// Prune arcs that are close to any of the arcs in the polyline.
89
const PRUNE_EPSILON: f64 = 1e-8;
10+
11+
// Set to true to use brute-force algorithm (for testing/comparison)
12+
const USE_BRUTE_FORCE: bool = false;
13+
914
pub fn offset_prune_invalid(
1015
polyraws: &Vec<Vec<OffsetRaw>>,
1116
offsets: &mut Vec<Arc>,
1217
off: f64,
18+
) -> Vec<Arc> {
19+
if USE_BRUTE_FORCE {
20+
offset_prune_invalid_brute_force(polyraws, offsets, off)
21+
} else {
22+
offset_prune_invalid_spatial(polyraws, offsets, off)
23+
}
24+
}
25+
26+
fn offset_prune_invalid_spatial(
27+
polyraws: &Vec<Vec<OffsetRaw>>,
28+
offsets: &mut Vec<Arc>,
29+
off: f64,
30+
) -> Vec<Arc> {
31+
let mut valid = Vec::new();
32+
let polyarcs: Vec<Arc> = polyraws
33+
.iter()
34+
.flatten()
35+
.map(|offset_raw| offset_raw.arc.clone())
36+
.filter(|arc| arc.is_valid(PRUNE_EPSILON))
37+
.collect();
38+
39+
// Build spatial index containing both polyarcs and offsets
40+
let polyarc_count = polyarcs.len();
41+
let mut spatial_index = HilbertRTree::with_capacity(polyarc_count + offsets.len());
42+
43+
// Add polyarcs to index with offset + epsilon expansion (done once)
44+
let search_radius = off + PRUNE_EPSILON;
45+
for arc in polyarcs.iter() {
46+
let (min_x, max_x, min_y, max_y) = arc_bounds_expanded(arc, search_radius);
47+
spatial_index.add(min_x, max_x, min_y, max_y);
48+
}
49+
50+
// Add offsets to index
51+
for arc in offsets.iter() {
52+
let (min_x, max_x, min_y, max_y) = arc_bounds(arc);
53+
spatial_index.add(min_x, max_x, min_y, max_y);
54+
}
55+
56+
spatial_index.build();
57+
58+
while offsets.len() > 0 {
59+
let offset = offsets.pop().unwrap();
60+
valid.push(offset.clone());
61+
62+
// Query nearby arcs using spatial index
63+
let (offset_min_x, offset_max_x, offset_min_y, offset_max_y) =
64+
arc_bounds(&offset);
65+
let mut nearby_indices = Vec::new();
66+
spatial_index.query_intersecting(
67+
offset_min_x,
68+
offset_max_x,
69+
offset_min_y,
70+
offset_max_y,
71+
&mut nearby_indices,
72+
);
73+
74+
// Check only nearby polyarcs for actual distance
75+
for idx in nearby_indices {
76+
if idx < polyarc_count {
77+
let p = &polyarcs[idx];
78+
if p.id == offset.id {
79+
continue; // skip self offsets
80+
}
81+
let dist = distance_element_element(p, &offset);
82+
if dist < off - PRUNE_EPSILON {
83+
valid.pop();
84+
break;
85+
}
86+
}
87+
}
88+
}
89+
valid
90+
}
91+
92+
fn offset_prune_invalid_brute_force(
93+
polyraws: &Vec<Vec<OffsetRaw>>,
94+
offsets: &mut Vec<Arc>,
95+
off: f64,
1396
) -> Vec<Arc> {
1497
let mut valid = Vec::new();
1598
let polyarcs: Vec<Arc> = polyraws
@@ -18,14 +101,13 @@ pub fn offset_prune_invalid(
18101
.map(|offset_raw| offset_raw.arc.clone())
19102
.filter(|arc| arc.is_valid(PRUNE_EPSILON))
20103
.collect();
21-
let _zzz = polyarcs.len();
22104

23105
while offsets.len() > 0 {
24106
let offset = offsets.pop().unwrap();
25107
valid.push(offset.clone());
26108
for p in polyarcs.iter() {
27109
if p.id == offset.id {
28-
continue; // skip self ofsets
110+
continue; // skip self offsets
29111
}
30112
let dist = distance_element_element(&p, &offset);
31113
if dist < off - PRUNE_EPSILON {
@@ -37,6 +119,35 @@ pub fn offset_prune_invalid(
37119
valid
38120
}
39121

122+
/// Get bounding box of an arc
123+
fn arc_bounds(arc: &Arc) -> (f64, f64, f64, f64) {
124+
if arc.is_seg() {
125+
// For line segments, just return min/max of endpoints
126+
let min_x = arc.a.x.min(arc.b.x);
127+
let max_x = arc.a.x.max(arc.b.x);
128+
let min_y = arc.a.y.min(arc.b.y);
129+
let max_y = arc.a.y.max(arc.b.y);
130+
(min_x, max_x, min_y, max_y)
131+
} else {
132+
// For arcs, return the bounding box of the circle (center ± radius)
133+
let cx = arc.c.x;
134+
let cy = arc.c.y;
135+
let r = arc.r;
136+
(cx - r, cx + r, cy - r, cy + r)
137+
}
138+
}
139+
140+
/// Get expanded bounding box of an arc (for spatial queries)
141+
fn arc_bounds_expanded(arc: &Arc, expansion: f64) -> (f64, f64, f64, f64) {
142+
let (min_x, max_x, min_y, max_y) = arc_bounds(arc);
143+
(
144+
min_x - expansion,
145+
max_x + expansion,
146+
min_y - expansion,
147+
max_y + expansion,
148+
)
149+
}
150+
40151
fn distance_element_element(seg0: &Arc, seg1: &Arc) -> f64 {
41152
let mut dist = std::f64::INFINITY;
42153
if seg0.is_seg() && seg1.is_seg() {

0 commit comments

Comments
 (0)