1#[cfg(feature = "serde")]
22use serde::{Deserialize, Serialize};
23
24use crate::contour::{ContourType, CutContour};
25use crate::pierce::PierceSelection;
26use crate::result::CutDirection;
27
28#[derive(Debug, Clone, Copy)]
30#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
31pub struct LeadInConfig {
32 pub lead_in_type: LeadInType,
34 pub lead_in_length: f64,
36 pub lead_out_length: f64,
38 pub lead_in_angle: f64,
41 pub arc_segments: usize,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
48pub enum LeadInType {
49 None,
51 Line,
53 Arc,
55}
56
57impl Default for LeadInConfig {
58 fn default() -> Self {
59 Self {
60 lead_in_type: LeadInType::None,
61 lead_in_length: 2.0,
62 lead_out_length: 1.0,
63 lead_in_angle: std::f64::consts::FRAC_PI_4, arc_segments: 8,
65 }
66 }
67}
68
69#[derive(Debug, Clone)]
71pub struct LeadInOut {
72 pub lead_in: Vec<(f64, f64)>,
75 pub lead_out: Vec<(f64, f64)>,
78}
79
80pub fn generate_lead_in_out(
86 contour: &CutContour,
87 pierce: &PierceSelection,
88 config: &LeadInConfig,
89) -> LeadInOut {
90 if config.lead_in_type == LeadInType::None {
91 return LeadInOut {
92 lead_in: Vec::new(),
93 lead_out: Vec::new(),
94 };
95 }
96
97 let tangent = compute_tangent_at_pierce(contour, pierce);
98 let outward_normal = compute_outward_normal(tangent, contour.contour_type, pierce.direction);
99
100 let lead_in = match config.lead_in_type {
101 LeadInType::None => Vec::new(),
102 LeadInType::Line => generate_line_lead_in(
103 pierce.point,
104 tangent,
105 outward_normal,
106 config.lead_in_length,
107 config.lead_in_angle,
108 ),
109 LeadInType::Arc => generate_arc_lead_in(
110 pierce.point,
111 tangent,
112 outward_normal,
113 config.lead_in_length,
114 config.arc_segments,
115 ),
116 };
117
118 let lead_out = if config.lead_out_length > 0.0 {
119 generate_line_lead_out(
120 pierce.end_point,
121 tangent,
122 outward_normal,
123 config.lead_out_length,
124 )
125 } else {
126 Vec::new()
127 };
128
129 LeadInOut { lead_in, lead_out }
130}
131
132fn compute_tangent_at_pierce(contour: &CutContour, pierce: &PierceSelection) -> (f64, f64) {
134 let n = contour.vertices.len();
135 if n < 2 {
136 return (1.0, 0.0); }
138
139 let i = pierce.vertex_index;
140 let j = (i + 1) % n;
141 let (ax, ay) = contour.vertices[i];
142 let (bx, by) = contour.vertices[j];
143
144 let dx = bx - ax;
145 let dy = by - ay;
146 let len = (dx * dx + dy * dy).sqrt();
147
148 if len < 1e-12 {
149 (1.0, 0.0)
150 } else {
151 (dx / len, dy / len)
152 }
153}
154
155fn compute_outward_normal(
160 tangent: (f64, f64),
161 contour_type: ContourType,
162 direction: CutDirection,
163) -> (f64, f64) {
164 let (tx, ty) = tangent;
167
168 match (contour_type, direction) {
169 (ContourType::Exterior, CutDirection::Ccw) => (-ty, tx),
171 (ContourType::Exterior, CutDirection::Cw) => (ty, -tx),
173 (ContourType::Interior, CutDirection::Ccw) => (ty, -tx),
175 (ContourType::Interior, CutDirection::Cw) => (-ty, tx),
177 }
178}
179
180fn generate_line_lead_in(
186 pierce: (f64, f64),
187 tangent: (f64, f64),
188 outward: (f64, f64),
189 length: f64,
190 angle: f64,
191) -> Vec<(f64, f64)> {
192 let (cos_a, sin_a) = (angle.cos(), angle.sin());
195 let approach_dx = outward.0 * cos_a - tangent.0 * sin_a;
196 let approach_dy = outward.1 * cos_a - tangent.1 * sin_a;
197
198 let start = (
199 pierce.0 + approach_dx * length,
200 pierce.1 + approach_dy * length,
201 );
202
203 vec![start, pierce]
204}
205
206fn generate_arc_lead_in(
208 pierce: (f64, f64),
209 _tangent: (f64, f64),
210 outward: (f64, f64),
211 radius: f64,
212 segments: usize,
213) -> Vec<(f64, f64)> {
214 let segments = segments.max(2);
215 let center = (pierce.0 + outward.0 * radius, pierce.1 + outward.1 * radius);
216
217 let start_angle = std::f64::consts::PI;
220 let end_angle = 0.0_f64; let angle_span = end_angle - start_angle; let mut points = Vec::with_capacity(segments + 1);
225 for i in 0..=segments {
226 let t = i as f64 / segments as f64;
227 let angle = start_angle + angle_span * t;
228
229 let local_x = angle.cos() * radius;
231 let local_y = angle.sin() * radius;
232
233 let perp = (-outward.1, outward.0);
236 let world_x = center.0 + local_x * outward.0 + local_y * perp.0;
237 let world_y = center.1 + local_x * outward.1 + local_y * perp.1;
238
239 points.push((world_x, world_y));
240 }
241
242 if let Some(last) = points.last_mut() {
244 *last = pierce;
245 }
246
247 points
248}
249
250fn generate_line_lead_out(
252 end_point: (f64, f64),
253 tangent: (f64, f64),
254 outward: (f64, f64),
255 length: f64,
256) -> Vec<(f64, f64)> {
257 let exit = (
259 end_point.0 + (tangent.0 + outward.0) * 0.5 * length,
260 end_point.1 + (tangent.1 + outward.1) * 0.5 * length,
261 );
262
263 vec![end_point, exit]
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269 use crate::contour::ContourType;
270
271 fn make_square_contour(id: usize, size: f64, ct: ContourType) -> CutContour {
272 let half = size / 2.0;
273 CutContour {
274 id,
275 geometry_id: format!("part{}", id),
276 instance: 0,
277 contour_type: ct,
278 vertices: vec![(-half, -half), (half, -half), (half, half), (-half, half)],
279 perimeter: 4.0 * size,
280 centroid: (0.0, 0.0),
281 }
282 }
283
284 fn make_pierce(
285 point: (f64, f64),
286 vertex_index: usize,
287 direction: CutDirection,
288 ) -> PierceSelection {
289 PierceSelection {
290 point,
291 vertex_index,
292 direction,
293 end_point: point,
294 }
295 }
296
297 #[test]
298 fn test_no_lead_in() {
299 let contour = make_square_contour(0, 10.0, ContourType::Exterior);
300 let pierce = make_pierce((-5.0, -5.0), 0, CutDirection::Ccw);
301 let config = LeadInConfig {
302 lead_in_type: LeadInType::None,
303 ..Default::default()
304 };
305
306 let result = generate_lead_in_out(&contour, &pierce, &config);
307 assert!(result.lead_in.is_empty());
308 assert!(result.lead_out.is_empty());
309 }
310
311 #[test]
312 fn test_line_lead_in_exterior() {
313 let contour = make_square_contour(0, 10.0, ContourType::Exterior);
314 let pierce = make_pierce((-5.0, -5.0), 0, CutDirection::Ccw);
315 let config = LeadInConfig {
316 lead_in_type: LeadInType::Line,
317 lead_in_length: 3.0,
318 lead_out_length: 1.5,
319 ..Default::default()
320 };
321
322 let result = generate_lead_in_out(&contour, &pierce, &config);
323
324 assert_eq!(result.lead_in.len(), 2);
326 let last = result.lead_in.last().expect("has points");
328 assert!((last.0 - (-5.0)).abs() < 1e-10);
329 assert!((last.1 - (-5.0)).abs() < 1e-10);
330
331 let start = result.lead_in[0];
333 let dist_start = ((start.0 - (-5.0)).powi(2) + (start.1 - (-5.0)).powi(2)).sqrt();
334 assert!(
335 dist_start > 2.0,
336 "Lead-in start should be offset from pierce: dist={}",
337 dist_start
338 );
339 }
340
341 #[test]
342 fn test_line_lead_out() {
343 let contour = make_square_contour(0, 10.0, ContourType::Exterior);
344 let pierce = make_pierce((-5.0, -5.0), 0, CutDirection::Ccw);
345 let config = LeadInConfig {
346 lead_in_type: LeadInType::Line,
347 lead_in_length: 3.0,
348 lead_out_length: 2.0,
349 ..Default::default()
350 };
351
352 let result = generate_lead_in_out(&contour, &pierce, &config);
353
354 assert_eq!(result.lead_out.len(), 2);
356 let first = result.lead_out[0];
358 assert!((first.0 - (-5.0)).abs() < 1e-10);
359 assert!((first.1 - (-5.0)).abs() < 1e-10);
360 }
361
362 #[test]
363 fn test_arc_lead_in() {
364 let contour = make_square_contour(0, 10.0, ContourType::Exterior);
365 let pierce = make_pierce((-5.0, -5.0), 0, CutDirection::Ccw);
366 let config = LeadInConfig {
367 lead_in_type: LeadInType::Arc,
368 lead_in_length: 3.0,
369 arc_segments: 8,
370 ..Default::default()
371 };
372
373 let result = generate_lead_in_out(&contour, &pierce, &config);
374
375 assert_eq!(result.lead_in.len(), 9);
377 let last = result.lead_in.last().expect("has points");
379 assert!((last.0 - (-5.0)).abs() < 1e-10);
380 assert!((last.1 - (-5.0)).abs() < 1e-10);
381 }
382
383 #[test]
384 fn test_interior_lead_in_direction() {
385 let contour = make_square_contour(0, 10.0, ContourType::Interior);
387 let pierce = make_pierce((5.0, 0.0), 1, CutDirection::Cw);
388 let config = LeadInConfig {
389 lead_in_type: LeadInType::Line,
390 lead_in_length: 3.0,
391 lead_out_length: 0.0,
392 ..Default::default()
393 };
394
395 let result = generate_lead_in_out(&contour, &pierce, &config);
396 assert_eq!(result.lead_in.len(), 2);
397
398 let start = result.lead_in[0];
401 assert!(
403 start.0 < 5.0,
404 "Interior lead-in start should be inside hole: x={}",
405 start.0
406 );
407 }
408
409 #[test]
410 fn test_zero_lead_out_length() {
411 let contour = make_square_contour(0, 10.0, ContourType::Exterior);
412 let pierce = make_pierce((-5.0, -5.0), 0, CutDirection::Ccw);
413 let config = LeadInConfig {
414 lead_in_type: LeadInType::Line,
415 lead_in_length: 3.0,
416 lead_out_length: 0.0,
417 ..Default::default()
418 };
419
420 let result = generate_lead_in_out(&contour, &pierce, &config);
421 assert_eq!(result.lead_in.len(), 2);
422 assert!(result.lead_out.is_empty());
423 }
424
425 #[test]
426 fn test_default_config() {
427 let config = LeadInConfig::default();
428 assert_eq!(config.lead_in_type, LeadInType::None);
429 assert!((config.lead_in_length - 2.0).abs() < 1e-10);
430 assert!((config.lead_out_length - 1.0).abs() < 1e-10);
431 assert!((config.lead_in_angle - std::f64::consts::FRAC_PI_4).abs() < 1e-10);
432 }
433}