From b44cbdb048720d4a3a9446d432b04420651cd070 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 11 Nov 2026 07:39:47 -0800 Subject: [PATCH] Gradients: picking a part of a symbol instance leaves no gradient stop selected A stop selected on a whole dynamic symbol instance's gradient stayed selected after Direct Selection picked an object of that instance's art, because the instance stayed the first selected object. The Gradient panel kept the stop highlighted and the Gradient tool took Delete and the arrow keys for it, while every gradient edit is refused for a picked object. The stop's owner now records whether a part is picked, so the stop is not selected while one is, and is selected again when the whole instance is. --- crates/engine/src/cmd/gradient.rs | 15 +++++++++++--- crates/engine/src/tests/dynamic_symbols.rs | 23 ++++++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/crates/engine/src/cmd/gradient.rs b/crates/engine/src/cmd/gradient.rs index c73a23d6c..8a3485c49 100644 --- a/crates/engine/src/cmd/gradient.rs +++ b/crates/engine/src/cmd/gradient.rs @@ -80,19 +80,28 @@ fn select_stop(s: &mut Session, p: &Value) -> Result { } /// Whose gradient a selected stop belongs to: the active document, its first selected object -/// (None: the default paint), the proxy in front and the Appearance panel's active item. -/// Selecting other art, toggling the proxy or picking another item leaves no stop selected. +/// (None: the default paint), whether a part of a symbol instance's art is picked, the proxy in +/// front and the Appearance panel's active item. Selecting other art, picking a part of the +/// instance, toggling the proxy or picking another item leaves no stop selected. #[derive(Clone, Copy, Debug, PartialEq)] pub(crate) struct StopOwner { doc: Option, object: Option, + part: bool, fill: bool, item: Option, } impl StopOwner { pub(crate) fn of(s: &Session) -> Self { - Self { doc: s.active, object: s.active().and_then(|d| d.selection.objects.first().copied()), fill: s.fill_active, item: s.appearance_item() } + let st = s.active(); + Self { + doc: s.active, + object: st.and_then(|d| d.selection.objects.first().copied()), + part: st.is_some_and(|d| d.selection.symbol_part.is_some()), + fill: s.fill_active, + item: s.appearance_item(), + } } } diff --git a/crates/engine/src/tests/dynamic_symbols.rs b/crates/engine/src/tests/dynamic_symbols.rs index eab2f3067..f4ae97bb1 100644 --- a/crates/engine/src/tests/dynamic_symbols.rs +++ b/crates/engine/src/tests/dynamic_symbols.rs @@ -903,6 +903,29 @@ fn gradients_and_patterns_on_a_picked_object_are_taken() { assert_eq!((undo_steps(&s), &s.paint.fill), (undo, &fill), "nothing changes"); } +/// A stop selected on the whole instance's gradient is not selected once a part of its art is +/// picked, so the Gradient tool does not take Delete for it, and is selected again when the whole +/// instance is. +#[test] +fn picking_a_part_leaves_no_gradient_stop_selected() { + let mut s = session(); + let inst = pair(&mut s, "dynamic"); + let g = json!({"kind": "linear", "stops": [{"offset": 0, "color": "#ff0000"}, {"offset": 1, "color": "#0000ff"}]}); + s.execute("select.set", &json!({"ids": [inst.0]})).unwrap(); + s.execute("paint.setFill", &json!({"gradient": g})).unwrap(); + s.fill_active = true; + let view = ViewInfo::default(); + s.select_tool("gradient", view).unwrap(); + s.execute("gradient.selectStop", &json!({"index": 1})).unwrap(); + assert_eq!(s.selected_stop(), Some(1)); + assert!(s.tool_claims_key(vectorcraft_tools::ToolKey::Delete, view)); + s.execute("symbol.instance.selectPart", &json!({"id": inst.0, "path": [1]})).unwrap(); + assert_eq!(s.selected_stop(), None); + assert!(!s.tool_claims_key(vectorcraft_tools::ToolKey::Delete, view)); + s.execute("select.set", &json!({"ids": [inst.0]})).unwrap(); + assert_eq!(s.selected_stop(), Some(1)); +} + #[test] fn opacity_and_blend_on_a_picked_object_go_through_transparency_set() { let mut s = session();