Select: Update Selection replaces the recalled saved selection with the current selection

The Select menu had no Update Selection: to add objects to a saved
selection or take objects out of it, the selection had to be saved
again under the same name. Select › Update Selection (`select.update`)
now gives the saved selection last recalled the objects that are
selected, in one undo step. It is enabled while objects are selected
and the document still holds that saved selection.

Each open document keeps session ids for its saved selections, so the
recalled one is found under a new name after Edit Selection…, and after
Undo and Redo. Deleting it disables Update Selection until another
saved selection is recalled or the delete is undone.

Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
dotCooCoo
2026-10-16 15:27:54 +07:00
committed by bflatastic
parent 0ca6503f09
commit 56acb8d3b6
25 changed files with 311 additions and 5 deletions
+60 -3
View File
@@ -100,6 +100,15 @@ pub fn specs() -> Vec<CommandSpec> {
has_selection,
save_selection
),
cmd!(
"select.update",
"Update Selection",
["Select"],
None,
"{} make the saved selection last recalled (`select.recall`) hold the selected objects instead of its own, in one undo step → {name}",
has_recalled_selection,
update_selection
),
cmd!(
"select.recall",
"Recall Selection",
@@ -342,6 +351,7 @@ fn save_selection(s: &mut Session, p: &Value) -> Result<Value> {
if existing.is_none() && saved.len() >= SavedSelection::MAX {
return Err(bad(C, format!("a document keeps at most {} saved selections", SavedSelection::MAX)));
}
let mut slots = saved_ids(s)?;
s.edit("Save Selection", |d, _| {
let entry = SavedSelection { name: name.clone(), objects: ids };
match existing.and_then(|i| d.saved_selections.get_mut(i)) {
@@ -350,18 +360,26 @@ fn save_selection(s: &mut Session, p: &Value) -> Result<Value> {
}
Ok(())
})?;
let st = s.doc_mut()?;
if existing.is_none() {
slots.push(st.recalled_selection.new_id());
}
st.recalled_selection.remember(&st.doc.saved_selections, slots);
Ok(json!({ "name": name }))
}
fn recall_selection(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "select.recall";
let name = str_param(p, "name").ok_or_else(|| bad(C, "missing name"))?;
let ids = s.doc()?.doc.saved_selections.iter().find(|x| x.name == name).map(|x| x.objects.clone());
let ids = ids.ok_or_else(|| bad(C, format!("no saved selection `{name}`")))?;
let saved = &s.doc()?.doc.saved_selections;
let i = saved.iter().position(|x| x.name == name).ok_or_else(|| bad(C, format!("no saved selection `{name}`")))?;
let ids = saved.get(i).map(|x| x.objects.clone()).unwrap_or_default();
let st = s.doc()?;
let can = selectable_ids(&st.doc, st.isolation);
s.select(|_, sel| sel.set(ids.into_iter().filter(|id| can.contains(id))))?;
Ok(json!({ "count": s.doc()?.selection.len() }))
let st = s.doc_mut()?;
st.recalled_selection.recall(&st.doc.saved_selections, i);
Ok(json!({ "count": st.selection.len() }))
}
/// Recall and Edit Selection… need a document with at least one saved selection.
@@ -370,6 +388,42 @@ fn has_saved_selection(s: &Session) -> std::result::Result<(), String> {
if st.doc.saved_selections.is_empty() { Err("no saved selections".into()) } else { Ok(()) }
}
/// The session ids of the active document's saved selections ([`crate::RecalledSelection`]).
fn saved_ids(s: &mut Session) -> Result<Vec<u64>> {
let st = s.doc_mut()?;
Ok(st.recalled_selection.ids(&st.doc.saved_selections))
}
/// Where the saved selection last recalled is, while the document has it.
fn recalled_index(st: &crate::DocState) -> Option<usize> {
st.recalled_selection.index_in(&st.doc.saved_selections)
}
/// Update Selection needs selected objects and a recalled saved selection the document still has.
fn has_recalled_selection(s: &Session) -> std::result::Result<(), String> {
has_selection(s)?;
let st = s.active().ok_or("no document open")?;
if recalled_index(st).is_some() { Ok(()) } else { Err("no saved selection recalled".into()) }
}
fn update_selection(s: &mut Session, _: &Value) -> Result<Value> {
const C: &str = "select.update";
let st = s.doc()?;
let i = recalled_index(st).ok_or_else(|| bad(C, "no saved selection recalled"))?;
let ids = st.selection.objects.clone();
let name = st.doc.saved_selections.get(i).map(|x| x.name.clone()).unwrap_or_default();
let slots = saved_ids(s)?;
s.edit("Update Selection", |d, _| {
if let Some(x) = d.saved_selections.get_mut(i) {
x.objects = ids;
}
Ok(())
})?;
let st = s.doc_mut()?;
st.recalled_selection.remember(&st.doc.saved_selections, slots);
Ok(json!({ "name": name }))
}
fn edit_saved(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "select.editSaved";
let edits = match p.get("edits").and_then(Value::as_array) {
@@ -396,6 +450,7 @@ fn edit_saved(s: &mut Session, p: &Value) -> Result<Value> {
x.name = n;
}
}
let slots: Vec<u64> = saved_ids(s)?.into_iter().zip(&after).filter(|(_, x)| x.is_some()).map(|(id, _)| id).collect();
let after: Vec<SavedSelection> = after.into_iter().flatten().collect();
let mut names = std::collections::HashSet::with_capacity(after.len());
for x in &after {
@@ -408,6 +463,8 @@ fn edit_saved(s: &mut Session, p: &Value) -> Result<Value> {
d.saved_selections = after.into();
Ok(())
})?;
let st = s.doc_mut()?;
st.recalled_selection.remember(&st.doc.saved_selections, slots);
}
ok()
}
+55
View File
@@ -300,6 +300,9 @@ pub struct DocState {
/// What Select → Reselect repeats: the last selection command, its params and the objects
/// selected when it ran (a Same command's reference objects).
pub last_selection_cmd: Option<(String, Value, Vec<NodeId>)>,
/// The saved selection last recalled (`select.recall`), which Select → Update Selection
/// changes. It is not saved with the file, and a recall is not an undo step.
pub recalled_selection: RecalledSelection,
/// Process-unique id of this open document (tab indices shift when tabs close).
pub uid: u64,
/// View Opacity Mask (Alt-click the mask thumbnail): the masked object whose mask the canvas
@@ -387,6 +390,57 @@ impl OpenRows {
}
}
/// The saved selection last recalled, which Select → Update Selection changes. The Select commands
/// give each saved selection an id for the session and keep the ids for every version of the
/// document's saved selections they make or read, so the recalled one is found again after Edit
/// Selection…, Undo and Redo. A version that nothing holds any more (neither the document nor its
/// undo history) is dropped the next time a version is added.
#[derive(Clone, Debug, Default)]
pub struct RecalledSelection {
/// The recalled saved selection's id.
id: Option<u64>,
/// Each version of the saved selections (identified by its allocation), with the id of each saved
/// selection in it.
versions: Vec<(std::sync::Weak<Vec<vectorcraft_doc::SavedSelection>>, Vec<u64>)>,
/// The last id given.
last: u64,
}
impl RecalledSelection {
/// The ids of `list`'s saved selections; a version not met before gets new ones.
pub(crate) fn ids(&mut self, list: &vectorcraft_doc::Shared<Vec<vectorcraft_doc::SavedSelection>>) -> Vec<u64> {
let version = vectorcraft_doc::Shared::downgrade(list);
if let Some((_, ids)) = self.versions.iter().find(|(v, _)| v.ptr_eq(&version)) {
return ids.clone();
}
let ids: Vec<u64> = (0..list.len()).map(|_| self.new_id()).collect();
self.remember(list, ids.clone());
ids
}
/// An id no saved selection has had.
pub(crate) fn new_id(&mut self) -> u64 {
self.last += 1;
self.last
}
/// Keep `ids` as those of `list`'s saved selections, dropping versions nothing holds any more.
pub(crate) fn remember(&mut self, list: &vectorcraft_doc::Shared<Vec<vectorcraft_doc::SavedSelection>>, ids: Vec<u64>) {
let version = vectorcraft_doc::Shared::downgrade(list);
self.versions.retain(|(v, _)| v.strong_count() > 0 && !v.ptr_eq(&version));
self.versions.push((version, ids));
}
/// Recall the saved selection at `index` in `list`.
pub(crate) fn recall(&mut self, list: &vectorcraft_doc::Shared<Vec<vectorcraft_doc::SavedSelection>>, index: usize) {
self.id = self.ids(list).get(index).copied();
}
/// Where the recalled saved selection is in `list`, if `list` holds it.
pub fn index_in(&self, list: &vectorcraft_doc::Shared<Vec<vectorcraft_doc::SavedSelection>>) -> Option<usize> {
let id = self.id?;
let version = vectorcraft_doc::Shared::downgrade(list);
let (_, ids) = self.versions.iter().find(|(v, _)| v.ptr_eq(&version))?;
ids.iter().position(|&x| x == id).filter(|&i| i < list.len())
}
}
impl DocState {
pub fn new(mut doc: Document, path: Option<String>) -> Self {
let active_layer = doc.default_layer();
@@ -411,6 +465,7 @@ impl DocState {
undo_group: None,
last_transform: None,
last_selection_cmd: None,
recalled_selection: RecalledSelection::default(),
uid: NEXT_DOC_UID.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
mask_view: None,
transparency_grid: false,
+135
View File
@@ -1204,6 +1204,141 @@ fn save_and_recall_selection() {
assert!(s.execute("select.recall", &json!({"name": "Pair"})).is_err());
}
#[test]
fn update_selection_replaces_the_recalled_saved_selection() {
let mut s = session();
let a = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
let b = rect(&mut s, 20.0, 0.0, 10.0, 10.0);
let c = rect(&mut s, 40.0, 0.0, 10.0, 10.0);
sel(&mut s, &[a, b]);
s.execute("select.save", &json!({"name": "A"})).unwrap();
s.execute("select.save", &json!({"name": "Other"})).unwrap();
// Nothing recalled yet: Update Selection is off.
assert!(s.execute("select.update", &json!({})).is_err());
s.execute("select.none", &json!({})).unwrap();
s.execute("select.recall", &json!({"name": "A"})).unwrap();
s.execute("select.add", &json!({"ids": [c.0]})).unwrap();
s.doc_mut().unwrap().mark_saved();
assert_eq!(s.execute("select.update", &json!({})).unwrap()["name"], "A");
assert!(s.doc().unwrap().is_dirty());
s.execute("select.none", &json!({})).unwrap();
assert_eq!(s.execute("select.recall", &json!({"name": "A"})).unwrap()["count"], 3);
assert_eq!(selected(&s), vec![a, b, c]);
// Only the recalled one changes.
s.execute("select.recall", &json!({"name": "Other"})).unwrap();
assert_eq!(selected(&s), vec![a, b]);
// It travels with the file.
let loaded = vectorcraft_format::load(&vectorcraft_format::save_file(&s.doc().unwrap().doc)).unwrap();
assert_eq!(loaded.saved_selections.iter().map(|x| x.objects.len()).collect::<Vec<_>>(), [3, 2]);
// It is one undo step.
s.execute("edit.undo", &json!({})).unwrap();
s.execute("select.recall", &json!({"name": "A"})).unwrap();
assert_eq!(selected(&s), vec![a, b]);
// Nothing selected: Update Selection is off.
s.execute("select.none", &json!({})).unwrap();
assert!(s.execute("select.update", &json!({})).is_err());
// It can run again without another recall.
sel(&mut s, &[c]);
s.execute("select.update", &json!({})).unwrap();
sel(&mut s, &[b]);
s.execute("select.update", &json!({})).unwrap();
s.execute("select.recall", &json!({"name": "A"})).unwrap();
assert_eq!(selected(&s), vec![b]);
}
#[test]
fn update_selection_follows_a_rename_and_stops_after_a_delete() {
let mut s = session();
let a = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
let b = rect(&mut s, 20.0, 0.0, 10.0, 10.0);
sel(&mut s, &[a]);
s.execute("select.save", &json!({"name": "One"})).unwrap();
s.execute("select.recall", &json!({"name": "One"})).unwrap();
s.execute("select.editSaved", &json!({"name": "One", "newName": "Uno"})).unwrap();
sel(&mut s, &[a, b]);
assert_eq!(s.execute("select.update", &json!({})).unwrap()["name"], "Uno");
s.execute("select.recall", &json!({"name": "Uno"})).unwrap();
assert_eq!(selected(&s), vec![a, b]);
// A saved selection of the same name saved after the delete is not the recalled one.
s.execute("select.editSaved", &json!({"name": "Uno", "delete": true})).unwrap();
s.execute("select.save", &json!({"name": "Uno"})).unwrap();
assert!(s.execute("select.update", &json!({})).is_err());
}
#[test]
fn update_selection_changes_the_recalled_saved_selection_after_undo_and_redo() {
let mut s = session();
let a = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
let b = rect(&mut s, 20.0, 0.0, 10.0, 10.0);
let c = rect(&mut s, 40.0, 0.0, 10.0, 10.0);
let recall = |s: &mut Session, name: &str| {
s.execute("select.recall", &json!({"name": name})).unwrap();
selected(s)
};
sel(&mut s, &[a]);
s.execute("select.save", &json!({"name": "A"})).unwrap();
sel(&mut s, &[b]);
s.execute("select.save", &json!({"name": "B"})).unwrap();
recall(&mut s, "A");
// Swap the two names, then undo the swap: Update Selection still changes the one recalled.
let swap = json!({"edits": [{"name": "A", "newName": "B"}, {"name": "B", "newName": "A"}]});
s.execute("select.editSaved", &swap).unwrap();
s.execute("edit.undo", &json!({})).unwrap();
sel(&mut s, &[a, c]);
s.execute("select.update", &json!({})).unwrap();
assert_eq!((recall(&mut s, "A"), recall(&mut s, "B")), (vec![a, c], vec![b]));
// Undo and redo a rename of the recalled one.
recall(&mut s, "A");
s.execute("select.editSaved", &json!({"name": "A", "newName": "First"})).unwrap();
s.execute("edit.undo", &json!({})).unwrap();
s.execute("edit.redo", &json!({})).unwrap();
sel(&mut s, &[c]);
assert_eq!(s.execute("select.update", &json!({})).unwrap()["name"], "First");
s.execute("edit.undo", &json!({})).unwrap();
s.execute("edit.undo", &json!({})).unwrap();
sel(&mut s, &[c]);
assert_eq!(s.execute("select.update", &json!({})).unwrap()["name"], "A");
assert_eq!(recall(&mut s, "A"), vec![c]);
// Undo and redo an Edit Selection… that renamed it and deleted the other one.
recall(&mut s, "A");
let edits = json!({"edits": [{"name": "A", "newName": "First"}, {"name": "B", "delete": true}]});
s.execute("select.editSaved", &edits).unwrap();
s.execute("edit.undo", &json!({})).unwrap();
s.execute("edit.redo", &json!({})).unwrap();
sel(&mut s, &[a]);
assert_eq!(s.execute("select.update", &json!({})).unwrap()["name"], "First");
s.execute("edit.undo", &json!({})).unwrap();
s.execute("edit.undo", &json!({})).unwrap();
sel(&mut s, &[a]);
assert_eq!(s.execute("select.update", &json!({})).unwrap()["name"], "A");
assert_eq!((recall(&mut s, "A"), recall(&mut s, "B")), (vec![a], vec![b]));
// A cancelled undo group that renamed it.
recall(&mut s, "B");
s.execute("edit.undoGroup.begin", &json!({})).unwrap();
s.execute("select.editSaved", &json!({"name": "B", "newName": "Second"})).unwrap();
s.execute("edit.undoGroup.end", &json!({"cancel": true})).unwrap();
sel(&mut s, &[a, b]);
assert_eq!(s.execute("select.update", &json!({})).unwrap()["name"], "B");
assert_eq!(recall(&mut s, "B"), vec![a, b]);
// Undo an Update Selection that gave it the other one's objects.
recall(&mut s, "A");
sel(&mut s, &[a, b]);
s.execute("select.update", &json!({})).unwrap();
s.execute("edit.undo", &json!({})).unwrap();
sel(&mut s, &[c]);
assert_eq!(s.execute("select.update", &json!({})).unwrap()["name"], "A");
assert_eq!((recall(&mut s, "A"), recall(&mut s, "B")), (vec![c], vec![a, b]));
// Save Selection made after the recall, and undone.
recall(&mut s, "A");
s.execute("select.save", &json!({"name": "C"})).unwrap();
sel(&mut s, &[b]);
assert_eq!(s.execute("select.update", &json!({})).unwrap()["name"], "A");
s.execute("edit.undo", &json!({})).unwrap();
s.execute("edit.undo", &json!({})).unwrap();
sel(&mut s, &[a]);
assert_eq!(s.execute("select.update", &json!({})).unwrap()["name"], "A");
}
#[test]
fn edit_selection_needs_a_saved_selection_and_a_free_name() {
let mut s = session();
+2
View File
@@ -2484,6 +2484,7 @@
Save Save for Web Preset Запази предварителна настройка за „Запази за уеб“
Save selected colors as swatches Запази избраните цветове като образци
Save Selection… Запази селекцията…
Update Selection Актуализиране на селекцията
Save Swatch Library Запази библиотека с образци
Save Swatch Library… Запази библиотека с образци…
Save the current layout under this name Запази текущото разположение под това име
@@ -3745,6 +3746,7 @@ axis Both И двете
@msg no saved presets няма запазени предварителни настройки
@msg no saved print preset named `{name}` ({DEFAULT_PRESET} is protected: start a new one from it) няма запазена предварителна настройка за печат с име „{name}“ ({DEFAULT_PRESET} е защитена: създайте нова от нея)
@msg no saved profiles няма запазени профили
@msg no saved selection recalled няма възстановена запазена селекция
@msg no saved selections няма запазени селекции
@msg no saved themes няма запазени теми
@msg no segment {segment} on line {line} няма сегмент {segment} на ред {line}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -391,6 +391,7 @@
Area Text Objects Textové objekty v oblasti
Start Global Edit Zahájit globální úpravy
Save Selection… Uložit výběr…
Update Selection Aktualizovat výběr
Edit Selection… Upravit výběr…
Effect Efekt
Apply Last Effect Použít poslední efekt
@@ -4119,6 +4120,7 @@ imageTrace Outline Obrys
@msg no saved presets žádná uložená přednastavení
@msg no saved print preset named `{name}` ({DEFAULT_PRESET} is protected: start a new one from it) uložené přednastavení tisku s názvem `{name}` neexistuje ({DEFAULT_PRESET} je chráněné: vytvořte z něj nové)
@msg no saved profiles žádné uložené profily
@msg no saved selection recalled není vyvolán žádný uložený výběr
@msg no saved selections žádné uložené výběry
@msg no saved themes žádné uložené motivy
@msg no segment {segment} on line {line} na čáře {line} není segment {segment}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -2482,6 +2482,7 @@
Save Save for Web Preset Vorgabe für „Für Web speichern“ speichern
Save selected colors as swatches Ausgewählte Farben als Farbfelder speichern
Save Selection… Auswahl speichern…
Update Selection Auswahl aktualisieren
Save Swatch Library Farbfeldbibliothek speichern
Save Swatch Library… Farbfeldbibliothek speichern…
Save the current layout under this name Das aktuelle Layout unter diesem Namen speichern
@@ -3724,6 +3725,7 @@ theme Appearance Erscheinungsbild
@msg no saved presets keine gespeicherten Vorgaben
@msg no saved print preset named `{name}` ({DEFAULT_PRESET} is protected: start a new one from it) keine gespeicherte Druckvorgabe namens `{name}` ({DEFAULT_PRESET} ist geschützt: Erstellen Sie daraus eine neue)
@msg no saved profiles keine gespeicherten Profile
@msg no saved selection recalled keine gespeicherte Auswahl abgerufen
@msg no saved selections keine gespeicherten Auswahlen
@msg no saved themes keine gespeicherten Themen
@msg no segment {segment} on line {line} kein Segment {segment} auf Linie {line}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -1627,6 +1627,7 @@
Save for Web (Legacy)… Αποθήκευση για ιστό (παλαιού τύπου)…
Save Preset… Αποθήκευση προεπιλογής…
Save Selection… Αποθήκευση επιλογής…
Update Selection Ενημέρωση επιλογής
Saved Αποθηκεύτηκε
Save… Αποθήκευση…
Scale Κλιμάκωση
@@ -3783,6 +3784,7 @@ theme Appearance Εμφάνιση
@msg no saved presets δεν υπάρχουν αποθηκευμένες προεπιλογές
@msg no saved print preset named `{name}` ({DEFAULT_PRESET} is protected: start a new one from it) δεν υπάρχει αποθηκευμένη προεπιλογή εκτύπωσης με όνομα `{name}` (η {DEFAULT_PRESET} προστατεύεται: δημιουργήστε νέα από αυτήν)
@msg no saved profiles δεν υπάρχουν αποθηκευμένα προφίλ
@msg no saved selection recalled δεν έχει ανακληθεί αποθηκευμένη επιλογή
@msg no saved selections δεν υπάρχουν αποθηκευμένες επιλογές
@msg no saved themes δεν υπάρχουν αποθηκευμένα θέματα
@msg no segment {segment} on line {line} δεν υπάρχει τμήμα {segment} στη γραμμή {line}
Can't render this file because it contains an unexpected character in line 1862 and column 19.
+2
View File
@@ -2530,6 +2530,7 @@
Save Save for Web Preset Guardar preajuste de Guardar para web
Save selected colors as swatches Guardar los colores seleccionados como muestras
Save Selection… Guardar selección…
Update Selection Actualizar selección
Save Swatch Library Guardar biblioteca de muestras
Save Swatch Library… Guardar biblioteca de muestras…
Save the current layout under this name Guardar el diseño actual con este nombre
@@ -3820,6 +3821,7 @@ axis Both Ambos
@msg no saved presets no hay preajustes guardados
@msg no saved print preset named `{name}` ({DEFAULT_PRESET} is protected: start a new one from it) no hay ningún preajuste de impresión guardado llamado `{name}` ({DEFAULT_PRESET} está protegido: crea uno nuevo a partir de él)
@msg no saved profiles no hay perfiles guardados
@msg no saved selection recalled no se ha recuperado ninguna selección guardada
@msg no saved selections no hay selecciones guardadas
@msg no saved themes no hay temas guardados
@msg no segment {segment} on line {line} no existe el segmento {segment} en la línea {line}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -388,6 +388,7 @@
Area Text Objects Aluetekstiobjektit
Start Global Edit Aloita yleismuokkaus
Save Selection… Tallenna valinta…
Update Selection Päivitä valinta
Edit Selection… Muokkaa valintaa…
Effect Tehoste
Apply Last Effect Käytä viimeisintä tehostetta
@@ -4042,6 +4043,7 @@ imageTrace Outline Ääriviiva
@msg no saved presets ei tallennettuja esiasetuksia
@msg no saved print preset named `{name}` ({DEFAULT_PRESET} is protected: start a new one from it) ei tallennettua tulostuksen esiasetusta nimeltä `{name}` ({DEFAULT_PRESET} on suojattu: luo uusi sen pohjalta)
@msg no saved profiles ei tallennettuja profiileja
@msg no saved selection recalled tallennettua valintaa ei ole palautettu
@msg no saved selections ei tallennettuja valintoja
@msg no saved themes ei tallennettuja teemoja
@msg no segment {segment} on line {line} ei segmenttiä {segment} viivalla {line}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -2527,6 +2527,7 @@
Save Save for Web Preset Enregistrer le paramètre prédéfini d’Enregistrer pour le web
Save selected colors as swatches Enregistrer les couleurs sélectionnées comme nuances
Save Selection… Enregistrer la sélection…
Update Selection Mettre à jour la sélection
Save Swatch Library Enregistrer la bibliothèque de nuances
Save Swatch Library… Enregistrer la bibliothèque de nuances…
Save the current layout under this name Enregistrer la disposition actuelle sous ce nom
@@ -3817,6 +3818,7 @@ axis Both Les deux
@msg no saved presets aucun paramètre prédéfini enregistré
@msg no saved print preset named `{name}` ({DEFAULT_PRESET} is protected: start a new one from it) aucun paramètre prédéfini d’impression enregistré nommé `{name}` ({DEFAULT_PRESET} est protégé : créez-en un nouveau à partir de lui)
@msg no saved profiles aucun profil enregistré
@msg no saved selection recalled aucune sélection enregistrée rappelée
@msg no saved selections aucune sélection enregistrée
@msg no saved themes aucun thème enregistré
@msg no segment {segment} on line {line} aucun segment {segment} sur la ligne {line}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -3075,6 +3075,7 @@
Save selected colors as swatches Kijelölt színek mentése színmintaként
Save Selection Kijelölés mentése
Save Selection… Kijelölés mentése…
Update Selection Kijelölés frissítése
Save Swatch Library Színminta-könyvtár mentése
Save Swatch Library… Színminta-könyvtár mentése…
Save the current layout under this name Az aktuális elrendezés mentése ezen a néven
@@ -4429,6 +4430,7 @@ imageTrace Outline Körvonal
@msg no saved presets nincs mentett előbeállítás
@msg no saved print preset named `{name}` ({DEFAULT_PRESET} is protected: start a new one from it) nincs ilyen nevű mentett nyomtatási előbeállítás: `{name}` (védett: {DEFAULT_PRESET}; új előbeállítást indíthatsz belőle)
@msg no saved profiles nincs mentett profil
@msg no saved selection recalled nincs visszahívott mentett kijelölés
@msg no saved selections nincs mentett kijelölés
@msg No saved selections (Select → Save Selection…) Nincs mentett kijelölés (Kijelölés → Kijelölés mentése…)
@msg no saved themes nincs mentett téma
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -2533,6 +2533,7 @@
Save Save for Web Preset Salva preset di Salva per il web
Save selected colors as swatches Salva i colori selezionati come campioni
Save Selection… Salva selezione…
Update Selection Aggiorna selezione
Save Swatch Library Salva libreria campioni
Save Swatch Library… Salva libreria campioni…
Save the current layout under this name Salva il layout corrente con questo nome
@@ -3823,6 +3824,7 @@ axis Both Entrambi
@msg no saved presets nessun preset salvato
@msg no saved print preset named `{name}` ({DEFAULT_PRESET} is protected: start a new one from it) nessun preset di stampa salvato denominato `{name}` ({DEFAULT_PRESET} è protetto: creane uno nuovo a partire da esso)
@msg no saved profiles nessun profilo salvato
@msg no saved selection recalled nessuna selezione salvata richiamata
@msg no saved selections nessuna selezione salvata
@msg no saved themes nessun tema salvato
@msg no segment {segment} on line {line} nessun segmento {segment} alla riga {line}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -396,6 +396,7 @@
Area Text Objects エリア内文字オブジェクト
Start Global Edit グローバル編集を開始
Save Selection… 選択範囲を保存…
Update Selection 選択範囲を更新
Edit Selection… 選択範囲を編集…
Effect 効果
Apply Last Effect 前回の効果を適用
@@ -3671,6 +3672,7 @@ swatch Bright Violet ビビッドバイオレット
@msg Make Guides: select paths ガイドを作成: パスを選択してください
@msg Release Guides: no guides ガイドを解除: ガイドがありません
@msg guides are locked ガイドがロックされています
@msg no saved selection recalled 保存した選択範囲を呼び出していません
@msg no saved selections 保存した選択範囲はありません
@msg no such artboard 指定されたアートボードが見つかりません
@msg cannot split an open path at its end point オープンパスは端点で分割できません
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -392,6 +392,7 @@
Area Text Objects 영역 문자 개체
Start Global Edit 전체 편집 시작
Save Selection… 선택 영역 저장…
Update Selection 선택 영역 업데이트
Edit Selection… 선택 영역 편집…
Effect 효과
Apply Last Effect 마지막 효과 적용
@@ -3603,6 +3604,7 @@ theme Appearance 외관
@msg Make Guides: select paths 안내선 만들기: 패스를 선택하세요
@msg Release Guides: no guides 안내선 해제: 안내선이 없습니다
@msg guides are locked 안내선이 잠겨 있습니다
@msg no saved selection recalled 불러온 저장된 선택 영역이 없습니다
@msg no saved selections 저장된 선택 영역이 없습니다
@msg no such artboard 해당 아트보드가 없습니다
@msg cannot split an open path at its end point 열린 패스는 끝점에서 나눌 수 없습니다
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -351,6 +351,7 @@
Area Text Objects Obiekty tekstu akapitowego
Start Global Edit Rozpocznij edycję globalną
Save Selection… Zapisz zaznaczenie…
Update Selection Aktualizuj zaznaczenie
Edit Selection… Edytuj zaznaczenie…
Effect Efekt
Apply Last Effect Zastosuj ostatni efekt
@@ -3934,6 +3935,7 @@ imageTrace Outline Kontur
@msg no saved presets brak zapisanych ustawień
@msg no saved print preset named `{name}` ({DEFAULT_PRESET} is protected: start a new one from it) brak zapisanego ustawienia drukowania o nazwie `{name}` ({DEFAULT_PRESET} jest chronione: utwórz na jego podstawie nowe)
@msg no saved profiles brak zapisanych profili
@msg no saved selection recalled nie przywołano zapisanego zaznaczenia
@msg no saved selections brak zapisanych zaznaczeń
@msg no saved themes brak zapisanych motywów
@msg no segment {segment} on line {line} brak segmentu {segment} na linii {line}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -2086,6 +2086,7 @@
Save Recovery Data Salvar Dados de Recuperação
Save Save for Web Preset Salvar Predefinido de Salvar para Web
Save Selection… Salvar Seleção…
Update Selection Atualizar Seleção
Save Swatch Library Salvar Biblioteca de Amostras
Save Swatch Library… Salvar Biblioteca de Amostras…
Save Theme Salvar Tema
@@ -3412,6 +3413,7 @@ axis Both Ambos
@msg no saved presets nenhuma predefinição salva
@msg no saved print preset named `{name}` ({DEFAULT_PRESET} is protected: start a new one from it) nenhuma predefinição de impressão salva chamada `{name}` ({DEFAULT_PRESET} é protegida: crie uma nova a partir dela)
@msg no saved profiles nenhum perfil salvo
@msg no saved selection recalled nenhuma seleção salva relembrada
@msg no saved selections nenhuma seleção salva
@msg no saved themes nenhum tema salvo
@msg no segment {segment} on line {line} nenhum segmento {segment} na linha {line}
Can't render this file because it contains an unexpected character in line 6 and column 47.
+2
View File
@@ -2529,6 +2529,7 @@
Save Save for Web Preset Сохранить набор «Сохранить для web»
Save selected colors as swatches Сохранить выбранные цвета как образцы
Save Selection… Сохранить выделение…
Update Selection Обновить выделение
Save Swatch Library Сохранить библиотеку образцов
Save Swatch Library… Сохранить библиотеку образцов…
Save the current layout under this name Сохранить текущий макет под этим именем
@@ -3819,6 +3820,7 @@ axis Both Оба
@msg no saved presets нет сохранённых наборов
@msg no saved print preset named `{name}` ({DEFAULT_PRESET} is protected: start a new one from it) нет сохранённого набора печати с именем `{name}` ({DEFAULT_PRESET} защищён: создайте новый на его основе)
@msg no saved profiles нет сохранённых профилей
@msg no saved selection recalled сохранённое выделение не восстановлено
@msg no saved selections нет сохранённых выделений
@msg no saved themes нет сохранённых тем
@msg no segment {segment} on line {line} нет сегмента {segment} на строке {line}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -2484,6 +2484,7 @@
Save Save for Web Preset Spara förinställning för Spara för webben
Save selected colors as swatches Spara markerade färger som färgrutor
Save Selection… Spara markering…
Update Selection Uppdatera markering
Save Swatch Library Spara färgrutebibliotek
Save Swatch Library… Spara färgrutebibliotek…
Save the current layout under this name Spara den aktuella layouten under det här namnet
@@ -3745,6 +3746,7 @@ axis Both Båda
@msg no saved presets inga sparade förinställningar
@msg no saved print preset named `{name}` ({DEFAULT_PRESET} is protected: start a new one from it) ingen sparad utskriftsförinställning med namnet `{name}` ({DEFAULT_PRESET} är skyddad: skapa en ny utifrån den)
@msg no saved profiles inga sparade profiler
@msg no saved selection recalled ingen sparad markering hämtad
@msg no saved selections inga sparade markeringar
@msg no saved themes inga sparade teman
@msg no segment {segment} on line {line} inget segment {segment} på rad {line}
Can't render this file because it contains an unexpected character in line 5 and column 25.
+2
View File
@@ -2525,6 +2525,7 @@
Save Save for Web Preset Зберегти набір «Зберегти для Інтернету»
Save selected colors as swatches Зберегти вибрані кольори як зразки
Save Selection… Зберегти виділення…
Update Selection Оновити виділення
Save Swatch Library Зберегти бібліотеку зразків
Save Swatch Library… Зберегти бібліотеку зразків…
Save the current layout under this name Зберегти поточний макет під цим ім’ям
@@ -3807,6 +3808,7 @@ axis Both Обидві
@msg no saved presets немає збережених наборів
@msg no saved print preset named `{name}` ({DEFAULT_PRESET} is protected: start a new one from it) немає збереженого набору друку з іменем `{name}` ({DEFAULT_PRESET} захищений: створіть новий на його основі)
@msg no saved profiles немає збережених профілів
@msg no saved selection recalled збережене виділення не відновлено
@msg no saved selections немає збережених виділень
@msg no saved themes немає збережених тем
@msg no segment {segment} on line {line} немає сегмента {segment} на рядку {line}
Can't render this file because it contains an unexpected character in line 720 and column 40.
+2
View File
@@ -2813,6 +2813,7 @@
Save Save for Web Preset Lưu thiết lập sẵn Lưu cho Web
Save selected colors as swatches Lưu màu đã chọn thành mẫu màu
Save Selection… Lưu vùng chọn…
Update Selection Cập nhật vùng chọn
Save Swatch Library Lưu thư viện mẫu màu
Save Swatch Library… Lưu thư viện mẫu màu…
Save the current layout under this name Lưu bố cục hiện tại với tên này
@@ -4059,6 +4060,7 @@ theme Appearance Giao diện
@msg no saved presets không có thiết lập sẵn đã lưu
@msg no saved print preset named `{name}` ({DEFAULT_PRESET} is protected: start a new one from it) không có thiết lập sẵn in đã lưu tên `{name}` ({DEFAULT_PRESET} được bảo vệ: tạo mới từ nó)
@msg no saved profiles không có hồ sơ đã lưu
@msg no saved selection recalled chưa khôi phục vùng chọn đã lưu nào
@msg no saved selections không có vùng chọn đã lưu
@msg No saved selections (Select → Save Selection…) Không có vùng chọn đã lưu (Chọn → Lưu vùng chọn…)
@msg no saved themes chưa có bộ màu nào được lưu
Can't render this file because it contains an unexpected character in line 3738 and column 10.
+1
View File
@@ -2408,6 +2408,7 @@
Save Save for Web Preset 保存 Web 预设
Save selected colors as swatches 将所选颜色存储为色板
Save Selection… 保存选区…
Update Selection 更新选区
Save Swatch Library 存储色板库
Save Swatch Library… 保存色板库…
Save the current layout under this name 以该名称保存当前布局
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2507,6 +2507,7 @@
Save Save for Web Preset 儲存「儲存為網頁用」預設集
Save selected colors as swatches 將所選顏色儲存為色票
Save Selection… 儲存選取範圍…
Update Selection 更新選取範圍
Save Swatch Library 儲存色票資料庫
Save Swatch Library… 儲存色票資料庫…
Save the current layout under this name 以此名稱儲存目前的版面配置
Can't render this file because it contains an unexpected character in line 6 and column 47.
+18
View File
@@ -2703,6 +2703,7 @@ pub fn menu_tree_named(english_names: bool) -> Vec<(&'static str, Vec<Item>)> {
todo("Start Global Edit"),
Sep,
c("Save Selection…", "select.save"),
c("Update Selection", "select.update"),
c("Edit Selection…", "select.editSaved"),
Sep,
];
@@ -4208,6 +4209,23 @@ mod tests {
assert!(shown_state(&app, "select.recall1", &Value::Null).is_some());
}
#[test]
fn update_selection_follows_save_selection_and_needs_a_recall() {
let mut app = app_with_a_rectangle();
let entries = menu_entries(&app);
let at = |id: &str| entries.iter().position(|e| e.command.as_deref() == Some(id));
assert_eq!(at("select.update").map(|i| i - 1), at("select.save"));
app.run("select.save", json!({"name": "Box"})).unwrap();
assert!(!enabled(&app, "select.update"));
app.run("shape.ellipse", json!({"x": 40, "y": 0, "width": 10, "height": 10})).unwrap();
app.run("select.recall1", json!({})).unwrap();
assert!(enabled(&app, "select.update"));
app.run("select.all", json!({})).unwrap();
invoke(&mut app, "select.update", json!({}));
app.run("select.none", json!({})).unwrap();
assert_eq!(app.run("select.recall1", json!({})).unwrap()["count"], 2);
}
#[test]
fn edit_selection_opens_its_dialog_on_the_saved_selections() {
let mut app = app_with_a_rectangle();
+5 -1
View File
@@ -960,7 +960,11 @@ Select → Save Selection… keeps the selected objects under a name, in the doc
the first free "Selection N"; an existing name is replaced with the current selection; at most 25 per document,
names up to 255 characters) → `{name}`, one undo step. `select.savedList` lists the names in menu order, and
`select.recall {name}` selects those objects again, except the ones deleted since and the ones hidden, locked or
outside the isolated group or layer (see Object selection) → `{count}`. Edit Selection…
outside the isolated group or layer (see Object selection) → `{count}`. Update Selection: `select.update {}`
replaces the objects of the saved selection last recalled in the document with the selected objects → `{name}`,
one undo step. The command is enabled while objects are selected and the document holds that saved selection,
under its own name or a name `select.editSaved` gave it; Undo and Redo do not change which saved selection is
recalled. Edit Selection…
renames and deletes: `select.editSaved {name, newName?, delete?}`, or several at once as
`{edits: [{name, newName?, delete?}…]}` in one undo step, where every `name` is the name before the edit and the
names must stay unique. In the desktop app the saved selections are listed at the bottom of the Select menu
+2 -1
View File
@@ -473,7 +473,8 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
Clear deletes individual selected compound-path members without deleting their siblings, and explicit object ids work without a selection; Cut still removes the same objects it copies (#786).
A command that leaves out an object selected inside a selected group also leaves it out when `ids` list it together with a group that holds it. Make Repeat and Live Paint no longer fail on such a list, New Symbol no longer stores the object twice, and `object.transform` no longer moves it twice.
- **Compound paths:** Make keeps the backmost object's appearance and each contour; selecting a member in Direct Selection reads and changes the compound's fill rule in Attributes. Direction remains a per-member edit. Rendered holes, unchanged contour geometry, no-op history and Undo/Redo are covered by `engine/tests/compound_attributes.rs` (#1197). Behaviour reference: [public compound-path documentation](https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/create-compound-paths.html). Make Compound Path on a group (such as outlined type) keeps the paint of its backmost path instead of drawing nothing, and compound paths joined into one keep their holes (#1537).
- **Saved selections:** Select ▸ Save Selection… asks for a name and keeps the selection in the document (saved with the file, undoable, up to 25); each one is listed at the bottom of the Select menu and a click selects its objects again; Edit Selection… lists them to rename or delete, applied together on OK. A file's saved selections are capped and checked against its objects when it opens. Not yet: Update Selection and the Layers panel's saved-selection menu.
- **Saved selections:** Select ▸ Save Selection… asks for a name and keeps the selection in the document (saved with the file, undoable, up to 25); each one is listed at the bottom of the Select menu and a click selects its objects again; Edit Selection… lists them to rename or delete, applied together on OK. A file's saved selections are capped and checked against its objects when it opens. Not yet: the Layers panel's saved-selection menu.
Select ▸ Update Selection replaces the objects of the saved selection last recalled with the selected objects, in one undo step that marks the document changed (`select.update`). After Edit Selection… renames that saved selection, Update Selection replaces it under the new name; after Edit Selection… deletes it, Update Selection is disabled until another saved selection is recalled or the delete is undone. Undo and Redo do not change which saved selection is recalled.
- **Drawing modes:** in Draw Inside, a double-click with a selection tool outside the object being drawn into sets the drawing mode to Draw Normal, as Shift+D does.
- **Drawing:** Pen, Curvature, anchor tools, Pencil, Paintbrush, Blob Brush, Smooth, Path Eraser, Join. Direction handles dragged with Direct Selection or the Anchor Point tool snap to smart guides, and Shift keeps them at 45° steps from their anchor. The Pen continues any open path from the end clicked, and while drawing, a click on an end of another open path joins the two into one (#776). The Curvature tool edits any selected path, whatever drew it, keeping its shape except where edited (#798): a click on a point makes it current and a drag moves it, re-curving only its two segments; Alt-click or double-click toggles it smooth or corner and Delete removes it; a click on a segment adds a point without changing the shape; with an end of an open path current, clicks go on from it and its other end closes it (`path.curvatureEdit`). Every point a drawing tool places snaps to Smart Guides through one snapper (#506): the Pen's and Curvature tool's anchors, the Pencil's ends, the shape tools' start and dragged corner or radius (Rectangle, Rounded Rectangle, Ellipse, Polygon, Star, Line Segment, Arc, Spiral, the grids), the Type tool's click and frame, a new artboard's corners. A filled Pencil path keeps a stroke of None (#1804, part of #1679). The [ and ] keys reach the Blob Brush, the Eraser and the Symbolism tools in the app (#1734, part of #1661).
Curvature point markers are circular, with no direction handles. While drawing, its own point feedback replaces generic selection markers; existing paths use the selection overlay, including partial selections and Hide Edges. Anchor Size applies, and Direct Selection and Pen retain their handles. The real egui-frame regressions in `crates/ui-egui/src/canvas.rs` verify marker positions, counts and unchanged documents (#1236, in part); the existing-path interpolation request remains open. The curve through the points (`curvature_spline` in `crates/tools/src/draw2.rs`) makes each stretch of three points follow the circle through them: a smooth point's handles lie along that circle's tangent, each `chord / (3 cos²(α/2))` long (α between handle and chord, capped at 90°), and the ends of an open path mirror their neighbour's tangent in their chord, so a J or a U curves evenly, a turn tighter than 30° comes to a point, and uneven spacing doesn't loop; the rubber band, a dragged or inserted point and `path.convertAnchor` to smooth use the same rule, while the Pencil's fitting keeps its own; `tests_draw2.rs` checks that the rubber band is the path the click makes (#1236, in part). The Smooth tool smooths only the stretch of path under the drag (#1547, fixes #1512).