diff --git a/crates/engine/src/cmd/mod.rs b/crates/engine/src/cmd/mod.rs index 8e348b8f..5fb39922 100644 --- a/crates/engine/src/cmd/mod.rs +++ b/crates/engine/src/cmd/mod.rs @@ -493,6 +493,9 @@ pub use flatten::FlattenOptions; /// A built-in or saved flattener preset ([`crate::Prefs::flattener_presets`]). pub use flatten::FlattenerPreset; +/// The shapes a group selected alone gives the Pathfinder panel. +pub use pathops::shape_count; + /// Command ids kept only so older scripts keep working: `(alias, the command it duplicates)`. /// They run as before but the command palette leaves them out (it lists the command once). pub const ALIASES: &[(&str, &str)] = &[("object.convertDocumentColorMode", "file.documentColorMode")]; diff --git a/crates/engine/src/cmd/pathops.rs b/crates/engine/src/cmd/pathops.rs index fe33554f..b3354b08 100644 --- a/crates/engine/src/cmd/pathops.rs +++ b/crates/engine/src/cmd/pathops.rs @@ -258,6 +258,14 @@ fn leaves(n: &Node, out: &mut Vec) { } } +/// How many shapes a Pathfinder operation takes from `n` selected alone: its leaf shapes, however +/// deep its groups nest (an expanded stroke is a group inside the group, #1635). +pub fn shape_count(n: &Node) -> usize { + let mut lv = vec![]; + leaves(n, &mut lv); + lv.len() +} + /// A node for `path`: a plain path for one subpath, else a compound path. pub(crate) fn shape_node(d: &mut Document, path: PathData, style_src: Option<&Node>) -> Node { let id = d.alloc_id(); diff --git a/crates/ui-egui/src/panels/pathfinder.rs b/crates/ui-egui/src/panels/pathfinder.rs index 6a78816f..42577236 100644 --- a/crates/ui-egui/src/panels/pathfinder.rs +++ b/crates/ui-egui/src/panels/pathfinder.rs @@ -29,12 +29,12 @@ 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). +/// How many objects the selection gives an operation: a group selected alone counts as the shapes +/// the operation takes from it, nested groups included (#1450, #1635). 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(), + match alone.filter(|n| matches!(n.kind, vectorcraft_doc::NodeKind::Group { .. })) { + Some(group) => vectorcraft_engine::cmd::shape_count(group), None => selection_len(app), } } @@ -96,3 +96,33 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { menu_item(ui, tl!("Release Compound Shape"), false, false); menu_item(ui, tl!("Expand Compound Shape"), false, false); } + +#[cfg(test)] +mod tests { + use super::*; + use vectorcraft_engine::doc::{NodeId, NodeKind}; + + /// #1635: a stroked object expanded inside a group leaves a group holding one group (its fill + /// and its outlined stroke). The Shape Modes take the group's shapes, so they stay enabled, and + /// Unite merges the fill and the stroke into one path. + #[test] + fn shape_modes_take_the_shapes_of_an_expanded_stroke_in_a_group() { + let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default()); + app.run("file.new", json!({"width": 200, "height": 200})).unwrap(); + app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap(); + app.run("stroke.set", json!({"weight": 10})).unwrap(); + app.run("object.group", json!({})).unwrap(); + app.run("object.expand", json!({})).unwrap(); + let doc = &app.session.active().unwrap().doc; + let group = doc.node(*app.session.active().unwrap().selection.objects.first().unwrap()).unwrap(); + let members = group.children().unwrap(); + assert_eq!(members.len(), 1, "one member: the expanded rectangle"); + assert!(matches!(members[0].kind, NodeKind::Group { .. }), "the fill and the outlined stroke, grouped"); + assert_eq!(operands(&app), 2, "the fill and the outlined stroke"); + let id = app.run("object.pathfinder.unite", json!({})).unwrap()["ids"][0].as_u64().unwrap(); + let united = app.session.active().unwrap().doc.node(NodeId(id)).unwrap(); + assert!(matches!(united.kind, NodeKind::Path { .. }), "one path, the stroke's inner edge gone: {:?}", united.kind); + let b = united.geometric_bounds().unwrap(); + assert_eq!((b.x0, b.y0, b.x1, b.y1), (5.0, 5.0, 65.0, 65.0), "the stroke's outer edge"); + } +}