u_nesting_cutting/
path.rs1use 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
21pub 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 let raw_contours = extract_contours(solve_result, geometries);
68
69 if raw_contours.is_empty() {
70 return CuttingPathResult::new();
71 }
72
73 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 let common_edges =
87 common_edge::detect_common_edges(&contours, config.kerf_width + config.tolerance, 0.1);
88
89 let dag = CuttingDag::build(&contours);
91
92 let mut result = CuttingPathResult::new();
95 let mut current_pos = config.home_position;
96
97 if config.pierce_candidates > 1 {
98 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);
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 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 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 #[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}