use crate::profile::Profile2D;
use crate::Point2;
const SMOOTH_CURVE_SPACING_RATIO: f64 = 1.0 / 16.0;
const SMOOTH_CURVE_LONGEST_EDGE_RATIO: f64 = 0.10;
const RDP_EPSILON_RATIO: f64 = 1.0 / 100.0;
const RDP_EPSILON_MIN: f64 = 5.0e-3;
const THIN_FEATURE_RATIO: f64 = 0.05;
const DOUBLE_BACK_REFLEX_ARC_FRACTION: f64 = 0.15;
const REFLEX_TURN_SIN_TOL: f64 = 1.0e-6;
const SMOOTH_CURVE_MIN_VERTICES: usize = 24;
const SIMPLIFIED_MIN_VERTICES: usize = 12;
pub(super) fn mirror_profile_about_y_axis(profile: &mut Profile2D) {
for p in &mut profile.outer {
p.x = -p.x;
}
profile.outer.reverse();
for hole in &mut profile.holes {
for p in hole.iter_mut() {
p.x = -p.x;
}
hole.reverse();
}
}
#[inline]
fn perpendicular_distance(p: Point2<f64>, a: Point2<f64>, b: Point2<f64>) -> f64 {
let dx = b.x - a.x;
let dy = b.y - a.y;
let len_sq = dx * dx + dy * dy;
if len_sq < f64::EPSILON {
let ex = p.x - a.x;
let ey = p.y - a.y;
return (ex * ex + ey * ey).sqrt();
}
let cross = (p.x - a.x) * dy - (p.y - a.y) * dx;
cross.abs() / len_sq.sqrt()
}
fn rdp_simplify_open(points: &[Point2<f64>], epsilon: f64) -> Vec<Point2<f64>> {
let n = points.len();
if n < 3 {
return points.to_vec();
}
let mut keep = vec![false; n];
keep[0] = true;
keep[n - 1] = true;
let mut stack: Vec<(usize, usize)> = Vec::new();
stack.push((0, n - 1));
while let Some((start, end)) = stack.pop() {
if end <= start + 1 {
continue;
}
let a = points[start];
let b = points[end];
let mut max_dist = 0.0;
let mut max_idx = start;
for (i, p) in points.iter().enumerate().take(end).skip(start + 1) {
let d = perpendicular_distance(*p, a, b);
if d > max_dist {
max_dist = d;
max_idx = i;
}
}
if max_dist > epsilon {
keep[max_idx] = true;
stack.push((start, max_idx));
stack.push((max_idx, end));
}
}
points
.iter()
.enumerate()
.filter_map(|(i, p)| if keep[i] { Some(*p) } else { None })
.collect()
}
const SELF_INTERSECT_SCAN_MAX_VERTICES: usize = 1024;
fn closed_loop_self_intersects(loop_: &[Point2<f64>]) -> bool {
let n = loop_.len();
if !(4..=SELF_INTERSECT_SCAN_MAX_VERTICES).contains(&n) {
return false;
}
#[inline]
fn orient(a: Point2<f64>, b: Point2<f64>, c: Point2<f64>) -> f64 {
(b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)
}
#[inline]
fn segments_properly_cross(
a: Point2<f64>,
b: Point2<f64>,
c: Point2<f64>,
d: Point2<f64>,
) -> bool {
let d1 = orient(c, d, a);
let d2 = orient(c, d, b);
let d3 = orient(a, b, c);
let d4 = orient(a, b, d);
((d1 > 0.0) != (d2 > 0.0)) && ((d3 > 0.0) != (d4 > 0.0))
}
for i in 0..n {
let a = loop_[i];
let b = loop_[(i + 1) % n];
for j in (i + 1)..n {
if (j + 1) % n == i || (i + 1) % n == j {
continue;
}
let c = loop_[j];
let d = loop_[(j + 1) % n];
if segments_properly_cross(a, b, c, d) {
return true;
}
}
}
false
}
fn loop_doubles_back(loop_: &[Point2<f64>]) -> bool {
let n = loop_.len();
if n < 4 {
return false;
}
let mut signed_area2 = 0.0;
for i in 0..n {
let a = loop_[i];
let b = loop_[(i + 1) % n];
signed_area2 += a.x * b.y - b.x * a.y;
}
if signed_area2 == 0.0 {
return false;
}
let winding = signed_area2.signum();
let mut perimeter = 0.0;
let mut reflex_len = 0.0;
for i in 0..n {
let prev = loop_[(i + n - 1) % n];
let cur = loop_[i];
let next = loop_[(i + 1) % n];
let (ex_in, ey_in) = (cur.x - prev.x, cur.y - prev.y);
let (ex_out, ey_out) = (next.x - cur.x, next.y - cur.y);
let out_len = (ex_out * ex_out + ey_out * ey_out).sqrt();
perimeter += out_len;
let in_len = (ex_in * ex_in + ey_in * ey_in).sqrt();
let denom = in_len * out_len;
if denom <= 0.0 {
continue; }
let sin_turn = (ex_in * ey_out - ey_in * ex_out) / denom;
if sin_turn * winding < -REFLEX_TURN_SIN_TOL {
reflex_len += out_len;
}
}
perimeter > 0.0 && reflex_len / perimeter > DOUBLE_BACK_REFLEX_ARC_FRACTION
}
pub(super) fn simplify_smooth_curve_polyline(points: &[Point2<f64>]) -> Vec<Point2<f64>> {
let raw_len = points.len();
if raw_len < SMOOTH_CURVE_MIN_VERTICES {
return points.to_vec();
}
let closed = raw_len >= 2
&& (points[0].x - points[raw_len - 1].x).abs() < 1e-9
&& (points[0].y - points[raw_len - 1].y).abs() < 1e-9;
let core: &[Point2<f64>] = if closed {
&points[..raw_len - 1]
} else {
points
};
let n = core.len();
if n < SMOOTH_CURVE_MIN_VERTICES {
return points.to_vec();
}
let mut min_x = f64::INFINITY;
let mut min_y = f64::INFINITY;
let mut max_x = f64::NEG_INFINITY;
let mut max_y = f64::NEG_INFINITY;
for p in core {
if p.x < min_x {
min_x = p.x;
}
if p.y < min_y {
min_y = p.y;
}
if p.x > max_x {
max_x = p.x;
}
if p.y > max_y {
max_y = p.y;
}
}
let dx = max_x - min_x;
let dy = max_y - min_y;
let diag = (dx * dx + dy * dy).sqrt();
if !diag.is_finite() || diag < f64::EPSILON {
return points.to_vec();
}
let mut perimeter = 0.0;
let mut longest_edge: f64 = 0.0;
let mut area2 = 0.0; for i in 0..n {
let a = core[i];
let b = core[(i + 1) % n];
let ex = b.x - a.x;
let ey = b.y - a.y;
let len = (ex * ex + ey * ey).sqrt();
perimeter += len;
if len > longest_edge {
longest_edge = len;
}
area2 += a.x * b.y - b.x * a.y;
}
let mean_edge = perimeter / n as f64;
if mean_edge / diag > SMOOTH_CURVE_SPACING_RATIO {
return points.to_vec();
}
if longest_edge / diag > SMOOTH_CURVE_LONGEST_EDGE_RATIO {
return points.to_vec();
}
let half_thickness = if perimeter > f64::EPSILON {
area2.abs() / (2.0 * perimeter)
} else {
0.0
};
let is_thin = half_thickness / diag < THIN_FEATURE_RATIO;
if is_thin && loop_doubles_back(core) {
return points.to_vec();
}
let epsilon = (diag * RDP_EPSILON_RATIO).max(RDP_EPSILON_MIN);
let mut working: Vec<Point2<f64>> = core.to_vec();
working.push(core[0]); let simplified = rdp_simplify_open(&working, epsilon);
let mut simplified_core = simplified;
if simplified_core.len() >= 2 {
let last = simplified_core.len() - 1;
if (simplified_core[0].x - simplified_core[last].x).abs() < 1e-9
&& (simplified_core[0].y - simplified_core[last].y).abs() < 1e-9
{
simplified_core.pop();
}
}
if simplified_core.len() < SIMPLIFIED_MIN_VERTICES || simplified_core.len() >= n {
return points.to_vec();
}
if simplified_core.len() > SELF_INTERSECT_SCAN_MAX_VERTICES
|| closed_loop_self_intersects(&simplified_core)
{
return points.to_vec();
}
if closed {
let first = simplified_core[0];
simplified_core.push(first);
}
simplified_core
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn closed_loop_self_intersects_detects_bowtie() {
let square = [
Point2::new(0.0, 0.0),
Point2::new(1.0, 0.0),
Point2::new(1.0, 1.0),
Point2::new(0.0, 1.0),
];
assert!(!closed_loop_self_intersects(&square));
let bowtie = [
Point2::new(0.0, 0.0),
Point2::new(1.0, 0.0),
Point2::new(0.0, 1.0),
Point2::new(1.0, 1.0),
];
assert!(closed_loop_self_intersects(&bowtie));
assert!(!closed_loop_self_intersects(&square[..3]));
}
fn annular_sector_loop(radius: f64, thickness: f64, span: f64, seg: usize) -> Vec<Point2<f64>> {
let c = Point2::new(0.0, -radius);
let mut loop_ = Vec::with_capacity(2 * seg + 2);
for i in 0..=seg {
let a = -span / 2.0 + span * (i as f64 / seg as f64);
loop_.push(Point2::new(c.x + radius * a.cos(), c.y + radius * a.sin()));
}
let inner = radius - thickness;
for i in 0..=seg {
let a = span / 2.0 - span * (i as f64 / seg as f64);
loop_.push(Point2::new(c.x + inner * a.cos(), c.y + inner * a.sin()));
}
loop_
}
fn loop_area(loop_: &[Point2<f64>]) -> f64 {
let n = loop_.len();
let mut a = 0.0;
for i in 0..n {
let p = loop_[i];
let q = loop_[(i + 1) % n];
a += p.x * q.y - q.x * p.y;
}
(a * 0.5).abs()
}
#[test]
fn simplify_thin_annular_sector_stays_simple_and_area_preserving() {
let cases = [
(12000.0, 100.0, 240.0_f64),
(12000.0, 300.0, 240.0),
(12000.0, 600.0, 90.0),
(12000.0, 600.0, 180.0),
];
for (radius, thickness, span_deg) in cases {
let span = span_deg.to_radians();
let dense = annular_sector_loop(radius, thickness, span, 200);
assert!(
!closed_loop_self_intersects(&dense),
"input sector r={radius} t={thickness} {span_deg}° should start simple",
);
let out = simplify_smooth_curve_polyline(&dense);
assert!(
!closed_loop_self_intersects(&out),
"simplified sector r={radius} t={thickness} {span_deg}° self-intersects",
);
let (a_in, a_out) = (loop_area(&dense), loop_area(&out));
let rel = (a_out - a_in).abs() / a_in;
assert!(
rel < 0.02,
"sector r={radius} t={thickness} {span_deg}°: area drift {:.1}% \
(in={a_in:.0} out={a_out:.0}) — thin curved wall was distorted",
rel * 100.0,
);
}
}
#[test]
fn simplify_still_reduces_fat_round_disk() {
let mut disk = Vec::new();
let seg = 127;
for i in 0..seg {
let a = std::f64::consts::TAU * (i as f64 / seg as f64);
disk.push(Point2::new(0.5 * a.cos(), 0.5 * a.sin()));
}
let out = simplify_smooth_curve_polyline(&disk);
assert!(
out.len() < disk.len() && out.len() >= SIMPLIFIED_MIN_VERTICES,
"round disk should simplify from {} to [{SIMPLIFIED_MIN_VERTICES}, {}) verts, got {}",
disk.len(),
disk.len(),
out.len(),
);
assert!(!closed_loop_self_intersects(&out));
}
fn ellipse_loop(ar: f64, seg: usize) -> Vec<Point2<f64>> {
let (a, b) = (ar * 0.5, 0.5);
(0..seg)
.map(|i| {
let t = std::f64::consts::TAU * (i as f64 / seg as f64);
Point2::new(a * t.cos(), b * t.sin())
})
.collect()
}
#[test]
fn elongated_filled_ellipse_is_not_thin_gated() {
for ar in [6.0_f64, 8.0, 12.0, 20.0] {
assert!(
!loop_doubles_back(&ellipse_loop(ar, 128)),
"AR={ar} filled ellipse is convex — must not read as doubling back",
);
}
let ellipse = ellipse_loop(8.0, 128);
let out = simplify_smooth_curve_polyline(&ellipse);
assert!(
out.len() < ellipse.len() && out.len() >= SIMPLIFIED_MIN_VERTICES,
"8:1 ellipse must still simplify (was wrongly thin-gated): {} -> {} verts",
ellipse.len(),
out.len(),
);
assert!(!closed_loop_self_intersects(&out));
let rel = (loop_area(&out) - loop_area(&ellipse)).abs() / loop_area(&ellipse);
assert!(rel < 0.08, "8:1 ellipse area drift {:.1}%", rel * 100.0);
}
#[test]
fn doubles_back_ignores_localized_spikes() {
assert!(loop_doubles_back(&annular_sector_loop(
12000.0,
100.0,
(240.0_f64).to_radians(),
128
)));
let mut e = ellipse_loop(8.0, 128);
assert!(!loop_doubles_back(&e));
let tip = (0..e.len())
.max_by(|&i, &j| e[i].x.abs().partial_cmp(&e[j].x.abs()).unwrap())
.unwrap();
e[tip].x *= 0.75;
assert!(
!loop_doubles_back(&e),
"a single notch on a convex ellipse must not read as doubling back",
);
let mut disk: Vec<Point2<f64>> = (0..128)
.map(|i| {
let t = std::f64::consts::TAU * (i as f64 / 128.0);
Point2::new(t.cos(), t.sin())
})
.collect();
disk.insert(1, Point2::new(disk[0].x * 0.9999, disk[0].y * 0.9999));
assert!(
!loop_doubles_back(&disk),
"a sub-mm seam jog on a convex loop must not read as doubling back",
);
}
#[test]
fn doubles_back_independent_of_per_boundary_sampling_density() {
let centre = Point2::new(0.0, -12000.0);
let (r_out, r_in) = (12000.0, 12000.0 - 10.0);
let span = (90.0_f64).to_radians();
let mut loop_ = Vec::new();
for i in 0..=96 {
let a = -span / 2.0 + span * (i as f64 / 96.0);
loop_.push(Point2::new(centre.x + r_out * a.cos(), centre.y + r_out * a.sin()));
}
for i in 0..=16 {
let a = span / 2.0 - span * (i as f64 / 16.0);
loop_.push(Point2::new(centre.x + r_in * a.cos(), centre.y + r_in * a.sin()));
}
assert!(
loop_doubles_back(&loop_),
"a non-uniformly tessellated thin sector must still read as doubling back",
);
let out = simplify_smooth_curve_polyline(&loop_);
assert_eq!(out.len(), loop_.len(), "skewed-sampling thin sector must be gated");
}
}