diff --git a/crates/engine/src/cmd/create.rs b/crates/engine/src/cmd/create.rs index 587d31ee3..fd2486c74 100644 --- a/crates/engine/src/cmd/create.rs +++ b/crates/engine/src/cmd/create.rs @@ -95,6 +95,12 @@ pub(crate) fn add_node(s: &mut Session, label: &str, kind: NodeKind, appearance: /// Insert a new object looking like `look` (drawing modes apply); select it. pub(crate) fn add_look(s: &mut Session, label: &str, kind: NodeKind, look: NewArt, name: Option) -> Result { + insert_look(s, label, kind, look, name, true) +} + +/// [`add_look`] that selects the new object only when `select` is set; otherwise the selection +/// stays as it was. +pub(crate) fn insert_look(s: &mut Session, label: &str, kind: NodeKind, look: NewArt, name: Option, select: bool) -> Result { let parent = s.doc()?.target_parent()?; let mode = s.draw_mode; let inside = s.draw_inside; @@ -137,7 +143,9 @@ pub(crate) fn add_look(s: &mut Session, label: &str, kind: NodeKind, look: NewAr d.insert(parent, usize::MAX, n)?; } } - sel.set([id]); + if select { + sel.set([id]); + } Ok(id) })?; Ok(json!({ "id": id.0 })) diff --git a/crates/engine/src/cmd/draw2.rs b/crates/engine/src/cmd/draw2.rs index f32585235..4ae53ed50 100644 --- a/crates/engine/src/cmd/draw2.rs +++ b/crates/engine/src/cmd/draw2.rs @@ -12,7 +12,7 @@ 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}; +use super::create::{add_look, add_node, insert_look}; use super::pathops::{Beside, remove_each}; use super::*; use crate::EngineError; @@ -24,7 +24,7 @@ pub fn specs() -> Vec { "Pencil", [], None, - "{points: [[x,y]…] or [[x,y,pressure]…] (pen pressure 0..1: the stroke keeps it, for Calligraphic brushes with Pressure variation; a point without one presses fully), fidelity?: pt (1.5), closed?, style?: \"pencil\"|\"brush\", fill?: bool, extend?: {id, end: \"start\"|\"end\"}} fit a freehand stroke → {id}", + "{points: [[x,y]…] or [[x,y,pressure]…] (pen pressure 0..1: the stroke keeps it, for Calligraphic brushes with Pressure variation; a point without one presses fully), fidelity?: pt (1.5), closed?, style?: \"pencil\"|\"brush\", fill?: bool, keepSelected?: bool (true: the drawn or continued path is selected; false: it is not, and the rest of the selection stays), extend?: {id, end: \"start\"|\"end\"}} fit a freehand stroke → {id}", has_doc, freehand ), @@ -544,6 +544,7 @@ fn freehand(s: &mut Session, p: &Value) -> Result { let tol = f64_or(p, "fidelity", 1.5).clamp(0.05, 100.0); let closed = bool_or(p, "closed", false); let brush = str_param(p, "style") == Some("brush"); + let keep_selected = bool_or(p, "keepSelected", true); if let Some(ext) = p.get("extend").filter(|v| !v.is_null()) { let id = id_param(ext, "id").ok_or_else(|| bad(C, "extend needs id"))?; let at_start = str_param(ext, "end") == Some("start"); @@ -605,7 +606,11 @@ fn freehand(s: &mut Session, p: &Value) -> Result { { st.pressure = if reverse { joined.map(|p| p.reversed()) } else { joined }; } - sel.set([id]); + if keep_selected { + sel.set([id]); + } else { + sel.remove(id); + } Ok(()) })?; return Ok(json!({ "id": id.0 })); @@ -622,7 +627,7 @@ fn freehand(s: &mut Session, p: &Value) -> Result { if let Some(st) = look.appearance.stroke_mut() { st.pressure = pressure; } - add_look(s, if brush { "Paintbrush" } else { "Pencil" }, path_kind(PathData::single(sp)), look, None) + insert_look(s, if brush { "Paintbrush" } else { "Pencil" }, path_kind(PathData::single(sp)), look, None, keep_selected) } // ---------- Curvature ---------- diff --git a/crates/engine/src/tests_draw2.rs b/crates/engine/src/tests_draw2.rs index 4d3f9ab3e..a17db1e9c 100644 --- a/crates/engine/src/tests_draw2.rs +++ b/crates/engine/src/tests_draw2.rs @@ -130,6 +130,39 @@ fn pencil_continues_selected_open_path() { assert_eq!(p.subpaths[0].anchors[0].p, Point::new(100.0, 100.0)); } +/// With Keep selected off, the path a stroke draws or continues is not selected afterwards, the +/// rest of the selection stays, and the stroke is one undo step. +#[test] +fn pencil_with_keep_selected_off_leaves_the_new_path_unselected() { + let mut s = session(); + let r = rect(&mut s, 400.0, 400.0, 50.0, 50.0); + let selected = |s: &Session| s.doc().unwrap().selection.objects.clone(); + // On (the default): the new path replaces the selection. + let on = s.execute("path.freehand", &json!({"points": [[100, 100], [150, 120], [200, 100]]})).unwrap(); + assert_eq!(selected(&s), vec![NodeId(on["id"].as_u64().unwrap())]); + s.execute("select.set", &json!({"ids": [r.0]})).unwrap(); + let undo = undo_steps(&s); + let off = s.execute("path.freehand", &json!({"points": [[100, 200], [150, 220], [200, 200]], "keepSelected": false})).unwrap(); + let off = NodeId(off["id"].as_u64().unwrap()); + assert!(s.doc().unwrap().doc.node(off).is_some()); + assert_eq!(selected(&s), vec![r]); + assert_eq!(undo_steps(&s), undo + 1); + // Continuing a selected open path deselects it and keeps the rest. + let l = line(&mut s, 100.0, 300.0, 200.0, 300.0); + s.execute("select.set", &json!({"ids": [r.0, l.0]})).unwrap(); + s.execute("path.freehand", &json!({"points": [[200, 300], [250, 320], [300, 300]], "extend": {"id": l.0}, "keepSelected": false})).unwrap(); + assert!(path(&s, l).anchor_count() >= 3, "continued"); + assert_eq!(selected(&s), vec![r]); + // The tools pass the option. + s.execute("select.none", &json!({})).unwrap(); + s.select_tool("paintbrush", view()).unwrap(); + s.set_tool_option("keepSelected", &json!(false)); + let before = paths(&s).len(); + drag(&mut s, "paintbrush", &wave(100.0, 300.0, 500.0), Mods::default()); + assert_eq!(paths(&s).len(), before + 1, "a new brush path"); + assert!(selected(&s).is_empty()); +} + #[test] fn paintbrush_uses_stroke_blob_brush_fills() { let mut s = session(); diff --git a/crates/tools/src/draw2/gesture.rs b/crates/tools/src/draw2/gesture.rs index 1de71d7fb..9edf6d5a5 100644 --- a/crates/tools/src/draw2/gesture.rs +++ b/crates/tools/src/draw2/gesture.rs @@ -33,6 +33,8 @@ 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. + pub keep_selected: bool, /// "Edit selected paths within N pixels". pub edit_within: f64, /// "Close paths when ends are within N pixels". @@ -64,6 +66,7 @@ impl GestureTool { fidelity, size: 10.0, fill: false, + keep_selected: true, edit_within: 12.0, close_within: 15.0, snap: DrawSnap::default(), @@ -144,6 +147,7 @@ impl GestureTool { "closed": closed, "style": if self.id == "paintbrush" { "brush" } else { "pencil" }, "fill": self.fill, + "keepSelected": self.keep_selected, }); if let Some((id, start)) = self.extend { v["extend"] = json!({"id": id.0, "end": if start { "start" } else { "end" }}); @@ -301,7 +305,13 @@ impl Tool for GestureTool { fn options(&self) -> Value { match self.id { "pencil" | "paintbrush" => { - json!({"fidelity": self.fidelity, "fill": self.fill, "editWithin": self.edit_within, "closeWithin": self.close_within}) + json!({ + "fidelity": self.fidelity, + "fill": self.fill, + "keepSelected": self.keep_selected, + "editWithin": self.edit_within, + "closeWithin": self.close_within + }) } "smooth" => json!({"fidelity": self.fidelity}), "blobBrush" | "eraser" => json!({"size": self.size}), @@ -313,6 +323,7 @@ impl Tool for GestureTool { "fidelity" => self.fidelity = v.as_f64().unwrap_or(self.fidelity).clamp(0.05, 100.0), "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), "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), _ => {} @@ -395,6 +406,26 @@ mod tests { assert!(!t.busy()); } + /// Keep selected is on by default, and every Pencil and Paintbrush preview sends it. + #[test] + fn pencil_and_paintbrush_send_keep_selected() { + let (d, _) = doc_with_rect(); + let s = Selection::default(); + let p = paint(); + let cx = cx(&d, &s, &p); + for id in ["pencil", "paintbrush"] { + let mut t = GestureTool::new(id); + assert_eq!(t.options()["keepSelected"], json!(true), "{id}"); + for keep in [true, false] { + t.set_option("keepSelected", &json!(keep)); + assert_eq!(t.options()["keepSelected"], json!(keep), "{id}"); + let acts = drag(&mut t, &cx, &[(0.0, 0.0), (10.0, 5.0), (20.0, 0.0)], Mods::default()); + assert!(acts[1..].iter().all(|a| a.iter().all(|a| !matches!(a, Action::Preview(_, v) if v["keepSelected"] != keep))), "{id}"); + assert!(matches!(&acts.last().unwrap()[0], Action::Preview(_, v) if v["keepSelected"] == keep), "{id}"); + } + } + } + #[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 77dfa1da7..8a467240d 100644 --- a/crates/tools/src/settings.rs +++ b/crates/tools/src/settings.rs @@ -30,7 +30,7 @@ const ROWS: &[Row] = &[ (&["spiral"], None, &["decay", "segments", "clockwise"]), (&["rectangularGrid"], None, &["rows", "columns", "rowSkew", "columnSkew", "frame", "fill"]), (&["polarGrid"], None, &["concentric", "radial", "concentricSkew", "radialSkew", "compound", "fill"]), - (&["pencil", "paintbrush"], None, &["fidelity", "fill", "editWithin", "closeWithin"]), + (&["pencil", "paintbrush"], None, &["fidelity", "fill", "keepSelected", "editWithin", "closeWithin"]), (&["smooth"], None, &["fidelity"]), (&["blobBrush", "eraser"], None, &["size"]), (&["polygon"], None, &["sides"]), diff --git a/crates/ui-egui/src/dialogs/freehand.rs b/crates/ui-egui/src/dialogs/freehand.rs index 8a6fd1889..cceadb52e 100644 --- a/crates/ui-egui/src/dialogs/freehand.rs +++ b/crates/ui-egui/src/dialogs/freehand.rs @@ -2,8 +2,8 @@ //! 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` (Pencil and -//! Paintbrush), `closeWithin` and `editWithin` (screen pixels, 0 turns it off; Pencil and +//! 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). use serde_json::{Map, Value, json}; @@ -23,7 +23,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; 5] = ["fidelity", "size", "fill", "closeWithin", "editWithin"]; +const KEYS: [&str; 6] = ["fidelity", "size", "fill", "keepSelected", "closeWithin", "editWithin"]; /// Width of the label column: wide enough for the tolerances' labels when there are any. fn label_w(d: &Dialog) -> f32 { @@ -98,6 +98,9 @@ fn body(_app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { } if d.fields.contains_key("fill") { check(ui, d, "fill", tl!("Fill new strokes")); + } + if d.fields.contains_key("keepSelected") { + check(ui, d, "keepSelected", tl!("Keep selected")); ui.add_space(6.0); } if d.fields.contains_key("closeWithin") { @@ -170,7 +173,7 @@ mod tests { app.run("tool.options", json!({"tool": "smooth"})).unwrap(); let text = painted(&mut app); assert!(text.contains("Smooth Tool Options") && text.contains("Fidelity:"), "{text}"); - assert!(!text.contains("Fill new strokes") && !text.contains("Size:"), "{text}"); + assert!(!text.contains("Fill new strokes") && !text.contains("Keep selected") && !text.contains("Size:"), "{text}"); for tool in ["blobBrush", "eraser"] { app.run("tool.options", json!({"tool": tool})).unwrap(); let text = painted(&mut app); @@ -208,6 +211,45 @@ mod tests { assert!(smooth < precise, "{smooth} anchors at fidelity 12, {precise} at the default"); } + #[test] + fn the_new_path_is_selected_only_with_keep_selected_on() { + let mut app = app(); + // A stroke of `tool` along height `y0`; the ids selected afterwards and the new path's id. + let stroke = |app: &mut VectorcraftApp, tool: &str, y0: f64| { + app.select_tool(tool); + let view = app.view_info(); + let pointer = |kind, x| vectorcraft_tools::PointerEvent::new(kind, x, y0); + crate::canvas::dispatch(app, &pointer(vectorcraft_tools::PointerKind::Down, 20.0), view); + for x in [60.0, 100.0, 140.0] { + crate::canvas::dispatch(app, &pointer(vectorcraft_tools::PointerKind::Drag, x), view); + } + crate::canvas::dispatch(app, &pointer(vectorcraft_tools::PointerKind::Up, 180.0), view); + let st = app.session.doc().unwrap(); + (st.selection.objects.clone(), st.doc.layers[0].children().unwrap().last().unwrap().id) + }; + for tool in ["pencil", "paintbrush"] { + app.run("select.set", json!({"ids": []})).unwrap(); + // On by default: the new path is selected. + app.run("tool.options", json!({"tool": tool})).unwrap(); + assert!(painted(&mut app).contains("Keep selected"), "{tool}"); + let d = app.ui.dialog.as_mut().unwrap(); + assert!(d.bool("keepSelected"), "{tool}"); + super::super::confirm(&mut app).unwrap(); + let (selected, id) = stroke(&mut app, tool, 100.0); + assert_eq!(selected, vec![id], "{tool}"); + // Off: the next stroke leaves the selection as it was. + app.run("tool.options", json!({"tool": tool})).unwrap(); + app.ui.dialog.as_mut().unwrap().fields.insert("keepSelected".into(), json!(false)); + super::super::confirm(&mut app).unwrap(); + // It lasts while another tool is chosen. + app.select_tool("selection"); + assert_eq!(app.session.tool_options_of(tool)["keepSelected"].as_bool(), Some(false), "{tool}"); + let (selected, new) = stroke(&mut app, tool, 200.0); + assert_ne!(new, id, "{tool} drew a new path"); + assert_eq!(selected, vec![id], "{tool}"); + } + } + #[test] fn the_other_tools_have_no_freehand_options() { let mut app = app(); diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 71d0548d0..df7e3cc8c 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -2086,6 +2086,7 @@ lighting Light Světlo Edit selected paths within: Upravovat vybrané cesty do: Fidelity: Věrnost: Fill new strokes Vyplňovat nové tahy + Keep selected Ponechat vybrané 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 68eba1886..a6be1ecc9 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -1024,6 +1024,7 @@ Fill (X), double-click for the Color Picker Fläche (X), Doppelklick öffnet den Farbwähler Fill and Stroke Fläche und Kontur Fill new strokes Neue Striche füllen + Keep selected Ausgewählt lassen 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/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 8a5717927..d26c97e85 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -1040,6 +1040,7 @@ Fill (X), double-click for the Color Picker Relleno (X), doble clic para el selector de color Fill and Stroke Relleno y trazo Fill new strokes Rellenar trazos nuevos + Keep selected Mantener seleccionado 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/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 1ef3d9db3..1373d0a9c 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -1040,6 +1040,7 @@ Fill (X), double-click for the Color Picker Fond (X), double-cliquez pour ouvrir le Sélecteur de couleurs Fill and Stroke Fond et contour Fill new strokes Remplir les nouveaux contours + Keep selected Garder sélectionné 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 5989a4079..eb5fa33e9 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -1328,6 +1328,7 @@ Fill and Stroke Kitöltés és körvonal Fill Color Kitöltőszín Fill new strokes Új vonások kitöltése + Keep selected Kijelölés megtartása 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 4fc5a29b2..20bb16835 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -1040,6 +1040,7 @@ Fill (X), double-click for the Color Picker Riempimento (X), fai doppio clic per il selettore colore Fill and Stroke Riempimento e traccia Fill new strokes Riempi nuove tracce + Keep selected Mantieni selezionato 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 c126dac5b..4075776dd 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -1410,6 +1410,7 @@ Fill Rule 塗りのルール Fill and Stroke 塗りと線 Fill new strokes 新しいストロークに塗りを適用 + Keep selected 選択を保持 Fill: 塗り: Find 検索 Find Font フォントを検索 diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index ff0a0b02f..caf128085 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -1405,6 +1405,7 @@ Fill Rule 칠 규칙 Fill and Stroke 칠과 선 Fill new strokes 새 선 칠하기 + Keep selected 선택 유지 Fill: 칠: Find 찾기 Find Font 글꼴 찾기 diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 9af5c261d..8d774e5bc 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -1993,6 +1993,7 @@ lighting Light Światło Edit selected paths within: Edytuj zaznaczone ścieżki w odległości: Fidelity: Wierność: Fill new strokes Wypełniaj nowe pociągnięcia + Keep selected Zachowaj zaznaczenie 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 0594bf829..d2c9b6ffd 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -929,6 +929,7 @@ Fill Rule Regra de Preenchimento Fill and Stroke Preenchimento e Traçado Fill new strokes Preencher novos traçados + Keep selected Manter selecionado 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 f03b1efdf..e5358e36c 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -1038,6 +1038,7 @@ Fill (X), double-click for the Color Picker Заливка (X), дважды щёлкните для палитры цветов Fill and Stroke Заливка и обводка Fill new strokes Заполнять новые обводки + Keep selected Оставлять выбранным Fill Color Цвет заливки Fill New Type Objects With Placeholder Text Заполнять новые текстовые объекты текстом-заполнителем Fill Rule Правило заливки diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 87fbe7b57..7caf61a39 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -1034,6 +1034,7 @@ Fill (X), double-click for the Color Picker Заливка (X), двічі клацніть на палітрі кольорів Fill and Stroke Заливка та обведення Fill new strokes Заповнювати нові обведення + Keep selected Залишати вибраним 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 ad468749a..651340d5a 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -1206,6 +1206,7 @@ Fill Color Màu tô Fill Grid Tô lưới Fill new strokes Tô nét mới + Keep selected Giữ trạng thái đã 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 3dd105ef2..bf004f86d 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -992,6 +992,7 @@ Fill (X), double-click for the Color Picker 填色 (X),双击打开拾色器 Fill and Stroke 填色和描边 Fill new strokes 为新描边应用填色 + Keep selected 保持选择状态 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 920b54bae..635ced8ee 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -1016,6 +1016,7 @@ Fill and Stroke 填色與筆畫 Fill Color 填色顏色 Fill new strokes 為新筆畫套用填色 + Keep selected 保持選取狀態 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 997100bbc..591c5eec5 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -443,7 +443,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, 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, 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 ff1180434..47641789b 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -754,8 +754,9 @@ 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` (Pencil, Paintbrush), `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` 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 `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 e2103e414..bdc7ff15a 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -470,7 +470,8 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - **Temporary tools:** Cmd with any other tool drags with the selection tool used last (Selection, Direct Selection or Group Selection). Held Space pans and Cmd+Space zooms (Cmd+Alt+Space out) whatever the tool. - **Swatches panel clicks:** Ctrl/Cmd-clicking New Swatch makes a spot color, and Ctrl/Cmd+Shift-clicking it makes a global process color. - **Panel drag and drop:** art dragged off the canvas onto the Symbols panel becomes a symbol, onto the Swatches panel a pattern swatch and onto the Brushes panel an Art brush; a symbol dragged onto the canvas places an instance where it is dropped, and a brush dragged onto a path is applied to it. - - **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, 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. + - **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`). - **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). `object.setProps` with `visible: false` or `locked: true` deselects every selected object that is then hidden or locked, the key object included, as the eye and lock columns do. diff --git a/docs/ui-parity.md b/docs/ui-parity.md index aa037d9da..222d8fa11 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -54,7 +54,7 @@ tools, never checked side by side) isn't captured row by row. | Each anchor snaps to Smart Guides and construction angles | done | `DrawSnap` | | Curvature tool: click/drag points, double-click or Alt-click for a corner, temporary Direct Selection, point conversion | partial | circular point markers without direction handles or duplicate markers, Anchor Size and Hide Edges regression-tested (#1236, in part); existing-path interpolation request and modifiers requested in #975 remain | | Add/Delete/Anchor Point tools; Anchor Point tool drags segments to reshape | done | | -| Pencil: Fidelity, Keep selected, Alt to Smooth, edit selected paths, close by proximity | partial | Keep selected and Alt to Smooth are missing; a filled Pencil path keeps a stroke of None (#1804, part of #1679) | +| Pencil: Fidelity, Keep selected, Alt to Smooth, edit selected paths, close by proximity | partial | Alt to Smooth is missing; with Keep selected (Pencil and Paintbrush, on by default) off, the path a stroke draws or continues is not selected afterwards; a filled Pencil path keeps a stroke of None (#1804, part of #1679) | | Join tool scrubbing, Average, Join | done | Join merges the direct-selected anchors lying on one another along a path, as after Average, into one (#1533) | | Average and Join in one step, with a corner or smooth choice for the joined anchor | missing | `path.join` and `path.average` are separate commands, and Join has no corner/smooth choice; [gap 1](gaps.md) | | Smooth tool smooths only the stretch of path under the drag | done | #1512 |