mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 10:37:03 -06:00
Merge PR #1245: Title: arrange.group: nest groups at most 32 deep
This commit is contained in:
@@ -146,6 +146,7 @@ app's JSON control channel (`deckcraft --control PORT`).
|
||||
| 2026-10-23 | Normal view: a slide zoomed past the window gets a horizontal and a vertical scroll bar on the axes that overflow; drag the thumb or click the track to reach every edge (#1157) |
|
||||
| 2026-10-22 | Shape Fill and Shape Outline format the members of a selected group, including nested groups, in one undo step. Fill transparency retains each member's effective colour (#1079) |
|
||||
| 2026-10-21 | 3-D contours, extrusion depths and bevels keep their size in points inside a group whose child coordinates scale its shapes: a 1 pt contour on a small extruded cube in a legacy group drew hundreds of points wide, a band across the slide |
|
||||
| 2026-10-21 | Groups nest at most 32 deep (`arrange.group` refuses more), as deep as PPTX import reads and slides draw: a deck grouped 39 or more levels deep saved without complaint and then could not be opened again |
|
||||
| 2026-10-20 | Chart number formats with explicit empty positive, negative or zero sections hide the corresponding labels, including all values with three semicolons; a blank whole format still uses General. Synthetic formatter regressions |
|
||||
| 2026-10-22 | Chart bars and their legend keys draw the series' own pattern fill (`a:pattFill`, now read and written) and draw nothing for `a:noFill`, with the series outline where it has one, as PowerPoint for Mac does (synthetic probe); they took the palette's colour before. Lines, areas and markers use a pattern's foreground colour |
|
||||
| 2026-10-18 | Native chart sidecar bytes move from JSON arrays into referenced ZIP entries, with shared media byte/item limits and bounded reads; older inline sidecars still load. Synthetic format regressions cover large workbooks and malformed members |
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user