M2.1: recurse into groups and compound paths for path.average

This commit is contained in:
Wadah Badawi
2026-11-02 14:36:07 +07:00
committed by bflatastic
parent 88d8a7839e
commit 66e22f4ba1
2 changed files with 98 additions and 2 deletions
+83
View File
@@ -4165,3 +4165,86 @@ fn shape_counts_that_are_not_whole_numbers_are_errors() {
assert!(s.execute("shape.spiral", &json!({"cx": 1, "cy": 1, "radius": 5, "segments": bad})).is_err(), "{bad}");
}
}
fn all_at(pts: &[Point], c: Point) -> bool {
!pts.is_empty() && pts.iter().all(|p| (p.x - c.x).abs() < 1e-6 && (p.y - c.y).abs() < 1e-6)
}
#[test]
fn average_averages_anchors_in_selected_groups() {
let mut s = session();
let a = rect(&mut s, 0.0, 0.0, 100.0, 100.0);
let b = rect(&mut s, 200.0, 200.0, 100.0, 100.0);
s.execute("select.set", &json!({"ids": [a.0, b.0]})).unwrap();
let g = s.execute("object.group", &json!({})).unwrap()["id"].as_u64().unwrap();
s.execute("select.set", &json!({"ids": [g]})).unwrap();
s.execute("path.average", &json!({"axis": "both"})).unwrap();
let c = Point::new(150.0, 150.0);
assert!(all_at(&anchor_points(&s, a), c), "{:?}", anchor_points(&s, a));
assert!(all_at(&anchor_points(&s, b), c), "{:?}", anchor_points(&s, b));
}
#[test]
fn average_averages_anchors_in_a_selected_compound_path() {
let mut s = session();
let a = rect(&mut s, 0.0, 0.0, 100.0, 100.0);
let b = rect(&mut s, 200.0, 200.0, 100.0, 100.0);
s.execute("select.set", &json!({"ids": [a.0, b.0]})).unwrap();
let cp = NodeId(s.execute("object.compoundPath.make", &json!({})).unwrap()["id"].as_u64().unwrap());
s.execute("select.set", &json!({"ids": [cp.0]})).unwrap();
s.execute("path.average", &json!({"axis": "both"})).unwrap();
let st = s.doc().unwrap();
let node = st.doc.node(cp).unwrap();
assert!(matches!(node.kind, NodeKind::Compound { .. }), "{}", node.kind_label());
let pts: Vec<Point> =
node.children().unwrap().iter().flat_map(|c| c.path_data().unwrap().anchors().map(|(_, _, a)| a.p).collect::<Vec<_>>()).collect();
assert_eq!(pts.len(), 8, "{pts:?}");
assert!(all_at(&pts, Point::new(150.0, 150.0)), "{pts:?}");
}
#[test]
fn average_with_nothing_selected_is_an_error() {
let mut s = session();
rect(&mut s, 0.0, 0.0, 100.0, 100.0);
s.execute("select.set", &json!({"ids": []})).unwrap();
assert!(s.execute("path.average", &json!({"axis": "both"})).is_err());
}
#[test]
fn average_leaves_sibling_geometry_unchanged_on_partial_selection() {
let mut s = session();
let a = rect(&mut s, 0.0, 0.0, 100.0, 100.0);
let b = rect(&mut s, 200.0, 200.0, 100.0, 100.0);
s.execute("select.set", &json!({"ids": [a.0, b.0]})).unwrap();
let g = s.execute("object.group", &json!({})).unwrap()["id"].as_u64().unwrap();
let orig_b = path(&s, b).clone();
// Select group, but add-select one anchor on descendant a
s.execute("select.set", &json!({"ids": [g]})).unwrap();
s.execute("select.anchors", &json!({"id": a.0, "anchors": [[0, 0]], "mode": "add"})).unwrap();
// Average should not move b's anchors
s.execute("path.average", &json!({"axis": "both"})).unwrap();
assert_eq!(path(&s, b), orig_b);
}
#[test]
fn average_skips_locked_descendants_in_group() {
let mut s = session();
let a = rect(&mut s, 0.0, 0.0, 100.0, 100.0);
let b = rect(&mut s, 200.0, 200.0, 100.0, 100.0);
s.execute("select.set", &json!({"ids": [a.0, b.0]})).unwrap();
let g = s.execute("object.group", &json!({})).unwrap()["id"].as_u64().unwrap();
let orig_b = path(&s, b).clone();
// Lock descendant b
s.execute("object.lock", &json!({"ids": [b.0]})).unwrap();
// Select group and run average
s.execute("select.set", &json!({"ids": [g]})).unwrap();
s.execute("path.average", &json!({"axis": "both"})).unwrap();
// b was locked so it remains unchanged; a is averaged on its own corners.
assert_eq!(path(&s, b), orig_b);
assert!(all_at(&anchor_points(&s, a), Point::new(50.0, 50.0)), "{:?}", anchor_points(&s, a));
}