mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-29 13:06:56 -06:00
Merge PR #601 from NikitkaRa: SmartArt: write the outline to .pptx as a diagram and read it back from the data model
# Conflicts: # crates/engine/Cargo.toml # crates/pptx/src/read/shapes.rs # crates/pptx/src/write/shapes.rs
This commit is contained in:
@@ -194,6 +194,9 @@ fn node_map(d: &Diagram) -> Vec<Vec<usize>> {
|
||||
|
||||
/// Read text typed into the diagram's shapes back into its nodes.
|
||||
fn sync_text(d: &mut Diagram, children: &[Shape]) {
|
||||
if d.imported {
|
||||
return;
|
||||
}
|
||||
for (shape, idx) in children.iter().zip(node_map(d)) {
|
||||
let Some(body) = &shape.text else { continue };
|
||||
if body.paragraphs.len() != idx.len() {
|
||||
@@ -436,6 +439,7 @@ fn edit_diagram<T>(s: &mut Session, p: &Value, cmd: &str, f: impl FnOnce(&mut Di
|
||||
sync_text(&mut d, &children);
|
||||
let out = f(&mut d)?;
|
||||
normalize(&mut d.nodes);
|
||||
d.imported = false;
|
||||
let kids = regenerate(doc, &d, child);
|
||||
let shapes = crate::shapes_mut(doc, sel).ok_or_else(|| bad(cmd, "no slide"))?;
|
||||
let sh = deckcraft_model::find_shape_mut(shapes, gid).ok_or_else(|| bad(cmd, "no such shape"))?;
|
||||
@@ -458,7 +462,7 @@ pub fn insert(s: &mut Session, kind: &str, mut nodes: Vec<DiagramNode>) -> Resul
|
||||
nodes = (0..3).map(|_| DiagramNode { text: "Text".into(), level: 0 }).collect();
|
||||
}
|
||||
normalize(&mut nodes);
|
||||
let d = Diagram { kind: kind.into(), nodes };
|
||||
let d = Diagram { kind: kind.into(), nodes, ..Default::default() };
|
||||
let size = s.doc()?.doc.slide_size;
|
||||
let area = Xfrm::new(size.width * 0.1, size.height * 0.28, size.width * 0.8, size.height * 0.6);
|
||||
let id = s.edit(|doc, sel| {
|
||||
@@ -542,6 +546,11 @@ fn change_layout(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const CMD: &str = "smartArt.layout";
|
||||
let kind = str_param(p, "kind").filter(|k| KINDS.contains(k)).ok_or_else(|| bad(CMD, "unknown layout"))?.to_string();
|
||||
edit_diagram(s, p, CMD, |d| {
|
||||
if d.kind != kind {
|
||||
// PowerPoint's own layout no longer applies.
|
||||
d.layout_id.clear();
|
||||
d.layout_xml = None;
|
||||
}
|
||||
d.kind = kind;
|
||||
Ok(Value::Null)
|
||||
})
|
||||
@@ -586,18 +595,18 @@ mod tests {
|
||||
#[test]
|
||||
fn layouts_map_nodes_to_shapes() {
|
||||
let area = Xfrm::new(0.0, 0.0, 800.0, 400.0);
|
||||
let d = Diagram { kind: "list".into(), nodes: mk(&[0, 1, 1, 0]) };
|
||||
let d = Diagram { kind: "list".into(), nodes: mk(&[0, 1, 1, 0]), ..Default::default() };
|
||||
let v = layout(&d, area);
|
||||
assert_eq!(v.len(), 2);
|
||||
let t = v[0].text.as_ref().map(|b| b.paragraphs.len());
|
||||
assert_eq!(t, Some(3), "sub-points are bullets in their parent's shape");
|
||||
let h = Diagram { kind: "hierarchy".into(), nodes: mk(&[0, 1, 2, 1]) };
|
||||
let h = Diagram { kind: "hierarchy".into(), nodes: mk(&[0, 1, 2, 1]), ..Default::default() };
|
||||
let v = layout(&h, area);
|
||||
assert_eq!(v.len(), 4 + 3, "a box per node and a line per parent link");
|
||||
for sh in &v {
|
||||
let x = sh.xfrm.unwrap_or_default();
|
||||
assert!(x.x >= -0.01 && x.x + x.w <= 800.01 && x.y >= -0.01 && x.y + x.h <= 400.01, "{x:?}");
|
||||
}
|
||||
assert!(layout(&Diagram { kind: "cycle".into(), nodes: vec![] }, area).is_empty());
|
||||
assert!(layout(&Diagram { kind: "cycle".into(), ..Default::default() }, area).is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1728,3 +1728,41 @@ fn smartart_text_pane_edits_regenerate_the_diagram() {
|
||||
let back = deckcraft_format::load(&bytes).unwrap();
|
||||
assert!(back.slides[0].shapes.iter().any(|x| x.diagram.as_ref().is_some_and(|d| d.nodes.len() == 3)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn smartart_round_trips_through_pptx_as_a_diagram() {
|
||||
let mut s = session();
|
||||
s.execute("slide.new", &json!({"layout": "blank"})).unwrap();
|
||||
s.execute("insert.smartArt", &json!({"kind": "hierarchy", "items": ["CEO", "\tCTO", "\t\tDev", "\tCFO"]})).unwrap();
|
||||
let doc = s.doc().unwrap().doc.as_ref().clone();
|
||||
let bytes = deckcraft_pptx::export(&doc).unwrap();
|
||||
let names: Vec<String> = {
|
||||
let z = zip::ZipArchive::new(std::io::Cursor::new(&bytes)).unwrap();
|
||||
z.file_names().map(String::from).collect()
|
||||
};
|
||||
for part in ["data1", "layout1", "quickStyle1", "colors1", "drawing1"] {
|
||||
assert!(names.iter().any(|n| n == &format!("ppt/diagrams/{part}.xml")), "{part} in {names:?}");
|
||||
}
|
||||
let back = deckcraft_pptx::import(&bytes).unwrap();
|
||||
assert!(back.validate().is_empty());
|
||||
let g = back.slides[1].shapes.iter().find(|x| x.diagram.is_some()).expect("a SmartArt group");
|
||||
let d = g.diagram.as_ref().unwrap();
|
||||
assert_eq!(d.kind, "hierarchy");
|
||||
assert!(d.imported);
|
||||
let outline: Vec<(&str, u8)> = d.nodes.iter().map(|n| (n.text.as_str(), n.level)).collect();
|
||||
assert_eq!(outline, [("CEO", 0), ("CTO", 1), ("Dev", 2), ("CFO", 1)]);
|
||||
assert!(g.children().len() >= 4, "the cached drawing is read back as shapes");
|
||||
|
||||
// The Text Pane works on the imported diagram, and a second round trip keeps the edit.
|
||||
let gid = g.id.0;
|
||||
let path = std::env::temp_dir().join(format!("deckcraft-smartart-{}.pptx", std::process::id()));
|
||||
std::fs::write(&path, &bytes).unwrap();
|
||||
let mut s2 = Session::new();
|
||||
s2.execute("file.open", &json!({"path": path.to_string_lossy()})).unwrap();
|
||||
let _ = std::fs::remove_file(&path);
|
||||
s2.execute("slide.go", &json!({"index": 1})).unwrap();
|
||||
s2.execute("smartArt.addNode", &json!({"id": gid, "index": 3, "position": "after", "text": "COO"})).unwrap();
|
||||
let again = deckcraft_pptx::import(&deckcraft_pptx::export(s2.doc().unwrap().doc.as_ref()).unwrap()).unwrap();
|
||||
let d = again.slides[1].shapes.iter().find_map(|x| x.diagram.clone()).unwrap();
|
||||
assert_eq!(d.nodes.last().map(|n| (n.text.as_str(), n.level)), Some(("COO", 1)));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user