From 7504a0e2e45889545159a7257acb010895ff5b2b Mon Sep 17 00:00:00 2001 From: jmverges Date: Tue, 10 Nov 2026 19:01:54 +0000 Subject: [PATCH] .ai import: an envelope mesh bent far round reads the way it runs An envelope's mesh is turned upright by which way its rows and columns run. That was read from the first row alone, so a warp bent past halfway (its end columns leaning more than 45 degrees) read as turned round and mapped its contents across instead of along. Read it from the first and last rows and columns added up instead. Co-Authored-By: Claude Opus 5.5 --- crates/eps/src/import/ai/envelope.rs | 20 +++++++++--- crates/eps/src/import/ai/tests.rs | 46 ++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 5 deletions(-) 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]