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
+15 -2
View File
@@ -1001,8 +1001,21 @@ pub(super) fn anchor_targets(s: &Session) -> Result<AnchorTargets> {
}
fn average(s: &mut Session, p: &Value) -> Result<Value> {
let t = anchor_targets(s)?;
let d0 = &s.doc()?.doc;
let st = s.doc()?;
let t: AnchorTargets = if st.selection.anchors.is_empty() {
let editable = st.doc.editable();
paths_in(&st.doc, &st.selection.objects)
.into_iter()
.filter(|id| editable(*id))
.filter_map(|id| {
let pd = st.doc.node(id)?.path_data()?;
Some((id, pd.anchors().map(|(s, a, _)| (s, a)).collect()))
})
.collect()
} else {
anchor_targets(s)?
};
let d0 = &st.doc;
let pts: Vec<Point> = t
.iter()
.flat_map(|(id, v)| v.iter().filter_map(move |(si, ai)| d0.node(*id)?.path_data()?.subpaths.get(*si)?.anchors.get(*ai).map(|a| a.p)))
+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));
}