diff --git a/crates/eps/src/import/ai/envelope.rs b/crates/eps/src/import/ai/envelope.rs index fe3ba51f4a..3a45bf3a16 100644 --- a/crates/eps/src/import/ai/envelope.rs +++ b/crates/eps/src/import/ai/envelope.rs @@ -189,9 +189,19 @@ fn upright(m: &GradientMesh) -> Option<(u32, u32, Vec)> { let mut pts: Vec = m.points.iter().map(|q| (q.p, q.handles)).collect(); let at = |pts: &[GridPoint], cols: u32, r: u32, c: u32| pts.get((r * (cols + 1) + c) as usize).map_or(Point::ZERO, |q| q.0); let remap = |h: [Vec2; 4], slots: [usize; 4]| slots.map(|s| h.get(s).copied().unwrap_or(Vec2::ZERO)); - // Columns that run down the page: the grid goes the other way round. - let along = at(&pts, cols, 0, cols) - at(&pts, cols, 0, 0); - if along.y.abs() > along.x.abs() { + // How the grid runs along its rows (its first and last rows, end to end added up) and down its + // columns (its first and last columns): read from both sides, a grid bent far round (its end + // columns leaning more than halfway over) still reads the way it runs. + let runs = |pts: &[GridPoint], rows: u32, cols: u32| { + let along = (at(pts, cols, 0, cols) - at(pts, cols, 0, 0)) + (at(pts, cols, rows, cols) - at(pts, cols, rows, 0)); + let down = (at(pts, cols, rows, 0) - at(pts, cols, 0, 0)) + (at(pts, cols, rows, cols) - at(pts, cols, 0, cols)); + (along, down) + }; + // How much of a run is across the page. + let level = |v: Vec2| if v.hypot() > 0.0 { v.x.abs() / v.hypot() } else { 0.0 }; + // Columns that run across the page more than rows do: the grid goes the other way round. + let (along, down) = runs(&pts, rows, cols); + if level(down) > level(along) { let mut t = Vec::with_capacity(len); for r in 0..=cols { for c in 0..=rows { @@ -205,7 +215,7 @@ fn upright(m: &GradientMesh) -> Option<(u32, u32, Vec)> { (rows, cols) = (cols, rows); pts = t; } - if (at(&pts, cols, 0, cols) - at(&pts, cols, 0, 0)).x < 0.0 { + if runs(&pts, rows, cols).0.x < 0.0 { for row in pts.chunks_mut(cols as usize + 1) { row.reverse(); for (_, h) in row.iter_mut() { @@ -213,7 +223,7 @@ fn upright(m: &GradientMesh) -> Option<(u32, u32, Vec)> { } } } - if (at(&pts, cols, rows, 0) - at(&pts, cols, 0, 0)).y < 0.0 { + if runs(&pts, rows, cols).1.y < 0.0 { let mut flipped: Vec = pts.chunks(cols as usize + 1).rev().flatten().copied().collect(); for (_, h) in &mut flipped { h.swap(H_DOWN, H_UP); diff --git a/crates/eps/src/import/ai/tests.rs b/crates/eps/src/import/ai/tests.rs index 2b127e275b..039f8e1a8a 100644 --- a/crates/eps/src/import/ai/tests.rs +++ b/crates/eps/src/import/ai/tests.rs @@ -7501,6 +7501,52 @@ fn a_mesh_envelope_opens_live_with_its_points_and_handles() { assert!(r.warnings.is_empty(), "{:?}", r.warnings); } +/// A mesh envelope bent far round, its end columns leaning more than halfway over (a warp's grid +/// bent into an arc), opens live the way it runs: its columns along the arc, as in the file. +#[test] +fn a_mesh_envelope_bent_far_round_opens_the_way_it_runs() { + use vectorcraft_doc::live::{EnvelopeKind, H_DOWN, H_LEFT, H_RIGHT, H_UP}; + // 3 columns along an arc round (100, 0) from 160 to 20 degrees, rows at radius 60 and 90: the + // end columns lean 70 degrees over. + let at = |(i, j): (usize, usize)| { + let (a, r) = ((160.0 - i as f64 * 140.0 / 3.0).to_radians(), 60.0 + j as f64 * 30.0); + (100.0 + r * a.cos(), r * a.sin()) + }; + let handle = |p: (usize, usize), q: (usize, usize)| { + let ((x, y), (u, v)) = (at(p), at(q)); + (x + (u - x) / 3.0, y + (v - y) / 3.0) + }; + // Written with its first index across the arc and its second along it, so that the grid reads + // turned round and has to be turned upright. + let shape = mesh_text(1, 3, &|(i, j)| at((j, i)), &|p, q| handle((p.1, p.0), (q.1, q.0)), None); + // Row r of the envelope is row 1 - r of the file (art space has y up). + let doc_pt = |(x, y): (f64, f64)| Point::new(x, 100.0 - y); + let (mut points, mut handles) = (vec![], vec![]); + for r in 0..=1usize { + for c in 0..=3usize { + let g = (c, 1 - r); + let p = doc_pt(at(g)); + points.push(p); + let towards = |q: Option<(usize, usize)>| q.filter(|q| q.0 <= 3 && q.1 <= 1).map_or(kurbo::Vec2::ZERO, |q| doc_pt(handle(g, q)) - p); + let mut h = [kurbo::Vec2::ZERO; 4]; + h[H_RIGHT] = towards(Some((g.0 + 1, g.1))); + h[H_LEFT] = towards(g.0.checked_sub(1).map(|i| (i, g.1))); + h[H_UP] = towards(Some((g.0, g.1 + 1))); + h[H_DOWN] = towards(g.1.checked_sub(1).map(|j| (g.0, j))); + handles.push(h); + } + } + let want = EnvelopeKind::Mesh { rows: 1, cols: 3, points, handles }; + let r = read_ok(&layer(&envelope_of(&shape, ENVELOPE_KEYS, &envelope_content(), 2, &drawn_content(&want)))); + let a = art(&r.doc); + assert_eq!(a.len(), 1, "{a:#?}"); + let EnvelopeKind::Mesh { rows, cols, points: got, .. } = live_envelope(&a[0]).1 else { panic!("{a:#?}") }; + assert_eq!((*rows, *cols), (1, 3)); + let EnvelopeKind::Mesh { points: want, .. } = &want else { panic!("{want:?}") }; + assert!(got.iter().zip(want).all(|(p, q)| (*p - *q).hypot() < 1e-6), "{got:?}"); + assert!(r.warnings.is_empty(), "{:?}", r.warnings); +} + /// An envelope made with a top object opens as a live envelope whose top path is the edge of its /// one-patch mesh, an anchor on the edge included; its contents stay the rectangle. #[test]