mirror of
https://github.com/storytold/deckcraft.git
synced 2026-11-13 13:43:50 +00:00
engine: arrange.ungroup frees a group nested in another group
arrange.ungroup looked only at the slide's top-level list, so a group
inside another group answered `{"ids": []}`, stayed grouped, and the
selection was emptied.
Such a group's children now take its place among the outer group's
children, with their boxes mapped into the outer group's space by
child_to_parent, as for a top-level group, so they are drawn where they
were. They are selected and returned. A shape freed from a named group
and then from a named group around it is listed once.
Test: ungrouping_a_nested_group_frees_its_shapes_into_the_outer_group.
This commit is contained in:
1 parent
fc35ad67c6
commit
1c8b14edad
2 files changed
+102
No files matched your search
@@ -365,6 +365,37 @@ fn ungroup(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
out.push(sh);
|
||||
}
|
||||
*list = out;
|
||||
// A group inside a group: its children take its place in the outer group, mapped into the
|
||||
// outer group's child space, where the inner group's own box is.
|
||||
for id in &ids {
|
||||
let Some(siblings) = deckcraft_model::sibling_list_mut(list, *id) else { continue };
|
||||
let Some(at) = siblings.iter().position(|x| x.id == *id) else { continue };
|
||||
let freed: Vec<Shape> = match siblings.get(at) {
|
||||
Some(Shape { kind: ShapeKind::Group { children, child }, xfrm: Some(gx), .. }) => children
|
||||
.iter()
|
||||
.map(|c| {
|
||||
let mut c = c.clone();
|
||||
if let Some(cx) = c.xfrm {
|
||||
c.xfrm = Some(child_to_parent(gx, child, &cx));
|
||||
}
|
||||
c
|
||||
})
|
||||
.collect(),
|
||||
_ => continue,
|
||||
};
|
||||
let freed_ids: Vec<ShapeId> = freed.iter().map(|c| c.id).collect();
|
||||
siblings.splice(at..=at, freed);
|
||||
// Ungrouped along with its outer group, it was listed among that group's children.
|
||||
match new_sel.iter().position(|x| x == id) {
|
||||
Some(i) => {
|
||||
new_sel.splice(i..=i, freed_ids);
|
||||
}
|
||||
None => new_sel.extend(freed_ids),
|
||||
}
|
||||
}
|
||||
// A shape freed from a named group and then from a named group around it is listed once.
|
||||
let mut listed = std::collections::HashSet::new();
|
||||
new_sel.retain(|x| listed.insert(*x));
|
||||
sel.shapes = new_sel.clone();
|
||||
Ok(json!({"ids": new_sel}))
|
||||
})
|
||||
|
||||
@@ -885,6 +885,77 @@ fn group_ungroup_roundtrip_positions() {
|
||||
assert!((v["box"]["x"].as_f64().unwrap() - 310.0).abs() < 1e-6, "{v}");
|
||||
}
|
||||
|
||||
/// Ungrouping a group inside another group frees its shapes into the outer group, in its place
|
||||
/// in the outer group's order, drawn where they were, and selects them. Before, arrange.ungroup
|
||||
/// looked only at the slide's top level: it answered `{ids: []}` and changed nothing.
|
||||
#[test]
|
||||
fn ungrouping_a_nested_group_frees_its_shapes_into_the_outer_group() {
|
||||
fn rect(s: &mut Session, x: f64, y: f64) -> u64 {
|
||||
s.execute("shape.insert", &json!({"preset": "rect", "rect": [x, y, 100, 100]})).unwrap()["id"].as_u64().unwrap()
|
||||
}
|
||||
let sid = |id: u64| ShapeId(u32::try_from(id).unwrap());
|
||||
let sites = |s: &Session, id: u64| {
|
||||
let st = s.doc().unwrap();
|
||||
connect::sites_of(&st.doc, &st.selection, st.shapes(), sid(id)).unwrap()
|
||||
};
|
||||
// The outer group moved and stretched; then an inner group turned and flipped, inside an outer
|
||||
// group that is turned and moved.
|
||||
type Edits<'a> = &'a [(&'a str, Value)];
|
||||
let cases: [(Edits<'_>, Edits<'_>); 2] = [
|
||||
(&[], &[("shape.move", json!({"dx": 200})), ("shape.resize", json!({"w": 450}))]),
|
||||
(
|
||||
&[("shape.rotate", json!({"deg": 30})), ("shape.flip", json!({"axis": "horizontal"}))],
|
||||
&[("shape.rotate", json!({"deg": 45})), ("shape.move", json!({"dx": 120, "dy": 40}))],
|
||||
),
|
||||
];
|
||||
let edit = |s: &mut Session, id: u64, edits: Edits<'_>| {
|
||||
for (cmd, p) in edits {
|
||||
let mut p = p.clone();
|
||||
p["ids"] = json!([id]);
|
||||
s.execute(cmd, &p).unwrap();
|
||||
}
|
||||
};
|
||||
for (n, (inner_edits, outer_edits)) in cases.into_iter().enumerate() {
|
||||
let mut s = session();
|
||||
s.execute("slide.new", &json!({"layout": "blank"})).unwrap();
|
||||
let (a, b, c) = (rect(&mut s, 100.0, 100.0), rect(&mut s, 300.0, 100.0), rect(&mut s, 100.0, 300.0));
|
||||
let inner = s.execute("arrange.group", &json!({"ids": [a, b]})).unwrap()["id"].as_u64().unwrap();
|
||||
edit(&mut s, inner, inner_edits);
|
||||
let outer = s.execute("arrange.group", &json!({"ids": [inner, c]})).unwrap()["id"].as_u64().unwrap();
|
||||
edit(&mut s, outer, outer_edits);
|
||||
let before = [sites(&s, a), sites(&s, b), sites(&s, c)];
|
||||
let r = s.execute("arrange.ungroup", &json!({"ids": [inner]})).unwrap();
|
||||
assert_eq!(r["ids"], json!([a, b]), "case {n}");
|
||||
assert_eq!(s.doc().unwrap().selection.shapes, [sid(a), sid(b)]);
|
||||
let st = s.doc().unwrap();
|
||||
let order: Vec<ShapeId> = deckcraft_model::find_shape(st.shapes(), sid(outer)).unwrap().children().iter().map(|x| x.id).collect();
|
||||
assert_eq!(order, [sid(a), sid(b), sid(c)], "case {n}");
|
||||
for (id, was) in [a, b, c].into_iter().zip(before) {
|
||||
let now = sites(&s, id);
|
||||
assert!(was.iter().zip(&now).all(|(p, q)| (*p - *q).hypot() < 1e-6), "case {n}: shape {id} moved from {was:?} to {now:?}");
|
||||
}
|
||||
}
|
||||
// Two named groups, one inside the other, inside a third: the shapes of both go to the third,
|
||||
// each listed once.
|
||||
let mut s = session();
|
||||
s.execute("slide.new", &json!({"layout": "blank"})).unwrap();
|
||||
let (a, b, c, d) = (rect(&mut s, 100.0, 100.0), rect(&mut s, 300.0, 100.0), rect(&mut s, 100.0, 300.0), rect(&mut s, 300.0, 300.0));
|
||||
let inner = s.execute("arrange.group", &json!({"ids": [a, b]})).unwrap()["id"].as_u64().unwrap();
|
||||
let middle = s.execute("arrange.group", &json!({"ids": [inner, c]})).unwrap()["id"].as_u64().unwrap();
|
||||
let outer = s.execute("arrange.group", &json!({"ids": [middle, d]})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("shape.resize", &json!({"ids": [outer], "w": 600})).unwrap();
|
||||
let before = [a, b, c, d].map(|id| sites(&s, id));
|
||||
let r = s.execute("arrange.ungroup", &json!({"ids": [inner, middle]})).unwrap();
|
||||
assert_eq!(r["ids"], json!([a, b, c]));
|
||||
let st = s.doc().unwrap();
|
||||
let order: Vec<ShapeId> = deckcraft_model::find_shape(st.shapes(), sid(outer)).unwrap().children().iter().map(|x| x.id).collect();
|
||||
assert_eq!(order, [sid(a), sid(b), sid(c), sid(d)]);
|
||||
for (id, was) in [a, b, c, d].into_iter().zip(before) {
|
||||
let now = sites(&s, id);
|
||||
assert!(was.iter().zip(&now).all(|(p, q)| (*p - *q).hypot() < 1e-6), "shape {id} moved from {was:?} to {now:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// 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]
|
||||
|
||||
Reference in new issue
Block a user