mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Merge pull request #3811 from ham9zah/fix/ai-gradient-mesh-one-grid
Import: an Illustrator gradient mesh comes in as one connected mesh
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)),
|
||||
|
||||
Reference in New Issue
Block a user