use std::collections::{HashMap, HashSet};
use crate::common_edge::CommonEdgeResult;
use crate::config::CuttingConfig;
use crate::contour::CutContour;
use crate::cost::{closest_point_on_polygon, point_distance};
use crate::hierarchy::CuttingDag;
use crate::pierce::{select_pierce, PierceSelection};
#[derive(Debug, Clone)]
pub struct SequenceResult {
pub order: Vec<usize>,
pub pierce_selections: Vec<PierceSelection>,
pub total_rapid_distance: f64,
}
pub fn optimize_sequence(
contours: &[CutContour],
dag: &CuttingDag,
config: &CuttingConfig,
) -> SequenceResult {
optimize_sequence_with_adjacency(contours, dag, config, None)
}
pub fn optimize_sequence_with_adjacency(
contours: &[CutContour],
dag: &CuttingDag,
config: &CuttingConfig,
common_edges: Option<&CommonEdgeResult>,
) -> SequenceResult {
if contours.is_empty() {
return SequenceResult {
order: Vec::new(),
pierce_selections: Vec::new(),
total_rapid_distance: 0.0,
};
}
let adjacency = build_adjacency_map(common_edges);
let mut order = nearest_neighbor_with_adjacency(contours, dag, config, &adjacency);
if config.max_2opt_iterations > 0 {
improve_2opt(&mut order, contours, dag, config);
}
let (pierce_selections, total_rapid) = compute_pierce_selections(&order, contours, config);
SequenceResult {
order,
pierce_selections,
total_rapid_distance: total_rapid,
}
}
fn build_adjacency_map(
common_edges: Option<&CommonEdgeResult>,
) -> HashMap<usize, Vec<(usize, f64)>> {
let mut adjacency: HashMap<usize, Vec<(usize, f64)>> = HashMap::new();
if let Some(result) = common_edges {
for edge in &result.common_edges {
adjacency
.entry(edge.contour_a)
.or_default()
.push((edge.contour_b, edge.overlap_length));
adjacency
.entry(edge.contour_b)
.or_default()
.push((edge.contour_a, edge.overlap_length));
}
}
adjacency
}
fn nearest_neighbor_with_adjacency(
contours: &[CutContour],
dag: &CuttingDag,
config: &CuttingConfig,
adjacency: &HashMap<usize, Vec<(usize, f64)>>,
) -> Vec<usize> {
let n = contours.len();
let mut visited: HashSet<usize> = HashSet::with_capacity(n);
let mut order = Vec::with_capacity(n);
let mut current_pos = config.home_position;
let mut last_id: Option<usize> = None;
const ADJACENCY_DISCOUNT: f64 = 0.5;
for _ in 0..n {
let mut best_idx = None;
let mut best_score = f64::MAX;
for contour in contours.iter() {
if visited.contains(&contour.id) {
continue;
}
let predecessors = dag.predecessors(contour.id);
let ready = predecessors.iter().all(|pred_id| visited.contains(pred_id));
if !ready {
continue;
}
let dist = closest_point_on_polygon(&contour.vertices, current_pos)
.map(|(pt, _, _)| point_distance(current_pos, pt))
.unwrap_or(f64::MAX);
let mut score = dist;
if let Some(last) = last_id {
if let Some(neighbors) = adjacency.get(&last) {
if neighbors.iter().any(|(adj_id, _)| *adj_id == contour.id) {
score *= ADJACENCY_DISCOUNT;
}
}
}
if score < best_score {
best_score = score;
best_idx = Some(contour.id);
}
}
if let Some(id) = best_idx {
visited.insert(id);
order.push(id);
last_id = Some(id);
if let Some(contour) = contours.iter().find(|c| c.id == id) {
let pierce = select_pierce(contour, current_pos, config);
current_pos = pierce.end_point;
}
}
}
order
}
fn improve_2opt(
order: &mut [usize],
contours: &[CutContour],
dag: &CuttingDag,
config: &CuttingConfig,
) {
let n = order.len();
if n < 3 {
return;
}
let mut improved = true;
let mut iterations = 0;
let mut current_rapid = compute_pierce_selections(order, contours, config).1;
while improved && iterations < config.max_2opt_iterations {
improved = false;
iterations += 1;
for i in 0..n - 1 {
for j in (i + 2)..n {
order[i + 1..=j].reverse();
if dag.is_valid_sequence(order) {
let new_rapid = compute_pierce_selections(order, contours, config).1;
if new_rapid < current_rapid - 1e-10 {
current_rapid = new_rapid;
improved = true;
} else {
order[i + 1..=j].reverse(); }
} else {
order[i + 1..=j].reverse(); }
}
}
}
}
fn compute_pierce_selections(
order: &[usize],
contours: &[CutContour],
config: &CuttingConfig,
) -> (Vec<PierceSelection>, f64) {
let mut selections = Vec::with_capacity(order.len());
let mut total_rapid = 0.0;
let mut current_pos = config.home_position;
for &contour_id in order {
let contour = match contours.iter().find(|c| c.id == contour_id) {
Some(c) => c,
None => continue,
};
let pierce = select_pierce(contour, current_pos, config);
let rapid = point_distance(current_pos, pierce.point);
total_rapid += rapid;
current_pos = pierce.end_point;
selections.push(pierce);
}
(selections, total_rapid)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::contour::ContourType;
fn make_contour(id: usize, cx: f64, cy: f64, ct: ContourType) -> CutContour {
CutContour {
id,
geometry_id: format!("part{}", id),
instance: 0,
contour_type: ct,
vertices: vec![
(cx - 5.0, cy - 5.0),
(cx + 5.0, cy - 5.0),
(cx + 5.0, cy + 5.0),
(cx - 5.0, cy + 5.0),
],
perimeter: 40.0,
centroid: (cx, cy),
}
}
#[test]
fn test_single_contour() {
let contours = vec![make_contour(0, 50.0, 50.0, ContourType::Exterior)];
let dag = CuttingDag::build(&contours);
let config = CuttingConfig::default();
let result = optimize_sequence(&contours, &dag, &config);
assert_eq!(result.order.len(), 1);
assert_eq!(result.order[0], 0);
}
#[test]
fn test_nn_selects_nearest() {
let contours = vec![
make_contour(0, 100.0, 0.0, ContourType::Exterior),
make_contour(1, 20.0, 0.0, ContourType::Exterior),
make_contour(2, 60.0, 0.0, ContourType::Exterior),
];
let dag = CuttingDag::build(&contours);
let config = CuttingConfig::default();
let result = optimize_sequence(&contours, &dag, &config);
assert_eq!(result.order, vec![1, 2, 0]);
}
#[test]
fn test_precedence_respected() {
let contours = vec![
CutContour {
id: 0,
geometry_id: "part1".to_string(),
instance: 0,
contour_type: ContourType::Exterior,
vertices: vec![(0.0, 0.0), (20.0, 0.0), (20.0, 20.0), (0.0, 20.0)],
perimeter: 80.0,
centroid: (10.0, 10.0),
},
CutContour {
id: 1,
geometry_id: "part1".to_string(),
instance: 0,
contour_type: ContourType::Interior,
vertices: vec![(5.0, 5.0), (15.0, 5.0), (15.0, 15.0), (5.0, 15.0)],
perimeter: 40.0,
centroid: (10.0, 10.0),
},
];
let dag = CuttingDag::build(&contours);
let config = CuttingConfig::default();
let result = optimize_sequence(&contours, &dag, &config);
let pos_interior = result
.order
.iter()
.position(|&id| id == 1)
.expect("interior should be in order");
let pos_exterior = result
.order
.iter()
.position(|&id| id == 0)
.expect("exterior should be in order");
assert!(pos_interior < pos_exterior);
}
#[test]
fn test_empty_contours() {
let contours: Vec<CutContour> = Vec::new();
let dag = CuttingDag::build(&contours);
let config = CuttingConfig::default();
let result = optimize_sequence(&contours, &dag, &config);
assert!(result.order.is_empty());
assert_eq!(result.total_rapid_distance, 0.0);
}
#[test]
fn test_nn_better_than_reverse() {
let contours: Vec<CutContour> = (0..5)
.map(|i| make_contour(i, 20.0 * i as f64 + 10.0, 10.0, ContourType::Exterior))
.collect();
let dag = CuttingDag::build(&contours);
let config = CuttingConfig::default();
let result = optimize_sequence(&contours, &dag, &config);
let reverse_order: Vec<usize> = (0..5).rev().collect();
let (_, reverse_rapid) = compute_pierce_selections(&reverse_order, &contours, &config);
assert!(
result.total_rapid_distance <= reverse_rapid + 1e-6,
"NN rapid {} should be <= reverse rapid {}",
result.total_rapid_distance,
reverse_rapid
);
}
#[test]
fn test_adjacency_bonus_prefers_neighbor() {
let contours = vec![
make_contour(0, 10.0, 10.0, ContourType::Exterior),
make_contour(1, 80.0, 10.0, ContourType::Exterior),
make_contour(2, 90.0, 10.0, ContourType::Exterior),
];
let dag = CuttingDag::build(&contours);
let config = CuttingConfig::default();
let common_edges = CommonEdgeResult {
common_edges: vec![crate::common_edge::CommonEdge {
contour_a: 0,
edge_a: 0,
contour_b: 2,
edge_b: 0,
overlap_length: 10.0,
midpoint: (50.0, 10.0),
}],
total_common_length: 10.0,
};
let result_with =
optimize_sequence_with_adjacency(&contours, &dag, &config, Some(&common_edges));
let result_without = optimize_sequence(&contours, &dag, &config);
assert_eq!(result_with.order.len(), 3);
assert_eq!(result_without.order.len(), 3);
if result_with.order[0] == 0 {
assert_eq!(
result_with.order[1], 2,
"Adjacent contour 2 should follow contour 0"
);
}
}
#[test]
fn test_adjacency_with_no_common_edges() {
let contours = vec![
make_contour(0, 10.0, 10.0, ContourType::Exterior),
make_contour(1, 30.0, 10.0, ContourType::Exterior),
];
let dag = CuttingDag::build(&contours);
let config = CuttingConfig::default();
let result_with = optimize_sequence_with_adjacency(&contours, &dag, &config, None);
let result_without = optimize_sequence(&contours, &dag, &config);
assert_eq!(result_with.order, result_without.order);
}
}