mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 10:37:03 -06:00
SmartArt: node pictures from the data model, Brick Scene camera, four more layouts
Node pictures are written on the data model's presentation points (blip through the data part's rels) and read back into the outline on import. Picture Accent Process is hProcess10, not pList2 (checked against PowerPoint's layout list); pList2 is now Horizontal Picture List. Brick Scene looks through an isometric camera. New layouts: Horizontal Picture List, Basic Timeline, Basic Venn, Basic Target. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -23,7 +23,7 @@ pub const MAX_LEVEL: u8 = 8;
|
||||
const MAX_SHAPES: usize = 40;
|
||||
|
||||
/// DeckCraft's layouts, with the names PowerPoint's gallery gives them.
|
||||
pub const LAYOUTS: [(&str, &str); 18] = [
|
||||
pub const LAYOUTS: [(&str, &str); 22] = [
|
||||
("list", "Vertical Bullet List"),
|
||||
("blockList", "Basic Block List"),
|
||||
("hBulletList", "Horizontal Bullet List"),
|
||||
@@ -42,6 +42,10 @@ pub const LAYOUTS: [(&str, &str); 18] = [
|
||||
("pictureList", "Vertical Picture List"),
|
||||
("pictureCaption", "Picture Caption List"),
|
||||
("pictureProcess", "Picture Accent Process"),
|
||||
("pictureHList", "Horizontal Picture List"),
|
||||
("timeline", "Basic Timeline"),
|
||||
("venn", "Basic Venn"),
|
||||
("target", "Basic Target"),
|
||||
];
|
||||
/// SmartArt Styles.
|
||||
pub const STYLES: [(&str, &str); 9] = [
|
||||
@@ -125,7 +129,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Change Layout",
|
||||
["SmartArt Design", "Layouts"],
|
||||
None,
|
||||
"{id?, kind: list|blockList|hBulletList|process|chevron|verticalProcess|stepUp|cycle|blockCycle|radial|hierarchy|pyramid|invertedPyramid|matrix|titledMatrix|pictureList|pictureCaption|pictureProcess}",
|
||||
"{id?, kind: list|blockList|hBulletList|process|chevron|verticalProcess|stepUp|cycle|blockCycle|radial|hierarchy|pyramid|invertedPyramid|matrix|titledMatrix|pictureList|pictureCaption|pictureProcess|pictureHList|timeline|venn|target}",
|
||||
has_selection,
|
||||
change_layout
|
||||
),
|
||||
@@ -323,14 +327,17 @@ fn shade(d: &Diagram, i: usize) -> Option<ColorTransform> {
|
||||
|
||||
/// The 3-D XML (`scene3d` + `sp3d`) for a 3-D SmartArt Style: a top bevel (`w`, `h` in points)
|
||||
/// lit by the three-point rig.
|
||||
fn three_d(bevel: &str, w: f64, h: f64) -> String {
|
||||
fn three_d(bevel: &str, w: f64, h: f64, camera: &str) -> String {
|
||||
format!(
|
||||
"<a:scene3d><a:camera prst=\"orthographicFront\"/><a:lightRig rig=\"threePt\" dir=\"t\"/></a:scene3d><a:sp3d prstMaterial=\"matte\"><a:bevelT w=\"{}\" h=\"{}\" prst=\"{bevel}\"/></a:sp3d>",
|
||||
"<a:scene3d><a:camera prst=\"{camera}\"/><a:lightRig rig=\"threePt\" dir=\"t\"/></a:scene3d><a:sp3d prstMaterial=\"matte\"><a:bevelT w=\"{}\" h=\"{}\" prst=\"{bevel}\"/></a:sp3d>",
|
||||
(w * 12700.0).round() as i64,
|
||||
(h * 12700.0).round() as i64
|
||||
)
|
||||
}
|
||||
|
||||
/// Brick Scene's camera: the diagram's shapes seen from above and to the side.
|
||||
pub const BRICK_CAMERA: &str = "isometricOffAxis2Top";
|
||||
|
||||
/// Whether `style` is one of the 3-D SmartArt Styles.
|
||||
pub fn is_3d(style: &str) -> bool {
|
||||
matches!(style, "polished" | "inset" | "cartoon" | "powder" | "brick")
|
||||
@@ -374,7 +381,7 @@ fn paint(d: &Diagram, sh: &mut Shape, i: usize, light: bool) {
|
||||
sh.effects = Some(Effects {
|
||||
outer_shadow: (d.style != "inset").then(|| shadow(6.0, 4.0)),
|
||||
bevel: Some(bevel.into()),
|
||||
raw3d: Some(three_d(bevel, bw, bw * 0.6)),
|
||||
raw3d: Some(three_d(bevel, bw, bw * 0.6, if d.style == "brick" { BRICK_CAMERA } else { "orthographicFront" })),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
@@ -778,6 +785,105 @@ fn layout_raw(d: &Diagram, area: Xfrm) -> Vec<Item> {
|
||||
}
|
||||
}
|
||||
}
|
||||
"pictureHList" => {
|
||||
// Horizontal Picture List: a column per node, a big picture over its text.
|
||||
let w = area.w / nf;
|
||||
for (i, g) in gs.iter().enumerate() {
|
||||
let x = area.x + i as f64 * w + w * 0.04;
|
||||
let bw = w * 0.92;
|
||||
let ph = area.h * 0.55;
|
||||
out.push((picture_shape(d, "rect", Xfrm::new(x, area.y, bw, ph), i, g[0]), vec![]));
|
||||
let mut sh = text_shape("rect", Xfrm::new(x, area.y + ph, bw, area.h - ph), &nodes(g), true);
|
||||
if let Some(b) = &mut sh.text {
|
||||
b.body.anchor = Some(deckcraft_model::text::Anchor::Top);
|
||||
}
|
||||
paint(d, &mut sh, i, false);
|
||||
out.push((sh, g.clone()));
|
||||
}
|
||||
}
|
||||
"timeline" => {
|
||||
// Basic Timeline: an arrow across the middle, a dot per node on it, the text above and
|
||||
// below in turn.
|
||||
let mid = area.y + area.h / 2.0;
|
||||
let bar = (area.h * 0.08).max(6.0);
|
||||
let mut sh = deco("rightArrow", Xfrm::new(area.x, mid - bar, area.w, bar * 2.0), 0.0);
|
||||
paint(d, &mut sh, 0, true);
|
||||
out.push((sh, vec![]));
|
||||
let w = area.w * 0.92 / nf;
|
||||
for (i, g) in gs.iter().enumerate() {
|
||||
let cx = area.x + w * (i as f64 + 0.5);
|
||||
let dot = bar * 1.4;
|
||||
let mut dsh = deco("ellipse", Xfrm::new(cx - dot / 2.0, mid - dot / 2.0, dot, dot), 0.0);
|
||||
paint(d, &mut dsh, i, false);
|
||||
out.push((dsh, vec![]));
|
||||
let th = area.h / 2.0 - bar * 1.6;
|
||||
let y = if i % 2 == 0 { area.y } else { mid + bar * 1.6 };
|
||||
let mut t = text_shape("rect", Xfrm::new(cx - w * 0.48, y, w * 0.96, th.max(1.0)), &nodes(g), true);
|
||||
if let Some(b) = &mut t.text {
|
||||
b.body.anchor = Some(if i % 2 == 0 { deckcraft_model::text::Anchor::Bottom } else { deckcraft_model::text::Anchor::Top });
|
||||
}
|
||||
paint(d, &mut t, i, true);
|
||||
t.fill = Some(Fill::None);
|
||||
t.line = Some(Line { fill: Some(Fill::None), ..Default::default() });
|
||||
out.push((t, g.clone()));
|
||||
}
|
||||
}
|
||||
"venn" => {
|
||||
// Basic Venn: see-through circles overlapping in a ring (two or three in a row/triangle).
|
||||
let s = area.h.min(area.w);
|
||||
let (cx, cy) = (area.x + area.w / 2.0, area.y + area.h / 2.0);
|
||||
let dc = if n == 1 { s } else { s * 0.62 };
|
||||
let r = if n == 1 { 0.0 } else { (s - dc) / 2.0 };
|
||||
for (i, g) in gs.iter().enumerate() {
|
||||
let a = (i as f64 / nf * 360.0 + if n == 2 { 180.0 } else { -90.0 }).to_radians();
|
||||
let mut sh = text_shape("ellipse", Xfrm::new(cx + r * a.cos() - dc / 2.0, cy + r * a.sin() - dc / 2.0, dc, dc), &nodes(g), true);
|
||||
paint(d, &mut sh, i, false);
|
||||
if let Some(Fill::Solid { color: c }) = &mut sh.fill {
|
||||
*c = c.clone().with(ColorTransform::Alpha(50_000));
|
||||
}
|
||||
if let Some(b) = &mut sh.text {
|
||||
// Text towards the circle's outer side, clear of the overlap.
|
||||
let p = dc * 0.18;
|
||||
let (dx, dy) = (a.cos() * p, a.sin() * p);
|
||||
b.body.inset_l = Some((7.2 + dx).max(2.0));
|
||||
b.body.inset_r = Some((7.2 - dx).max(2.0));
|
||||
b.body.inset_t = Some((3.6 + dy).max(2.0));
|
||||
b.body.inset_b = Some((3.6 - dy).max(2.0));
|
||||
}
|
||||
out.push((sh, g.clone()));
|
||||
}
|
||||
}
|
||||
"target" => {
|
||||
// Basic Target: nested rings, the outermost first, each ring's text in a box beside it
|
||||
// with a line from the ring.
|
||||
let s = area.h.min(area.w * 0.5);
|
||||
let (cx, cy) = (area.x + s / 2.0, area.y + area.h / 2.0);
|
||||
let band = s / 2.0 / nf;
|
||||
for (i, _) in gs.iter().enumerate() {
|
||||
let dc = s - 2.0 * band * i as f64;
|
||||
let mut sh = deco("ellipse", Xfrm::new(cx - dc / 2.0, cy - dc / 2.0, dc, dc), 0.0);
|
||||
paint(d, &mut sh, i, false);
|
||||
out.push((sh, vec![]));
|
||||
}
|
||||
let tx = area.x + s + area.w * 0.06;
|
||||
let rh = area.h / nf;
|
||||
for (i, g) in gs.iter().enumerate() {
|
||||
let ty = area.y + rh * (i as f64 + 0.5);
|
||||
// The point on ring i's band level with the text, else its top.
|
||||
let ry = cy - s / 2.0 + band * (i as f64 + 0.5);
|
||||
out.push((line_shape((cx, ry.min(ty)), (tx, ty)), vec![]));
|
||||
let mut t = text_shape("rect", Xfrm::new(tx, ty - rh * 0.45, (area.x + area.w - tx).max(1.0), rh * 0.9), &nodes(g), true);
|
||||
paint(d, &mut t, i, true);
|
||||
t.fill = Some(Fill::None);
|
||||
t.line = Some(Line { fill: Some(Fill::None), ..Default::default() });
|
||||
if let Some(b) = &mut t.text {
|
||||
for p in &mut b.paragraphs {
|
||||
p.props.align = Some(Align::Left);
|
||||
}
|
||||
}
|
||||
out.push((t, g.clone()));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
out
|
||||
|
||||
@@ -952,7 +952,12 @@ fn smartart_pictures_3d_styles_and_colour_variants() {
|
||||
let bytes = deckcraft_pptx::export(s.doc().unwrap().doc.as_ref()).unwrap();
|
||||
let back = deckcraft_pptx::import(&bytes).unwrap();
|
||||
assert!(back.validate().is_empty());
|
||||
assert_eq!(back.slides[1].shapes.iter().find_map(|x| x.diagram.as_ref()).map(|d| d.kind.clone()).as_deref(), Some("pictureCaption"));
|
||||
let bd = back.slides[1].shapes.iter().find_map(|x| x.diagram.as_ref()).unwrap();
|
||||
assert_eq!(bd.kind, "pictureCaption");
|
||||
// The node's picture comes back from the data model's blip, as its media.
|
||||
let m = bd.nodes[0].picture.expect("node picture read back");
|
||||
assert!(back.media(m).is_some_and(|x| !x.data.is_empty()));
|
||||
assert!(bd.nodes.iter().skip(1).all(|n| n.picture.is_none()));
|
||||
s.execute("smartArt.picture", &json!({"index": 0, "reset": true})).unwrap();
|
||||
assert!(pics(&s).iter().all(|p| !matches!(p.fill, Some(deckcraft_model::Fill::Picture(_)))));
|
||||
|
||||
|
||||
+84
-12
@@ -111,7 +111,11 @@ pub fn layout_id(kind: &str) -> &'static str {
|
||||
"titledMatrix" => "urn:microsoft.com/office/officeart/2005/8/layout/matrix1",
|
||||
"pictureList" => "urn:microsoft.com/office/officeart/2005/8/layout/vList4",
|
||||
"pictureCaption" => "urn:microsoft.com/office/officeart/2005/8/layout/pList1",
|
||||
"pictureProcess" => "urn:microsoft.com/office/officeart/2005/8/layout/pList2",
|
||||
"pictureProcess" => "urn:microsoft.com/office/officeart/2005/8/layout/hProcess10",
|
||||
"pictureHList" => "urn:microsoft.com/office/officeart/2005/8/layout/pList2",
|
||||
"timeline" => "urn:microsoft.com/office/officeart/2005/8/layout/hProcess11",
|
||||
"venn" => "urn:microsoft.com/office/officeart/2005/8/layout/venn1",
|
||||
"target" => "urn:microsoft.com/office/officeart/2005/8/layout/target1",
|
||||
_ => "urn:microsoft.com/office/officeart/2005/8/layout/vList2",
|
||||
}
|
||||
}
|
||||
@@ -138,6 +142,10 @@ pub fn kind_for(layout_id: &str, category: &str) -> &'static str {
|
||||
"pictureList",
|
||||
"pictureCaption",
|
||||
"pictureProcess",
|
||||
"pictureHList",
|
||||
"timeline",
|
||||
"venn",
|
||||
"target",
|
||||
"list",
|
||||
] {
|
||||
if layout_id == layout_id_of(k) {
|
||||
@@ -145,7 +153,13 @@ pub fn kind_for(layout_id: &str, category: &str) -> &'static str {
|
||||
}
|
||||
}
|
||||
let s = format!("{last} {}", category.to_ascii_lowercase());
|
||||
if s.contains("hierarchy") || s.contains("orgchart") {
|
||||
if s.contains("venn") {
|
||||
"venn"
|
||||
} else if s.contains("target") {
|
||||
"target"
|
||||
} else if s.contains("timeline") {
|
||||
"timeline"
|
||||
} else if s.contains("hierarchy") || s.contains("orgchart") {
|
||||
"hierarchy"
|
||||
} else if s.contains("radial") {
|
||||
"radial"
|
||||
@@ -174,7 +188,8 @@ fn layout_id_of(kind: &str) -> &'static str {
|
||||
|
||||
fn category(kind: &str) -> &'static str {
|
||||
match kind {
|
||||
"process" | "chevron" | "verticalProcess" | "stepUp" | "pictureProcess" => "process",
|
||||
"process" | "chevron" | "verticalProcess" | "stepUp" | "pictureProcess" | "timeline" => "process",
|
||||
"venn" | "target" => "relationship",
|
||||
"cycle" | "blockCycle" | "radial" => "cycle",
|
||||
"hierarchy" => "hierarchy",
|
||||
"pyramid" | "invertedPyramid" => "pyramid",
|
||||
@@ -206,8 +221,8 @@ fn text(w: &mut W, t: &str) {
|
||||
}
|
||||
|
||||
/// `ppt/diagrams/dataN.xml` for the outline. `drawing_rel` is the slide's relationship id of the
|
||||
/// cached drawing.
|
||||
pub fn data_xml(d: &Diagram, seed: u32, drawing_rel: &str) -> Vec<u8> {
|
||||
/// cached drawing; `pics` the data part's relationship ids of node pictures, by node index.
|
||||
pub fn data_xml(d: &Diagram, seed: u32, drawing_rel: &str, pics: &[(usize, String)]) -> Vec<u8> {
|
||||
let lo = if d.layout_id.is_empty() { layout_id(&d.kind).to_string() } else { d.layout_id.clone() };
|
||||
let mut w = W::new();
|
||||
w.open("dgm:dataModel", A::new().a("xmlns:dgm", NS_DGM).a("xmlns:a", crate::opc::NS_A).a("xmlns:r", crate::opc::NS_R));
|
||||
@@ -245,6 +260,24 @@ pub fn data_xml(d: &Diagram, seed: u32, drawing_rel: &str) -> Vec<u8> {
|
||||
w.close("dgm:pt");
|
||||
}
|
||||
}
|
||||
// A node's picture sits on the presentation point of its picture placeholder, as PowerPoint
|
||||
// stores it (`presAssocID` names the node, the blip is a relationship of the data part).
|
||||
for (k, rid) in pics {
|
||||
if *k >= d.nodes.len() {
|
||||
continue;
|
||||
}
|
||||
w.open("dgm:pt", A::new().a("modelId", guid(seed, 0x20_0000 + k)).a("type", "pres"));
|
||||
w.empty("dgm:prSet", A::new().a("presAssocID", node(*k)).a("presName", "Image").a("presStyleLbl", "fgImgPlace1"));
|
||||
w.open0("dgm:spPr");
|
||||
w.open0("a:blipFill");
|
||||
w.empty("a:blip", A::new().a("r:embed", rid.as_str()));
|
||||
w.open0("a:stretch");
|
||||
w.empty0("a:fillRect");
|
||||
w.close("a:stretch");
|
||||
w.close("a:blipFill");
|
||||
w.close("dgm:spPr");
|
||||
w.close("dgm:pt");
|
||||
}
|
||||
w.close("dgm:ptLst");
|
||||
w.open0("dgm:cxnLst");
|
||||
let mut stack: Vec<(u8, String)> = vec![];
|
||||
@@ -286,6 +319,31 @@ pub fn data_xml(d: &Diagram, seed: u32, drawing_rel: &str) -> Vec<u8> {
|
||||
|
||||
/// The outline of a data part: the `parOf` tree under the document point, depth first.
|
||||
pub fn outline(data: &El) -> Vec<DiagramNode> {
|
||||
outline_ids(data).into_iter().map(|(_, n)| n).collect()
|
||||
}
|
||||
|
||||
/// The relationship ids (of the data part) of node pictures, by node index in [`outline`]: the
|
||||
/// blip fill of a presentation point associated with the node.
|
||||
pub fn node_pictures(data: &El) -> Vec<(usize, String)> {
|
||||
let ids: HashMap<String, usize> = outline_ids(data).into_iter().enumerate().map(|(k, (id, _))| (id, k)).collect();
|
||||
let Some(pts) = data.child("ptLst") else { return vec![] };
|
||||
let mut out: Vec<(usize, String)> = vec![];
|
||||
for pt in pts.children_named("pt").take(MAX_NODES * 8) {
|
||||
if pt.attr("type") != Some("pres") {
|
||||
continue;
|
||||
}
|
||||
let Some(k) = pt.child("prSet").and_then(|p| p.attr("presAssocID")).and_then(|a| ids.get(a)) else { continue };
|
||||
let Some(rid) = pt.child("spPr").and_then(|s| s.child("blipFill")).and_then(|b| b.child("blip")).and_then(|b| b.attr("r:embed")) else {
|
||||
continue;
|
||||
};
|
||||
if !out.iter().any(|(j, _)| j == k) {
|
||||
out.push((*k, rid.to_string()));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn outline_ids(data: &El) -> Vec<(String, DiagramNode)> {
|
||||
let Some(pts) = data.child("ptLst") else { return vec![] };
|
||||
let mut texts: HashMap<&str, String> = HashMap::new();
|
||||
let mut doc: Option<&str> = None;
|
||||
@@ -324,7 +382,7 @@ pub fn outline(data: &El) -> Vec<DiagramNode> {
|
||||
if out.len() >= MAX_NODES || !seen.insert(id) {
|
||||
continue;
|
||||
}
|
||||
out.push(DiagramNode { text: texts.get(id).cloned().unwrap_or_default(), level: level.min(8), picture: None });
|
||||
out.push((id.to_string(), DiagramNode { text: texts.get(id).cloned().unwrap_or_default(), level: level.min(8), picture: None }));
|
||||
if let Some(v) = kids.get(id) {
|
||||
stack.extend(v.iter().rev().map(|(_, c)| (*c, level.saturating_add(1))));
|
||||
}
|
||||
@@ -348,10 +406,12 @@ fn paragraphs(t: &El) -> String {
|
||||
/// The layout algorithm and node shape of a DeckCraft layout.
|
||||
fn alg(kind: &str) -> (&'static str, Vec<(&'static str, &'static str)>, &'static str) {
|
||||
match kind {
|
||||
"process" | "stepUp" | "pictureProcess" | "pictureCaption" => ("lin", vec![("linDir", "fromL"), ("nodeVertAlign", "mid")], "roundRect"),
|
||||
"process" | "stepUp" | "pictureProcess" | "pictureCaption" | "pictureHList" | "timeline" => {
|
||||
("lin", vec![("linDir", "fromL"), ("nodeVertAlign", "mid")], "roundRect")
|
||||
}
|
||||
"chevron" => ("lin", vec![("linDir", "fromL"), ("nodeVertAlign", "mid")], "chevron"),
|
||||
"verticalProcess" => ("lin", vec![("linDir", "fromT"), ("nodeHorzAlign", "ctr")], "roundRect"),
|
||||
"cycle" | "radial" => ("cycle", vec![("stAng", "0"), ("spanAng", "360")], "ellipse"),
|
||||
"cycle" | "radial" | "venn" | "target" => ("cycle", vec![("stAng", "0"), ("spanAng", "360")], "ellipse"),
|
||||
"blockCycle" => ("cycle", vec![("stAng", "0"), ("spanAng", "360")], "roundRect"),
|
||||
"pyramid" | "invertedPyramid" => {
|
||||
("pyra", vec![("linDir", if kind == "pyramid" { "fromT" } else { "fromB" }), ("txDir", "fromT"), ("pyraAcctPos", "bef")], "trapezoid")
|
||||
@@ -500,6 +560,8 @@ fn bevel(style: &str) -> Option<(&'static str, i64)> {
|
||||
|
||||
/// `ppt/diagrams/quickStyleN.xml`: flat shapes drawn from the theme's first styles.
|
||||
pub fn style_xml(style: &str) -> Vec<u8> {
|
||||
// Brick Scene looks at the diagram from above and to the side.
|
||||
let camera = if style == "brick" { "isometricOffAxis2Top" } else { "orthographicFront" };
|
||||
let mut w = W::new();
|
||||
w.open("dgm:styleDef", A::new().a("xmlns:dgm", NS_DGM).a("xmlns:a", crate::opc::NS_A).a("uniqueId", qs_id(style)));
|
||||
w.empty("dgm:title", A::new().a("val", ""));
|
||||
@@ -508,12 +570,12 @@ pub fn style_xml(style: &str) -> Vec<u8> {
|
||||
w.empty("dgm:cat", A::new().a("type", if bevel(style).is_some() { "3D" } else { "simple" }).a("pri", "10100"));
|
||||
w.close("dgm:catLst");
|
||||
w.open0("dgm:scene3d");
|
||||
w.empty("a:camera", A::new().a("prst", "orthographicFront"));
|
||||
w.empty("a:camera", A::new().a("prst", camera));
|
||||
w.empty("a:lightRig", A::new().a("rig", "threePt").a("dir", "t"));
|
||||
w.close("dgm:scene3d");
|
||||
w.open("dgm:styleLbl", A::new().a("name", "node1"));
|
||||
w.open0("dgm:scene3d");
|
||||
w.empty("a:camera", A::new().a("prst", "orthographicFront"));
|
||||
w.empty("a:camera", A::new().a("prst", camera));
|
||||
w.empty("a:lightRig", A::new().a("rig", "threePt").a("dir", "t"));
|
||||
w.close("dgm:scene3d");
|
||||
match bevel(style) {
|
||||
@@ -593,7 +655,7 @@ mod tests {
|
||||
fn outline_survives_the_data_model() {
|
||||
let d =
|
||||
Diagram { kind: "hierarchy".into(), nodes: nodes(&[("CEO", 0), ("CTO", 1), ("Dev", 2), ("CFO", 1), ("Board", 0)]), ..Default::default() };
|
||||
let xml = data_xml(&d, 7, "rId9");
|
||||
let xml = data_xml(&d, 7, "rId9", &[]);
|
||||
let doc = crate::xml::parse(&xml).unwrap();
|
||||
assert_eq!(outline(&doc.root), d.nodes);
|
||||
let s = String::from_utf8(xml).unwrap();
|
||||
@@ -613,6 +675,14 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn node_pictures_come_from_presentation_points() {
|
||||
let d = Diagram { kind: "pictureList".into(), nodes: nodes(&[("A", 0), ("a", 1), ("B", 0)]), ..Default::default() };
|
||||
let xml = data_xml(&d, 3, "rId1", &[(2, "rId7".into()), (9, "rId8".into())]);
|
||||
let doc = crate::xml::parse(&xml).unwrap();
|
||||
assert_eq!(node_pictures(&doc.root), vec![(2, "rId7".to_string())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn style_and_colour_ids_round_trip() {
|
||||
for st in ["", "subtle", "intense", "outline", "polished", "inset", "cartoon", "powder", "brick"] {
|
||||
@@ -622,7 +692,9 @@ mod tests {
|
||||
assert_eq!(colors_for(&cs_id(c)), c);
|
||||
}
|
||||
assert!(String::from_utf8(style_xml("cartoon")).unwrap().contains("bevelT"));
|
||||
for k in ["pictureList", "pictureCaption", "pictureProcess"] {
|
||||
assert!(layout_id("pictureProcess").ends_with("/hProcess10"), "Picture Accent Process");
|
||||
assert!(layout_id("pictureHList").ends_with("/pList2"), "Horizontal Picture List");
|
||||
for k in ["pictureList", "pictureCaption", "pictureProcess", "pictureHList", "timeline", "venn", "target"] {
|
||||
assert_eq!(kind_for(layout_id(k), ""), k);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -433,6 +433,17 @@ fn smartart(imp: &mut Imp, part: &Part, gd: &El, frame: &Shape, depth: usize) ->
|
||||
let x = frame.xfrm.unwrap_or_default();
|
||||
g.kind = ShapeKind::Group { children, child: Xfrm::new(0.0, 0.0, x.w, x.h) };
|
||||
g.diagram = diagram_outline(imp, part, rel_ids, &data.root);
|
||||
// Node pictures, from the blips of the data model's presentation points.
|
||||
if let Some(d) = &mut g.diagram {
|
||||
let rels = imp.pkg.rels(&dm.target);
|
||||
for (k, rid) in crate::dgm::node_pictures(&data.root) {
|
||||
let Some(target) = rels.get(&rid).filter(|r| !r.external).map(|r| r.target.clone()) else { continue };
|
||||
let media = imp.media_for(&target);
|
||||
if let Some(n) = d.nodes.get_mut(k) {
|
||||
n.picture = media;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(g)
|
||||
}
|
||||
|
||||
|
||||
@@ -470,7 +470,12 @@ fn diagram(w: &mut W, x: &mut Exp, o: &mut Out, cx: &mut Ctx, s: &Shape, d: &dec
|
||||
let rel = |o: &mut Out, kind: &str, part: &str| o.rels.add_new(kind, &crate::opc::relative(&o.name, part), false);
|
||||
let drawing_rel = rel(o, dgm::RT_DRAWING, &dname);
|
||||
let data = name("data");
|
||||
x.pkg.xml_part(&data, dgm::CT_DATA, dgm::data_xml(d, s.id.0, &drawing_rel));
|
||||
// Node pictures: on the data model's presentation points, through the data part's rels.
|
||||
let mut mout = Out::new(&data);
|
||||
let pics: Vec<(usize, String)> =
|
||||
d.nodes.iter().enumerate().filter_map(|(k, n)| n.picture.and_then(|m| x.media_rel(&mut mout, m, "image")).map(|r| (k, r))).collect();
|
||||
x.pkg.xml_part(&data, dgm::CT_DATA, dgm::data_xml(d, s.id.0, &drawing_rel, &pics));
|
||||
x.pkg.rels(&data, &mout.rels);
|
||||
let dm = rel(o, "diagramData", &data);
|
||||
let layout = name("layout");
|
||||
let lo_xml = d.layout_xml.clone().map(String::into_bytes).unwrap_or_else(|| dgm::layout_xml(&d.kind));
|
||||
|
||||
Reference in New Issue
Block a user