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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## [0.2.2] - 2025-11-09
- performance atan2
- parformance unstable sort (total 64%)

## [0.2.0] - 2025-11-09
- Error enum for returning the exact cause.
- Added test/benchmark data generators.
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "nfp"
version = "0.2.1"
version = "0.2.2"
edition = "2024"
description = "No Fit Polygon"
categories = ["science::geo", "data-structures", "game-development", "graphics"]
Expand Down
16 changes: 13 additions & 3 deletions benches/nfp_bench100.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ fn main() {
println!("\nResults:");
println!("--------");
println!("Total time: {:?}", elapsed);
println!("Time per run: {:.3} ms", elapsed.as_secs_f64() * 1000.0 / ITERATIONS as f64);
println!("Time per run: {:.3} μs", elapsed.as_secs_f64() * 1_000_000.0 / ITERATIONS as f64);
println!("Runs per sec: {:.0}", ITERATIONS as f64 / elapsed.as_secs_f64());
}

Expand Down Expand Up @@ -82,8 +82,7 @@ fn compute_centroid(points: &[Point]) -> Point {
}

/*
cargo bench --bench nfp_benchmark
samply record cargo run --release --example perf_build
cargo bench --bench nfp_bench100

Iterations: 1000

Expand All @@ -99,5 +98,16 @@ Total time: 4.152361987s
Time per run: 4.152 ms
Runs per sec: 241
_______________________________________________
Opt 1 ditch atan2

Total time: 1.725334029s
Time per run: 1725.334 μs
Runs per sec: 580
_______________________________________________
Opt2 unstable sorting

Total time: 1.44724319s
Time per run: 1447.243 μs
Runs per sec: 691
_______________________________________________
*/
16 changes: 13 additions & 3 deletions benches/nfp_bench200.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ fn main() {
println!("\nResults:");
println!("--------");
println!("Total time: {:?}", elapsed);
println!("Time per run: {:.3} ms", elapsed.as_secs_f64() * 1000.0 / ITERATIONS as f64);
println!("Time per run: {:.3} μs", elapsed.as_secs_f64() * 1_000_000.0 / ITERATIONS as f64);
println!("Runs per sec: {:.0}", ITERATIONS as f64 / elapsed.as_secs_f64());
}

Expand Down Expand Up @@ -82,8 +82,7 @@ fn compute_centroid(points: &[Point]) -> Point {
}

/*
cargo bench --bench nfp_benchmark

cargo bench --bench nfp_bench200

Iterations: 300

Expand All @@ -99,5 +98,16 @@ Total time: 5.604518697s
Time per run: 18.682 ms
Runs per sec: 54
_______________________________________________
Opt 1 ditch atan2

Total time: 2.456991368s
Time per run: 8189.971 μs
Runs per sec: 122
_______________________________________________
Opt2 unstable sorting

Total time: 2.027956028s
Time per run: 6759.853 μs
Runs per sec: 148
_______________________________________________
*/
16 changes: 13 additions & 3 deletions benches/nfp_bench500.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ fn main() {
println!("\nResults:");
println!("--------");
println!("Total time: {:?}", elapsed);
println!("Time per run: {:.3} ms", elapsed.as_secs_f64() * 1000.0 / ITERATIONS as f64);
println!("Time per run: {:.3} μs", elapsed.as_secs_f64() * 1_000_000.0 / ITERATIONS as f64);
println!("Runs per sec: {:.0}", ITERATIONS as f64 / elapsed.as_secs_f64());
}

Expand Down Expand Up @@ -82,8 +82,7 @@ fn compute_centroid(points: &[Point]) -> Point {
}

/*
cargo bench --bench nfp_benchmark

cargo bench --bench nfp_bench500

Iterations: 100

Expand All @@ -99,5 +98,16 @@ Total time: 13.578851406s
Time per run: 135.789 ms
Runs per sec: 7
_______________________________________________
Opt 1 ditch atan2

Total time: 6.140549308s
Time per run: 61405.493 μs
Runs per sec: 16
_______________________________________________
Opt2 unstable sorting

Total time: 4.848619896s
Time per run: 48486.199 μs
Runs per sec: 21
_______________________________________________
*/
43 changes: 36 additions & 7 deletions src/nfp_points.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ impl NFP {

// Remove near-duplicate points (within tolerance)
let tolerance = 1e-10;
nfp_vertices.sort_by(|a, b| {
nfp_vertices.sort_unstable_by(|a, b| {
a.x.partial_cmp(&b.x)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| a.y.partial_cmp(&b.y).unwrap_or(std::cmp::Ordering::Equal))
Expand All @@ -166,13 +166,9 @@ impl NFP {
(a.x - b.x).abs() < tolerance && (a.y - b.y).abs() < tolerance
});

// Sort vertices in CCW order by angle from centroid
// Sort vertices in CCW order by angle from centroid (avoid atan2)
let centroid = compute_centroid(&nfp_vertices);
nfp_vertices.sort_by(|a, b| {
let angle_a = (a.y - centroid.y).atan2(a.x - centroid.x);
let angle_b = (b.y - centroid.y).atan2(b.x - centroid.x);
angle_a.partial_cmp(&angle_b).unwrap_or(std::cmp::Ordering::Equal)
});
nfp_vertices.sort_unstable_by(|a, b| angle_cmp(a, b, &centroid));

Ok(nfp_vertices)
}
Expand All @@ -190,6 +186,39 @@ fn compute_centroid(points: &[Point]) -> Point {
Point::new(sum_x / len, sum_y / len)
}

/// Tolerance for comparing collinear points in angle sorting
const ANGLE_CMP_EPSILON: f64 = 1e-10;

// Compare points by angle around a given `centroid` without using `atan2`.
// Uses half-plane test and perp (2D cross product) to determine CCW ordering.
fn angle_cmp(a: &Point, b: &Point, centroid: &Point) -> std::cmp::Ordering {
use std::cmp::Ordering;

let ax = a.x - centroid.x;
let ay = a.y - centroid.y;
let bx = b.x - centroid.x;
let by = b.y - centroid.y;

// Place points into two half-planes: upper (y>0 or y==0 && x>=0) and lower.
let a_up = (ay > 0.0) || (ay == 0.0 && ax >= 0.0);
let b_up = (by > 0.0) || (by == 0.0 && bx >= 0.0);
if a_up != b_up {
// a_up true should come before b_up false
return a_up.cmp(&b_up).reverse();
}

// Same half-plane: use perp (2D cross product) to determine order
let perp = ax * by - ay * bx;
if perp.abs() > ANGLE_CMP_EPSILON {
return if perp > 0.0 { Ordering::Less } else { Ordering::Greater };
}

// Collinear: sort by distance from centroid (closer first)
let da = ax * ax + ay * ay;
let db = bx * bx + by * by;
da.partial_cmp(&db).unwrap_or(Ordering::Equal)
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down