Skip to main content

u_nesting_cutting/
path.rs

1//! Final cutting path assembly and public API.
2//!
3//! Combines contour extraction, hierarchy analysis, sequence optimization,
4//! and pierce point selection into a complete cutting path.
5
6use u_nesting_core::timing::Timer;
7
8use u_nesting_core::geometry::{Geometry, Geometry2DExt};
9use u_nesting_core::SolveResult;
10
11use crate::common_edge;
12use crate::config::CuttingConfig;
13use crate::contour::extract_contours;
14use crate::cost::point_distance;
15use crate::gtsp;
16use crate::hierarchy::CuttingDag;
17use crate::kerf;
18use crate::result::{CutStep, CuttingPathResult};
19use crate::sequence::optimize_sequence_with_adjacency;
20
21/// Optimizes the cutting path for a nesting solve result.
22///
23/// This is the main entry point for cutting path optimization. It:
24/// 1. Extracts contours from the placed geometries
25/// 2. Builds precedence constraints (holes before exteriors)
26/// 3. Optimizes the cutting sequence (NN + 2-opt)
27/// 4. Selects optimal pierce points
28/// 5. Assembles the final cutting path
29///
30/// # Arguments
31///
32/// * `solve_result` - The nesting solve result with placements
33/// * `geometries` - The original geometry definitions
34/// * `config` - Cutting path optimization parameters
35///
36/// # Returns
37///
38/// A `CuttingPathResult` with the optimized cutting sequence.
39///
40/// # Example
41///
42/// ```rust,ignore
43/// use u_nesting_cutting::{CuttingConfig, optimize_cutting_path};
44/// use u_nesting::d2::{Geometry2D, Boundary2D, Nester2D};
45/// use u_nesting::core::{Solver, Config};
46///
47/// let geometries = vec![Geometry2D::rectangle("R1", 10.0, 5.0).with_quantity(3)];
48/// let boundary = Boundary2D::rectangle(100.0, 50.0);
49/// let nester = Nester2D::new(Config::default());
50/// let solve_result = nester.solve(&geometries, &boundary).unwrap();
51///
52/// let cutting_config = CuttingConfig::default();
53/// let path = optimize_cutting_path(&solve_result, &geometries, &cutting_config);
54/// println!("Rapid distance: {}", path.total_rapid_distance);
55/// ```
56pub fn optimize_cutting_path<G>(
57    solve_result: &SolveResult<f64>,
58    geometries: &[G],
59    config: &CuttingConfig,
60) -> CuttingPathResult
61where
62    G: Geometry2DExt<Scalar = f64> + Geometry<Scalar = f64>,
63{
64    let start = Timer::now();
65
66    // Step 1: Extract contours
67    let raw_contours = extract_contours(solve_result, geometries);
68
69    if raw_contours.is_empty() {
70        return CuttingPathResult::new();
71    }
72
73    // Step 1.5: Apply kerf compensation (if kerf_width > 0)
74    let contours = if config.kerf_width > 0.0 {
75        let kerf_results = kerf::apply_kerf_compensation(&raw_contours, config);
76        kerf::filter_compensated(kerf_results)
77    } else {
78        raw_contours
79    };
80
81    if contours.is_empty() {
82        return CuttingPathResult::new();
83    }
84
85    // Step 2: Detect common edges (used for sequence adjacency bonus)
86    let common_edges =
87        common_edge::detect_common_edges(&contours, config.kerf_width + config.tolerance, 0.1);
88
89    // Step 3: Build precedence DAG
90    let dag = CuttingDag::build(&contours);
91
92    // Step 4: Optimize sequence
93    // Use GTSP solver when multiple pierce candidates are configured
94    let mut result = CuttingPathResult::new();
95    let mut current_pos = config.home_position;
96
97    if config.pierce_candidates > 1 {
98        // GTSP path: discretize → build instance → solve with precedence
99        let clusters = gtsp::discretize_contours(&contours, config);
100        let instance = gtsp::build_gtsp_instance(clusters, config.home_position);
101        let solution = gtsp::solve_constrained(&instance, &dag, config.max_2opt_iterations);
102
103        for (i, &global_idx) in solution.iter().enumerate() {
104            let candidate = instance.candidate(global_idx);
105            let contour = match contours.iter().find(|c| c.id == candidate.contour_id) {
106                Some(c) => c,
107                None => continue,
108            };
109
110            let rapid_dist = point_distance(current_pos, candidate.point);
111
112            result.sequence.push(CutStep {
113                contour_id: candidate.contour_id,
114                geometry_id: contour.geometry_id.clone(),
115                instance: contour.instance,
116                contour_type: contour.contour_type,
117                pierce_point: candidate.point,
118                cut_direction: candidate.direction,
119                rapid_from: if i == 0 { None } else { Some(current_pos) },
120                rapid_distance: rapid_dist,
121                cut_distance: contour.perimeter,
122            });
123
124            result.total_rapid_distance += rapid_dist;
125            result.total_cut_distance += contour.perimeter;
126            result.total_pierces += 1;
127            current_pos = candidate.end_point;
128        }
129    } else {
130        // Legacy path: NN + 2-opt with single pierce selection + adjacency bonus
131        let seq_result =
132            optimize_sequence_with_adjacency(&contours, &dag, config, Some(&common_edges));
133
134        for (i, &contour_id) in seq_result.order.iter().enumerate() {
135            let contour = match contours.iter().find(|c| c.id == contour_id) {
136                Some(c) => c,
137                None => continue,
138            };
139
140            let pierce = &seq_result.pierce_selections[i];
141            let rapid_dist = point_distance(current_pos, pierce.point);
142
143            result.sequence.push(CutStep {
144                contour_id,
145                geometry_id: contour.geometry_id.clone(),
146                instance: contour.instance,
147                contour_type: contour.contour_type,
148                pierce_point: pierce.point,
149                cut_direction: pierce.direction,
150                rapid_from: if i == 0 { None } else { Some(current_pos) },
151                rapid_distance: rapid_dist,
152                cut_distance: contour.perimeter,
153            });
154
155            result.total_rapid_distance += rapid_dist;
156            result.total_cut_distance += contour.perimeter;
157            result.total_pierces += 1;
158            current_pos = pierce.end_point;
159        }
160    }
161
162    result.computation_time_ms = start.elapsed_ms();
163
164    // Estimate time if speeds are configured
165    if config.rapid_speed > 0.0 && config.cut_speed > 0.0 {
166        let rapid_time = result.total_rapid_distance / config.rapid_speed;
167        let cut_time = result.total_cut_distance / config.cut_speed;
168        result.estimated_time_seconds = Some(rapid_time + cut_time);
169    }
170
171    result
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177
178    #[test]
179    fn test_empty_solve_result() {
180        let solve_result: SolveResult<f64> = SolveResult::new();
181        let geometries: Vec<DummyGeom> = Vec::new();
182        let config = CuttingConfig::default();
183
184        let result = optimize_cutting_path(&solve_result, &geometries, &config);
185        assert!(result.sequence.is_empty());
186        assert_eq!(result.total_pierces, 0);
187    }
188
189    // Dummy geometry for testing (since Geometry2D is in u-nesting-d2)
190    #[derive(Clone)]
191    struct DummyGeom;
192
193    impl u_nesting_core::geometry::Geometry for DummyGeom {
194        type Scalar = f64;
195        fn id(&self) -> &u_nesting_core::GeometryId {
196            unimplemented!()
197        }
198        fn quantity(&self) -> usize {
199            1
200        }
201        fn measure(&self) -> f64 {
202            0.0
203        }
204        fn aabb_vec(&self) -> (Vec<f64>, Vec<f64>) {
205            (vec![0.0, 0.0], vec![0.0, 0.0])
206        }
207        fn centroid(&self) -> Vec<f64> {
208            vec![0.0, 0.0]
209        }
210        fn validate(&self) -> u_nesting_core::Result<()> {
211            Ok(())
212        }
213        fn rotation_constraint(&self) -> &u_nesting_core::RotationConstraint<f64> {
214            &u_nesting_core::RotationConstraint::None
215        }
216    }
217
218    impl u_nesting_core::geometry::Geometry2DExt for DummyGeom {
219        fn aabb_2d(&self) -> u_nesting_core::transform::AABB2D<f64> {
220            u_nesting_core::transform::AABB2D::new(0.0, 0.0, 0.0, 0.0)
221        }
222        fn outer_ring(&self) -> &[(f64, f64)] {
223            &[]
224        }
225        fn holes(&self) -> &[Vec<(f64, f64)>] {
226            &[]
227        }
228        fn is_convex(&self) -> bool {
229            true
230        }
231        fn convex_hull(&self) -> Vec<(f64, f64)> {
232            Vec::new()
233        }
234        fn perimeter(&self) -> f64 {
235            0.0
236        }
237    }
238}