mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-01 05:37:23 -06:00
Fix #3592: a gradient mesh from a file is budgeted at about a million quads
A mesh read from a file can have any number of patches, and the view any zoom: a 150 x 150 mesh from a 3 MB file exported at 20x asked for about 23 million quads (2 GB), and the command's own cap of 50 rows and columns does not apply to a loaded one. One tessellation now makes at most 1,048,576 quads, the subdivision per patch giving way before that, and a mesh with more patches than that is malformed, like a point count that does not fit.
This commit is contained in:
committed by
bflatastic
parent
b13b3260cd
commit
a1a3a99104
@@ -181,6 +181,29 @@ mod tests {
|
||||
assert!((p.x - 37.0).abs() < 1e-6 && (p.y - 81.0).abs() < 1e-6, "{p:?}");
|
||||
}
|
||||
|
||||
/// #3592: a mesh read from a file can have any number of patches, and the view any zoom, so the
|
||||
/// quads one tessellation makes are budgeted: the subdivision per patch gives way before memory
|
||||
/// does, and each patch stays at least one quad.
|
||||
#[test]
|
||||
fn a_large_mesh_tessellates_within_the_quad_budget() {
|
||||
let mesh = |n: u32| GradientMesh::from_surface(n, n, &|u, v| Point::new(u * 300.0, v * 300.0), &|u, v| Color::rgb(u as f32, v as f32, 0.5));
|
||||
// 3,600 patches at 32 x 32 would be 3.7 million quads.
|
||||
let m = mesh(60);
|
||||
let quads = m.quads(32);
|
||||
assert!(quads.len() <= GradientMesh::MAX_QUADS, "{} quads", quads.len());
|
||||
assert!(quads.len() >= 3600 * 4, "still finer than a quad per patch: {}", quads.len());
|
||||
// A small mesh keeps the subdivision asked for.
|
||||
assert_eq!(mesh(3).quads(8).len(), 9 * 64);
|
||||
// The finest the view asks for, on a mesh of 22,500 patches, is a few quads per patch.
|
||||
let big = mesh(150).quads(64);
|
||||
assert!(big.len() <= GradientMesh::MAX_QUADS && big.len() >= 22_500, "{} quads", big.len());
|
||||
// More patches than the budget has quads is malformed, as a point count that doesn't fit is.
|
||||
let point = MeshPoint { p: Point::ZERO, color: Color::rgb(0.0, 0.0, 0.0), opacity: 1.0, handles: [Vec2::ZERO; 4] };
|
||||
let huge = GradientMesh { rows: 2000, cols: 2000, points: vec![point; 2001 * 2001] };
|
||||
assert!(!huge.is_valid());
|
||||
assert!(huge.quads(1).is_empty());
|
||||
}
|
||||
|
||||
/// A file's rows and columns can be anything: a point count too large to compute is no match
|
||||
/// for the points (u32::MAX × u32::MAX wrapped to 0 and passed for a grid without points).
|
||||
#[test]
|
||||
|
||||
@@ -170,11 +170,22 @@ pub(super) fn grid_len(rows: u32, cols: u32) -> Option<usize> {
|
||||
}
|
||||
|
||||
impl GradientMesh {
|
||||
/// The most quads one tessellation ([`Self::quads`]) makes, and so the most patches a mesh can
|
||||
/// have: about 170 bytes a quad, so about 170 MB. A file's mesh can have any number of patches
|
||||
/// and the view any zoom; the subdivision per patch gives way before memory does, and a mesh
|
||||
/// with more patches than that is malformed, like a point count that doesn't fit (#3592).
|
||||
pub const MAX_QUADS: usize = 1 << 20;
|
||||
|
||||
pub fn idx(&self, r: usize, c: usize) -> usize {
|
||||
r * (self.cols as usize + 1) + c
|
||||
}
|
||||
pub fn is_valid(&self) -> bool {
|
||||
self.rows >= 1 && self.cols >= 1 && grid_len(self.rows, self.cols) == Some(self.points.len())
|
||||
self.rows >= 1 && self.cols >= 1 && grid_len(self.rows, self.cols) == Some(self.points.len()) && self.patches() <= Self::MAX_QUADS
|
||||
}
|
||||
|
||||
/// The number of patches (rows times columns), saturating.
|
||||
fn patches(&self) -> usize {
|
||||
(self.rows as usize).saturating_mul(self.cols as usize)
|
||||
}
|
||||
|
||||
/// A 1×1 mesh from its four corners (bottom-left, top-left, top-right, bottom-right in u/v
|
||||
@@ -352,8 +363,9 @@ impl GradientMesh {
|
||||
if !self.is_valid() {
|
||||
return vec![];
|
||||
}
|
||||
let n = n.clamp(1, 64);
|
||||
let mut out = Vec::with_capacity(self.rows as usize * self.cols as usize * n * n);
|
||||
// Finer than the budget allows gives way: a quad per patch is the least.
|
||||
let n = n.clamp(1, 64).min(((Self::MAX_QUADS / self.patches()) as f64).sqrt() as usize).max(1);
|
||||
let mut out = Vec::with_capacity(self.patches() * n * n);
|
||||
let mut grid = vec![Point::ZERO; (n + 1) * (n + 1)];
|
||||
let mut colors = vec![(Color::default(), 0.0f32); (n + 1) * (n + 1)];
|
||||
for r in 0..self.rows as usize {
|
||||
|
||||
+8
-1
@@ -1,6 +1,6 @@
|
||||
# Gaps
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (G13: gradient presets done; G1: Average & Join with a corner or smooth choice, Cmd+Alt+Shift+J, done; estimate unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-31 · **Change:** minor (G8: a gradient mesh from a file is budgeted at about a million patches and quads, #3592; estimate unchanged; previously: G13: gradient presets done; G1: Average & Join with a corner or smooth choice, Cmd+Alt+Shift+J, done; estimate unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
|
||||
## Feature submission evidence
|
||||
|
||||
@@ -224,6 +224,12 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall
|
||||
taken whole or not at all, and the walk that counts an art tile's anchors charges the whole budget for a tree it
|
||||
cannot finish, so nesting buys no room (#3594). The budget is per brushed stroke, not per document: many strokes
|
||||
that each reach it still add up, and a brush inside a symbol is bounded per stroke rather than per instantiation.
|
||||
- **Done (2026-10-31):** a gradient mesh is budgeted (#3592). One tessellation makes at most 1,048,576 quads, and a
|
||||
mesh with more patches than that (rows times columns) is malformed, like a point count that doesn't fit, so it draws
|
||||
nothing instead of exhausting memory: a 150 x 150 mesh from a 3 MB file exported at 20x asked for about 23 million
|
||||
quads (2 GB). Below the budget the subdivision per patch gives way a step at a time (a 60 x 60 mesh draws at 16 x 16
|
||||
quads a patch instead of 32 x 32); a small mesh is drawn as finely as before. The command's own cap of 50 rows and
|
||||
columns is unchanged.
|
||||
- **Done (2026-10-29):** a blend makes at most 100,000 steps in all, counting the steps of the blends inside its keys
|
||||
and steps. Each step between keys that are blends is a copy of one, which makes its own steps when drawn, so step
|
||||
counts capped at 1,000 a pair multiplied by nesting: five levels of 15 steps were 1.3 million steps from a 23 KB
|
||||
@@ -660,6 +666,7 @@ trap, and the defaults (0.25 pt, 100%, 40%) are VectorCraft's own, the pages giv
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-31 | minor | G8: a gradient mesh read from a file is budgeted: at most about a million quads per tessellation, and a mesh with more patches than that is malformed (#3592); estimate unchanged. |
|
||||
| 2026-10-30 | minor | G9: `.ai`/EPS editing data: a blend takes the transparency (opacity, blend mode, knockout) in its state block after its record, so its steps knock each other out as the page draws them; a user's file comes in 0.6% from its page instead of 1.3%. Scores and estimates unchanged. |
|
||||
| 2026-10-30 | minor | G1: Object › Path › Average & Join… (`path.averageJoin`, Cmd+Alt+Shift+J) moves two open path ends to their midpoint and joins them there, the joined anchor a corner or a smooth point as its dialog's Points choice asks, in one undo step; the "Average and Join in one step" entry is done. Estimate unchanged |
|
||||
| 2026-10-30 | minor | G12: Preferences audit (#394): 5 options are stored but not acted on (Open Documents As Tabs, Units › East Asian Type, Hyphenation › Default Language, Scratch Disks, Legacy vector clipboard), each waiting on a subsystem; Highlight Alternate Glyphs, Real-time Drawing and the Devices options act. Estimate unchanged. |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Parity with Adobe Illustrator
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-31 · **Change:** minor (Path Precision Tools add-on: Shared Edge Merge Tool; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-31 · **Change:** minor (G8: a gradient mesh from a file is budgeted, #3592; scores unchanged; previously: Path Precision Tools add-on: Shared Edge Merge Tool; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026)
|
||||
|
||||
## Feature submission evidence
|
||||
|
||||
@@ -19,6 +19,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-31 | minor | Hardening: a gradient mesh read from a file is budgeted at about a million quads, so a huge one no longer exhausts memory when drawn or exported (#3592); scores unchanged. |
|
||||
| 2026-10-30 | minor | File formats: `.ai`/EPS editing data: a blend takes the transparency (opacity, blend mode, knockout) in its state block after its record, so its steps knock each other out as the page draws them; a user's file comes in 0.6% from its page instead of 1.3%. Scores and estimates unchanged. |
|
||||
| 2026-10-30 | minor | File formats: `.ai`/EPS editing-data fidelity from an audit of a user's files: All Caps and Small Caps (#4708); placed files at their box's corner, kept where they draw closer to the page (#4664, #4723, #4742); pages cropped to the art, with `UserUnit` (#4658, #4727); gradient mesh corners with handle weights of 0 and the state written after a mesh (#4738, #4744, #4748); type on a path spaced as the file lays it out (#4743); synthetic regressions in `crates/eps`; evidence added; scores unchanged. |
|
||||
| 2026-10-30 | minor | Effects: bounded length inputs clamp on commit and reject invalid text visibly (#4638); scores unchanged. |
|
||||
|
||||
Reference in New Issue
Block a user