mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-29 20:27:05 -06:00
Merge pull request #4433 from el-j/fix/dxf-circular-sliver-4151
Fix #4151: DXF export: verify closed circular subpaths complete a full turn without reversing
This commit is contained in:
@@ -126,6 +126,61 @@ fn circular_subpath(sub: &Sub) -> Option<CircularSub> {
|
||||
if samples.iter().any(|p| (centre.distance(*p) - radius).abs() > tolerance) {
|
||||
return None;
|
||||
}
|
||||
// Verify that the curve progresses monotonically around the circle without reversing,
|
||||
// both along each cubic segment's angular derivative and across sampled intervals,
|
||||
// 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<bool> = None;
|
||||
let ang_threshold = radius * radius * 1e-9;
|
||||
let mut seg_from = sub.start;
|
||||
for seg in &sub.segs {
|
||||
let Seg::Cubic(a, b, to) = *seg else { return None };
|
||||
for step in 0..=32 {
|
||||
let t = f64::from(step) / 32.0;
|
||||
let p = cubic_point(seg_from, a, b, to, t);
|
||||
let u = 1.0 - t;
|
||||
let dx = 3.0 * u * u * (a.x - seg_from.x) + 6.0 * u * t * (b.x - a.x) + 3.0 * t * t * (to.x - b.x);
|
||||
let dy = 3.0 * u * u * (a.y - seg_from.y) + 6.0 * u * t * (b.y - a.y) + 3.0 * t * t * (to.y - b.y);
|
||||
let cross = (p.x - centre.x) * dy - (p.y - centre.y) * dx;
|
||||
if cross.abs() > ang_threshold {
|
||||
let is_ccw = cross > 0.0;
|
||||
if let Some(expected_ccw) = dir {
|
||||
if is_ccw != expected_ccw {
|
||||
return None;
|
||||
}
|
||||
} else {
|
||||
dir = Some(is_ccw);
|
||||
}
|
||||
}
|
||||
}
|
||||
seg_from = to;
|
||||
}
|
||||
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 })
|
||||
}
|
||||
|
||||
|
||||
@@ -246,6 +246,52 @@ fn closed_circular_arc_with_a_closing_chord_is_not_a_circle() {
|
||||
assert!(!kinds(&r12).iter().any(|k| k == "CIRCLE"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_closed_sliver_along_a_circle_is_not_a_circle() {
|
||||
let d = doc_with(|d| {
|
||||
let (cx, cy, r) = (60.0, 250.0, 200.0);
|
||||
let at = |deg: f64| {
|
||||
let rad = deg.to_radians();
|
||||
Point::new(cx + r * rad.cos(), cy + r * rad.sin())
|
||||
};
|
||||
let along = |sliver: &mut vectorcraft_geom::BezPath, deg0: f64, deg1: f64| {
|
||||
let p0 = at(deg0);
|
||||
let p3 = at(deg1);
|
||||
let step = (deg1 - deg0).to_radians();
|
||||
let k = 4.0 / 3.0 * (step / 4.0).tan();
|
||||
let r0 = deg0.to_radians();
|
||||
let r1 = deg1.to_radians();
|
||||
let p1 = Point::new(p0.x - r * r0.sin() * k, p0.y + r * r0.cos() * k);
|
||||
let p2 = Point::new(p3.x + r * r1.sin() * k, p3.y - r * r1.cos() * k);
|
||||
sliver.curve_to(p1, p2, p3);
|
||||
};
|
||||
let mut sliver = vectorcraft_geom::BezPath::new();
|
||||
sliver.move_to(at(-110.0));
|
||||
along(&mut sliver, -110.0, -90.0);
|
||||
along(&mut sliver, -90.0, -70.0);
|
||||
along(&mut sliver, -70.0, -110.0);
|
||||
sliver.close_path();
|
||||
vec![path(d, PathData::from_bezpath(&sliver), Paint::None, Paint::solid(Color::BLACK), 1.0)]
|
||||
});
|
||||
|
||||
let modern = export(&d, &opts(&d)).unwrap();
|
||||
assert_ne!(kinds(&modern), ["CIRCLE"]);
|
||||
assert_eq!(kinds(&modern), ["SPLINE"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cubic_with_hidden_angular_reversal_is_not_promoted_to_arc() {
|
||||
let d = doc_with(|d| {
|
||||
let mut b = vectorcraft_geom::BezPath::new();
|
||||
b.move_to(Point::new(-8.404028665134, 62.061475842818));
|
||||
b.curve_to(Point::new(-13.285253116737, 62.103123153545), Point::new(52.811163576591, 52.471253544459), Point::new(60.0, 50.0));
|
||||
vec![path(d, PathData::from_bezpath(&b), Paint::None, Paint::solid(Color::BLACK), 1.0)]
|
||||
});
|
||||
let dxf = export(&d, &opts(&d)).unwrap();
|
||||
assert_ne!(kinds(&dxf), ["ARC"]);
|
||||
assert_eq!(kinds(&dxf), ["SPLINE"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spline_closed_flags_follow_path_intent_across_versions_without_changing_geometry() {
|
||||
let cases = [(true, true), (true, false), (false, false), (false, true)];
|
||||
|
||||
Reference in New Issue
Block a user