Skip to main content

u_nesting_d2/
lib.rs

1//! # U-Nesting 2D
2//!
3//! 2D nesting algorithms for the U-Nesting spatial optimization engine.
4//!
5//! This crate provides polygon-based 2D nesting with NFP (No-Fit Polygon) computation
6//! and various placement algorithms.
7//!
8//! ## Features
9//!
10//! - Polygon geometry with holes support
11//! - Multiple placement strategies (BLF, NFP-guided, GA, BRKGA, SA)
12//! - Convex hull and convexity detection
13//! - Configurable rotation and mirroring constraints
14//! - NFP-based collision-free placement
15//! - Spatial indexing for fast queries
16//!
17//! ## Quick Start
18//!
19//! ```rust
20//! use u_nesting_d2::{Geometry2D, Boundary2D, Nester2D, Config, Strategy, Solver};
21//!
22//! // Create geometries
23//! let rect = Geometry2D::rectangle("rect1", 100.0, 50.0)
24//!     .with_quantity(5)
25//!     .with_rotations_deg(vec![0.0, 90.0]);
26//!
27//! // Create boundary
28//! let boundary = Boundary2D::rectangle(500.0, 300.0);
29//!
30//! // Configure and solve
31//! let config = Config::new()
32//!     .with_strategy(Strategy::NfpGuided)
33//!     .with_spacing(2.0);
34//!
35//! let nester = Nester2D::new(config);
36//! let result = nester.solve(&[rect], &boundary).unwrap();
37//!
38//! println!("Placed {} items, utilization: {:.1}%",
39//!     result.placements.len(),
40//!     result.utilization * 100.0);
41//! ```
42//!
43//! ## Geometry Creation
44//!
45//! ```rust
46//! use u_nesting_d2::Geometry2D;
47//!
48//! // Rectangle
49//! let rect = Geometry2D::rectangle("r1", 100.0, 50.0);
50//!
51//! // Circle (approximated)
52//! let circle = Geometry2D::circle("c1", 25.0, 32);
53//!
54//! // L-shape
55//! let l_shape = Geometry2D::l_shape("l1", 100.0, 80.0, 30.0, 30.0);
56//!
57//! // Custom polygon
58//! let custom = Geometry2D::new("custom")
59//!     .with_polygon(vec![(0.0, 0.0), (100.0, 0.0), (50.0, 80.0)])
60//!     .with_quantity(3);
61//! ```
62
63pub mod alns_nesting;
64pub mod boundary;
65pub mod brkga_nesting;
66pub mod check;
67pub mod ga_nesting;
68pub mod gdrr_nesting;
69pub mod geometry;
70pub mod nester;
71pub mod nfp;
72#[cfg(feature = "milp")]
73pub mod nfp_cm_solver;
74pub mod nfp_sliding;
75pub mod placement_utils;
76pub(crate) mod polygon_ops;
77pub mod sa_nesting;
78pub mod spatial_index;
79
80/// Computes valid placement bounds and clamps a position to keep geometry within boundary.
81///
82/// Returns `Some((clamped_x, clamped_y))` if the geometry can fit in the boundary,
83/// `None` if the geometry is too large to fit.
84///
85/// # Arguments
86/// * `x`, `y` - The proposed placement position for the geometry's origin
87/// * `geom_aabb` - The AABB `(min, max)` of the geometry at the given rotation
88/// * `boundary_aabb` - The AABB `(min, max)` of the boundary
89pub fn clamp_placement_to_boundary(
90    x: f64,
91    y: f64,
92    geom_aabb: ([f64; 2], [f64; 2]),
93    boundary_aabb: ([f64; 2], [f64; 2]),
94) -> Option<(f64, f64)> {
95    let (g_min, g_max) = geom_aabb;
96    let (b_min, b_max) = boundary_aabb;
97
98    // Calculate valid position bounds
99    // For geometry to stay inside boundary:
100    // - x + g_min[0] >= b_min[0]  => x >= b_min[0] - g_min[0]
101    // - x + g_max[0] <= b_max[0]  => x <= b_max[0] - g_max[0]
102    let min_valid_x = b_min[0] - g_min[0];
103    let max_valid_x = b_max[0] - g_max[0];
104    let min_valid_y = b_min[1] - g_min[1];
105    let max_valid_y = b_max[1] - g_max[1];
106
107    // Check if geometry can fit
108    if max_valid_x < min_valid_x || max_valid_y < min_valid_y {
109        // Geometry is too large to fit in boundary
110        return None;
111    }
112
113    let clamped_x = x.clamp(min_valid_x, max_valid_x);
114    let clamped_y = y.clamp(min_valid_y, max_valid_y);
115
116    Some((clamped_x, clamped_y))
117}
118
119/// Computes valid placement bounds with margin and clamps a position to keep geometry within boundary.
120///
121/// Returns `Some((clamped_x, clamped_y))` if the geometry can fit in the boundary (with margin),
122/// `None` if the geometry is too large to fit.
123///
124/// # Arguments
125/// * `x`, `y` - The proposed placement position for the geometry's origin
126/// * `geom_aabb` - The AABB `(min, max)` of the geometry at the given rotation
127/// * `boundary_aabb` - The AABB `(min, max)` of the boundary
128/// * `margin` - The margin to apply inside the boundary
129pub fn clamp_placement_to_boundary_with_margin(
130    x: f64,
131    y: f64,
132    geom_aabb: ([f64; 2], [f64; 2]),
133    boundary_aabb: ([f64; 2], [f64; 2]),
134    margin: f64,
135) -> Option<(f64, f64)> {
136    let (g_min, g_max) = geom_aabb;
137    let (b_min, b_max) = boundary_aabb;
138
139    // Calculate valid position bounds (with margin applied to effective boundary)
140    // Effective boundary: [b_min + margin, b_max - margin]
141    // For geometry to stay inside effective boundary:
142    // - x + g_min[0] >= b_min[0] + margin  => x >= b_min[0] + margin - g_min[0]
143    // - x + g_max[0] <= b_max[0] - margin  => x <= b_max[0] - margin - g_max[0]
144    let min_valid_x = b_min[0] + margin - g_min[0];
145    let max_valid_x = b_max[0] - margin - g_max[0];
146    let min_valid_y = b_min[1] + margin - g_min[1];
147    let max_valid_y = b_max[1] - margin - g_max[1];
148
149    // Check if geometry can fit
150    if max_valid_x < min_valid_x || max_valid_y < min_valid_y {
151        // Geometry is too large to fit in boundary with the given margin
152        return None;
153    }
154
155    let clamped_x = x.clamp(min_valid_x, max_valid_x);
156    let clamped_y = y.clamp(min_valid_y, max_valid_y);
157
158    Some((clamped_x, clamped_y))
159}
160
161/// Checks if a placement lies within the boundary, at least `margin` from its
162/// edges (exterior and holes).
163///
164/// Returns `true` if the geometry at the given placement — rotated, and
165/// mirrored when the placement says so — is fully within the boundary and no
166/// closer than `margin` to any boundary edge, `false` otherwise.
167///
168/// # Arguments
169/// * `placement` - The placement to validate (position, rotation, mirroring)
170/// * `geometry` - The geometry being placed
171/// * `boundary` - The boundary to check against
172/// * `margin` - Minimum distance from the piece to every boundary edge
173/// * `tolerance` - Small tolerance for floating point comparison (e.g., 1e-6)
174pub fn is_placement_within_bounds(
175    placement: &Placement<f64>,
176    geometry: &Geometry2D,
177    boundary: &Boundary2D,
178    margin: f64,
179    tolerance: f64,
180) -> bool {
181    use u_nesting_core::geometry::Boundary;
182    use u_nesting_core::Boundary2DExt;
183
184    let x = placement.position.first().copied().unwrap_or(0.0);
185    let y = placement.position.get(1).copied().unwrap_or(0.0);
186    let rotation = placement.rotation.first().copied().unwrap_or(0.0);
187
188    let (g_min, g_max) = geometry.aabb_at_rotation_mirrored(rotation, placement.mirrored);
189    let (b_min, b_max) = boundary.aabb();
190
191    // AABB containment within the margin — a necessary condition, and *exact*
192    // for a hole-free axis-aligned rectangular boundary.
193    let aabb_inside = x + g_min[0] >= b_min[0] + margin - tolerance
194        && x + g_max[0] <= b_max[0] - margin + tolerance
195        && y + g_min[1] >= b_min[1] + margin - tolerance
196        && y + g_max[1] <= b_max[1] - margin + tolerance;
197
198    // For a plain rectangle (width & height set, no holes) the AABB check is the
199    // exact answer. Infinite strips must also stay on the AABB path: their
200    // exterior carries `f64::MAX` vertices, so ray-cast polygon containment is
201    // meaningless. Everything else — an arbitrary boundary polygon, or a
202    // rectangle carrying holes — needs true polygon-in-polygon containment,
203    // because the AABB of a triangular/concave/holed boundary spans empty
204    // regions where a piece would sit fully inside the box yet outside the shape.
205    let plain_rectangle =
206        boundary.width().is_some() && boundary.height().is_some() && boundary.holes().is_empty();
207    if boundary.is_infinite() || plain_rectangle {
208        return aabb_inside;
209    }
210    if !aabb_inside {
211        return false;
212    }
213
214    let base = if placement.mirrored {
215        Geometry2D::new(geometry.id().clone())
216            .with_polygon(polygon_ops::mirror_polygon(geometry.exterior()))
217    } else {
218        geometry.clone()
219    };
220    let piece = base.transformed_exterior(x, y, rotation);
221    if !boundary.contains_polygon(&piece) {
222        return false;
223    }
224    margin <= 0.0
225        || std::iter::once(boundary.exterior())
226            .chain(boundary.holes().iter().map(Vec::as_slice))
227            .all(|ring| polygon_ops::ring_distance(&piece, ring) >= margin - tolerance)
228}
229
230/// Validates all placements in a SolveResult and removes any that are outside the boundary.
231///
232/// Returns a new SolveResult with only valid placements, updated utilization,
233/// and invalid placements added to the unplaced list.
234///
235/// # Arguments
236/// * `result` - The solve result to validate
237/// * `geometries` - The geometries that were being placed
238/// * `boundary` - The boundary to check against
239/// * `margin` - Minimum distance a placement must keep from the boundary edges
240pub fn validate_and_filter_placements(
241    mut result: SolveResult<f64>,
242    geometries: &[Geometry2D],
243    boundary: &Boundary2D,
244    margin: f64,
245) -> SolveResult<f64> {
246    use std::collections::HashMap;
247    use u_nesting_core::geometry::{Boundary, Geometry};
248
249    const TOLERANCE: f64 = 1e-6;
250
251    // Build a map from geometry ID to geometry for quick lookup
252    let geom_map: HashMap<_, _> = geometries.iter().map(|g| (g.id().clone(), g)).collect();
253
254    let (b_min, b_max) = boundary.aabb();
255    log::debug!(
256        "Validating placements against boundary: ({:.2}, {:.2}) to ({:.2}, {:.2})",
257        b_min[0],
258        b_min[1],
259        b_max[0],
260        b_max[1]
261    );
262
263    let mut valid_placements = Vec::new();
264    let mut total_valid_area = 0.0;
265    let mut filtered_count = 0;
266    // Used-footprint accumulator: the AABB union of the placed pieces, which is
267    // boundary-padding independent (unlike `utilization`, which divides by the
268    // full boundary and shrinks arbitrarily as boundary height grows).
269    let mut used_min_x = f64::INFINITY;
270    let mut used_min_y = f64::INFINITY;
271    let mut used_max_x = f64::NEG_INFINITY;
272    let mut used_max_y = f64::NEG_INFINITY;
273
274    for placement in result.placements {
275        if let Some(geom) = geom_map.get(&placement.geometry_id) {
276            let px = placement.position.first().copied().unwrap_or(0.0);
277            let py = placement.position.get(1).copied().unwrap_or(0.0);
278            let rot = placement.rotation.first().copied().unwrap_or(0.0);
279
280            if is_placement_within_bounds(&placement, geom, boundary, margin, TOLERANCE) {
281                total_valid_area += geom.measure();
282                let (pg_min, pg_max) = geom.aabb_at_rotation_mirrored(rot, placement.mirrored);
283                used_min_x = used_min_x.min(px + pg_min[0]);
284                used_min_y = used_min_y.min(py + pg_min[1]);
285                used_max_x = used_max_x.max(px + pg_max[0]);
286                used_max_y = used_max_y.max(py + pg_max[1]);
287                valid_placements.push(placement);
288            } else {
289                // Calculate actual bounds for debugging
290                let (g_min, g_max) = geom.aabb_at_rotation(rot);
291                let placed_min_x = px + g_min[0];
292                let placed_max_x = px + g_max[0];
293                let placed_min_y = py + g_min[1];
294                let placed_max_y = py + g_max[1];
295
296                log::warn!(
297                    "FILTERED: {} at ({:.2}, {:.2}) rot={:.2}° - bounds ({:.2}, {:.2}) to ({:.2}, {:.2}) outside boundary",
298                    placement.geometry_id,
299                    px, py,
300                    rot.to_degrees(),
301                    placed_min_x, placed_min_y, placed_max_x, placed_max_y
302                );
303                filtered_count += 1;
304                // Add to unplaced list
305                result.unplaced.push(placement.geometry_id.clone());
306            }
307        } else {
308            // Geometry not found - shouldn't happen but handle gracefully
309            log::warn!("Geometry {} not found in lookup map", placement.geometry_id);
310            result.unplaced.push(placement.geometry_id.clone());
311        }
312    }
313
314    if filtered_count > 0 {
315        log::warn!(
316            "Validation filtered out {} placements as out-of-bounds",
317            filtered_count
318        );
319    }
320
321    // Update result with valid placements only
322    result.placements = valid_placements;
323    result.utilization = total_valid_area / boundary.measure();
324
325    // Record the used-footprint metrics (padding-independent). `total_piece_area`
326    // is otherwise only populated on the multi-strip path; set it here so the
327    // single-sheet `used_utilization = piece_area / used_bbox_area` is meaningful.
328    result.total_piece_area = total_valid_area;
329    if result.placements.is_empty() {
330        result.used_bounding_box = [0.0, 0.0];
331    } else {
332        result.used_bounding_box = [used_max_x - used_min_x, used_max_y - used_min_y];
333    }
334
335    result
336}
337
338// Re-exports
339pub use boundary::Boundary2D;
340pub use geometry::Geometry2D;
341pub use nester::Nester2D;
342pub use nfp::{NfpConfig, NfpMethod};
343pub use spatial_index::{SpatialEntry2D, SpatialIndex2D};
344pub use u_nesting_core::{
345    Boundary, Boundary2DExt, Config, Error, Geometry, Geometry2DExt, Placement, Result,
346    RotationConstraint, SolveResult, Solver, StopReason, Strategy, Transform2D, AABB2D,
347};