diff --git a/crates/eps/src/import/ai/envelope.rs b/crates/eps/src/import/ai/envelope.rs index 461be66fc..a3396a251 100644 --- a/crates/eps/src/import/ai/envelope.rs +++ b/crates/eps/src/import/ai/envelope.rs @@ -208,19 +208,38 @@ 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)); - // 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. + // How far the grid reaches: a run much shorter than that is rounding, not a direction. + let reach = { + let first = pts.first().map_or(Point::ZERO, |q| q.0); + let bounds = pts.iter().fold(Rect::from_points(first, first), |b, q| b.union_pt(q.0)); + bounds.width().hypot(bounds.height()) * 1e-6 + }; + // How the grid runs along its rows (every row end to end, added up) and down its columns (every + // column): read from all of them, a grid bent far round (its end columns leaning more than + // halfway over) still reads the way it runs. Each sum comes with how long its parts are: where + // they nearly cancel out, as in a ring, what is left is rounding, not a direction, and the grid + // reads by its first row or column instead. 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) + let (mut along, mut down, mut along_len, mut down_len) = (Vec2::ZERO, Vec2::ZERO, 0.0, 0.0); + for r in 0..=rows { + let v = at(pts, cols, r, cols) - at(pts, cols, r, 0); + (along, along_len) = (along + v, along_len + v.hypot()); + } + for c in 0..=cols { + let v = at(pts, cols, rows, c) - at(pts, cols, 0, c); + (down, down_len) = (down + v, down_len + v.hypot()); + } + let clear = |v: Vec2, len: f64| (v.hypot() >= len * 0.01 && v.hypot() > reach).then_some(v); + let first_row = at(pts, cols, 0, cols) - at(pts, cols, 0, 0); + let first_col = at(pts, cols, rows, 0) - at(pts, cols, 0, 0); + (clear(along, along_len).unwrap_or(first_row), clear(down, down_len).unwrap_or(first_col)) }; // 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. + // Columns that run across the page more than rows do: the grid goes the other way round. Nearly + // as much (a grid turned an eighth round) keeps the file's way round. let (along, down) = runs(&pts, rows, cols); - if level(down) > level(along) { + if level(down) > level(along) + 1e-3 { let mut t = Vec::with_capacity(len); for r in 0..=cols { for c in 0..=rows { diff --git a/crates/eps/src/import/ai/tests.rs b/crates/eps/src/import/ai/tests.rs index 039f8e1a8..7b208caf3 100644 --- a/crates/eps/src/import/ai/tests.rs +++ b/crates/eps/src/import/ai/tests.rs @@ -7547,6 +7547,55 @@ fn a_mesh_envelope_bent_far_round_opens_the_way_it_runs() { assert!(r.warnings.is_empty(), "{:?}", r.warnings); } +/// A mesh envelope bent halfway round, its grid running down an arc from top to bottom so that its +/// end rows cancel out, opens the same way round wherever it sits on the page and whichever way +/// round the file writes it. +#[test] +fn a_mesh_envelope_bent_halfway_round_opens_the_same_way_wherever_it_is() { + use vectorcraft_doc::live::{EnvelopeKind, H_DOWN, H_LEFT, H_RIGHT, H_UP}; + for cx in [100.0, 100.1, 37.0, 250.3, 12.7] { + // 4 rows down an arc round (cx, 0) from 90 to 270 degrees, columns at radius 90 and 60. + let at = |(i, j): (usize, usize)| { + let (a, r) = ((90.0 + i as f64 * 45.0_f64).to_radians(), 60.0 + j as f64 * 30.0); + (cx + 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) + }; + // Row r of the envelope is the arc's point r from the top, column c the radius 1 - c: the + // outer one is on the left. + let doc_pt = |(x, y): (f64, f64)| Point::new(x, 100.0 - y); + let (mut points, mut handles) = (vec![], vec![]); + for r in 0..=4usize { + for c in 0..=1usize { + let g = (r, 1 - c); + let p = doc_pt(at(g)); + points.push(p); + let towards = |q: Option<(usize, usize)>| q.filter(|q| q.0 <= 4 && q.1 <= 1).map_or(kurbo::Vec2::ZERO, |q| doc_pt(handle(g, q)) - p); + let mut h = [kurbo::Vec2::ZERO; 4]; + h[H_DOWN] = towards(Some((g.0 + 1, g.1))); + h[H_UP] = towards(g.0.checked_sub(1).map(|i| (i, g.1))); + h[H_RIGHT] = towards(g.1.checked_sub(1).map(|j| (g.0, j))); + h[H_LEFT] = towards(Some((g.0, g.1 + 1))); + handles.push(h); + } + } + let want = EnvelopeKind::Mesh { rows: 4, cols: 1, points, handles }; + let along = mesh_text(4, 1, &at, &handle, None); + let across = mesh_text(1, 4, &|(i, j)| at((j, i)), &|p, q| handle((p.1, p.0), (q.1, q.0)), None); + for shape in [along, across] { + 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!("at {cx}: {a:#?}") }; + assert_eq!((*rows, *cols), (4, 1), "at {cx}"); + let EnvelopeKind::Mesh { points: want, .. } = &want else { panic!("{want:?}") }; + assert!(got.iter().zip(want).all(|(p, q)| (*p - *q).hypot() < 1e-6), "at {cx}: {got:?}"); + } + } +} + /// 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]