From 2c28ccf0207ad5e70b8f755227616ac216b53149 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sat, 31 Oct 2026 18:55:28 +0700 Subject: [PATCH] Gradient mesh: flat pieces follow the quad budget's subdivision A mesh over the quad budget is cut coarser than asked (#3592); its flat pieces now use the same subdivision, so each patch takes its own quads. --- crates/doc/src/live.rs | 15 +++++++++++++++ crates/doc/src/live/mesh.rs | 14 ++++++++++---- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/crates/doc/src/live.rs b/crates/doc/src/live.rs index 102a614b9..1b234c212 100644 --- a/crates/doc/src/live.rs +++ b/crates/doc/src/live.rs @@ -204,6 +204,21 @@ mod tests { assert!(huge.quads(1).is_empty()); } + /// Flat pieces of a mesh over the quad budget follow the coarser subdivision [`GradientMesh::quads`] + /// gives way to: each patch's pieces are its own quads', so together they cover the mesh once. + #[test] + fn a_large_meshs_pieces_follow_the_quad_budget() { + let m = GradientMesh::from_surface(60, 60, &|u, v| Point::new(u * 300.0, v * 300.0), &|u, v| Color::rgb(u as f32, v as f32, 0.5)); + let area = |pts: &[Point]| { + let n = pts.len(); + (0..n).map(|i| pts[i].x * pts[(i + 1) % n].y - pts[(i + 1) % n].x * pts[i].y).sum::().abs() / 2.0 + }; + let pieces = m.pieces(32); + assert!(pieces.len() >= 3600, "{} pieces", pieces.len()); + let total: f64 = pieces.iter().map(|p| area(&p.pts)).sum(); + assert!((total - 90_000.0).abs() < 90.0, "covered {total}"); + } + /// 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] diff --git a/crates/doc/src/live/mesh.rs b/crates/doc/src/live/mesh.rs index 10540266a..f72dd548c 100644 --- a/crates/doc/src/live/mesh.rs +++ b/crates/doc/src/live/mesh.rs @@ -367,13 +367,19 @@ impl GradientMesh { (Color::rgb(rgb[0], rgb[1], rgb[2]), op) } - /// Tessellate into `n`×`n` quads per patch, each coloured at its centre and its corners. + /// The quads per patch side [`Self::quads`] cuts at when `n` is asked: 1 to 64, and coarser + /// where the patches at `n` would pass [`Self::MAX_QUADS`] (a quad per patch is the least). + fn subdivision(&self, n: usize) -> usize { + n.clamp(1, 64).min(((Self::MAX_QUADS / self.patches().max(1)) as f64).sqrt() as usize).max(1) + } + + /// Tessellate into `n`×`n` quads per patch ([`Self::subdivision`]), each coloured at its centre + /// and its corners. pub fn quads(&self, n: usize) -> Vec { if !self.is_valid() { return vec![]; } - // 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 n = self.subdivision(n); 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)]; @@ -411,7 +417,7 @@ impl GradientMesh { /// all its quads. A piece's outline runs through its quads' outer corners, so it covers what /// they covered, and it takes the colour at its centre. pub fn pieces(&self, n: usize) -> Vec { - let n = n.clamp(1, 64); + let n = self.subdivision(n); let quads = self.quads(n); let cols = (self.cols as usize).max(1); let mut out = Vec::with_capacity(quads.len());