Skip to main content

pptx_rs/oxml/
sppr.rs

1//! 形状属性:`<p:spPr>`(几何、变换、填充、线)。
2//!
3//! `spPr` 是所有视觉形状(sp/pic/grpSp/cxnSp)共享的元素;它包含:
4//!
5//! - `<a:xfrm>`:位置、尺寸、旋转、翻转。
6//! - `<a:prstGeom>` 或 `<a:custGeom>`:几何。
7//! - `<a:solidFill>` / 其它填充。
8//! - `<a:ln>`:线。
9//!
10//! # 设计要点
11//!
12//! - 所有几何量都用 [`Emu`] 表示,序列化时按 OOXML 整数规范输出;
13//! - 旋转单位为"60000 分之一度"(即 `rot=5400000` 表示 90°),由 `Transform::rot` 直接承载;
14//! - 填充/边框未设置时**不**写出 XML(让 PowerPoint 走母版默认);
15//! - [`ShapeProperties`] 是 `Sp` / `Pic` / `Group` / `Connector` 共享的字段。
16//!
17//! # 与 python-pptx 的对应
18//!
19//! - `pptx.oxml.shapes.shared.ShapeProperties` ←→ [`ShapeProperties`];
20//! - python-pptx 的 `BaseShapeElement` 风格未直接移植(避免 OOP 复杂性);
21//!   本库以"组合 + 共享字段"达到同样效果。
22
23use std::str::FromStr;
24
25use crate::oxml::color::Color;
26use crate::oxml::simpletypes::PresetGeometry;
27use crate::units::Emu;
28
29/// 仿射变换。
30#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
31pub struct Transform {
32    pub off_x: Option<Emu>,
33    pub off_y: Option<Emu>,
34    pub ext_cx: Option<Emu>,
35    pub ext_cy: Option<Emu>,
36    /// 顺时针旋转(60000 分之一度)。
37    pub rot: Option<i32>,
38    /// 水平翻转。
39    pub flip_h: bool,
40    /// 垂直翻转。
41    pub flip_v: bool,
42}
43
44impl Transform {
45    /// 写一段 XML。
46    ///
47    /// # 元素结构(OOXML)
48    ///
49    /// ```text
50    /// <a:xfrm rot="..." flipH="1" flipV="1">     ← 属性可省略
51    ///   <a:off x="..." y="..."/>                 ← 可选
52    ///   <a:ext cx="..." cy="..."/>               ← 可选
53    /// </a:xfrm>
54    /// ```
55    ///
56    /// # 错误模式
57    ///
58    /// - 早期版本曾把属性错误地写成嵌套的 `<a:xfrm/>` + `</a:xfrm>`,导致
59    ///   PowerPoint 报 "Invalid OOXML"。本方法已**合并**到**同一个**外层
60    ///   `<a:xfrm>` 开始标签上。
61    pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
62        if self.is_empty() {
63            return;
64        }
65        // 提前取出所有要序列化的字符串,扩展到函数末尾
66        let rot_s = self.rot.map(|v| v.to_string());
67        let off_x_s = self.off_x.map(|v| v.value().to_string());
68        let off_y_s = self.off_y.map(|v| v.value().to_string());
69        let ext_cx_s = self.ext_cx.map(|v| v.value().to_string());
70        let ext_cy_s = self.ext_cy.map(|v| v.value().to_string());
71
72        // 统一在外层 <a:xfrm> 上带属性——避免历史 bug 出现的"嵌套 a:xfrm"双标签。
73        let mut attrs: Vec<(&str, &str)> = Vec::new();
74        if self.flip_h {
75            attrs.push(("flipH", "1"));
76        }
77        if self.flip_v {
78            attrs.push(("flipV", "1"));
79        }
80        if let Some(s) = &rot_s {
81            attrs.push(("rot", s.as_str()));
82        }
83        w.open_with("a:xfrm", &attrs);
84        if let (Some(xs), Some(ys)) = (off_x_s.as_ref(), off_y_s.as_ref()) {
85            w.empty_with("a:off", &[("x", xs.as_str()), ("y", ys.as_str())]);
86        }
87        if let (Some(xs), Some(ys)) = (ext_cx_s.as_ref(), ext_cy_s.as_ref()) {
88            w.empty_with("a:ext", &[("cx", xs.as_str()), ("cy", ys.as_str())]);
89        }
90        w.close("a:xfrm");
91    }
92
93    /// 是否所有字段都为空。
94    pub fn is_empty(&self) -> bool {
95        self.off_x.is_none()
96            && self.off_y.is_none()
97            && self.ext_cx.is_none()
98            && self.ext_cy.is_none()
99            && self.rot.is_none()
100            && !self.flip_h
101            && !self.flip_v
102    }
103}
104
105/// 渐变类型(`<a:lin>` / `<a:path>`)。
106#[derive(Copy, Clone, Debug, Eq, PartialEq)]
107pub enum GradientType {
108    /// 线性渐变(`<a:lin ang="..." scaled="..."/>`)。
109    /// `ang` 单位为 1/60000 度(0 = 向右,5400000 = 向下)。
110    Linear(i32),
111    /// 路径渐变(`<a:path path="..."/>`)。
112    Path(GradientPath),
113}
114
115/// 路径渐变形状。
116#[derive(Copy, Clone, Debug, Eq, PartialEq)]
117pub enum GradientPath {
118    /// 圆形(`path="circle"`)。
119    Circle,
120    /// 矩形(`path="rect"`)。
121    Rect,
122    /// 形状轮廓(`path="shape"`)。
123    Shape,
124}
125
126impl GradientPath {
127    /// 转为 OOXML 属性值。
128    pub fn as_str(self) -> &'static str {
129        match self {
130            GradientPath::Circle => "circle",
131            GradientPath::Rect => "rect",
132            GradientPath::Shape => "shape",
133        }
134    }
135}
136
137/// 渐变光轨(`<a:gs pos="...">`)。
138#[derive(Clone, Debug, Default, PartialEq, Eq)]
139pub struct GradientStop {
140    /// 位置(0-100000,单位 1/1000 个百分点)。
141    pub pos: u32,
142    /// 颜色。
143    pub color: Color,
144}
145
146/// 渐变填充(`<a:gradFill>`)。
147#[derive(Clone, Debug, PartialEq, Eq)]
148pub struct GradientFill {
149    /// 光轨列表(至少 2 个)。
150    pub stops: Vec<GradientStop>,
151    /// 渐变类型(线性/路径)。
152    pub gradient_type: GradientType,
153    /// 是否翻转(`flip="none|tx|ty|xy|yx"`,默认 none)。
154    pub flip: Option<String>,
155    /// 是否与形状一起旋转(`rotWithShape="1|0"`)。
156    pub rot_with_shape: Option<bool>,
157}
158
159/// 图案填充(`<a:pattFill>`)。
160#[derive(Clone, Debug, Default, PartialEq, Eq)]
161pub struct PatternFill {
162    /// 预置图案类型(如 `"pct5"` / `"horz"` / `"vert"` / `"cross"` 等)。
163    pub prst: String,
164    /// 前景色(`<a:fgClr>`)。
165    pub fg_color: Color,
166    /// 背景色(`<a:bgClr>`)。
167    pub bg_color: Color,
168}
169
170/// 图片填充模式(`<a:stretch>` / `<a:tile>`)。
171///
172/// 对应 OOXML `CT_BlipFillProperties` 中的 `stretch` / `tile` 子元素。
173/// 用于控制图片在形状区域内的填充方式。
174///
175/// # OOXML 结构
176/// - `<a:stretch><a:fillRect/></a:stretch>`:拉伸填充(默认)
177/// - `<a:tile tx="..." ty="..." sx="..." sy="..." flip="..." algn="..."/>`:平铺填充
178#[derive(Clone, Debug, Default, PartialEq, Eq)]
179pub enum BlipFillMode {
180    /// 拉伸填充(`<a:stretch><a:fillRect/></a:stretch>`)。
181    ///
182    /// 图片被拉伸以填充整个形状区域。这是 PowerPoint 的默认模式。
183    #[default]
184    Stretch,
185    /// 平铺填充(`<a:tile .../>`)。
186    ///
187    /// 图片按指定间距、缩放、翻转和对齐方式平铺重复。
188    Tile {
189        /// 水平偏移(EMU)。`None` 表示不写出 `tx` 属性。
190        tx: Option<i64>,
191        /// 垂直偏移(EMU)。`None` 表示不写出 `ty` 属性。
192        ty: Option<i64>,
193        /// 水平缩放(千分比,100000 = 100%)。`None` 表示不写出 `sx` 属性。
194        sx: Option<i32>,
195        /// 垂直缩放(千分比,100000 = 100%)。`None` 表示不写出 `sy` 属性。
196        sy: Option<i32>,
197        /// 翻转模式(`"none"` / `"x"` / `"y"` / `"xy"`)。`None` 表示不写出 `flip` 属性。
198        flip: Option<String>,
199        /// 对齐方式(`"tl"` / `"t"` / `"tr"` / `"l"` / `"ctr"` / `"r"` / `"bl"` / `"b"` / `"br"`)。
200        /// `None` 表示不写出 `algn` 属性。
201        algn: Option<String>,
202    },
203    /// 无填充模式(不写出 `<a:stretch>` 或 `<a:tile>`)。
204    ///
205    /// 罕见但合法:图片仅按原始尺寸放置在形状左上角。
206    None,
207}
208
209/// 填充。
210#[derive(Clone, Debug, Default, PartialEq, Eq)]
211pub enum Fill {
212    /// 无填充(`<a:noFill/>`)。
213    None,
214    /// 实色。
215    Solid(Color),
216    /// 图片(`blipFill`),由 `blip_rid` 引用媒体。
217    Blip {
218        /// 关系 id(指向 `imageN.png`)。
219        rid: String,
220        /// 填充模式(拉伸/平铺/无)。
221        mode: BlipFillMode,
222    },
223    /// 渐变填充(`<a:gradFill>`)。
224    Gradient(GradientFill),
225    /// 图案填充(`<a:pattFill>`)。
226    Pattern(PatternFill),
227    /// 继承(不写)。
228    #[default]
229    Inherit,
230}
231
232impl BlipFillMode {
233    /// 写 XML。
234    ///
235    /// 根据 `BlipFillMode` 变体写出对应的 `<a:stretch>` / `<a:tile>` 元素,
236    /// 或什么都不写(`None` 变体)。
237    pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
238        match self {
239            BlipFillMode::Stretch => {
240                w.open("a:stretch");
241                w.empty("a:fillRect");
242                w.close("a:stretch");
243            }
244            BlipFillMode::Tile {
245                tx,
246                ty,
247                sx,
248                sy,
249                flip,
250                algn,
251            } => {
252                // 把所有属性值先取到块外,扩展生命周期
253                let tx_s = tx.map(|v| v.to_string());
254                let ty_s = ty.map(|v| v.to_string());
255                let sx_s = sx.map(|v| v.to_string());
256                let sy_s = sy.map(|v| v.to_string());
257                let mut attrs: Vec<(&str, &str)> = Vec::new();
258                if let Some(s) = &tx_s {
259                    attrs.push(("tx", s));
260                }
261                if let Some(s) = &ty_s {
262                    attrs.push(("ty", s));
263                }
264                if let Some(s) = &sx_s {
265                    attrs.push(("sx", s));
266                }
267                if let Some(s) = &sy_s {
268                    attrs.push(("sy", s));
269                }
270                if let Some(f) = flip {
271                    attrs.push(("flip", f));
272                }
273                if let Some(a) = algn {
274                    attrs.push(("algn", a));
275                }
276                w.empty_with("a:tile", &attrs);
277            }
278            BlipFillMode::None => {
279                // 不写出任何填充模式元素
280            }
281        }
282    }
283}
284
285impl Fill {
286    /// 写 XML。
287    pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
288        match self {
289            Fill::None => {
290                w.empty("a:noFill");
291            }
292            Fill::Solid(c) => c.write_solid_fill(w),
293            Fill::Blip { rid, mode } => {
294                w.open_with(
295                    "a:blipFill",
296                    &[("xmlns:r", crate::oxml::ns::NS_DRAWING_RELS)],
297                );
298                // 写出 <a:blip r:embed="..."/>(自闭合)
299                // 注意:此前实现错误地 w.open("a:blip") 后又 w.empty_with("a:blip", ...)
300                // 生成嵌套的无效 XML <a:blip><a:blip .../></a:blip>,已修复为单次 empty_with。
301                w.empty_with("a:blip", &[("r:embed", rid.as_str())]);
302                // 写出填充模式
303                mode.write_xml(w);
304                w.close("a:blipFill");
305            }
306            Fill::Gradient(g) => {
307                // <a:gradFill flip="..." rotWithShape="...">
308                let flip_s = g.flip.as_deref();
309                let rws_s = g.rot_with_shape.map(|b| if b { "1" } else { "0" });
310                let mut attrs: Vec<(&str, &str)> = Vec::new();
311                if let Some(f) = flip_s {
312                    attrs.push(("flip", f));
313                }
314                if let Some(r) = rws_s {
315                    attrs.push(("rotWithShape", r));
316                }
317                if attrs.is_empty() {
318                    w.open("a:gradFill");
319                } else {
320                    w.open_with("a:gradFill", &attrs);
321                }
322                // gsLst:光轨列表
323                w.open("a:gsLst");
324                for stop in &g.stops {
325                    let pos_s = stop.pos.to_string();
326                    w.open_with("a:gs", &[("pos", pos_s.as_str())]);
327                    stop.color.write_solid_fill(w);
328                    w.close("a:gs");
329                }
330                w.close("a:gsLst");
331                // 渐变类型
332                match &g.gradient_type {
333                    GradientType::Linear(ang) => {
334                        let ang_s = ang.to_string();
335                        w.empty_with("a:lin", &[("ang", ang_s.as_str()), ("scaled", "1")]);
336                    }
337                    GradientType::Path(p) => {
338                        w.empty_with("a:path", &[("path", p.as_str())]);
339                    }
340                }
341                w.close("a:gradFill");
342            }
343            Fill::Pattern(p) => {
344                w.open_with("a:pattFill", &[("prst", p.prst.as_str())]);
345                // fgClr
346                w.open("a:fgClr");
347                p.fg_color.write_solid_fill(w);
348                w.close("a:fgClr");
349                // bgClr
350                w.open("a:bgClr");
351                p.bg_color.write_solid_fill(w);
352                w.close("a:bgClr");
353                w.close("a:pattFill");
354            }
355            Fill::Inherit => { /* noop */ }
356        }
357    }
358}
359
360/// 箭头类型(`<a:headEnd>` / `<a:tailEnd>` 的 type 属性)。
361#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
362pub enum ArrowType {
363    /// 无箭头(`type="none"`)。
364    #[default]
365    None,
366    /// 箭头(`type="triangle"`)。
367    Triangle,
368    /// 箭头(stealth,`type="stealth"`)。
369    Stealth,
370    /// 菱形(`type="diamond"`)。
371    Diamond,
372    /// 椭圆(`type="oval"`)。
373    Oval,
374    /// 开放箭头(`type="arrow"`)。
375    Arrow,
376}
377
378impl ArrowType {
379    /// 转为 OOXML 属性值。
380    pub fn as_str(self) -> &'static str {
381        match self {
382            ArrowType::None => "none",
383            ArrowType::Triangle => "triangle",
384            ArrowType::Stealth => "stealth",
385            ArrowType::Diamond => "diamond",
386            ArrowType::Oval => "oval",
387            ArrowType::Arrow => "arrow",
388        }
389    }
390}
391
392/// 箭头尺寸(宽度/长度)。
393///
394/// OOXML 中 `w` 和 `len` 属性均取以下枚举值。
395#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
396pub enum ArrowSize {
397    /// 小(`"sm"`)。
398    Small,
399    /// 中等(`"med"`,默认)。
400    #[default]
401    Medium,
402    /// 大(`"lg"`)。
403    Large,
404}
405
406impl ArrowSize {
407    /// 转为 OOXML 属性值。
408    pub fn as_str(self) -> &'static str {
409        match self {
410            ArrowSize::Small => "sm",
411            ArrowSize::Medium => "med",
412            ArrowSize::Large => "lg",
413        }
414    }
415}
416
417/// 线条端点箭头(`<a:headEnd>` / `<a:tailEnd>`)。
418#[derive(Copy, Clone, Debug, Default)]
419pub struct ArrowHead {
420    /// 箭头类型。
421    pub arrow_type: ArrowType,
422    /// 箭头宽度。
423    pub width: ArrowSize,
424    /// 箭头长度。
425    pub length: ArrowSize,
426}
427
428/// 线条连接类型(`<a:round>` / `<a:miter>` / `<a:bevel>`)。
429#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
430pub enum LineJoin {
431    /// 圆角连接(`<a:round/>`,默认)。
432    #[default]
433    Round,
434    /// 尖角连接(`<a:miter lim="..."/>`)。
435    ///
436    /// `lim` 为斜接限制(1/1000 度,如 800000 表示 800 度)。
437    Miter(i32),
438    /// 斜角连接(`<a:bevel/>`)。
439    Bevel,
440}
441
442/// 边框(`<a:ln>`)。
443#[derive(Clone, Debug, Default)]
444pub struct Line {
445    pub width: Option<Emu>, // EMU
446    pub color: Color,
447    pub dash: Option<Dash>,
448    pub cap: Option<String>,
449    pub compound: Option<String>,
450    pub no_fill: bool,
451    /// 起点箭头(`<a:headEnd>`)。
452    pub head_end: Option<ArrowHead>,
453    /// 终点箭头(`<a:tailEnd>`)。
454    pub tail_end: Option<ArrowHead>,
455    /// 连接类型(`<a:round>` / `<a:miter>` / `<a:bevel>`)。
456    pub join: Option<LineJoin>,
457    /// 渐变/图案填充(`<a:gradFill>` / `<a:pattFill>`)。
458    ///
459    /// - `Fill::Inherit`(默认):使用 `color` / `no_fill` 字段(solidFill 或 noFill);
460    /// - `Fill::Gradient` / `Fill::Pattern`:写出对应的渐变/图案填充,**忽略** `color`。
461    pub fill: Fill,
462}
463
464/// 线型虚实(枚举子集)。
465#[derive(Copy, Clone, Debug, Eq, PartialEq)]
466pub enum Dash {
467    Solid,
468    Dash,
469    DashDot,
470    Dot,
471    LgDash,
472    LgDashDot,
473    LgDashDotDot,
474    SysDash,
475    SysDashDot,
476    SysDashDotDot,
477    SysDot,
478}
479
480impl Dash {
481    pub fn as_str(self) -> &'static str {
482        match self {
483            Dash::Solid => "solid",
484            Dash::Dash => "dash",
485            Dash::DashDot => "dashDot",
486            Dash::Dot => "dot",
487            Dash::LgDash => "lgDash",
488            Dash::LgDashDot => "lgDashDot",
489            Dash::LgDashDotDot => "lgDashDotDot",
490            Dash::SysDash => "sysDash",
491            Dash::SysDashDot => "sysDashDot",
492            Dash::SysDashDotDot => "sysDashDotDot",
493            Dash::SysDot => "sysDot",
494        }
495    }
496}
497
498impl FromStr for Dash {
499    type Err = ();
500    fn from_str(s: &str) -> Result<Self, Self::Err> {
501        Ok(match s {
502            "solid" => Dash::Solid,
503            "dash" => Dash::Dash,
504            "dashDot" => Dash::DashDot,
505            "dot" => Dash::Dot,
506            "lgDash" => Dash::LgDash,
507            "lgDashDot" => Dash::LgDashDot,
508            "lgDashDotDot" => Dash::LgDashDotDot,
509            "sysDash" => Dash::SysDash,
510            "sysDashDot" => Dash::SysDashDot,
511            "sysDashDotDot" => Dash::SysDashDotDot,
512            "sysDot" => Dash::SysDot,
513            _ => return Err(()),
514        })
515    }
516}
517
518impl Line {
519    /// 写 XML。
520    ///
521    /// # 元素结构
522    ///
523    /// ```text
524    /// <a:ln w="..." cap="..." cmpd="...">     ← 属性可省略
525    ///   <a:noFill/>                            ← 或 <a:solidFill><a:srgbClr .../></a:solidFill>
526    ///   <a:prstDash val="dash"/>               ← 可选
527    /// </a:ln>
528    /// ```
529    ///
530    /// # 行为
531    ///
532    /// - **总是**写出 `<a:ln>` 外壳(即便 width/color 都没设置)—— PowerPoint
533    ///   期望"显式无边框"也是 `<a:ln><a:noFill/></a:ln>` 的形态;
534    /// - 当 [`Line::no_fill`] 为真时,写 `<a:noFill/>`;否则写 solidFill。
535    pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
536        // 提前取出 width 字符串,扩展到函数末尾
537        let w_s = self.width.map(|v| v.value().to_string());
538        let mut attrs: Vec<(&str, &str)> = Vec::new();
539        if let Some(s) = &w_s {
540            attrs.push(("w", s.as_str()));
541        }
542        if let Some(c) = &self.cap {
543            attrs.push(("cap", c.as_str()));
544        }
545        if let Some(c) = &self.compound {
546            attrs.push(("cmpd", c.as_str()));
547        }
548        w.open_with("a:ln", &attrs);
549        if self.no_fill {
550            w.empty("a:noFill");
551        } else {
552            // 优先使用 fill 字段(渐变/图案填充);否则回退到 solidFill
553            match &self.fill {
554                Fill::Gradient(_) | Fill::Pattern(_) => self.fill.write_xml(w),
555                _ => self.color.write_solid_fill(w),
556            }
557        }
558        if let Some(d) = &self.dash {
559            w.open("a:prstDash");
560            w.empty_with("a:prst", &[("val", d.as_str())]);
561            w.close("a:prstDash");
562        }
563        // OOXML 顺序:headEnd → tailEnd → join(ECMA-376 §20.1.8.46-49)
564        if let Some(h) = &self.head_end {
565            w.empty_with(
566                "a:headEnd",
567                &[
568                    ("type", h.arrow_type.as_str()),
569                    ("w", h.width.as_str()),
570                    ("len", h.length.as_str()),
571                ],
572            );
573        }
574        if let Some(t) = &self.tail_end {
575            w.empty_with(
576                "a:tailEnd",
577                &[
578                    ("type", t.arrow_type.as_str()),
579                    ("w", t.width.as_str()),
580                    ("len", t.length.as_str()),
581                ],
582            );
583        }
584        if let Some(j) = &self.join {
585            match j {
586                LineJoin::Round => {
587                    w.empty("a:round");
588                }
589                LineJoin::Miter(lim) => {
590                    let lim_s = lim.to_string();
591                    w.empty_with("a:miter", &[("lim", lim_s.as_str())]);
592                }
593                LineJoin::Bevel => {
594                    w.empty("a:bevel");
595                }
596            }
597        }
598        w.close("a:ln");
599    }
600}
601
602/// 阴影效果(`<a:outerShdw>` / `<a:innerShdw>`)。
603///
604/// 对应 python-pptx 中 `ShadowFormat` 的底层支撑。
605#[derive(Clone, Debug, Default, PartialEq, Eq)]
606pub struct ShadowEffect {
607    /// 阴影方向(1/60000 度,0 = 向右,2700000 = 向下,5400000 = 向左,8100000 = 向上)。
608    pub dir: i32,
609    /// 距离(EMU)。
610    pub dist: i64,
611    /// 模糊半径(EMU)。
612    pub blur_rad: i64,
613    /// 阴影颜色。
614    pub color: Color,
615    /// 是否随形状旋转(仅 outerShdw)。
616    pub rot_with_shape: Option<bool>,
617}
618
619/// 发光效果(`<a:glow>`)。
620#[derive(Clone, Debug, Default, PartialEq, Eq)]
621pub struct GlowEffect {
622    /// 发光半径(EMU)。
623    pub rad: i64,
624    /// 发光颜色。
625    pub color: Color,
626}
627
628/// 柔化边缘效果(`<a:softEdge>`)。
629#[derive(Clone, Debug, Default, PartialEq, Eq)]
630pub struct SoftEdgeEffect {
631    /// 柔化半径(EMU)。
632    pub rad: i64,
633}
634
635/// 反射效果(`<a:reflection>`)。
636#[derive(Clone, Debug, Default, PartialEq, Eq)]
637pub struct ReflectionEffect {
638    /// 模糊半径(EMU)。
639    pub blur_rad: Option<i64>,
640    /// 起始透明度(0-100000)。
641    pub st_a: Option<i32>,
642    /// 起始位置(0-100000)。
643    pub st_pos: Option<i32>,
644    /// 结束透明度(0-100000)。
645    pub end_a: Option<i32>,
646    /// 结束位置(0-100000)。
647    pub end_pos: Option<i32>,
648    /// 距离(EMU)。
649    pub dist: Option<i64>,
650    /// 方向(1/60000 度)。
651    pub dir: Option<i32>,
652    /// 是否随形状旋转。
653    pub rot_with_shape: Option<bool>,
654}
655
656/// 效果列表(`<a:effectLst>`)。
657///
658/// 对应 OOXML 中 `<p:spPr>` 内 `<a:effectLst>` 元素。
659/// 按 OOXML 顺序:blur → fillOverlay → glow → innerShdw → outerShdw → prstShdw → reflection → softEdge。
660#[derive(Clone, Debug, Default, PartialEq, Eq)]
661pub struct EffectList {
662    /// 外阴影(`<a:outerShdw>`,最常用)。
663    pub outer_shadow: Option<ShadowEffect>,
664    /// 内阴影(`<a:innerShdw>`)。
665    pub inner_shadow: Option<ShadowEffect>,
666    /// 发光(`<a:glow>`)。
667    pub glow: Option<GlowEffect>,
668    /// 柔化边缘(`<a:softEdge>`)。
669    pub soft_edge: Option<SoftEdgeEffect>,
670    /// 反射(`<a:reflection>`)。
671    pub reflection: Option<ReflectionEffect>,
672}
673
674impl EffectList {
675    /// 是否所有效果都为 None。
676    pub fn is_empty(&self) -> bool {
677        self.outer_shadow.is_none()
678            && self.inner_shadow.is_none()
679            && self.glow.is_none()
680            && self.soft_edge.is_none()
681            && self.reflection.is_none()
682    }
683
684    /// 写出 `<a:effectLst>...</a:effectLst>`。
685    ///
686    /// 若 `is_empty()` 为 true,则**不**写出(调用方应先判断)。
687    pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
688        if self.is_empty() {
689            return;
690        }
691        w.open("a:effectLst");
692        // 按 OOXML 顺序:glow → innerShdw → outerShdw → reflection → softEdge
693        if let Some(g) = &self.glow {
694            let rad_s = g.rad.to_string();
695            w.open_with("a:glow", &[("rad", rad_s.as_str())]);
696            g.color.write_solid_fill(w);
697            w.close("a:glow");
698        }
699        if let Some(s) = &self.inner_shadow {
700            let dir_s = s.dir.to_string();
701            let dist_s = s.dist.to_string();
702            let blur_s = s.blur_rad.to_string();
703            w.open_with(
704                "a:innerShdw",
705                &[
706                    ("blurRad", blur_s.as_str()),
707                    ("dist", dist_s.as_str()),
708                    ("dir", dir_s.as_str()),
709                ],
710            );
711            s.color.write_solid_fill(w);
712            w.close("a:innerShdw");
713        }
714        if let Some(s) = &self.outer_shadow {
715            let dir_s = s.dir.to_string();
716            let dist_s = s.dist.to_string();
717            let blur_s = s.blur_rad.to_string();
718            let mut attrs: Vec<(&str, &str)> = vec![
719                ("blurRad", blur_s.as_str()),
720                ("dist", dist_s.as_str()),
721                ("dir", dir_s.as_str()),
722            ];
723            let rot_s;
724            if let Some(r) = s.rot_with_shape {
725                rot_s = if r { "1".to_string() } else { "0".to_string() };
726                attrs.push(("rotWithShape", rot_s.as_str()));
727            }
728            w.open_with("a:outerShdw", &attrs);
729            s.color.write_solid_fill(w);
730            w.close("a:outerShdw");
731        }
732        if let Some(r) = &self.reflection {
733            // 用 owned String 收集属性值,避免生命周期问题
734            let mut attrs: Vec<(String, String)> = Vec::new();
735            if let Some(v) = r.blur_rad {
736                attrs.push(("blurRad".to_string(), v.to_string()));
737            }
738            if let Some(v) = r.st_a {
739                attrs.push(("stA".to_string(), v.to_string()));
740            }
741            if let Some(v) = r.st_pos {
742                attrs.push(("stPos".to_string(), v.to_string()));
743            }
744            if let Some(v) = r.end_a {
745                attrs.push(("endA".to_string(), v.to_string()));
746            }
747            if let Some(v) = r.end_pos {
748                attrs.push(("endPos".to_string(), v.to_string()));
749            }
750            if let Some(v) = r.dist {
751                attrs.push(("dist".to_string(), v.to_string()));
752            }
753            if let Some(v) = r.dir {
754                attrs.push(("dir".to_string(), v.to_string()));
755            }
756            if let Some(v) = r.rot_with_shape {
757                attrs.push((
758                    "rotWithShape".to_string(),
759                    if v { "1".to_string() } else { "0".to_string() },
760                ));
761            }
762            let refs: Vec<(&str, &str)> = attrs
763                .iter()
764                .map(|(k, v)| (k.as_str(), v.as_str()))
765                .collect();
766            w.empty_with("a:reflection", &refs);
767        }
768        if let Some(s) = &self.soft_edge {
769            let rad_s = s.rad.to_string();
770            w.empty_with("a:softEdge", &[("rad", rad_s.as_str())]);
771        }
772        w.close("a:effectLst");
773    }
774}
775
776// ===== TODO-024:自定义几何(custGeom)=====
777
778/// 几何矩形(`<a:rect l="..." t="..." r="..." b="..."/>`)。
779///
780/// 用于 `<a:custGeom>` 内部的内嵌区域定义,值可以是百分比或 EMU。
781#[derive(Clone, Debug, Default, PartialEq, Eq)]
782pub struct GeomRect {
783    /// 左边界(`l="..."`)。
784    pub l: String,
785    /// 上边界(`t="..."`)。
786    pub t: String,
787    /// 右边界(`r="..."`)。
788    pub r: String,
789    /// 下边界(`b="..."`)。
790    pub b: String,
791}
792
793/// 路径段(`<a:moveTo>` / `<a:lnTo>` / `<a:cubicBezTo>` / `<a:quadBezTo>` / `<a:arcTo>` / `<a:close/>`)。
794///
795/// 对应 OOXML `<a:path>` 内的子元素,描述自由路径的绘制步骤。
796#[derive(Clone, Debug, PartialEq, Eq)]
797pub enum PathSegment {
798    /// 移动到(`<a:moveTo><a:pt x="..." y="..."/></a:moveTo>`)。
799    MoveTo { x: i64, y: i64 },
800    /// 直线到(`<a:lnTo><a:pt x="..." y="..."/></a:lnTo>`)。
801    LineTo { x: i64, y: i64 },
802    /// 三次贝塞尔曲线(`<a:cubicBezTo>`,含 3 个控制点)。
803    CubicBezTo {
804        x1: i64,
805        y1: i64,
806        x2: i64,
807        y2: i64,
808        x3: i64,
809        y3: i64,
810    },
811    /// 二次贝塞尔曲线(`<a:quadBezTo>`,含 2 个控制点)。
812    QuadBezTo { x1: i64, y1: i64, x2: i64, y2: i64 },
813    /// 弧线(`<a:arcTo wR="..." hR="..." stAng="..." swAng="..."/>`)。
814    ///
815    /// - `w_r` / `h_r`:椭圆半径(EMU);
816    /// - `st_ang` / `sw_ang`:起始角 / 扫掠角(1/60000 度)。
817    ArcTo {
818        w_r: i64,
819        h_r: i64,
820        st_ang: i32,
821        sw_ang: i32,
822    },
823    /// 关闭路径(`<a:close/>`)。
824    Close,
825}
826
827impl PathSegment {
828    /// 写出路径段 XML 到 writer。
829    pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
830        match self {
831            PathSegment::MoveTo { x, y } => {
832                let xs = x.to_string();
833                let ys = y.to_string();
834                w.open("a:moveTo");
835                w.empty_with("a:pt", &[("x", xs.as_str()), ("y", ys.as_str())]);
836                w.close("a:moveTo");
837            }
838            PathSegment::LineTo { x, y } => {
839                let xs = x.to_string();
840                let ys = y.to_string();
841                w.open("a:lnTo");
842                w.empty_with("a:pt", &[("x", xs.as_str()), ("y", ys.as_str())]);
843                w.close("a:lnTo");
844            }
845            PathSegment::CubicBezTo {
846                x1,
847                y1,
848                x2,
849                y2,
850                x3,
851                y3,
852            } => {
853                let x1s = x1.to_string();
854                let y1s = y1.to_string();
855                let x2s = x2.to_string();
856                let y2s = y2.to_string();
857                let x3s = x3.to_string();
858                let y3s = y3.to_string();
859                w.open("a:cubicBezTo");
860                w.empty_with("a:pt", &[("x", x1s.as_str()), ("y", y1s.as_str())]);
861                w.empty_with("a:pt", &[("x", x2s.as_str()), ("y", y2s.as_str())]);
862                w.empty_with("a:pt", &[("x", x3s.as_str()), ("y", y3s.as_str())]);
863                w.close("a:cubicBezTo");
864            }
865            PathSegment::QuadBezTo { x1, y1, x2, y2 } => {
866                let x1s = x1.to_string();
867                let y1s = y1.to_string();
868                let x2s = x2.to_string();
869                let y2s = y2.to_string();
870                w.open("a:quadBezTo");
871                w.empty_with("a:pt", &[("x", x1s.as_str()), ("y", y1s.as_str())]);
872                w.empty_with("a:pt", &[("x", x2s.as_str()), ("y", y2s.as_str())]);
873                w.close("a:quadBezTo");
874            }
875            PathSegment::ArcTo {
876                w_r,
877                h_r,
878                st_ang,
879                sw_ang,
880            } => {
881                let wr_s = w_r.to_string();
882                let hr_s = h_r.to_string();
883                let st_s = st_ang.to_string();
884                let sw_s = sw_ang.to_string();
885                w.empty_with(
886                    "a:arcTo",
887                    &[
888                        ("wR", wr_s.as_str()),
889                        ("hR", hr_s.as_str()),
890                        ("stAng", st_s.as_str()),
891                        ("swAng", sw_s.as_str()),
892                    ],
893                );
894            }
895            PathSegment::Close => {
896                w.empty("a:close");
897            }
898        }
899    }
900}
901
902/// 路径(`<a:path>`)。
903///
904/// 一条路径由宽度/高度和一组 [`PathSegment`] 组成。
905#[derive(Clone, Debug, Default, PartialEq, Eq)]
906pub struct Path {
907    /// 宽度(EMU,`w="..."`)。
908    pub width: i64,
909    /// 高度(EMU,`h="..."`)。
910    pub height: i64,
911    /// 填充模式(`fill="none|norm|darken|darkenLess|lighten|lightenLess"`,可选)。
912    pub fill: Option<String>,
913    /// 描边模式(`stroke="none|norm"`,可选)。
914    pub stroke: Option<String>,
915    /// 路径段列表。
916    pub segments: Vec<PathSegment>,
917}
918
919impl Path {
920    /// 写出 `<a:path>` 元素到 writer。
921    pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
922        let w_s = self.width.to_string();
923        let h_s = self.height.to_string();
924        let mut attrs: Vec<(&str, &str)> = Vec::new();
925        attrs.push(("w", w_s.as_str()));
926        attrs.push(("h", h_s.as_str()));
927        if let Some(f) = &self.fill {
928            attrs.push(("fill", f.as_str()));
929        }
930        if let Some(s) = &self.stroke {
931            attrs.push(("stroke", s.as_str()));
932        }
933        w.open_with("a:path", &attrs);
934        for seg in &self.segments {
935            seg.write_xml(w);
936        }
937        w.close("a:path");
938    }
939}
940
941/// 自定义几何(`<a:custGeom>`)。
942///
943/// 对应 OOXML 中 `<a:custGeom>` 元素,包含路径列表和可选的填充/描边/内嵌区域。
944#[derive(Clone, Debug, Default, PartialEq, Eq)]
945pub struct CustomGeometry {
946    /// 是否允许填充(`<a:fill>`,默认 "norm")。`None` 表示不写出。
947    pub fill: Option<String>,
948    /// 是否允许描边(`<a:stroke>`,默认 "norm")。`None` 表示不写出。
949    pub stroke: Option<String>,
950    /// 内嵌区域(`<a:rect l="..." t="..." r="..." b="..."/>`,可选)。
951    pub rect: Option<GeomRect>,
952    /// 路径列表(`<a:pathLst>`)。
953    pub path_list: Vec<Path>,
954}
955
956impl CustomGeometry {
957    /// 写出 `<a:custGeom>` 元素到 writer。
958    ///
959    /// # 元素顺序(OOXML 规范)
960    ///
961    /// ```text
962    /// <a:custGeom>
963    ///   <a:avLst/>          ← 可选,调整手柄列表(暂不支持)
964    ///   <a:fill>...</a:fill> ← 可选
965    ///   <a:stroke>...</a:stroke> ← 可选
966    ///   <a:rect .../>        ← 可选
967    ///   <a:pathLst>
968    ///     <a:path>...</a:path>
969    ///   </a:pathLst>
970    /// </a:custGeom>
971    /// ```
972    pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
973        w.open("a:custGeom");
974        // avLst(调整手柄列表,暂不支持,写空)
975        w.empty("a:avLst");
976        // fill(可选)
977        if let Some(f) = &self.fill {
978            w.leaf("a:fill", f.as_str());
979        }
980        // stroke(可选)
981        if let Some(s) = &self.stroke {
982            w.leaf("a:stroke", s.as_str());
983        }
984        // rect(可选)
985        if let Some(r) = &self.rect {
986            w.empty_with(
987                "a:rect",
988                &[
989                    ("l", r.l.as_str()),
990                    ("t", r.t.as_str()),
991                    ("r", r.r.as_str()),
992                    ("b", r.b.as_str()),
993                ],
994            );
995        }
996        // pathLst
997        w.open("a:pathLst");
998        for p in &self.path_list {
999            p.write_xml(w);
1000        }
1001        w.close("a:pathLst");
1002        w.close("a:custGeom");
1003    }
1004}
1005
1006/// 调整值(`<a:gd name="..." fmla="val <value>"/>`)。
1007///
1008/// 对应 python-pptx `Adjustment`,控制预设形状的调整手柄(如圆角矩形的圆角半径)。
1009///
1010/// # OOXML 结构
1011///
1012/// ```text
1013/// <a:avLst>
1014///   <a:gd name="adj" fmla="val 16667"/>   ← 调整值,16667 = 16.667%
1015/// </a:avLst>
1016/// ```
1017///
1018/// # 值的含义
1019///
1020/// 调整值以 1/100000 为单位(即 `100000` = 100%)。`effective_value()` 返回归一化的
1021/// 0.0-1.0 浮点值,`raw_value` 保存原始整数。
1022#[derive(Clone, Debug, Default, PartialEq, Eq)]
1023pub struct AdjustmentValue {
1024    /// 调整值名称(如 "adj"、"adj1"、"adj2")。
1025    pub name: String,
1026    /// 原始值(从 `fmla="val <value>"` 中提取的数值,单位 1/100000)。
1027    pub raw_value: i64,
1028}
1029
1030impl AdjustmentValue {
1031    /// 用名称和原始值构造。
1032    ///
1033    /// # 参数
1034    /// - `name`:调整值名称(如 "adj");
1035    /// - `raw_value`:原始值(单位 1/100000,如 16667 表示 16.667%)。
1036    pub fn new(name: impl Into<String>, raw_value: i64) -> Self {
1037        Self {
1038            name: name.into(),
1039            raw_value,
1040        }
1041    }
1042
1043    /// 用名称和归一化值(0.0-1.0)构造。
1044    ///
1045    /// # 参数
1046    /// - `name`:调整值名称;
1047    /// - `value`:归一化值(0.0-1.0,会被转换为 1/100000 单位)。
1048    pub fn from_normalized(name: impl Into<String>, value: f64) -> Self {
1049        Self {
1050            name: name.into(),
1051            raw_value: (value * 100000.0).round() as i64,
1052        }
1053    }
1054
1055    /// 归一化值(0.0-1.0,即 `raw_value / 100000.0`)。
1056    ///
1057    /// 对应 python-pptx `Adjustment.effective_value`。
1058    pub fn effective_value(&self) -> f64 {
1059        self.raw_value as f64 / 100000.0
1060    }
1061
1062    /// 写出 `<a:gd name="..." fmla="val ..."/>` 元素。
1063    pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
1064        let val_s = self.raw_value.to_string();
1065        w.empty_with(
1066            "a:gd",
1067            &[
1068                ("name", self.name.as_str()),
1069                ("fmla", &format!("val {}", val_s)),
1070            ],
1071        );
1072    }
1073}
1074
1075/// 几何类型(`<a:prstGeom>` 或 `<a:custGeom>`)。
1076///
1077/// 对标 python-pptx 的 `Geometry` 概念,统一表达预设几何和自定义几何。
1078#[derive(Clone, Debug, PartialEq, Eq)]
1079pub enum Geometry {
1080    /// 预设几何(`<a:prstGeom prst="...">`)。
1081    ///
1082    /// 第二个元素是调整值列表(`<a:avLst>`),控制形状的调整手柄。
1083    Preset(PresetGeometry, Vec<AdjustmentValue>),
1084    /// 自定义几何(`<a:custGeom>`)。
1085    Custom(CustomGeometry),
1086}
1087
1088impl Default for Geometry {
1089    fn default() -> Self {
1090        Geometry::Preset(PresetGeometry::Rectangle, Vec::new())
1091    }
1092}
1093
1094impl Geometry {
1095    /// 创建无调整值的预设几何。
1096    ///
1097    /// 等价于 `Geometry::Preset(prst, Vec::new())`。
1098    pub fn preset(prst: PresetGeometry) -> Self {
1099        Geometry::Preset(prst, Vec::new())
1100    }
1101
1102    /// 写出几何 XML(`<a:prstGeom>` 或 `<a:custGeom>`)。
1103    pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
1104        match self {
1105            Geometry::Preset(p, adjustments) => {
1106                w.open_with("a:prstGeom", &[("prst", p.as_str())]);
1107                // avLst:调整值列表
1108                if adjustments.is_empty() {
1109                    w.empty("a:avLst");
1110                } else {
1111                    w.open("a:avLst");
1112                    for adj in adjustments {
1113                        adj.write_xml(w);
1114                    }
1115                    w.close("a:avLst");
1116                }
1117                w.close("a:prstGeom");
1118            }
1119            Geometry::Custom(c) => {
1120                c.write_xml(w);
1121            }
1122        }
1123    }
1124}
1125
1126/// 形状属性 `<p:spPr>`。
1127#[derive(Clone, Debug, Default)]
1128pub struct ShapeProperties {
1129    /// 仿射变换(位置/尺寸/旋转/翻转)。
1130    pub xfrm: Transform,
1131    /// 几何(预设或自定义)。`None` 默认为 `Preset(Rectangle)`。
1132    ///
1133    /// TODO-024:从 `Option<PresetGeometry>` 改为 `Option<Geometry>`,
1134    /// 支持自定义几何(`<a:custGeom>`)。
1135    pub geometry: Option<Geometry>,
1136    /// 填充(实色/图片/无/继承)。
1137    pub fill: Fill,
1138    /// 边框(`<a:ln>`)。`None` 表示不写出边框。
1139    pub line: Option<Line>,
1140    /// 效果列表(`<a:effectLst>`,可选)。
1141    pub effects: Option<EffectList>,
1142    /// 三维场景(`<a:scene3d>`,可选,TODO-050)。
1143    ///
1144    /// 定义相机与光照;与 `sp3d` 共同表达 3D 效果。
1145    pub scene3d: Option<Scene3d>,
1146    /// 形状 3D 属性(`<a:sp3d>`,可选,TODO-050)。
1147    ///
1148    /// 定义形状本身的拉伸/棱台/材质。
1149    pub sp3d: Option<Sp3d>,
1150    /// 旋转角度(度数,正向顺时针)。
1151    pub rot_deg: Option<f64>,
1152}
1153
1154impl ShapeProperties {
1155    /// 写 XML。`tag` 一般为 `"p:spPr"`。
1156    ///
1157    /// 按 OOXML 规范,`<p:spPr>` 内部必须按以下顺序排列:
1158    ///
1159    /// 1. `<a:xfrm>`(可选):位置/尺寸/旋转;
1160    /// 2. `<a:prstGeom>` / `<a:custGeom>`:几何;
1161    /// 3. 填充相关(`<a:noFill>` / `<a:solidFill>` / `<a:gradFill>` / `<a:blipFill>` / `<a:pattFill>`);
1162    /// 4. `<a:ln>`:边框;
1163    /// 5. 效果(`<a:effectLst>` 等);
1164    /// 6. `<a:scene3d>` / `<a:sp3d>`:三维效果;
1165    /// 7. `<a:extLst>`:扩展。
1166    ///
1167    /// 顺序错误会导致 PowerPoint 弹出"Invalid OOXML"对话框。
1168    pub fn write_xml(&self, w: &mut super::writer::XmlWriter, tag: &str) {
1169        w.open(tag);
1170        // xfrm
1171        if !self.xfrm.is_empty() {
1172            self.xfrm.write_xml(w);
1173        }
1174        // 几何(prstGeom 或 custGeom,TODO-024)
1175        let geom = self.geometry.clone().unwrap_or_default();
1176        geom.write_xml(w);
1177        // fill
1178        self.fill.write_xml(w);
1179        // ln
1180        if let Some(ln) = &self.line {
1181            ln.write_xml(w);
1182        }
1183        // effectLst(TODO-011:形状效果)
1184        if let Some(effects) = &self.effects {
1185            effects.write_xml(w);
1186        }
1187        // scene3d(TODO-050:三维场景)
1188        if let Some(scene) = &self.scene3d {
1189            scene.write_xml(w);
1190        }
1191        // sp3d(TODO-050:形状 3D 属性)
1192        if let Some(sp3d) = &self.sp3d {
1193            sp3d.write_xml(w);
1194        }
1195        w.close(tag);
1196    }
1197
1198    /// **仅**写 xfrm 元素(`<p:xfrm><a:xfrm>...</a:xfrm></p:xfrm>`)。
1199    ///
1200    /// 仅 [`crate::oxml::shape::GraphicFrame`] 使用 —— OOXML 中
1201    /// `<p:graphicFrame>` 的子元素顺序是:
1202    ///
1203    /// ```text
1204    /// <p:graphicFrame>
1205    ///   <p:nvGraphicFramePr>...</p:nvGraphicFramePr>
1206    ///   <p:xfrm>...</p:xfrm>          ← 本方法
1207    ///   <a:graphic>...</a:graphic>
1208    ///   <p:extLst>...</p:extLst>      ← 可选
1209    /// </p:graphicFrame>
1210    /// ```
1211    ///
1212    /// 与 [`ShapeProperties::write_xml`] 的区别是:本方法**不**输出 prstGeom /
1213    /// 填充 / 边框(这些是 `<p:spPr>` 的职责),只输出位置 / 尺寸变换。
1214    pub fn write_xfrm_only(&self, w: &mut super::writer::XmlWriter) {
1215        if self.xfrm.is_empty() {
1216            // 仍然输出空 <p:xfrm/> 以保证 OOXML 顺序稳定
1217            w.empty("p:xfrm");
1218            return;
1219        }
1220        w.open("p:xfrm");
1221        self.xfrm.write_xml(w);
1222        w.close("p:xfrm");
1223    }
1224
1225    // --------------------- 形状效果 API(TODO-011 高阶) ---------------------
1226    //
1227    // 以下便捷方法封装 `<a:effectLst>` 的常用操作,对标 python-pptx 的
1228    // `shape.shadow` / `shape.glow` 等高阶接口。底层逻辑都通过 `effects`
1229    // 字段(`Option<EffectList>`)承接,序列化时由 `write_xml` 自动按
1230    // OOXML 顺序输出在 `<a:ln>` 之后。
1231
1232    /// 读取效果列表(`<a:effectLst>`)。`None` 表示未设置。
1233    pub fn effects(&self) -> Option<&EffectList> {
1234        self.effects.as_ref()
1235    }
1236
1237    /// 读取效果列表的可变引用。若未设置,自动初始化为空 `EffectList`。
1238    ///
1239    /// 调用此方法后即使不设置任何效果,也会写出空 `<a:effectLst/>`(PowerPoint 兼容)。
1240    /// 若要避免写出空元素,请用 [`Self::clear_effects`]。
1241    pub fn effects_mut(&mut self) -> &mut EffectList {
1242        self.effects.get_or_insert_with(EffectList::default)
1243    }
1244
1245    /// 设置外阴影(`<a:outerShdw>`)。覆盖既有外阴影,保留其他效果。
1246    ///
1247    /// 对标 python-pptx `shape.shadow.inherit = False` + `shape.shadow.outerShadow`。
1248    ///
1249    /// # 参数
1250    /// - `shadow`:阴影配置(方向/距离/模糊半径/颜色)
1251    pub fn set_outer_shadow(&mut self, shadow: ShadowEffect) {
1252        self.effects_mut().outer_shadow = Some(shadow);
1253    }
1254
1255    /// 设置内阴影(`<a:innerShdw>`)。覆盖既有内阴影,保留其他效果。
1256    pub fn set_inner_shadow(&mut self, shadow: ShadowEffect) {
1257        self.effects_mut().inner_shadow = Some(shadow);
1258    }
1259
1260    /// 设置发光(`<a:glow>`)。覆盖既有发光,保留其他效果。
1261    pub fn set_glow(&mut self, glow: GlowEffect) {
1262        self.effects_mut().glow = Some(glow);
1263    }
1264
1265    /// 设置柔化边缘(`<a:softEdge>`)。覆盖既有柔边,保留其他效果。
1266    pub fn set_soft_edge(&mut self, rad: i64) {
1267        self.effects_mut().soft_edge = Some(SoftEdgeEffect { rad });
1268    }
1269
1270    /// 设置反射(`<a:reflection>`)。覆盖既有反射,保留其他效果。
1271    pub fn set_reflection(&mut self, reflection: ReflectionEffect) {
1272        self.effects_mut().reflection = Some(reflection);
1273    }
1274
1275    /// 清除外阴影。
1276    pub fn clear_outer_shadow(&mut self) {
1277        if let Some(e) = self.effects.as_mut() {
1278            e.outer_shadow = None;
1279        }
1280    }
1281
1282    /// 清除内阴影。
1283    pub fn clear_inner_shadow(&mut self) {
1284        if let Some(e) = self.effects.as_mut() {
1285            e.inner_shadow = None;
1286        }
1287    }
1288
1289    /// 清除发光。
1290    pub fn clear_glow(&mut self) {
1291        if let Some(e) = self.effects.as_mut() {
1292            e.glow = None;
1293        }
1294    }
1295
1296    /// 清除柔化边缘。
1297    pub fn clear_soft_edge(&mut self) {
1298        if let Some(e) = self.effects.as_mut() {
1299            e.soft_edge = None;
1300        }
1301    }
1302
1303    /// 清除反射。
1304    pub fn clear_reflection(&mut self) {
1305        if let Some(e) = self.effects.as_mut() {
1306            e.reflection = None;
1307        }
1308    }
1309
1310    /// 清除所有效果(删除整个 `<a:effectLst>` 元素)。
1311    pub fn clear_effects(&mut self) {
1312        self.effects = None;
1313    }
1314}
1315
1316// ====================================================================
1317// 高阶 Fill / Line 视图(python-pptx 风格)
1318// ====================================================================
1319
1320use crate::oxml::color::ColorFormat;
1321
1322/// 填充高阶视图(`pptx.dml.fill.FillFormat`)。
1323///
1324/// # 与 python-pptx 的对应
1325///
1326/// - `pptx.dml.fill.FillFormat` ←→ [`FillFormat`];
1327/// - `shape.fill.solid()` + `shape.fill.fore_color.rgb = ...` ←→
1328///   `fill_format.solid().set_rgb(...)`。
1329///
1330/// # 设计要点
1331///
1332/// - **借用 + 透明代理**:构造时传入 `&mut Fill`;所有写都走底层;
1333/// - **零分配**:颜色写入走 [`ColorFormat`] 的借用;
1334/// - **类型安全**:通过 [`super::simpletypes::MsoFillType`] 表达"当前填充类型"。
1335#[derive(Debug)]
1336pub struct FillFormat<'a> {
1337    /// 底层 [`Fill`] 引用。
1338    fill: &'a mut Fill,
1339}
1340
1341impl<'a> FillFormat<'a> {
1342    /// 构造一个 fill 视图。
1343    pub fn new(fill: &'a mut Fill) -> Self {
1344        FillFormat { fill }
1345    }
1346
1347    /// 底层 fill 不可变引用。
1348    pub fn fill(&self) -> &Fill {
1349        self.fill
1350    }
1351    /// 底层 fill 可变引用。
1352    pub fn fill_mut(&mut self) -> &mut Fill {
1353        self.fill
1354    }
1355
1356    /// 当前填充类型(python-pptx `fill.type`)。
1357    pub fn fill_type(&self) -> super::simpletypes::MsoFillType {
1358        match self.fill {
1359            Fill::None => super::simpletypes::MsoFillType::Background,
1360            Fill::Solid(_) => super::simpletypes::MsoFillType::Solid,
1361            Fill::Blip { .. } => super::simpletypes::MsoFillType::Picture,
1362            Fill::Gradient(_) => super::simpletypes::MsoFillType::Gradient,
1363            Fill::Pattern(_) => super::simpletypes::MsoFillType::Pattern,
1364            Fill::Inherit => super::simpletypes::MsoFillType::Inherit,
1365        }
1366    }
1367
1368    /// 切到**实色**模式并返回 [`ColorFormat`] 代理。
1369    ///
1370    /// 对应 python-pptx 中 `fill.solid()` 后再 `fill.fore_color.rgb = ...`。
1371    /// 调用本方法会把 `Fill` 切到 `Solid(Color::None)`,后续 `set_rgb` /
1372    /// `set_theme` 等会原地更新 `Color`。
1373    pub fn solid(&mut self) -> ColorFormat<'_> {
1374        // 如果不是 Solid,先切到 Solid(None)
1375        if !matches!(self.fill, Fill::Solid(_)) {
1376            *self.fill = Fill::Solid(Color::None);
1377        }
1378        match self.fill {
1379            Fill::Solid(c) => ColorFormat::new(c),
1380            _ => unreachable!("just set to Solid"),
1381        }
1382    }
1383
1384    /// 切到**无填充**。
1385    ///
1386    /// 对应 python-pptx 中 `fill.background()`。但通常我们用 [`FillFormat::clear`] 更直白。
1387    pub fn set_none(&mut self) {
1388        *self.fill = Fill::None;
1389    }
1390
1391    /// 切到图片填充。
1392    ///
1393    /// # 参数
1394    /// - `rid`:图片关系 id(形如 `rIdImg1`)。
1395    /// - `mode`:填充模式(拉伸/平铺/无)。使用 [`BlipFillMode::Stretch`] 为默认拉伸。
1396    pub fn set_picture(&mut self, rid: impl Into<String>, mode: BlipFillMode) {
1397        *self.fill = Fill::Blip {
1398            rid: rid.into(),
1399            mode,
1400        };
1401    }
1402
1403    /// 重置(继承主题默认)。
1404    pub fn clear(&mut self) {
1405        *self.fill = Fill::Inherit;
1406    }
1407
1408    /// 便捷:直接设成 sRGB 实色。
1409    pub fn set_solid_rgb(&mut self, c: impl Into<crate::units::RGBColor>) {
1410        *self.fill = Fill::Solid(Color::RGB(c.into()));
1411    }
1412    /// 便捷:直接设成主题色实色。
1413    pub fn set_solid_theme(&mut self, t: super::simpletypes::MsoThemeColorIndex) {
1414        if let Some(s) = t.as_str() {
1415            if let Ok(sc) = s.parse::<crate::oxml::color::SchemeColor>() {
1416                *self.fill = Fill::Solid(Color::Scheme(sc));
1417            }
1418        }
1419    }
1420
1421    // ===== 渐变填充 API(TODO-009)=====
1422
1423    /// 切到**渐变**模式并返回 `&mut GradientFill` 供进一步配置。
1424    ///
1425    /// 对应 python-pptx 中 `fill.gradient()`。调用本方法会把 `Fill` 切到
1426    /// `Gradient(GradientFill { stops: vec![], gradient_type: Linear(0), .. })`,
1427    /// 调用方随后通过返回的引用添加光轨、设置角度等。
1428    ///
1429    /// # 示例
1430    /// ```ignore
1431    /// use pptx_rs::oxml::{Fill, GradientStop, GradientType};
1432    /// use pptx_rs::units::RGBColor;
1433    /// use pptx_rs::oxml::color::Color;
1434    ///
1435    /// let mut fill = Fill::Inherit;
1436    /// let mut fmt = FillFormat::new(&mut fill);
1437    /// let g = fmt.gradient();
1438    /// g.stops.push(GradientStop { pos: 0, color: Color::RGB(RGBColor::new(0xFF, 0x00, 0x00)) });
1439    /// g.stops.push(GradientStop { pos: 100000, color: Color::RGB(RGBColor::new(0x00, 0x00, 0xFF)) });
1440    /// g.gradient_type = GradientType::Linear(2_700_000); // 向下
1441    /// ```
1442    pub fn gradient(&mut self) -> &mut GradientFill {
1443        if !matches!(self.fill, Fill::Gradient(_)) {
1444            *self.fill = Fill::Gradient(GradientFill {
1445                stops: Vec::new(),
1446                gradient_type: GradientType::Linear(0),
1447                flip: None,
1448                rot_with_shape: None,
1449            });
1450        }
1451        match &mut self.fill {
1452            Fill::Gradient(g) => g,
1453            _ => unreachable!("just set to Gradient"),
1454        }
1455    }
1456
1457    /// 便捷:直接设成**线性渐变**。
1458    ///
1459    /// # 参数
1460    /// - `stops`:光轨列表(至少 2 个)。
1461    /// - `angle`:角度(1/60000 度,0 = 向右,5400000 = 向下)。
1462    pub fn set_gradient_linear(&mut self, stops: Vec<GradientStop>, angle: i32) {
1463        *self.fill = Fill::Gradient(GradientFill {
1464            stops,
1465            gradient_type: GradientType::Linear(angle),
1466            flip: None,
1467            rot_with_shape: None,
1468        });
1469    }
1470
1471    /// 便捷:直接设成**路径渐变**。
1472    ///
1473    /// # 参数
1474    /// - `stops`:光轨列表。
1475    /// - `path`:路径形状(`Circle` / `Rect` / `Shape`)。
1476    pub fn set_gradient_path(&mut self, stops: Vec<GradientStop>, path: GradientPath) {
1477        *self.fill = Fill::Gradient(GradientFill {
1478            stops,
1479            gradient_type: GradientType::Path(path),
1480            flip: None,
1481            rot_with_shape: None,
1482        });
1483    }
1484
1485    // ===== 图案填充 API(TODO-010)=====
1486
1487    /// 切到**图案**模式并返回 `&mut PatternFill` 供进一步配置。
1488    ///
1489    /// 调用本方法会把 `Fill` 切到 `Pattern(PatternFill::default())`,
1490    /// 调用方随后通过返回的引用设置 prst / fg_color / bg_color。
1491    pub fn pattern(&mut self) -> &mut PatternFill {
1492        if !matches!(self.fill, Fill::Pattern(_)) {
1493            *self.fill = Fill::Pattern(PatternFill {
1494                prst: String::new(),
1495                fg_color: Color::None,
1496                bg_color: Color::None,
1497            });
1498        }
1499        match &mut self.fill {
1500            Fill::Pattern(p) => p,
1501            _ => unreachable!("just set to Pattern"),
1502        }
1503    }
1504
1505    /// 便捷:直接设成图案填充。
1506    ///
1507    /// # 参数
1508    /// - `prst`:预置图案类型(如 `"pct5"` / `"horz"` / `"vert"` / `"cross"`)。
1509    /// - `fg`:前景色。
1510    /// - `bg`:背景色。
1511    pub fn set_pattern(&mut self, prst: impl Into<String>, fg: Color, bg: Color) {
1512        *self.fill = Fill::Pattern(PatternFill {
1513            prst: prst.into(),
1514            fg_color: fg,
1515            bg_color: bg,
1516        });
1517    }
1518}
1519
1520impl<'a> From<&'a mut Fill> for FillFormat<'a> {
1521    fn from(f: &'a mut Fill) -> Self {
1522        FillFormat::new(f)
1523    }
1524}
1525
1526/// 线框高阶视图(`pptx.dml.line.LineFormat`)。
1527///
1528/// # 与 python-pptx 的对应
1529///
1530/// - `pptx.dml.line.LineFormat` ←→ [`LineFormat`];
1531/// - `line.color.rgb = ...` / `line.width = Pt(1)` / `line.dash_style = MSO_LINE.DASH`
1532///   ←→ [`LineFormat::color`] / [`LineFormat::set_width`] / [`LineFormat::set_dash_style`]
1533#[derive(Debug)]
1534pub struct LineFormat<'a> {
1535    /// 底层 [`Line`] 引用。
1536    line: &'a mut Line,
1537}
1538
1539impl<'a> LineFormat<'a> {
1540    /// 构造一个 line 视图。
1541    pub fn new(line: &'a mut Line) -> Self {
1542        LineFormat { line }
1543    }
1544
1545    /// 底层 line 不可变引用。
1546    pub fn line(&self) -> &Line {
1547        self.line
1548    }
1549    /// 底层 line 可变引用。
1550    pub fn line_mut(&mut self) -> &mut Line {
1551        self.line
1552    }
1553
1554    /// 颜色 [`ColorFormat`] 代理。
1555    pub fn color(&mut self) -> ColorFormat<'_> {
1556        ColorFormat::new(&mut self.line.color)
1557    }
1558
1559    /// 读取宽度(EMU)。
1560    pub fn width(&self) -> Option<crate::units::Emu> {
1561        self.line.width
1562    }
1563    /// 设置宽度(EMU)。通常 `Pt(1.0).emu()` 即可。
1564    pub fn set_width(&mut self, w: crate::units::Emu) {
1565        self.line.width = Some(w);
1566    }
1567
1568    /// 读取线型。
1569    pub fn dash_style(&self) -> Option<Dash> {
1570        self.line.dash
1571    }
1572    /// 设置线型(用 [`super::simpletypes::MsoLineDashStyle`],自动转 [`Dash`])。
1573    pub fn set_dash_style(&mut self, s: super::simpletypes::MsoLineDashStyle) {
1574        self.line.dash = Some(s.into());
1575    }
1576
1577    /// 设为无填充(透明线框)。
1578    pub fn set_no_fill(&mut self) {
1579        self.line.no_fill = true;
1580        self.line.color = crate::oxml::color::Color::None;
1581    }
1582
1583    /// 便捷:设宽度为磅值。
1584    pub fn set_width_pt(&mut self, pt: crate::units::Pt) {
1585        self.line.width = Some(crate::units::Emu((pt.value() * 12_700.0) as i64));
1586    }
1587}
1588
1589impl<'a> From<&'a mut Line> for LineFormat<'a> {
1590    fn from(l: &'a mut Line) -> Self {
1591        LineFormat::new(l)
1592    }
1593}
1594
1595// ===================== 三维效果(TODO-050) =====================
1596//
1597// OOXML 中 `<a:scene3d>` / `<a:sp3d>` 位于 `<p:spPr>` 的 effectLst 之后、extLst 之前。
1598// - scene3d 定义相机与光照(场景级 3D)
1599// - sp3d 定义形状本身的 3D 拉伸/棱台/材质(形状级 3D)
1600//
1601// 典型 XML 结构(来自 Office 默认 fmtScheme 第三个 effectStyle):
1602// ```xml
1603// <a:scene3d>
1604//   <a:camera prst="orthographicFront">
1605//     <a:rot lat="0" lon="0" rev="0"/>
1606//   </a:camera>
1607//   <a:lightRig rig="threePt" dir="t">
1608//     <a:rot lat="0" lon="0" rev="1200000"/>
1609//   </a:lightRig>
1610// </a:scene3d>
1611// <a:sp3d>
1612//   <a:bevelT w="63500" h="25400"/>
1613// </a:sp3d>
1614// ```
1615//
1616// 所有角度单位为"60000 分之一度"(OOXML ST_Angle),与 Transform::rot 一致。
1617
1618/// 三维旋转(`<a:rot>`,OOXML CT_SphereCoords)。
1619///
1620/// 三个角度均以 1/60000 度为单位:
1621/// - `lat`:纬度(-90°~90°,即 -5400000~5400000)
1622/// - `lon`:经度(0°~360°,即 0~21600000)
1623/// - `rev`:滚转(沿视线轴的旋转)
1624#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
1625pub struct Rotation3d {
1626    /// 纬度(1/60000 度)。
1627    pub lat: i32,
1628    /// 经度(1/60000 度)。
1629    pub lon: i32,
1630    /// 滚转(1/60000 度)。
1631    pub rev: i32,
1632}
1633
1634impl Rotation3d {
1635    /// 写出 `<a:rot lat="..." lon="..." rev="..."/>`(自闭合)。
1636    pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
1637        let lat = self.lat.to_string();
1638        let lon = self.lon.to_string();
1639        let rev = self.rev.to_string();
1640        w.empty_with(
1641            "a:rot",
1642            &[
1643                ("lat", lat.as_str()),
1644                ("lon", lon.as_str()),
1645                ("rev", rev.as_str()),
1646            ],
1647        );
1648    }
1649}
1650
1651/// 相机预设类型(OOXML ST_PresetCameraType,常用子集)。
1652///
1653/// 完整列表参见 ECMA-376 Part 1 §20.1.10.13。
1654#[derive(Clone, Debug, Default, PartialEq, Eq)]
1655pub enum CameraPreset {
1656    /// `orthographicFront`(默认,正交前视图)。
1657    #[default]
1658    OrthographicFront,
1659    /// `isometricOffAxis1` ~ `isometricOffAxis4`:等轴侧视图。
1660    IsometricOffAxis1,
1661    IsometricOffAxis2,
1662    IsometricOffAxis3,
1663    IsometricOffAxis4,
1664    /// `isometricLeftDown` / `isometricLeftUp` / `isometricRightDown` / `isometricRightUp`。
1665    IsometricLeftDown,
1666    IsometricLeftUp,
1667    IsometricRightDown,
1668    IsometricRightUp,
1669    /// `perspectiveFront`:透视前视图。
1670    PerspectiveFront,
1671    /// `perspectiveLeft` / `perspectiveRight`:透视侧视图。
1672    PerspectiveLeft,
1673    PerspectiveRight,
1674    /// 其它未显式枚举的预设(保留原始字符串)。
1675    Other(String),
1676}
1677
1678impl CameraPreset {
1679    /// 转 OOXML 字符串。
1680    pub fn as_str(&self) -> &str {
1681        match self {
1682            CameraPreset::OrthographicFront => "orthographicFront",
1683            CameraPreset::IsometricOffAxis1 => "isometricOffAxis1",
1684            CameraPreset::IsometricOffAxis2 => "isometricOffAxis2",
1685            CameraPreset::IsometricOffAxis3 => "isometricOffAxis3",
1686            CameraPreset::IsometricOffAxis4 => "isometricOffAxis4",
1687            CameraPreset::IsometricLeftDown => "isometricLeftDown",
1688            CameraPreset::IsometricLeftUp => "isometricLeftUp",
1689            CameraPreset::IsometricRightDown => "isometricRightDown",
1690            CameraPreset::IsometricRightUp => "isometricRightUp",
1691            CameraPreset::PerspectiveFront => "perspectiveFront",
1692            CameraPreset::PerspectiveLeft => "perspectiveLeft",
1693            CameraPreset::PerspectiveRight => "perspectiveRight",
1694            CameraPreset::Other(s) => s.as_str(),
1695        }
1696    }
1697
1698    /// 从字符串解析(不区分大小写、未识别则归入 `Other`)。
1699    ///
1700    /// 注:方法名为 `parse` 而非 `from_str`,以避免与 `std::str::FromStr` trait 冲突。
1701    pub fn parse(s: &str) -> Self {
1702        match s {
1703            "orthographicFront" => CameraPreset::OrthographicFront,
1704            "isometricOffAxis1" => CameraPreset::IsometricOffAxis1,
1705            "isometricOffAxis2" => CameraPreset::IsometricOffAxis2,
1706            "isometricOffAxis3" => CameraPreset::IsometricOffAxis3,
1707            "isometricOffAxis4" => CameraPreset::IsometricOffAxis4,
1708            "isometricLeftDown" => CameraPreset::IsometricLeftDown,
1709            "isometricLeftUp" => CameraPreset::IsometricLeftUp,
1710            "isometricRightDown" => CameraPreset::IsometricRightDown,
1711            "isometricRightUp" => CameraPreset::IsometricRightUp,
1712            "perspectiveFront" => CameraPreset::PerspectiveFront,
1713            "perspectiveLeft" => CameraPreset::PerspectiveLeft,
1714            "perspectiveRight" => CameraPreset::PerspectiveRight,
1715            other => CameraPreset::Other(other.to_string()),
1716        }
1717    }
1718}
1719
1720/// 相机(`<a:camera>`,OOXML CT_Camera)。
1721///
1722/// 定义观察形状的虚拟相机:预设类型、视野(FOV)、缩放、可选旋转。
1723#[derive(Clone, Debug, Default, PartialEq, Eq)]
1724pub struct Camera {
1725    /// 相机预设类型(默认 `orthographicFront`)。
1726    pub preset: CameraPreset,
1727    /// 视野角度(1/60000 度,0 表示使用预设默认)。
1728    pub fov: i32,
1729    /// 缩放(百分比 * 1000,100000 = 100%)。
1730    pub zoom: i32,
1731    /// 可选旋转(覆盖预设默认视角)。
1732    pub rotation: Option<Rotation3d>,
1733}
1734
1735impl Camera {
1736    /// 写出 `<a:camera prst="..." fov="..." zoom="...">...</a:camera>`。
1737    pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
1738        let prst = self.preset.as_str();
1739        let mut attrs: Vec<(&str, &str)> = vec![("prst", prst)];
1740        let fov_s;
1741        if self.fov != 0 {
1742            fov_s = self.fov.to_string();
1743            attrs.push(("fov", fov_s.as_str()));
1744        }
1745        let zoom_s;
1746        if self.zoom != 0 && self.zoom != 100000 {
1747            zoom_s = self.zoom.to_string();
1748            attrs.push(("zoom", zoom_s.as_str()));
1749        }
1750        if let Some(rot) = &self.rotation {
1751            w.open_with("a:camera", &attrs);
1752            rot.write_xml(w);
1753            w.close("a:camera");
1754        } else {
1755            w.empty_with("a:camera", &attrs);
1756        }
1757    }
1758}
1759
1760/// 光照设备预设类型(OOXML ST_LightRigType,常用子集)。
1761#[derive(Clone, Debug, Default, PartialEq, Eq)]
1762pub enum LightRigType {
1763    /// `balanced`(平衡光,默认)。
1764    #[default]
1765    Balanced,
1766    /// `bright`:明亮。
1767    Bright,
1768    /// `chilly`:冷光。
1769    Chilly,
1770    /// `contrasting`:对比光。
1771    Contrasting,
1772    /// `flat`:平光。
1773    Flat,
1774    /// `harsh`:强光。
1775    Harsh,
1776    /// `morning`:晨光。
1777    Morning,
1778    /// `soft`:柔光。
1779    Soft,
1780    /// `sunrise`:日出光。
1781    Sunrise,
1782    /// `sunset`:日落光。
1783    Sunset,
1784    /// `threePt`:三点光(Office 默认 effectStyle 中使用)。
1785    ThreePt,
1786    /// `twoPt`:两点光。
1787    TwoPt,
1788    /// 其它未显式枚举的光照类型(保留原始字符串)。
1789    Other(String),
1790}
1791
1792impl LightRigType {
1793    /// 转 OOXML 字符串。
1794    pub fn as_str(&self) -> &str {
1795        match self {
1796            LightRigType::Balanced => "balanced",
1797            LightRigType::Bright => "bright",
1798            LightRigType::Chilly => "chilly",
1799            LightRigType::Contrasting => "contrasting",
1800            LightRigType::Flat => "flat",
1801            LightRigType::Harsh => "harsh",
1802            LightRigType::Morning => "morning",
1803            LightRigType::Soft => "soft",
1804            LightRigType::Sunrise => "sunrise",
1805            LightRigType::Sunset => "sunset",
1806            LightRigType::ThreePt => "threePt",
1807            LightRigType::TwoPt => "twoPt",
1808            LightRigType::Other(s) => s.as_str(),
1809        }
1810    }
1811
1812    /// 从字符串解析。
1813    ///
1814    /// 注:方法名为 `parse` 而非 `from_str`,以避免与 `std::str::FromStr` trait 冲突。
1815    pub fn parse(s: &str) -> Self {
1816        match s {
1817            "balanced" => LightRigType::Balanced,
1818            "bright" => LightRigType::Bright,
1819            "chilly" => LightRigType::Chilly,
1820            "contrasting" => LightRigType::Contrasting,
1821            "flat" => LightRigType::Flat,
1822            "harsh" => LightRigType::Harsh,
1823            "morning" => LightRigType::Morning,
1824            "soft" => LightRigType::Soft,
1825            "sunrise" => LightRigType::Sunrise,
1826            "sunset" => LightRigType::Sunset,
1827            "threePt" => LightRigType::ThreePt,
1828            "twoPt" => LightRigType::TwoPt,
1829            other => LightRigType::Other(other.to_string()),
1830        }
1831    }
1832}
1833
1834/// 光照方向(OOXML ST_LightRigDirection)。
1835#[derive(Clone, Debug, Default, PartialEq, Eq)]
1836pub enum LightRigDirection {
1837    /// `tl`:左上。
1838    TopLeft,
1839    /// `t`:上(默认,Office effectStyle 中使用)。
1840    #[default]
1841    Top,
1842    /// `tr`:右上。
1843    TopRight,
1844    /// `l`:左。
1845    Left,
1846    /// `r`:右。
1847    Right,
1848    /// `bl`:左下。
1849    BottomLeft,
1850    /// `b`:下。
1851    Bottom,
1852    /// `br`:右下。
1853    BottomRight,
1854}
1855
1856impl LightRigDirection {
1857    /// 转 OOXML 字符串。
1858    pub fn as_str(&self) -> &str {
1859        match self {
1860            LightRigDirection::TopLeft => "tl",
1861            LightRigDirection::Top => "t",
1862            LightRigDirection::TopRight => "tr",
1863            LightRigDirection::Left => "l",
1864            LightRigDirection::Right => "r",
1865            LightRigDirection::BottomLeft => "bl",
1866            LightRigDirection::Bottom => "b",
1867            LightRigDirection::BottomRight => "br",
1868        }
1869    }
1870
1871    /// 从字符串解析。
1872    ///
1873    /// 注:方法名为 `parse` 而非 `from_str`,以避免与 `std::str::FromStr` trait 冲突。
1874    pub fn parse(s: &str) -> Self {
1875        match s {
1876            "tl" => LightRigDirection::TopLeft,
1877            "t" => LightRigDirection::Top,
1878            "tr" => LightRigDirection::TopRight,
1879            "l" => LightRigDirection::Left,
1880            "r" => LightRigDirection::Right,
1881            "bl" => LightRigDirection::BottomLeft,
1882            "b" => LightRigDirection::Bottom,
1883            "br" => LightRigDirection::BottomRight,
1884            _ => LightRigDirection::Top,
1885        }
1886    }
1887}
1888
1889/// 光照设备(`<a:lightRig>`,OOXML CT_LightRig)。
1890#[derive(Clone, Debug, Default, PartialEq, Eq)]
1891pub struct LightRig {
1892    /// 光照类型(默认 `balanced`)。
1893    pub rig: LightRigType,
1894    /// 光照方向(默认 `t`)。
1895    pub dir: LightRigDirection,
1896    /// 可选旋转(覆盖预设默认)。
1897    pub rotation: Option<Rotation3d>,
1898}
1899
1900impl LightRig {
1901    /// 写出 `<a:lightRig rig="..." dir="...">...</a:lightRig>`。
1902    pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
1903        let rig = self.rig.as_str();
1904        let dir = self.dir.as_str();
1905        if let Some(rot) = &self.rotation {
1906            w.open_with("a:lightRig", &[("rig", rig), ("dir", dir)]);
1907            rot.write_xml(w);
1908            w.close("a:lightRig");
1909        } else {
1910            w.empty_with("a:lightRig", &[("rig", rig), ("dir", dir)]);
1911        }
1912    }
1913}
1914
1915/// 三维场景(`<a:scene3d>`,OOXML CT_Scene3D)。
1916///
1917/// 包含相机与光照,定义观察形状的虚拟 3D 环境。
1918#[derive(Clone, Debug, Default, PartialEq, Eq)]
1919pub struct Scene3d {
1920    /// 相机(必填,默认 `orthographicFront`)。
1921    pub camera: Camera,
1922    /// 光照设备(必填,默认 `balanced` + `t`)。
1923    pub light_rig: LightRig,
1924    /// 三维场景背景(`<a:backdrop>`,可选,TODO-050)。
1925    ///
1926    /// 定义 6 个背景平面(地板/墙壁/左/右/顶/底),启用的平面会渲染为可见的背景面。
1927    /// `None` 表示不写出 `<a:backdrop>` 元素(与 Office 默认行为一致)。
1928    pub backdrop: Option<Backdrop>,
1929}
1930
1931impl Scene3d {
1932    /// 写出 `<a:scene3d>...</a:scene3d>`。
1933    ///
1934    /// # 元素顺序(OOXML CT_Scene3D)
1935    ///
1936    /// ```text
1937    /// <a:scene3d>
1938    ///   <a:camera>...</a:camera>        ← 必填
1939    ///   <a:lightRig>...</a:lightRig>    ← 必填
1940    ///   <a:backdrop>...</a:backdrop>    ← 可选(TODO-050)
1941    /// </a:scene3d>
1942    /// ```
1943    pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
1944        w.open("a:scene3d");
1945        self.camera.write_xml(w);
1946        self.light_rig.write_xml(w);
1947        if let Some(b) = &self.backdrop {
1948            b.write_xml(w);
1949        }
1950        w.close("a:scene3d");
1951    }
1952}
1953
1954/// 三维点(`<a:anchor>`,OOXML CT_Point3D)。
1955///
1956/// 用于 [`Backdrop`] 的锚点位置。三个坐标均以 EMU 为单位。
1957#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
1958pub struct Point3d {
1959    /// X 坐标(EMU)。
1960    pub x: i32,
1961    /// Y 坐标(EMU)。
1962    pub y: i32,
1963    /// Z 坐标(EMU)。
1964    pub z: i32,
1965}
1966
1967impl Point3d {
1968    /// 写出 `<a:anchor x="..." y="..." z="..."/>`(自闭合)。
1969    pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
1970        let xs = self.x.to_string();
1971        let ys = self.y.to_string();
1972        let zs = self.z.to_string();
1973        w.empty_with(
1974            "a:anchor",
1975            &[("x", xs.as_str()), ("y", ys.as_str()), ("z", zs.as_str())],
1976        );
1977    }
1978}
1979
1980/// 三维场景背景(`<a:backdrop>`,OOXML CT_Backdrop,TODO-050)。
1981///
1982/// 定义 3D 场景中的 6 个背景平面,每个平面可独立启用/禁用。
1983/// 启用的平面会渲染为可见的背景面(如地板/墙壁),用于营造空间感。
1984///
1985/// # OOXML 元素顺序
1986///
1987/// ```text
1988/// <a:backdrop>
1989///   <a:anchor x="..." y="..." z="..."/>   ← 可选:锚点位置
1990///   <a:floor/>                            ← 可选:地板平面
1991///   <a:wall/>                             ← 可选:后墙平面
1992///   <a:l/>                                ← 可选:左平面
1993///   <a:r/>                                ← 可选:右平面
1994///   <a:t/>                                ← 可选:顶平面
1995///   <a:b/>                                ← 可选:底平面
1996/// </a:backdrop>
1997/// ```
1998///
1999/// # 与 python-pptx 的对应
2000///
2001/// python-pptx 不支持 backdrop 编辑;本结构是 pptx-rs 的扩展能力。
2002#[derive(Clone, Debug, Default, PartialEq, Eq)]
2003pub struct Backdrop {
2004    /// 锚点位置(`<a:anchor>`)。
2005    ///
2006    /// 定义背景平面集合的参考原点(EMU 单位)。`None` 表示不写出 `<a:anchor>`。
2007    pub anchor: Option<Point3d>,
2008    /// 是否启用地板平面(`<a:floor/>`)。
2009    pub floor: bool,
2010    /// 是否启用后墙平面(`<a:wall/>`)。
2011    pub wall: bool,
2012    /// 是否启用左平面(`<a:l/>`)。
2013    pub left: bool,
2014    /// 是否启用右平面(`<a:r/>`)。
2015    pub right: bool,
2016    /// 是否启用顶平面(`<a:t/>`)。
2017    pub top: bool,
2018    /// 是否启用底平面(`<a:b/>`)。
2019    pub bottom: bool,
2020}
2021
2022impl Backdrop {
2023    /// 写出 `<a:backdrop>...</a:backdrop>`。
2024    ///
2025    /// 仅写出启用(`true`)的平面元素,未启用的平面不写出。
2026    pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
2027        w.open("a:backdrop");
2028        if let Some(a) = &self.anchor {
2029            a.write_xml(w);
2030        }
2031        if self.floor {
2032            w.empty("a:floor");
2033        }
2034        if self.wall {
2035            w.empty("a:wall");
2036        }
2037        if self.left {
2038            w.empty("a:l");
2039        }
2040        if self.right {
2041            w.empty("a:r");
2042        }
2043        if self.top {
2044            w.empty("a:t");
2045        }
2046        if self.bottom {
2047            w.empty("a:b");
2048        }
2049        w.close("a:backdrop");
2050    }
2051}
2052
2053/// 棱台(`<a:bevelT>` / `<a:bevelB>`,OOXML CT_Bevel)。
2054///
2055/// 宽高均以 EMU 为单位(典型值 63500 = 5pt)。
2056#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
2057pub struct Bevel {
2058    /// 棱台宽度(EMU)。
2059    pub w: i32,
2060    /// 棱台高度(EMU)。
2061    pub h: i32,
2062}
2063
2064impl Bevel {
2065    /// 写出 `<a:bevelT w="..." h="..."/>`(自闭合,tag 由调用方指定)。
2066    pub fn write_xml(&self, w: &mut super::writer::XmlWriter, tag: &str) {
2067        let w_s = self.w.to_string();
2068        let h_s = self.h.to_string();
2069        w.empty_with(tag, &[("w", w_s.as_str()), ("h", h_s.as_str())]);
2070    }
2071}
2072
2073/// 形状材质预设(OOXML ST_PresetMaterialType,常用子集)。
2074#[derive(Clone, Debug, Default, PartialEq, Eq)]
2075pub enum MaterialPreset {
2076    /// `warmMatte`(暖色哑光,默认)。
2077    #[default]
2078    WarmMatte,
2079    /// `clear`:透明。
2080    Clear,
2081    /// `dkEdge`:暗边缘。
2082    DarkEdge,
2083    /// `flat`:平面。
2084    Flat,
2085    /// `legacyMatte`:旧版哑光。
2086    LegacyMatte,
2087    /// `legacyMetallic`:旧版金属。
2088    LegacyMetallic,
2089    /// `legacyPlastic`:旧版塑料。
2090    LegacyPlastic,
2091    /// `legacyWireframe`:旧版线框。
2092    LegacyWireframe,
2093    /// `matte`:哑光。
2094    Matte,
2095    /// `metallic`:金属。
2096    Metallic,
2097    /// `plastic`:塑料。
2098    Plastic,
2099    /// `powder`:粉末。
2100    Powder,
2101    /// `softEdge`:柔边。
2102    SoftEdge,
2103    /// `softmetal`:软金属。
2104    SoftMetal,
2105    /// 其它未显式枚举的材质(保留原始字符串)。
2106    Other(String),
2107}
2108
2109impl MaterialPreset {
2110    /// 转 OOXML 字符串。
2111    pub fn as_str(&self) -> &str {
2112        match self {
2113            MaterialPreset::WarmMatte => "warmMatte",
2114            MaterialPreset::Clear => "clear",
2115            MaterialPreset::DarkEdge => "dkEdge",
2116            MaterialPreset::Flat => "flat",
2117            MaterialPreset::LegacyMatte => "legacyMatte",
2118            MaterialPreset::LegacyMetallic => "legacyMetallic",
2119            MaterialPreset::LegacyPlastic => "legacyPlastic",
2120            MaterialPreset::LegacyWireframe => "legacyWireframe",
2121            MaterialPreset::Matte => "matte",
2122            MaterialPreset::Metallic => "metallic",
2123            MaterialPreset::Plastic => "plastic",
2124            MaterialPreset::Powder => "powder",
2125            MaterialPreset::SoftEdge => "softEdge",
2126            MaterialPreset::SoftMetal => "softmetal",
2127            MaterialPreset::Other(s) => s.as_str(),
2128        }
2129    }
2130
2131    /// 从字符串解析。
2132    ///
2133    /// 注:方法名为 `parse` 而非 `from_str`,以避免与 `std::str::FromStr` trait 冲突。
2134    pub fn parse(s: &str) -> Self {
2135        match s {
2136            "warmMatte" => MaterialPreset::WarmMatte,
2137            "clear" => MaterialPreset::Clear,
2138            "dkEdge" => MaterialPreset::DarkEdge,
2139            "flat" => MaterialPreset::Flat,
2140            "legacyMatte" => MaterialPreset::LegacyMatte,
2141            "legacyMetallic" => MaterialPreset::LegacyMetallic,
2142            "legacyPlastic" => MaterialPreset::LegacyPlastic,
2143            "legacyWireframe" => MaterialPreset::LegacyWireframe,
2144            "matte" => MaterialPreset::Matte,
2145            "metallic" => MaterialPreset::Metallic,
2146            "plastic" => MaterialPreset::Plastic,
2147            "powder" => MaterialPreset::Powder,
2148            "softEdge" => MaterialPreset::SoftEdge,
2149            "softmetal" => MaterialPreset::SoftMetal,
2150            other => MaterialPreset::Other(other.to_string()),
2151        }
2152    }
2153}
2154
2155/// 形状 3D 属性(`<a:sp3d>`,OOXML CT_Shape3D)。
2156///
2157/// 定义形状本身的 3D 拉伸、棱台、材质。
2158#[derive(Clone, Debug, Default, PartialEq, Eq)]
2159pub struct Sp3d {
2160    /// 拉伸高度(EMU,典型值 38100 = 3pt)。
2161    pub extrusion_h: i32,
2162    /// 轮廓宽度(EMU)。
2163    pub contour_w: i32,
2164    /// 材质预设(默认 `warmMatte`)。
2165    pub prst_material: MaterialPreset,
2166    /// 顶部棱台(`<a:bevelT>`)。
2167    pub bevel_top: Option<Bevel>,
2168    /// 底部棱台(`<a:bevelB>`)。
2169    pub bevel_bottom: Option<Bevel>,
2170    /// 拉伸颜色(`<a:extrusionClr>`,`None` 表示不写出)。
2171    pub extrusion_color: Option<Color>,
2172    /// 轮廓颜色(`<a:contourClr>`,`None` 表示不写出)。
2173    pub contour_color: Option<Color>,
2174}
2175
2176impl Sp3d {
2177    /// 写出 `<a:sp3d>...</a:sp3d>`。
2178    pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
2179        let mut attrs: Vec<(&str, &str)> = Vec::new();
2180        let eh_s;
2181        if self.extrusion_h != 0 {
2182            eh_s = self.extrusion_h.to_string();
2183            attrs.push(("extrusionH", eh_s.as_str()));
2184        }
2185        let cw_s;
2186        if self.contour_w != 0 {
2187            cw_s = self.contour_w.to_string();
2188            attrs.push(("contourW", cw_s.as_str()));
2189        }
2190        // prstMaterial 仅在非默认时输出(避免覆盖默认值)
2191        let pm_s;
2192        if !matches!(self.prst_material, MaterialPreset::WarmMatte) {
2193            pm_s = self.prst_material.as_str().to_string();
2194            attrs.push(("prstMaterial", pm_s.as_str()));
2195        }
2196        let has_children = self.bevel_top.is_some()
2197            || self.bevel_bottom.is_some()
2198            || self.extrusion_color.is_some()
2199            || self.contour_color.is_some();
2200        if !has_children && attrs.is_empty() {
2201            w.empty("a:sp3d");
2202            return;
2203        }
2204        if !has_children {
2205            w.empty_with("a:sp3d", &attrs);
2206            return;
2207        }
2208        w.open_with("a:sp3d", &attrs);
2209        if let Some(b) = &self.bevel_top {
2210            b.write_xml(w, "a:bevelT");
2211        }
2212        if let Some(b) = &self.bevel_bottom {
2213            b.write_xml(w, "a:bevelB");
2214        }
2215        if let Some(c) = &self.extrusion_color {
2216            w.open("a:extrusionClr");
2217            c.write_solid_fill(w);
2218            w.close("a:extrusionClr");
2219        }
2220        if let Some(c) = &self.contour_color {
2221            w.open("a:contourClr");
2222            c.write_solid_fill(w);
2223            w.close("a:contourClr");
2224        }
2225        w.close("a:sp3d");
2226    }
2227}
2228
2229#[cfg(test)]
2230mod tests {
2231    use super::*;
2232    use crate::oxml::color::Color;
2233    use crate::oxml::writer::XmlWriter;
2234    use crate::units::RGBColor;
2235
2236    // --------------------- 调整值测试(TODO-038) ---------------------
2237
2238    /// `AdjustmentValue::new` 正确设置字段。
2239    #[test]
2240    fn adjustment_value_new() {
2241        let av = AdjustmentValue::new("adj", 16667);
2242        assert_eq!(av.name, "adj");
2243        assert_eq!(av.raw_value, 16667);
2244    }
2245
2246    /// `AdjustmentValue::from_normalized` 正确转换归一化值。
2247    #[test]
2248    fn adjustment_value_from_normalized() {
2249        let av = AdjustmentValue::from_normalized("adj", 0.5);
2250        assert_eq!(av.raw_value, 50000);
2251        let av2 = AdjustmentValue::from_normalized("adj1", 0.25);
2252        assert_eq!(av2.raw_value, 25000);
2253    }
2254
2255    /// `AdjustmentValue::effective_value` 正确返回归一化值。
2256    #[test]
2257    fn adjustment_value_effective_value() {
2258        let av = AdjustmentValue::new("adj", 16667);
2259        assert!((av.effective_value() - 0.16667).abs() < 0.0001);
2260    }
2261
2262    /// `AdjustmentValue::write_xml` 正确序列化 `<a:gd>` 元素。
2263    #[test]
2264    fn adjustment_value_write_xml() {
2265        let av = AdjustmentValue::new("adj", 16667);
2266        let mut w = XmlWriter::new();
2267        av.write_xml(&mut w);
2268        let xml = &w.buf;
2269        assert!(xml.contains("<a:gd"), "xml: {}", xml);
2270        assert!(xml.contains(r#"name="adj""#), "xml: {}", xml);
2271        assert!(xml.contains(r#"fmla="val 16667""#), "xml: {}", xml);
2272    }
2273
2274    /// `Geometry::Preset` 带调整值正确序列化 `<a:avLst>`。
2275    #[test]
2276    fn geometry_preset_with_adjustments_write_xml() {
2277        let geom = Geometry::Preset(
2278            PresetGeometry::RoundRectangle,
2279            vec![AdjustmentValue::new("adj", 16667)],
2280        );
2281        let mut w = XmlWriter::new();
2282        geom.write_xml(&mut w);
2283        let xml = &w.buf;
2284        assert!(xml.contains(r#"prst="roundRect""#), "xml: {}", xml);
2285        assert!(xml.contains("<a:avLst>"), "xml: {}", xml);
2286        assert!(xml.contains(r#"fmla="val 16667""#), "xml: {}", xml);
2287        assert!(xml.contains("</a:avLst>"), "xml: {}", xml);
2288    }
2289
2290    /// `Geometry::Preset` 无调整值时写出空 `<a:avLst/>`。
2291    #[test]
2292    fn geometry_preset_no_adjustments_write_xml() {
2293        let geom = Geometry::preset(PresetGeometry::Rectangle);
2294        let mut w = XmlWriter::new();
2295        geom.write_xml(&mut w);
2296        assert!(w.buf.contains("<a:avLst/>"), "xml: {}", w.buf);
2297    }
2298
2299    /// `Geometry::preset` 辅助方法创建无调整值的预设几何。
2300    #[test]
2301    fn geometry_preset_helper() {
2302        let geom = Geometry::preset(PresetGeometry::Ellipse);
2303        match geom {
2304            Geometry::Preset(p, adj) => {
2305                assert_eq!(p, PresetGeometry::Ellipse);
2306                assert!(adj.is_empty());
2307            }
2308            _ => panic!("应为 Preset 变体"),
2309        }
2310    }
2311
2312    // --------------------- 其他测试 ---------------------
2313
2314    /// 验证 `FillFormat::gradient()` 切到渐变模式并返回可变引用。
2315    #[test]
2316    fn fill_format_gradient_switches_mode() {
2317        let mut fill = Fill::Inherit;
2318        let mut fmt = FillFormat::new(&mut fill);
2319        let g = fmt.gradient();
2320        g.stops.push(GradientStop {
2321            pos: 0,
2322            color: Color::RGB(RGBColor(0xFF, 0x00, 0x00)),
2323        });
2324        g.stops.push(GradientStop {
2325            pos: 100_000,
2326            color: Color::RGB(RGBColor(0x00, 0x00, 0xFF)),
2327        });
2328        g.gradient_type = GradientType::Linear(2_700_000);
2329        assert!(matches!(fill, Fill::Gradient(_)));
2330        if let Fill::Gradient(g) = &fill {
2331            assert_eq!(g.stops.len(), 2);
2332            assert_eq!(g.stops[0].pos, 0);
2333            assert_eq!(g.stops[1].pos, 100_000);
2334            assert!(matches!(g.gradient_type, GradientType::Linear(2_700_000)));
2335        }
2336    }
2337
2338    /// 验证 `FillFormat::set_gradient_linear` 便捷方法。
2339    #[test]
2340    fn fill_format_set_gradient_linear() {
2341        let mut fill = Fill::Inherit;
2342        let mut fmt = FillFormat::new(&mut fill);
2343        let stops = vec![
2344            GradientStop {
2345                pos: 0,
2346                color: Color::RGB(RGBColor(0xFF, 0x00, 0x00)),
2347            },
2348            GradientStop {
2349                pos: 100_000,
2350                color: Color::RGB(RGBColor(0x00, 0xFF, 0x00)),
2351            },
2352        ];
2353        fmt.set_gradient_linear(stops, 5_400_000);
2354        assert!(matches!(fill, Fill::Gradient(_)));
2355        if let Fill::Gradient(g) = &fill {
2356            assert_eq!(g.stops.len(), 2);
2357            assert!(matches!(g.gradient_type, GradientType::Linear(5_400_000)));
2358        }
2359    }
2360
2361    /// 验证 `FillFormat::set_gradient_path` 便捷方法。
2362    #[test]
2363    fn fill_format_set_gradient_path() {
2364        let mut fill = Fill::Inherit;
2365        let mut fmt = FillFormat::new(&mut fill);
2366        let stops = vec![GradientStop {
2367            pos: 50_000,
2368            color: Color::RGB(RGBColor(0x80, 0x80, 0x80)),
2369        }];
2370        fmt.set_gradient_path(stops, GradientPath::Circle);
2371        if let Fill::Gradient(g) = &fill {
2372            assert!(matches!(
2373                g.gradient_type,
2374                GradientType::Path(GradientPath::Circle)
2375            ));
2376        }
2377    }
2378
2379    /// 验证 `FillFormat::pattern()` 切到图案模式并返回可变引用。
2380    #[test]
2381    fn fill_format_pattern_switches_mode() {
2382        let mut fill = Fill::Inherit;
2383        let mut fmt = FillFormat::new(&mut fill);
2384        let p = fmt.pattern();
2385        p.prst = "horz".to_string();
2386        p.fg_color = Color::RGB(RGBColor(0xFF, 0x00, 0x00));
2387        p.bg_color = Color::RGB(RGBColor(0xFF, 0xFF, 0xFF));
2388        assert!(matches!(fill, Fill::Pattern(_)));
2389        if let Fill::Pattern(p) = &fill {
2390            assert_eq!(p.prst, "horz");
2391        }
2392    }
2393
2394    /// 验证 `FillFormat::set_pattern` 便捷方法。
2395    #[test]
2396    fn fill_format_set_pattern() {
2397        let mut fill = Fill::Inherit;
2398        let mut fmt = FillFormat::new(&mut fill);
2399        fmt.set_pattern(
2400            "cross",
2401            Color::RGB(RGBColor(0x00, 0x00, 0x00)),
2402            Color::RGB(RGBColor(0xFF, 0xFF, 0xFF)),
2403        );
2404        if let Fill::Pattern(p) = &fill {
2405            assert_eq!(p.prst, "cross");
2406            assert!(matches!(p.fg_color, Color::RGB(_)));
2407            assert!(matches!(p.bg_color, Color::RGB(_)));
2408        } else {
2409            panic!("应为 Pattern 变体");
2410        }
2411    }
2412
2413    /// 验证 `fill_type()` 对渐变和图案返回正确的枚举值。
2414    #[test]
2415    fn fill_format_fill_type_for_gradient_and_pattern() {
2416        let mut fill = Fill::Gradient(GradientFill {
2417            stops: vec![],
2418            gradient_type: GradientType::Linear(0),
2419            flip: None,
2420            rot_with_shape: None,
2421        });
2422        let fmt = FillFormat::new(&mut fill);
2423        assert_eq!(
2424            fmt.fill_type(),
2425            crate::oxml::simpletypes::MsoFillType::Gradient
2426        );
2427
2428        let mut fill = Fill::Pattern(PatternFill::default());
2429        let fmt = FillFormat::new(&mut fill);
2430        assert_eq!(
2431            fmt.fill_type(),
2432            crate::oxml::simpletypes::MsoFillType::Pattern
2433        );
2434    }
2435
2436    /// 验证 `EffectList::is_empty` 和 `write_xml`(外阴影)。
2437    #[test]
2438    fn effect_list_outer_shadow_serialization() {
2439        let mut effects = EffectList::default();
2440        assert!(effects.is_empty());
2441        effects.outer_shadow = Some(ShadowEffect {
2442            dir: 2_700_000,
2443            dist: 38100,
2444            blur_rad: 40000,
2445            color: Color::RGB(RGBColor(0x00, 0x00, 0x00)),
2446            rot_with_shape: Some(false),
2447        });
2448        assert!(!effects.is_empty());
2449
2450        let mut w = XmlWriter::new();
2451        effects.write_xml(&mut w);
2452        let buf = &w.buf;
2453        assert!(buf.contains("<a:effectLst>"));
2454        assert!(buf.contains("<a:outerShdw"));
2455        assert!(buf.contains("dir=\"2700000\""));
2456        assert!(buf.contains("dist=\"38100\""));
2457        assert!(buf.contains("blurRad=\"40000\""));
2458        assert!(buf.contains("rotWithShape=\"0\""));
2459        assert!(buf.contains("<a:srgbClr val=\"000000\""));
2460        assert!(buf.contains("</a:effectLst>"));
2461    }
2462
2463    /// 验证 `EffectList` 发光和柔化边缘序列化。
2464    #[test]
2465    fn effect_list_glow_and_soft_edge() {
2466        let effects = EffectList {
2467            glow: Some(GlowEffect {
2468                rad: 50000,
2469                color: Color::RGB(RGBColor(0xFF, 0x00, 0xFF)),
2470            }),
2471            soft_edge: Some(SoftEdgeEffect { rad: 25000 }),
2472            ..Default::default()
2473        };
2474
2475        let mut w = XmlWriter::new();
2476        effects.write_xml(&mut w);
2477        let buf = &w.buf;
2478        assert!(buf.contains("<a:glow rad=\"50000\">"));
2479        assert!(buf.contains("<a:srgbClr val=\"FF00FF\""));
2480        assert!(buf.contains("<a:softEdge rad=\"25000\"/>"));
2481    }
2482
2483    /// 验证 `EffectList` 反射效果序列化(仅属性,无子元素)。
2484    #[test]
2485    fn effect_list_reflection_serialization() {
2486        let effects = EffectList {
2487            reflection: Some(ReflectionEffect {
2488                blur_rad: Some(50000),
2489                st_a: Some(52000),
2490                st_pos: Some(0),
2491                end_a: Some(30000),
2492                end_pos: Some(50000),
2493                dist: Some(38100),
2494                dir: Some(5_400_000),
2495                rot_with_shape: None,
2496            }),
2497            ..Default::default()
2498        };
2499
2500        let mut w = XmlWriter::new();
2501        effects.write_xml(&mut w);
2502        let buf = &w.buf;
2503        assert!(buf.contains("<a:reflection"));
2504        assert!(buf.contains("blurRad=\"50000\""));
2505        assert!(buf.contains("stA=\"52000\""));
2506        assert!(buf.contains("endPos=\"50000\""));
2507        assert!(buf.contains("dist=\"38100\""));
2508        assert!(buf.contains("/>")); // 自闭合
2509    }
2510
2511    /// 验证空 `EffectList` 不写出任何 XML。
2512    #[test]
2513    fn effect_list_empty_writes_nothing() {
2514        let effects = EffectList::default();
2515        let mut w = XmlWriter::new();
2516        effects.write_xml(&mut w);
2517        assert!(w.buf.is_empty());
2518    }
2519
2520    // --------------------- TODO-046: 图片填充模式测试 ---------------------
2521
2522    /// 验证 `BlipFillMode::Stretch` 序列化。
2523    #[test]
2524    fn blip_fill_mode_stretch_serialize() {
2525        let mode = BlipFillMode::Stretch;
2526        let mut w = XmlWriter::new();
2527        mode.write_xml(&mut w);
2528        let s = &w.buf;
2529        assert!(s.contains("<a:stretch>"), "应输出 a:stretch,实际: {s}");
2530        assert!(s.contains("<a:fillRect/>"), "应输出 a:fillRect,实际: {s}");
2531        assert!(s.contains("</a:stretch>"), "应关闭 a:stretch,实际: {s}");
2532    }
2533
2534    /// 验证 `BlipFillMode::Tile` 序列化(带全部属性)。
2535    #[test]
2536    fn blip_fill_mode_tile_serialize_full() {
2537        let mode = BlipFillMode::Tile {
2538            tx: Some(914400),
2539            ty: Some(457200),
2540            sx: Some(100_000),
2541            sy: Some(50_000),
2542            flip: Some("x".to_string()),
2543            algn: Some("tl".to_string()),
2544        };
2545        let mut w = XmlWriter::new();
2546        mode.write_xml(&mut w);
2547        let s = &w.buf;
2548        assert!(s.contains("<a:tile"), "应输出 a:tile,实际: {s}");
2549        assert!(s.contains("tx=\"914400\""), "应包含 tx,实际: {s}");
2550        assert!(s.contains("ty=\"457200\""), "应包含 ty,实际: {s}");
2551        assert!(s.contains("sx=\"100000\""), "应包含 sx,实际: {s}");
2552        assert!(s.contains("sy=\"50000\""), "应包含 sy,实际: {s}");
2553        assert!(s.contains("flip=\"x\""), "应包含 flip,实际: {s}");
2554        assert!(s.contains("algn=\"tl\""), "应包含 algn,实际: {s}");
2555        assert!(s.contains("/>"), "应为自闭合标签,实际: {s}");
2556    }
2557
2558    /// 验证 `BlipFillMode::Tile` 序列化(仅部分属性)。
2559    #[test]
2560    fn blip_fill_mode_tile_serialize_partial() {
2561        let mode = BlipFillMode::Tile {
2562            tx: None,
2563            ty: None,
2564            sx: Some(200_000),
2565            sy: None,
2566            flip: None,
2567            algn: Some("ctr".to_string()),
2568        };
2569        let mut w = XmlWriter::new();
2570        mode.write_xml(&mut w);
2571        let s = &w.buf;
2572        assert!(s.contains("<a:tile"), "应输出 a:tile,实际: {s}");
2573        assert!(!s.contains("tx="), "不应包含 tx,实际: {s}");
2574        assert!(!s.contains("ty="), "不应包含 ty,实际: {s}");
2575        assert!(s.contains("sx=\"200000\""), "应包含 sx,实际: {s}");
2576        assert!(!s.contains("sy="), "不应包含 sy,实际: {s}");
2577        assert!(s.contains("algn=\"ctr\""), "应包含 algn,实际: {s}");
2578    }
2579
2580    /// 验证 `BlipFillMode::None` 不写出任何 XML。
2581    #[test]
2582    fn blip_fill_mode_none_serialize() {
2583        let mode = BlipFillMode::None;
2584        let mut w = XmlWriter::new();
2585        mode.write_xml(&mut w);
2586        assert!(w.buf.is_empty(), "None 模式不应写出任何 XML");
2587    }
2588
2589    /// 验证 `Fill::Blip` 使用 `BlipFillMode::Stretch` 序列化。
2590    #[test]
2591    fn fill_blip_with_stretch_serialize() {
2592        let fill = Fill::Blip {
2593            rid: "rIdImg1".to_string(),
2594            mode: BlipFillMode::Stretch,
2595        };
2596        let mut w = XmlWriter::new();
2597        fill.write_xml(&mut w);
2598        let s = &w.buf;
2599        assert!(s.contains("<a:blipFill"), "应输出 a:blipFill,实际: {s}");
2600        assert!(
2601            s.contains("r:embed=\"rIdImg1\""),
2602            "应包含 r:embed,实际: {s}"
2603        );
2604        assert!(s.contains("<a:stretch>"), "应输出 a:stretch,实际: {s}");
2605        // BUG-001 回归检测:<a:blip 标签应只出现 1 次(不应有嵌套的双重标签)
2606        // 注意:不能用 matches("<a:blip"),因为它会同时匹配 <a:blipFill
2607        let blip_count = s.matches("<a:blip ").count()
2608            + s.matches("<a:blip/>").count()
2609            + s.matches("<a:blip>").count();
2610        assert_eq!(
2611            blip_count, 1,
2612            "应仅输出 1 个 <a:blip 标签,实际 {} 次(可能存在双重标签 bug): {s}",
2613            blip_count
2614        );
2615        // 同时验证不会出现嵌套的 <a:blip><a:blip
2616        assert!(
2617            !s.contains("<a:blip><a:blip"),
2618            "检测到嵌套的双重 <a:blip> 标签(BUG-001 回归): {s}"
2619        );
2620    }
2621
2622    /// 验证 `Fill::Blip` 使用 `BlipFillMode::Tile` 序列化。
2623    #[test]
2624    fn fill_blip_with_tile_serialize() {
2625        let fill = Fill::Blip {
2626            rid: "rIdImg2".to_string(),
2627            mode: BlipFillMode::Tile {
2628                tx: Some(100),
2629                ty: None,
2630                sx: None,
2631                sy: None,
2632                flip: Some("xy".to_string()),
2633                algn: None,
2634            },
2635        };
2636        let mut w = XmlWriter::new();
2637        fill.write_xml(&mut w);
2638        let s = &w.buf;
2639        assert!(s.contains("<a:blipFill"), "应输出 a:blipFill,实际: {s}");
2640        assert!(
2641            s.contains("r:embed=\"rIdImg2\""),
2642            "应包含 r:embed,实际: {s}"
2643        );
2644        assert!(s.contains("<a:tile"), "应输出 a:tile,实际: {s}");
2645        assert!(s.contains("tx=\"100\""), "应包含 tx,实际: {s}");
2646        assert!(s.contains("flip=\"xy\""), "应包含 flip,实际: {s}");
2647    }
2648
2649    /// 验证 `BlipFillMode` 默认值为 `Stretch`。
2650    #[test]
2651    fn blip_fill_mode_default_is_stretch() {
2652        let mode = BlipFillMode::default();
2653        assert!(matches!(mode, BlipFillMode::Stretch));
2654    }
2655
2656    // --------------------- TODO-050: 三维效果测试 ---------------------
2657
2658    /// 验证 `Rotation3d` 序列化为自闭合 `<a:rot/>`。
2659    #[test]
2660    fn rotation_3d_serialize() {
2661        let rot = Rotation3d {
2662            lat: 0,
2663            lon: 0,
2664            rev: 1200000,
2665        };
2666        let mut w = XmlWriter::new();
2667        rot.write_xml(&mut w);
2668        let s = &w.buf;
2669        assert!(s.contains("<a:rot"), "应输出 a:rot,实际: {s}");
2670        assert!(s.contains("lat=\"0\""), "应包含 lat,实际: {s}");
2671        assert!(s.contains("lon=\"0\""), "应包含 lon,实际: {s}");
2672        assert!(s.contains("rev=\"1200000\""), "应包含 rev,实际: {s}");
2673        assert!(s.contains("/>"), "应为自闭合,实际: {s}");
2674    }
2675
2676    /// 验证 `Camera` 序列化(默认预设、无旋转)。
2677    #[test]
2678    fn camera_default_serialize() {
2679        let camera = Camera::default();
2680        let mut w = XmlWriter::new();
2681        camera.write_xml(&mut w);
2682        let s = &w.buf;
2683        assert!(
2684            s.contains("<a:camera prst=\"orthographicFront\""),
2685            "应包含默认 prst,实际: {s}"
2686        );
2687        // 默认 fov=0/zoom=0 不应输出
2688        assert!(!s.contains("fov="), "默认 fov 不应输出,实际: {s}");
2689        assert!(!s.contains("zoom="), "默认 zoom 不应输出,实际: {s}");
2690    }
2691
2692    /// 验证 `Camera` 带旋转的序列化。
2693    #[test]
2694    fn camera_with_rotation_serialize() {
2695        let camera = Camera {
2696            preset: CameraPreset::PerspectiveFront,
2697            fov: 3600000,
2698            // 注意:zoom=100000 是 OOXML 默认值,write_xml 会省略;
2699            // 这里用 200000 测试非默认值的输出。
2700            zoom: 200000,
2701            rotation: Some(Rotation3d {
2702                lat: 30,
2703                lon: 45,
2704                rev: 0,
2705            }),
2706        };
2707        let mut w = XmlWriter::new();
2708        camera.write_xml(&mut w);
2709        let s = &w.buf;
2710        assert!(
2711            s.contains("<a:camera prst=\"perspectiveFront\""),
2712            "应包含 perspectiveFront,实际: {s}"
2713        );
2714        assert!(s.contains("fov=\"3600000\""), "应包含 fov,实际: {s}");
2715        assert!(s.contains("zoom=\"200000\""), "应包含 zoom,实际: {s}");
2716        assert!(s.contains("<a:rot"), "应包含 a:rot 子元素,实际: {s}");
2717        assert!(s.contains("</a:camera>"), "应关闭 a:camera,实际: {s}");
2718    }
2719
2720    /// 验证 `Scene3d` 完整序列化(相机 + 光照)。
2721    #[test]
2722    fn scene3d_full_serialize() {
2723        let scene = Scene3d {
2724            camera: Camera {
2725                preset: CameraPreset::OrthographicFront,
2726                fov: 0,
2727                zoom: 0,
2728                rotation: Some(Rotation3d {
2729                    lat: 0,
2730                    lon: 0,
2731                    rev: 0,
2732                }),
2733            },
2734            light_rig: LightRig {
2735                rig: LightRigType::ThreePt,
2736                dir: LightRigDirection::Top,
2737                rotation: Some(Rotation3d {
2738                    lat: 0,
2739                    lon: 0,
2740                    rev: 1200000,
2741                }),
2742            },
2743            backdrop: None,
2744        };
2745        let mut w = XmlWriter::new();
2746        scene.write_xml(&mut w);
2747        let s = &w.buf;
2748        assert!(s.contains("<a:scene3d>"), "应输出 a:scene3d,实际: {s}");
2749        assert!(
2750            s.contains("<a:camera prst=\"orthographicFront\">"),
2751            "应包含 camera 子元素,实际: {s}"
2752        );
2753        assert!(
2754            s.contains("<a:lightRig rig=\"threePt\" dir=\"t\">"),
2755            "应包含 lightRig 子元素,实际: {s}"
2756        );
2757        assert!(s.contains("</a:scene3d>"), "应关闭 a:scene3d,实际: {s}");
2758    }
2759
2760    /// 验证 `Sp3d` 默认序列化(仅 prstMaterial 默认值,输出空 `<a:sp3d/>`)。
2761    #[test]
2762    fn sp3d_default_serialize() {
2763        let sp3d = Sp3d::default();
2764        let mut w = XmlWriter::new();
2765        sp3d.write_xml(&mut w);
2766        let s = &w.buf;
2767        assert!(s.contains("<a:sp3d"), "应输出 a:sp3d,实际: {s}");
2768        // 默认 warmMatte 不输出 prstMaterial
2769        assert!(
2770            !s.contains("prstMaterial="),
2771            "默认 warmMatte 不应输出,实际: {s}"
2772        );
2773    }
2774
2775    /// 验证 `Sp3d` 带棱台和颜色的序列化。
2776    #[test]
2777    fn sp3d_with_bevel_and_colors_serialize() {
2778        let sp3d = Sp3d {
2779            extrusion_h: 38100,
2780            contour_w: 12700,
2781            prst_material: MaterialPreset::Metallic,
2782            bevel_top: Some(Bevel { w: 63500, h: 25400 }),
2783            bevel_bottom: None,
2784            extrusion_color: Some(Color::RGB(crate::units::RGBColor::BLACK)),
2785            contour_color: Some(Color::RGB(crate::units::RGBColor::WHITE)),
2786        };
2787        let mut w = XmlWriter::new();
2788        sp3d.write_xml(&mut w);
2789        let s = &w.buf;
2790        assert!(
2791            s.contains("extrusionH=\"38100\""),
2792            "应包含 extrusionH,实际: {s}"
2793        );
2794        assert!(
2795            s.contains("contourW=\"12700\""),
2796            "应包含 contourW,实际: {s}"
2797        );
2798        assert!(
2799            s.contains("prstMaterial=\"metallic\""),
2800            "应包含 prstMaterial,实际: {s}"
2801        );
2802        assert!(
2803            s.contains("<a:bevelT w=\"63500\" h=\"25400\"/>"),
2804            "应包含 bevelT,实际: {s}"
2805        );
2806        assert!(
2807            s.contains("<a:extrusionClr>"),
2808            "应包含 extrusionClr,实际: {s}"
2809        );
2810        assert!(s.contains("<a:contourClr>"), "应包含 contourClr,实际: {s}");
2811        assert!(s.contains("</a:sp3d>"), "应关闭 a:sp3d,实际: {s}");
2812    }
2813
2814    /// 验证 `ShapeProperties` 中 scene3d/sp3d 在 effectLst 之后输出。
2815    #[test]
2816    fn shape_properties_with_3d_serialize() {
2817        let sp = ShapeProperties {
2818            scene3d: Some(Scene3d::default()),
2819            sp3d: Some(Sp3d::default()),
2820            ..Default::default()
2821        };
2822        let mut w = XmlWriter::new();
2823        sp.write_xml(&mut w, "p:spPr");
2824        let s = &w.buf;
2825        // 验证 scene3d 与 sp3d 都被输出
2826        assert!(s.contains("<a:scene3d>"), "应输出 scene3d,实际: {s}");
2827        assert!(s.contains("<a:sp3d"), "应输出 sp3d,实际: {s}");
2828        // 验证顺序:scene3d 在 sp3d 之前
2829        let scene_pos = s.find("<a:scene3d>").expect("scene3d 应存在");
2830        let sp3d_pos = s.find("<a:sp3d").expect("sp3d 应存在");
2831        assert!(
2832            scene_pos < sp3d_pos,
2833            "scene3d 应在 sp3d 之前,实际 scene3d@{scene_pos} sp3d@{sp3d_pos}"
2834        );
2835    }
2836
2837    /// 验证 `CameraPreset::parse` 解析已知预设。
2838    #[test]
2839    fn camera_preset_from_str() {
2840        assert!(matches!(
2841            CameraPreset::parse("orthographicFront"),
2842            CameraPreset::OrthographicFront
2843        ));
2844        assert!(matches!(
2845            CameraPreset::parse("perspectiveFront"),
2846            CameraPreset::PerspectiveFront
2847        ));
2848        // 未知值归入 Other
2849        match CameraPreset::parse("customUnknown") {
2850            CameraPreset::Other(s) => assert_eq!(s, "customUnknown"),
2851            other => panic!("未知预设应归入 Other,实际: {other:?}"),
2852        }
2853    }
2854
2855    /// 验证 `MaterialPreset::parse` 解析已知材质。
2856    #[test]
2857    fn material_preset_from_str() {
2858        assert!(matches!(
2859            MaterialPreset::parse("warmMatte"),
2860            MaterialPreset::WarmMatte
2861        ));
2862        assert!(matches!(
2863            MaterialPreset::parse("metallic"),
2864            MaterialPreset::Metallic
2865        ));
2866        // 未知值归入 Other
2867        match MaterialPreset::parse("futureMaterial") {
2868            MaterialPreset::Other(s) => assert_eq!(s, "futureMaterial"),
2869            other => panic!("未知材质应归入 Other,实际: {other:?}"),
2870        }
2871    }
2872
2873    // ===================== TODO-050 backdrop 背景元素测试 =====================
2874
2875    /// 验证 `Backdrop` 默认序列化(无平面启用,仅空 backdrop)。
2876    #[test]
2877    fn backdrop_default_serialize() {
2878        let bd = Backdrop::default();
2879        let mut w = XmlWriter::new();
2880        bd.write_xml(&mut w);
2881        let s = &w.buf;
2882        assert!(s.contains("<a:backdrop>"), "应输出 a:backdrop,实际: {s}");
2883        assert!(s.contains("</a:backdrop>"), "应关闭 a:backdrop,实际: {s}");
2884        // 默认所有平面未启用,不应输出 a:floor/a:wall/a:l/a:r/a:t/a:b
2885        // 注意:用 <a:xxx/> 精确匹配,避免 <a:b 误匹配 <a:backdrop>
2886        assert!(!s.contains("<a:floor/>"), "默认不应输出 floor: {s}");
2887        assert!(!s.contains("<a:wall/>"), "默认不应输出 wall: {s}");
2888        assert!(!s.contains("<a:l/>"), "默认不应输出 l: {s}");
2889        assert!(!s.contains("<a:r/>"), "默认不应输出 r: {s}");
2890        assert!(!s.contains("<a:t/>"), "默认不应输出 t: {s}");
2891        assert!(!s.contains("<a:b/>"), "默认不应输出 b: {s}");
2892    }
2893
2894    /// 验证 `Backdrop` 带锚点 + 全平面启用的序列化。
2895    #[test]
2896    fn backdrop_with_anchor_and_all_planes_serialize() {
2897        let bd = Backdrop {
2898            anchor: Some(Point3d {
2899                x: 100000,
2900                y: 200000,
2901                z: 300000,
2902            }),
2903            floor: true,
2904            wall: true,
2905            left: true,
2906            right: true,
2907            top: true,
2908            bottom: true,
2909        };
2910        let mut w = XmlWriter::new();
2911        bd.write_xml(&mut w);
2912        let s = &w.buf;
2913        // 锚点
2914        assert!(
2915            s.contains("<a:anchor x=\"100000\" y=\"200000\" z=\"300000\"/>"),
2916            "应包含 anchor,实际: {s}"
2917        );
2918        // 6 个平面(按 OOXML 顺序)
2919        assert!(s.contains("<a:floor/>"), "应包含 floor: {s}");
2920        assert!(s.contains("<a:wall/>"), "应包含 wall: {s}");
2921        assert!(s.contains("<a:l/>"), "应包含 l: {s}");
2922        assert!(s.contains("<a:r/>"), "应包含 r: {s}");
2923        assert!(s.contains("<a:t/>"), "应包含 t: {s}");
2924        assert!(s.contains("<a:b/>"), "应包含 b: {s}");
2925        // 验证顺序:anchor → floor → wall → l → r → t → b
2926        // 注意:用 <a:xxx/> 精确匹配,避免 <a:b 误匹配 <a:backdrop>
2927        let anchor_pos = s.find("<a:anchor").expect("anchor 应存在");
2928        let floor_pos = s.find("<a:floor/>").expect("floor 应存在");
2929        let wall_pos = s.find("<a:wall/>").expect("wall 应存在");
2930        let l_pos = s.find("<a:l/>").expect("l 应存在");
2931        let r_pos = s.find("<a:r/>").expect("r 应存在");
2932        let t_pos = s.find("<a:t/>").expect("t 应存在");
2933        let b_pos = s.find("<a:b/>").expect("b 应存在");
2934        assert!(anchor_pos < floor_pos, "anchor 应在 floor 之前");
2935        assert!(floor_pos < wall_pos, "floor 应在 wall 之前");
2936        assert!(wall_pos < l_pos, "wall 应在 l 之前");
2937        assert!(l_pos < r_pos, "l 应在 r 之前");
2938        assert!(r_pos < t_pos, "r 应在 t 之前");
2939        assert!(t_pos < b_pos, "t 应在 b 之前");
2940    }
2941
2942    /// 验证 `Backdrop` 仅启用部分平面(floor + wall)。
2943    #[test]
2944    fn backdrop_partial_planes_serialize() {
2945        let bd = Backdrop {
2946            floor: true,
2947            wall: true,
2948            ..Default::default()
2949        };
2950        let mut w = XmlWriter::new();
2951        bd.write_xml(&mut w);
2952        let s = &w.buf;
2953        assert!(s.contains("<a:floor/>"), "应包含 floor: {s}");
2954        assert!(s.contains("<a:wall/>"), "应包含 wall: {s}");
2955        // 注意:用 <a:xxx/> 精确匹配,避免 <a:b 误匹配 <a:backdrop>
2956        assert!(!s.contains("<a:l/>"), "不应包含 l: {s}");
2957        assert!(!s.contains("<a:r/>"), "不应包含 r: {s}");
2958        assert!(!s.contains("<a:t/>"), "不应包含 t: {s}");
2959        assert!(!s.contains("<a:b/>"), "不应包含 b: {s}");
2960    }
2961
2962    /// 验证 `Scene3d` 带 backdrop 的完整序列化。
2963    #[test]
2964    fn scene3d_with_backdrop_serialize() {
2965        let scene = Scene3d {
2966            camera: Camera::default(),
2967            light_rig: LightRig::default(),
2968            backdrop: Some(Backdrop {
2969                floor: true,
2970                wall: true,
2971                ..Default::default()
2972            }),
2973        };
2974        let mut w = XmlWriter::new();
2975        scene.write_xml(&mut w);
2976        let s = &w.buf;
2977        assert!(s.contains("<a:scene3d>"), "应包含 scene3d: {s}");
2978        assert!(s.contains("<a:backdrop>"), "应包含 backdrop: {s}");
2979        assert!(s.contains("<a:floor/>"), "应包含 floor: {s}");
2980        assert!(s.contains("<a:wall/>"), "应包含 wall: {s}");
2981        // 验证顺序:camera → lightRig → backdrop
2982        let cam_pos = s.find("<a:camera").expect("camera 应存在");
2983        let rig_pos = s.find("<a:lightRig").expect("lightRig 应存在");
2984        let bd_pos = s.find("<a:backdrop>").expect("backdrop 应存在");
2985        assert!(cam_pos < rig_pos, "camera 应在 lightRig 之前");
2986        assert!(rig_pos < bd_pos, "lightRig 应在 backdrop 之前");
2987    }
2988
2989    /// 验证 `Scene3d` 无 backdrop 时不输出 backdrop 元素(向后兼容)。
2990    #[test]
2991    fn scene3d_without_backdrop_no_element() {
2992        let scene = Scene3d::default();
2993        let mut w = XmlWriter::new();
2994        scene.write_xml(&mut w);
2995        let s = &w.buf;
2996        assert!(s.contains("<a:scene3d>"), "应包含 scene3d: {s}");
2997        assert!(!s.contains("<a:backdrop>"), "无 backdrop 时不应输出: {s}");
2998    }
2999
3000    /// 验证 `Point3d` 序列化坐标值。
3001    #[test]
3002    fn point3d_serialize() {
3003        let p = Point3d {
3004            x: -100000,
3005            y: 0,
3006            z: 500000,
3007        };
3008        let mut w = XmlWriter::new();
3009        p.write_xml(&mut w);
3010        let s = &w.buf;
3011        assert!(s.contains("x=\"-100000\""), "应包含 x: {s}");
3012        assert!(s.contains("y=\"0\""), "应包含 y: {s}");
3013        assert!(s.contains("z=\"500000\""), "应包含 z: {s}");
3014    }
3015}