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 ga_nesting;
67pub mod gdrr_nesting;
68pub mod geometry;
69#[cfg(feature = "milp")]
70pub mod milp_solver;
71pub mod nester;
72pub mod nfp;
73#[cfg(feature = "milp")]
74pub mod nfp_cm_solver;
75pub mod nfp_sliding;
76pub mod placement_utils;
77pub(crate) mod polygon_ops;
78pub mod sa_nesting;
79pub mod spatial_index;
80
81/// Computes valid placement bounds and clamps a position to keep geometry within boundary.
82///
83/// Returns `Some((clamped_x, clamped_y))` if the geometry can fit in the boundary,
84/// `None` if the geometry is too large to fit.
85///
86/// # Arguments
87/// * `x`, `y` - The proposed placement position for the geometry's origin
88/// * `geom_aabb` - The AABB `(min, max)` of the geometry at the given rotation
89/// * `boundary_aabb` - The AABB `(min, max)` of the boundary
90pub fn clamp_placement_to_boundary(
91 x: f64,
92 y: f64,
93 geom_aabb: ([f64; 2], [f64; 2]),
94 boundary_aabb: ([f64; 2], [f64; 2]),
95) -> Option<(f64, f64)> {
96 let (g_min, g_max) = geom_aabb;
97 let (b_min, b_max) = boundary_aabb;
98
99 // Calculate valid position bounds
100 // For geometry to stay inside boundary:
101 // - x + g_min[0] >= b_min[0] => x >= b_min[0] - g_min[0]
102 // - x + g_max[0] <= b_max[0] => x <= b_max[0] - g_max[0]
103 let min_valid_x = b_min[0] - g_min[0];
104 let max_valid_x = b_max[0] - g_max[0];
105 let min_valid_y = b_min[1] - g_min[1];
106 let max_valid_y = b_max[1] - g_max[1];
107
108 // Check if geometry can fit
109 if max_valid_x < min_valid_x || max_valid_y < min_valid_y {
110 // Geometry is too large to fit in boundary
111 return None;
112 }
113
114 let clamped_x = x.clamp(min_valid_x, max_valid_x);
115 let clamped_y = y.clamp(min_valid_y, max_valid_y);
116
117 Some((clamped_x, clamped_y))
118}
119
120/// Computes valid placement bounds with margin and clamps a position to keep geometry within boundary.
121///
122/// Returns `Some((clamped_x, clamped_y))` if the geometry can fit in the boundary (with margin),
123/// `None` if the geometry is too large to fit.
124///
125/// # Arguments
126/// * `x`, `y` - The proposed placement position for the geometry's origin
127/// * `geom_aabb` - The AABB `(min, max)` of the geometry at the given rotation
128/// * `boundary_aabb` - The AABB `(min, max)` of the boundary
129/// * `margin` - The margin to apply inside the boundary
130pub fn clamp_placement_to_boundary_with_margin(
131 x: f64,
132 y: f64,
133 geom_aabb: ([f64; 2], [f64; 2]),
134 boundary_aabb: ([f64; 2], [f64; 2]),
135 margin: f64,
136) -> Option<(f64, f64)> {
137 let (g_min, g_max) = geom_aabb;
138 let (b_min, b_max) = boundary_aabb;
139
140 // Calculate valid position bounds (with margin applied to effective boundary)
141 // Effective boundary: [b_min + margin, b_max - margin]
142 // For geometry to stay inside effective boundary:
143 // - x + g_min[0] >= b_min[0] + margin => x >= b_min[0] + margin - g_min[0]
144 // - x + g_max[0] <= b_max[0] - margin => x <= b_max[0] - margin - g_max[0]
145 let min_valid_x = b_min[0] + margin - g_min[0];
146 let max_valid_x = b_max[0] - margin - g_max[0];
147 let min_valid_y = b_min[1] + margin - g_min[1];
148 let max_valid_y = b_max[1] - margin - g_max[1];
149
150 // Check if geometry can fit
151 if max_valid_x < min_valid_x || max_valid_y < min_valid_y {
152 // Geometry is too large to fit in boundary with the given margin
153 return None;
154 }
155
156 let clamped_x = x.clamp(min_valid_x, max_valid_x);
157 let clamped_y = y.clamp(min_valid_y, max_valid_y);
158
159 Some((clamped_x, clamped_y))
160}
161
162/// Checks if a placement is within the boundary.
163///
164/// Returns `true` if the geometry at the given placement is fully within the boundary,
165/// `false` otherwise.
166///
167/// # Arguments
168/// * `placement` - The placement to validate (contains position and rotation)
169/// * `geometry` - The geometry being placed
170/// * `boundary` - The boundary to check against
171/// * `tolerance` - Small tolerance for floating point comparison (e.g., 1e-6)
172pub fn is_placement_within_bounds(
173 placement: &Placement<f64>,
174 geometry: &Geometry2D,
175 boundary: &Boundary2D,
176 tolerance: f64,
177) -> bool {
178 use u_nesting_core::geometry::Boundary;
179 use u_nesting_core::Boundary2DExt;
180
181 // Extract position (Vec<f64> with [x, y] for 2D)
182 let x = placement.position.first().copied().unwrap_or(0.0);
183 let y = placement.position.get(1).copied().unwrap_or(0.0);
184
185 // Extract rotation (Vec<f64> with [θ] for 2D)
186 let rotation = placement.rotation.first().copied().unwrap_or(0.0);
187
188 // Get geometry AABB at the placement rotation
189 let (g_min, g_max) = geometry.aabb_at_rotation(rotation);
190
191 // Get boundary AABB
192 let (b_min, b_max) = boundary.aabb();
193
194 // Calculate the actual bounds of the placed geometry
195 let placed_min_x = x + g_min[0];
196 let placed_max_x = x + g_max[0];
197 let placed_min_y = y + g_min[1];
198 let placed_max_y = y + g_max[1];
199
200 // AABB containment — a necessary condition, and *exact* for a hole-free
201 // axis-aligned rectangular boundary.
202 let aabb_inside = placed_min_x >= b_min[0] - tolerance
203 && placed_max_x <= b_max[0] + tolerance
204 && placed_min_y >= b_min[1] - tolerance
205 && placed_max_y <= b_max[1] + tolerance;
206
207 // Fast reject and the exact-boundary shortcut.
208 //
209 // For a plain rectangle (width & height set, no holes) the AABB check is the
210 // exact answer. Infinite strips must also stay on the AABB path: their
211 // exterior carries `f64::MAX` vertices, so ray-cast polygon containment is
212 // meaningless. Everything else — an arbitrary boundary polygon, or a
213 // rectangle carrying holes — needs true polygon-in-polygon containment,
214 // because the AABB of a triangular/concave/holed boundary spans empty
215 // regions where a piece would sit fully inside the box yet outside the shape.
216 let plain_rectangle =
217 boundary.width().is_some() && boundary.height().is_some() && boundary.holes().is_empty();
218 if boundary.is_infinite() || plain_rectangle {
219 return aabb_inside;
220 }
221 if !aabb_inside {
222 return false;
223 }
224
225 let piece = geometry.transformed_exterior(x, y, rotation);
226 boundary.contains_polygon(&piece)
227}
228
229/// Validates all placements in a SolveResult and removes any that are outside the boundary.
230///
231/// Returns a new SolveResult with only valid placements, updated utilization,
232/// and invalid placements added to the unplaced list.
233///
234/// # Arguments
235/// * `result` - The solve result to validate
236/// * `geometries` - The geometries that were being placed
237/// * `boundary` - The boundary to check against
238pub fn validate_and_filter_placements(
239 mut result: SolveResult<f64>,
240 geometries: &[Geometry2D],
241 boundary: &Boundary2D,
242) -> SolveResult<f64> {
243 use std::collections::HashMap;
244 use u_nesting_core::geometry::{Boundary, Geometry};
245
246 const TOLERANCE: f64 = 1e-6;
247
248 // Build a map from geometry ID to geometry for quick lookup
249 let geom_map: HashMap<_, _> = geometries.iter().map(|g| (g.id().clone(), g)).collect();
250
251 let (b_min, b_max) = boundary.aabb();
252 log::debug!(
253 "Validating placements against boundary: ({:.2}, {:.2}) to ({:.2}, {:.2})",
254 b_min[0],
255 b_min[1],
256 b_max[0],
257 b_max[1]
258 );
259
260 let mut valid_placements = Vec::new();
261 let mut total_valid_area = 0.0;
262 let mut filtered_count = 0;
263 // Used-footprint accumulator: the AABB union of the placed pieces, which is
264 // boundary-padding independent (unlike `utilization`, which divides by the
265 // full boundary and shrinks arbitrarily as boundary height grows).
266 let mut used_min_x = f64::INFINITY;
267 let mut used_min_y = f64::INFINITY;
268 let mut used_max_x = f64::NEG_INFINITY;
269 let mut used_max_y = f64::NEG_INFINITY;
270
271 for placement in result.placements {
272 if let Some(geom) = geom_map.get(&placement.geometry_id) {
273 let px = placement.position.first().copied().unwrap_or(0.0);
274 let py = placement.position.get(1).copied().unwrap_or(0.0);
275 let rot = placement.rotation.first().copied().unwrap_or(0.0);
276
277 if is_placement_within_bounds(&placement, geom, boundary, TOLERANCE) {
278 total_valid_area += geom.measure();
279 let (pg_min, pg_max) = geom.aabb_at_rotation(rot);
280 used_min_x = used_min_x.min(px + pg_min[0]);
281 used_min_y = used_min_y.min(py + pg_min[1]);
282 used_max_x = used_max_x.max(px + pg_max[0]);
283 used_max_y = used_max_y.max(py + pg_max[1]);
284 valid_placements.push(placement);
285 } else {
286 // Calculate actual bounds for debugging
287 let (g_min, g_max) = geom.aabb_at_rotation(rot);
288 let placed_min_x = px + g_min[0];
289 let placed_max_x = px + g_max[0];
290 let placed_min_y = py + g_min[1];
291 let placed_max_y = py + g_max[1];
292
293 log::warn!(
294 "FILTERED: {} at ({:.2}, {:.2}) rot={:.2}° - bounds ({:.2}, {:.2}) to ({:.2}, {:.2}) outside boundary",
295 placement.geometry_id,
296 px, py,
297 rot.to_degrees(),
298 placed_min_x, placed_min_y, placed_max_x, placed_max_y
299 );
300 filtered_count += 1;
301 // Add to unplaced list
302 result.unplaced.push(placement.geometry_id.clone());
303 }
304 } else {
305 // Geometry not found - shouldn't happen but handle gracefully
306 log::warn!("Geometry {} not found in lookup map", placement.geometry_id);
307 result.unplaced.push(placement.geometry_id.clone());
308 }
309 }
310
311 if filtered_count > 0 {
312 log::warn!(
313 "Validation filtered out {} placements as out-of-bounds",
314 filtered_count
315 );
316 }
317
318 // Update result with valid placements only
319 result.placements = valid_placements;
320 result.utilization = total_valid_area / boundary.measure();
321
322 // Record the used-footprint metrics (padding-independent). `total_piece_area`
323 // is otherwise only populated on the multi-strip path; set it here so the
324 // single-sheet `used_utilization = piece_area / used_bbox_area` is meaningful.
325 result.total_piece_area = total_valid_area;
326 if result.placements.is_empty() {
327 result.used_bounding_box = [0.0, 0.0];
328 } else {
329 result.used_bounding_box = [used_max_x - used_min_x, used_max_y - used_min_y];
330 }
331
332 result
333}
334
335// Re-exports
336pub use boundary::Boundary2D;
337pub use geometry::Geometry2D;
338pub use nester::Nester2D;
339pub use nfp::{NfpConfig, NfpMethod};
340pub use spatial_index::{SpatialEntry2D, SpatialIndex2D};
341pub use u_nesting_core::{
342 Boundary, Boundary2DExt, Config, Error, Geometry, Geometry2DExt, Placement, Result,
343 RotationConstraint, SolveResult, Solver, Strategy, Transform2D, AABB2D,
344};