mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-14 06:22:24 +00:00
fix(dxf): bound spline controls and preserve rational lines
This commit is contained in:
1 parent
361b565670
commit
e51022fc6c
1 file changed
+46
-15
+46
-15
@@ -225,10 +225,9 @@ impl Homogeneous {
|
||||
// spans through cubic degree, tolerance-bounded rational/high-degree spans, invalid-weight
|
||||
// fallback, and fit-point fallback when control data is malformed.
|
||||
fn spline_path(spline: &crate::model::Spline) -> Result<Option<BezPath>, Error> {
|
||||
let points: Vec<Point> = spline.control.iter().map(|p| Point::new(p[0], p[1])).collect();
|
||||
let weights = (spline.flags & 4 != 0 && spline.weights.len() == points.len()).then_some(spline.weights.as_slice());
|
||||
let weights = (spline.flags & 4 != 0 && spline.weights.len() == spline.control.len()).then_some(spline.weights.as_slice());
|
||||
let mut limit_hit = false;
|
||||
let converted = usize::try_from(spline.degree).ok().and_then(|degree| nurbs(degree, &spline.knots, &points, weights, &mut limit_hit));
|
||||
let converted = usize::try_from(spline.degree).ok().and_then(|degree| nurbs(degree, &spline.knots, &spline.control, weights, &mut limit_hit));
|
||||
let path = converted
|
||||
.map(|mut path| {
|
||||
if spline.flags & 1 != 0 {
|
||||
@@ -254,13 +253,13 @@ fn spline_path(spline: &crate::model::Spline) -> Result<Option<BezPath>, Error>
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
fn nurbs(degree: usize, knots: &[f64], control: &[Point], weights: Option<&[f64]>, limit_hit: &mut bool) -> Option<BezPath> {
|
||||
fn nurbs(degree: usize, knots: &[f64], control: &[[f64; 3]], weights: Option<&[f64]>, limit_hit: &mut bool) -> Option<BezPath> {
|
||||
let n = control.len();
|
||||
if !(1..=MAX_SPLINE_DEGREE).contains(°ree)
|
||||
|| n <= degree
|
||||
|| knots.len() != n + degree + 1
|
||||
|| knots.windows(2).any(|w| w[1] < w[0])
|
||||
|| control.iter().any(|p| !p.x.is_finite() || !p.y.is_finite())
|
||||
|| control.iter().any(|p| !p[0].is_finite() || !p[1].is_finite())
|
||||
|| knots.iter().any(|k| !k.is_finite())
|
||||
{
|
||||
return None;
|
||||
@@ -271,10 +270,9 @@ fn nurbs(degree: usize, knots: &[f64], control: &[Point], weights: Option<&[f64]
|
||||
return None;
|
||||
}
|
||||
let weight = |i: usize| (raw(i) / max_w).max(MIN_RELATIVE_WEIGHT);
|
||||
let homogeneous: Vec<_> = control.iter().enumerate().map(|(i, p)| Homogeneous { x: p.x * weight(i), y: p.y * weight(i), w: weight(i) }).collect();
|
||||
let rational = (0..n).any(|i| (weight(i) - weight(0)).abs() > 1e-12);
|
||||
let (lo, hi) = control.iter().fold((Point::new(f64::MAX, f64::MAX), Point::new(f64::MIN, f64::MIN)), |(lo, hi), p| {
|
||||
(Point::new(lo.x.min(p.x), lo.y.min(p.y)), Point::new(hi.x.max(p.x), hi.y.max(p.y)))
|
||||
(Point::new(lo.x.min(p[0]), lo.y.min(p[1])), Point::new(hi.x.max(p[0]), hi.y.max(p[1])))
|
||||
});
|
||||
let tolerance = ((hi - lo).hypot() * SPLINE_TOLERANCE).max(1e-12);
|
||||
let mut path = BezPath::new();
|
||||
@@ -286,7 +284,7 @@ fn nurbs(degree: usize, knots: &[f64], control: &[Point], weights: Option<&[f64]
|
||||
let bezier: Vec<Homogeneous> = (0..=degree)
|
||||
.map(|j| {
|
||||
let args: Vec<_> = (0..degree).map(|k| if k < degree - j { t0 } else { t1 }).collect();
|
||||
blossom(degree, knots, &homogeneous, span, &args)
|
||||
blossom(degree, knots, control, span, &args, &weight)
|
||||
})
|
||||
.collect::<Option<_>>()?;
|
||||
let start = bezier.first()?.point()?;
|
||||
@@ -294,7 +292,9 @@ fn nurbs(degree: usize, knots: &[f64], control: &[Point], weights: Option<&[f64]
|
||||
if needs_new_subpath {
|
||||
path.move_to(start);
|
||||
}
|
||||
let polynomial: Option<Vec<Point>> = if rational { None } else { bezier.iter().map(|p| p.point()).collect() };
|
||||
// A rational degree-one span changes its parameterization with its weights, but its locus
|
||||
// is still the straight segment between its endpoints. Preserve that exact geometry.
|
||||
let polynomial: Option<Vec<Point>> = if rational && degree > 1 { None } else { bezier.iter().map(|p| p.point()).collect() };
|
||||
match polynomial.as_deref() {
|
||||
Some([_, p]) => path.line_to(*p),
|
||||
Some([_, q, p]) => path.quad_to(*q, *p),
|
||||
@@ -318,8 +318,19 @@ fn path_endpoint(path: &BezPath) -> Option<Point> {
|
||||
}
|
||||
}
|
||||
|
||||
fn blossom(degree: usize, knots: &[f64], control: &[Homogeneous], span: usize, args: &[f64]) -> Option<Homogeneous> {
|
||||
let mut points = control.get(span - degree..=span)?.to_vec();
|
||||
fn blossom(degree: usize, knots: &[f64], control: &[[f64; 3]], span: usize, args: &[f64], weight: &impl Fn(usize) -> f64) -> Option<Homogeneous> {
|
||||
// Only the active degree+1 control points are needed for one span. Convert this small window
|
||||
// here instead of making a second vector proportional to an untrusted entity's full controls.
|
||||
let first = span.checked_sub(degree)?;
|
||||
let mut points: Vec<_> = control
|
||||
.get(first..=span)?
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(offset, p)| {
|
||||
let w = weight(first + offset);
|
||||
Homogeneous { x: p[0] * w, y: p[1] * w, w }
|
||||
})
|
||||
.collect();
|
||||
for r in 1..=degree {
|
||||
let u = *args.get(r - 1)?;
|
||||
for j in (r..=degree).rev() {
|
||||
@@ -650,6 +661,10 @@ mod spline_tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn xyz(points: &[Point]) -> Vec<[f64; 3]> {
|
||||
points.iter().map(|point| [point.x, point.y, 0.0]).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spline_path_follows_clamped_control_curve_and_bounds_work() {
|
||||
let path = spline_path(&cubic()).unwrap().expect("valid cubic spline");
|
||||
@@ -662,6 +677,22 @@ mod spline_tests {
|
||||
|
||||
#[test]
|
||||
fn rational_closed_periodic_and_fit_point_splines_keep_compatibility() {
|
||||
let mut rational_line = Spline {
|
||||
degree: 1,
|
||||
flags: 4,
|
||||
knots: vec![0.0, 0.0, 1.0, 1.0],
|
||||
control: vec![[2.0, 3.0, 0.0], [5.0, 7.0, 0.0]],
|
||||
weights: vec![1.0, 100.0],
|
||||
fit: Vec::new(),
|
||||
start_tangent: None,
|
||||
end_tangent: None,
|
||||
fit_tolerance: 0.0,
|
||||
};
|
||||
rational_line.flags |= 8;
|
||||
let line = spline_path(&rational_line).unwrap().expect("rational degree-one spline");
|
||||
assert!(matches!(line.elements(), [kurbo::PathEl::MoveTo(a), kurbo::PathEl::LineTo(b)]
|
||||
if *a == Point::new(2.0, 3.0) && *b == Point::new(5.0, 7.0)));
|
||||
|
||||
let mut rational = cubic();
|
||||
rational.flags |= 4;
|
||||
rational.weights = vec![1.0, 0.5, 0.5, 1.0];
|
||||
@@ -716,7 +747,7 @@ mod spline_tests {
|
||||
let path = nurbs(
|
||||
1,
|
||||
&[0.0, 0.0, 0.5, 0.5, 1.0, 1.0],
|
||||
&[Point::new(0.0, 0.0), Point::new(1.0, 0.0), Point::new(5.0, 5.0), Point::new(6.0, 5.0)],
|
||||
&xyz(&[Point::new(0.0, 0.0), Point::new(1.0, 0.0), Point::new(5.0, 5.0), Point::new(6.0, 5.0)]),
|
||||
None,
|
||||
&mut limit_hit,
|
||||
)
|
||||
@@ -731,7 +762,7 @@ mod spline_tests {
|
||||
let start = 1.0e12;
|
||||
let delta = 1.0e-3;
|
||||
let mut limit_hit = false;
|
||||
let path = nurbs(1, &[start, start, start + delta, start + delta], &[Point::new(2.0, 3.0), Point::new(5.0, 7.0)], None, &mut limit_hit)
|
||||
let path = nurbs(1, &[start, start, start + delta, start + delta], &xyz(&[Point::new(2.0, 3.0), Point::new(5.0, 7.0)]), None, &mut limit_hit)
|
||||
.expect("positive-width span");
|
||||
assert!(!limit_hit);
|
||||
assert!(matches!(path.elements(), [kurbo::PathEl::MoveTo(a), kurbo::PathEl::LineTo(b)]
|
||||
@@ -780,8 +811,8 @@ mod spline_tests {
|
||||
#[test]
|
||||
fn rational_quadratic_circle_matches_the_prior_importer_tolerance() {
|
||||
let weight = std::f64::consts::FRAC_1_SQRT_2;
|
||||
let control = [(1.0, 0.0), (1.0, 1.0), (0.0, 1.0), (-1.0, 1.0), (-1.0, 0.0), (-1.0, -1.0), (0.0, -1.0), (1.0, -1.0), (1.0, 0.0)]
|
||||
.map(|(x, y)| Point::new(200.0 + x * 100.0, 200.0 + y * 100.0));
|
||||
let control = xyz(&[(1.0, 0.0), (1.0, 1.0), (0.0, 1.0), (-1.0, 1.0), (-1.0, 0.0), (-1.0, -1.0), (0.0, -1.0), (1.0, -1.0), (1.0, 0.0)]
|
||||
.map(|(x, y)| Point::new(200.0 + x * 100.0, 200.0 + y * 100.0)));
|
||||
let knots = [0.0, 0.0, 0.0, 1.0, 1.0, 2.0, 2.0, 3.0, 3.0, 4.0, 4.0, 4.0];
|
||||
let weights = [1.0, weight, 1.0, weight, 1.0, weight, 1.0, weight, 1.0];
|
||||
let mut limit_hit = false;
|
||||
|
||||
Reference in new issue
Block a user