From d27ae11d150fd4c159c574f675fa818e28f9180f Mon Sep 17 00:00:00 2001 From: Barzabel Date: Thu, 15 Oct 2026 23:06:31 +0300 Subject: [PATCH] M1: a duplicate's fill stays its own (#586) Regression test for #586: with the colour picker open on a red rectangle or pen path, duplicating it (edit.duplicate, or copy and paste) and recolouring the copy leaves the original red. The cause was the picker staying bound to the original after the selection moved to the copy, fixed by #523; without that fix this test fails. The duplicated paint is already independent data. --- crates/ui-egui/src/duplicate_paint_tests.rs | 71 +++++++++++++++++++++ crates/ui-egui/src/lib.rs | 2 + 2 files changed, 73 insertions(+) create mode 100644 crates/ui-egui/src/duplicate_paint_tests.rs diff --git a/crates/ui-egui/src/duplicate_paint_tests.rs b/crates/ui-egui/src/duplicate_paint_tests.rs new file mode 100644 index 00000000..b8ca81c6 --- /dev/null +++ b/crates/ui-egui/src/duplicate_paint_tests.rs @@ -0,0 +1,71 @@ +//! A duplicate's fill is its own: recolouring it, with the picker that was open on the original, +//! leaves the original as it was (#586). + +use figcraft_doc::NodeId; +use figcraft_engine::Session; +use serde_json::json; + +use crate::colorpicker::PickerTarget; +use crate::layers_drag_tests::Ui; + +fn id(v: &serde_json::Value) -> NodeId { + NodeId(v["id"].as_u64().or_else(|| v["ids"][0].as_u64()).unwrap()) +} + +fn fill(u: &Ui, id: NodeId) -> String { + u.app.session.doc().unwrap().node(id).unwrap().fills[0].solid_color().unwrap().to_css_hex() +} + +/// A red rectangle or pen path, selected, with the colour picker of its fill open. +fn red(vector: bool) -> (Ui, NodeId) { + let mut s = Session::new(); + let a = if vector { + id(&s.execute("vector.create", &json!({"points": [[0, 0], [60, 0], [30, 50]], "closed": true})).unwrap()) + } else { + id(&s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 40, "height": 40})).unwrap()) + }; + s.execute("fill.set", &json!({"ids": [a], "color": "#FF0000"})).unwrap(); + s.execute("select.set", &json!({"ids": [a]})).unwrap(); + let mut u = Ui::new(s); + u.app.ui.picker = Some(PickerTarget::Paint { fills: true, index: 0, ids: vec![a] }); + u.frame(vec![]); + (u, a) +} + +/// Duplicate the selection the way `how` names, moved clear of the original; the copy's id. +fn duplicate(u: &mut Ui, how: &str) -> NodeId { + let copy = match how { + "duplicate" => id(&u.app.session.execute("edit.duplicate", &json!({"dx": 100})).unwrap()), + _ => { + u.app.session.execute("edit.copy", &json!({})).unwrap(); + id(&u.app.session.execute("edit.paste", &json!({"x": 100, "y": 0})).unwrap()) + } + }; + u.frame(vec![]); + u.frame(vec![]); + copy +} + +#[test] +fn recolouring_a_duplicate_leaves_the_original() { + for vector in [false, true] { + for how in ["duplicate", "paste"] { + let (mut u, a) = red(vector); + let b = duplicate(&mut u, how); + assert_ne!(a, b); + assert_eq!(u.app.session.selection(), vec![b], "{how} selects the copy"); + // The picker opened for the original doesn't follow it onto the copy's edits. + let picker = u.app.ui.picker.clone(); + assert!( + !matches!(&picker, Some(PickerTarget::Paint { ids, .. }) if ids.contains(&a)), + "vector {vector}, {how}: the picker still edits the original: {picker:?}" + ); + // Recolour the selection as the fill row does. + u.app.ui.picker = Some(PickerTarget::Paint { fills: true, index: 0, ids: u.app.session.selection() }); + u.app.session.execute("fill.set", &json!({"ids": u.app.session.selection(), "color": "#0000FF"})).unwrap(); + u.frame(vec![]); + assert_eq!(fill(&u, b), "#0000FF", "vector {vector}, {how}"); + assert_eq!(fill(&u, a), "#FF0000", "vector {vector}, {how}: the original keeps its fill"); + } + } +} diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 936c32a9..d5d6bdfd 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -87,6 +87,8 @@ mod cursors_tests; #[cfg(test)] mod dev_mode_tests; #[cfg(test)] +mod duplicate_paint_tests; +#[cfg(test)] mod export_preview_tests; #[cfg(test)] mod field_tests;