diff --git a/crates/eps/src/import/ai/mesh.rs b/crates/eps/src/import/ai/mesh.rs index 4866ffad1..b5a3ee574 100644 --- a/crates/eps/src/import/ai/mesh.rs +++ b/crates/eps/src/import/ai/mesh.rs @@ -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, + /// `/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, corners: Vec>, /// 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], anchors: &[(usize, [f64; 6])], to_doc: Affine) -> Option { + /// The four corners of a patch, in the document. + fn read_corners(&self, corners: &[Vec], 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| -> 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], anchors: &[(usize, [f64; 6])], to_doc: Affine) -> Option { + 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 { + 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, Vec) { + 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::>(); + // Columns along the second and fourth edges (u), rows along the first and third (v). + let merge = |x: Vec, y: Vec| 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 { + let mut c: Vec = 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 { + // 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]| -> Vec { + 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]| cuts.iter().map(|c| c.len().saturating_sub(1)).sum::(); + 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> = 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 = points.into_iter().collect::>()?; + 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 = 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> = (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); + } } diff --git a/crates/eps/src/import/ai/read.rs b/crates/eps/src/import/ai/read.rs index 7e362432c..f862fbe40 100644 --- a/crates/eps/src/import/ai/read.rs +++ b/crates/eps/src/import/ai/read.rs @@ -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 = 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)),