From 7625feea08b3af8fd3824c6457fb47df5bbd475f Mon Sep 17 00:00:00 2001 From: bflatastic Date: Tue, 13 Oct 2026 00:49:47 +0700 Subject: [PATCH] Fix #1450: the Pathfinder panel works on a group selected alone --- crates/engine/src/cmd/pathops.rs | 4 +++- crates/engine/src/tests_pathops.rs | 14 +++++++++++++ crates/ui-egui/src/i18n/cs.tsv | 1 + crates/ui-egui/src/i18n/de.tsv | 1 + crates/ui-egui/src/i18n/es.tsv | 1 + crates/ui-egui/src/i18n/fr.tsv | 1 + crates/ui-egui/src/i18n/hu.tsv | 1 + crates/ui-egui/src/i18n/it.tsv | 1 + crates/ui-egui/src/i18n/ja.tsv | 1 + crates/ui-egui/src/i18n/ko.tsv | 1 + crates/ui-egui/src/i18n/pt-br.tsv | 1 + crates/ui-egui/src/i18n/ru.tsv | 1 + crates/ui-egui/src/i18n/uk.tsv | 1 + crates/ui-egui/src/i18n/zh-hans.tsv | 1 + crates/ui-egui/src/i18n/zh-hant.tsv | 1 + crates/ui-egui/src/panels/pathfinder.rs | 26 ++++++++++++++++++++++--- 16 files changed, 53 insertions(+), 4 deletions(-) diff --git a/crates/engine/src/cmd/pathops.rs b/crates/engine/src/cmd/pathops.rs index c7937a11..8b35f394 100644 --- a/crates/engine/src/cmd/pathops.rs +++ b/crates/engine/src/cmd/pathops.rs @@ -343,10 +343,12 @@ fn run_pf(s: &mut Session, op: PathfinderOp) -> Result { continue; } if shape_mode + && roots.len() > 1 && lv.len() > 1 && let Some(top) = lv.last() { - // A group acts as one shape (the union of its leaves) with its top leaf's style. + // Among other objects, a group acts as one shape (the union of its leaves) with + // its top leaf's style. Selected alone, its members are the shapes (#1450). let parts: Vec<(PathData, FillRule)> = lv.iter().filter_map(node_path).collect(); let refs: Vec<(&PathData, FillRule)> = parts.iter().map(|(p, r)| (p, *r)).collect(); let name = n.name.clone().or_else(|| lv.iter().find_map(|l| l.name.clone())); diff --git a/crates/engine/src/tests_pathops.rs b/crates/engine/src/tests_pathops.rs index 05036dd0..6796fd60 100644 --- a/crates/engine/src/tests_pathops.rs +++ b/crates/engine/src/tests_pathops.rs @@ -191,6 +191,20 @@ fn pathfinder_undo_restores() { assert!(s.doc().unwrap().selection.contains(a) && s.doc().unwrap().selection.contains(b)); } +/// #1450: a group selected alone gives the shape modes its members to work on. +#[test] +fn a_group_selected_alone_is_combined_member_by_member() { + let mut s = session(); + let a = rect(&mut s, 0.0, 0.0, 20.0, 20.0); + let b = rect(&mut s, 10.0, 0.0, 20.0, 20.0); + s.execute("select.set", &json!({"ids": [a.0, b.0]})).unwrap(); + s.execute("object.group", &json!({})).unwrap(); + assert_eq!(s.doc().unwrap().selection.len(), 1); + let id = s.execute("object.pathfinder.minusFront", &json!({})).unwrap()["ids"][0].as_u64().unwrap(); + let bounds = s.doc().unwrap().doc.node(NodeId(id)).unwrap().geometric_bounds().unwrap(); + assert_eq!((bounds.x0, bounds.x1), (0.0, 10.0), "the back member less the front one"); +} + #[test] fn unite_with_group() { let mut s = session(); diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 94798738..8069b93c 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -4640,6 +4640,7 @@ mark-style Japanese Japonské Use Outside Rectangle As Frame Použít vnější obdélník jako rámeček Create Compound Path From Ellipses Vytvořit složenou cestu z elips Fill Grid Vyplnit mřížku + Select two or more objects, or a group Vyberte dva nebo více objektů, nebo skupinu # 3D and Materials › Extrude & Bevel, Inflate and Rotate: options, Position presets and bevel shapes. Back Zezadu Both sides Obě strany diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 949f4b84..b271a586 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -4163,6 +4163,7 @@ theme Appearance Erscheinungsbild Use Outside Rectangle As Frame Äußeres Rechteck als Rahmen verwenden Create Compound Path From Ellipses Zusammengesetzten Pfad aus Ellipsen erstellen Fill Grid Raster füllen + Select two or more objects, or a group Wählen Sie zwei oder mehr Objekte oder eine Gruppe aus # 3D and Materials › Extrude & Bevel, Inflate and Rotate: options, Position presets and bevel shapes. Back Hinten Both sides Beidseitig diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index ae6ac751..9664be52 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -4580,6 +4580,7 @@ mark-style Japanese Japonesas X: X: Y: Y: @id file.close Cerrar archivo + Select two or more objects, or a group Seleccione dos o más objetos, o un grupo # 3D and Materials › Extrude & Bevel, Inflate and Rotate: options, Position presets and bevel shapes. Back Posterior Both sides Ambos lados diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 85242a40..c8e81339 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -4216,6 +4216,7 @@ lighting Light Lumière Use Outside Rectangle As Frame Utiliser le rectangle extérieur comme cadre Create Compound Path From Ellipses Créer un tracé transparent à partir des ellipses Fill Grid Remplir la grille + Select two or more objects, or a group Sélectionnez au moins deux objets, ou un groupe # 3D and Materials › Extrude & Bevel, Inflate and Rotate: options, Position presets and bevel shapes. Back Arrière Both sides Deux côtés diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 3662f906..f89a3dcf 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -4606,6 +4606,7 @@ theme Appearance Kinézet Use Outside Rectangle As Frame Külső téglalap használata keretként Create Compound Path From Ellipses Összetett útvonal létrehozása az ellipszisekből Fill Grid Rács kitöltése + Select two or more objects, or a group Jelöljön ki két vagy több objektumot, vagy egy csoportot # 3D and Materials › Extrude & Bevel, Inflate and Rotate: options, Position presets and bevel shapes. Back Hátulról Both sides Mindkét oldal diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index f1c529c7..edd90df1 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -4208,6 +4208,7 @@ lighting Light Luce Use Outside Rectangle As Frame Usa rettangolo esterno come cornice Create Compound Path From Ellipses Crea tracciato composto dalle ellissi Fill Grid Riempi griglia + Select two or more objects, or a group Selezionate due o più oggetti, oppure un gruppo # 3D and Materials › Extrude & Bevel, Inflate and Rotate: options, Position presets and bevel shapes. Back Retro Both sides Entrambi i lati diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 3fdeeb5c..32899992 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -4202,6 +4202,7 @@ lighting Light ライト Use Outside Rectangle As Frame 外側の長方形をフレームとして使用 Create Compound Path From Ellipses 楕円形から複合パスを作成 Fill Grid グリッドを塗りつぶす + Select two or more objects, or a group 2 つ以上のオブジェクト、またはグループを選択してください # 3D and Materials › Extrude & Bevel, Inflate and Rotate: options, Position presets and bevel shapes. Back 背面 Both sides 両面 diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index cb1dad70..c3f1595c 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -4172,6 +4172,7 @@ lighting Light 광원 Use Outside Rectangle As Frame 바깥쪽 사각형을 프레임으로 사용 Create Compound Path From Ellipses 원에서 컴파운드 패스 만들기 Fill Grid 격자 칠하기 + Select two or more objects, or a group 두 개 이상의 개체 또는 그룹을 선택하십시오 # 3D and Materials › Extrude & Bevel, Inflate and Rotate: options, Position presets and bevel shapes. Back 뒷면 Both sides 양면 diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 50ed39d9..5fe36b5d 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -3687,3 +3687,4 @@ axis Both Ambos Use Outside Rectangle As Frame Usar Retângulo Externo como Moldura Create Compound Path From Ellipses Criar Caminho Composto a partir das Elipses Fill Grid Preencher Grade + Select two or more objects, or a group Selecione dois ou mais objetos, ou um grupo diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index b22407a6..9c51c488 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -4204,6 +4204,7 @@ lighting Light Свет Use Outside Rectangle As Frame Использовать внешний прямоугольник как рамку Create Compound Path From Ellipses Создать составной контур из эллипсов Fill Grid Залить сетку + Select two or more objects, or a group Выделите два или более объектов либо группу # 3D and Materials › Extrude & Bevel, Inflate and Rotate: options, Position presets and bevel shapes. Back Сзади Both sides С обеих сторон diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 293f79fa..55c2cab9 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -4188,6 +4188,7 @@ lighting Light Світло Use Outside Rectangle As Frame Використовувати зовнішній прямокутник як рамку Create Compound Path From Ellipses Створити складений контур з еліпсів Fill Grid Залити сітку + Select two or more objects, or a group Виділіть два або більше об’єктів чи групу # 3D and Materials › Extrude & Bevel, Inflate and Rotate: options, Position presets and bevel shapes. Back Ззаду Both sides З обох боків diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 95dd4b20..c07ef172 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -3799,6 +3799,7 @@ lighting Light 光源 Use Outside Rectangle As Frame 使用外部矩形作为框架 Create Compound Path From Ellipses 从椭圆形创建复合路径 Fill Grid 网格填色 + Select two or more objects, or a group 请选择两个或更多对象,或一个组 # 3D and Materials › Extrude & Bevel, Inflate and Rotate: options, Position presets and bevel shapes. Back 背面 Both sides 双面 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 84d609b0..54d8c344 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -3580,6 +3580,7 @@ lighting Light 光源 Use Outside Rectangle As Frame 使用外部矩形作為框架 Create Compound Path From Ellipses 從橢圓形建立複合路徑 Fill Grid 格線填色 + Select two or more objects, or a group 請選取兩個或更多物件,或一個群組 # 3D and Materials › Extrude & Bevel, Inflate and Rotate: options, Position presets and bevel shapes. Back 背面 Both sides 雙面 diff --git a/crates/ui-egui/src/panels/pathfinder.rs b/crates/ui-egui/src/panels/pathfinder.rs index 7f25d5e1..6a78816f 100644 --- a/crates/ui-egui/src/panels/pathfinder.rs +++ b/crates/ui-egui/src/panels/pathfinder.rs @@ -29,14 +29,34 @@ fn run(app: &mut VectorcraftApp, ui: &Ui, op: &str, label: &str) { } } +/// How many objects the selection gives an operation: a group selected alone counts as its +/// members (#1450). +fn operands(app: &VectorcraftApp) -> usize { + let alone = app.session.active().filter(|d| d.selection.len() == 1).and_then(|d| d.doc.node(*d.selection.objects.first()?)); + match alone.and_then(|n| n.children().filter(|_| matches!(n.kind, vectorcraft_doc::NodeKind::Group { .. }))) { + Some(members) => members.len(), + None => selection_len(app), + } +} + +/// An operation's button, greyed out and saying what it needs unless the selection has `min` operands. +fn button(ui: &mut Ui, icon: &str, tip: &str, n: usize, min: usize) -> bool { + let resp = widgets::icon_button_enabled(ui, icon, tip, false, n >= min, 34.0); + if n < min { + resp.on_hover_text(tl!("Select two or more objects, or a group")); + return false; + } + resp.clicked() +} + pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { - let n = selection_len(app); + let n = operands(app); let t = crate::theme::Tokens::get(ui.ctx()); widgets::subheader(ui, tl!("Shape Modes:")); ui.horizontal(|ui| { ui.spacing_mut().item_spacing.x = 5.0; for (icon, tip, op) in SHAPE_MODES { - if widgets::icon_button_enabled(ui, icon, tip, false, n >= 2, 34.0).clicked() { + if button(ui, icon, tip, n, 2) { run(app, ui, op, tip); } } @@ -53,7 +73,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { ui.spacing_mut().item_spacing.x = 5.0; for (icon, tip, op) in PATHFINDERS { let min = if op == "outline" || op == "divide" { 1 } else { 2 }; - if widgets::icon_button_enabled(ui, icon, tip, false, n >= min, 34.0).clicked() { + if button(ui, icon, tip, n, min) { run(app, ui, op, tip); } }