Merge pull request #1558 from marcoarnulfo/fix/zigzag-regular-waveform

Zig Zag: ridges are spaced evenly along each segment's length, so a straight line gets a regular wave
This commit is contained in:
BFlat 2026-10-13 09:31:04 +07:00 committed by GitHub
commit d79fb145c2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 66 additions and 2 deletions

View File

@ -1,6 +1,7 @@
//! Distort & Transform effects.
use serde_json::Value;
use vectorcraft_geom::kurbo::ParamCurveArclen;
use vectorcraft_geom::{Affine, Anchor, ParamCurve, PathData, Point, Rect, SubPath, Vec2};
use crate::util::*;
@ -194,7 +195,10 @@ pub fn twist(path: &PathData, b: Rect, angle: f64) -> PathData {
map_nonlinear(path, pieces, f)
}
/// Zig Zag: `ridges` extra points per segment, alternately displaced along the normal.
const ZIGZAG_ARCLEN_ACCURACY: f64 = 1e-4;
/// Zig Zag: `ridges` extra points per segment, evenly spaced along its length and alternately
/// displaced along the normal.
pub fn zig_zag(path: &PathData, b: Rect, p: &Value) -> PathData {
let size = num(p, "size", 10.0).clamp(-1e4, 1e4);
let amp = if flag(p, "relative", false) { size / 100.0 * mean_size(b) } else { size };
@ -207,8 +211,12 @@ pub fn zig_zag(path: &PathData, b: Rect, p: &Value) -> PathData {
let mut k = 0usize;
for i in 0..segs {
let c = sp.segment(i);
// Ridges are spaced by length, not by `t`: a line's retracted handles bunch `t` up
// near the anchors, and so does any uneven curve.
let len = c.arclen(ZIGZAG_ARCLEN_ACCURACY);
for j in 0..=ridges {
let t = j as f64 / (ridges + 1) as f64;
let s = j as f64 / (ridges + 1) as f64;
let t = if j > 0 && len > 1e-9 { c.inv_arclen(s * len, ZIGZAG_ARCLEN_ACCURACY).clamp(0.0, 1.0) } else { s };
let nrm = if j == 0 {
// Average the normals of the segments meeting at the anchor.
let prev = if i > 0 {

View File

@ -244,6 +244,62 @@ fn zigzag_ridge_count_and_amplitude() {
assert!(b.width() > 100.0 && b.width() <= 120.0 + 1e-6, "{b:?}");
}
#[test]
fn zigzag_spaces_ridges_evenly_along_a_straight_line() {
// #1554: on a straight line every ridge is the same distance from the line and as far from
// the next one, whether the points are corners or smooth.
let line = PathData::single(SubPath::polyline(&[Point::new(0.0, 0.0), Point::new(0.0, 500.0)], false));
for points in ["corner", "smooth"] {
let out = run("distort.zigZag", json!({"ridges": 4, "points": points, "size": 20}), &line);
let ps: Vec<Point> = out.subpaths[0].anchors.iter().map(|a| a.p).collect();
assert_eq!(ps.len(), 6, "{points}");
for (k, q) in ps.iter().enumerate() {
assert!(close(q.x.abs(), 20.0, 1e-6), "{points} {k}: {q:?}");
assert!(close(q.y, 100.0 * k as f64, 1e-3), "{points} {k}: {q:?}");
}
}
}
/// #1554 on curves: a circle's ridges sit at equal angles, at the radius plus and minus the size in
/// turn all the way round.
#[test]
fn zigzag_on_a_circle_is_regular() {
let circle = shapes::ellipse(Rect::new(-50.0, -50.0, 50.0, 50.0));
let out = run("distort.zigZag", json!({"ridges": 3, "points": "corner", "size": 10}), &circle);
let pts: Vec<Point> = out.subpaths[0].anchors.iter().map(|a| a.p).collect();
assert_eq!(pts.len(), 16);
let start = pts[0].y.atan2(pts[0].x);
let mut radii = vec![];
for (i, q) in pts.iter().enumerate() {
let turn = (q.y.atan2(q.x) - start).to_degrees().abs() / 22.5;
assert!(close(turn, turn.round(), 0.02), "point {i} at {turn} sixteenths");
radii.push(q.to_vec2().hypot().round());
}
assert!(radii.iter().all(|r| *r == 60.0 || *r == 40.0) && (0..16).all(|i| radii[i] != radii[(i + 1) % 16]), "{radii:?}");
}
/// #1554 on an uneven curve: the ridges divide its length, not its parameter, into equal parts.
#[test]
fn zigzag_spaces_ridges_by_length_on_an_uneven_curve() {
use vectorcraft_geom::kurbo::{ParamCurve, ParamCurveArclen, ParamCurveNearest};
let (a, b) = (Point::new(0.0, 0.0), Point::new(300.0, 0.0));
let curve = PathData::single(SubPath::new(
vec![
vectorcraft_geom::Anchor::with_handles(a, a, Point::new(10.0, 40.0)),
vectorcraft_geom::Anchor::with_handles(b, Point::new(100.0, 200.0), b),
],
false,
));
let c = curve.subpaths[0].segment(0);
let len = c.arclen(1e-6);
let out = run("distort.zigZag", json!({"ridges": 3, "points": "corner", "size": 0}), &curve);
let along: Vec<f64> = out.subpaths[0].anchors.iter().map(|q| c.subsegment(0.0..c.nearest(q.p, 1e-9).t).arclen(1e-6) / len).collect();
assert_eq!(along.len(), 5);
for (k, share) in along.iter().enumerate() {
assert!(close(*share, k as f64 / 4.0, 1e-3), "{along:?}");
}
}
#[test]
fn offset_path_grows_and_shrinks() {
let grow = run("path.offsetPath", json!({"offset": 10, "joins": "miter"}), &square()).bounds().unwrap();