Symbols: Reset Transformation returns an instance to its symbol's scale, rotation and shear

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, shear or reflection, as
one undo step (symbol.instance.resetTransform). Each instance keeps the
center of its art's bounding box in place. Instances in perspective and
instances already at their symbol's scale are left as they are, no undo
step is added when nothing changes, and both controls are enabled only
while a selected instance would change.

Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
dotCooCoo
2026-11-08 12:21:12 +07:00
committed by bflatastic
parent 1d32aff63f
commit d687456978
34 changed files with 291 additions and 4 deletions
+4
View File
@@ -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")];
+14 -2
View File
@@ -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<CommandSpec> {
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",
+46
View File
@@ -696,6 +696,52 @@ pub(super) fn symbol_replace(s: &mut Session, p: &Value) -> Result<Value> {
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<Affine> {
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<Value> {
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<NodeId> = changes.iter().map(|(id, _)| *id).collect();
Ok(json!({ "ids": ids_json(&changed) }))
}
pub(super) fn symbol_set_current(s: &mut Session, p: &Value) -> Result<Value> {
let name = resolve_symbol(&s.doc()?.doc, p, "symbol.setCurrent")?;
set_unknown(s, CURRENT_SYMBOL, Some(json!(name)))?;
+111
View File
@@ -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(&centers) {
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]
+1
View File
@@ -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 Система
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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ů
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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 Σύστημα
Can't render this file because it contains an unexpected character in line 163 and column 5.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3685,6 +3685,7 @@
Replace Missing Fonts… अनुपलब्ध फ़ॉन्ट रिप्लेस करें…
Replace Spine स्पाइन रिप्लेस करें
Replace Symbol सिंबल रिप्लेस करें
Reset Transformation रूपांतरण रीसेट करें
Replace With Font इस फ़ॉन्ट से रिप्लेस करें
Replace With Font From इस स्रोत के फ़ॉन्ट से रिप्लेस करें
Replace with Top Object सबसे ऊपर के ऑब्जेक्ट से रिप्लेस करें
Can't render this file because it is too large.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2243 and column 5.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2753,6 +2753,7 @@ mark-style Japanese 日本式
Replace: 置換:
Replace Font フォントを置換
Replace Symbol シンボルを置き換え
Reset Transformation 変形をリセット
Replace With Font 置換するフォント
Replace With Font From 置換するフォントの一覧
System システム
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2765,6 +2765,7 @@ theme Appearance იერსახე
Redefine Symbol სიმბოლოს ხელახლა განსაზღვრა
Remove Brush Stroke ფუნჯის შტრიხის მოხსნა
Replace Symbol სიმბოლოს ჩანაცვლება
Reset Transformation გარდაქმნის ჩამოყრა
Select All Instances ყველა ეგზემპლარის მონიშვნა
Symbol Sprayer სიმბოლოების სპრეი
Symbolism Tool სიმბოლოების ხელსაწყო
Can't render this file because it is too large.
+1
View File
@@ -2742,6 +2742,7 @@ mark-style Japanese 일본식
Replace: 바꾸기:
Replace Font 글꼴 바꾸기
Replace Symbol 심볼 바꾸기
Reset Transformation 변형 재설정
Replace With Font 바꿀 글꼴
Replace With Font From 바꿀 글꼴 목록
System 시스템
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -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 Система
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 5 and column 25.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 5 and column 25.
+1
View File
@@ -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 Система
Can't render this file because it contains an unexpected character in line 177 and column 5.
+1
View File
@@ -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
Can't render this file because it contains an unexpected character in line 199 and column 5.
+1
View File
@@ -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 系统
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -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 系統
Can't render this file because it contains an unexpected character in line 6 and column 47.
+81 -2
View File
@@ -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<Vec<String>> {
let st = app.session.active()?;
@@ -178,7 +186,9 @@ fn selected_instance_symbols(app: &VectorcraftApp) -> Option<Vec<String>> {
/// 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"
/// (<https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/edit-symbols.html>).
/// (<https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/edit-symbols.html>),
/// and the Reset button, which gives them their symbol's scale, rotation and shear again
/// (<https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/transform-symbols.html>).
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<egui::Event>| {
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));
}
}
+7
View File
@@ -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 "<name> 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
+1
View File
@@ -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.