Skip to main content

u_nesting_cutting/
leadin.rs

1//! Lead-in and lead-out path generation.
2//!
3//! Generates entry and exit paths at pierce points to ensure smooth tool
4//! engagement and disengagement with the cut contour.
5//!
6//! # Lead-In Types
7//!
8//! - **Line**: Straight approach at a configurable angle to the contour edge
9//! - **Arc**: Circular arc approach tangent to the contour at the pierce point
10//!
11//! # Placement Rules
12//!
13//! - Exterior contours: lead-in positioned **outside** the part boundary
14//! - Interior contours (holes): lead-in positioned **inside** the hole
15//!   (i.e., in the waste material that will be removed)
16//!
17//! # References
18//!
19//! - Industry standard: lead-in length ~2x kerf width, 45-degree angle
20
21#[cfg(feature = "serde")]
22use serde::{Deserialize, Serialize};
23
24use crate::contour::{ContourType, CutContour};
25use crate::pierce::PierceSelection;
26use crate::result::CutDirection;
27
28/// Configuration for lead-in/lead-out generation.
29#[derive(Debug, Clone, Copy)]
30#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
31pub struct LeadInConfig {
32    /// Type of lead-in path.
33    pub lead_in_type: LeadInType,
34    /// Lead-in length (distance from start of lead-in to pierce point).
35    pub lead_in_length: f64,
36    /// Lead-out length (distance from contour close point to end of lead-out).
37    pub lead_out_length: f64,
38    /// Angle in radians for line lead-in (relative to the contour tangent at pierce).
39    /// Default: PI/4 (45 degrees).
40    pub lead_in_angle: f64,
41    /// Number of arc segments for arc-type lead-in.
42    pub arc_segments: usize,
43}
44
45/// Type of lead-in/lead-out path.
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
48pub enum LeadInType {
49    /// No lead-in (pierce directly at contour).
50    None,
51    /// Straight line approach at an angle.
52    Line,
53    /// Circular arc tangent to the contour.
54    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, // 45 degrees
64            arc_segments: 8,
65        }
66    }
67}
68
69/// Generated lead-in/lead-out paths for a pierce point.
70#[derive(Debug, Clone)]
71pub struct LeadInOut {
72    /// Points forming the lead-in path (from start to pierce point).
73    /// Empty if no lead-in.
74    pub lead_in: Vec<(f64, f64)>,
75    /// Points forming the lead-out path (from contour close to end).
76    /// Empty if no lead-out.
77    pub lead_out: Vec<(f64, f64)>,
78}
79
80/// Generates lead-in/lead-out paths for a pierce point on a contour.
81///
82/// The lead-in starts outside the part (for exterior contours) or inside the
83/// hole (for interior contours) and ends at the pierce point. The lead-out
84/// starts where the contour cut closes and extends away.
85pub 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
132/// Computes the tangent direction at the pierce point along the contour.
133fn 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); // Default direction
137    }
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
155/// Computes the outward normal direction for lead-in placement.
156///
157/// For exterior contours (CCW): outward is to the left of the tangent
158/// For interior contours (CW): outward (into waste) is to the right
159fn compute_outward_normal(
160    tangent: (f64, f64),
161    contour_type: ContourType,
162    direction: CutDirection,
163) -> (f64, f64) {
164    // Left normal of tangent: (-ty, tx)
165    // Right normal of tangent: (ty, -tx)
166    let (tx, ty) = tangent;
167
168    match (contour_type, direction) {
169        // Exterior CCW: outward is left (away from part)
170        (ContourType::Exterior, CutDirection::Ccw) => (-ty, tx),
171        // Exterior CW: outward is right
172        (ContourType::Exterior, CutDirection::Cw) => (ty, -tx),
173        // Interior CCW: into waste is right
174        (ContourType::Interior, CutDirection::Ccw) => (ty, -tx),
175        // Interior CW: into waste is left
176        (ContourType::Interior, CutDirection::Cw) => (-ty, tx),
177    }
178}
179
180/// Generates a straight-line lead-in path.
181///
182/// The lead-in starts at a point offset from the pierce point along a direction
183/// that combines the outward normal and the negative tangent (approaching the
184/// contour at an angle).
185fn 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    // Direction: rotate outward normal by -angle toward the approach direction
193    // Approach = outward * cos(angle) + (-tangent) * sin(angle)
194    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
206/// Generates an arc lead-in path (quarter circle tangent to contour).
207fn 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    // Arc from 180 degrees (opposite of outward) to pierce point (0 degrees relative)
218    // In the local frame where outward = (1, 0), pierce is at angle = PI
219    let start_angle = std::f64::consts::PI;
220    let end_angle = 0.0_f64; // NOT 2*PI, but 0.0 to go from start to pierce point
221    let angle_span = end_angle - start_angle; // -PI = clockwise quarter circle
222                                              // Actually we want a half-circle from start to pierce
223
224    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        // Point on arc relative to center, in the frame of the outward normal
230        let local_x = angle.cos() * radius;
231        let local_y = angle.sin() * radius;
232
233        // Transform to world coordinates using outward as the x-axis
234        // outward = (ox, oy), perpendicular = (-oy, ox)
235        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    // Ensure the last point is exactly the pierce point
243    if let Some(last) = points.last_mut() {
244        *last = pierce;
245    }
246
247    points
248}
249
250/// Generates a straight-line lead-out path.
251fn 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    // Lead-out continues along the tangent then moves outward
258    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        // Lead-in should have 2 points (start + pierce)
325        assert_eq!(result.lead_in.len(), 2);
326        // Last point should be the pierce point
327        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        // Start point should be further away from the contour
332        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        // Lead-out should have 2 points
355        assert_eq!(result.lead_out.len(), 2);
356        // First point is the end point (same as pierce for closed contour)
357        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        // Arc should have segments+1 points
376        assert_eq!(result.lead_in.len(), 9);
377        // Last point should be the pierce point
378        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        // Interior hole: lead-in should go into the waste (inside the hole)
386        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        // For interior CW on the right edge going up,
399        // outward (into waste) should be to the left (toward center)
400        let start = result.lead_in[0];
401        // Start should be to the left of the pierce (toward interior)
402        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}