mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
Merge pull request #1646 from switch87/fix/1643-roughen-cusps
Roughen (Smooth): a turn-back stays smooth instead of becoming a cusp
This commit is contained in:
2 files changed
+65
-2
No files matched your search
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user