mirror of
https://github.com/storytold/deckcraft.git
synced 2026-11-13 13:43:50 +00:00
Merge pull request #1968 from scks1/fix/copy-group-member-slide-box
engine: a shape copied from a group pastes where it was drawn
This commit is contained in:
2 files changed
+104
No files matched your search
@@ -120,6 +120,12 @@ fn copy_inner(s: &mut Session, p: &Value) -> Result<String> {
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
// A shape in a group is copied with the box it is drawn in, so it pastes where it was seen.
|
||||
for sh in &mut shapes {
|
||||
if let Some(x) = sh.xfrm.and_then(|x| box_on_slide(&st.doc, &st.selection, st.shapes(), sh.id, &x)) {
|
||||
sh.xfrm = Some(x);
|
||||
}
|
||||
}
|
||||
// Copying is the last chance to read what a placeholder inherits: a cut removes it, and the
|
||||
// paste can be in another deck. Slide Master view pastes the originals instead.
|
||||
let placeholders: Vec<Shape> = shapes.iter().filter(|x| x.ph.is_some()).cloned().collect();
|
||||
@@ -147,6 +153,39 @@ pub(crate) fn detach_on_slide(doc: &deckcraft_model::Presentation, sel: &crate::
|
||||
}
|
||||
}
|
||||
|
||||
/// Where shape `id` is drawn on the slide when it is inside a group: `x`, its box in its group's
|
||||
/// space, mapped out through each group around it, as Ungroup places a freed shape. `None` for a
|
||||
/// shape on the top level or not found. A turned shape in a group stretched more one way than the
|
||||
/// other is drawn skewed, which a box cannot hold, so it gets the unskewed box Ungroup gives it.
|
||||
fn box_on_slide(
|
||||
doc: &deckcraft_model::Presentation,
|
||||
sel: &crate::Selection,
|
||||
shapes: &[Shape],
|
||||
id: ShapeId,
|
||||
x: &deckcraft_geom::Xfrm,
|
||||
) -> Option<deckcraft_geom::Xfrm> {
|
||||
fn rec(
|
||||
doc: &deckcraft_model::Presentation,
|
||||
sel: &crate::Selection,
|
||||
shapes: &[Shape],
|
||||
id: ShapeId,
|
||||
x: &deckcraft_geom::Xfrm,
|
||||
depth: usize,
|
||||
) -> Option<deckcraft_geom::Xfrm> {
|
||||
if depth > 64 {
|
||||
return None;
|
||||
}
|
||||
shapes.iter().find_map(|sh| {
|
||||
let deckcraft_model::ShapeKind::Group { children, child } = &sh.kind else {
|
||||
return None;
|
||||
};
|
||||
let inner = if children.iter().any(|c| c.id == id) { *x } else { rec(doc, sel, children, id, x, depth + 1)? };
|
||||
Some(super::arrange::child_to_parent(&xfrm_of(doc, sel, sh), child, &inner))
|
||||
})
|
||||
}
|
||||
rec(doc, sel, shapes, id, x, 0)
|
||||
}
|
||||
|
||||
fn copying_caret(s: &Session) -> bool {
|
||||
s.active().is_some_and(|d| d.selection.text.as_ref().is_some_and(|t| !t.is_range()))
|
||||
}
|
||||
|
||||
@@ -241,6 +241,71 @@ fn clipboard_copy_paste_shapes_and_slides() {
|
||||
assert!(s.doc().unwrap().doc.validate().is_empty());
|
||||
}
|
||||
|
||||
/// A shape copied or cut from inside a group pastes where it was drawn: on another slide in the
|
||||
/// same place, on its own slide offset as any paste there is. Its box is mapped out through each
|
||||
/// group around it, however the groups were moved, stretched, turned or flipped. Before, the
|
||||
/// clipboard kept the box inside the group, so the shape pasted where it had been before the group
|
||||
/// was moved or stretched.
|
||||
#[test]
|
||||
fn a_shape_copied_from_a_group_pastes_where_it_was_drawn() {
|
||||
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: ShapeId| {
|
||||
let st = s.doc().unwrap();
|
||||
connect::sites_of(&st.doc, &st.selection, st.shapes(), id).unwrap()
|
||||
};
|
||||
type Edits<'a> = &'a [(&'a str, Value)];
|
||||
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();
|
||||
}
|
||||
};
|
||||
// Edits to the shape's group, then to a group around that one (none: no outer group).
|
||||
let cases: [(Edits<'_>, Edits<'_>); 4] = [
|
||||
(&[("shape.move", json!({"dx": 200}))], &[]),
|
||||
(&[("shape.resize", json!({"w": 600}))], &[]),
|
||||
(&[("shape.rotate", json!({"deg": 30})), ("shape.flip", json!({"axis": "vertical"})), ("shape.move", json!({"dx": 50, "dy": 20}))], &[]),
|
||||
(&[("shape.move", json!({"dx": 200}))], &[("shape.resize", json!({"w": 450})), ("shape.move", json!({"dy": 60}))]),
|
||||
];
|
||||
let off = session().prefs.duplicate_offset;
|
||||
for (n, (inner_edits, outer_edits)) in cases.into_iter().enumerate() {
|
||||
for (cmd, same_slide) in [("edit.copy", false), ("edit.cut", false), ("edit.copy", true)] {
|
||||
let mut s = session();
|
||||
s.execute("slide.new", &json!({"layout": "blank"})).unwrap();
|
||||
let (a, b) = (rect(&mut s, 100.0, 100.0), rect(&mut s, 300.0, 100.0));
|
||||
let g = s.execute("arrange.group", &json!({"ids": [a, b]})).unwrap()["id"].as_u64().unwrap();
|
||||
edit(&mut s, g, inner_edits);
|
||||
if !outer_edits.is_empty() {
|
||||
let c = rect(&mut s, 100.0, 300.0);
|
||||
let outer = s.execute("arrange.group", &json!({"ids": [g, c]})).unwrap()["id"].as_u64().unwrap();
|
||||
edit(&mut s, outer, outer_edits);
|
||||
}
|
||||
let was = sites(&s, sid(a));
|
||||
s.doc_mut().unwrap().selection.shapes = vec![sid(a)];
|
||||
s.execute(cmd, &json!({})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().shape(sid(a)).is_some(), cmd == "edit.copy", "case {n}: {cmd}");
|
||||
if !same_slide {
|
||||
s.execute("slide.new", &json!({"layout": "blank"})).unwrap();
|
||||
}
|
||||
let r = s.execute("edit.paste", &json!({})).unwrap();
|
||||
let pasted = sid(r["ids"][0].as_u64().unwrap());
|
||||
assert!(s.doc().unwrap().shapes().iter().any(|x| x.id == pasted), "case {n}: {cmd} pastes on the top level");
|
||||
let d = if same_slide { off } else { 0.0 };
|
||||
let now = sites(&s, pasted);
|
||||
assert_eq!(was.len(), now.len(), "case {n}: {cmd}");
|
||||
assert!(
|
||||
was.iter().zip(&now).all(|(p, q)| (q.x - p.x - d).hypot(q.y - p.y - d) < 1e-6),
|
||||
"case {n}: {cmd} on the same slide {same_slide}: drawn at {was:?}, pasted at {now:?}"
|
||||
);
|
||||
assert_eq!(s.doc().unwrap().doc.validate(), Vec::<String>::new());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The resolved look of every paragraph and run of shape `id` on the current slide: size, font,
|
||||
/// colour, bold, bullet, indent, alignment, and the box's insets and anchor.
|
||||
fn resolved_look(s: &Session, id: ShapeId) -> String {
|
||||
|
||||
Reference in new issue
Block a user