diff --git a/crates/engine/src/cmd/appearance.rs b/crates/engine/src/cmd/appearance.rs index 45a3d4965..71f23b1c6 100644 --- a/crates/engine/src/cmd/appearance.rs +++ b/crates/engine/src/cmd/appearance.rs @@ -560,7 +560,7 @@ fn set_item(s: &mut Session, p: &Value) -> Result { } }; if let Some(pa) = &paint { - *pp = super::gradient::place_paint(pa, p, bounds); + *pp = super::gradient::place_paint(&super::paint::keep_pattern_xf(pa, p, Some(&*pp)), p, bounds); } if let Some(o) = p.get("opacity").and_then(Value::as_f64) { *op = percent(o); diff --git a/crates/engine/src/cmd/paint.rs b/crates/engine/src/cmd/paint.rs index 2e0a346ea..dd7f05092 100644 --- a/crates/engine/src/cmd/paint.rs +++ b/crates/engine/src/cmd/paint.rs @@ -457,7 +457,8 @@ fn apply_paint(s: &mut Session, p: &Value, paint: Paint, fill: bool) -> Result) -> Paint { + match (paint, old) { + (Paint::Pattern { pattern, xf }, Some(Paint::Pattern { xf: kept, .. })) if p.get("swatch").is_some() && *xf == Affine::IDENTITY => { + Paint::Pattern { pattern: pattern.clone(), xf: *kept } + } + _ => paint.clone(), + } +} + /// Use the existing 1 pt default when adding paint to an unpainted zero-width stroke. pub(super) fn restore_unpainted_stroke(ap: &mut Appearance, index: Option, paint: &Paint) { if !paint.is_none() diff --git a/crates/engine/src/tests_pattern.rs b/crates/engine/src/tests_pattern.rs index 7a18ac6a7..0b36ea3bd 100644 --- a/crates/engine/src/tests_pattern.rs +++ b/crates/engine/src/tests_pattern.rs @@ -530,3 +530,31 @@ fn repeat_commands_validate() { s.execute("select.none", &json!({})).ok(); assert!(s.execute("object.pattern.make", &json!({})).is_err()); } + +/// #1970: picking another pattern swatch keeps the transform the fill already had; a fill that was +/// a colour gets the pattern untransformed. +#[test] +fn picking_another_pattern_swatch_keeps_the_pattern_transform() { + let mut s = session(); + make_dots(&mut s); + let bars = rect(&mut s, 0.0, 0.0, 20.0, 10.0); + s.execute("paint.setFill", &json!({"ids": [bars.0], "color": "#0000ff"})).unwrap(); + s.execute("object.pattern.make", &json!({"ids": [bars.0], "name": "Bars", "edit": false})).unwrap(); + let big = rect(&mut s, 20.0, 20.0, 200.0, 200.0); + s.execute("paint.setFill", &json!({"ids": [big.0], "color": "#00ff00"})).unwrap(); + s.execute("paint.setFill", &json!({"ids": [big.0], "swatch": "Bars"})).unwrap(); + let Paint::Pattern { xf, .. } = node(&s, big).appearance.fill_paint() else { panic!() }; + assert_eq!(xf.as_coeffs(), [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]); + s.execute("paint.setFill", &json!({"ids": [big.0], "swatch": "Dots"})).unwrap(); + s.execute("pattern.transform", &json!({"ids": [big.0], "matrix": [2, 0, 0, 2, 15, 10]})).unwrap(); + s.execute("paint.setFill", &json!({"ids": [big.0], "swatch": "Bars"})).unwrap(); + let Paint::Pattern { pattern, xf } = node(&s, big).appearance.fill_paint() else { panic!() }; + assert_eq!(pattern, "Bars"); + assert_eq!(xf.as_coeffs(), [2.0, 0.0, 0.0, 2.0, 15.0, 10.0]); + // The Appearance panel picks the swatch for one fill through `appearance.setItem`. + let index = node(&s, big).appearance.items.iter().position(|it| matches!(it, vectorcraft_doc::AppearanceItem::Fill(_))).unwrap(); + s.execute("appearance.setItem", &json!({"ids": [big.0], "index": index, "swatch": "Dots"})).unwrap(); + let Paint::Pattern { pattern, xf } = node(&s, big).appearance.fill_paint() else { panic!() }; + assert_eq!(pattern, "Dots"); + assert_eq!(xf.as_coeffs(), [2.0, 0.0, 0.0, 2.0, 15.0, 10.0]); +}