mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-01 13:47:21 -06:00
Merge pull request #4958 from dotCooCoo/fix/issue-3187
Part of #3187: a gradient mesh made from an ellipse keeps its shape
This commit is contained in:
@@ -233,6 +233,60 @@ mod tests {
|
||||
assert_eq!(envelope_mapper(&kind, Rect::new(0.0, 0.0, 100.0, 100.0))(p), p);
|
||||
}
|
||||
|
||||
/// How far the border of `m` strays from the ellipse in `r`, in points (measured across the
|
||||
/// ellipse's short radius), checked at 20 places along every border edge.
|
||||
fn mesh_border_off_ellipse(m: &GradientMesh, r: Rect) -> f64 {
|
||||
let (rows, cols) = (m.rows as usize, m.cols as usize);
|
||||
let edges = (0..cols).flat_map(|c| [m.edge_u(0, c), m.edge_u(rows, c)]).chain((0..rows).flat_map(|k| [m.edge_v(k, 0), m.edge_v(k, cols)]));
|
||||
let (c, a, b) = (r.center(), r.width() / 2.0, r.height() / 2.0);
|
||||
edges
|
||||
.flat_map(|e| (0..=20).map(move |i| e.eval(f64::from(i) / 20.0)))
|
||||
.map(|p| ((p.x - c.x) / a).hypot((p.y - c.y) / b) - 1.0)
|
||||
.fold(0.0f64, |w, d| w.max(d.abs() * a.min(b)))
|
||||
}
|
||||
|
||||
/// #3187: a gradient mesh made from an ellipse keeps the ellipse's shape: a Mesh tool click
|
||||
/// (a 1×1 mesh with lines through the point) and Create Gradient Mesh's 4×4 grid both lay
|
||||
/// their border on the outline, within about twice the 0.35 pt the outline is flattened to.
|
||||
/// The 1×1 mesh was up to 7 pt off this 400 × 300 pt ellipse.
|
||||
#[test]
|
||||
fn a_mesh_from_an_ellipse_keeps_its_outline() {
|
||||
const OFF: f64 = 0.75;
|
||||
let r = Rect::new(100.0, 100.0, 500.0, 400.0);
|
||||
let e = shapes::ellipse(r);
|
||||
for (rows, cols) in [(1, 1), (4, 4), (2, 5)] {
|
||||
let m = GradientMesh::for_path(&e, rows, cols, Color::BLACK, MeshAppearance::Flat, 0.0).unwrap();
|
||||
let off = mesh_border_off_ellipse(&m, r);
|
||||
assert!(off < OFF, "{rows}×{cols} mesh: border {off} pt off the ellipse");
|
||||
}
|
||||
let made = GradientMesh::for_path(&e, 1, 1, Color::BLACK, MeshAppearance::Flat, 0.0).unwrap();
|
||||
let mut m = made.clone();
|
||||
// Off centre, so each line splits its edges unevenly.
|
||||
let i = m.add_lines_at(Point::new(200.0, 330.0), 0.0).unwrap();
|
||||
let off = mesh_border_off_ellipse(&m, r);
|
||||
assert!(off < OFF, "after a Mesh tool click: {off} pt");
|
||||
// Deleting the lines again (Alt-click) gives the border back as it was.
|
||||
assert!(m.remove_point_lines(i));
|
||||
assert_eq!((m.rows, m.cols), (1, 1));
|
||||
for (k, (now, was)) in m.points.iter().zip(&made.points).enumerate() {
|
||||
let moved = now.handles.iter().zip(&was.handles).map(|(a, b)| (*a - *b).hypot()).fold(0.0, f64::max);
|
||||
assert!(now.p.distance(was.p) < 1e-6 && moved < 1e-3, "point {k}: {now:?}, made as {was:?}");
|
||||
}
|
||||
// With a handle at the new border point shortened since, the two edges no longer make one
|
||||
// curve: the merged edge is straight rather than grown past its ends.
|
||||
let mut m = made.clone();
|
||||
let i = m.add_lines_at(Point::new(200.0, 330.0), 0.0).unwrap();
|
||||
let top = m.idx(0, 1);
|
||||
m.points[top].handles[H_LEFT] *= 1e-4;
|
||||
assert!(m.remove_point_lines(i));
|
||||
let (a, b) = (m.points[0].p, m.points[1].p);
|
||||
assert!((m.points[0].handles[H_RIGHT] - (b - a) / 3.0).hypot() < 1e-9, "{:?}", m.points[0].handles);
|
||||
// A rectangle's mesh keeps straight edges with handles a third of the way along.
|
||||
let m = GradientMesh::for_path(&shapes::rectangle(Rect::new(0.0, 0.0, 90.0, 60.0)), 1, 1, Color::BLACK, MeshAppearance::Flat, 0.0).unwrap();
|
||||
let e = m.edge_u(0, 0);
|
||||
assert!(e.p1.distance(Point::new(30.0, 0.0)) < 1e-6 && e.p2.distance(Point::new(60.0, 0.0)) < 1e-6, "{e:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mesh_insert_and_remove_lines() {
|
||||
let mut m =
|
||||
|
||||
+123
-8
@@ -166,6 +166,76 @@ impl Coons {
|
||||
// Gradient meshes
|
||||
// =====================================================================================
|
||||
|
||||
/// The handles, as offsets from `ends`, of the cubic from `ends.0` to `ends.1` that comes closest
|
||||
/// (least squares) to `side` between `span.0` and `span.1`; `None` when they can't be computed.
|
||||
/// Each point of the side is matched to the curve's nearest point, refined over a few rounds.
|
||||
fn fit_edge(side: &[Point], span: (f64, f64), ends: (Point, Point)) -> Option<(Vec2, Vec2)> {
|
||||
const N: usize = 32;
|
||||
const ROUNDS: usize = 6;
|
||||
let (p0, p3) = (ends.0, ends.1);
|
||||
let qs: Vec<Point> = (1..N).map(|i| Coons::sample(side, span.0 + (span.1 - span.0) * i as f64 / N as f64)).collect();
|
||||
let mut ts: Vec<f64> = (1..N).map(|i| i as f64 / N as f64).collect();
|
||||
let mut fit = None;
|
||||
for _ in 0..ROUNDS {
|
||||
let (mut c11, mut c12, mut c22) = (0.0, 0.0, 0.0);
|
||||
let (mut x1, mut x2) = (Vec2::ZERO, Vec2::ZERO);
|
||||
for (q, &t) in qs.iter().zip(&ts) {
|
||||
let s = 1.0 - t;
|
||||
let (b0, b1, b2, b3) = (s * s * s, 3.0 * s * s * t, 3.0 * s * t * t, t * t * t);
|
||||
let r = q.to_vec2() - b0 * p0.to_vec2() - b3 * p3.to_vec2();
|
||||
c11 += b1 * b1;
|
||||
c12 += b1 * b2;
|
||||
c22 += b2 * b2;
|
||||
x1 += b1 * r;
|
||||
x2 += b2 * r;
|
||||
}
|
||||
let det = c11 * c22 - c12 * c12;
|
||||
if det.abs() < 1e-12 {
|
||||
break;
|
||||
}
|
||||
let (p1, p2) = (((c22 * x1 - c12 * x2) / det).to_point(), ((c11 * x2 - c12 * x1) / det).to_point());
|
||||
if !(p1.is_finite() && p2.is_finite()) {
|
||||
break;
|
||||
}
|
||||
fit = Some((p1 - p0, p2 - p3));
|
||||
// One Newton step towards the nearest point of the curve for each point of the side.
|
||||
let c = CubicBez::new(p0, p1, p2, p3);
|
||||
let (a, b, k) = (p1 - p0, p2 - p1, p3 - p2);
|
||||
for (q, t) in qs.iter().zip(ts.iter_mut()) {
|
||||
let s = 1.0 - *t;
|
||||
let v = 3.0 * (s * s * a + 2.0 * s * *t * b + *t * *t * k);
|
||||
let acc = 6.0 * (s * (b - a) + *t * (k - b));
|
||||
let e = c.eval(*t) - *q;
|
||||
let den = v.hypot2() + e.dot(acc);
|
||||
if den.abs() > 1e-12 {
|
||||
*t = (*t - e.dot(v) / den).clamp(0.0, 1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
fit
|
||||
}
|
||||
|
||||
/// The handles of one cubic standing for two edges that meet at point `mid.0`, whose handles
|
||||
/// towards them are `mid.1` (towards `a`) and `mid.2` (towards `b`), as offsets from their outer
|
||||
/// ends: each outer handle grown back by the share of the curve its edge had, which undoes the
|
||||
/// split [`GradientMesh::insert_row`] and [`GradientMesh::insert_col`] make, so deleting a mesh
|
||||
/// line keeps the curve of the border it crossed (#3187). The share is the split parameter, read
|
||||
/// from the lengths of the middle handles. When the edges no longer fit one cubic (a point or a
|
||||
/// handle was moved since the split), the cubic grown back would not pass through the middle
|
||||
/// point, and the merged edge is straight.
|
||||
fn merged_handles(a: (Point, Vec2), mid: (Point, Vec2, Vec2), b: (Point, Vec2)) -> (Vec2, Vec2) {
|
||||
let straight = ((b.0 - a.0) / 3.0, (a.0 - b.0) / 3.0);
|
||||
let (la, lb) = (mid.1.hypot(), mid.2.hypot());
|
||||
let t = la / (la + lb);
|
||||
if !(0.01..=0.99).contains(&t) {
|
||||
return straight;
|
||||
}
|
||||
let (ha, hb) = (a.1 / t, b.1 / (1.0 - t));
|
||||
let through = CubicBez::new(a.0, a.0 + ha, b.0 + hb, b.0).eval(t);
|
||||
let tol = 1e-3 * (a.0.distance(mid.0) + mid.0.distance(b.0)).max(1e-9);
|
||||
if ha.is_finite() && hb.is_finite() && through.distance(mid.0) <= tol { (ha, hb) } else { straight }
|
||||
}
|
||||
|
||||
/// `0, 1/n, …, 1` for `n` (clamped to 1..=200) even steps.
|
||||
fn even_steps(n: u32) -> Vec<f64> {
|
||||
let n = n.clamp(1, 200);
|
||||
@@ -283,7 +353,49 @@ impl GradientMesh {
|
||||
/// colour and opacity `color_at` gives at its position (a gradient fill's, say).
|
||||
pub fn for_path_with(path: &PathData, rows: u32, cols: u32, color_at: &dyn Fn(Point) -> (Color, f32)) -> Option<Self> {
|
||||
let coons = Coons::from_path(path)?;
|
||||
Some(Self::from_grid(&even_steps(cols), &even_steps(rows), &|u, v| coons.eval(u, v), &|_, _, p| color_at(p)))
|
||||
let (us, vs) = (even_steps(cols), even_steps(rows));
|
||||
let mut m = Self::from_grid(&us, &vs, &|u, v| coons.eval(u, v), &|_, _, p| color_at(p));
|
||||
m.fit_border(&coons, &us, &vs);
|
||||
Some(m)
|
||||
}
|
||||
|
||||
/// Give each edge on the mesh's border the handles that make it follow the outline it stands
|
||||
/// for, in place of the handles [`Self::from_grid`] estimates from the surface's derivatives
|
||||
/// (#3187). Where two border edges meet along a side, their handles are made collinear, so the
|
||||
/// border stays smooth there; the four corners keep the handles their own edges were fitted with.
|
||||
fn fit_border(&mut self, coons: &Coons, us: &[f64], vs: &[f64]) {
|
||||
let (rows, cols) = (self.rows as usize, self.cols as usize);
|
||||
if !self.is_valid() || us.len() != cols + 1 || vs.len() != rows + 1 {
|
||||
return;
|
||||
}
|
||||
let edges = [(0, &coons.top), (rows, &coons.bottom)]
|
||||
.map(|(r, side)| (r, side, true))
|
||||
.into_iter()
|
||||
.chain([(0, &coons.left), (cols, &coons.right)].map(|(c, side)| (c, side, false)));
|
||||
for (line, side, along_u) in edges {
|
||||
let (n, steps) = if along_u { (cols, us) } else { (rows, vs) };
|
||||
for k in 0..n {
|
||||
let (a, b, out, back) = if along_u {
|
||||
(self.idx(line, k), self.idx(line, k + 1), H_RIGHT, H_LEFT)
|
||||
} else {
|
||||
(self.idx(k, line), self.idx(k + 1, line), H_DOWN, H_UP)
|
||||
};
|
||||
if let Some((h1, h2)) = fit_edge(side, (steps[k], steps[k + 1]), (self.points[a].p, self.points[b].p)) {
|
||||
self.points[a].handles[out] = h1;
|
||||
self.points[b].handles[back] = h2;
|
||||
}
|
||||
}
|
||||
for k in 1..n {
|
||||
let (i, out, back) = if along_u { (self.idx(line, k), H_RIGHT, H_LEFT) } else { (self.idx(k, line), H_DOWN, H_UP) };
|
||||
let h = &mut self.points[i].handles;
|
||||
let (fwd, bwd) = (h[out], h[back]);
|
||||
let dir = (fwd.normalize() - bwd.normalize()).normalize();
|
||||
if fwd.hypot2() > 0.0 && bwd.hypot2() > 0.0 && dir.is_finite() {
|
||||
h[out] = dir * fwd.hypot();
|
||||
h[back] = -dir * bwd.hypot();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create Gradient Mesh → Appearance: lighten the points towards white, to the centre or to
|
||||
@@ -792,10 +904,11 @@ impl GradientMesh {
|
||||
let above = self.idx(r - 1, cc);
|
||||
let below = self.idx(r + 1, cc);
|
||||
let mid = self.idx(r, cc);
|
||||
let (pa, pm, pb) = (self.points[above].p, self.points[mid].p, self.points[below].p);
|
||||
let _ = pm;
|
||||
self.points[above].handles[H_DOWN] = (pb - pa) / 3.0;
|
||||
self.points[below].handles[H_UP] = (pa - pb) / 3.0;
|
||||
let (pa, pb, m) = (self.points[above].p, self.points[below].p, &self.points[mid]);
|
||||
let hm = (m.p, m.handles[H_UP], m.handles[H_DOWN]);
|
||||
let (down, up) = merged_handles((pa, self.points[above].handles[H_DOWN]), hm, (pb, self.points[below].handles[H_UP]));
|
||||
self.points[above].handles[H_DOWN] = down;
|
||||
self.points[below].handles[H_UP] = up;
|
||||
}
|
||||
self.points.drain(r * w..(r + 1) * w);
|
||||
self.rows -= 1;
|
||||
@@ -806,9 +919,11 @@ impl GradientMesh {
|
||||
let rows = self.rows as usize;
|
||||
for rr in 0..=rows {
|
||||
let (l, rt) = (rr * w + c - 1, rr * w + c + 1);
|
||||
let (pl, pr) = (self.points[l].p, self.points[rt].p);
|
||||
self.points[l].handles[H_RIGHT] = (pr - pl) / 3.0;
|
||||
self.points[rt].handles[H_LEFT] = (pl - pr) / 3.0;
|
||||
let (pl, pr, m) = (self.points[l].p, self.points[rt].p, &self.points[rr * w + c]);
|
||||
let hm = (m.p, m.handles[H_LEFT], m.handles[H_RIGHT]);
|
||||
let (right, left) = merged_handles((pl, self.points[l].handles[H_RIGHT]), hm, (pr, self.points[rt].handles[H_LEFT]));
|
||||
self.points[l].handles[H_RIGHT] = right;
|
||||
self.points[rt].handles[H_LEFT] = left;
|
||||
}
|
||||
let mut k = 0;
|
||||
self.points.retain(|_| {
|
||||
|
||||
Reference in New Issue
Block a user