1use crate::geometry::GeometryId;
4use crate::placement::{Placement, PlacementStats};
5
6#[cfg(feature = "serde")]
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Default)]
11#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
12pub struct StripStats {
13 pub strip_index: usize,
15 pub used_length: f64,
17 pub piece_area: f64,
19 pub piece_count: usize,
21 pub strip_width: f64,
23 pub strip_height: f64,
25}
26
27#[derive(Debug, Clone)]
29#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
30pub struct SolveResult<S> {
31 pub placements: Vec<Placement<S>>,
33
34 pub boundaries_used: usize,
36
37 pub utilization: f64,
40
41 pub unplaced: Vec<GeometryId>,
43
44 pub computation_time_ms: u64,
46
47 pub generations: Option<u32>,
49
50 pub iterations: Option<u64>,
52
53 pub best_fitness: Option<f64>,
55
56 pub fitness_history: Option<Vec<f64>>,
58
59 pub strategy: Option<String>,
61
62 pub cancelled: bool,
64
65 pub target_reached: bool,
67
68 pub strip_stats: Vec<StripStats>,
71
72 pub total_piece_area: f64,
74
75 pub total_material_used: f64,
77
78 pub total_requested: usize,
84}
85
86impl<S> SolveResult<S> {
87 pub fn new() -> Self {
89 Self {
90 placements: Vec::new(),
91 boundaries_used: 0,
92 utilization: 0.0,
93 unplaced: Vec::new(),
94 computation_time_ms: 0,
95 generations: None,
96 iterations: None,
97 best_fitness: None,
98 fitness_history: None,
99 strategy: None,
100 cancelled: false,
101 target_reached: false,
102 strip_stats: Vec::new(),
103 total_piece_area: 0.0,
104 total_material_used: 0.0,
105 total_requested: 0,
106 }
107 }
108
109 pub fn all_placed(&self) -> bool {
111 self.unplaced.is_empty()
112 }
113
114 pub fn placed_count(&self) -> usize {
116 self.placements.len()
117 }
118
119 pub fn unplaced_count(&self) -> usize {
121 self.unplaced.len()
122 }
123
124 pub fn is_successful(&self) -> bool {
126 !self.placements.is_empty()
127 }
128
129 pub fn completed_normally(&self) -> bool {
131 !self.cancelled
132 }
133
134 pub fn with_strategy(mut self, strategy: impl Into<String>) -> Self {
136 self.strategy = Some(strategy.into());
137 self
138 }
139
140 pub fn with_generations(mut self, generations: u32) -> Self {
142 self.generations = Some(generations);
143 self
144 }
145
146 pub fn with_best_fitness(mut self, fitness: f64) -> Self {
148 self.best_fitness = Some(fitness);
149 self
150 }
151
152 pub fn with_fitness_history(mut self, history: Vec<f64>) -> Self {
154 self.fitness_history = Some(history);
155 self
156 }
157
158 pub fn deduplicate_unplaced(&mut self) {
161 let mut seen = std::collections::HashSet::new();
162 self.unplaced.retain(|id| seen.insert(id.clone()));
163 }
164
165 pub fn placement_stats(&self) -> PlacementStats {
167 PlacementStats::from_placements(&self.placements)
168 }
169
170 pub fn utilization_percent(&self) -> String {
172 format!("{:.1}%", self.utilization * 100.0)
173 }
174
175 pub fn merge(&mut self, other: SolveResult<S>, boundary_offset: usize) {
177 for mut placement in other.placements {
179 placement.boundary_index += boundary_offset;
180 self.placements.push(placement);
181 }
182
183 self.boundaries_used = self
184 .boundaries_used
185 .max(other.boundaries_used + boundary_offset);
186 self.unplaced.extend(other.unplaced);
187 self.computation_time_ms += other.computation_time_ms;
188
189 for mut strip_stat in other.strip_stats {
191 strip_stat.strip_index += boundary_offset;
192 self.strip_stats.push(strip_stat);
193 }
194 self.total_piece_area += other.total_piece_area;
195 self.total_material_used += other.total_material_used;
196 self.total_requested += other.total_requested;
197
198 if self.total_material_used > 0.0 {
200 self.utilization = self.total_piece_area / self.total_material_used;
201 }
202 }
203
204 pub fn with_strip_stats(mut self, stats: Vec<StripStats>) -> Self {
206 self.strip_stats = stats;
207 self
208 }
209
210 pub fn calculate_utilization(&mut self) {
213 if self.strip_stats.is_empty() {
214 return;
215 }
216
217 self.total_piece_area = self.strip_stats.iter().map(|s| s.piece_area).sum();
218 self.total_material_used = self
219 .strip_stats
220 .iter()
221 .map(|s| s.strip_width * s.used_length)
222 .sum();
223
224 if self.total_material_used > 0.0 {
225 self.utilization = self.total_piece_area / self.total_material_used;
226 }
227 }
228}
229
230impl<S> Default for SolveResult<S> {
231 fn default() -> Self {
232 Self::new()
233 }
234}
235
236impl SolveResult<f64> {
237 pub fn to_boundary_local(&mut self, extent: f64) {
251 for placement in &mut self.placements {
252 if let Some(x) = placement.position.first_mut() {
253 *x -= placement.boundary_index as f64 * extent;
254 }
255 }
256 }
257}
258
259#[derive(Debug, Clone)]
261#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
262pub struct SolveSummary {
263 pub total_requested: usize,
265 pub total_placed: usize,
267 pub utilization_percent: f64,
269 pub bins_used: usize,
271 pub time_ms: u64,
273 pub strategy: String,
275}
276
277impl<S> From<&SolveResult<S>> for SolveSummary {
278 fn from(result: &SolveResult<S>) -> Self {
279 Self {
280 total_requested: result.total_requested,
285 total_placed: result.placements.len(),
286 utilization_percent: result.utilization * 100.0,
287 bins_used: result.boundaries_used,
288 time_ms: result.computation_time_ms,
289 strategy: result
290 .strategy
291 .clone()
292 .unwrap_or_else(|| "unknown".to_string()),
293 }
294 }
295}
296
297#[cfg(test)]
298mod tests {
299 use super::*;
300
301 #[test]
302 fn test_result_new() {
303 let result: SolveResult<f64> = SolveResult::new();
304 assert!(result.placements.is_empty());
305 assert_eq!(result.utilization, 0.0);
306 assert!(result.all_placed());
307 }
308
309 #[test]
310 fn test_result_with_placements() {
311 let mut result: SolveResult<f64> = SolveResult::new();
312 result
313 .placements
314 .push(Placement::new_2d("test".to_string(), 0, 0.0, 0.0, 0.0));
315 result.utilization = 0.85;
316
317 assert_eq!(result.placed_count(), 1);
318 assert!(result.is_successful());
319 assert_eq!(result.utilization_percent(), "85.0%");
320 }
321
322 #[test]
323 fn test_result_with_unplaced() {
324 let mut result: SolveResult<f64> = SolveResult::new();
325 result.unplaced.push("G1".to_string());
326 result.unplaced.push("G2".to_string());
327
328 assert!(!result.all_placed());
329 assert_eq!(result.unplaced_count(), 2);
330 }
331
332 #[test]
333 fn test_solve_summary() {
334 let mut result: SolveResult<f64> = SolveResult::new();
335 result
336 .placements
337 .push(Placement::new_2d("test".to_string(), 0, 0.0, 0.0, 0.0));
338 result.utilization = 0.75;
339 result.boundaries_used = 1;
340 result.computation_time_ms = 100;
341 result.strategy = Some("GA".to_string());
342
343 let summary = SolveSummary::from(&result);
344 assert_eq!(summary.total_placed, 1);
345 assert_eq!(summary.utilization_percent, 75.0);
346 assert_eq!(summary.strategy, "GA");
347 }
348
349 #[test]
350 fn test_solve_summary_total_requested_is_instance_level() {
351 let mut result: SolveResult<f64> = SolveResult::new();
357 result
358 .placements
359 .push(Placement::new_2d("A".to_string(), 0, 0.0, 0.0, 0.0));
360 result.unplaced.push("A".to_string()); result.total_requested = 5;
362
363 let summary = SolveSummary::from(&result);
364 assert_eq!(summary.total_requested, 5);
365 assert_eq!(summary.total_placed, 1);
366 assert_eq!(summary.total_requested - summary.total_placed, 4);
368 }
369
370 #[test]
371 fn test_merge_sums_total_requested() {
372 let mut a: SolveResult<f64> = SolveResult::new();
373 a.total_requested = 3;
374 let mut b: SolveResult<f64> = SolveResult::new();
375 b.total_requested = 4;
376 a.merge(b, 0);
377 assert_eq!(a.total_requested, 7);
378 }
379
380 #[test]
381 fn test_deduplicate_unplaced() {
382 let mut result: SolveResult<f64> = SolveResult::new();
383 result.unplaced.push("G1".to_string());
385 result.unplaced.push("G1".to_string());
386 result.unplaced.push("G2".to_string());
387 result.unplaced.push("G1".to_string());
388 result.unplaced.push("G2".to_string());
389
390 assert_eq!(result.unplaced.len(), 5);
391
392 result.deduplicate_unplaced();
393
394 assert_eq!(result.unplaced.len(), 2);
395 assert!(result.unplaced.contains(&"G1".to_string()));
396 assert!(result.unplaced.contains(&"G2".to_string()));
397 }
398
399 #[test]
400 fn test_to_boundary_local_subtracts_per_sheet_offset() {
401 let extent = 100.0;
404 let mut result: SolveResult<f64> = SolveResult::new();
405 for sheet in 0..3 {
406 result.placements.push(
407 Placement::new_2d(
408 "p".to_string(),
409 sheet,
410 10.0 + sheet as f64 * extent,
411 5.0,
412 0.0,
413 )
414 .with_boundary(sheet),
415 );
416 }
417
418 result.to_boundary_local(extent);
419
420 for p in &result.placements {
421 assert_eq!(p.position[0], 10.0, "x must be sheet-local after transform");
422 assert_eq!(p.position[1], 5.0, "y is untouched");
423 }
424 }
425}