Skip to content

Commit be4950b

Browse files
committed
release:v0.2
1 parent 31f1d3b commit be4950b

9 files changed

Lines changed: 325 additions & 49 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Changelog
22

3-
## [0.1.0] - 2025-11-08
3+
## [0.2.0] - 2025-11-09
4+
- Error enum for returning the exact cause.
5+
- Added test/benchmark data generators.
46

7+
## [0.1.0] - 2025-11-08
58
- Initial release: Minkowski sum (No Fit Polygon) computation for CCW-oriented polygons via `NFP::nfp()`

Cargo.lock

Lines changed: 129 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "nfp"
3-
version = "0.1.0"
3+
version = "0.2.0"
44
edition = "2024"
55
description = "No Fit Polygon"
66
categories = ["science::geo", "data-structures", "game-development", "graphics"]
@@ -17,7 +17,11 @@ name = "nfp"
1717
crate-type = ["lib"]
1818

1919
[dependencies]
20-
togo = "0.6"
20+
#togo = "0.6"
21+
22+
[dev-dependencies]
23+
rand = "0.9"
24+
2125

2226
[profile.release]
2327
lto = true

README.md

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,27 @@
11
# NFP - No Fit Polygon
2+
[![Crates.io](https://img.shields.io/crates/v/nfp.svg?color=blue)](https://crates.io/crates/nfp)
3+
[![Documentation](https://docs.rs/nfp/badge.svg)](https://docs.rs/nfp)
4+
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
25

36
A Rust library that computes the No Fit Polygon (Minkowski sum) of two closed counter-clockwise oriented polygons.
47

58
## Overview
69

710
NFP implements the Minkowski sum algorithm for polygon nesting and packing problems. Given two CCW-oriented polygons, it computes the boundary region where one polygon can be placed relative to another without overlapping.
811

12+
## Installation
13+
14+
Add this to your `Cargo.toml`:
15+
16+
```toml
17+
[dependencies]
18+
nfp = "0.2"
19+
```
20+
921
## Usage
1022

1123
```rust
12-
use nfp::{point, NFP};
24+
use nfp::prelude::*;
1325

1426
// Define two CCW-oriented polygons using the point() shortcut
1527
let poly_a = vec![
@@ -53,11 +65,20 @@ Shortcut function to create a new point.
5365
- `add(other)` - Vector addition
5466
- `sub(other)` - Vector subtraction
5567

68+
### `NfpError`
69+
Error type for NFP operations.
70+
71+
**Variants:**
72+
- `EmptyPolygon` - One or both input polygons are empty
73+
- `InsufficientVertices` - One or both polygons have fewer than 3 vertices
74+
75+
Implements `Display` and `std::error::Error` traits. You can simply print errors with `{}` formatting or use the enum variants for pattern matching when you need programmatic error handling.
76+
5677
### `NFP`
5778
Main calculator for Minkowski sums.
5879

5980
**Methods:**
60-
- `nfp(poly_a, poly_b)` - Compute NFP of two polygons
81+
- `nfp(poly_a, poly_b) -> Result<Vec<Point>, NfpError>` - Compute NFP of two polygons
6182

6283
### `polygon` module
6384
Utility functions for polygon operations:
@@ -66,3 +87,7 @@ Utility functions for polygon operations:
6687
- `is_ccw(vertices)` - Check counter-clockwise orientation
6788
- `ensure_ccw(vertices)` - Ensure CCW orientation (mutates)
6889
- `translate(vertices, offset)` - Translate polygon by offset
90+
91+
## Related Projects
92+
93+
NFP is part of the open-sourced [Nest2D](https://nest2d.com) projects collection.

docs/PointNFP.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
11
The nfp-points.rs implements No Fit Polygon for vector of points that represent vertices in
22
closed CCW oriented polylines.
33

4+
### Algorithm Implementation Approach:
5+
The current implementation uses:
6+
7+
- Offset method: For each edge of polygon B, compute offset points from vertices of polygon A
8+
- Sorting by angle: Sort resulting vertices by angle from centroid to ensure proper CCW ordering
9+
- Deduplication: Remove near-duplicate points (within tolerance)
10+

src/datagen.rs

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
use crate::Point;
2+
use std::f64::consts::PI;
3+
use rand::SeedableRng;
4+
use rand::Rng;
5+
6+
pub struct DataGen;
7+
8+
impl DataGen {
9+
/// Generate a closed, non-intersecting random polygon with specified number of edges
10+
///
11+
/// Uses a convex hull approach: generates random points in polar coordinates
12+
/// and sorts them by angle to ensure no self-intersections.
13+
///
14+
/// # Arguments
15+
/// * `num_edges` - Number of edges for the polygon (minimum 3)
16+
/// * `seed` - Optional seed for reproducible random generation. If None, uses system entropy
17+
pub fn random_polygon(num_edges: usize, seed: u64) -> Vec<Point> {
18+
if num_edges < 3 {
19+
panic!("Polygon must have at least 3 edges");
20+
}
21+
22+
let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
23+
24+
// Generate random points in polar coordinates
25+
let mut points: Vec<Point> = (0..num_edges)
26+
.map(|_| {
27+
let angle = rng.random_range(0.0..(2.0 * PI));
28+
let radius = rng.random_range(0.5..2.0);
29+
Point::new(radius * angle.cos(), radius * angle.sin())
30+
})
31+
.collect();
32+
33+
// Sort by angle from centroid to ensure CCW ordering and no self-intersections
34+
let centroid = Self::centroid(&points);
35+
points.sort_by(|a, b| {
36+
let angle_a = (a.y - centroid.y).atan2(a.x - centroid.x);
37+
let angle_b = (b.y - centroid.y).atan2(b.x - centroid.x);
38+
angle_a.partial_cmp(&angle_b).unwrap_or(std::cmp::Ordering::Equal)
39+
});
40+
41+
points
42+
}
43+
44+
fn centroid(points: &[Point]) -> Point {
45+
if points.is_empty() {
46+
return Point::new(0.0, 0.0);
47+
}
48+
49+
let sum_x: f64 = points.iter().map(|p| p.x).sum();
50+
let sum_y: f64 = points.iter().map(|p| p.y).sum();
51+
let len = points.len() as f64;
52+
53+
Point::new(sum_x / len, sum_y / len)
54+
}
55+
}
56+
57+
#[cfg(test)]
58+
mod tests {
59+
use super::*;
60+
use crate::nfp_points::polygon;
61+
62+
#[test]
63+
fn test_random_polygon_generation() {
64+
let poly = DataGen::random_polygon(100, 42);
65+
66+
// Verify correct number of edges
67+
assert_eq!(poly.len(), 100);
68+
69+
// Verify polygon is closed and non-degenerate
70+
assert!(!poly.is_empty());
71+
72+
// Verify CCW orientation
73+
assert!(polygon::is_ccw(&poly));
74+
}
75+
76+
#[test]
77+
fn test_random_polygon_minimum_size() {
78+
let poly = DataGen::random_polygon(3, 42);
79+
assert_eq!(poly.len(), 3);
80+
assert!(polygon::is_ccw(&poly));
81+
}
82+
83+
#[test]
84+
fn test_random_polygon_with_seed() {
85+
// Same seed should produce identical polygons
86+
let poly1 = DataGen::random_polygon(50, 12345);
87+
let poly2 = DataGen::random_polygon(50, 12345);
88+
89+
assert_eq!(poly1.len(), poly2.len());
90+
for (p1, p2) in poly1.iter().zip(poly2.iter()) {
91+
assert!((p1.x - p2.x).abs() < 1e-10);
92+
assert!((p1.y - p2.y).abs() < 1e-10);
93+
}
94+
}
95+
96+
#[test]
97+
#[should_panic]
98+
fn test_random_polygon_invalid_size() {
99+
DataGen::random_polygon(2, 42);
100+
}
101+
}

src/lib.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,19 @@
3131
3232
pub mod nfp_points;
3333

34+
#[cfg(test)]
35+
pub mod datagen;
36+
37+
pub mod prelude;
38+
3439
// Re-export the main types for easier use
35-
pub use nfp_points::{point, Point, NFP};
40+
pub use nfp_points::point;
41+
pub use nfp_points::Point;
42+
pub use nfp_points::NFP;
43+
pub use nfp_points::NfpError;
44+
45+
#[cfg(test)]
46+
pub use datagen::DataGen;
3647

3748
#[cfg(test)]
3849
mod tests {

0 commit comments

Comments
 (0)