use crate::geometry::GeometryId;
use crate::placement::{Placement, PlacementStats};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct StripStats {
pub strip_index: usize,
pub used_length: f64,
pub piece_area: f64,
pub piece_count: usize,
pub strip_width: f64,
pub strip_height: f64,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct SolveResult<S> {
pub placements: Vec<Placement<S>>,
pub boundaries_used: usize,
pub utilization: f64,
pub unplaced: Vec<GeometryId>,
pub computation_time_ms: u64,
pub generations: Option<u32>,
pub iterations: Option<u64>,
pub best_fitness: Option<f64>,
pub fitness_history: Option<Vec<f64>>,
pub strategy: Option<String>,
pub cancelled: bool,
pub target_reached: bool,
pub strip_stats: Vec<StripStats>,
pub total_piece_area: f64,
pub total_material_used: f64,
}
impl<S> SolveResult<S> {
pub fn new() -> Self {
Self {
placements: Vec::new(),
boundaries_used: 0,
utilization: 0.0,
unplaced: Vec::new(),
computation_time_ms: 0,
generations: None,
iterations: None,
best_fitness: None,
fitness_history: None,
strategy: None,
cancelled: false,
target_reached: false,
strip_stats: Vec::new(),
total_piece_area: 0.0,
total_material_used: 0.0,
}
}
pub fn all_placed(&self) -> bool {
self.unplaced.is_empty()
}
pub fn placed_count(&self) -> usize {
self.placements.len()
}
pub fn unplaced_count(&self) -> usize {
self.unplaced.len()
}
pub fn is_successful(&self) -> bool {
!self.placements.is_empty()
}
pub fn completed_normally(&self) -> bool {
!self.cancelled
}
pub fn with_strategy(mut self, strategy: impl Into<String>) -> Self {
self.strategy = Some(strategy.into());
self
}
pub fn with_generations(mut self, generations: u32) -> Self {
self.generations = Some(generations);
self
}
pub fn with_best_fitness(mut self, fitness: f64) -> Self {
self.best_fitness = Some(fitness);
self
}
pub fn with_fitness_history(mut self, history: Vec<f64>) -> Self {
self.fitness_history = Some(history);
self
}
pub fn deduplicate_unplaced(&mut self) {
let mut seen = std::collections::HashSet::new();
self.unplaced.retain(|id| seen.insert(id.clone()));
}
pub fn placement_stats(&self) -> PlacementStats {
PlacementStats::from_placements(&self.placements)
}
pub fn utilization_percent(&self) -> String {
format!("{:.1}%", self.utilization * 100.0)
}
pub fn merge(&mut self, other: SolveResult<S>, boundary_offset: usize) {
for mut placement in other.placements {
placement.boundary_index += boundary_offset;
self.placements.push(placement);
}
self.boundaries_used = self
.boundaries_used
.max(other.boundaries_used + boundary_offset);
self.unplaced.extend(other.unplaced);
self.computation_time_ms += other.computation_time_ms;
for mut strip_stat in other.strip_stats {
strip_stat.strip_index += boundary_offset;
self.strip_stats.push(strip_stat);
}
self.total_piece_area += other.total_piece_area;
self.total_material_used += other.total_material_used;
if self.total_material_used > 0.0 {
self.utilization = self.total_piece_area / self.total_material_used;
}
}
pub fn with_strip_stats(mut self, stats: Vec<StripStats>) -> Self {
self.strip_stats = stats;
self
}
pub fn calculate_utilization(&mut self) {
if self.strip_stats.is_empty() {
return;
}
self.total_piece_area = self.strip_stats.iter().map(|s| s.piece_area).sum();
self.total_material_used = self
.strip_stats
.iter()
.map(|s| s.strip_width * s.used_length)
.sum();
if self.total_material_used > 0.0 {
self.utilization = self.total_piece_area / self.total_material_used;
}
}
}
impl<S> Default for SolveResult<S> {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct SolveSummary {
pub total_requested: usize,
pub total_placed: usize,
pub utilization_percent: f64,
pub bins_used: usize,
pub time_ms: u64,
pub strategy: String,
}
impl<S> From<&SolveResult<S>> for SolveSummary {
fn from(result: &SolveResult<S>) -> Self {
Self {
total_requested: result.placements.len() + result.unplaced.len(),
total_placed: result.placements.len(),
utilization_percent: result.utilization * 100.0,
bins_used: result.boundaries_used,
time_ms: result.computation_time_ms,
strategy: result
.strategy
.clone()
.unwrap_or_else(|| "unknown".to_string()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_result_new() {
let result: SolveResult<f64> = SolveResult::new();
assert!(result.placements.is_empty());
assert_eq!(result.utilization, 0.0);
assert!(result.all_placed());
}
#[test]
fn test_result_with_placements() {
let mut result: SolveResult<f64> = SolveResult::new();
result
.placements
.push(Placement::new_2d("test".to_string(), 0, 0.0, 0.0, 0.0));
result.utilization = 0.85;
assert_eq!(result.placed_count(), 1);
assert!(result.is_successful());
assert_eq!(result.utilization_percent(), "85.0%");
}
#[test]
fn test_result_with_unplaced() {
let mut result: SolveResult<f64> = SolveResult::new();
result.unplaced.push("G1".to_string());
result.unplaced.push("G2".to_string());
assert!(!result.all_placed());
assert_eq!(result.unplaced_count(), 2);
}
#[test]
fn test_solve_summary() {
let mut result: SolveResult<f64> = SolveResult::new();
result
.placements
.push(Placement::new_2d("test".to_string(), 0, 0.0, 0.0, 0.0));
result.utilization = 0.75;
result.boundaries_used = 1;
result.computation_time_ms = 100;
result.strategy = Some("GA".to_string());
let summary = SolveSummary::from(&result);
assert_eq!(summary.total_placed, 1);
assert_eq!(summary.utilization_percent, 75.0);
assert_eq!(summary.strategy, "GA");
}
#[test]
fn test_deduplicate_unplaced() {
let mut result: SolveResult<f64> = SolveResult::new();
result.unplaced.push("G1".to_string());
result.unplaced.push("G1".to_string());
result.unplaced.push("G2".to_string());
result.unplaced.push("G1".to_string());
result.unplaced.push("G2".to_string());
assert_eq!(result.unplaced.len(), 5);
result.deduplicate_unplaced();
assert_eq!(result.unplaced.len(), 2);
assert!(result.unplaced.contains(&"G1".to_string()));
assert!(result.unplaced.contains(&"G2".to_string()));
}
}