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
79impl<S> SolveResult<S> {
80 pub fn new() -> Self {
82 Self {
83 placements: Vec::new(),
84 boundaries_used: 0,
85 utilization: 0.0,
86 unplaced: Vec::new(),
87 computation_time_ms: 0,
88 generations: None,
89 iterations: None,
90 best_fitness: None,
91 fitness_history: None,
92 strategy: None,
93 cancelled: false,
94 target_reached: false,
95 strip_stats: Vec::new(),
96 total_piece_area: 0.0,
97 total_material_used: 0.0,
98 }
99 }
100
101 pub fn all_placed(&self) -> bool {
103 self.unplaced.is_empty()
104 }
105
106 pub fn placed_count(&self) -> usize {
108 self.placements.len()
109 }
110
111 pub fn unplaced_count(&self) -> usize {
113 self.unplaced.len()
114 }
115
116 pub fn is_successful(&self) -> bool {
118 !self.placements.is_empty()
119 }
120
121 pub fn completed_normally(&self) -> bool {
123 !self.cancelled
124 }
125
126 pub fn with_strategy(mut self, strategy: impl Into<String>) -> Self {
128 self.strategy = Some(strategy.into());
129 self
130 }
131
132 pub fn with_generations(mut self, generations: u32) -> Self {
134 self.generations = Some(generations);
135 self
136 }
137
138 pub fn with_best_fitness(mut self, fitness: f64) -> Self {
140 self.best_fitness = Some(fitness);
141 self
142 }
143
144 pub fn with_fitness_history(mut self, history: Vec<f64>) -> Self {
146 self.fitness_history = Some(history);
147 self
148 }
149
150 pub fn deduplicate_unplaced(&mut self) {
153 let mut seen = std::collections::HashSet::new();
154 self.unplaced.retain(|id| seen.insert(id.clone()));
155 }
156
157 pub fn placement_stats(&self) -> PlacementStats {
159 PlacementStats::from_placements(&self.placements)
160 }
161
162 pub fn utilization_percent(&self) -> String {
164 format!("{:.1}%", self.utilization * 100.0)
165 }
166
167 pub fn merge(&mut self, other: SolveResult<S>, boundary_offset: usize) {
169 for mut placement in other.placements {
171 placement.boundary_index += boundary_offset;
172 self.placements.push(placement);
173 }
174
175 self.boundaries_used = self
176 .boundaries_used
177 .max(other.boundaries_used + boundary_offset);
178 self.unplaced.extend(other.unplaced);
179 self.computation_time_ms += other.computation_time_ms;
180
181 for mut strip_stat in other.strip_stats {
183 strip_stat.strip_index += boundary_offset;
184 self.strip_stats.push(strip_stat);
185 }
186 self.total_piece_area += other.total_piece_area;
187 self.total_material_used += other.total_material_used;
188
189 if self.total_material_used > 0.0 {
191 self.utilization = self.total_piece_area / self.total_material_used;
192 }
193 }
194
195 pub fn with_strip_stats(mut self, stats: Vec<StripStats>) -> Self {
197 self.strip_stats = stats;
198 self
199 }
200
201 pub fn calculate_utilization(&mut self) {
204 if self.strip_stats.is_empty() {
205 return;
206 }
207
208 self.total_piece_area = self.strip_stats.iter().map(|s| s.piece_area).sum();
209 self.total_material_used = self
210 .strip_stats
211 .iter()
212 .map(|s| s.strip_width * s.used_length)
213 .sum();
214
215 if self.total_material_used > 0.0 {
216 self.utilization = self.total_piece_area / self.total_material_used;
217 }
218 }
219}
220
221impl<S> Default for SolveResult<S> {
222 fn default() -> Self {
223 Self::new()
224 }
225}
226
227#[derive(Debug, Clone)]
229#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
230pub struct SolveSummary {
231 pub total_requested: usize,
233 pub total_placed: usize,
235 pub utilization_percent: f64,
237 pub bins_used: usize,
239 pub time_ms: u64,
241 pub strategy: String,
243}
244
245impl<S> From<&SolveResult<S>> for SolveSummary {
246 fn from(result: &SolveResult<S>) -> Self {
247 Self {
248 total_requested: result.placements.len() + result.unplaced.len(),
249 total_placed: result.placements.len(),
250 utilization_percent: result.utilization * 100.0,
251 bins_used: result.boundaries_used,
252 time_ms: result.computation_time_ms,
253 strategy: result
254 .strategy
255 .clone()
256 .unwrap_or_else(|| "unknown".to_string()),
257 }
258 }
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264
265 #[test]
266 fn test_result_new() {
267 let result: SolveResult<f64> = SolveResult::new();
268 assert!(result.placements.is_empty());
269 assert_eq!(result.utilization, 0.0);
270 assert!(result.all_placed());
271 }
272
273 #[test]
274 fn test_result_with_placements() {
275 let mut result: SolveResult<f64> = SolveResult::new();
276 result
277 .placements
278 .push(Placement::new_2d("test".to_string(), 0, 0.0, 0.0, 0.0));
279 result.utilization = 0.85;
280
281 assert_eq!(result.placed_count(), 1);
282 assert!(result.is_successful());
283 assert_eq!(result.utilization_percent(), "85.0%");
284 }
285
286 #[test]
287 fn test_result_with_unplaced() {
288 let mut result: SolveResult<f64> = SolveResult::new();
289 result.unplaced.push("G1".to_string());
290 result.unplaced.push("G2".to_string());
291
292 assert!(!result.all_placed());
293 assert_eq!(result.unplaced_count(), 2);
294 }
295
296 #[test]
297 fn test_solve_summary() {
298 let mut result: SolveResult<f64> = SolveResult::new();
299 result
300 .placements
301 .push(Placement::new_2d("test".to_string(), 0, 0.0, 0.0, 0.0));
302 result.utilization = 0.75;
303 result.boundaries_used = 1;
304 result.computation_time_ms = 100;
305 result.strategy = Some("GA".to_string());
306
307 let summary = SolveSummary::from(&result);
308 assert_eq!(summary.total_placed, 1);
309 assert_eq!(summary.utilization_percent, 75.0);
310 assert_eq!(summary.strategy, "GA");
311 }
312
313 #[test]
314 fn test_deduplicate_unplaced() {
315 let mut result: SolveResult<f64> = SolveResult::new();
316 result.unplaced.push("G1".to_string());
318 result.unplaced.push("G1".to_string());
319 result.unplaced.push("G2".to_string());
320 result.unplaced.push("G1".to_string());
321 result.unplaced.push("G2".to_string());
322
323 assert_eq!(result.unplaced.len(), 5);
324
325 result.deduplicate_unplaced();
326
327 assert_eq!(result.unplaced.len(), 2);
328 assert!(result.unplaced.contains(&"G1".to_string()));
329 assert!(result.unplaced.contains(&"G2".to_string()));
330 }
331}