From a24326e04625e39291cdb19521b038ac85e682e0 Mon Sep 17 00:00:00 2001 From: el-j Date: Wed, 28 Oct 2026 10:52:00 +0100 Subject: [PATCH] fix(cad): verify circular subpaths complete a full turn without reversing (#4151) --- crates/cad/src/scene.rs | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/crates/cad/src/scene.rs b/crates/cad/src/scene.rs index 1e7baac26..d4cd3ec30 100644 --- a/crates/cad/src/scene.rs +++ b/crates/cad/src/scene.rs @@ -126,6 +126,36 @@ fn circular_subpath(sub: &Sub) -> Option { if samples.iter().any(|p| (centre.distance(*p) - radius).abs() > tolerance) { return None; } + // Verify that the curve progresses monotonically around the circle without reversing, + // and that closed subpaths make exactly one full turn while open subpaths do not exceed one turn. + let mut total_turn = 0.0; + let mut dir: Option = None; + for w in samples.windows(2) { + let (p0, p1) = (w[0], w[1]); + let (v0x, v0y) = (p0.x - centre.x, p0.y - centre.y); + let (v1x, v1y) = (p1.x - centre.x, p1.y - centre.y); + let cross = v0x * v1y - v0y * v1x; + let dot = v0x * v1x + v0y * v1y; + let d_theta = cross.atan2(dot); + if d_theta.abs() > 1e-9 { + let is_ccw = d_theta > 0.0; + if let Some(expected_ccw) = dir { + if is_ccw != expected_ccw { + return None; + } + } else { + dir = Some(is_ccw); + } + total_turn += d_theta; + } + } + if sub.closed { + if (total_turn.abs() - std::f64::consts::TAU).abs() > 1e-3 { + return None; + } + } else if total_turn.abs() < 1e-6 || total_turn.abs() >= std::f64::consts::TAU - 1e-3 { + return None; + } Some(CircularSub { centre, radius, samples }) }