Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions benches/bench_offset_multiple1000.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
13 changes: 10 additions & 3 deletions benches/bench_offset_multiple200.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
12 changes: 9 additions & 3 deletions benches/bench_offset_multiple500.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
7 changes: 5 additions & 2 deletions examples/offset_arcline200.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
5 changes: 0 additions & 5 deletions examples/offset_multi200.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
// );
}
9 changes: 6 additions & 3 deletions examples/offset_pline1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
115 changes: 113 additions & 2 deletions src/offset_prune_invalid.rs
Original file line number Diff line number Diff line change
@@ -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<Vec<OffsetRaw>>,
offsets: &mut Vec<Arc>,
off: f64,
) -> Vec<Arc> {
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<Vec<OffsetRaw>>,
offsets: &mut Vec<Arc>,
off: f64,
) -> Vec<Arc> {
let mut valid = Vec::new();
let polyarcs: Vec<Arc> = 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<Vec<OffsetRaw>>,
offsets: &mut Vec<Arc>,
off: f64,
) -> Vec<Arc> {
let mut valid = Vec::new();
let polyarcs: Vec<Arc> = polyraws
Expand All @@ -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 {
Expand All @@ -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() {
Expand Down