diff --git a/crates/eps/src/import/ai/mesh.rs b/crates/eps/src/import/ai/mesh.rs index 2bdabaab1..8e4fa9622 100644 --- a/crates/eps/src/import/ai/mesh.rs +++ b/crates/eps/src/import/ai/mesh.rs @@ -3,7 +3,12 @@ //! the origin round to the one beside it), ended by `/E` — and `/End X!`. //! //! A corner is `[colour… opacity x y` then the handle towards the next corner, a weight, the handle -//! towards the previous one and a weight `]`, in art space. Each patch becomes a 1×1 mesh. +//! towards the previous one and a weight `]`, in art space. A patch whose edges bend through more +//! anchors has them as `[x y` with a handle towards each neighbor and a weight `] /A` or `/S`, +//! after the corner their edge starts from (before the first corner for the edge back to it), in +//! the edge's direction (`/A`) or against it (`/S`). A patch with single cubic edges becomes a 1×1 +//! mesh; one with more anchors, the Coons patch of its edges, cut into cells where any edge has an +//! anchor. //! //! `X!`/`X#` aren't publicly documented (the AI7 specification predates meshes). @@ -20,6 +25,9 @@ const MAX_PATCHES: usize = 4096; pub(super) struct MeshReader { space: String, corners: Vec>, + /// The patch's anchors between its corners (`/A`, `/S`): the edge, whether listed against it, and + /// a point and its two handles. + anchors: Vec<(usize, bool, [f64; 6])>, pub patches: Vec, /// Something in it this doesn't read. pub unread: bool, @@ -42,9 +50,23 @@ impl MeshReader { } } } + "A" | "S" => { + if let Some(V::Arr(a)) = value { + let n: Vec = a.iter().filter_map(V::num).take(7).collect(); + match n.as_slice() { + &[x, y, ax, ay, bx, by, ..] if self.anchors.len() < 64 && [x, y, ax, ay, bx, by].iter().all(|v| v.is_finite()) => { + let p = |x, y| to_doc * Point::new(x, y); + let (at, a, b) = (p(x, y), p(ax, ay), p(bx, by)); + self.anchors.push(((self.corners.len() + 3) % 4, key == "S", [at.x, at.y, a.x, a.y, b.x, b.y])); + } + _ => self.unread = true, + } + } + } "E" => { let corners = std::mem::take(&mut self.corners); - match self.patch(&corners, to_doc) { + let anchors = std::mem::take(&mut self.anchors); + match self.patch(&corners, &anchors, to_doc) { Some(m) if self.patches.len() < MAX_PATCHES => self.patches.push(m), _ => self.unread = true, } @@ -63,7 +85,7 @@ impl MeshReader { }) } - fn patch(&self, corners: &[Vec], to_doc: Affine) -> Option { + fn patch(&self, corners: &[Vec], anchors: &[(usize, bool, [f64; 6])], to_doc: Affine) -> Option { 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)> { @@ -77,6 +99,9 @@ impl MeshReader { 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)?); + if !anchors.is_empty() { + return Some(coons([a, b, c, d], anchors)); + } // 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]| { let mut h = [Vec2::ZERO; 4]; @@ -97,6 +122,143 @@ impl MeshReader { } } +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, bool, [f64; 6])]) -> GradientMesh { + let runs: Vec> = (0..4) + .map(|e| { + let (p, q) = (&corners[e], &corners[(e + 1) % 4]); + // Each anchor's first handle points to the one listed before it, so back along the edge + // for `/A`, forward for `/S`, which lists them against the edge. + let mut stops: Vec<(Point, Point, Point)> = anchors + .iter() + .filter(|(edge, ..)| *edge == e) + .map(|&(_, against, [x, y, ax, ay, bx, by])| { + let (at, h1, h2) = (Point::new(x, y), Point::new(ax, ay), Point::new(bx, by)); + if against { (h2, at, h1) } else { (h1, at, h2) } + }) + .collect(); + if anchors.iter().any(|&(edge, against, _)| edge == e && against) { + stops.reverse(); + } + let (mut from, mut out) = (p.0, p.0 + p.1); + let mut run = vec![]; + for (back, at, ahead) in stops { + run.push([from, out, back, at]); + (from, out) = (at, ahead); + } + run.push([from, out, q.0 + q.2, q.0]); + run + }) + .collect(); + // The patch is over u (first corner → fourth) and v (first → second). + let [left, bottom, right, top] = [ + Path { run: runs[0].clone(), reversed: false }, + Path { run: runs[1].clone(), reversed: false }, + Path { run: runs[2].clone(), reversed: true }, + Path { run: runs[3].clone(), reversed: true }, + ]; + let [ca, cb, cc, cd] = corners.map(|c| c.0.to_vec2()); + let surface = |u: f64, v: f64| -> Vec2 { + top.at(u) * (1.0 - v) + bottom.at(u) * v + left.at(v) * (1.0 - u) + right.at(v) * u + - (ca * ((1.0 - u) * (1.0 - v)) + cd * (u * (1.0 - v)) + cb * ((1.0 - u) * v) + cc * (u * v)) + }; + let du = |u: f64, v: f64, after: bool| -> Vec2 { + top.slope(u, after) * (1.0 - v) + bottom.slope(u, after) * v - left.at(v) + right.at(v) - (cd - ca) * (1.0 - v) - (cc - cb) * v + }; + let dv = |u: f64, v: f64, after: bool| -> Vec2 { + left.slope(v, after) * (1.0 - u) + right.slope(v, after) * u - top.at(u) + bottom.at(u) - (cb - ca) * (1.0 - u) - (cc - cd) * u + }; + let cuts = |a: &Path, b: &Path| { + let mut c: Vec = a.cuts().into_iter().chain(b.cuts()).collect(); + c.sort_by(f64::total_cmp); + c.dedup_by(|x, y| (*x - *y).abs() < 1e-9); + c + }; + let (us, vs) = (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() { + for (c, &u) in us.iter().enumerate() { + let mut h = [Vec2::ZERO; 4]; + if let Some(&next) = us.get(c + 1) { + h[H_RIGHT] = du(u, v, true) * ((next - u) / 3.0); + } + if let Some(&prev) = c.checked_sub(1).and_then(|i| us.get(i)) { + h[H_LEFT] = du(u, v, false) * (-(u - prev) / 3.0); + } + if let Some(&next) = vs.get(r + 1) { + h[H_DOWN] = dv(u, v, true) * ((next - v) / 3.0); + } + if let Some(&prev) = r.checked_sub(1).and_then(|i| vs.get(i)) { + h[H_UP] = dv(u, v, false) * (-(v - prev) / 3.0); + } + let (u32_, v32) = (u as f32, v as f32); + let color = mix(&mix(&ra.3, &rd.3, u32_), &mix(&rb.3, &rc.3, u32_), v32); + let opacity = (ra.4 * (1.0 - u32_) + rd.4 * u32_) * (1.0 - v32) + (rb.4 * (1.0 - u32_) + rc.4 * u32_) * v32; + points.push(MeshPoint { p: surface(u, v).to_point(), color, opacity, handles: h }); + } + } + GradientMesh { rows: (vs.len() - 1) as u32, cols: (us.len() - 1) as u32, points } +} + +/// An edge as a run of cubics, each taking an equal share of its parameter, read backwards if +/// `reversed`. +struct Path { + run: Vec, + reversed: bool, +} + +impl Path { + fn piece(&self, t: f64, after: bool) -> (usize, f64) { + let n = self.run.len(); + let t = if self.reversed { 1.0 - t } else { t }; + // At a joint, the piece the moving direction enters. + let towards_end = after != self.reversed; + let scaled = t * n as f64; + let mut i = if towards_end { scaled.floor() } else { scaled.ceil() - 1.0 }.clamp(0.0, (n - 1) as f64) as usize; + if i >= n { + i = n - 1; + } + (i, scaled - i as f64) + } + fn at(&self, t: f64) -> Vec2 { + let (i, s) = self.piece(t, true); + let [p0, p1, p2, p3] = self.run[i].map(|p| p.to_vec2()); + let r = 1.0 - s; + p0 * (r * r * r) + p1 * (3.0 * r * r * s) + p2 * (3.0 * r * s * s) + p3 * (s * s * s) + } + /// The derivative along the patch's parameter, from the piece `after` (towards larger `t`) or before. + fn slope(&self, t: f64, after: bool) -> Vec2 { + let (i, s) = self.piece(t, after); + let [p0, p1, p2, p3] = self.run[i].map(|p| p.to_vec2()); + let r = 1.0 - s; + let d = (p1 - p0) * (3.0 * r * r) + (p2 - p1) * (6.0 * r * s) + (p3 - p2) * (3.0 * s * s); + let scale = self.run.len() as f64; + if self.reversed { d * -scale } else { d * scale } + } + /// Where its pieces meet, with its ends, in the patch's parameter. + fn cuts(&self) -> Vec { + let n = self.run.len(); + (0..=n).map(|i| i as f64 / n as f64).collect() + } +} + +/// `a` towards `b` by `t`, in their own colour model where they share one. +fn mix(a: &Color, b: &Color, t: f32) -> Color { + let l = |x: f32, y: f32| x + (y - x) * t; + match (a, b) { + (Color::Rgb { r, g, b: bl }, Color::Rgb { r: r2, g: g2, b: b2 }) => Color::rgb(l(*r, *r2), l(*g, *g2), l(*bl, *b2)), + (Color::Cmyk { c, m, y, k }, Color::Cmyk { c: c2, m: m2, y: y2, k: k2 }) => Color::cmyk(l(*c, *c2), l(*m, *m2), l(*y, *y2), l(*k, *k2)), + (Color::Gray { k }, Color::Gray { k: k2 }) => Color::gray(l(*k, *k2)), + _ => a.lerp(b, t), + } +} + #[cfg(test)] mod tests { use super::*; @@ -113,6 +275,32 @@ mod tests { reader.entry(&[V::Num(0.0), V::Name("E".into())], Affine::IDENTITY); } + /// An anchor on a patch's edge (`/A` after the first corner: on the edge from it to the second) + /// bends the edge through it: the patch is cut there into two cells whose shared corner is the + /// anchor, with the anchor's handles along the edge. + #[test] + fn an_edge_anchor_cuts_its_patch_there() { + let mut reader = MeshReader { space: "DeviceGray".into(), ..MeshReader::default() }; + let corner = |x: f64, y: f64, next: (f64, f64), prev: (f64, f64)| { + vec![V::Arr([0.0, 1.0, x, y, next.0, next.1, 1.0, prev.0, prev.1, 1.0].map(V::Num).to_vec()), V::Name("N".into())] + }; + reader.entry(&corner(0.0, 0.0, (0.0, 2.0), (2.0, 0.0)), Affine::IDENTITY); + reader.entry(&[V::Arr([-1.0, 5.0, -1.0, 4.0, -1.0, 6.0, 1.0].map(V::Num).to_vec()), V::Name("A".into())], Affine::IDENTITY); + reader.entry(&corner(0.0, 10.0, (2.0, 10.0), (0.0, 8.0)), Affine::IDENTITY); + reader.entry(&corner(10.0, 10.0, (10.0, 8.0), (8.0, 10.0)), Affine::IDENTITY); + reader.entry(&corner(10.0, 0.0, (8.0, 0.0), (10.0, 2.0)), Affine::IDENTITY); + reader.entry(&[V::Num(0.0), V::Name("E".into())], Affine::IDENTITY); + assert!(!reader.unread); + let mesh = &reader.patches[0]; + assert_eq!((mesh.rows, mesh.cols), (2, 1)); + let left = &mesh.points[2]; + assert!((left.p - Point::new(-1.0, 5.0)).hypot() < 1e-9, "{:?}", left.p); + assert!((left.handles[H_UP] - Vec2::new(0.0, -1.0)).hypot() < 1e-9 && (left.handles[H_DOWN] - Vec2::new(0.0, 1.0)).hypot() < 1e-9); + // The edge still starts and ends at its corners, with their handles. + assert!((mesh.points[0].handles[H_DOWN] - Vec2::new(0.0, 2.0)).hypot() < 1e-9); + assert!((mesh.points[4].p - Point::new(0.0, 10.0)).hypot() < 1e-9); + } + #[test] fn hostile_meshes_are_capped_and_checked() { let mut reader = MeshReader { space: "DeviceGray".into(), ..MeshReader::default() };