From db6c5ea9df62e428be57a00ddeae030e47ffca8f Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 31 Oct 2026 09:11:43 -0700 Subject: [PATCH] Fix #2678: Ungroup no longer multiplies the group's opacity into its members Ungroup multiplied each member's opacity by the group's, so a member of a 50% group stayed at 50% after the group was gone, and members that overlapped showed through each other. Each member now keeps its own opacity, and the group's opacity is removed with the group. --- crates/engine/src/cmd/object/arrange.rs | 2 +- crates/engine/src/tests/containers.rs | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/crates/engine/src/cmd/object/arrange.rs b/crates/engine/src/cmd/object/arrange.rs index 741475d87..9cafae899 100644 --- a/crates/engine/src/cmd/object/arrange.rs +++ b/crates/engine/src/cmd/object/arrange.rs @@ -129,7 +129,7 @@ pub(super) fn ungroup(s: &mut Session, _: &Value) -> Result { { *clipping = false; } - c.opacity *= n.opacity; + // Each member keeps its own opacity; the group's opacity goes with the group. new_sel.push(c.id); out.push(c); } diff --git a/crates/engine/src/tests/containers.rs b/crates/engine/src/tests/containers.rs index aa72ebecc..1b147ecc1 100644 --- a/crates/engine/src/tests/containers.rs +++ b/crates/engine/src/tests/containers.rs @@ -69,6 +69,28 @@ fn a_group_fill_paints_both_members_and_the_contents_slot_moves() { assert!(s.execute("appearance.moveItem", &json!({"from": "contents", "to": 1})).is_err()); } +#[test] +fn ungroup_leaves_the_groups_opacity_with_the_group() { + let mut s = session(); + let red = id_of(&run(&mut s, "shape.rectangle", json!({"x": 20, "y": 20, "width": 60, "height": 40}))); + run(&mut s, "paint.setFill", json!({"color": "#ff0000"})); + let blue = id_of(&run(&mut s, "shape.rectangle", json!({"x": 100, "y": 20, "width": 60, "height": 40}))); + run(&mut s, "paint.setFill", json!({"color": "#0000ff"})); + run(&mut s, "object.setProps", json!({"ids": [blue.0], "opacity": 40})); + run(&mut s, "select.set", json!({"ids": [red.0, blue.0]})); + let g = id_of(&run(&mut s, "object.group", json!({}))); + run(&mut s, "object.setProps", json!({"ids": [g.0], "opacity": 50})); + run(&mut s, "object.ungroup", json!({})); + // Each member keeps its own opacity: the red square is opaque again and the blue one stays at 40%. + assert!((node(&s, red).opacity - 1.0).abs() < 1e-6, "{}", node(&s, red).opacity); + assert!((node(&s, blue).opacity - 0.4).abs() < 1e-6, "{}", node(&s, blue).opacity); + assert_eq!(pixel(&s, 40, 40), RED); + // One undo step brings the group back at 50%. + run(&mut s, "edit.undo", json!({})); + assert!((node(&s, g).opacity - 0.5).abs() < 1e-6); + assert!((node(&s, red).opacity - 1.0).abs() < 1e-6); +} + #[test] fn appearance_commands_target_the_object_or_its_contents() { let mut s = session();