diff --git a/crates/engine/src/cmd/draw2.rs b/crates/engine/src/cmd/draw2.rs index 24ffb39db..872eb9cb1 100644 --- a/crates/engine/src/cmd/draw2.rs +++ b/crates/engine/src/cmd/draw2.rs @@ -13,7 +13,8 @@ use vectorcraft_geom::{Anchor, AnchorKind, BezPath, FillRule, PathData, Point, R use vectorcraft_pathops as po; use vectorcraft_pathops::{BoolOp, Cap, Join, SimplifyOptions}; -use super::create::{add_look, add_node, insert_look}; +use super::create::{add_look, insert_look}; +use super::newart::NewArt; use super::pathops::{Beside, remove_each}; use super::*; use crate::EngineError; @@ -142,7 +143,7 @@ pub fn specs() -> Vec { "Blob Brush", [], None, - "{points: [[x,y]…], size?: pt (10), merge?: true} filled brush shape, merged with touching same-colour blobs → {id}", + "{points: [[x,y]…], size?: pt (10), merge?: true, mergeOnlySelection?: bool (false: merge with any touching same-color blob; true: only with selected ones), keepSelected?: bool (false: the drawn or merged path is not selected, and the rest of the selection stays; true: it is selected)} filled brush shape, merged with touching same-colour blobs → {id}", has_doc, blob ), @@ -1346,11 +1347,14 @@ fn blob(s: &mut Session, p: &Value) -> Result { } else { Paint::solid(Color::BLACK) }; + let keep_selected = bool_or(p, "keepSelected", false); + let only_selection = bool_or(p, "mergeOnlySelection", false); let st = s.doc()?; let mut merge_ids = vec![]; - if bool_or(p, "merge", true) { + if bool_or(p, "merge", true) && !(only_selection && st.selection.is_empty()) { let all = Selection::default(); - for id in target_paths(&st.doc, &all, poly_bounds(&pts, size)) { + let from = if only_selection { &st.selection } else { &all }; + for id in target_paths(&st.doc, from, poly_bounds(&pts, size)) { let Some(n) = st.doc.node(id) else { continue }; if n.appearance.fill_paint() != paint || !n.appearance.stroke_paint().is_none() { continue; @@ -1361,20 +1365,26 @@ fn blob(s: &mut Session, p: &Value) -> Result { } } } - let ap = Appearance::basic(paint, Paint::None, 1.0); + let ap = NewArt::plain(Appearance::basic(paint, Paint::None, 1.0)); if merge_ids.is_empty() { - return add_node(s, "Blob Brush", path_kind(region), ap, None); + return insert_look(s, "Blob Brush", path_kind(region), ap, None, keep_selected); } let mut inputs: Vec<(&PathData, FillRule)> = vec![(®ion, FillRule::NonZero)]; inputs.extend(merge_ids.iter().map(|(_, pd, r)| (pd, *r))); let merged = po::unite_all(&inputs); - let Some(((keep, _, _), rest)) = merge_ids.split_last() else { return add_node(s, "Blob Brush", path_kind(region), ap, None) }; + let Some(((keep, _, _), rest)) = merge_ids.split_last() else { + return insert_look(s, "Blob Brush", path_kind(region), ap, None, keep_selected); + }; let keep = *keep; let others: Vec = rest.iter().map(|m| m.0).collect(); s.edit("Blob Brush", |d, sel| { *path_mut(d, keep)? = merged; remove_each(d, &others)?; - sel.set([keep]); + if keep_selected { + sel.set([keep]); + } else { + sel.remove(keep); + } Ok(()) })?; Ok(json!({ "id": keep.0 })) diff --git a/crates/engine/src/tests_draw2.rs b/crates/engine/src/tests_draw2.rs index bf600eeca..98e4b67ed 100644 --- a/crates/engine/src/tests_draw2.rs +++ b/crates/engine/src/tests_draw2.rs @@ -186,6 +186,61 @@ fn paintbrush_uses_stroke_blob_brush_fills() { assert!(area(&ps[1].1) > 1500.0); } +/// Merge only with selection: a Blob Brush stroke merges with the selected blobs it touches and +/// leaves the others. Keep selected, off by default: the blob a stroke draws or merges into is not +/// selected afterwards, and the rest of the selection stays; on, it is selected. +#[test] +fn blob_brush_merges_only_with_the_selection_and_keep_selected_off_deselects() { + let mut s = session(); + let r = rect(&mut s, 600.0, 400.0, 50.0, 50.0); + let selected = |s: &Session| s.doc().unwrap().selection.objects.clone(); + let blob = |s: &mut Session, p: serde_json::Value| NodeId(s.execute("path.blob", &p).unwrap()["id"].as_u64().unwrap()); + s.execute("select.set", &json!({"ids": [r.0]})).unwrap(); + let a = blob(&mut s, json!({"points": [[100, 60], [100, 140]], "size": 20})); + assert_eq!(selected(&s), vec![r], "Keep selected is off by default: the new blob is not selected"); + let b = blob(&mut s, json!({"points": [[200, 60], [200, 140]], "size": 20, "keepSelected": true})); + assert_eq!(selected(&s), vec![b], "Keep selected on: the new blob is selected"); + let n = paths(&s).len(); + let (area_a, area_b) = (area(&path(&s, a)), area(&path(&s, b))); + // Only `b` is selected: the stroke across both merges into `b` alone. + let undo = undo_steps(&s); + let across = json!({"points": [[50, 100], [250, 100]], "size": 10, "mergeOnlySelection": true, "keepSelected": false}); + assert_eq!(blob(&mut s, across.clone()), b); + assert_eq!(paths(&s).len(), n); + assert_eq!(area(&path(&s, a)), area_a, "a is left as it was"); + assert!(area(&path(&s, b)) > area_b + 500.0, "the stroke joined b"); + assert!(selected(&s).is_empty()); + assert_eq!(undo_steps(&s), undo + 1); + // With nothing selected, the stroke merges with nothing, and Keep selected off leaves the new + // blob unselected. + let new = blob(&mut s, across); + assert!(new != a && new != b && paths(&s).len() == n + 1, "a new blob"); + assert!(selected(&s).is_empty()); + // A selection that holds no blob: a new blob, and the selection stays. + s.execute("select.set", &json!({"ids": [r.0]})).unwrap(); + let other = blob(&mut s, json!({"points": [[50, 300], [250, 300]], "size": 10, "mergeOnlySelection": true, "keepSelected": false})); + assert_eq!(paths(&s).len(), n + 2); + assert_eq!(selected(&s), vec![r]); + // Without the options, a stroke merges with every touching blob, selected or not, and the + // blob it merges into is not selected. + let all = blob(&mut s, json!({"points": [[50, 100], [250, 100]], "size": 10})); + assert!([a, b, new].contains(&all) && paths(&s).len() == n, "one blob of a, b and the new one"); + assert_eq!(selected(&s), vec![r]); + // Keep selected on: the blob a stroke merges into is selected. + assert_eq!(blob(&mut s, json!({"points": [[150, 80], [150, 120]], "size": 10, "keepSelected": true})), all); + assert_eq!(paths(&s).len(), n); + assert_eq!(selected(&s), vec![all]); + // The tool passes both options; Keep selected is off by default. + s.execute("select.set", &json!({"ids": [other.0]})).unwrap(); + s.select_tool("blobBrush", view()).unwrap(); + assert_eq!(s.tool_options()["keepSelected"], json!(false)); + s.set_tool_option("mergeOnlySelection", &json!(true)); + drag(&mut s, "blobBrush", &[(150.0, 50.0), (150.0, 320.0)], Mods::default()); + assert_eq!(paths(&s).len(), n, "merged into the selected blob only"); + assert!(selected(&s).is_empty()); + assert!(s.doc().unwrap().doc.node(all).is_some() && s.doc().unwrap().doc.node(other).is_some()); +} + /// Three points already curve in the saved path: both ends carry a handle into the curve. #[test] fn path_curvature_with_three_points_saves_end_handles() { diff --git a/crates/tools/src/draw2/gesture.rs b/crates/tools/src/draw2/gesture.rs index 9edf6d5a5..4aef19665 100644 --- a/crates/tools/src/draw2/gesture.rs +++ b/crates/tools/src/draw2/gesture.rs @@ -33,8 +33,11 @@ pub struct GestureTool { pub size: f64, /// Fill new pencil/brush strokes. pub fill: bool, - /// "Keep selected": select the path a Pencil or Paintbrush stroke draws or continues. + /// "Keep selected": select the path a Pencil, Paintbrush or Blob Brush stroke draws, continues + /// or merges into (on for the Pencil and Paintbrush, off for the Blob Brush). pub keep_selected: bool, + /// "Merge only with selection": a Blob Brush stroke merges only with selected paths. + pub merge_only_selection: bool, /// "Edit selected paths within N pixels". pub edit_within: f64, /// "Close paths when ends are within N pixels". @@ -66,7 +69,8 @@ impl GestureTool { fidelity, size: 10.0, fill: false, - keep_selected: true, + keep_selected: id != "blobBrush", + merge_only_selection: false, edit_within: 12.0, close_within: 15.0, snap: DrawSnap::default(), @@ -168,7 +172,15 @@ impl GestureTool { let closed = m.alt || (self.extend.is_none() && near); vec![Action::Preview("path.freehand".into(), self.freehand_params(closed)), Action::Commit] } - "blobBrush" => vec![Action::Exec("path.blob".into(), json!({"points": points_json(pts), "size": self.size}))], + "blobBrush" => vec![Action::Exec( + "path.blob".into(), + json!({ + "points": points_json(pts), + "size": self.size, + "keepSelected": self.keep_selected, + "mergeOnlySelection": self.merge_only_selection + }), + )], "eraser" => vec![Action::Exec("path.eraseRegion".into(), json!({"points": points_json(pts), "size": self.size}))], "knife" if pts.len() >= 2 => vec![Action::Exec("path.knife".into(), json!({"points": points_json(pts)}))], "smooth" if has_sel && pts.len() >= 2 => { @@ -314,7 +326,8 @@ impl Tool for GestureTool { }) } "smooth" => json!({"fidelity": self.fidelity}), - "blobBrush" | "eraser" => json!({"size": self.size}), + "blobBrush" => json!({"size": self.size, "keepSelected": self.keep_selected, "mergeOnlySelection": self.merge_only_selection}), + "eraser" => json!({"size": self.size}), _ => Value::Null, } } @@ -324,6 +337,7 @@ impl Tool for GestureTool { "size" => self.size = v.as_f64().unwrap_or(self.size).clamp(0.1, 1000.0), "fill" => self.fill = v.as_bool().unwrap_or(self.fill), "keepSelected" => self.keep_selected = v.as_bool().unwrap_or(self.keep_selected), + "mergeOnlySelection" => self.merge_only_selection = v.as_bool().unwrap_or(self.merge_only_selection), "editWithin" => self.edit_within = v.as_f64().unwrap_or(self.edit_within).clamp(0.0, 100.0), "closeWithin" => self.close_within = v.as_f64().unwrap_or(self.close_within).clamp(0.0, 100.0), _ => {} @@ -426,6 +440,29 @@ mod tests { } } + /// The Blob Brush has Keep selected and Merge only with selection, both off by default, and its + /// stroke sends both. + #[test] + fn blob_brush_sends_keep_selected_and_merge_only_with_selection() { + let (d, _) = doc_with_rect(); + let s = Selection::default(); + let p = paint(); + let cx = cx(&d, &s, &p); + let mut t = GestureTool::new("blobBrush"); + assert_eq!((t.options()["keepSelected"].clone(), t.options()["mergeOnlySelection"].clone()), (json!(false), json!(false))); + for (keep, only) in [(false, false), (true, false), (false, true)] { + t.set_option("keepSelected", &json!(keep)); + t.set_option("mergeOnlySelection", &json!(only)); + let acts = drag(&mut t, &cx, &[(50.0, 150.0), (250.0, 150.0)], Mods::default()); + assert!( + matches!(&acts[2][0], Action::Exec(c, v) if c == "path.blob" && v["keepSelected"] == keep && v["mergeOnlySelection"] == only), + "{acts:?}" + ); + } + // The Eraser keeps Size only. + assert_eq!(GestureTool::new("eraser").options(), json!({"size": 10.0})); + } + #[test] fn pencil_alt_closes_and_continues_selected_path() { let (d, id) = doc_with_rect(); diff --git a/crates/tools/src/settings.rs b/crates/tools/src/settings.rs index 8a467240d..a03532947 100644 --- a/crates/tools/src/settings.rs +++ b/crates/tools/src/settings.rs @@ -33,6 +33,7 @@ const ROWS: &[Row] = &[ (&["pencil", "paintbrush"], None, &["fidelity", "fill", "keepSelected", "editWithin", "closeWithin"]), (&["smooth"], None, &["fidelity"]), (&["blobBrush", "eraser"], None, &["size"]), + (&["blobBrush"], None, &["keepSelected", "mergeOnlySelection"]), (&["polygon"], None, &["sides"]), (&["star"], None, &["points"]), (&["freeTransform"], None, &["constrain"]), diff --git a/crates/ui-egui/src/dialogs/freehand.rs b/crates/ui-egui/src/dialogs/freehand.rs index cceadb52e..8918cd079 100644 --- a/crates/ui-egui/src/dialogs/freehand.rs +++ b/crates/ui-egui/src/dialogs/freehand.rs @@ -2,9 +2,10 @@ //! Eraser): the options each tool keeps, which last across tool switches and are saved with the //! preferences. OK sets them with `tool.setOption {tool, values}`. //! -//! Fields: `tool`; `fidelity` (points; Pencil, Paintbrush and Smooth), `fill` and `keepSelected` -//! (Pencil and Paintbrush), `closeWithin` and `editWithin` (screen pixels, 0 turns it off; Pencil and -//! Paintbrush) and `size` (points; Blob Brush and Eraser). +//! Fields: `tool`; `fidelity` (points; Pencil, Paintbrush and Smooth), `fill` (Pencil and +//! Paintbrush), `keepSelected` (Pencil, Paintbrush and Blob Brush), `mergeOnlySelection` (Blob +//! Brush), `closeWithin` and `editWithin` (screen pixels, 0 turns it off; Pencil and Paintbrush) and +//! `size` (points; Blob Brush and Eraser). use serde_json::{Map, Value, json}; @@ -23,7 +24,7 @@ pub(super) const SPEC: DialogSpec = DialogSpec { heading, body, confirm, min_wid pub const TOOLS: [&str; 5] = ["pencil", "paintbrush", "smooth", "blobBrush", "eraser"]; /// The options the dialog edits, in the order it shows them (a tool has those it reports). -const KEYS: [&str; 6] = ["fidelity", "size", "fill", "keepSelected", "closeWithin", "editWithin"]; +const KEYS: [&str; 7] = ["fidelity", "size", "fill", "keepSelected", "mergeOnlySelection", "closeWithin", "editWithin"]; /// Width of the label column: wide enough for the tolerances' labels when there are any. fn label_w(d: &Dialog) -> f32 { @@ -101,6 +102,9 @@ fn body(_app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { } if d.fields.contains_key("keepSelected") { check(ui, d, "keepSelected", tl!("Keep selected")); + if d.fields.contains_key("mergeOnlySelection") { + check(ui, d, "mergeOnlySelection", tl!("Merge only with selection")); + } ui.add_space(6.0); } if d.fields.contains_key("closeWithin") { @@ -169,7 +173,7 @@ mod tests { let d = app.ui.dialog.as_ref().unwrap(); assert_eq!((d.f64("fidelity", 0.0), d.bool("fill")), (1.5, false)); assert!(painted(&mut app).contains("Paintbrush Tool Options")); - // Smooth has Fidelity only, the Blob Brush and the Eraser Size only. + // Smooth has Fidelity only, the Eraser Size only, and the Blob Brush Size, Keep selected and Merge only with selection. app.run("tool.options", json!({"tool": "smooth"})).unwrap(); let text = painted(&mut app); assert!(text.contains("Smooth Tool Options") && text.contains("Fidelity:"), "{text}"); @@ -178,6 +182,9 @@ mod tests { app.run("tool.options", json!({"tool": tool})).unwrap(); let text = painted(&mut app); assert!(text.contains("Size:") && !text.contains("Fidelity:"), "{tool}: {text}"); + // The Blob Brush also has Keep selected and Merge only with selection. + let blob = tool == "blobBrush"; + assert_eq!((text.contains("Keep selected"), text.contains("Merge only with selection")), (blob, blob), "{tool}: {text}"); app.ui.dialog.as_mut().unwrap().fields.insert("size".into(), json!(24)); super::super::confirm(&mut app).unwrap(); assert_eq!(app.session.tool_options_of(tool)["size"].as_f64(), Some(24.0), "{tool}"); @@ -250,6 +257,43 @@ mod tests { } } + #[test] + fn the_blob_brush_options_set_what_a_stroke_merges_with_and_selects() { + let mut app = app(); + // A Blob Brush stroke across the canvas at height 100; the ids selected afterwards. + let stroke = |app: &mut VectorcraftApp| { + app.select_tool("blobBrush"); + let view = app.view_info(); + let pointer = |kind, x| vectorcraft_tools::PointerEvent::new(kind, x, 100.0); + crate::canvas::dispatch(app, &pointer(vectorcraft_tools::PointerKind::Down, 20.0), view); + for x in [80.0, 140.0, 200.0, 260.0] { + crate::canvas::dispatch(app, &pointer(vectorcraft_tools::PointerKind::Drag, x), view); + } + crate::canvas::dispatch(app, &pointer(vectorcraft_tools::PointerKind::Up, 300.0), view); + app.session.doc().unwrap().selection.objects.clone() + }; + let ids = |app: &VectorcraftApp| app.session.doc().unwrap().doc.layers[0].children().unwrap().iter().map(|n| n.id).collect::>(); + // Two blobs the stroke crosses, drawn with the same color. + let blob = |app: &mut VectorcraftApp, x: f64| { + let r = app.run("path.blob", json!({"points": [[x, 60], [x, 140]], "size": 20})).unwrap(); + vectorcraft_doc::NodeId(r["id"].as_u64().unwrap()) + }; + let (a, b) = (blob(&mut app, 100.0), blob(&mut app, 240.0)); + app.run("tool.options", json!({"tool": "blobBrush"})).unwrap(); + let d = app.ui.dialog.as_mut().unwrap(); + // Both are off by default. + assert!(!d.bool("keepSelected") && !d.bool("mergeOnlySelection")); + d.fields.insert("keepSelected".into(), json!(true)); + d.fields.insert("mergeOnlySelection".into(), json!(true)); + super::super::confirm(&mut app).unwrap(); + let o = app.session.tool_options_of("blobBrush"); + assert_eq!((o["keepSelected"].as_bool(), o["mergeOnlySelection"].as_bool()), (Some(true), Some(true))); + // With `b` selected, the stroke merges into `b` alone, and leaves it selected. + app.run("select.set", json!({"ids": [b.0]})).unwrap(); + assert_eq!(stroke(&mut app), vec![b]); + assert_eq!(ids(&app), vec![a, b], "merged into b only"); + } + #[test] fn the_other_tools_have_no_freehand_options() { let mut app = app(); diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 35d9a1659..27e066bc8 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -4873,6 +4873,7 @@ theme Appearance Външен вид Darkness Затъмняване Isolate Selected Object Изолиране на избрания обект Keep selected Запазване на избора + Merge only with selection Сливане само с избраното Sample Color from Screen Взимане на цвят от екрана The active document changed while the screen color sampler was open Активният документ се промени, докато взимането на цвят от екрана беше отворено This system has no screen color sampler Тази система няма инструмент за взимане на цвят от екрана diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index ac134fad5..b1ad663a8 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -2110,6 +2110,7 @@ lighting Light Světlo Fidelity: Věrnost: Fill new strokes Vyplňovat nové tahy Keep selected Ponechat vybrané + Merge only with selection Sloučit jen s výběrem The document has no colour swatches. Dokument nemá ve vzorníku žádné barvy. Style Name: Název stylu: Crop To Oříznout na diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 7e986d9ac..e7afaf612 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -1029,6 +1029,7 @@ Fill and Stroke Fläche und Kontur Fill new strokes Neue Striche füllen Keep selected Ausgewählt lassen + Merge only with selection Nur mit Auswahl zusammenfügen Fill Color Flächenfarbe Fill New Type Objects With Placeholder Text Neue Textobjekte mit Platzhaltertext füllen Fill Rule Füllregel diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 9efdca915..654ef1da0 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -4819,6 +4819,7 @@ effect Points Σημεία Intertwine Διαπλοκή Isolate Selected Object Απομόνωση επιλεγμένου αντικειμένου Keep selected Διατήρηση επιλογής + Merge only with selection Συγχώνευση μόνο με την επιλογή Legacy (version 7) Παλαιού τύπου (έκδοση 7) Legacy .ai Options Επιλογές .ai παλαιού τύπου Linked Files Συνδεδεμένα αρχεία diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 47d131831..24943b9f9 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -1045,6 +1045,7 @@ Fill and Stroke Relleno y trazo Fill new strokes Rellenar trazos nuevos Keep selected Mantener seleccionado + Merge only with selection Combinar solo con la selección Fill Color Color de relleno Fill New Type Objects With Placeholder Text Rellenar nuevos objetos de texto con texto de muestra Fill Rule Regla de relleno diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 9744f7667..22b97239f 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -4831,6 +4831,7 @@ mark-style Japanese Japanilainen Intertwine Lomita Isolate Selected Object Eristä valittu objekti Keep selected Pidä valittuna + Merge only with selection Yhdistä vain valintaan Legacy (version 7) Vanha (versio 7) Legacy .ai Options Vanhan .ai-muodon asetukset Linked Files Linkitetyt tiedostot diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 5033040c2..4018fd952 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -1045,6 +1045,7 @@ Fill and Stroke Fond et contour Fill new strokes Remplir les nouveaux contours Keep selected Garder sélectionné + Merge only with selection Fusionner uniquement avec la sélection Fill Color Couleur de fond Fill New Type Objects With Placeholder Text Remplir les nouveaux objets texte avec un texte de substitution Fill Rule Règle de remplissage diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index e4a80ec36..548dc567a 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -1332,6 +1332,7 @@ Fill Color Kitöltőszín Fill new strokes Új vonások kitöltése Keep selected Kijelölés megtartása + Merge only with selection Összeolvasztás csak a kijelöléssel Fill New Type Objects With Placeholder Text Új szövegobjektumok kitöltése helykitöltő szöveggel Fill Rule Kitöltési szabály Fill with Placeholder Text Kitöltés helykitöltő szöveggel diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index ee1e20b53..1b5c1414f 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -1045,6 +1045,7 @@ Fill and Stroke Riempimento e traccia Fill new strokes Riempi nuove tracce Keep selected Mantieni selezionato + Merge only with selection Unisci solo con la selezione Fill Color Colore riempimento Fill New Type Objects With Placeholder Text Riempi i nuovi oggetti di testo con testo segnaposto Fill Rule Regola di riempimento diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index b0eb34426..e1a90f60e 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -1421,6 +1421,7 @@ Fill and Stroke 塗りと線 Fill new strokes 新しいストロークに塗りを適用 Keep selected 選択を保持 + Merge only with selection 選択範囲とのみ結合 Fill: 塗り: Find 検索 Find Font フォントを検索 diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index ed8c0bd4b..9620af943 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -1416,6 +1416,7 @@ Fill and Stroke 칠과 선 Fill new strokes 새 선 칠하기 Keep selected 선택 유지 + Merge only with selection 선택 영역과만 병합 Fill: 칠: Find 찾기 Find Font 글꼴 찾기 diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index a875eae8d..dc11f5479 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -2018,6 +2018,7 @@ lighting Light Światło Fidelity: Wierność: Fill new strokes Wypełniaj nowe pociągnięcia Keep selected Zachowaj zaznaczenie + Merge only with selection Scalaj tylko z zaznaczeniem The document has no colour swatches. Dokument nie zawiera próbek kolorów. Style Name: Nazwa stylu: Crop To Kadruj do diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 0cab8d8c5..45c87e734 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -931,6 +931,7 @@ Fill and Stroke Preenchimento e Traçado Fill new strokes Preencher novos traçados Keep selected Manter selecionado + Merge only with selection Mesclar somente com a seleção Fill with Placeholder Text Preencher com Texto de Placeholder Insert Inline Symbol Inserir Símbolo no Texto Fill: Preenchimento: diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 42c02fc85..342539b89 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -1043,6 +1043,7 @@ Fill and Stroke Заливка и обводка Fill new strokes Заполнять новые обводки Keep selected Оставлять выбранным + Merge only with selection Объединять только с выделенным Fill Color Цвет заливки Fill New Type Objects With Placeholder Text Заполнять новые текстовые объекты текстом-заполнителем Fill Rule Правило заливки diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 95b4b5d0a..b3fe0cfe2 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -4823,6 +4823,7 @@ theme Appearance Utseende Intertwine Sammanfläta Isolate Selected Object Isolera markerat objekt Keep selected Behåll markerad + Merge only with selection Sammanfoga endast med markeringen Legacy (version 7) Äldre (version 7) Legacy .ai Options Alternativ för äldre .ai Linked Files Länkade filer diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 57e161b0f..b3663eb5f 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -1039,6 +1039,7 @@ Fill and Stroke Заливка та обведення Fill new strokes Заповнювати нові обведення Keep selected Залишати вибраним + Merge only with selection Об’єднувати лише з виділеним Fill Color Колір заливки Fill New Type Objects With Placeholder Text Заповнювати нові текстові об’єкти текстом-заповнювачем Fill Rule Правило заливки diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 0a086ba21..f527496ab 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -1210,6 +1210,7 @@ Fill Grid Tô lưới Fill new strokes Tô nét mới Keep selected Giữ trạng thái đã chọn + Merge only with selection Chỉ hợp nhất với vùng chọn Fill New Type Objects With Placeholder Text Tô đối tượng chữ mới bằng văn bản giữ chỗ Fill Rule Quy tắc tô Fill with Placeholder Text Tô bằng văn bản giữ chỗ diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index d0598f470..9de58f23e 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -993,6 +993,7 @@ Fill and Stroke 填色和描边 Fill new strokes 为新描边应用填色 Keep selected 保持选择状态 + Merge only with selection 仅与所选对象合并 Fill Color 填色 Fill New Type Objects With Placeholder Text 用占位文本填充新文字对象 Fill Rule 填充规则 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 32c752315..3f460e7b9 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -1017,6 +1017,7 @@ Fill Color 填色顏色 Fill new strokes 為新筆畫套用填色 Keep selected 保持選取狀態 + Merge only with selection 僅與選取物件合併 Fill New Type Objects With Placeholder Text 以預留位置文字填滿新文字物件 Fill Rule 填色規則 Fill with Placeholder Text 以預留位置文字填滿 diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 7b0ab37fd..a0550909e 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -471,7 +471,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ "tool.options", "Tool Options…", "", - "{tool: id} what double-clicking a tool button opens: hand → fits the artboard in the window (view.fitArtboard), zoom → 100% (view.actualSize), rotate|scale|reflect|shear → that Object › Transform dialog (dialog `rotate`, `scale`, `reflect` or `shear`; error `nothing selected` without a selection), selection|directSelection|groupSelection → the Move dialog (dialog `move`; the same error), gradient|magicWand → the Gradient or Magic Wand panel (window.panel), artboard → Artboard Options of the active artboard (dialog `artboardOptions`, fields index, name, x, y, width, height; OK runs artboard.setProps), flare → Flare Tool Options (dialog `flareOptions`, fields diameter and pathLength (pt), opacity, brightness, growth, fuzziness, longest, rayFuzziness, largest (%), rays, rings, direction (°), raysOn, ringsOn; OK runs tool.setOption {tool: \"flare\", values}: the next flare dragged out uses them, and a Flare click's dialog, which also has x and y, draws one there with them), columnGraph|stackedColumnGraph|barGraph|stackedBarGraph|lineGraph|areaGraph|scatterGraph|pieGraph|radarGraph → Graph Type for the selected graph (dialog `command` running graph.setType; an error without one), symbolSprayer|symbolShifter|symbolScruncher|symbolSizer|symbolSpinner|symbolStainer|symbolScreener|symbolStyler → Symbolism Tools Options (dialog `symbolismOptions`, fields tool, diameter (pt), intensity and density (1–10), shared by the eight tools; OK runs tool.setOption {tool, values}), blend → Blend Options, perspectiveGrid → Perspective Grid Options, eyedropper → Eyedropper Options (dialog `eyedropperOptions`, fields sampleSize, pickUp, apply; OK runs eyedropper.setOptions), printTiling → resets the print tiling (print.tiling.set {reset: true}), warp|twirl|pucker|bloat|scallop|crystallize|wrinkle → that tool's Tool Options (dialog `liquifyOptions`, fields tool, width, height, angle, intensity %, usePressure, detail, simplify, simplifyOn, rate, complexity, horizontal %, vertical %, affectAnchors, affectIn, affectOut, showBrush; OK runs tool.setOption {tool, values}), pencil|paintbrush|smooth|blobBrush|eraser → that tool's Tool Options (dialog `freehandOptions`, fields tool and the options the tool keeps: fidelity (pt), fill, keepSelected, closeWithin and editWithin (px, 0 is off), size (pt); OK runs tool.setOption {tool, values})", + "{tool: id} what double-clicking a tool button opens: hand → fits the artboard in the window (view.fitArtboard), zoom → 100% (view.actualSize), rotate|scale|reflect|shear → that Object › Transform dialog (dialog `rotate`, `scale`, `reflect` or `shear`; error `nothing selected` without a selection), selection|directSelection|groupSelection → the Move dialog (dialog `move`; the same error), gradient|magicWand → the Gradient or Magic Wand panel (window.panel), artboard → Artboard Options of the active artboard (dialog `artboardOptions`, fields index, name, x, y, width, height; OK runs artboard.setProps), flare → Flare Tool Options (dialog `flareOptions`, fields diameter and pathLength (pt), opacity, brightness, growth, fuzziness, longest, rayFuzziness, largest (%), rays, rings, direction (°), raysOn, ringsOn; OK runs tool.setOption {tool: \"flare\", values}: the next flare dragged out uses them, and a Flare click's dialog, which also has x and y, draws one there with them), columnGraph|stackedColumnGraph|barGraph|stackedBarGraph|lineGraph|areaGraph|scatterGraph|pieGraph|radarGraph → Graph Type for the selected graph (dialog `command` running graph.setType; an error without one), symbolSprayer|symbolShifter|symbolScruncher|symbolSizer|symbolSpinner|symbolStainer|symbolScreener|symbolStyler → Symbolism Tools Options (dialog `symbolismOptions`, fields tool, diameter (pt), intensity and density (1–10), shared by the eight tools; OK runs tool.setOption {tool, values}), blend → Blend Options, perspectiveGrid → Perspective Grid Options, eyedropper → Eyedropper Options (dialog `eyedropperOptions`, fields sampleSize, pickUp, apply; OK runs eyedropper.setOptions), printTiling → resets the print tiling (print.tiling.set {reset: true}), warp|twirl|pucker|bloat|scallop|crystallize|wrinkle → that tool's Tool Options (dialog `liquifyOptions`, fields tool, width, height, angle, intensity %, usePressure, detail, simplify, simplifyOn, rate, complexity, horizontal %, vertical %, affectAnchors, affectIn, affectOut, showBrush; OK runs tool.setOption {tool, values}), pencil|paintbrush|smooth|blobBrush|eraser → that tool's Tool Options (dialog `freehandOptions`, fields tool and the options the tool keeps: fidelity (pt), fill, keepSelected (on for pencil and paintbrush, off for blobBrush), mergeOnlySelection (off), closeWithin and editWithin (px, 0 is off), size (pt); OK runs tool.setOption {tool, values})", ), ( "ui.colorGuideOptions", diff --git a/docs/control-protocol.md b/docs/control-protocol.md index 57dca75f2..b7ddb648c 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -764,9 +764,11 @@ events take `pressure` (0..1, default 1): it is the Liquify intensity while Use Freehand Tool Options: double-clicking the Pencil, Paintbrush, Smooth, Blob Brush or Eraser tool (`tool.options {tool: "pencil"}`) opens a `freehandOptions` dialog. Its fields are `tool` and the options the tool -keeps: `fidelity` (pt; Pencil, Paintbrush, Smooth), `fill` and `keepSelected` (Pencil, Paintbrush; `keepSelected` -is on by default, and while it is off the path a stroke draws or continues is not selected afterwards), `closeWithin` -and `editWithin` (screen pixels, 0 turns it off; Pencil, Paintbrush) and `size` (pt; Blob Brush, Eraser). `ui.dialog.confirm` runs +keeps: `fidelity` (pt; Pencil, Paintbrush, Smooth), `fill` (Pencil, Paintbrush), `keepSelected` (Pencil, Paintbrush, +Blob Brush; on by default for the Pencil and Paintbrush and off for the Blob Brush, and while it is off the path a +stroke draws, continues or merges into is not selected afterwards), `mergeOnlySelection` (Blob Brush; off by default, +and while it is on a stroke merges only with selected blobs), `closeWithin` and `editWithin` (screen pixels, 0 turns +it off; Pencil, Paintbrush) and `size` (pt; Blob Brush, Eraser). `ui.dialog.confirm` runs `tool.setOption {tool, values}`. `ui.pointer` events also take `holdMs` (0..60000): the pointer then holds still that long, button down, before the diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 73d2be307..d793fe628 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -498,6 +498,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur In the Swatches panel, Alt-dragging a swatch onto another swatch of its kind (not a tint swatch) replaces that swatch's color, gradient or pattern, and the swatch keeps its name and place. - **Tool double-clicks:** double-clicking a tool's button opens its options: Hand fits the artboard in the window, Zoom shows 100 %, Rotate, Scale, Reflect and Shear open their Object › Transform dialogs, the Selection, Direct Selection and Group Selection tools the Move dialog (Enter opens these seven tools' dialogs too), Gradient, Eyedropper, Blend and the Liquify tools their panels or options, the Pencil, Paintbrush, Smooth, Blob Brush and Eraser tools their Tool Options (Fidelity, fill, Keep selected, the tolerances, the brush size), and (#812) the Flare tool its Flare Tool Options, the Artboard tool the active artboard's Artboard Options, the graph tools Graph Type for the selected graph, the Symbolism tools the Symbolism Tools Options (Diameter, Intensity, Symbol Set Density) and the Magic Wand tool its panel. Keep selected in the Pencil and Paintbrush Tool Options is on by default; while it is off, the path a stroke draws or continues is not selected afterwards and the rest of the selection stays as it was (`path.freehand` `keepSelected`). + The Blob Brush Tool Options have Keep selected and Merge only with selection, both off by default: while Keep selected is off, the blob a stroke draws or merges into is not selected afterwards, and while Merge only with selection is on, a stroke merges only with selected blobs of its color that it touches (`path.blob` `keepSelected` and `mergeOnlySelection`). - **Ruler guides as layer objects:** a guide goes on the current layer, shows, hides, locks and is deleted with it, is listed in the Layers panel (its row selects it) and moves to another layer (`guide.setLayer`, or its row dragged onto that layer's rows); guides on a hidden or locked layer can't be selected, moved or deleted, Duplicate copies a layer's guides and Merge Selected and Flatten Artwork keep them on the merged layer; guides from files saved before guides had layers go on the top visible, unlocked layer when the document opens. Not done: guides stacked among the art of their layer (they are listed above it), a guide row's own eye and lock. Lock Guides also locks the objects made into guides: they can't be selected, moved or deleted (#1543); they are drawn in the guide colour and style of Preferences, and a dragged selection snaps to ruler guides with Smart Guides on (#1583, fixes #1534). The Transform panel shows a selected ruler guide's X or Y, and typing a value moves it (#1659, part of #1597). This is not established by the documentation we could find: the public help pages on guides, rulers and the Transform, Control and Properties panels (read again on 2026-10-14) describe selecting and dragging unlocked guides and the panels' X and Y for a selection, not a guide's position in a panel, and the one public statement of it is a user's [community answer](https://community.adobe.com/questions-652/adding-a-new-custom-guide-785586), which is not documentation. It is kept as a convenience pending the owner's decision, with no parity claim and nothing counted towards a score. - **Layers panel:** every layer, sublayer, group and object is a clickable row at any depth: a click highlights it and makes its layer current (new art goes there), Shift-click a range, Ctrl/Cmd-click toggles. Sublayers are no longer treated as objects: clicks, marquees, Select All and the Select menu take the art inside them, and each sublayer selects in its own colour. Selection squares on rows with selected art (click selects a row's art, Shift adds or removes; dragging the square moves the selection to another row, Alt copies), the eye and lock on every row with drag-down-the-column (one undo step), Ctrl/Cmd-click an eye for per-layer Outline, Alt-click an eye or lock to hide or lock the other layers (again: show or unlock them all), triangles with Alt to open or close everything inside, drag and drop rows above, below or into layers and groups with a drop line (Alt copies) and a dimmed copy of the row under the pointer (dropped on the trash rows are deleted, on New Layer duplicated, as in Illustrator), inline rename, Delete key on highlighted rows. Buttons and the full panel menu: New Layer/Sublayer (Alt: options, Ctrl: on top), Duplicate, Delete, Options for Selection, clipping mask, isolation, Locate Object, Merge Selected, Flatten Artwork, Collect in New Layer, Release to Layers (Sequence and Build), Reverse Order, Template, Hide/Outline/Lock Others and Show/Preview/Unlock All, Paste Remembers Layers, Panel Options (row size, thumbnails per kind, Show Layers Only). Layer Options: name, colour (presets, each with a chip of its colour, or custom), Template, Lock, Show, Print, Preview (outline drawing and clicking) and Dim Images to %. Every operation is a command (`layer.move`, `layer.highlight`, `layer.merge`…) with validation against cycles and invalid nesting. A document opens with only its top-level layers open; the rows left open are saved in the native file (nothing is written for the default) and reopen that way. Only the rows scrolled into view are laid out, from a row list made once per change: a layer of 5,000 groups of 10 paths, all open, draws the panel in 0.4 ms a frame instead of about 29 s (#638). The Filter button at the right of Search All keeps the rows that are of an object kind ticked under By Object (paths, shapes, text, images, effects, groups, linked files, guides) and in a state ticked under By Layer (locked, hidden), with the rows that hold them; a section with nothing ticked takes every row. The filter combines with the search, the button is lit while a filter is on, and Clear All in its popup turns the filter off (`ui.layersFilter`). The filter lasts the session.