Import: an Illustrator gradient mesh comes in as one connected mesh

The editing data writes a gradient mesh as its patches, each with its
place in the grid (`/P`) and the grid's size (`/Size`); the importer
read each patch as a mesh of its own, in a group, so moving a mesh
point tore a patch away and left a hole, and a patch with anchors on
its edges was cut into cells on its own.

The patches are now placed by `/P` in the `/Size` grid and joined at
the corners they share into one GradientMesh. Which index of `/P` runs
along the columns is taken from the one under which the shared corners
meet. Where an edge bends through anchors, a mesh line runs through the
whole row or column of patches there, each patch cut at the same place,
so every cell keeps single-cubic edges. A mesh whose patches don't say
where they are reads as before; one whose patches fill the grid but
don't join up reads as before with a warning. Sizes and lines are
capped; non-finite values fall back.

Hand-written tests: a 2 x 2 grid as one mesh with the old patches'
colours, a moved inner point shared by its four cells, an anchor on a
shared edge adding a line through the mesh, a collapsed patch with
finite values, and unplaced or unjoined patches staying separate.
This commit is contained in:
ham9zah
2026-10-24 03:52:21 +07:00
committed by bflatastic
parent d0919ad0df
commit 1653790965
2 changed files with 367 additions and 5 deletions
+355 -5
View File
@@ -24,9 +24,42 @@ const MAX_PATCHES: usize = 4096;
/// 34 × 34 points, so the patch cap alone doesn't bound them.
const MAX_POINTS: usize = MAX_PATCHES * 16;
/// The largest grid `/Size` read as one mesh, a side.
const MAX_SIDE: usize = 64;
/// The most rows or columns a mesh stitched from its patches may have once the lines through the
/// anchors on its patches' edges are added.
const MAX_LINES: usize = 256;
/// A patch as read: its place in the grid (`/P`), its corners and the anchors on its edges.
#[derive(Debug, Clone)]
struct Cell {
at: Option<(i64, i64)>,
corners: [Corner; 4],
anchors: Vec<(usize, [f64; 6])>,
}
/// Why the patches of a mesh don't make one grid.
#[derive(Debug, PartialEq)]
pub(super) enum NotOne {
/// They don't say where they are in it (no `/Size` or `/P`, or ones that don't add up): read
/// as separate patches, as files that don't say are.
Unplaced,
/// They fill the grid but don't join up (corners or edge anchors that don't meet, or more
/// lines than are kept).
Unjoined,
}
#[derive(Debug, Default)]
pub(super) struct MeshReader {
space: String,
/// `/Type` (`Cartesian` in the files read).
kind: Option<String>,
/// `/Size [a b]`: the grid's patches along the first and the second index of `/P`.
size: Option<(i64, i64)>,
/// The current patch's `/P [i j]`.
at: Option<(i64, i64)>,
/// The patches read, in order, for [`Self::one_mesh`].
cells: Vec<Cell>,
corners: Vec<Vec<f64>>,
/// The patch's anchors between its corners (`/A`, `/S`): the edge, and a point and its two handles.
anchors: Vec<(usize, [f64; 6])>,
@@ -45,6 +78,9 @@ impl MeshReader {
let value = at.checked_sub(1).and_then(|i| vals.get(i));
match key.as_str() {
"CS" => self.space = value.and_then(V::text).unwrap_or_default(),
"Type" => self.kind = value.and_then(V::text),
"Size" => self.size = pair(value),
"P" => self.at = pair(value),
"N" => {
if let Some(V::Arr(a)) = value {
if self.corners.len() < 4 {
@@ -70,6 +106,12 @@ impl MeshReader {
"E" => {
let corners = std::mem::take(&mut self.corners);
let anchors = std::mem::take(&mut self.anchors);
let at = self.at.take();
if let Some(c) = self.read_corners(&corners, to_doc)
&& self.cells.len() < MAX_PATCHES
{
self.cells.push(Cell { at, corners: c, anchors: anchors.clone() });
}
match self.patch(&corners, &anchors, to_doc) {
Some(m) if self.patches.len() < MAX_PATCHES && self.points + m.points.len() <= MAX_POINTS => {
self.points += m.points.len();
@@ -92,7 +134,8 @@ impl MeshReader {
})
}
fn patch(&self, corners: &[Vec<f64>], anchors: &[(usize, [f64; 6])], to_doc: Affine) -> Option<GradientMesh> {
/// The four corners of a patch, in the document.
fn read_corners(&self, corners: &[Vec<f64>], to_doc: Affine) -> Option<[Corner; 4]> {
let [n0, n1, n2, n3] = corners else { return None };
// (anchor, towards the next corner, towards the previous one, colour, opacity)
let read = |n: &Vec<f64>| -> Option<(Point, Vec2, Vec2, Color, f32)> {
@@ -121,9 +164,13 @@ impl MeshReader {
let color = self.color(n.get(..comps)?)?;
Some((at, to_doc * Point::new(nx, ny) - at, to_doc * Point::new(px, py) - at, color, opacity.clamp(0.0, 1.0) as f32))
};
let (a, b, c, d) = (read(n0)?, read(n1)?, read(n2)?, read(n3)?);
Some([read(n0)?, read(n1)?, read(n2)?, read(n3)?])
}
fn patch(&self, corners: &[Vec<f64>], anchors: &[(usize, [f64; 6])], to_doc: Affine) -> Option<GradientMesh> {
let [a, b, c, d] = self.read_corners(corners, to_doc)?;
if !anchors.is_empty() {
return Some(coons([a, b, c, d], anchors));
return Some(coons([a, b, c, d], anchors, None));
}
// Rows run from the first corner to the second, columns from the first to the fourth.
let point = |(p, _, _, color, opacity): (Point, Vec2, Vec2, Color, f32), handles: [(usize, Vec2); 2]| {
@@ -145,13 +192,150 @@ impl MeshReader {
}
}
impl MeshReader {
/// The mesh as one grid of shared points: each patch placed by its `/P` in a grid of `/Size`,
/// the corners neighbours share joined, and where a patch's edge bends through anchors, a mesh
/// line through the whole row or column there (every patch in it cut at the same place), so
/// each cell's edges stay single cubics. In each patch, rows run from its first corner to its
/// second and columns from its first to its fourth; which index of `/P` is which is taken from
/// the one under which the shared corners meet.
pub fn one_mesh(&self) -> Result<GradientMesh, NotOne> {
if self.unread || self.kind.as_deref().is_some_and(|k| k != "Cartesian") {
return Err(NotOne::Unplaced);
}
let Some((a, b)) = self.size else { return Err(NotOne::Unplaced) };
let side = |n: i64| usize::try_from(n).ok().filter(|n| (1..=MAX_SIDE).contains(n));
let (Some(a), Some(b)) = (side(a), side(b)) else { return Err(NotOne::Unplaced) };
if self.cells.len() != a * b {
return Err(NotOne::Unplaced);
}
// Each patch's place, once each.
let mut seen = vec![false; a * b];
let mut places = Vec::with_capacity(self.cells.len());
for cell in &self.cells {
let Some((i, j)) = cell.at else { return Err(NotOne::Unplaced) };
let (Ok(i), Ok(j)) = (usize::try_from(i), usize::try_from(j)) else { return Err(NotOne::Unplaced) };
if i >= a || j >= b {
return Err(NotOne::Unplaced);
}
let Some(slot) = seen.get_mut(i * b + j) else { return Err(NotOne::Unplaced) };
if std::mem::replace(slot, true) {
return Err(NotOne::Unplaced);
}
places.push((i, j));
}
// The first index along the columns (`/P [i j]` at row j, column i), or along the rows.
for across in [true, false] {
let (rows, cols) = if across { (b, a) } else { (a, b) };
let at: Vec<(usize, usize)> = places.iter().map(|&(i, j)| if across { (j, i) } else { (i, j) }).collect();
if let Some(mesh) = stitch(&self.cells, &at, rows, cols) {
return Ok(mesh);
}
}
Err(NotOne::Unjoined)
}
}
/// The cuts (0 to 1) of a patch's columns and rows: where the anchors on its edges along each
/// direction cut it, the pieces of an edge taking equal shares (as [`coons`] lays them out).
fn own_cuts(cell: &Cell) -> (Vec<f64>, Vec<f64>) {
let pieces = |e: usize| cell.anchors.iter().filter(|(edge, ..)| *edge == e).count() + 1;
let cuts = |n: usize| (0..=n).map(|i| i as f64 / n as f64).collect::<Vec<_>>();
// Columns along the second and fourth edges (u), rows along the first and third (v).
let merge = |x: Vec<f64>, y: Vec<f64>| union(&x, &y);
(merge(cuts(pieces(1)), cuts(pieces(3))), merge(cuts(pieces(0)), cuts(pieces(2))))
}
/// The sorted union of two lists of cuts.
fn union(a: &[f64], b: &[f64]) -> Vec<f64> {
let mut c: Vec<f64> = a.iter().chain(b).copied().filter(|v| v.is_finite()).collect();
c.sort_by(f64::total_cmp);
c.dedup_by(|x, y| (*x - *y).abs() < 1e-9);
c
}
/// The patches `cells`, each at `at` (row, column) of a `rows` × `cols` grid, as one mesh; `None`
/// when their shared points don't meet, a value isn't finite, or the mesh would be too large.
fn stitch(cells: &[Cell], at: &[(usize, usize)], rows: usize, cols: usize) -> Option<GradientMesh> {
// The cuts each row and column of patches takes: those of every patch in it.
let (mut col_cuts, mut row_cuts) = (vec![vec![0.0, 1.0]; cols], vec![vec![0.0, 1.0]; rows]);
for (cell, &(r, c)) in cells.iter().zip(at) {
let (us, vs) = own_cuts(cell);
let cc = col_cuts.get_mut(c)?;
*cc = union(cc, &us);
let rc = row_cuts.get_mut(r)?;
*rc = union(rc, &vs);
}
// Where each row and column of patches starts among the mesh's lines.
let starts = |cuts: &[Vec<f64>]| -> Vec<usize> {
cuts.iter()
.scan(0usize, |n, c| {
let s = *n;
*n += c.len().saturating_sub(1);
Some(s)
})
.collect()
};
let (col_at, row_at) = (starts(&col_cuts), starts(&row_cuts));
let total = |cuts: &[Vec<f64>]| cuts.iter().map(|c| c.len().saturating_sub(1)).sum::<usize>();
let (mesh_cols, mesh_rows) = (total(&col_cuts), total(&row_cuts));
if mesh_cols == 0 || mesh_rows == 0 || mesh_cols > MAX_LINES || mesh_rows > MAX_LINES || (mesh_rows + 1) * (mesh_cols + 1) > MAX_POINTS {
return None;
}
let width = mesh_cols + 1;
let mut points: Vec<Option<MeshPoint>> = vec![None; (mesh_rows + 1) * width];
for (cell, &(r, c)) in cells.iter().zip(at) {
let (us, vs) = (col_cuts.get(c)?, row_cuts.get(r)?);
let part = coons(cell.corners, &cell.anchors, Some((us, vs)));
let part_width = part.cols as usize + 1;
for (k, q) in part.points.into_iter().enumerate() {
let (pr, pc) = (k / part_width, k % part_width);
let slot = points.get_mut((row_at.get(r)? + pr) * width + col_at.get(c)? + pc)?;
match slot {
None => *slot = Some(q),
Some(had) => {
// A point two patches share: where both have it.
if (had.p - q.p).hypot() > 1e-6 * (1.0 + had.p.to_vec2().hypot()) {
return None;
}
// Its handles towards each side come from the patch on that side.
for (h, n) in had.handles.iter_mut().zip(q.handles) {
if h.hypot() == 0.0 {
*h = n;
}
}
}
}
}
}
let points: Vec<MeshPoint> = points.into_iter().collect::<Option<_>>()?;
let finite = |v: f64| v.is_finite();
if !points.iter().all(|q| finite(q.p.x) && finite(q.p.y) && q.opacity.is_finite() && q.handles.iter().all(|h| finite(h.x) && finite(h.y))) {
return None;
}
Some(GradientMesh { rows: mesh_rows as u32, cols: mesh_cols as u32, points })
}
/// A two-number value (`[a b]`) as whole numbers.
fn pair(v: Option<&V>) -> Option<(i64, i64)> {
let V::Arr(a) = v? else { return None };
let n: Vec<f64> = a.iter().filter_map(V::num).collect();
match n.as_slice() {
&[x, y] if x.fract() == 0.0 && y.fract() == 0.0 && x.abs() < 1e6 && y.abs() < 1e6 => Some((x as i64, y as i64)),
_ => None,
}
}
type Corner = (Point, Vec2, Vec2, Color, f32);
type Cubic = [Point; 4];
/// A patch whose edges bend through more anchors: its edges (first corner → second → third → fourth
/// → first) as runs of cubics, each anchor on the edge it lies along, and the Coons patch they bound
/// as a mesh cut wherever an edge has an anchor, so each cell's edges are single cubics again.
fn coons(corners: [Corner; 4], anchors: &[(usize, [f64; 6])]) -> GradientMesh {
///
/// `cuts`: the columns and rows (0 to 1) to cut it at instead of its own, which they include: those
/// of the mesh's whole column and row of patches ([`stitch`]).
fn coons(corners: [Corner; 4], anchors: &[(usize, [f64; 6])], cuts_at: Option<(&[f64], &[f64])>) -> GradientMesh {
let runs: Vec<Vec<Cubic>> = (0..4)
.map(|e| {
let (p, q) = (&corners[e], &corners[(e + 1) % 4]);
@@ -202,7 +386,10 @@ fn coons(corners: [Corner; 4], anchors: &[(usize, [f64; 6])]) -> GradientMesh {
c.dedup_by(|x, y| (*x - *y).abs() < 1e-9);
c
};
let (us, vs) = (cuts(&top, &bottom), cuts(&left, &right));
let (us, vs) = match cuts_at {
Some((us, vs)) => (us.to_vec(), vs.to_vec()),
None => (cuts(&top, &bottom), cuts(&left, &right)),
};
let (ra, rb, rc, rd) = (&corners[0], &corners[1], &corners[2], &corners[3]);
let mut points = Vec::with_capacity(us.len() * vs.len());
for (r, &v) in vs.iter().enumerate() {
@@ -376,4 +563,167 @@ mod tests {
}
assert!(reader.unread && reader.corners.len() == 4);
}
/// Hand-written grid of grey patches: `/Type`, `/Size [cols rows]` and each patch at `/P [x y]`
/// (the first index along x, as the files read write it), its corners from the one nearest the
/// origin up, across and down (`a b c d`), straight edges with handles a third of the way, the
/// grey of a point `at(x, y)`. `moved` puts the points listed elsewhere; `anchor` adds an anchor
/// to the edges at x = 10 (between the first two columns), at height 5 and 1 to the right.
struct Grid {
cols: usize,
rows: usize,
moved: Vec<((f64, f64), (f64, f64))>,
anchor: bool,
place: bool,
}
impl Grid {
fn new(cols: usize, rows: usize) -> Self {
Grid { cols, rows, moved: vec![], anchor: false, place: true }
}
fn at(&self, x: f64, y: f64) -> (f64, f64) {
self.moved.iter().find(|(from, _)| *from == (x, y)).map_or((x, y), |(_, to)| *to)
}
fn read(&self, size: [f64; 2]) -> MeshReader {
let mut r = MeshReader::default();
let name = |n: &str| V::Name(n.into());
let arr = |v: &[f64]| V::Arr(v.iter().map(|x| V::Num(*x)).collect());
r.entry(&[name("DeviceGray"), name("CS")], Affine::IDENTITY);
r.entry(&[name("Cartesian"), name("Type")], Affine::IDENTITY);
r.entry(&[arr(&size), name("Size")], Affine::IDENTITY);
for i in 0..self.cols {
for j in 0..self.rows {
let (x0, y0, x1, y1) = (i as f64 * 10.0, j as f64 * 10.0, (i + 1) as f64 * 10.0, (j + 1) as f64 * 10.0);
if self.place {
r.entry(&[arr(&[i as f64, j as f64]), name("P")], Affine::IDENTITY);
}
let third = |p: (f64, f64), q: (f64, f64)| (p.0 + (q.0 - p.0) / 3.0, p.1 + (q.1 - p.1) / 3.0);
let (a, b, c, d) = (self.at(x0, y0), self.at(x0, y1), self.at(x1, y1), self.at(x1, y0));
let grey = |p: (f64, f64)| (p.0 + p.1) / 40.0;
for (k, (p, next, prev)) in [(a, b, d), (b, c, a), (c, d, b), (d, a, c)].into_iter().enumerate() {
let (n, q) = (third(p, next), third(p, prev));
r.entry(&[arr(&[grey(p), 1.0, p.0, p.1, n.0, n.1, 1.0, q.0, q.1, 1.0]), name("N")], Affine::IDENTITY);
// The edge at x = 10: the third edge (c → d) of the first column, the first
// (a → b) of the second; listed upwards either way.
let on_shared = (k == 2 && i == 0) || (k == 0 && i == 1);
if self.anchor && on_shared {
r.entry(&[arr(&[11.0, 5.0, 11.0, 3.0, 11.0, 7.0, 1.0]), name("A")], Affine::IDENTITY);
}
}
r.entry(&[V::Num(0.0), name("E")], Affine::IDENTITY);
}
}
r
}
}
/// A 2 × 2 mesh comes in as one mesh of 9 shared points, each where the patches put it, with
/// the colours the separate patches had at every place.
#[test]
fn a_grid_of_patches_is_one_mesh() {
let g = Grid::new(2, 2);
let r = g.read([2.0, 2.0]);
let mesh = r.one_mesh().unwrap();
assert_eq!((mesh.rows, mesh.cols, mesh.points.len()), (2, 2, 9));
for row in 0..=2 {
for col in 0..=2 {
let p = mesh.points[mesh.idx(row, col)].p;
assert!((p - Point::new(col as f64 * 10.0, row as f64 * 10.0)).hypot() < 1e-9, "({row}, {col}) at {p:?}");
}
}
// The colour across each cell is that of the patch it was (patches are read x first).
for (k, patch) in r.patches.iter().enumerate() {
let (col, row) = (k / 2, k % 2);
for (u, v) in [(0.25, 0.25), (0.5, 0.5), (0.9, 0.1)] {
let (one, _) = mesh.color_at(row, col, u, v);
let (own, _) = patch.color_at(0, 0, u, v);
assert!(
one.to_rgb().iter().zip(own.to_rgb()).all(|(a, b)| (a - b).abs() < 1e-5),
"cell ({row}, {col}) at {u} {v}: {one:?} vs {own:?}"
);
}
}
// Inner handles point at the neighbours, a third of the way.
let inner = &mesh.points[mesh.idx(1, 1)];
assert!((inner.handles[H_RIGHT] - Vec2::new(10.0 / 3.0, 0.0)).hypot() < 1e-9);
assert!((inner.handles[H_UP] - Vec2::new(0.0, -10.0 / 3.0)).hypot() < 1e-9);
}
/// A mesh point moved inside the grid is one point of the mesh, which the four cells round it
/// share: moving it moves them all, leaving no hole.
#[test]
fn a_moved_inner_point_stays_shared() {
let mut g = Grid::new(2, 2);
g.moved = vec![((10.0, 10.0), (13.0, 8.0))];
let mut mesh = g.read([2.0, 2.0]).one_mesh().unwrap();
let k = mesh.idx(1, 1);
assert!((mesh.points[k].p - Point::new(13.0, 8.0)).hypot() < 1e-9);
mesh.points[k].p = Point::new(15.0, 15.0);
for (r, c) in [(0, 0), (0, 1), (1, 0), (1, 1)] {
let corner = mesh.eval(r, c, 1.0 - c as f64, 1.0 - r as f64);
assert!((corner - Point::new(15.0, 15.0)).hypot() < 1e-9, "cell ({r}, {c}): {corner:?}");
}
}
/// An anchor on the edge two patches share runs a mesh line through the whole mesh there: one
/// mesh, three points across that edge, the anchor's the middle one, its handles along the edge.
#[test]
fn an_anchor_on_a_shared_edge_adds_a_line_through_the_mesh() {
let mut g = Grid::new(2, 1);
g.anchor = true;
let r = g.read([2.0, 1.0]);
let mesh = r.one_mesh().unwrap();
assert_eq!((mesh.rows, mesh.cols), (2, 2), "split at the anchor's height");
let mid = &mesh.points[mesh.idx(1, 1)];
assert!((mid.p - Point::new(11.0, 5.0)).hypot() < 1e-9, "{:?}", mid.p);
assert!(
(mid.handles[H_UP] - Vec2::new(0.0, -2.0)).hypot() < 1e-9 && (mid.handles[H_DOWN] - Vec2::new(0.0, 2.0)).hypot() < 1e-9,
"{:?}",
mid.handles
);
// The patch without an anchor of its own is cut on the same line, on its straight edge.
let left = mesh.points[mesh.idx(1, 0)].p;
assert!((left - Point::new(0.0, 5.0)).hypot() < 1e-9, "{left:?}");
}
/// A patch whose first corner and the anchors on its edges sit on one point (no handles)
/// comes in, every value a number.
#[test]
fn a_collapsed_patch_comes_in_with_finite_values() {
let mut r = MeshReader::default();
let name = |n: &str| V::Name(n.into());
let arr = |v: &[f64]| V::Arr(v.iter().map(|x| V::Num(*x)).collect());
r.entry(&[name("DeviceGray"), name("CS")], Affine::IDENTITY);
r.entry(&[arr(&[1.0, 1.0]), name("Size")], Affine::IDENTITY);
r.entry(&[arr(&[0.0, 0.0]), name("P")], Affine::IDENTITY);
// An anchor on the edge back to the first corner, before it; then the corner on that point.
r.entry(&[arr(&[5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 1.0]), name("A")], Affine::IDENTITY);
r.entry(&[arr(&[0.5, 1.0, 5.0, 5.0, 5.0, 5.0, 1.0, 5.0, 5.0, 1.0]), name("N")], Affine::IDENTITY);
r.entry(&[arr(&[5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 1.0]), name("A")], Affine::IDENTITY);
for (x, y) in [(5.0, 10.0), (10.0, 10.0), (10.0, 5.0)] {
r.entry(&[arr(&[0.2, 1.0, x, y, x, y, 1.0, x, y, 1.0]), name("N")], Affine::IDENTITY);
}
r.entry(&[V::Num(0.0), name("E")], Affine::IDENTITY);
let mesh = r.one_mesh().unwrap();
assert!(mesh.points.iter().all(|q| q.p.x.is_finite() && q.p.y.is_finite() && q.handles.iter().all(|h| h.x.is_finite() && h.y.is_finite())));
}
/// Patches that don't say where they are, or not as many as the grid has, are read as before:
/// each a mesh of its own. Ones that fill the grid but don't meet say so.
#[test]
fn unplaced_or_unjoined_patches_stay_separate() {
let mut g = Grid::new(2, 2);
g.place = false;
assert_eq!(g.read([2.0, 2.0]).one_mesh().unwrap_err(), NotOne::Unplaced);
let g = Grid::new(2, 2);
assert_eq!(g.read([3.0, 2.0]).one_mesh().unwrap_err(), NotOne::Unplaced);
assert_eq!(g.read([f64::NAN, 2.0]).one_mesh().unwrap_err(), NotOne::Unplaced);
assert_eq!(g.read([2.0, 2.0]).patches.len(), 4, "the separate patches are still read");
// A corner that only one of the patches sharing it moved.
let mut r = Grid::new(2, 1).read([2.0, 1.0]);
if let Some(c) = r.cells.get_mut(1) {
c.corners[0].0 = Point::new(10.0, 1.0);
}
assert_eq!(r.one_mesh().unwrap_err(), NotOne::Unjoined);
}
}
+12
View File
@@ -2009,6 +2009,18 @@ impl<'a> Reader<'a> {
return Ok(());
}
let gs = self.gs.clone();
// One mesh where the patches join up into the grid they say they make; else each patch is
// a mesh of its own, in a group.
match m.one_mesh() {
Ok(one) => {
let mut node = self.new_node(NodeKind::Mesh(one), &gs);
node.opacity = gs.opacity;
node.blend = gs.blend;
return self.add(node, hidden);
}
Err(super::mesh::NotOne::Unjoined) => self.warn("a gradient mesh whose patches don't join up comes in as a group of its patches"),
Err(super::mesh::NotOne::Unplaced) => {}
}
let mut patches: Vec<Node> = m.patches.into_iter().map(|p| self.new_node(NodeKind::Mesh(p), &gs)).collect();
let mut node = match patches.len() {
1 => patches.pop().unwrap_or_else(|| self.new_node(NodeKind::Group { children: vec![], clip: false }, &gs)),