#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::contour::{ContourType, CutContour};
use crate::pierce::PierceSelection;
use crate::result::CutDirection;
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct LeadInConfig {
pub lead_in_type: LeadInType,
pub lead_in_length: f64,
pub lead_out_length: f64,
pub lead_in_angle: f64,
pub arc_segments: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum LeadInType {
None,
Line,
Arc,
}
impl Default for LeadInConfig {
fn default() -> Self {
Self {
lead_in_type: LeadInType::None,
lead_in_length: 2.0,
lead_out_length: 1.0,
lead_in_angle: std::f64::consts::FRAC_PI_4, arc_segments: 8,
}
}
}
#[derive(Debug, Clone)]
pub struct LeadInOut {
pub lead_in: Vec<(f64, f64)>,
pub lead_out: Vec<(f64, f64)>,
}
pub fn generate_lead_in_out(
contour: &CutContour,
pierce: &PierceSelection,
config: &LeadInConfig,
) -> LeadInOut {
if config.lead_in_type == LeadInType::None {
return LeadInOut {
lead_in: Vec::new(),
lead_out: Vec::new(),
};
}
let tangent = compute_tangent_at_pierce(contour, pierce);
let outward_normal = compute_outward_normal(tangent, contour.contour_type, pierce.direction);
let lead_in = match config.lead_in_type {
LeadInType::None => Vec::new(),
LeadInType::Line => generate_line_lead_in(
pierce.point,
tangent,
outward_normal,
config.lead_in_length,
config.lead_in_angle,
),
LeadInType::Arc => generate_arc_lead_in(
pierce.point,
tangent,
outward_normal,
config.lead_in_length,
config.arc_segments,
),
};
let lead_out = if config.lead_out_length > 0.0 {
generate_line_lead_out(
pierce.end_point,
tangent,
outward_normal,
config.lead_out_length,
)
} else {
Vec::new()
};
LeadInOut { lead_in, lead_out }
}
fn compute_tangent_at_pierce(contour: &CutContour, pierce: &PierceSelection) -> (f64, f64) {
let n = contour.vertices.len();
if n < 2 {
return (1.0, 0.0); }
let i = pierce.vertex_index;
let j = (i + 1) % n;
let (ax, ay) = contour.vertices[i];
let (bx, by) = contour.vertices[j];
let dx = bx - ax;
let dy = by - ay;
let len = (dx * dx + dy * dy).sqrt();
if len < 1e-12 {
(1.0, 0.0)
} else {
(dx / len, dy / len)
}
}
fn compute_outward_normal(
tangent: (f64, f64),
contour_type: ContourType,
direction: CutDirection,
) -> (f64, f64) {
let (tx, ty) = tangent;
match (contour_type, direction) {
(ContourType::Exterior, CutDirection::Ccw) => (-ty, tx),
(ContourType::Exterior, CutDirection::Cw) => (ty, -tx),
(ContourType::Interior, CutDirection::Ccw) => (ty, -tx),
(ContourType::Interior, CutDirection::Cw) => (-ty, tx),
}
}
fn generate_line_lead_in(
pierce: (f64, f64),
tangent: (f64, f64),
outward: (f64, f64),
length: f64,
angle: f64,
) -> Vec<(f64, f64)> {
let (cos_a, sin_a) = (angle.cos(), angle.sin());
let approach_dx = outward.0 * cos_a - tangent.0 * sin_a;
let approach_dy = outward.1 * cos_a - tangent.1 * sin_a;
let start = (
pierce.0 + approach_dx * length,
pierce.1 + approach_dy * length,
);
vec![start, pierce]
}
fn generate_arc_lead_in(
pierce: (f64, f64),
_tangent: (f64, f64),
outward: (f64, f64),
radius: f64,
segments: usize,
) -> Vec<(f64, f64)> {
let segments = segments.max(2);
let center = (pierce.0 + outward.0 * radius, pierce.1 + outward.1 * radius);
let start_angle = std::f64::consts::PI;
let end_angle = 0.0_f64; let angle_span = end_angle - start_angle;
let mut points = Vec::with_capacity(segments + 1);
for i in 0..=segments {
let t = i as f64 / segments as f64;
let angle = start_angle + angle_span * t;
let local_x = angle.cos() * radius;
let local_y = angle.sin() * radius;
let perp = (-outward.1, outward.0);
let world_x = center.0 + local_x * outward.0 + local_y * perp.0;
let world_y = center.1 + local_x * outward.1 + local_y * perp.1;
points.push((world_x, world_y));
}
if let Some(last) = points.last_mut() {
*last = pierce;
}
points
}
fn generate_line_lead_out(
end_point: (f64, f64),
tangent: (f64, f64),
outward: (f64, f64),
length: f64,
) -> Vec<(f64, f64)> {
let exit = (
end_point.0 + (tangent.0 + outward.0) * 0.5 * length,
end_point.1 + (tangent.1 + outward.1) * 0.5 * length,
);
vec![end_point, exit]
}
#[cfg(test)]
mod tests {
use super::*;
use crate::contour::ContourType;
fn make_square_contour(id: usize, size: f64, ct: ContourType) -> CutContour {
let half = size / 2.0;
CutContour {
id,
geometry_id: format!("part{}", id),
instance: 0,
contour_type: ct,
vertices: vec![(-half, -half), (half, -half), (half, half), (-half, half)],
perimeter: 4.0 * size,
centroid: (0.0, 0.0),
}
}
fn make_pierce(
point: (f64, f64),
vertex_index: usize,
direction: CutDirection,
) -> PierceSelection {
PierceSelection {
point,
vertex_index,
direction,
end_point: point,
}
}
#[test]
fn test_no_lead_in() {
let contour = make_square_contour(0, 10.0, ContourType::Exterior);
let pierce = make_pierce((-5.0, -5.0), 0, CutDirection::Ccw);
let config = LeadInConfig {
lead_in_type: LeadInType::None,
..Default::default()
};
let result = generate_lead_in_out(&contour, &pierce, &config);
assert!(result.lead_in.is_empty());
assert!(result.lead_out.is_empty());
}
#[test]
fn test_line_lead_in_exterior() {
let contour = make_square_contour(0, 10.0, ContourType::Exterior);
let pierce = make_pierce((-5.0, -5.0), 0, CutDirection::Ccw);
let config = LeadInConfig {
lead_in_type: LeadInType::Line,
lead_in_length: 3.0,
lead_out_length: 1.5,
..Default::default()
};
let result = generate_lead_in_out(&contour, &pierce, &config);
assert_eq!(result.lead_in.len(), 2);
let last = result.lead_in.last().expect("has points");
assert!((last.0 - (-5.0)).abs() < 1e-10);
assert!((last.1 - (-5.0)).abs() < 1e-10);
let start = result.lead_in[0];
let dist_start = ((start.0 - (-5.0)).powi(2) + (start.1 - (-5.0)).powi(2)).sqrt();
assert!(
dist_start > 2.0,
"Lead-in start should be offset from pierce: dist={}",
dist_start
);
}
#[test]
fn test_line_lead_out() {
let contour = make_square_contour(0, 10.0, ContourType::Exterior);
let pierce = make_pierce((-5.0, -5.0), 0, CutDirection::Ccw);
let config = LeadInConfig {
lead_in_type: LeadInType::Line,
lead_in_length: 3.0,
lead_out_length: 2.0,
..Default::default()
};
let result = generate_lead_in_out(&contour, &pierce, &config);
assert_eq!(result.lead_out.len(), 2);
let first = result.lead_out[0];
assert!((first.0 - (-5.0)).abs() < 1e-10);
assert!((first.1 - (-5.0)).abs() < 1e-10);
}
#[test]
fn test_arc_lead_in() {
let contour = make_square_contour(0, 10.0, ContourType::Exterior);
let pierce = make_pierce((-5.0, -5.0), 0, CutDirection::Ccw);
let config = LeadInConfig {
lead_in_type: LeadInType::Arc,
lead_in_length: 3.0,
arc_segments: 8,
..Default::default()
};
let result = generate_lead_in_out(&contour, &pierce, &config);
assert_eq!(result.lead_in.len(), 9);
let last = result.lead_in.last().expect("has points");
assert!((last.0 - (-5.0)).abs() < 1e-10);
assert!((last.1 - (-5.0)).abs() < 1e-10);
}
#[test]
fn test_interior_lead_in_direction() {
let contour = make_square_contour(0, 10.0, ContourType::Interior);
let pierce = make_pierce((5.0, 0.0), 1, CutDirection::Cw);
let config = LeadInConfig {
lead_in_type: LeadInType::Line,
lead_in_length: 3.0,
lead_out_length: 0.0,
..Default::default()
};
let result = generate_lead_in_out(&contour, &pierce, &config);
assert_eq!(result.lead_in.len(), 2);
let start = result.lead_in[0];
assert!(
start.0 < 5.0,
"Interior lead-in start should be inside hole: x={}",
start.0
);
}
#[test]
fn test_zero_lead_out_length() {
let contour = make_square_contour(0, 10.0, ContourType::Exterior);
let pierce = make_pierce((-5.0, -5.0), 0, CutDirection::Ccw);
let config = LeadInConfig {
lead_in_type: LeadInType::Line,
lead_in_length: 3.0,
lead_out_length: 0.0,
..Default::default()
};
let result = generate_lead_in_out(&contour, &pierce, &config);
assert_eq!(result.lead_in.len(), 2);
assert!(result.lead_out.is_empty());
}
#[test]
fn test_default_config() {
let config = LeadInConfig::default();
assert_eq!(config.lead_in_type, LeadInType::None);
assert!((config.lead_in_length - 2.0).abs() < 1e-10);
assert!((config.lead_out_length - 1.0).abs() < 1e-10);
assert!((config.lead_in_angle - std::f64::consts::FRAC_PI_4).abs() < 1e-10);
}
}