Skip to main content

pptx_rs/shape/
smartartshape.rs

1//! `SmartArtShape`:高阶 SmartArt 形状(TODO-037 创建 API)。
2//!
3//! SmartArt(diagram)在 OOXML 中通过 `<p:graphicFrame>` +
4//! `<a:graphicData uri=".../diagram">` + `<dgm:relIds r:dm=".." r:lo=".." r:qs=".." r:cs=".."/>`
5//! 引用 4 个独立的 `/ppt/diagrams/{data,layout,quickStyles,colors}N.xml` part。
6//! 本高阶 API 把 graphicFrame 包装为 [`SmartArtShape`],提供 4 个关系 id 的
7//! 便捷访问与设置,并让 [`Shape`] trait 直接可用。
8//!
9//! # 与 python-pptx 的对应
10//!
11//! - python-pptx 当前**未提供** SmartArt 创建 API(截至 0.6.23);
12//! - 本类型参考 `ChartShape` / `OleObjectShape` 的设计模式自研。
13//!
14//! # 写出语义
15//!
16//! - `SmartArtShape` 序列化时只写出 `<p:graphicFrame>` + `<a:graphicData>` 内的
17//!   `<dgm:relIds r:dm=".." r:lo=".." r:qs=".." r:cs=".."/>` 引用元素;
18//! - 真正的 4 个 diagram part(data / layout / quickStyles / colors)由
19//!   [`crate::presentation::Presentation::save`] 在 `to_opc_package` 中遍历每张
20//!   slide 的 `diagram_entries` 写出独立 part;
21//! - slide 的 `_rels/slideN.xml.rels` 中会添加 4 个关系(DiagramData /
22//!   DiagramLayout / DiagramQuickStyle / DiagramColors)指向对应 part。
23//!
24//! # 创建路径
25//!
26//! - [`ShapesMut::add_smartart`](crate::slide::ShapesMut::add_smartart):从结构化
27//!   模型(`DataModel` + `LayoutDef` + `QuickStyleDef` + `ColorsDef`)创建;
28//! - [`ShapesMut::add_smartart_from_xml`](crate::slide::ShapesMut::add_smartart_from_xml):
29//!   从 4 份原始 XML 字符串创建(round-trip 友好的"逃生舱"入口)。
30
31use crate::oxml::shape::{Graphic as OxmlGraphic, GraphicFrame as OxmlFrame, SmartArtRef};
32use crate::shape::base::Shape;
33use crate::units::Emu;
34
35/// 高阶 SmartArt 形状(承载 `<p:graphicFrame>` + `<dgm:relIds>` 引用)。
36///
37/// 通过 [`SmartArtShape::smart_art`] / [`SmartArtShape::smart_art_mut`] 访问
38/// 内部 [`SmartArtRef`](含 4 个关系 id 与原始 XML);通过 [`Shape`] trait 方法
39/// (`left` / `top` / `width` / `height`)调整位置与尺寸。
40///
41/// # 内部不变量
42///
43/// `frame.graphic` 始终保持为 `Graphic::SmartArt(_)`。本类型所有便捷方法
44/// (`dm_rid` / `set_dm_rid` / `lo_rid` / `set_lo_rid` / ...)在不变量被破坏时
45/// **静默忽略**或返回空字符串,绝不 panic——
46/// 这与库整体"零 panic"约定一致(参见 `.trae/rules/project_rules.md` §5)。
47#[derive(Clone, Debug)]
48pub struct SmartArtShape {
49    /// 内部 oxml 句柄(`GraphicFrame`,承载 `Graphic::SmartArt`)。
50    pub(crate) frame: OxmlFrame,
51}
52
53impl SmartArtShape {
54    /// 构造一个 SmartArt 形状(4 个关系 id 留空,由 presentation 层填充)。
55    ///
56    /// 内部调用 [`SmartArtRef::from_rids`] 构造初始 `raw_xml`。
57    /// 后续通过 [`SmartArtShape::set_dm_rid`] 等 setter 更新任一 rid 时,
58    /// `raw_xml` 会**整体重新生成**(4 个 rid 同步刷新)。
59    pub fn new() -> Self {
60        let smart = SmartArtRef::from_rids("", "", "", "");
61        let frame = OxmlFrame {
62            graphic: OxmlGraphic::SmartArt(smart),
63            ..Default::default()
64        };
65        SmartArtShape { frame }
66    }
67
68    /// 从 4 个关系 id 构造 SmartArt 形状(直接初始化路径)。
69    pub fn from_rids(dm_rid: &str, lo_rid: &str, qs_rid: &str, cs_rid: &str) -> Self {
70        let smart = SmartArtRef::from_rids(dm_rid, lo_rid, qs_rid, cs_rid);
71        let frame = OxmlFrame {
72            graphic: OxmlGraphic::SmartArt(smart),
73            ..Default::default()
74        };
75        SmartArtShape { frame }
76    }
77
78    /// 从 oxml Frame 构造(通常用于读取已有 SmartArt 时)。
79    pub fn from_frame(frame: OxmlFrame) -> Self {
80        SmartArtShape { frame }
81    }
82
83    /// 取内部 oxml SmartArtRef 引用。
84    ///
85    /// 返回 `None` 仅在内部不变量被外部错误破坏时(`frame.graphic` 不是 `SmartArt` 变体)。
86    pub fn smart_art(&self) -> Option<&SmartArtRef> {
87        match &self.frame.graphic {
88            OxmlGraphic::SmartArt(s) => Some(s),
89            _ => None,
90        }
91    }
92
93    /// 取内部 oxml SmartArtRef 可变引用。
94    pub fn smart_art_mut(&mut self) -> Option<&mut SmartArtRef> {
95        match &mut self.frame.graphic {
96            OxmlGraphic::SmartArt(s) => Some(s),
97            _ => None,
98        }
99    }
100
101    /// 数据模型关系 id(`r:dm`,指向 `/ppt/diagrams/diagramDataN.xml`)。
102    ///
103    /// 不变量被破坏时返回空字符串。
104    pub fn dm_rid(&self) -> &str {
105        self.smart_art()
106            .and_then(|s| s.dm_rid.as_deref())
107            .unwrap_or("")
108    }
109
110    /// 布局定义关系 id(`r:lo`,指向 `/ppt/diagrams/diagramLayoutN.xml`)。
111    pub fn lo_rid(&self) -> &str {
112        self.smart_art()
113            .and_then(|s| s.lo_rid.as_deref())
114            .unwrap_or("")
115    }
116
117    /// 样式关系 id(`r:qs`,指向 `/ppt/diagrams/diagramQuickStyleN.xml`)。
118    pub fn qs_rid(&self) -> &str {
119        self.smart_art()
120            .and_then(|s| s.qs_rid.as_deref())
121            .unwrap_or("")
122    }
123
124    /// 颜色关系 id(`r:cs`,指向 `/ppt/diagrams/diagramColorsN.xml`)。
125    pub fn cs_rid(&self) -> &str {
126        self.smart_art()
127            .and_then(|s| s.cs_rid.as_deref())
128            .unwrap_or("")
129    }
130
131    /// 设置数据模型关系 id(`r:dm`)。
132    ///
133    /// **重要**:任一 rid 变更都会触发 `raw_xml` 整体重建(4 个 rid 同步刷新),
134    /// 因为 `raw_xml` 内的 `<dgm:relIds>` 元素同时承载 4 个属性。
135    /// 不变量被破坏时静默忽略。
136    pub fn set_dm_rid(&mut self, rid: impl Into<String>) {
137        if let Some(s) = self.smart_art_mut() {
138            s.dm_rid = Some(rid.into());
139            rebuild_raw_xml(s);
140        }
141    }
142
143    /// 设置布局定义关系 id(`r:lo`)。语义同 [`Self::set_dm_rid`]。
144    pub fn set_lo_rid(&mut self, rid: impl Into<String>) {
145        if let Some(s) = self.smart_art_mut() {
146            s.lo_rid = Some(rid.into());
147            rebuild_raw_xml(s);
148        }
149    }
150
151    /// 设置样式关系 id(`r:qs`)。语义同 [`Self::set_dm_rid`]。
152    pub fn set_qs_rid(&mut self, rid: impl Into<String>) {
153        if let Some(s) = self.smart_art_mut() {
154            s.qs_rid = Some(rid.into());
155            rebuild_raw_xml(s);
156        }
157    }
158
159    /// 设置颜色关系 id(`r:cs`)。语义同 [`Self::set_dm_rid`]。
160    pub fn set_cs_rid(&mut self, rid: impl Into<String>) {
161        if let Some(s) = self.smart_art_mut() {
162            s.cs_rid = Some(rid.into());
163            rebuild_raw_xml(s);
164        }
165    }
166
167    /// 一次性设置全部 4 个关系 id(比逐个 setter 高效:仅重建一次 raw_xml)。
168    pub fn set_all_rids(
169        &mut self,
170        dm_rid: impl Into<String>,
171        lo_rid: impl Into<String>,
172        qs_rid: impl Into<String>,
173        cs_rid: impl Into<String>,
174    ) {
175        if let Some(s) = self.smart_art_mut() {
176            s.dm_rid = Some(dm_rid.into());
177            s.lo_rid = Some(lo_rid.into());
178            s.qs_rid = Some(qs_rid.into());
179            s.cs_rid = Some(cs_rid.into());
180            rebuild_raw_xml(s);
181        }
182    }
183
184    /// 将本 SmartArt 形状标记为占位符。
185    ///
186    /// 写出 XML 时会在 `<p:nvGraphicFramePr>/<p:nvPr>` 内插入
187    /// `<p:ph type="..." idx="..."/>`。
188    pub fn set_placeholder(&mut self, ph_idx: u32, ph_type: Option<&str>) {
189        self.frame.is_placeholder = true;
190        self.frame.ph_idx = Some(ph_idx);
191        self.frame.ph_type = ph_type.map(|s| s.to_string());
192    }
193
194    /// 清除占位符标记。
195    pub fn clear_placeholder(&mut self) {
196        self.frame.is_placeholder = false;
197        self.frame.ph_idx = None;
198        self.frame.ph_type = None;
199    }
200
201    /// 是否被标记为占位符。
202    pub fn is_placeholder(&self) -> bool {
203        self.frame.is_placeholder
204    }
205
206    /// 占位符索引(若已标记)。
207    pub fn ph_idx(&self) -> Option<u32> {
208        self.frame.ph_idx
209    }
210
211    /// 占位符类型字符串(若已标记)。
212    pub fn ph_type(&self) -> Option<&str> {
213        self.frame.ph_type.as_deref()
214    }
215}
216
217impl Default for SmartArtShape {
218    fn default() -> Self {
219        Self::new()
220    }
221}
222
223/// 根据 SmartArtRef 当前的 4 个 rid 重建 raw_xml。
224///
225/// 内部委托 [`SmartArtRef::from_rids`],保证写路径一致。
226/// 4 个 rid 中任一为 `None` 时用空字符串兜底。
227fn rebuild_raw_xml(s: &mut SmartArtRef) {
228    let dm = s.dm_rid.as_deref().unwrap_or("");
229    let lo = s.lo_rid.as_deref().unwrap_or("");
230    let qs = s.qs_rid.as_deref().unwrap_or("");
231    let cs = s.cs_rid.as_deref().unwrap_or("");
232    s.raw_xml = SmartArtRef::from_rids(dm, lo, qs, cs).raw_xml;
233}
234
235impl Shape for SmartArtShape {
236    fn id(&self) -> u32 {
237        self.frame.id
238    }
239    fn set_id(&mut self, id: u32) {
240        self.frame.id = id;
241    }
242    fn name(&self) -> &str {
243        &self.frame.name
244    }
245    fn set_name(&mut self, name: String) {
246        self.frame.name = name;
247    }
248    fn shape_type(&self) -> &'static str {
249        "smart_art"
250    }
251
252    fn left(&self) -> Emu {
253        self.frame.properties.xfrm.off_x.unwrap_or_default()
254    }
255    fn set_left(&mut self, emu: Emu) {
256        self.frame.properties.xfrm.off_x = Some(emu);
257    }
258    fn top(&self) -> Emu {
259        self.frame.properties.xfrm.off_y.unwrap_or_default()
260    }
261    fn set_top(&mut self, emu: Emu) {
262        self.frame.properties.xfrm.off_y = Some(emu);
263    }
264    fn width(&self) -> Emu {
265        self.frame.properties.xfrm.ext_cx.unwrap_or_default()
266    }
267    fn set_width(&mut self, emu: Emu) {
268        self.frame.properties.xfrm.ext_cx = Some(emu);
269    }
270    fn height(&self) -> Emu {
271        self.frame.properties.xfrm.ext_cy.unwrap_or_default()
272    }
273    fn set_height(&mut self, emu: Emu) {
274        self.frame.properties.xfrm.ext_cy = Some(emu);
275    }
276
277    /// SmartArt 不支持旋转(OOXML 规范)。调用 [`Shape::set_rotation`] 会被忽略。
278    fn rotation(&self) -> f64 {
279        0.0
280    }
281    fn set_rotation(&mut self, _deg: f64) {}
282}
283
284#[cfg(test)]
285mod tests {
286    use super::*;
287
288    /// `new` 构造的 SmartArtShape:4 个 rid 默认空,但 raw_xml 已初始化。
289    #[test]
290    fn new_smartart_shape_basics() {
291        let s = SmartArtShape::new();
292        assert_eq!(s.dm_rid(), "");
293        assert_eq!(s.lo_rid(), "");
294        assert_eq!(s.qs_rid(), "");
295        assert_eq!(s.cs_rid(), "");
296        // raw_xml 非空(包含 graphicData 外壳 + dgm:relIds 空属性)
297        assert!(s.smart_art().unwrap().raw_xml.contains("dgm:relIds"));
298    }
299
300    /// `from_rids` 直接初始化 4 个 rid。
301    #[test]
302    fn from_rids_works() {
303        let s = SmartArtShape::from_rids("rId1", "rId2", "rId3", "rId4");
304        assert_eq!(s.dm_rid(), "rId1");
305        assert_eq!(s.lo_rid(), "rId2");
306        assert_eq!(s.qs_rid(), "rId3");
307        assert_eq!(s.cs_rid(), "rId4");
308        let xml = &s.smart_art().unwrap().raw_xml;
309        assert!(xml.contains("r:dm=\"rId1\""));
310        assert!(xml.contains("r:lo=\"rId2\""));
311        assert!(xml.contains("r:qs=\"rId3\""));
312        assert!(xml.contains("r:cs=\"rId4\""));
313    }
314
315    /// 单个 setter 触发 raw_xml 整体重建(4 个 rid 同步刷新)。
316    #[test]
317    fn set_single_rid_rebuilds_raw_xml() {
318        let mut s = SmartArtShape::from_rids("rId1", "rId2", "rId3", "rId4");
319        s.set_dm_rid("rIdDmNew");
320        assert_eq!(s.dm_rid(), "rIdDmNew");
321        // 其余 3 个 rid 保持不变
322        assert_eq!(s.lo_rid(), "rId2");
323        assert_eq!(s.qs_rid(), "rId3");
324        assert_eq!(s.cs_rid(), "rId4");
325        // raw_xml 同步刷新
326        let xml = &s.smart_art().unwrap().raw_xml;
327        assert!(xml.contains("r:dm=\"rIdDmNew\""));
328        assert!(xml.contains("r:lo=\"rId2\""));
329    }
330
331    /// `set_all_rids` 一次性更新 4 个 rid(仅重建一次 raw_xml)。
332    #[test]
333    fn set_all_rids_works() {
334        let mut s = SmartArtShape::new();
335        s.set_all_rids("a", "b", "c", "d");
336        assert_eq!(s.dm_rid(), "a");
337        assert_eq!(s.lo_rid(), "b");
338        assert_eq!(s.qs_rid(), "c");
339        assert_eq!(s.cs_rid(), "d");
340        let xml = &s.smart_art().unwrap().raw_xml;
341        assert!(xml.contains("r:dm=\"a\""));
342        assert!(xml.contains("r:cs=\"d\""));
343    }
344
345    /// `set_placeholder` / `clear_placeholder` 标记占位符。
346    #[test]
347    fn set_placeholder_works() {
348        let mut s = SmartArtShape::new();
349        assert!(!s.is_placeholder());
350        s.set_placeholder(0, Some("obj"));
351        assert!(s.is_placeholder());
352        assert_eq!(s.ph_idx(), Some(0));
353        assert_eq!(s.ph_type(), Some("obj"));
354        s.clear_placeholder();
355        assert!(!s.is_placeholder());
356        assert_eq!(s.ph_idx(), None);
357    }
358
359    /// Shape trait 基本几何操作生效。
360    #[test]
361    fn shape_trait_geometry() {
362        let mut s = SmartArtShape::new();
363        s.set_left(crate::Emu(100));
364        s.set_top(crate::Emu(200));
365        s.set_width(crate::Emu(300));
366        s.set_height(crate::Emu(400));
367        assert_eq!(s.left(), crate::Emu(100));
368        assert_eq!(s.top(), crate::Emu(200));
369        assert_eq!(s.width(), crate::Emu(300));
370        assert_eq!(s.height(), crate::Emu(400));
371        // 旋转始终为 0
372        s.set_rotation(45.0);
373        assert_eq!(s.rotation(), 0.0);
374    }
375}