Appearance: the active row is matched in each selected object on its own

Reduce to Basic Appearance and Add New Fill matched the Appearance
panel's active row only when every selected object had an equal row.
With three objects whose third had a different lower fill, the second
object lost the match too: Reduce kept its top fill instead of the
active one, and Add New Fill put the copy on top instead of above the
active row.

items_per_target now matches each object against the first one
separately, so an object keeps the match whenever it has an equal row.
This commit is contained in:
dotCooCoo
2026-11-04 19:19:00 +07:00
committed by bflatastic
parent af8be8dc2e
commit bc2af8df35
2 changed files with 55 additions and 4 deletions
+16 -4
View File
@@ -357,11 +357,23 @@ pub(crate) fn appearances_differ(d: &vectorcraft_doc::Document, ids: &[NodeId])
/// Indices `idx` of the first target's items (aligned with `aligned`) in each of `ids`: the first
/// target's own, and in each other one the items equal to them there (see [`SharedStack`]), each
/// index on its own: one the targets don't share is left out of the others (never taken as the
/// same index there). Not aligned, or one target: the same indices in each.
/// index and each target on its own: an index a target doesn't share with the first is left out
/// of that target (never taken as the same index there), whatever the other targets have. Not
/// aligned, or one target: the same indices in each.
pub(crate) fn items_per_target(d: &vectorcraft_doc::Document, ids: &[NodeId], idx: &[usize], aligned: bool) -> Vec<Vec<usize>> {
match shared_stack(d, ids).filter(|_| aligned) {
Some(sh) => (0..ids.len()).map(|k| if k == 0 { idx.to_vec() } else { idx.iter().filter_map(|i| sh.item_in(*i, k)).collect() }).collect(),
let aps: Option<Vec<&Appearance>> = ids.iter().map(|id| d.node(*id).map(|n| &*n.appearance)).collect();
match aps.filter(|aps| aligned && aps.len() >= 2) {
Some(aps) => aps
.iter()
.enumerate()
.map(|(k, ap)| match aps.first() {
Some(first) if k > 0 => {
let sh = SharedStack::of(&[*first, *ap]);
idx.iter().filter_map(|i| sh.item_in(*i, 1)).collect()
}
_ => idx.to_vec(),
})
.collect(),
None => vec![idx.to_vec(); ids.len()],
}
}
+39
View File
@@ -463,6 +463,45 @@ fn reduce_to_basic_keeps_the_active_row_of_the_first_selected_object() {
assert_eq!(fill_paint(&node(&s, a), 0).color().unwrap().to_hex(), "#0000ff");
}
/// Three objects with equal stacks, the third's lower fill changed; the lower, green fill row of
/// the first is active, selected a, b, c.
fn three_with_one_changed(s: &mut Session) -> [NodeId; 3] {
let ids = [rect(s, 50.0), rect(s, 200.0), rect(s, 300.0)];
run(s, "select.set", json!({"ids": ids.map(|id| id.0)}));
run(s, "appearance.addFill", json!({}));
run(s, "appearance.setItem", json!({"index": 0, "color": "#00ff00"}));
run(s, "appearance.setItem", json!({"index": 2, "color": "#ff0000"})); // [Fill green, Stroke, Fill red]
run(s, "appearance.setItem", json!({"index": 0, "color": "#0000ff", "ids": [ids[2].0]}));
run(s, "appearance.setActiveItem", json!({"index": 0}));
ids
}
/// The active row is matched in each other object on its own: one object without an equal row
/// does not stop another that has one from keeping it.
#[test]
fn reduce_to_basic_keeps_the_active_row_in_each_object_that_has_it() {
let mut s = session();
let [a, b, c] = three_with_one_changed(&mut s);
run(&mut s, "appearance.reduceToBasic", json!({}));
assert_eq!(fill_paint(&node(&s, a), 0).color().unwrap().to_hex(), "#00ff00");
assert_eq!(fill_paint(&node(&s, b), 0).color().unwrap().to_hex(), "#00ff00");
assert_eq!(fill_paint(&node(&s, c), 0).color().unwrap().to_hex(), "#ff0000");
}
/// Add New Fill puts its copy of the active row right above that row in each object that has an
/// equal row, even when another selected object has none.
#[test]
fn add_fill_copies_the_active_row_in_each_object_that_has_it() {
let mut s = session();
let [a, b, _] = three_with_one_changed(&mut s);
run(&mut s, "appearance.addFill", json!({}));
for id in [a, b] {
let n = node(&s, id);
assert_eq!(fill_paint(&n, 1).color().unwrap().to_hex(), "#00ff00", "{:?}", n.appearance);
assert_eq!(fill_paint(&n, 3).color().unwrap().to_hex(), "#ff0000", "{:?}", n.appearance);
}
}
/// b `[Fill red, Fill white, Stroke]` above a `[Fill white, Stroke]`, selected b first: b's rows 1
/// and 2 equal a's rows 0 and 1.
fn reversed_pair(s: &mut Session) -> (NodeId, NodeId) {