From e87534342beb17c87579378a2475455ce3045d51 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 22 Oct 2026 00:12:48 +0000 Subject: [PATCH] Fix #3480: pattern edit preview honors Overlap stacking ## Summary In pattern editing mode, preview copies were drawn in row-major order and the centre tile was always composited on top, so Pattern Options Overlap had no visible effect when negative spacing made tiles overlap (as in Illustrator). Preview copies now paint in the same order as tiled pattern fills. ## Tests - vectorcraft-doc: preview_paint_order_respects_overlap Closes storytold/vectorcraft#3480 Co-authored-by: nrq6993-a11y --- crates/doc/src/pattern.rs | 28 +++++++++++++++++++++++++++- crates/render/src/pattern.rs | 24 +++++++++++++++--------- 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/crates/doc/src/pattern.rs b/crates/doc/src/pattern.rs index 74bdf0514..ac00a6206 100644 --- a/crates/doc/src/pattern.rs +++ b/crates/doc/src/pattern.rs @@ -279,7 +279,7 @@ impl PatternDef { out } /// Offsets of the pattern-editing preview copies (`copies`×`copies` around the tile, the - /// centre `(0, 0)` excluded). + /// centre `(0, 0)` excluded), in row-major order (not paint order). pub fn preview_offsets(&self) -> Vec { let k = (self.copies.clamp(1, 15) / 2) as i64; let mut out = vec![]; @@ -292,6 +292,21 @@ impl PatternDef { } out } + /// Offsets of every pattern-editing preview copy, including the centre tile, in the same paint + /// order [`offsets_covering`] uses for [`Overlap`]. + pub fn preview_offsets_in_paint_order(&self) -> Vec { + let k = (self.copies.clamp(1, 15) / 2) as i64; + let (i0, i1, j0, j1) = (-k, k, -k, k); + let mut out = vec![]; + let rows: Vec = if self.overlap.bottom_in_front { (j0..=j1).collect() } else { (j0..=j1).rev().collect() }; + for &j in &rows { + let cols: Vec = if self.overlap.right_in_front { (i0..=i1).collect() } else { (i0..=i1).rev().collect() }; + for i in cols { + out.push(self.offset(i, j)); + } + } + out + } /// Instances covering `region` (document space) as plain nodes for a pattern painted with /// `paint_xf` (pattern space → document), fresh-id-less (id 0). Used by exporters and Distort /// Pattern Fills; Expand uses [`Self::instances_in_with`]. @@ -744,6 +759,17 @@ mod tests { } } + #[test] + fn preview_paint_order_respects_overlap() { + let mut def = PatternDef::new("p", vec![sq(0.0, 0.0, 10.0)]); + def.copies = 3; + assert!(close(*def.preview_offsets_in_paint_order().last().unwrap(), Vec2::new(-10.0, -10.0))); + let mut d2 = def.clone(); + d2.overlap = Overlap { right_in_front: true, bottom_in_front: true }; + assert!(close(*d2.preview_offsets_in_paint_order().last().unwrap(), Vec2::new(10.0, 10.0))); + assert_eq!(def.preview_offsets_in_paint_order().len(), 9); + } + #[test] fn covering_offsets_and_overlap_order() { let def = PatternDef::new("p", vec![sq(0.0, 0.0, 10.0)]); diff --git a/crates/render/src/pattern.rs b/crates/render/src/pattern.rs index e1c7c2901..29c788a13 100644 --- a/crates/render/src/pattern.rs +++ b/crates/render/src/pattern.rs @@ -308,17 +308,23 @@ impl Renderer { let children: Vec> = layer.children().cloned().unwrap_or_default(); if layer.visible && !children.is_empty() { let dim = (def.dim_copies / 100.0).clamp(0.0, 1.0); - let comp = crate::group::Composite { opacity: dim, ..Default::default() }; - self.group(ctx, f, comp, &mut |r, c, fr| { - for o in def.preview_offsets() { - let frame = Frame { view: fr.view * Affine::translate(o), visible: fr.visible - o, ..*fr }; - for a in &children { - r.draw_arc(c, &frame, a); - } + for o in def.preview_offsets_in_paint_order() { + let is_center = o.x == 0.0 && o.y == 0.0; + if is_center { + self.draw_arc(ctx, f, layer); + } else { + let comp = crate::group::Composite { opacity: dim, ..Default::default() }; + self.group(ctx, f, comp, &mut |r, c, fr| { + let frame = Frame { view: fr.view * Affine::translate(o), visible: fr.visible - o, ..*fr }; + for a in &children { + r.draw_arc(c, &frame, a); + } + }); } - }); + } + } else { + self.draw_arc(ctx, f, layer); } - self.draw_arc(ctx, f, layer); // The tile edge and swatch bounds are editing aids: exports and thumbnails leave them out, // as they do guides (#3261). if f.opts.skip_templates {