Merge pull request #3950 from Taha-Mahmoodi/feat/properties-contextual-controls

Properties: Blend Options, Merge Live Paint, Width Profile, an effect's Delete icon and Direct-Selected corners
This commit is contained in:
BFlat
2026-10-24 19:07:05 +07:00
committed by GitHub
32 changed files with 476 additions and 9 deletions
+18 -2
View File
@@ -58,7 +58,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Make",
["Object", "Live Paint"],
Some("Cmd+Alt+X"),
"{ids?|id?} build a Live Paint group from the selected paths; with a Live Paint group among them, add the others to it as livePaint.merge does → {id, faces, edges}",
"{ids?|id?} build a Live Paint group from the selected paths (paths, compound paths and groups of them; type, images and other objects are refused, nothing changes); with a Live Paint group among them, add the others to it as livePaint.merge does → {id, faces, edges}",
has_selection_or_ids,
lp_make
),
@@ -67,7 +67,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Merge",
["Object", "Live Paint"],
None,
"{ids?|id?} add the other selected paths (and Live Paint groups) to the bottom-most Live Paint group, keeping its id, its paint and its own properties (name, lock, visibility, transparency, appearance, opacity mask, graphic style link) → {id, faces, edges}",
"{ids?|id?} add the other selected paths (compound paths, groups of paths and Live Paint groups too; type, images and other objects are refused, nothing changes) to the bottom-most Live Paint group, keeping its id, its paint and its own properties (name, lock, visibility, transparency, appearance, opacity mask, graphic style link) → {id, faces, edges}",
has_multi_or_ids,
lp_merge
),
@@ -609,9 +609,24 @@ fn merged_gap_options(groups: &[GapOptions]) -> GapOptions {
if !bottom.detect && groups.iter().any(|g| g.detect) { GapOptions { detect: true, size: GapSize::Small, ..bottom } } else { bottom }
}
/// Refuse `roots` when one of them is something Live Paint can't take ([`b::live_paint_takes`]):
/// its faces and edges come from path outlines alone, so type or an image would vanish among the
/// group's hidden sources. Checked before anything changes.
fn check_live_paint_takes(d: &Document, roots: &[NodeId]) -> Result<()> {
for r in roots {
if !b::live_paint_takes(d.node(*r).ok_or(EngineError::NoNode(*r))?) {
return Err(EngineError::Other(
"Live Paint takes paths and compound paths: convert type, images and other objects to paths first".into(),
));
}
}
Ok(())
}
/// Make a Live Paint group from `roots`, or add them to the first Live Paint group among them
/// as Merge does; returns (group, faces, edges).
fn make_live(d: &mut Document, sel: &mut Selection, roots: &[NodeId]) -> Result<(NodeId, usize, usize)> {
check_live_paint_takes(d, roots)?;
if roots.iter().any(|r| d.node(*r).is_some_and(is_live_paint)) {
return merge_live(d, sel, roots);
}
@@ -644,6 +659,7 @@ fn merge_live(d: &mut Document, sel: &mut Selection, roots: &[NodeId]) -> Result
.copied()
.find(|r| d.node(*r).is_some_and(is_live_paint))
.ok_or_else(|| EngineError::Other("Live Paint Merge: select a Live Paint group and the paths to add".into()))?;
check_live_paint_takes(d, roots)?;
let old = d.node(target).cloned().ok_or(EngineError::NoNode(target))?;
// Paint to carry over: (point inside face, fill) and (point on edge, stroke).
let keep_faces: Vec<(Point, Paint)> = b::faces(&old)
+44
View File
@@ -1581,6 +1581,50 @@ fn live_paint_merge_keeps_paint_and_adds_faces() {
assert_eq!(layer_children(&s), 1);
}
/// Live Paint builds its faces and edges from path outlines alone, so Merge, Make and the Live
/// Paint Bucket refuse type, an image, or a group holding type: before, they went into the
/// group's hidden sources and vanished from the canvas. Nothing changes, not even the undo
/// history. Paths, and groups of paths, still merge.
#[test]
fn live_paint_refuses_type_and_images_and_changes_nothing() {
let mut s = session();
let g = live(&mut s);
let text = NodeId(s.execute("text.create", &json!({"x": 200, "y": 300, "text": "Hi"})).unwrap()["id"].as_u64().unwrap());
let image = add_image(&mut s, &vectorcraft_trace::Raster::from_fn(4, 4, |_, _| [0, 0, 0, 255]), Affine::translate((300.0, 300.0)));
let path = rect(&mut s, 400.0, 20.0, 40.0, 40.0);
s.execute("select.set", &json!({"ids": [text.0, path.0]})).unwrap();
let text_group = NodeId(s.execute("object.group", &json!({})).unwrap()["id"].as_u64().unwrap());
let state = |s: &Session| {
let d = s.doc().unwrap();
(d.doc.clone(), d.selection.clone(), d.history.undo.len())
};
let want = "Live Paint takes paths and compound paths: convert type, images and other objects to paths first";
for other in [text, image, text_group] {
let calls = [
("livePaint.merge", json!({"ids": [g.0, other.0]})),
("livePaint.make", json!({"ids": [g.0, other.0]})),
("livePaint.make", json!({"ids": [other.0]})),
("livePaint.fill", json!({"ids": [other.0], "point": [200, 300], "color": "#00ff00"})),
];
for (c, p) in calls {
s.execute("select.set", &json!({"ids": [g.0, other.0]})).unwrap();
let before = state(&s);
let r = s.execute(c, &p);
assert!(matches!(&r, Err(EngineError::Other(m)) if m == want), "{c} {p}: {r:?}");
assert!(state(&s) == before, "{c} {p} changed the document");
}
}
// A group of paths and a path still merge.
let (a, b) = (rect(&mut s, 120.0, 20.0, 100.0, 20.0), rect(&mut s, 120.0, 60.0, 100.0, 20.0));
s.execute("select.set", &json!({"ids": [a.0, b.0]})).unwrap();
let paths = NodeId(s.execute("object.group", &json!({})).unwrap()["id"].as_u64().unwrap());
let c = rect(&mut s, 0.0, 120.0, 100.0, 20.0);
let r = s.execute("livePaint.merge", &json!({"ids": [g.0, paths.0, c.0]})).unwrap();
assert_eq!(NodeId(r["id"].as_u64().unwrap()), g);
assert!(vectorcraft_tools::builder::live_paint_takes(&node(&s, g)));
assert!(!vectorcraft_tools::builder::live_paint_takes(&node(&s, text_group)));
}
/// Give Live Paint group `g` a lock, hidden state, appearance, knockout, opacity mask and
/// graphic style link, and return it as it then is.
fn dress_live_group(s: &mut Session, g: NodeId) -> Node {
+13
View File
@@ -39,6 +39,19 @@ pub fn is_live_paint(n: &Node) -> bool {
matches!(n.kind, NodeKind::Group { clip: false, .. }) && n.name.as_deref().is_some_and(|s| s.starts_with(LIVE_PAINT_PREFIX))
}
/// Whether Live Paint can take `n` as it is: a Live Paint group, a path or compound path, or a
/// group of those. Type, images, symbols, blends, meshes and the rest have to be converted to
/// paths first (<https://helpx.adobe.com/illustrator/desktop/paint-and-fill/learn-painting-basics/create-live-paint-groups.html>);
/// Make and Merge refuse them rather than hide them among the group's sources.
pub fn live_paint_takes(n: &Node) -> bool {
is_live_paint(n)
|| match &n.kind {
NodeKind::Path { .. } | NodeKind::Compound { .. } => true,
NodeKind::Group { children, .. } => children.iter().all(|c| live_paint_takes(c)),
_ => false,
}
}
fn named(n: &Node, name: &str) -> bool {
n.name.as_deref() == Some(name)
}
+1
View File
@@ -3754,6 +3754,7 @@ axis Both И двете
@msg Installed plug-in {_1} Инсталирана добавка {_1}
@msg Internal error: {msg} (please report this bug) Вътрешна грешка: {msg} (моля, докладвайте този софтуерен дефект)
@msg Live Paint Merge: select a Live Paint group and the paths to add Обединяване в интерактивно боядисване: изберете група за интерактивно боядисване и пътищата за добавяне
@msg Live Paint takes paths and compound paths: convert type, images and other objects to paths first Интерактивното боядисване приема пътища и съставни пътища: първо преобразувайте текста, изображенията и другите обекти в пътища
@msg Live Paint: select paths or compound paths Интерактивно боядисване: изберете пътища или съставни пътища
@msg Live Paint: these paths are too degenerate to paint Интерактивно боядисване: тези пътища са твърде дегенерирали за боядисване
@msg Make Guides: select paths Създаване на водачи: изберете пътища
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4142,6 +4142,7 @@ imageTrace Outline Obrys
@msg Installed plug-in {_1} Nainstalován zásuvný modul {_1}
@msg Internal error: {msg} (please report this bug) Vnitřní chyba: {msg} (nahlaste prosím tuto chybu)
@msg Live Paint Merge: select a Live Paint group and the paths to add Sloučit živou malbu: vyberte skupinu živé malby a cesty, které chcete přidat
@msg Live Paint takes paths and compound paths: convert type, images and other objects to paths first Živá malba přijímá cesty a složené cesty: text, obrazy a jiné objekty nejprve převeďte na cesty
@msg Live Paint: select paths or compound paths Živá malba: vyberte cesty nebo složené cesty
@msg Live Paint: these paths are too degenerate to paint Živá malba: tyto cesty jsou příliš degenerované, aby je bylo možné malovat
@msg Make Guides: select paths Vytvořit vodítka: vyberte cesty
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3875,6 +3875,7 @@ imageTrace Outline Kontur
@msg Installed plug-in {_1} Installerede plug-in'en {_1}
@msg Internal error: {msg} (please report this bug) Intern fejl: {msg} (meld venligst denne fejl)
@msg Live Paint Merge: select a Live Paint group and the paths to add Live Paint-fletning: markér en Live Paint-gruppe og de kurver, der skal tilføjes
@msg Live Paint takes paths and compound paths: convert type, images and other objects to paths first Live Paint tager kurver og sammensatte kurver: konvertér først tekst, billeder og andre objekter til kurver
@msg Live Paint: select paths or compound paths Live Paint: markér kurver eller sammensatte kurver
@msg Live Paint: these paths are too degenerate to paint Live Paint: disse kurver er for degenererede til at blive malet
@msg Make Guides: select paths Opret hjælpelinjer: markér kurver
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3762,6 +3762,7 @@ theme Appearance Erscheinungsbild
@msg Installed plug-in {_1} Plug-in {_1} installiert
@msg Internal error: {msg} (please report this bug) Interner Fehler: {msg} (bitte melden Sie diesen Fehler)
@msg Live Paint Merge: select a Live Paint group and the paths to add Interaktiv malen – Zusammenfügen: Wählen Sie eine Interaktiv-malen-Gruppe und die hinzuzufügenden Pfade aus
@msg Live Paint takes paths and compound paths: convert type, images and other objects to paths first Interaktiv malen nimmt Pfade und zusammengesetzte Pfade an: Wandeln Sie Text, Bilder und andere Objekte zuerst in Pfade um
@msg Live Paint: select paths or compound paths Interaktiv malen: Wählen Sie Pfade oder zusammengesetzte Pfade aus
@msg Live Paint: these paths are too degenerate to paint Interaktiv malen: Diese Pfade sind zu entartet, um sie zu bemalen
@msg Make Guides: select paths Hilfslinien erstellen: Wählen Sie Pfade aus
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2104,6 +2104,7 @@ axis Both Και τα δύο
@msg Installed plug-in {_1} Εγκαταστάθηκε η επέκταση {_1}
@msg Internal error: {msg} (please report this bug) Εσωτερικό σφάλμα: {msg} (παρακαλούμε αναφέρετε αυτό το σφάλμα)
@msg Live Paint Merge: select a Live Paint group and the paths to add Συγχώνευση δυναμικής βαφής: επιλέξτε ομάδα δυναμικής βαφής και τις διαδρομές προς προσθήκη
@msg Live Paint takes paths and compound paths: convert type, images and other objects to paths first Η δυναμική βαφή δέχεται διαδρομές και σύνθετες διαδρομές: μετατρέψτε πρώτα κείμενο, εικόνες και άλλα αντικείμενα σε διαδρομές
@msg Live Paint: select paths or compound paths Δυναμική βαφή: επιλέξτε διαδρομές ή σύνθετες διαδρομές
@msg Live Paint: these paths are too degenerate to paint Δυναμική βαφή: αυτές οι διαδρομές είναι πολύ εκφυλισμένες για να χρωματιστούν
@msg Make Guides: select paths Δημιουργία οδηγών: επιλέξτε διαδρομές
Can't render this file because it contains an unexpected character in line 2069 and column 19.
+1
View File
@@ -3838,6 +3838,7 @@ axis Both Ambos
@msg Installed plug-in {_1} Se instaló el plugin {_1}
@msg Internal error: {msg} (please report this bug) Error interno: {msg} (por favor, informa de este error)
@msg Live Paint Merge: select a Live Paint group and the paths to add Combinar pintura interactiva: selecciona un grupo de pintura interactiva y los trazados que quieres agregar
@msg Live Paint takes paths and compound paths: convert type, images and other objects to paths first La pintura interactiva admite trazados y trazados compuestos: convierte primero el texto, las imágenes y otros objetos en trazados
@msg Live Paint: select paths or compound paths Pintura interactiva: selecciona trazados o trazados compuestos
@msg Live Paint: these paths are too degenerate to paint Pintura interactiva: estos trazados están demasiado degenerados para pintarlos
@msg Make Guides: select paths Crear guías: selecciona trazados
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4060,6 +4060,7 @@ imageTrace Outline Ääriviiva
@msg Installed plug-in {_1} Asennettiin laajennus {_1}
@msg Internal error: {msg} (please report this bug) Sisäinen virhe: {msg} (ilmoita tästä virheestä)
@msg Live Paint Merge: select a Live Paint group and the paths to add Elävän maalauksen yhdistäminen: valitse elävän maalauksen ryhmä ja lisättävät polut
@msg Live Paint takes paths and compound paths: convert type, images and other objects to paths first Elävä maalaus ottaa polkuja ja yhdistelmäpolkuja: muunna teksti, kuvat ja muut objektit ensin poluiksi
@msg Live Paint: select paths or compound paths Elävä maalaus: valitse polkuja tai yhdistelmäpolkuja
@msg Live Paint: these paths are too degenerate to paint Elävä maalaus: nämä polut ovat liian surkastuneita maalattaviksi
@msg Make Guides: select paths Luo apuviivat: valitse polkuja
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3835,6 +3835,7 @@ axis Both Les deux
@msg Installed plug-in {_1} Module externe {_1} installé
@msg Internal error: {msg} (please report this bug) Erreur interne : {msg} (merci de signaler ce bogue)
@msg Live Paint Merge: select a Live Paint group and the paths to add Fusion de peinture dynamique : sélectionnez un groupe de peinture dynamique et les tracés à ajouter
@msg Live Paint takes paths and compound paths: convert type, images and other objects to paths first La peinture dynamique accepte les tracés et les tracés transparents : convertissez d'abord le texte, les images et les autres objets en tracés
@msg Live Paint: select paths or compound paths Peinture dynamique : sélectionnez des tracés ou des tracés transparents
@msg Live Paint: these paths are too degenerate to paint Peinture dynamique : ces tracés sont trop dégénérés pour être peints
@msg Make Guides: select paths Créer des repères : sélectionnez des tracés
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4705,6 +4705,7 @@ imageTrace Outline Körvonal
@msg larger than 256 MB nagyobb 256 MB-nál
@msg layers can't be wrap objects réteg nem lehet körbefuttató objektum
@msg Live Paint Merge: select a Live Paint group and the paths to add Élő festés egyesítése: jelölj ki egy élőfestés-csoportot és a hozzáadandó útvonalakat
@msg Live Paint takes paths and compound paths: convert type, images and other objects to paths first Az élő festés útvonalakat és összetett útvonalakat fogad: előbb alakítsd útvonallá a szöveget, a képeket és a többi objektumot
@msg Live Paint: select paths or compound paths Élő festés: jelölj ki útvonalakat vagy összetett útvonalakat
@msg Live Paint: these paths are too degenerate to paint Élő festés: ezek az útvonalak túlságosan elfajultak a festéshez
@msg Loading VectorCraft… A VectorCraft betöltése…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3875,6 +3875,7 @@ imageTrace Outline Kontur
@msg Installed plug-in {_1} Berhasil memasang plug-in {_1}
@msg Internal error: {msg} (please report this bug) Kesalahan internal: {msg} (harap laporkan bug ini)
@msg Live Paint Merge: select a Live Paint group and the paths to add Gabungkan Live Paint: pilih grup Live Paint dan jalur yang akan ditambahkan
@msg Live Paint takes paths and compound paths: convert type, images and other objects to paths first Live Paint menerima jalur dan jalur majemuk: konversikan dulu teks, gambar, dan objek lain menjadi jalur
@msg Live Paint: select paths or compound paths Live Paint: pilih jalur atau jalur majemuk
@msg Live Paint: these paths are too degenerate to paint Live Paint: jalur ini terlalu terdegenerasi untuk dicat
@msg Make Guides: select paths Buat panduan: pilih jalur
Can't render this file because it contains an unexpected character in line 3840 and column 19.
+1
View File
@@ -3878,6 +3878,7 @@ axis Both Entrambi
@msg Installed plug-in {_1} Plug-in installato: {_1}
@msg Internal error: {msg} (please report this bug) Errore interno: {msg} (segnala questo bug)
@msg Live Paint Merge: select a Live Paint group and the paths to add Fondi pittura dinamica: seleziona un gruppo di pittura dinamica e i tracciati da aggiungere
@msg Live Paint takes paths and compound paths: convert type, images and other objects to paths first La pittura dinamica accetta tracciati e tracciati composti: converti prima in tracciati testo, immagini e altri oggetti
@msg Live Paint: select paths or compound paths Pittura dinamica: seleziona tracciati o tracciati composti
@msg Live Paint: these paths are too degenerate to paint Pittura dinamica: questi tracciati sono troppo degeneri per essere dipinti
@msg Make Guides: select paths Crea guide: seleziona tracciati
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3929,6 +3929,7 @@ swatch Bright Violet ビビッドバイオレット
@msg the art has no bounds アートに境界がありません
@msg Image Trace: the traced image is missing 画像トレース: トレース元の画像が見つかりません
@msg Live Paint Merge: select a Live Paint group and the paths to add ライブペイントの結合: ライブペイントグループと、追加するパスを選択してください
@msg Live Paint takes paths and compound paths: convert type, images and other objects to paths first ライブペイントで扱えるのはパスと複合パスです: テキスト、画像、その他のオブジェクトは先にパスに変換してください
@msg Live Paint: select paths or compound paths ライブペイント: パスまたは複合パスを選択してください
@msg Live Paint: these paths are too degenerate to paint ライブペイント: これらのパスは形状がつぶれすぎていてペイントできません
@msg Shape Builder: no region under the pointer シェイプ形成ツール: ポインターの下に領域がありません
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3637,6 +3637,7 @@ font-technology Variable ცვლადი
@msg Installed plug-in {_1} დაინსტალირდა პლაგინი {_1}
@msg Internal error: {msg} (please report this bug) შიდა შეცდომა: {msg} (გთხოვთ, შეგვატყობინოთ ამ ხარვეზის შესახებ)
@msg Live Paint Merge: select a Live Paint group and the paths to add ცოცხალი შეღებვის შერწყმა: მონიშნეთ ცოცხალი შეღებვის ჯგუფი და დასამატებელი ტრაექტორიები
@msg Live Paint takes paths and compound paths: convert type, images and other objects to paths first ცოცხალი შეღებვა იღებს ტრაექტორიებს და შედგენილ ტრაექტორიებს: ჯერ გადააქციეთ ტექსტი, გამოსახულებები და სხვა ობიექტები ტრაექტორიებად
@msg Live Paint: select paths or compound paths ცოცხალი შეღებვა: მონიშნეთ ტრაექტორიები ან შედგენილი ტრაექტორიები
@msg Live Paint: these paths are too degenerate to paint ცოცხალი შეღებვა: ეს ტრაექტორიები ზედმეტად გადაგვარებულია შესაღებად
@msg Make Guides: select paths დამხმარე ხაზების შექმნა: მონიშნეთ ტრაექტორიები
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3861,6 +3861,7 @@ theme Appearance 외관
@msg the art has no bounds 아트웍에 경계가 없습니다
@msg Image Trace: the traced image is missing 이미지 추적: 추적한 이미지가 없습니다
@msg Live Paint Merge: select a Live Paint group and the paths to add 라이브 페인트 병합: 라이브 페인트 그룹과 추가할 패스를 선택하세요
@msg Live Paint takes paths and compound paths: convert type, images and other objects to paths first 라이브 페인트는 패스와 컴파운드 패스만 사용할 수 있습니다: 텍스트, 이미지 및 기타 개체를 먼저 패스로 변환하세요
@msg Live Paint: select paths or compound paths 라이브 페인트: 패스 또는 컴파운드 패스를 선택하세요
@msg Live Paint: these paths are too degenerate to paint 라이브 페인트: 이 패스는 퇴화되어 칠할 수 없습니다
@msg Shape Builder: no region under the pointer 모양 빌더: 포인터 아래에 영역이 없습니다
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3889,6 +3889,7 @@ imageTrace Outline Contour
@msg Installed plug-in {_1} Plug-in {_1} geïnstalleerd
@msg Internal error: {msg} (please report this bug) Interne fout: {msg} (meld deze bug alsjeblieft)
@msg Live Paint Merge: select a Live Paint group and the paths to add Live Paint samenvoegen: selecteer een Live Paint-groep en de paden om toe te voegen
@msg Live Paint takes paths and compound paths: convert type, images and other objects to paths first Live Paint neemt paden en samengestelde paden: zet tekst, afbeeldingen en andere objecten eerst om in paden
@msg Live Paint: select paths or compound paths Live Paint: selecteer paden of samengestelde paden
@msg Live Paint: these paths are too degenerate to paint Live Paint: deze paden zijn te gedegenereerd om te verven
@msg Make Guides: select paths Hulplijnen maken: selecteer paden
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3964,6 +3964,7 @@ imageTrace Outline Kontur
@msg Installed plug-in {_1} Zainstalowano wtyczkę {_1}
@msg Internal error: {msg} (please report this bug) Błąd wewnętrzny: {msg} (zgłoś ten błąd)
@msg Live Paint Merge: select a Live Paint group and the paths to add Scal aktywne malowanie: zaznacz grupę aktywnego malowania i ścieżki do dodania
@msg Live Paint takes paths and compound paths: convert type, images and other objects to paths first Aktywne malowanie przyjmuje ścieżki i ścieżki złożone: najpierw przekształć tekst, obrazy i inne obiekty w ścieżki
@msg Live Paint: select paths or compound paths Aktywne malowanie: zaznacz ścieżki lub ścieżki złożone
@msg Live Paint: these paths are too degenerate to paint Aktywne malowanie: te ścieżki są zbyt zdegenerowane, aby je malować
@msg Make Guides: select paths Utwórz linie pomocnicze: zaznacz ścieżki
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3421,6 +3421,7 @@ axis Both Ambos
@msg Installed plug-in {_1} Plug-in {_1} instalado
@msg Internal error: {msg} (please report this bug) Erro interno: {msg} (relate este bug)
@msg Live Paint Merge: select a Live Paint group and the paths to add Mesclar Pintura dinâmica: selecione um grupo de Pintura dinâmica e os traçados a adicionar
@msg Live Paint takes paths and compound paths: convert type, images and other objects to paths first A pintura dinâmica aceita traçados e traçados compostos: converta primeiro texto, imagens e outros objetos em traçados
@msg Live Paint: select paths or compound paths Pintura dinâmica: selecione traçados ou traçados compostos
@msg Live Paint: these paths are too degenerate to paint Pintura dinâmica: estes traçados são degenerados demais para pintar
@msg Make Guides: select paths Criar guias: selecione traçados
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -3874,6 +3874,7 @@ axis Both Оба
@msg Installed plug-in {_1} Установлен модуль {_1}
@msg Internal error: {msg} (please report this bug) Внутренняя ошибка: {msg} (пожалуйста, сообщите об этой ошибке)
@msg Live Paint Merge: select a Live Paint group and the paths to add Объединение быстрой заливки: выделите группу быстрой заливки и контуры для добавления
@msg Live Paint takes paths and compound paths: convert type, images and other objects to paths first Быстрая заливка принимает контуры и составные контуры: сначала преобразуйте текст, изображения и другие объекты в контуры
@msg Live Paint: select paths or compound paths Быстрая заливка: выделите контуры или составные контуры
@msg Live Paint: these paths are too degenerate to paint Быстрая заливка: эти контуры слишком вырождены для заливки
@msg Make Guides: select paths Создание направляющих: выделите контуры
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3758,6 +3758,7 @@ axis Both Båda
@msg Installed plug-in {_1} Installerade insticksmodulen {_1}
@msg Internal error: {msg} (please report this bug) Internt fel: {msg} (rapportera gärna felet)
@msg Live Paint Merge: select a Live Paint group and the paths to add Sammanfoga Live Paint: markera en Live Paint-grupp och de banor som ska läggas till
@msg Live Paint takes paths and compound paths: convert type, images and other objects to paths first Live Paint tar banor och sammansatta banor: konvertera först text, bilder och andra objekt till banor
@msg Live Paint: select paths or compound paths Live Paint: markera banor eller sammansatta banor
@msg Live Paint: these paths are too degenerate to paint Live Paint: de här banorna är för degenererade för att målas
@msg Make Guides: select paths Skapa stödlinjer: markera banor
Can't render this file because it contains an unexpected character in line 5 and column 25.
+1
View File
@@ -3647,6 +3647,7 @@ axis Both Her İkisi
@msg Installed plug-in {_1} Yüklü eklenti {_1}
@msg Internal error: {msg} (please report this bug) Dahili hata: {msg} (lütfen bu hatayı bildirin)
@msg Live Paint Merge: select a Live Paint group and the paths to add Canlı Boyama Birleştirme: Canlı Boyama grubunu ve eklenecek yolları seçin
@msg Live Paint takes paths and compound paths: convert type, images and other objects to paths first Canlı Boyama yolları ve bileşik yolları kabul eder: önce metni, görüntüleri ve diğer nesneleri yollara dönüştürün
@msg Live Paint: select paths or compound paths Canlı Boyama: yolları veya bileşik yolları seçin
@msg Live Paint: these paths are too degenerate to paint Canlı Boyama: Bu yollar boyanamayacak kadar yozlaşmış
@msg Make Guides: select paths Kılavuzlar Oluşturun: yolları seçin
Can't render this file because it contains an unexpected character in line 5 and column 25.
+1
View File
@@ -3862,6 +3862,7 @@ axis Both Обидві
@msg Installed plug-in {_1} Встановлено модуль {_1}
@msg Internal error: {msg} (please report this bug) Внутрішня помилка: {msg} (будь ласка, повідомте про цю помилку)
@msg Live Paint Merge: select a Live Paint group and the paths to add Об’єднання швидкої заливки: виділіть групу швидкої заливки та контури для додавання
@msg Live Paint takes paths and compound paths: convert type, images and other objects to paths first Швидка заливка приймає контури та складені контури: спершу перетворіть текст, зображення й інші об'єкти на контури
@msg Live Paint: select paths or compound paths Швидка заливка: виділіть контури або складені контури
@msg Live Paint: these paths are too degenerate to paint Швидка заливка: ці контури занадто вироджені для заливки
@msg Make Guides: select paths Створення напрямних: виділіть контури
Can't render this file because it contains an unexpected character in line 749 and column 40.
+1
View File
@@ -4351,6 +4351,7 @@ theme Appearance Giao diện
@msg layers can't be wrap objects lớp không thể là đối tượng bọc
@msg linked pictures show the low-resolution preview saved in the file until their files are found: relink them in the Links panel ảnh liên kết hiển thị bản xem trước độ phân giải thấp đã lưu trong tệp cho đến khi tìm thấy tệp gốc: liên kết lại trong bảng Liên kết
@msg Live Paint Merge: select a Live Paint group and the paths to add Hợp nhất tô trực tiếp: chọn nhóm tô trực tiếp và các đường dẫn để thêm
@msg Live Paint takes paths and compound paths: convert type, images and other objects to paths first Tô trực tiếp nhận đường dẫn và đường dẫn phức: hãy chuyển văn bản, hình ảnh và các đối tượng khác thành đường dẫn trước
@msg Live Paint: select paths or compound paths Tô trực tiếp: chọn đường dẫn hoặc đường dẫn phức
@msg Live Paint: these paths are too degenerate to paint Tô trực tiếp: các đường dẫn này quá suy biến để tô
@msg Loading VectorCraft… Đang tải VectorCraft…
Can't render this file because it contains an unexpected character in line 4054 and column 10.
+1
View File
@@ -3672,6 +3672,7 @@ imageTrace Outline 轮廓
@msg the art has no bounds 图稿没有边界
@msg Image Trace: the traced image is missing 图像描摹:缺少所描摹的图像
@msg Live Paint Merge: select a Live Paint group and the paths to add 实时上色合并:请选择一个实时上色组以及要添加的路径
@msg Live Paint takes paths and compound paths: convert type, images and other objects to paths first 实时上色只接受路径和复合路径:请先将文字、图像和其他对象转换为路径
@msg Live Paint: select paths or compound paths 实时上色:请选择路径或复合路径
@msg Live Paint: these paths are too degenerate to paint 实时上色:这些路径过于退化,无法上色
@msg Shape Builder: no region under the pointer 形状生成器:指针下方没有区域
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -1654,6 +1654,7 @@ file-filter Templates 範本
@msg the art has no bounds 圖稿沒有邊界
@msg Image Trace: the traced image is missing 影像描圖:找不到描圖的影像
@msg Live Paint Merge: select a Live Paint group and the paths to add 即時上色合併:請選取即時上色群組和要加入的路徑
@msg Live Paint takes paths and compound paths: convert type, images and other objects to paths first 即時上色只接受路徑和複合路徑:請先將文字、影像和其他物件轉換為路徑
@msg Live Paint: select paths or compound paths 即時上色:請選取路徑或複合路徑
@msg Live Paint: these paths are too degenerate to paint 即時上色:這些路徑過度退化,無法上色
@msg Shape Builder: no region under the pointer 形狀建立:指標下方沒有區域
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1 -1
View File
@@ -162,7 +162,7 @@ fn built_in_effect(id: &str) -> bool {
/// [`effect_label`] as shown: a built-in effect's in the UI language (an `@id` row for the
/// effect's id wins over the plain row of its English label), a plug-in's as it is.
fn shown_effect_label(id: &str) -> String {
pub(crate) fn shown_effect_label(id: &str) -> String {
match catalog().iter().find(|c| c.0 == id) {
Some(c) => crate::i18n::t_id(id, &c.1).to_string(),
None => effect_label(id),
+363 -1
View File
@@ -3,7 +3,7 @@
use egui::Ui;
use serde_json::json;
use vectorcraft_color::{Color, Paint};
use vectorcraft_doc::{LiveShape, NodeKind, Unit};
use vectorcraft_doc::{LiveCorners, LiveShape, NodeKind, Unit, WidthProfile};
use vectorcraft_engine::cmd::newdoc;
use super::{corner_radius_row, first_selected, pstate, set_pstate};
@@ -48,6 +48,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
divider(ui);
}
transform_section(app, ui);
corner_rows(app, ui);
divider(ui);
// The Gradient tool: the gradient's type and, for a freeform one, how clicks add points (#921).
if app.session.tool_id() == "gradient" {
@@ -111,11 +112,22 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
section_header(ui, tl!("Quick Actions"));
let is_group = matches!(first.as_ref().map(|n| &n.kind), Some(NodeKind::Group { .. }));
let is_live = matches!(first.as_ref().map(|n| &n.kind), Some(NodeKind::Path { live: Some(l), .. }) if !matches!(**l, LiveShape::Path { .. }));
// Blend Options for one blend while the Blend tool is in use, Merge Live Paint for a Live
// Paint group selected with more to merge into it, as the Properties panel overview lists them
// (<https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/properties-panel-overview.html>).
let blend_options = n_sel == 1 && app.session.tool_id() == "blend" && matches!(first.as_ref().map(|n| &n.kind), Some(NodeKind::Blend { .. }));
let merge_live_paint = n_sel > 1 && can_merge_live_paint(app);
let mut actions: Vec<(&str, &str)> = vec![];
if n_sel > 1 {
actions.push((tl!("Group"), "object.group"));
actions.push((tl!("Make Clipping Mask"), "object.clippingMask.make"));
}
if merge_live_paint {
actions.push((tl!("Merge Live Paint"), "livePaint.merge"));
}
if blend_options {
actions.push((tl!("Blend Options"), "ui.blendOptions"));
}
if is_group {
actions.push((tl!("Ungroup"), "object.ungroup"));
actions.push((tl!("Isolate Group"), "object.isolate"));
@@ -148,6 +160,52 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
global_edit_row(app, ui);
}
/// Whether Merge Live Paint can take the selection: a Live Paint group among the selected objects
/// and nothing Live Paint refuses (type, images and the rest; `livePaint.merge` refuses them too).
fn can_merge_live_paint(app: &VectorcraftApp) -> bool {
use vectorcraft_tools::builder::{is_live_paint, live_paint_takes};
app.session.active().is_some_and(|st| {
let nodes = || st.selection.objects.iter().filter_map(|id| st.doc.node(*id));
nodes().any(is_live_paint) && nodes().all(live_paint_takes)
})
}
/// The Corners of the one path whose corners Direct Selection has selected: their kind and radius,
/// which `object.setLiveShape` sets on those corners. The Properties panel overview shows the
/// control while the Direct Selection tool is chosen and a corner of an object is selected
/// (<https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/properties-panel-overview.html>).
/// A live rectangle's and a live polygon's own rows already set the selected corners.
fn corner_rows(app: &mut VectorcraftApp, ui: &mut Ui) {
if app.session.tool_id() != "directSelection" {
return;
}
let Some(st) = app.session.active() else { return };
let [id] = st.selection.objects[..] else { return };
let Some(n) = st.doc.node(id) else { return };
if matches!(n.live_shape(), Some(LiveShape::Rectangle { .. } | LiveShape::Polygon { .. })) {
return;
}
let Some(lc) = LiveCorners::of(n) else { return };
if st.selection.partial(id).is_none_or(|a| lc.corners_of(a).is_empty()) {
return;
}
let n = n.clone();
let kind = super::corner_style(app, &n).1;
let kinds: Vec<&str> = vectorcraft_geom::shapes::CornerKind::ALL.iter().map(|k| crate::dialogs::corners::kind_label(*k)).collect();
let fw = super::field_width(ui);
egui::Grid::new("props-corners").num_columns(2).spacing([6.0, 6.0]).show(ui, |ui| {
dim_label(ui, tl!("Corner:"));
let picked = widgets::dropdown_names(ui, ("props-corners", "kind"), kind.map_or("", crate::dialogs::corners::kind_label), &kinds, fw);
if let Some(k) = picked.and_then(|i| vectorcraft_geom::shapes::CornerKind::ALL.get(i)) {
app.run("object.setLiveShape", json!({"kind": k})).ok();
}
ui.end_row();
dim_label(ui, tl!("Corner Radius:"));
super::corner_radius_field(app, ui, &n, ("props-corners", "radius"), fw);
ui.end_row();
});
}
/// Start Global Edit (Stop Global Edit while it is on) with the Global Edit Options beside it, in
/// Quick Actions as the help page places them
/// (<https://helpx.adobe.com/illustrator/desktop/manage-objects/edit-objects/edit-similar-objects.html>).
@@ -713,6 +771,7 @@ fn appearance_section(app: &mut VectorcraftApp, ui: &mut Ui) {
let Some(n) = first_selected(app) else { return };
section_header(ui, tl!("Appearance"));
fill_stroke_rows(app, ui);
width_profile_row(app, ui);
let fw = super::field_width(ui);
ui.horizontal(|ui| {
widgets::field_label(ui, egui::RichText::new(tl!("Opacity")).size(12.0));
@@ -724,16 +783,68 @@ fn appearance_section(app: &mut VectorcraftApp, ui: &mut Ui) {
app.ui.open_panel = Some("transparency".into());
}
});
// The effects of one selected object: the fill's and stroke's as well as its own.
let effects: Vec<(Option<usize>, usize, &str)> = if super::selection_len(app) == 1 {
let own = n.appearance.effects.iter().enumerate().map(|(k, e)| (None, k, e.id.as_str()));
let items =
n.appearance.items.iter().enumerate().flat_map(|(i, it)| it.effects().iter().enumerate().map(move |(k, e)| (Some(i), k, e.id.as_str())));
own.chain(items).collect()
} else {
vec![]
};
ui.horizontal(|ui| {
// The fx button opens the effect menu, as the Appearance panel's does.
let r = widgets::flat_button(ui, "fx", 34.0).on_hover_text(tl!("Add New Effect"));
egui::Popup::menu(&r).show(|ui| super::appearance::fx_menu(app, ui));
effect_delete(app, ui, n.id, &effects);
if widgets::icon_button(ui, "ellipsis", tl!("Appearance panel"), false, 22.0).clicked() {
app.ui.open_panel = Some("appearance".into());
}
});
}
/// The Delete icon beside the fx button, as the Properties panel overview has it
/// (<https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/properties-panel-overview.html>):
/// with one effect applied, its name and an icon that removes it (`effect.remove`); with several,
/// the icon opens the Appearance panel to remove them there. Nothing without effects.
fn effect_delete(app: &mut VectorcraftApp, ui: &mut Ui, id: vectorcraft_doc::NodeId, effects: &[(Option<usize>, usize, &str)]) {
match effects {
[] => {}
[(item, k, effect)] => {
let t = Tokens::get(ui.ctx());
let name = super::appearance::shown_effect_label(effect);
// A long name is cut short before the Delete and Appearance panel icons.
let room = (ui.available_width() - 2.0 * (22.0 + ui.spacing().item_spacing.x)).max(0.0);
ui.allocate_ui(egui::vec2(room, 22.0), |ui| ui.add(egui::Label::new(egui::RichText::new(name).size(12.0).color(t.text)).truncate()));
if widgets::icon_button(ui, "trash-2", tl!("Remove Effect"), false, 22.0).clicked() {
app.run("effect.remove", json!({"ids": [id.0], "item": item, "index": k})).ok();
}
}
_ => {
if widgets::icon_button(ui, "trash-2", tl!("Remove Effect"), false, 22.0).clicked() {
app.ui.open_panel = Some("appearance".into());
}
}
}
}
/// The stroke's Variable Width Profile (the Control bar's list), shown while the Width tool is in
/// use or the stroke has a variable width, as the Properties panel overview places it
/// (<https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/properties-panel-overview.html>).
fn width_profile_row(app: &mut VectorcraftApp, ui: &mut Ui) {
let stroke = super::current_stroke(app);
let profile = stroke.as_ref().and_then(|s| s.profile.as_ref());
if app.session.tool_id() != "width" && WidthProfile::id_of(profile) == "uniform" {
return;
}
ui.horizontal(|ui| {
dim_label(ui, tl!("Profile:"));
if let Some(id) = super::stroke::profile_dropdown(app, ui, profile) {
app.run("stroke.set", json!({"profile": id})).ok();
}
});
}
/// The Fill and Stroke rows, with the Control bar's widgets: the chips (a click opens the
/// swatches, Shift-click the mixer), the Stroke link that opens the Stroke panel as a popover
/// and the weight spinner with its presets. With nothing selected they set up the next object.
@@ -1979,4 +2090,255 @@ mod tests {
let b = d.node(vectorcraft_doc::NodeId(r)).unwrap().geometric_bounds().unwrap();
assert_eq!((b.x0, b.y0, b.width(), b.height()), (10.0, 40.0, 20.0, 80.0));
}
/// One frame of the panel 1600 points tall, so Quick Actions at its foot are laid out, with
/// `events` → the texts painted, with their rects.
fn tall(app: &mut VectorcraftApp, ctx: &egui::Context, events: Vec<Event>) -> Vec<(String, Rect)> {
let raw = egui::RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(300.0, 1600.0))), events, ..Default::default() };
let mut out = ctx.run_ui(raw, |ui| show(app, ui));
out.textures_delta.clear();
out.shapes
.iter()
.filter_map(|s| match &s.shape {
egui::Shape::Text(t) => Some((t.galley.text().to_string(), Rect::from_min_size(t.pos, t.galley.size()))),
_ => None,
})
.collect()
}
/// Clicks `at` in the tall panel over two frames (press, release), then lets a popup it opens
/// lay out → the texts of the frame after.
fn press(app: &mut VectorcraftApp, ctx: &egui::Context, at: Pos2) -> Vec<(String, Rect)> {
let b = |pressed| Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers: Default::default() };
tall(app, ctx, vec![Event::PointerMoved(at), b(true)]);
tall(app, ctx, vec![b(false)]);
tall(app, ctx, vec![]);
tall(app, ctx, vec![])
}
/// Where the tall panel painted `label`.
fn text_at(texts: &[(String, Rect)], label: &str) -> Rect {
texts.iter().find(|(t, _)| t == label).map(|(_, r)| *r).unwrap_or_else(|| panic!("no {label:?} in {texts:?}"))
}
fn shows(texts: &[(String, Rect)], label: &str) -> bool {
texts.iter().any(|(t, _)| t == label)
}
fn undo_depth(app: &VectorcraftApp) -> usize {
app.session.doc().unwrap().history.undo.len()
}
fn new_app() -> VectorcraftApp {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
app
}
fn rect(app: &mut VectorcraftApp, x: f64, y: f64, w: f64, h: f64) -> serde_json::Value {
app.run("shape.rectangle", json!({"x": x, "y": y, "width": w, "height": h})).unwrap()["id"].clone()
}
/// One blend selected with the Blend tool has Blend Options in Quick Actions, which opens the
/// Blend Options dialog on it; OK is one undo step. Not with another tool, nor for a blend
/// selected with another object.
#[test]
fn blend_options_is_a_quick_action_for_one_blend_with_the_blend_tool() {
let mut app = new_app();
let (a, b) = (rect(&mut app, 20.0, 20.0, 40.0, 40.0), rect(&mut app, 200.0, 20.0, 40.0, 40.0));
app.run("select.set", json!({"ids": [a, b]})).unwrap();
let blend = app.run("object.blend.make", json!({})).unwrap()["id"].clone();
app.run("select.set", json!({"ids": [blend]})).unwrap();
let ctx = egui::Context::default();
assert!(!shows(&tall(&mut app, &ctx, vec![]), "Blend Options"), "not with the Selection tool");
app.select_tool("blend");
app.run("select.set", json!({"ids": [blend]})).unwrap();
let texts = tall(&mut app, &ctx, vec![]);
let depth = undo_depth(&app);
press(&mut app, &ctx, text_at(&texts, "Blend Options").center());
assert_eq!(app.ui.dialog.as_ref().map(|d| d.kind.as_str()), Some(crate::dialogs::blend_options::KIND));
assert_eq!(undo_depth(&app), depth, "opening the dialog changes nothing");
let d = app.ui.dialog.as_mut().unwrap();
d.fields.insert("spacing".into(), json!("steps"));
d.fields.insert("steps".into(), json!(3));
crate::dialogs::confirm(&mut app).unwrap();
assert_eq!(undo_depth(&app), depth + 1, "OK is one undo step");
let c = rect(&mut app, 20.0, 200.0, 40.0, 40.0);
app.select_tool("blend");
app.run("select.set", json!({"ids": [blend, c]})).unwrap();
assert!(!shows(&tall(&mut app, &ctx, vec![]), "Blend Options"), "a blend and another object");
app.run("select.set", json!({"ids": [c]})).unwrap();
assert!(!shows(&tall(&mut app, &ctx, vec![]), "Blend Options"), "a rectangle");
}
/// A Live Paint group selected with a path has Merge Live Paint in Quick Actions, which adds
/// the path to the group in one undo step. Not for the group alone, nor for plain paths, nor
/// with type selected, which Live Paint refuses.
#[test]
fn merge_live_paint_is_a_quick_action_for_a_live_paint_group_and_more() {
let mut app = new_app();
let (a, b) = (rect(&mut app, 20.0, 20.0, 100.0, 60.0), rect(&mut app, 60.0, 40.0, 100.0, 60.0));
app.run("select.set", json!({"ids": [a, b]})).unwrap();
let group = app.run("livePaint.make", json!({})).unwrap()["id"].clone();
let (c, d) = (rect(&mut app, 140.0, 30.0, 60.0, 60.0), rect(&mut app, 300.0, 200.0, 40.0, 40.0));
let text = app.run("text.create", json!({"x": 50, "y": 250, "text": "Hi"})).unwrap()["id"].clone();
let ctx = egui::Context::default();
for (ids, why) in [
(json!([group]), "the group alone"),
(json!([c, d]), "two plain paths"),
(json!([group, text]), "the group and type"),
(json!([group, c, text]), "the group, a path and type"),
] {
app.run("select.set", json!({"ids": ids})).unwrap();
assert!(!shows(&tall(&mut app, &ctx, vec![]), "Merge Live Paint"), "{why}");
}
app.run("select.set", json!({"ids": [group, c]})).unwrap();
let c_id = vectorcraft_doc::NodeId(c.as_u64().unwrap());
let parent = app.session.active().unwrap().doc.parent_of(c_id);
let texts = tall(&mut app, &ctx, vec![]);
let depth = undo_depth(&app);
press(&mut app, &ctx, text_at(&texts, "Merge Live Paint").center());
assert_eq!(undo_depth(&app), depth + 1, "one undo step");
let st = app.session.active().unwrap();
assert_eq!(st.selection.objects.iter().map(|n| json!(n.0)).collect::<Vec<_>>(), std::slice::from_ref(&group));
assert!(st.doc.node(c_id).is_none() || st.doc.parent_of(c_id) != parent, "the path is merged into the group");
app.run("edit.undo", json!({})).unwrap();
assert_eq!(app.session.active().unwrap().doc.parent_of(c_id), parent, "Undo takes it out again");
}
/// A stroke with a variable width shows its Width Profile in the Appearance section, as every
/// stroke does with the Width tool; choosing another profile is one undo step. A uniform stroke
/// shows none with other tools.
#[test]
fn the_width_profile_shows_for_a_variable_width_or_the_width_tool() {
let mut app = new_app();
let line = app.run("shape.line", json!({"x1": 20, "y1": 100, "x2": 300, "y2": 100})).unwrap()["id"].clone();
app.run("select.set", json!({"ids": [line]})).unwrap();
let ctx = egui::Context::default();
assert!(!shows(&tall(&mut app, &ctx, vec![]), "Profile:"), "a uniform stroke");
app.select_tool("width");
app.run("select.set", json!({"ids": [line]})).unwrap();
assert!(shows(&tall(&mut app, &ctx, vec![]), "Profile:"), "the Width tool");
app.select_tool("selection");
app.run("select.set", json!({"ids": [line]})).unwrap();
app.run("stroke.set", json!({"profile": "lens"})).unwrap();
let texts = tall(&mut app, &ctx, vec![]);
// The list sits right of its label.
let spacing = ctx.global_style().spacing.item_spacing.x;
let texts = press(&mut app, &ctx, text_at(&texts, "Profile:").right_center() + vec2(spacing + 50.0, 0.0));
let depth = undo_depth(&app);
press(&mut app, &ctx, text_at(&texts, "Taper Start").center());
assert_eq!(undo_depth(&app), depth + 1, "one undo step");
let profile = app
.session
.active()
.unwrap()
.doc
.node(vectorcraft_doc::NodeId(line.as_u64().unwrap()))
.unwrap()
.appearance
.stroke()
.unwrap()
.profile
.clone();
assert_eq!(WidthProfile::id_of(profile.as_ref()), "taperStart");
}
/// With Direct Selection, a path whose corner is selected shows that corner's kind and radius;
/// each edit sets that corner alone in one undo step. Not with the Selection tool, nor with the
/// whole path selected; a live rectangle keeps its own Corner Radius row instead.
#[test]
fn direct_selected_corners_show_their_kind_and_radius() {
let mut app = new_app();
let star = app.run("shape.star", json!({"cx": 200, "cy": 150, "radius1": 80, "radius2": 40})).unwrap()["id"].as_u64().unwrap();
let ctx = egui::Context::default();
app.run("select.anchors", json!({"id": star, "anchors": [[0, 2]], "mode": "set"})).unwrap();
assert!(!shows(&tall(&mut app, &ctx, vec![]), "Corner:"), "not with the Selection tool");
app.select_tool("directSelection");
app.run("select.anchors", json!({"id": star, "anchors": [[0, 2]], "mode": "set"})).unwrap();
let texts = tall(&mut app, &ctx, vec![]);
assert!(shows(&texts, "Corner:") && shows(&texts, "Corner Radius:"), "{texts:?}");
let corners = |app: &VectorcraftApp| {
let n = app.session.active().unwrap().doc.node(vectorcraft_doc::NodeId(star)).unwrap().clone();
let lc = LiveCorners::of(&n).unwrap();
(lc.radius(0), lc.radius(2), lc.kind(0), lc.kind(2))
};
// The radius: the field right of its label.
let label = text_at(&texts, "Corner Radius:");
let field = texts.iter().find(|(t, r)| t == "0 pt" && r.left() > label.right() && (r.center().y - label.center().y).abs() < 6.0).unwrap().1;
press(&mut app, &ctx, field.center());
let depth = undo_depth(&app);
let key = |key, modifiers| Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers };
tall(&mut app, &ctx, vec![key(egui::Key::A, egui::Modifiers::COMMAND)]);
tall(&mut app, &ctx, vec![Event::Text("6".into())]);
tall(&mut app, &ctx, vec![key(egui::Key::Enter, egui::Modifiers::NONE)]);
let texts = tall(&mut app, &ctx, vec![]);
assert_eq!(undo_depth(&app), depth + 1, "one undo step");
let round = vectorcraft_geom::shapes::CornerKind::Round;
assert_eq!(corners(&app), (0.0, 6.0, round, round));
// The kind, from its list.
let texts = press(&mut app, &ctx, text_at(&texts, "Round").center());
press(&mut app, &ctx, text_at(&texts, "Chamfer").center());
assert_eq!(undo_depth(&app), depth + 2, "one undo step");
assert_eq!(corners(&app), (0.0, 6.0, round, vectorcraft_geom::shapes::CornerKind::Chamfer));
// The whole path selected.
app.run("select.set", json!({"ids": [star]})).unwrap();
assert!(!shows(&tall(&mut app, &ctx, vec![]), "Corner:"), "no corner selected");
// A live rectangle's corner: its own Corner Radius row, once.
let r = rect(&mut app, 20.0, 20.0, 60.0, 40.0).as_u64().unwrap();
app.run("select.anchors", json!({"id": r, "anchors": [[0, 1]], "mode": "set"})).unwrap();
let texts = tall(&mut app, &ctx, vec![]);
assert!(!shows(&texts, "Corner:"), "{texts:?}");
assert_eq!(texts.iter().filter(|(t, _)| t == "Corner Radius:").count(), 1, "{texts:?}");
}
/// One effect on the one selected object shows its name and a Delete icon beside the fx button,
/// which removes it in one undo step, the object's own or its stroke's. With several effects the
/// icon opens the Appearance panel; without one, or with several objects selected, no name.
#[test]
fn one_effect_has_a_delete_icon_in_appearance() {
let mut app = new_app();
let id = rect(&mut app, 20.0, 20.0, 80.0, 60.0);
let node = vectorcraft_doc::NodeId(id.as_u64().unwrap());
app.run("select.set", json!({"ids": [id]})).unwrap();
let ctx = egui::Context::default();
let spacing = ctx.global_style().spacing.item_spacing.x;
let effects = |app: &VectorcraftApp| {
let n = app.session.active().unwrap().doc.node(node).unwrap().clone();
n.appearance.effects.len() + n.appearance.items.iter().map(|i| i.effects().len()).sum::<usize>()
};
let name = super::super::appearance::shown_effect_label("stylize.dropShadow");
assert!(!shows(&tall(&mut app, &ctx, vec![]), &name), "no effect");
// The object's own effect.
app.run("effect.apply", json!({"effect": "stylize.dropShadow"})).unwrap();
let texts = tall(&mut app, &ctx, vec![]);
let depth = undo_depth(&app);
press(&mut app, &ctx, text_at(&texts, &name).right_center() + vec2(spacing + 11.0, 0.0));
assert_eq!((effects(&app), undo_depth(&app)), (0, depth + 1), "removed in one undo step");
app.run("edit.undo", json!({})).unwrap();
assert_eq!(effects(&app), 1);
// The stroke's effect.
app.run("edit.undo", json!({})).unwrap();
let stroke = app.session.active().unwrap().doc.node(node).unwrap().appearance.items.iter().position(|i| !i.is_fill()).unwrap();
app.run("effect.apply", json!({"effect": "stylize.dropShadow", "item": stroke})).unwrap();
let texts = tall(&mut app, &ctx, vec![]);
let depth = undo_depth(&app);
press(&mut app, &ctx, text_at(&texts, &name).right_center() + vec2(spacing + 11.0, 0.0));
assert_eq!((effects(&app), undo_depth(&app)), (0, depth + 1), "the stroke's effect removed in one undo step");
// Two effects: the icon opens the Appearance panel.
app.run("effect.apply", json!({"effect": "stylize.dropShadow"})).unwrap();
app.run("effect.apply", json!({"effect": "stylize.dropShadow", "item": stroke})).unwrap();
let texts = tall(&mut app, &ctx, vec![]);
assert!(!shows(&texts, &name), "{texts:?}");
let depth = undo_depth(&app);
press(&mut app, &ctx, text_at(&texts, "fx").center() + vec2(17.0 + spacing + 11.0, 0.0));
assert_eq!(app.ui.open_panel.as_deref(), Some("appearance"));
assert_eq!((effects(&app), undo_depth(&app)), (2, depth));
// Two objects with one effect each: no name.
app.run("edit.undo", json!({})).unwrap();
let other = rect(&mut app, 200.0, 20.0, 40.0, 40.0);
app.run("effect.apply", json!({"effect": "stylize.dropShadow"})).unwrap();
app.run("select.set", json!({"ids": [id, other]})).unwrap();
assert!(!shows(&tall(&mut app, &ctx, vec![]), &name), "two objects");
}
}
+4 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+4 -2
View File
File diff suppressed because one or more lines are too long