Roughen (Smooth): a turn-back stays smooth instead of becoming a cusp (#1643)
Roughen's jitter can put a point behind its predecessor, so the path turns back. The Catmull-Rom tangent next - prev then shrinks to almost nothing and the anchor becomes a cusp, which PDF viewers (MuPDF, poppler, Ghostscript) draw with a miter spike; the preview doesn't. catmull_rom now keeps the tangent at least half as long as the longer chord, and where it is shorter (turns of more than about 150 degrees) points it along the bisector of the two chords: the turn becomes a round hairpin. Gentler turns keep the plain Catmull-Rom tangent, so Zig Zag, Scribble and the render goldens are unchanged. Fixes #1643
This commit is contained in:
parent
f05fbfc292
commit
977b8fb642
|
|
@ -236,6 +236,44 @@ fn roughen_seed_changes_result_and_detail_adds_points() {
|
|||
assert!(bb.x0 > -8.0 && bb.x1 < 108.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn catmull_rom_keeps_a_turn_back_smooth() {
|
||||
// #1643: the path runs out to (10, 0) and turns back to (0.5, 0.2). The Catmull-Rom tangent
|
||||
// `next - prev` is almost zero there; the anchor gets handles along the chords' bisector
|
||||
// (here nearly perpendicular to them), collinear and as long as half the longer chord / 6.
|
||||
let pts = [Point::new(0.0, 0.0), Point::new(10.0, 0.0), Point::new(0.5, 0.2), Point::new(10.0, 5.0)];
|
||||
let sp = util::catmull_rom(&pts, false, 1.0);
|
||||
let a = sp.anchors[1];
|
||||
let (hin, hout) = (a.h_in - a.p, a.h_out - a.p);
|
||||
assert!(close(hin.x, -hout.x, 1e-9) && close(hin.y, -hout.y, 1e-9), "collinear: {a:?}");
|
||||
assert!(close(hout.hypot(), 0.5 * 10.0 / 6.0, 1e-3), "{a:?}");
|
||||
assert!(hout.x.abs() < 0.05 * hout.hypot(), "across the chords: {a:?}");
|
||||
// A regular turn keeps the plain Catmull-Rom tangent.
|
||||
let pts = [Point::new(0.0, 0.0), Point::new(10.0, 0.0), Point::new(10.0, 10.0)];
|
||||
let a = util::catmull_rom(&pts, false, 1.0).anchors[1];
|
||||
let (got, want) = (a.h_out - a.p, (pts[2] - pts[0]) / 6.0);
|
||||
assert!(close(got.x, want.x, 1e-12) && close(got.y, want.y, 1e-12), "{got:?} vs {want:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roughen_smooth_makes_no_cusps() {
|
||||
// #1643: wherever Roughen's jitter turns the path back, the smooth points stay smooth: no
|
||||
// anchor's handles shrink below 1/12 of its longer chord (a cusp that PDF viewers miter).
|
||||
let star = shapes::star(Point::new(150.0, 100.0), 70.0, 32.0, 5, 0.0);
|
||||
for seed in 0..50 {
|
||||
let out = run("distort.roughen", json!({"size": 3, "detail": 10, "seed": seed}), &star);
|
||||
for sp in &out.subpaths {
|
||||
let n = sp.anchors.len();
|
||||
for i in 0..n {
|
||||
let a = sp.anchors[i];
|
||||
let (prev, next) = (sp.anchors[(i + n - 1) % n].p, sp.anchors[(i + 1) % n].p);
|
||||
let chord = (a.p - prev).hypot().max((next - a.p).hypot());
|
||||
assert!((a.h_out - a.p).hypot() >= chord / 12.0 - 1e-9, "seed {seed}: cusp at {:?}", a.p);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zigzag_ridge_count_and_amplitude() {
|
||||
let out = run("distort.zigZag", json!({"ridges": 4, "points": "corner", "size": 10}), &square());
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ pub fn seg_cubic(sp: &SubPath, i: usize) -> CubicBez {
|
|||
/// Non-linear path mapping (shared with live envelopes in `vectorcraft-doc`).
|
||||
pub use vectorcraft_doc::live::map_nonlinear;
|
||||
|
||||
/// Smooth subpath through `pts` (Catmull-Rom tangents).
|
||||
/// Smooth subpath through `pts` (Catmull-Rom tangents, see [`turn_tangent`] for sharp turns).
|
||||
pub fn catmull_rom(pts: &[Point], closed: bool, tension: f64) -> SubPath {
|
||||
let n = pts.len();
|
||||
if n < 3 {
|
||||
|
|
@ -82,13 +82,38 @@ pub fn catmull_rom(pts: &[Point], closed: bool, tension: f64) -> SubPath {
|
|||
.map(|i| {
|
||||
let prev = if i == 0 { if closed { pts[n - 1] } else { pts[0] } } else { pts[i - 1] };
|
||||
let next = if i == n - 1 { if closed { pts[0] } else { pts[n - 1] } } else { pts[i + 1] };
|
||||
let t: Vec2 = (next - prev) * (tension / 6.0);
|
||||
let t: Vec2 = turn_tangent(prev, pts[i], next) * (tension / 6.0);
|
||||
Anchor::with_handles(pts[i], pts[i] - t, pts[i] + t)
|
||||
})
|
||||
.collect();
|
||||
SubPath::new(anchors, closed)
|
||||
}
|
||||
|
||||
/// The Catmull-Rom tangent `next − prev` at `p`. Where the path turns back sharply, `next`
|
||||
/// nearly meets `prev` and that tangent shrinks to nothing: the anchor would be a cusp, which
|
||||
/// PDF viewers draw with a miter spike (#1643). So it is kept at least half as long as the
|
||||
/// longer chord, and there it follows the bisector of the two chords (the perpendicular of the
|
||||
/// incoming one when the path reverses exactly): the turn becomes a round hairpin. Turns of
|
||||
/// less than about 150° keep the plain Catmull-Rom tangent.
|
||||
fn turn_tangent(prev: Point, p: Point, next: Point) -> Vec2 {
|
||||
let t = next - prev;
|
||||
let (a, b) = (p - prev, next - p);
|
||||
let floor = 0.5 * a.hypot().max(b.hypot());
|
||||
if t.hypot() >= floor {
|
||||
return t;
|
||||
}
|
||||
let unit = |v: Vec2| {
|
||||
let l = v.hypot();
|
||||
if l > 1e-12 { v / l } else { Vec2::ZERO }
|
||||
};
|
||||
let (u, v) = (unit(a), unit(b));
|
||||
let mut d = unit(u + v);
|
||||
if d == Vec2::ZERO {
|
||||
d = Vec2::new(-u.y, u.x);
|
||||
}
|
||||
if d == Vec2::ZERO { t } else { d * floor }
|
||||
}
|
||||
|
||||
/// Unit normal (left of travel direction in y-down coordinates) of cubic `c` at `t`.
|
||||
pub fn normal_at(c: &CubicBez, t: f64) -> Vec2 {
|
||||
let eps = 1e-4;
|
||||
|
|
|
|||
Loading…
Reference in New Issue