.ai import: an envelope's grid reads its way round from every row and column

Summing only the end rows and columns let a grid symmetric about its
middle (a mesh bent halfway round) read by rounding, so it came out
turned or not depending on where it sat on the page. Every row and
column is now summed, a sum shorter than 1% of its parts (or than the
grid's reach) counts as no direction and falls back to the first row or
column, and the same comparison decides both cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jmverges
2026-11-11 03:06:24 +00:00
co-authored by Claude Opus 5.5
parent 8820a15e20
commit 198bb6dffd
2 changed files with 76 additions and 8 deletions
+27 -8
View File
@@ -208,19 +208,38 @@ fn upright(m: &GradientMesh) -> Option<(u32, u32, Vec<GridPoint>)> {
let mut pts: Vec<GridPoint> = 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 {
+49
View File
@@ -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]