Merge PR #1245: Title: arrange.group: nest groups at most 32 deep

This commit is contained in:
aiongg
2026-10-24 06:16:18 +00:00
3 changed files with 50 additions and 0 deletions
+19
View File
@@ -171,6 +171,22 @@ pub(crate) fn union_bounds(xs: &[Xfrm]) -> Option<Rect> {
Some(it.fold(first, |a, b| a.union(b)))
}
/// How deep groups may nest: as deep as the PPTX reader reads and the renderer draws. A saved
/// .deckcraft file stops opening at about 39 (the JSON reader's recursion limit), and walks over
/// the shape tree stop at 64.
pub const MAX_GROUP_DEPTH: usize = 32;
/// How many groups deep `shapes` nest (0 without groups).
fn group_depth(shapes: &[Shape]) -> usize {
let mut d = 0;
deckcraft_model::walk(shapes, &mut |sh, depth| {
if sh.is_group() {
d = d.max(depth + 1);
}
});
d
}
fn group(s: &mut Session, p: &Value) -> Result<Value> {
let ids = targets(s, p)?;
s.edit(|doc, sel| {
@@ -179,6 +195,9 @@ fn group(s: &mut Session, p: &Value) -> Result<Value> {
if members.len() < 2 {
return Err(bad("arrange.group", "select two or more objects on the slide"));
}
if group_depth(&members) + 1 > MAX_GROUP_DEPTH {
return Err(bad("arrange.group", format!("groups can be nested at most {MAX_GROUP_DEPTH} deep")));
}
for m in &mut members {
if m.xfrm.is_none() {
m.xfrm = Some(xfrm_of(doc, sel, m));
+30
View File
@@ -322,6 +322,36 @@ fn group_ungroup_roundtrip_positions() {
assert!((v["box"]["x"].as_f64().unwrap() - 310.0).abs() < 1e-6, "{v}");
}
/// Grouping a group again and again stops at `MAX_GROUP_DEPTH`: past about 39 levels a saved
/// .deckcraft file no longer opens, and past 64 commands such as format.languageAll miss the text.
#[test]
fn groups_nest_only_as_deep_as_a_saved_file_reopens() {
let mut s = session();
s.execute("slide.new", &json!({"layout": "blank"})).unwrap();
// A table (deeper in the file than a text box) at the bottom of the nesting.
let mut g = s.execute("insert.table", &json!({"rows": 2, "cols": 2})).unwrap()["id"].as_u64().unwrap();
for _ in 0..cmd::arrange::MAX_GROUP_DEPTH {
let r = s.execute("shape.insert", &json!({"preset": "rect", "rect": [10, 10, 20, 20], "text": "deep"})).unwrap()["id"].as_u64().unwrap();
s.execute("edit.select", &json!({"ids": [g, r]})).unwrap();
g = s.execute("arrange.group", &json!({})).unwrap()["id"].as_u64().unwrap();
}
let r = s.execute("shape.insert", &json!({"preset": "rect", "rect": [10, 10, 20, 20]})).unwrap()["id"].as_u64().unwrap();
s.execute("edit.select", &json!({"ids": [g, r]})).unwrap();
let before = s.doc().unwrap().revision;
let err = s.execute("arrange.group", &json!({})).unwrap_err().to_string();
assert!(err.contains(&format!("nested at most {}", cmd::arrange::MAX_GROUP_DEPTH)), "{err}");
assert_eq!(s.doc().unwrap().revision, before, "a refused group leaves the slide as it was");
// The deepest deck the command allows saves and opens again.
let bytes = deckcraft_format::save(&s.doc().unwrap().doc).unwrap();
let back = deckcraft_format::load(&bytes).unwrap();
let depth = |shapes: &[deckcraft_model::Shape]| {
let mut d = 0;
deckcraft_model::walk(shapes, &mut |sh, depth| d = d.max(depth + usize::from(sh.is_group())));
d
};
assert_eq!(depth(&back.slides.last().unwrap().shapes), cmd::arrange::MAX_GROUP_DEPTH);
}
#[test]
fn align_and_distribute() {
let mut s = session();