diff --git a/crates/engine/src/cmd.rs b/crates/engine/src/cmd.rs index f02d6f6aa..4648b8dd1 100644 --- a/crates/engine/src/cmd.rs +++ b/crates/engine/src/cmd.rs @@ -722,6 +722,10 @@ pub use pathops::shape_count; /// The gaps View › Show Live Paint Gaps shows on a Live Paint group. pub use buildcmds::live_paint_gaps; +/// The transform Reset Transformation gives a symbol instance (the Symbols panel menu and the +/// Control bar enable it when there is one). +pub use brushsym::reset_transformation; + /// Command ids kept only so older scripts keep working: `(alias, the command it duplicates)`. /// They run as before but the command palette leaves them out (it lists the command once). pub const ALIASES: &[(&str, &str)] = &[("object.convertDocumentColorMode", "file.documentColorMode")]; diff --git a/crates/engine/src/cmd/brushsym.rs b/crates/engine/src/cmd/brushsym.rs index 3e097f8fc..9bdae569f 100644 --- a/crates/engine/src/cmd/brushsym.rs +++ b/crates/engine/src/cmd/brushsym.rs @@ -24,6 +24,9 @@ use super::{ }; use crate::EngineError; +/// The transform Reset Transformation gives a symbol instance. +pub use symbols::reset_transformation; + mod brushes; mod dynamic; mod symbols; @@ -40,8 +43,8 @@ pub(crate) use symbols::{ stored_natural_size, }; use symbols::{ - symbol_adjust, symbol_break_link, symbol_delete, symbol_duplicate, symbol_list, symbol_new, symbol_options, symbol_place, symbol_replace, - symbol_select_instances, symbol_set_current, symbol_spray, symbol_unused, symbol_update, + symbol_adjust, symbol_break_link, symbol_delete, symbol_duplicate, symbol_instance_reset_transform, symbol_list, symbol_new, symbol_options, + symbol_place, symbol_replace, symbol_select_instances, symbol_set_current, symbol_spray, symbol_unused, symbol_update, }; const SIZES_KEY: &str = vectorcraft_doc::SYMBOL_SIZES; @@ -272,6 +275,15 @@ pub fn specs() -> Vec { has_selection, symbol_instance_reset ), + cmd!( + "symbol.instance.resetTransform", + "Reset Transformation", + [], + None, + "{ids?|id?} give the selected symbol instances (in groups too) their symbol's own scale, without rotation, shear or reflection, keeping the center of each one's bounding box in place, as one undo step; other objects, instances in perspective and instances already at their symbol's scale are left as they are, and no undo step is added when nothing changes; an id naming no object, or no symbol instance among the objects, is an error → {ids: the instances changed}", + has_selection, + symbol_instance_reset_transform + ), cmd!( "symbol.setCurrent", "Current Symbol", diff --git a/crates/engine/src/cmd/brushsym/symbols.rs b/crates/engine/src/cmd/brushsym/symbols.rs index 406511ff1..c895a128c 100644 --- a/crates/engine/src/cmd/brushsym/symbols.rs +++ b/crates/engine/src/cmd/brushsym/symbols.rs @@ -696,6 +696,52 @@ pub(super) fn symbol_replace(s: &mut Session, p: &Value) -> Result { Ok(json!({ "ids": ids_json(&ids) })) } +/// How far a transform coefficient may be from Reset Transformation's for the instance to count as +/// already reset. +const RESET_TOLERANCE: f64 = 1e-9; + +/// The transform Reset Transformation gives instance `n` of the document of `boxes`: its symbol's +/// own scale, without rotation, shear or reflection, with the center of its art's bounding box +/// where it is. `None` when `n` is not an instance of a symbol the document holds, is in +/// perspective, or already has that transform. +pub fn reset_transformation(boxes: &vectorcraft_doc::SymbolBoxes, n: &Node) -> Option { + let d = boxes.document(); + let NodeKind::SymbolInstance { symbol, xf } = &n.kind else { return None }; + if n.extras().perspective.is_some() || !d.symbols.iter().any(|s| s.name == *symbol) { + return None; + } + let (w, h) = natural_size(d, symbol); + let scale = Affine::scale_non_uniform(w / (2.0 * HALF), h / (2.0 * HALF)); + // The art's box in symbol space (the ±HALF square when the art cannot be measured): its + // center keeps its place on the page. + let art = boxes.art_bounds(symbol).unwrap_or_else(|| vectorcraft_geom::Rect::new(-HALF, -HALF, HALF, HALF)); + let center = xf.transform_rect_bbox(art).center(); + let new = Affine::translate(center - scale * art.center()) * scale; + xf.as_coeffs().iter().zip(new.as_coeffs()).any(|(a, b)| (a - b).abs() > RESET_TOLERANCE).then_some(new) +} + +pub(super) fn symbol_instance_reset_transform(s: &mut Session, p: &Value) -> Result { + let ids = selected_instances(s, p)?; + if ids.is_empty() { + return Err(EngineError::Other("no symbol instances selected".into())); + } + let d = &s.doc()?.doc; + let boxes = d.symbol_boxes(); + let changes: Vec<(NodeId, Affine)> = ids.iter().filter_map(|id| Some((*id, reset_transformation(&boxes, d.node(*id)?)?))).collect(); + if !changes.is_empty() { + s.edit("Reset Transformation", |d, _| { + for (id, new) in &changes { + if let Some(NodeKind::SymbolInstance { xf, .. }) = d.node_mut(*id).map(|n| &mut n.kind) { + *xf = *new; + } + } + Ok(()) + })?; + } + let changed: Vec = changes.iter().map(|(id, _)| *id).collect(); + Ok(json!({ "ids": ids_json(&changed) })) +} + pub(super) fn symbol_set_current(s: &mut Session, p: &Value) -> Result { let name = resolve_symbol(&s.doc()?.doc, p, "symbol.setCurrent")?; set_unknown(s, CURRENT_SYMBOL, Some(json!(name)))?; diff --git a/crates/engine/src/tests/brushsym.rs b/crates/engine/src/tests/brushsym.rs index 4784831ef..c1964a63b 100644 --- a/crates/engine/src/tests/brushsym.rs +++ b/crates/engine/src/tests/brushsym.rs @@ -1343,6 +1343,117 @@ fn delete_duplicate_and_replace_symbols() { assert!(s.execute("symbol.delete", &json!({"name": "Small"})).is_err()); } +fn xf_of(s: &Session, id: NodeId) -> vectorcraft_geom::Affine { + match node(s, id).kind { + NodeKind::SymbolInstance { xf, .. } => xf, + _ => panic!("{id:?} is not a symbol instance"), + } +} + +fn close_xf(a: vectorcraft_geom::Affine, b: vectorcraft_geom::Affine) -> bool { + a.as_coeffs().iter().zip(b.as_coeffs()).all(|(x, y)| (x - y).abs() < 1e-9) +} + +fn undo_len(s: &Session) -> usize { + s.doc().unwrap().history.undo.len() +} + +/// Reset Transformation gives rotated, scaled, sheared and reflected instances (in a group too) +/// their symbol's own scale again, each keeping the center of its bounding box, as one undo step. +#[test] +fn reset_transformation_restores_the_symbols_scale_and_keeps_the_center() { + let mut s = session(); + let (a, name, placed) = make_symbol(&mut s); + let b = NodeId(s.execute("symbol.place", &json!({"name": name, "x": 300, "y": 200})).unwrap()["id"].as_u64().unwrap()); + let c = NodeId(s.execute("symbol.place", &json!({"name": name, "x": 100, "y": 220})).unwrap()["id"].as_u64().unwrap()); + let master = xf_of(&s, b).with_translation(vectorcraft_geom::Vec2::ZERO); + select(&mut s, &[a]); + s.execute("object.rotate", &json!({"angle": 30})).unwrap(); + s.execute("object.scale", &json!({"sx": 150, "sy": 50})).unwrap(); + select(&mut s, &[b]); + s.execute("object.shear", &json!({"angle": 20})).unwrap(); + select(&mut s, &[c]); + s.execute("object.reflect", &json!({"axis": "vertical"})).unwrap(); + let turned: Vec<_> = [a, b, c].iter().map(|id| xf_of(&s, *id)).collect(); + let centers: Vec<_> = [a, b, c].iter().map(|id| bounds(&s, *id).center()).collect(); + let plain = rect(&mut s, 0.0, 0.0, 10.0, 10.0); + select(&mut s, &[b, c]); + let group = NodeId(s.execute("object.group", &json!({})).unwrap()["id"].as_u64().unwrap()); + select(&mut s, &[a, group, plain]); + let plain_before = node(&s, plain); + let before = undo_len(&s); + let r = s.execute("symbol.instance.resetTransform", &json!({})).unwrap(); + assert_eq!(r["ids"], json!([a.0, b.0, c.0])); + assert_eq!(undo_len(&s), before + 1, "one undo step"); + for (id, center) in [a, b, c].into_iter().zip(¢ers) { + let got = bounds(&s, id); + assert!((got.width() - placed.width()).abs() < 1e-9 && (got.height() - placed.height()).abs() < 1e-9, "{id:?}: {got:?}"); + assert!((got.center() - *center).hypot() < 1e-9, "{id:?} keeps its center: {got:?} vs {center:?}"); + let xf = xf_of(&s, id); + assert!(close_xf(xf.with_translation(vectorcraft_geom::Vec2::ZERO), master), "{id:?}: {xf:?}"); + } + assert!(node(&s, plain) == plain_before, "other objects are left as they are"); + s.execute("edit.undo", &json!({})).unwrap(); + for (id, xf) in [a, b, c].into_iter().zip(turned) { + assert!(close_xf(xf_of(&s, id), xf), "Undo gives {id:?} its own transform back"); + } +} + +/// An instance of a symbol whose art lies away from the symbol's origin (as imported symbols can) +/// keeps the center of the box its art fills when Reset Transformation turns it back. +#[test] +fn reset_transformation_keeps_the_center_of_art_away_from_the_origin() { + let mut s = session(); + let r = rect(&mut s, 300.0, 10.0, 20.0, 20.0); + let art = node(&s, r); + s.execute("edit.clear", &json!({"ids": [r.0]})).unwrap(); + let st = s.doc_mut().unwrap(); + let d = std::sync::Arc::make_mut(&mut st.doc); + d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Off".into(), art: std::sync::Arc::new(art) }); + let layer = d.layers[0].id; + let id = d.alloc_id(); + let xf = vectorcraft_geom::Affine::translate((100.0, 100.0)) * vectorcraft_geom::Affine::rotate(0.5); + d.insert(Some(layer), usize::MAX, Node::new(id, NodeKind::SymbolInstance { symbol: "Off".into(), xf })).unwrap(); + let measured = |s: &Session| s.doc().unwrap().doc.bounds_of(&[id], false).unwrap(); + let before = measured(&s); + assert_eq!(s.execute("symbol.instance.resetTransform", &json!({"ids": [id.0]})).unwrap()["ids"], json!([id.0])); + let after = measured(&s); + assert!((after.center() - before.center()).hypot() < 1e-9, "{after:?} vs {before:?}"); + assert!((after.width() - 20.0).abs() < 1e-9 && (after.height() - 20.0).abs() < 1e-9, "{after:?}"); +} + +/// Reset Transformation adds no undo step for an instance already at its symbol's scale or for +/// one in perspective, and refuses an id naming no object and objects without an instance, +/// changing nothing. +#[test] +fn reset_transformation_leaves_unchanged_instances_and_refuses_bad_ids() { + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 1000, "height": 800})).unwrap(); + let (inst, name, _) = make_symbol(&mut s); + let before = undo_len(&s); + assert_eq!(s.execute("symbol.instance.resetTransform", &json!({"ids": [inst.0]})).unwrap()["ids"], json!([])); + assert_eq!(undo_len(&s), before, "nothing changed: no undo step"); + let plain = rect(&mut s, 0.0, 0.0, 10.0, 10.0); + select(&mut s, &[inst]); + s.execute("object.rotate", &json!({"angle": 45})).unwrap(); + let turned = s.doc().unwrap().doc.clone(); + let before = undo_len(&s); + for bad in [json!({"ids": [inst.0, 999_999]}), json!({"id": 999_999}), json!({"ids": [plain.0]}), json!({"ids": "x"})] { + assert!(s.execute("symbol.instance.resetTransform", &bad).is_err(), "{bad}"); + } + assert!(*s.doc().unwrap().doc == *turned && undo_len(&s) == before, "the refusals change nothing"); + // An instance in perspective is left as it is. + s.execute("perspective.grid.preset", &json!({"kind": 2})).unwrap(); + let projected = NodeId(s.execute("symbol.place", &json!({"name": name, "x": 500, "y": 350})).unwrap()["id"].as_u64().unwrap()); + select(&mut s, &[projected]); + s.execute("object.rotate", &json!({"angle": 30})).unwrap(); + s.execute("perspective.attach", &json!({"ids": [projected.0], "plane": "right"})).unwrap(); + let xf = xf_of(&s, projected); + let before = undo_len(&s); + assert_eq!(s.execute("symbol.instance.resetTransform", &json!({"ids": [projected.0]})).unwrap()["ids"], json!([])); + assert!(undo_len(&s) == before && close_xf(xf_of(&s, projected), xf), "an instance in perspective stays as it was"); +} + /// New Symbol and Duplicate Symbol trim the name they are given, as Symbol Options trims /// `newName`; the trimmed name is the one compared with the symbols there are. #[test] diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 976dce077..6d544cda8 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -2670,6 +2670,7 @@ digits Malayalam Малаялам Replace it with another file, or ignore it: its images keep showing the preview saved with the document. Заменете го с друг файл или го игнорирайте: неговите изображения ще продължат да показват записания с документа преглед. Replace Spine Замяна на оста Replace Symbol Замяна на символ + Reset Transformation Нулиране на трансформацията Replace With Font Замяна с шрифт Replace With Font From Замяна с шрифт от System Система diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index a3d05a026..e8deb527d 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -3163,6 +3163,7 @@ theme Appearance Izgled Redefine Symbol Ponovo definiši simbol Remove Brush Stroke Ukloni potez četkicom Replace Symbol Zamijeni simbol + Reset Transformation Vrati transformaciju na početno Select All Instances Odaberi sve instance Symbol Sprayer Raspršivač simbola Symbolism Tool Alat simbolizam diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index 6eb76fbdd..edb90e8a9 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -3372,6 +3372,7 @@ digits Malayalam Malaiàlam Replace it with another file, or ignore it: its images keep showing the preview saved with the document. Reemplaça’l per un altre fitxer o ignora’l: les seves imatges continuaran mostrant la previsualització desada amb el document. Replace Spine Reemplaça l’espina Replace Symbol Reemplaça el símbol + Reset Transformation Restableix la transformació Replace With Font Reemplaça per un tipus de lletra Replace With Font From Reemplaça per un tipus de lletra de System Sistema diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 390e35eb4..559e7c8be 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -3202,6 +3202,7 @@ theme Appearance Motiv Redefine Symbol Předefinovat symbol Remove Brush Stroke Odebrat tah štětcem Replace Symbol Nahradit symbol + Reset Transformation Obnovit transformaci Select All Instances Vybrat všechny instance Symbol Sprayer Rozprašovač symbolů Symbolism Tool Nástroj symbolů diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index b5a2946d2..a279b4254 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -2946,6 +2946,7 @@ theme Appearance Udseende Redefine Symbol Omdefinér symbol Remove Brush Stroke Fjern penselstrøg Replace Symbol Erstat symbol + Reset Transformation Nulstil transformation Select All Instances Markér alle forekomster Symbol Sprayer Symbolsprøjter Symbolism Tool Symbolværktøj diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 3bdbfabbe..11897c2cc 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -2697,6 +2697,7 @@ digits Malayalam Malayalam Replace it with another file, or ignore it: its images keep showing the preview saved with the document. Ersetzen Sie sie durch eine andere Datei oder ignorieren Sie sie: Ihre Bilder zeigen weiterhin die mit dem Dokument gespeicherte Vorschau. Replace Spine Achse ersetzen Replace Symbol Symbol ersetzen + Reset Transformation Transformation zurücksetzen Replace With Font Durch Schrift ersetzen Replace With Font From Ersetzende Schrift aus System System diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 4e5c347c0..91dcff85f 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -3296,6 +3296,7 @@ digits Malayalam Μαλαγιάλαμ Replace it with another file, or ignore it: its images keep showing the preview saved with the document. Αντικαταστήστε το με άλλο αρχείο ή αγνοήστε το: οι εικόνες του θα συνεχίσουν να εμφανίζουν την προεπισκόπηση που αποθηκεύτηκε στο έγγραφο. Replace Spine Αντικατάσταση κεντρικής καμπύλης Replace Symbol Αντικατάσταση συμβόλου + Reset Transformation Επαναφορά μετασχηματισμού Replace With Font Αντικατάσταση με γραμματοσειρά Replace With Font From Αντικατάσταση με γραμματοσειρά από System Σύστημα diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index a9a940eee..082826c39 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -2740,6 +2740,7 @@ digits Malayalam Malayálam Replace it with another file, or ignore it: its images keep showing the preview saved with the document. Reemplázalo con otro archivo o ignóralo: sus imágenes seguirán mostrando la vista previa guardada con el documento. Replace Spine Reemplazar eje Replace Symbol Reemplazar símbolo + Reset Transformation Restablecer transformación Replace With Font Reemplazar con fuente Replace With Font From Reemplazar con fuente de System Sistema diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 625a93eb7..b518868f6 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -3141,6 +3141,7 @@ theme Appearance Ulkoasu Redefine Symbol Määritä symboli uudelleen Remove Brush Stroke Poista sivellinviiva Replace Symbol Korvaa symboli + Reset Transformation Palauta muunnos Select All Instances Valitse kaikki ilmentymät Symbol Sprayer Symbolisuihkutin Symbolism Tool Symbolityökalu diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 761c44bc9..5b983385c 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -2732,6 +2732,7 @@ digits Malayalam Malayalam Replace it with another file, or ignore it: its images keep showing the preview saved with the document. Remplacez-le par un autre fichier ou ignorez-le : ses images continueront d’afficher l’aperçu enregistré avec le document. Replace Spine Remplacer la dorsale Replace Symbol Remplacer le symbole + Reset Transformation Réinitialiser la transformation Replace With Font Remplacer par la police Replace With Font From Remplacer par une police de System Système diff --git a/crates/ui-egui/src/i18n/hi.tsv b/crates/ui-egui/src/i18n/hi.tsv index cad5a6781..10f7ea308 100644 --- a/crates/ui-egui/src/i18n/hi.tsv +++ b/crates/ui-egui/src/i18n/hi.tsv @@ -3685,6 +3685,7 @@ Replace Missing Fonts… अनुपलब्ध फ़ॉन्ट रिप्लेस करें… Replace Spine स्पाइन रिप्लेस करें Replace Symbol सिंबल रिप्लेस करें + Reset Transformation रूपांतरण रीसेट करें Replace With Font इस फ़ॉन्ट से रिप्लेस करें Replace With Font From इस स्रोत के फ़ॉन्ट से रिप्लेस करें Replace with Top Object सबसे ऊपर के ऑब्जेक्ट से रिप्लेस करें diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index f53038da8..a0c164c54 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -3268,6 +3268,7 @@ mark-style Japanese Japán Replace it with another file, or ignore it: its images keep showing the preview saved with the document. Cseréld le egy másik fájlra, vagy hagyd ki: a képei a dokumentummal mentett előnézetet mutatják tovább. Replace Spine Gerinc cseréje Replace Symbol Szimbólum cseréje + Reset Transformation Átalakítás visszaállítása Replace With Font Helyettesítő betűtípus Replace With Font From Helyettesítő betűtípus forrása System Rendszer diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index b882af832..d08aef173 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -2946,6 +2946,7 @@ theme Appearance Tampilan Redefine Symbol Definisikan ulang simbol Remove Brush Stroke Hapus guratan kuas Replace Symbol Ganti simbol + Reset Transformation Atur ulang transformasi Select All Instances Pilih semua instansi Symbol Sprayer Penyemprot simbol Symbolism Tool Alat simbolisme diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 474df13a5..6e26739b5 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -2773,6 +2773,7 @@ digits Malayalam Malayalam Replace it with another file, or ignore it: its images keep showing the preview saved with the document. Sostituiscilo con un altro file oppure ignoralo: le sue immagini continueranno a mostrare l'anteprima salvata con il documento. Replace Spine Sostituisci dorsale Replace Symbol Sostituisci simbolo + Reset Transformation Reimposta trasformazione Replace With Font Sostituisci con font Replace With Font From Sostituisci con font da System Sistema diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 3fcdc449e..1304256f8 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -2753,6 +2753,7 @@ mark-style Japanese 日本式 Replace: 置換: Replace Font フォントを置換 Replace Symbol シンボルを置き換え + Reset Transformation 変形をリセット Replace With Font 置換するフォント Replace With Font From 置換するフォントの一覧 System システム diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index 9c45ce547..8af691caa 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -2765,6 +2765,7 @@ theme Appearance იერსახე Redefine Symbol სიმბოლოს ხელახლა განსაზღვრა Remove Brush Stroke ფუნჯის შტრიხის მოხსნა Replace Symbol სიმბოლოს ჩანაცვლება + Reset Transformation გარდაქმნის ჩამოყრა Select All Instances ყველა ეგზემპლარის მონიშვნა Symbol Sprayer სიმბოლოების სპრეი Symbolism Tool სიმბოლოების ხელსაწყო diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 6f76d1297..f1dbfaadc 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -2742,6 +2742,7 @@ mark-style Japanese 일본식 Replace: 바꾸기: Replace Font 글꼴 바꾸기 Replace Symbol 심볼 바꾸기 + Reset Transformation 변형 재설정 Replace With Font 바꿀 글꼴 Replace With Font From 바꿀 글꼴 목록 System 시스템 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index 323d05a9d..3c9f7b9cf 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -2960,6 +2960,7 @@ theme Appearance Vormgeving Redefine Symbol Symbool opnieuw definiëren Remove Brush Stroke Penseelstreek verwijderen Replace Symbol Symbool vervangen + Reset Transformation Transformatie opnieuw instellen Select All Instances Alle exemplaren selecteren Symbol Sprayer Symboolspuit Symbolism Tool Symbolisatiegereedschap diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index f16d0160c..1bc9d7c51 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -3081,6 +3081,7 @@ theme Appearance Wygląd Redefine Symbol Zdefiniuj ponownie symbol Remove Brush Stroke Usuń pociągnięcie pędzla Replace Symbol Zastąp symbol + Reset Transformation Resetuj przekształcenie Select All Instances Zaznacz wszystkie wystąpienia Symbol Sprayer Rozpylanie symboli Symbolism Tool Narzędzie symboli diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 25e8a8fcd..db631ddfd 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -2282,6 +2282,7 @@ digits Malayalam Malaiala Replace Font Substituir Fonte Replace Spine Substituir Coluna Replace Symbol Substituir Símbolo + Reset Transformation Redefinir Transformação Replace With Font Substituir Com Fonte Replace With Font From Substituir Com Fonte De System Sistema diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 05030934a..1cbfb62a2 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -2768,6 +2768,7 @@ digits Malayalam Малаялам Replace it with another file, or ignore it: its images keep showing the preview saved with the document. Замените его другим файлом или игнорируйте: его изображения продолжат показывать предварительный просмотр, сохранённый с документом. Replace Spine Заменить остов Replace Symbol Заменить символ + Reset Transformation Сбросить трансформацию Replace With Font Заменить шрифтом Replace With Font From Заменить шрифтом из System Система diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index fec29cca5..fb7afaa9f 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -2676,6 +2676,7 @@ digits Malayalam Malayalam Replace it with another file, or ignore it: its images keep showing the preview saved with the document. Ersätt den med en annan fil eller ignorera den: dess bilder fortsätter att visa förhandsgranskningen som sparats med dokumentet. Replace Spine Ersätt ryggrad Replace Symbol Ersätt symbol + Reset Transformation Återställ transformering Replace With Font Ersätt med typsnitt Replace With Font From Ersätt med typsnitt från System System diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index e622e6cd7..092d54070 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -2545,6 +2545,7 @@ digits Malayalam Malayalam Replace it with another file, or ignore it: its images keep showing the preview saved with the document. Onu başka bir dosyayla değiştirin veya yok sayın: resimleri, belgeyle birlikte kaydedilen önizlemeyi göstermeye devam eder. Replace Spine Omurgayı Değiştir Replace Symbol Sembolü Değiştir + Reset Transformation Dönüşümü Sıfırla Replace With Font Yazı Tipiyle Değiştir Replace With Font From Değiştirilecek Yazı Tipi Kaynağı System Sistem diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 54249c059..58abaae18 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -2765,6 +2765,7 @@ digits Malayalam Малаялам Replace it with another file, or ignore it: its images keep showing the preview saved with the document. Замініть його іншим файлом або ігноруйте: зображення продовжать показувати попередній перегляд, збережений з документом. Replace Spine Замінити вісь Replace Symbol Замінити символ + Reset Transformation Скинути трансформацію Replace With Font Замінити шрифтом Replace With Font From Замінити шрифтом з System Система diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index d98cc3519..bdee09c89 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -3036,6 +3036,7 @@ mark-style Japanese Kiểu Nhật Replace it with another file, or ignore it: its images keep showing the preview saved with the document. Thay thế bằng tệp khác hoặc bỏ qua: ảnh của nó vẫn hiển thị bản xem trước đã lưu cùng tài liệu. Replace Spine Thay thế trục Replace Symbol Thay thế ký hiệu + Reset Transformation Đặt lại biến đổi Replace With Font Thay thế bằng phông chữ Replace With Font From Thay thế bằng phông chữ từ System Hệ thống diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index b7a65b421..3979c9e85 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -2644,6 +2644,7 @@ digits Malayalam 马拉雅拉姆数字 Replace it with another file, or ignore it: its images keep showing the preview saved with the document. 将其替换为其他文件,或忽略:其图像继续显示随文档保存的预览。 Replace Spine 替换脊线 Replace Symbol 替换符号 + Reset Transformation 重置变换 Replace With Font 替换为字体 Replace With Font From 替换字体来源 System 系统 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index cbbd740bc..259e0c0f7 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -3637,6 +3637,7 @@ digits Malayalam 馬來亞拉姆數字 Replace it with another file, or ignore it: its images keep showing the preview saved with the document. 以其他檔案取代,或忽略:其影像會繼續顯示隨文件儲存的預覽。 Replace Spine 取代脊線 Replace Symbol 取代符號 + Reset Transformation 重設變形 Replace With Font 取代為字體 Replace With Font From 取代字體來源 System 系統 diff --git a/crates/ui-egui/src/panels/symbols.rs b/crates/ui-egui/src/panels/symbols.rs index 1b2ff3559..a1ecaf6c5 100644 --- a/crates/ui-egui/src/panels/symbols.rs +++ b/crates/ui-egui/src/panels/symbols.rs @@ -160,6 +160,14 @@ pub(crate) fn changed_instances_selected(app: &VectorcraftApp) -> bool { instance_selected(app, |c| !c.extras().symbol_overrides.is_empty()) } +/// Does the selection hold a symbol instance whose scale, rotation or shear differs from its +/// symbol's, which Reset Transformation restores? +fn transformed_instances_selected(app: &VectorcraftApp) -> bool { + let Some(doc) = app.session.active().map(|st| st.doc.clone()) else { return false }; + let boxes = doc.symbol_boxes(); + instance_selected(app, |c| vectorcraft_engine::cmd::reset_transformation(&boxes, c).is_some()) +} + /// The symbols of the selected objects when every one is a symbol instance; `None` otherwise. fn selected_instance_symbols(app: &VectorcraftApp) -> Option> { let st = app.session.active()?; @@ -178,7 +186,9 @@ fn selected_instance_symbols(app: &VectorcraftApp) -> Option> { /// The Control bar's Replace menu while symbol instances alone are selected: the document's /// symbols, showing the one the instances share (blank when they differ); picking another /// replaces them all, as "Select a new symbol from the Replace dropdown in the Control panel" -/// (). +/// (), +/// and the Reset button, which gives them their symbol's scale, rotation and shear again +/// (). pub(crate) fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) { let Some(symbols) = selected_instance_symbols(app) else { return }; let Some(st) = app.session.active() else { return }; @@ -199,6 +209,14 @@ pub(crate) fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) { { app.status(e); } + // Reset: Reset Transformation, enabled while an instance's scale, rotation or shear differs + // from its symbol's. + let reset = transformed_instances_selected(app); + if ui.add_enabled_ui(reset, |ui| widgets::flat_button_fit(ui, tl!("Reset"), 48.0).on_hover_text(tl!("Reset Transformation"))).inner.clicked() + && let Err(e) = app.run("symbol.instance.resetTransform", json!({})) + { + app.status(e); + } ui.add_space(4.0); } @@ -328,7 +346,7 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { if menu_item(ui, tl!("Delete Symbol"), sel.is_some() || !marked.is_empty(), false) { delete(app, &marked, sel.as_deref(), false); } - let items: [(&str, bool, &str, Value); 7] = [ + let items: [(&str, bool, &str, Value); 8] = [ (tl!("Symbol Options…"), sel.is_some(), "ui.symbolOptions", json!({"edit": true, "name": sel})), // The selected instance, else the symbol selected in the panel. (tl!("Edit Symbol"), inst || sel.is_some(), "symbol.edit", if inst { json!({}) } else { name.clone() }), @@ -336,6 +354,7 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { (tl!("Replace Symbol"), inst && sel.is_some(), "symbol.replace", name.clone()), (tl!("Break Link to Symbol"), inst, "symbol.breakLink", json!({})), (tl!("Reset Symbol Instance"), changed_instances_selected(app), "symbol.instance.reset", json!({})), + (tl!("Reset Transformation"), transformed_instances_selected(app), "symbol.instance.resetTransform", json!({})), (tl!("Select All Instances"), sel.is_some(), "symbol.selectInstances", name), ]; for (label, enabled, cmd, params) in items { @@ -1300,4 +1319,64 @@ mod tests { let doc = &app.session.active().unwrap().doc; assert!(matches!(&doc.node(leaf).unwrap().kind, NodeKind::SymbolInstance { symbol, .. } if symbol == "Star"), "the instance is replaced"); } + + /// The panel menu's Reset Transformation and the Control bar's Reset are enabled only while a + /// selected instance's scale, rotation or shear differs from its symbol's, and give the + /// instance its symbol's again. + #[test] + fn reset_transformation_is_enabled_for_a_transformed_instance_and_resets_it() { + let mut app = leaf_and_dot(); + let leaf = instance_of(&app, "Leaf").expect("a Leaf instance"); + app.run("select.set", json!({"ids": [leaf.0]})).unwrap(); + let xf = |app: &VectorcraftApp| match app.session.active().unwrap().doc.node(leaf).map(|n| &n.kind) { + Some(NodeKind::SymbolInstance { xf, .. }) => *xf, + _ => panic!("the Leaf instance"), + }; + let master = xf(&app); + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + ctx.enable_accesskit(); + let item_disabled = |app: &mut VectorcraftApp| { + let raw = egui::RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(236.0, 400.0))), ..Default::default() }; + let mut out = ctx.run_ui(raw, |ui| menu(app, ui)); + out.textures_delta.clear(); + let nodes = &out.platform_output.accesskit_update.as_ref().expect("an accessibility tree").nodes; + nodes + .iter() + .find(|(_, n)| n.label().is_some_and(|l| l.trim() == "Reset Transformation")) + .map(|(_, n)| n.is_disabled()) + .expect("the menu item") + }; + // At its symbol's scale the menu item is disabled and a click changes nothing. + assert!(item_disabled(&mut app), "the item is disabled at the symbol's scale"); + let before = app.session.active().unwrap().doc.clone(); + click_menu(&ctx, &mut app, "Reset Transformation"); + assert!(Arc::ptr_eq(&before, &app.session.active().unwrap().doc), "the item is disabled"); + app.run("object.rotate", json!({"angle": 30})).unwrap(); + assert!(!item_disabled(&mut app), "a turned instance enables it"); + click_menu(&ctx, &mut app, "Reset Transformation"); + let close = + |a: vectorcraft_geom::Affine, b: vectorcraft_geom::Affine| a.as_coeffs().iter().zip(b.as_coeffs()).all(|(x, y)| (x - y).abs() < 1e-9); + assert!(close(xf(&app), master), "the menu item resets the instance: {:?} vs {master:?}", xf(&app)); + // The Control bar's Reset. + let bar = |app: &mut VectorcraftApp, events: Vec| { + let raw = egui::RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(800.0, 400.0))), events, ..Default::default() }; + let mut out = ctx.run_ui(raw, |ui| { + ui.horizontal(|ui| control_bar(app, ui)); + }); + out.textures_delta.clear(); + out + }; + let reset_disabled = |out: &egui::FullOutput| { + let nodes = &out.platform_output.accesskit_update.as_ref().expect("an accessibility tree").nodes; + nodes.iter().find(|(_, n)| n.label().is_some_and(|l| l.trim() == "Reset")).map(|(_, n)| n.is_disabled()).expect("a Reset button") + }; + assert!(reset_disabled(&bar(&mut app, vec![])), "Reset is disabled at the symbol's scale"); + app.run("object.scale", json!({"sx": 150, "sy": 50})).unwrap(); + let out = bar(&mut app, vec![]); + assert!(!reset_disabled(&out)); + let at = clickable(&out).into_iter().find(|(l, _)| l == "Reset").expect("a Reset button").1; + bar(&mut app, vec![egui::Event::PointerMoved(at), button(at, true), button(at, false)]); + assert!(close(xf(&app), master), "the Control bar's Reset resets the instance: {:?} vs {master:?}", xf(&app)); + } } diff --git a/docs/mcp.md b/docs/mcp.md index 82bdb808c..abb1c5902 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1717,6 +1717,13 @@ already walks Image Trace. Short agent entry points: `symbol.selectInstances {name}`, `symbol.delete {name | names, expandInstances?}` (`names` deletes several in one undo step). `symbol.new` and `symbol.duplicate {name, newName?}` trim the white space around the name they are given, as `symbol.options` trims `newName`; a blank one gives "New Symbol" or " copy". +- Reset Transformation (the Symbols panel menu, and Reset on the Control bar while symbol instances are selected): + `symbol.instance.resetTransform {ids?}` gives the selected symbol instances, in groups too, their symbol's own + scale again, without rotation, shear or reflection, as one undo step. VectorCraft's own rules: each instance keeps + the center of its bounding box in place; other objects, instances in perspective and instances already at their + symbol's scale are left as they are, and no undo step is added when nothing changes; an id that names no object, or + objects without a symbol instance, is an error. It returns `{ids}`, the instances it changed. The menu item and the + button are enabled only while a selected instance would change. - Dynamic symbols: an instance of a symbol whose `type` is `dynamic` changes the appearance of objects in the symbol's art for itself only and stays linked. `symbol.instance.setAppearance {id?, path, fill?, stroke?, opacity?, blend?}` names the object by `path`, child indices in paint order from the art's root down through its groups (`[]` is the diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 59ff808d4..3ebf29674 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -713,6 +713,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - **Delete Symbol asks first** ([Symbols panel](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/symbols-panel-options.html), [symbol libraries](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/create-or-import-symbol-libraries.html), [Alt-click Delete](https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html)): the Symbols panel's Delete Symbol button and menu item ask "Delete the selected symbol?" with Yes and No for a symbol no instance uses. For a symbol with placed instances they ask with Expand Instances (the instances become ordinary art), Delete Instances and Cancel; either Instances button deletes the symbol in one undo step. Alt-click on the button deletes an unused symbol without asking and still asks about one with instances. A question left open while another document became active deletes nothing. `symbol.delete` itself does not ask. Tests: `panels::symbols::tests::delete_symbol_asks_first_and_alt_click_deletes_an_unused_symbol_at_once`, `the_in_use_question_expands_or_deletes_the_instances`, `a_stale_delete_symbol_question_deletes_nothing`, `the_delete_symbol_questions_show_their_buttons`. - **Symbols › Select All Unused** ([Symbols panel](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/symbols-panel-options.html), #4918): the Symbols panel menu's Select All Unused shows the symbols nothing places selected (`symbol.unused`): instances in the art (opacity masks included), graph designs, pattern tiles and brush art count, and so do instances in a used symbol's art. Delete Symbol, the button or the menu item, then asks "Delete the selected symbols?" once and deletes those still unused in one undo step (`symbol.delete {names}`); Alt-click on the button deletes them without asking, and a click on a symbol picks just it again. Tests: `tests::brushsym::symbol_unused_lists_the_symbols_nothing_places_and_delete_takes_several`, `panels::symbols::tests::select_all_unused_picks_the_unused_symbols_and_delete_deletes_them`, `dialogs::symbol_delete::tests::several_symbols_get_the_plural_question`. - **Expand of symbol instances:** Object › Expand turns selected symbol instances (without effects, not in perspective, in groups or placed documents too) into their art as Break Link to Symbol places it, one level deep, as one undo step; the Expand dialog's Fill and Stroke reach that art (`object.expand`, `crates/engine/src/tests/expand_symbols.rs`; estimated, score unchanged). + - **Reset Transformation** ([Transform symbols](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/transform-symbols.html), [Symbols panel options](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/symbols-panel-options.html)): the Symbols panel menu's Reset Transformation and the Control bar's Reset, shown while symbol instances are selected, give the selected instances their symbol's own scale again, without rotation or shear, as one undo step (`symbol.instance.resetTransform`); an instance in perspective is left as it is. VectorCraft's own rules: each instance keeps the center of its bounding box in place, reflection is removed too, and the menu item and the button are enabled only while a selected instance would change. Tests: `tests::brushsym::reset_transformation_restores_the_symbols_scale_and_keeps_the_center`, `reset_transformation_leaves_unchanged_instances_and_refuses_bad_ids`, `panels::symbols::tests::reset_transformation_is_enabled_for_a_transformed_instance_and_resets_it`. - **Expand of dimensions:** Object › Expand, Expand Appearance, Create Outlines and Outline Stroke turn a dimension whose art they change into a plain group of that art, which stays as it is through later moves, scales and edits of what it measured; undo gives the live dimension back (`crates/engine/tests/it/dimensions.rs`; VectorCraft's own rule; estimated, score unchanged). - **Brush libraries** ([public source](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/apply-and-edit-strokes/create-new-brush-libraries.html), [import](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/apply-and-edit-strokes/import-brushes-from-brush-libraries-or-other-files.html), [Brushes panel](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/apply-and-edit-strokes/brushes-panel-overview.html)): Window › Brush Libraries, the Brushes panel's Brush Libraries Menu and its menu's Open Brush Library open each library in a library panel of its own, as swatch, graphic style and symbol libraries do. The built-in libraries (Art Ribbons, Calligraphic Nibs, Pattern Borders, Scatter Shapes) are original brushes generated in code (`crates/engine/src/cmd/brushlib.rs`); Striped Ribbon's art is filled with the library's own pattern and Sparkles scatters the library's own symbol. A click on a brush adds it to the document's Brushes as one undo step with every resource its art uses (patterns, symbols, swatches, graphic styles, images), makes it the current brush and, with paths selected, applies it to them in the same step; a brush the document already has is not added again and a different brush of the same name comes in numbered ("Wave 2"). Shift/Cmd-clicks select brushes for Add to Brushes; a brush dragged onto a path is added and applied to it, and one dragged onto the Brushes panel is added. Save Brushes… / the Brushes panel's Save Brush Library… write the document's brushes (or the selected ones) as a `.vectorcraft` library into the `Brushes` folder next to the preferences, listed under User Defined, or to a file; Other Library… reads the brushes of any document VectorCraft opens. Thumbnails, in the library panels, the Brushes panel and Brush Options, render brush art with the patterns and symbols it uses. Copying brushed art now carries the patterns and symbols its brush's art uses too. Tests: `crates/engine/src/tests/brushlib.rs`, `crates/ui-egui/src/panels/tests_brush_libraries.rs`, `panels::brushes::tests::library_brush_previews_show_the_patterns_their_art_uses`, `canvas::tests::a_library_brush_dropped_on_a_path_is_added_and_applied_in_one_step`. - A canvas context menu: right-click selects the object under the pointer and lists what applies to the selection (Undo/Redo, clipboard, group, isolation, join, Remove Anchor Points when anchors are selected, masks, compound paths, guides, Transform, Arrange, Select, Export Selection), or the view commands on empty canvas.