From e87534342beb17c87579378a2475455ce3045d51 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 22 Oct 2026 00:12:48 +0000 Subject: [PATCH 1/2] 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 { From d792e7b2be4a8cde021e8b160cd9c45af8de50e4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 22 Oct 2026 02:21:35 +0000 Subject: [PATCH 2/2] Pattern edit preview: two dimmed copy groups around centre tile Review feedback on #3498: dimmed neighbours share one opacity composite before the centre tile and one after, preserving overlap paint order without per-copy compositing. Co-authored-by: nrq6993-a11y --- crates/doc/src/pattern.rs | 20 ++++++++++++++++++++ crates/render/src/pattern.rs | 36 +++++++++++++++++++++--------------- 2 files changed, 41 insertions(+), 15 deletions(-) diff --git a/crates/doc/src/pattern.rs b/crates/doc/src/pattern.rs index ac00a6206..fdab92861 100644 --- a/crates/doc/src/pattern.rs +++ b/crates/doc/src/pattern.rs @@ -307,6 +307,23 @@ impl PatternDef { } out } + /// Preview copies drawn before and after the centre tile in [`Self::preview_offsets_in_paint_order`], + /// for one dimmed composite each (the centre tile is drawn at full strength between them). + pub fn preview_offsets_dimmed_sides(&self) -> (Vec, Vec) { + let mut before = vec![]; + let mut after = vec![]; + let mut past_center = false; + for o in self.preview_offsets_in_paint_order() { + if o.x == 0.0 && o.y == 0.0 { + past_center = true; + } else if past_center { + after.push(o); + } else { + before.push(o); + } + } + (before, after) + } /// 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`]. @@ -768,6 +785,9 @@ mod tests { 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); + let (before, after) = def.preview_offsets_dimmed_sides(); + assert_eq!(before.len() + after.len(), 8); + assert!(before.iter().all(|o| *o != Vec2::ZERO) && after.iter().all(|o| *o != Vec2::ZERO)); } #[test] diff --git a/crates/render/src/pattern.rs b/crates/render/src/pattern.rs index 29c788a13..9cb5e874f 100644 --- a/crates/render/src/pattern.rs +++ b/crates/render/src/pattern.rs @@ -22,7 +22,7 @@ use std::sync::{Arc, Weak}; use vectorcraft_color::cms::Cms; use vectorcraft_doc::pattern::{Overlap, PatternDef, TileType}; use vectorcraft_doc::{Document, Node}; -use vectorcraft_geom::{Affine, Rect, Shape}; +use vectorcraft_geom::{Affine, Rect, Shape, Vec2}; use vello_cpu::peniko::{Extend, ImageQuality, ImageSampler}; use vello_cpu::{Pixmap, RenderContext}; @@ -299,6 +299,22 @@ pub fn render_pattern_swatch(doc: &Document, name: &str, size: u32) -> Option]) { + if offsets.is_empty() { + return; + } + let comp = crate::group::Composite { opacity: dim, ..Default::default() }; + self.group(ctx, f, comp, &mut |r, c, fr| { + for o in 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); + } + } + }); + } + /// Pattern editing mode: draw the tile layer, its dimmed copies and the tile edge instead of /// the document. Returns false when not in pattern editing mode. pub(crate) fn draw_pattern_edit(&mut self, ctx: &mut RenderContext, f: &Frame) -> bool { @@ -308,20 +324,10 @@ 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); - 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); - } - }); - } - } + let (before, after) = def.preview_offsets_dimmed_sides(); + self.draw_dimmed_pattern_copies(ctx, f, dim, &before, &children); + self.draw_arc(ctx, f, layer); + self.draw_dimmed_pattern_copies(ctx, f, dim, &after, &children); } else { self.draw_arc(ctx, f, layer); }