Skip to main content

ooxml_core/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    /// 仅 pptx-rs 主 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    ///
1215    /// `tag` 参数指定外层元素标签名(如 `"p:xfrm"` 用于 PPTX)。
1216    pub fn write_xfrm_only(&self, w: &mut super::writer::XmlWriter, tag: &str) {
1217        if self.xfrm.is_empty() {
1218            // 仍然输出空 <p:xfrm/> 以保证 OOXML 顺序稳定
1219            w.empty(tag);
1220            return;
1221        }
1222        w.open(tag);
1223        self.xfrm.write_xml(w);
1224        w.close(tag);
1225    }
1226
1227    // --------------------- 形状效果 API(TODO-011 高阶) ---------------------
1228    //
1229    // 以下便捷方法封装 `<a:effectLst>` 的常用操作,对标 python-pptx 的
1230    // `shape.shadow` / `shape.glow` 等高阶接口。底层逻辑都通过 `effects`
1231    // 字段(`Option<EffectList>`)承接,序列化时由 `write_xml` 自动按
1232    // OOXML 顺序输出在 `<a:ln>` 之后。
1233
1234    /// 读取效果列表(`<a:effectLst>`)。`None` 表示未设置。
1235    pub fn effects(&self) -> Option<&EffectList> {
1236        self.effects.as_ref()
1237    }
1238
1239    /// 读取效果列表的可变引用。若未设置,自动初始化为空 `EffectList`。
1240    ///
1241    /// 调用此方法后即使不设置任何效果,也会写出空 `<a:effectLst/>`(PowerPoint 兼容)。
1242    /// 若要避免写出空元素,请用 [`Self::clear_effects`]。
1243    pub fn effects_mut(&mut self) -> &mut EffectList {
1244        self.effects.get_or_insert_with(EffectList::default)
1245    }
1246
1247    /// 设置外阴影(`<a:outerShdw>`)。覆盖既有外阴影,保留其他效果。
1248    ///
1249    /// 对标 python-pptx `shape.shadow.inherit = False` + `shape.shadow.outerShadow`。
1250    ///
1251    /// # 参数
1252    /// - `shadow`:阴影配置(方向/距离/模糊半径/颜色)
1253    pub fn set_outer_shadow(&mut self, shadow: ShadowEffect) {
1254        self.effects_mut().outer_shadow = Some(shadow);
1255    }
1256
1257    /// 设置内阴影(`<a:innerShdw>`)。覆盖既有内阴影,保留其他效果。
1258    pub fn set_inner_shadow(&mut self, shadow: ShadowEffect) {
1259        self.effects_mut().inner_shadow = Some(shadow);
1260    }
1261
1262    /// 设置发光(`<a:glow>`)。覆盖既有发光,保留其他效果。
1263    pub fn set_glow(&mut self, glow: GlowEffect) {
1264        self.effects_mut().glow = Some(glow);
1265    }
1266
1267    /// 设置柔化边缘(`<a:softEdge>`)。覆盖既有柔边,保留其他效果。
1268    pub fn set_soft_edge(&mut self, rad: i64) {
1269        self.effects_mut().soft_edge = Some(SoftEdgeEffect { rad });
1270    }
1271
1272    /// 设置反射(`<a:reflection>`)。覆盖既有反射,保留其他效果。
1273    pub fn set_reflection(&mut self, reflection: ReflectionEffect) {
1274        self.effects_mut().reflection = Some(reflection);
1275    }
1276
1277    /// 清除外阴影。
1278    pub fn clear_outer_shadow(&mut self) {
1279        if let Some(e) = self.effects.as_mut() {
1280            e.outer_shadow = None;
1281        }
1282    }
1283
1284    /// 清除内阴影。
1285    pub fn clear_inner_shadow(&mut self) {
1286        if let Some(e) = self.effects.as_mut() {
1287            e.inner_shadow = None;
1288        }
1289    }
1290
1291    /// 清除发光。
1292    pub fn clear_glow(&mut self) {
1293        if let Some(e) = self.effects.as_mut() {
1294            e.glow = None;
1295        }
1296    }
1297
1298    /// 清除柔化边缘。
1299    pub fn clear_soft_edge(&mut self) {
1300        if let Some(e) = self.effects.as_mut() {
1301            e.soft_edge = None;
1302        }
1303    }
1304
1305    /// 清除反射。
1306    pub fn clear_reflection(&mut self) {
1307        if let Some(e) = self.effects.as_mut() {
1308            e.reflection = None;
1309        }
1310    }
1311
1312    /// 清除所有效果(删除整个 `<a:effectLst>` 元素)。
1313    pub fn clear_effects(&mut self) {
1314        self.effects = None;
1315    }
1316}
1317
1318// ====================================================================
1319// 高阶 Fill / Line 视图(python-pptx 风格)
1320// ====================================================================
1321
1322use crate::oxml::color::ColorFormat;
1323
1324/// 填充高阶视图(`pptx.dml.fill.FillFormat`)。
1325///
1326/// # 与 python-pptx 的对应
1327///
1328/// - `pptx.dml.fill.FillFormat` ←→ [`FillFormat`];
1329/// - `shape.fill.solid()` + `shape.fill.fore_color.rgb = ...` ←→
1330///   `fill_format.solid().set_rgb(...)`。
1331///
1332/// # 设计要点
1333///
1334/// - **借用 + 透明代理**:构造时传入 `&mut Fill`;所有写都走底层;
1335/// - **零分配**:颜色写入走 [`ColorFormat`] 的借用;
1336/// - **类型安全**:通过 [`super::simpletypes::MsoFillType`] 表达"当前填充类型"。
1337#[derive(Debug)]
1338pub struct FillFormat<'a> {
1339    /// 底层 [`Fill`] 引用。
1340    fill: &'a mut Fill,
1341}
1342
1343impl<'a> FillFormat<'a> {
1344    /// 构造一个 fill 视图。
1345    pub fn new(fill: &'a mut Fill) -> Self {
1346        FillFormat { fill }
1347    }
1348
1349    /// 底层 fill 不可变引用。
1350    pub fn fill(&self) -> &Fill {
1351        self.fill
1352    }
1353    /// 底层 fill 可变引用。
1354    pub fn fill_mut(&mut self) -> &mut Fill {
1355        self.fill
1356    }
1357
1358    /// 当前填充类型(python-pptx `fill.type`)。
1359    pub fn fill_type(&self) -> super::simpletypes::MsoFillType {
1360        match self.fill {
1361            Fill::None => super::simpletypes::MsoFillType::Background,
1362            Fill::Solid(_) => super::simpletypes::MsoFillType::Solid,
1363            Fill::Blip { .. } => super::simpletypes::MsoFillType::Picture,
1364            Fill::Gradient(_) => super::simpletypes::MsoFillType::Gradient,
1365            Fill::Pattern(_) => super::simpletypes::MsoFillType::Pattern,
1366            Fill::Inherit => super::simpletypes::MsoFillType::Inherit,
1367        }
1368    }
1369
1370    /// 切到**实色**模式并返回 [`ColorFormat`] 代理。
1371    ///
1372    /// 对应 python-pptx 中 `fill.solid()` 后再 `fill.fore_color.rgb = ...`。
1373    /// 调用本方法会把 `Fill` 切到 `Solid(Color::None)`,后续 `set_rgb` /
1374    /// `set_theme` 等会原地更新 `Color`。
1375    pub fn solid(&mut self) -> ColorFormat<'_> {
1376        // 如果不是 Solid,先切到 Solid(None)
1377        if !matches!(self.fill, Fill::Solid(_)) {
1378            *self.fill = Fill::Solid(Color::None);
1379        }
1380        match self.fill {
1381            Fill::Solid(c) => ColorFormat::new(c),
1382            _ => unreachable!("just set to Solid"),
1383        }
1384    }
1385
1386    /// 切到**无填充**。
1387    ///
1388    /// 对应 python-pptx 中 `fill.background()`。但通常我们用 [`FillFormat::clear`] 更直白。
1389    pub fn set_none(&mut self) {
1390        *self.fill = Fill::None;
1391    }
1392
1393    /// 切到图片填充。
1394    ///
1395    /// # 参数
1396    /// - `rid`:图片关系 id(形如 `rIdImg1`)。
1397    /// - `mode`:填充模式(拉伸/平铺/无)。使用 [`BlipFillMode::Stretch`] 为默认拉伸。
1398    pub fn set_picture(&mut self, rid: impl Into<String>, mode: BlipFillMode) {
1399        *self.fill = Fill::Blip {
1400            rid: rid.into(),
1401            mode,
1402        };
1403    }
1404
1405    /// 重置(继承主题默认)。
1406    pub fn clear(&mut self) {
1407        *self.fill = Fill::Inherit;
1408    }
1409
1410    /// 便捷:直接设成 sRGB 实色。
1411    pub fn set_solid_rgb(&mut self, c: impl Into<crate::units::RGBColor>) {
1412        *self.fill = Fill::Solid(Color::RGB(c.into()));
1413    }
1414    /// 便捷:直接设成主题色实色。
1415    pub fn set_solid_theme(&mut self, t: super::simpletypes::MsoThemeColorIndex) {
1416        if let Some(s) = t.as_str() {
1417            if let Ok(sc) = s.parse::<crate::oxml::color::SchemeColor>() {
1418                *self.fill = Fill::Solid(Color::Scheme(sc));
1419            }
1420        }
1421    }
1422
1423    // ===== 渐变填充 API(TODO-009)=====
1424
1425    /// 切到**渐变**模式并返回 `&mut GradientFill` 供进一步配置。
1426    ///
1427    /// 对应 python-pptx 中 `fill.gradient()`。调用本方法会把 `Fill` 切到
1428    /// `Gradient(GradientFill { stops: vec![], gradient_type: Linear(0), .. })`,
1429    /// 调用方随后通过返回的引用添加光轨、设置角度等。
1430    ///
1431    /// # 示例
1432    /// ```ignore
1433    /// use ooxml_core::oxml::sppr::{Fill, GradientStop, GradientType};
1434    /// use ooxml_core::units::RGBColor;
1435    /// use ooxml_core::oxml::color::Color;
1436    ///
1437    /// let mut fill = Fill::Inherit;
1438    /// let mut fmt = FillFormat::new(&mut fill);
1439    /// let g = fmt.gradient();
1440    /// g.stops.push(GradientStop { pos: 0, color: Color::RGB(RGBColor::new(0xFF, 0x00, 0x00)) });
1441    /// g.stops.push(GradientStop { pos: 100000, color: Color::RGB(RGBColor::new(0x00, 0x00, 0xFF)) });
1442    /// g.gradient_type = GradientType::Linear(2_700_000); // 向下
1443    /// ```
1444    pub fn gradient(&mut self) -> &mut GradientFill {
1445        if !matches!(self.fill, Fill::Gradient(_)) {
1446            *self.fill = Fill::Gradient(GradientFill {
1447                stops: Vec::new(),
1448                gradient_type: GradientType::Linear(0),
1449                flip: None,
1450                rot_with_shape: None,
1451            });
1452        }
1453        match &mut self.fill {
1454            Fill::Gradient(g) => g,
1455            _ => unreachable!("just set to Gradient"),
1456        }
1457    }
1458
1459    /// 便捷:直接设成**线性渐变**。
1460    ///
1461    /// # 参数
1462    /// - `stops`:光轨列表(至少 2 个)。
1463    /// - `angle`:角度(1/60000 度,0 = 向右,5400000 = 向下)。
1464    pub fn set_gradient_linear(&mut self, stops: Vec<GradientStop>, angle: i32) {
1465        *self.fill = Fill::Gradient(GradientFill {
1466            stops,
1467            gradient_type: GradientType::Linear(angle),
1468            flip: None,
1469            rot_with_shape: None,
1470        });
1471    }
1472
1473    /// 便捷:直接设成**路径渐变**。
1474    ///
1475    /// # 参数
1476    /// - `stops`:光轨列表。
1477    /// - `path`:路径形状(`Circle` / `Rect` / `Shape`)。
1478    pub fn set_gradient_path(&mut self, stops: Vec<GradientStop>, path: GradientPath) {
1479        *self.fill = Fill::Gradient(GradientFill {
1480            stops,
1481            gradient_type: GradientType::Path(path),
1482            flip: None,
1483            rot_with_shape: None,
1484        });
1485    }
1486
1487    // ===== 图案填充 API(TODO-010)=====
1488
1489    /// 切到**图案**模式并返回 `&mut PatternFill` 供进一步配置。
1490    ///
1491    /// 调用本方法会把 `Fill` 切到 `Pattern(PatternFill::default())`,
1492    /// 调用方随后通过返回的引用设置 prst / fg_color / bg_color。
1493    pub fn pattern(&mut self) -> &mut PatternFill {
1494        if !matches!(self.fill, Fill::Pattern(_)) {
1495            *self.fill = Fill::Pattern(PatternFill {
1496                prst: String::new(),
1497                fg_color: Color::None,
1498                bg_color: Color::None,
1499            });
1500        }
1501        match &mut self.fill {
1502            Fill::Pattern(p) => p,
1503            _ => unreachable!("just set to Pattern"),
1504        }
1505    }
1506
1507    /// 便捷:直接设成图案填充。
1508    ///
1509    /// # 参数
1510    /// - `prst`:预置图案类型(如 `"pct5"` / `"horz"` / `"vert"` / `"cross"`)。
1511    /// - `fg`:前景色。
1512    /// - `bg`:背景色。
1513    pub fn set_pattern(&mut self, prst: impl Into<String>, fg: Color, bg: Color) {
1514        *self.fill = Fill::Pattern(PatternFill {
1515            prst: prst.into(),
1516            fg_color: fg,
1517            bg_color: bg,
1518        });
1519    }
1520}
1521
1522impl<'a> From<&'a mut Fill> for FillFormat<'a> {
1523    fn from(f: &'a mut Fill) -> Self {
1524        FillFormat::new(f)
1525    }
1526}
1527
1528/// 线框高阶视图(`pptx.dml.line.LineFormat`)。
1529///
1530/// # 与 python-pptx 的对应
1531///
1532/// - `pptx.dml.line.LineFormat` ←→ [`LineFormat`];
1533/// - `line.color.rgb = ...` / `line.width = Pt(1)` / `line.dash_style = MSO_LINE.DASH`
1534///   ←→ [`LineFormat::color`] / [`LineFormat::set_width`] / [`LineFormat::set_dash_style`]
1535#[derive(Debug)]
1536pub struct LineFormat<'a> {
1537    /// 底层 [`Line`] 引用。
1538    line: &'a mut Line,
1539}
1540
1541impl<'a> LineFormat<'a> {
1542    /// 构造一个 line 视图。
1543    pub fn new(line: &'a mut Line) -> Self {
1544        LineFormat { line }
1545    }
1546
1547    /// 底层 line 不可变引用。
1548    pub fn line(&self) -> &Line {
1549        self.line
1550    }
1551    /// 底层 line 可变引用。
1552    pub fn line_mut(&mut self) -> &mut Line {
1553        self.line
1554    }
1555
1556    /// 颜色 [`ColorFormat`] 代理。
1557    pub fn color(&mut self) -> ColorFormat<'_> {
1558        ColorFormat::new(&mut self.line.color)
1559    }
1560
1561    /// 读取宽度(EMU)。
1562    pub fn width(&self) -> Option<crate::units::Emu> {
1563        self.line.width
1564    }
1565    /// 设置宽度(EMU)。通常 `Pt(1.0).emu()` 即可。
1566    pub fn set_width(&mut self, w: crate::units::Emu) {
1567        self.line.width = Some(w);
1568    }
1569
1570    /// 读取线型。
1571    pub fn dash_style(&self) -> Option<Dash> {
1572        self.line.dash
1573    }
1574    /// 设置线型(用 [`super::simpletypes::MsoLineDashStyle`],自动转 [`Dash`])。
1575    pub fn set_dash_style(&mut self, s: super::simpletypes::MsoLineDashStyle) {
1576        self.line.dash = Some(s.into());
1577    }
1578
1579    /// 设为无填充(透明线框)。
1580    pub fn set_no_fill(&mut self) {
1581        self.line.no_fill = true;
1582        self.line.color = crate::oxml::color::Color::None;
1583    }
1584
1585    /// 便捷:设宽度为磅值。
1586    pub fn set_width_pt(&mut self, pt: crate::units::Pt) {
1587        self.line.width = Some(crate::units::Emu((pt.value() * 12_700.0) as i64));
1588    }
1589}
1590
1591impl<'a> From<&'a mut Line> for LineFormat<'a> {
1592    fn from(l: &'a mut Line) -> Self {
1593        LineFormat::new(l)
1594    }
1595}
1596
1597// ===================== 三维效果(TODO-050) =====================
1598//
1599// OOXML 中 `<a:scene3d>` / `<a:sp3d>` 位于 `<p:spPr>` 的 effectLst 之后、extLst 之前。
1600// - scene3d 定义相机与光照(场景级 3D)
1601// - sp3d 定义形状本身的 3D 拉伸/棱台/材质(形状级 3D)
1602//
1603// 典型 XML 结构(来自 Office 默认 fmtScheme 第三个 effectStyle):
1604// ```xml
1605// <a:scene3d>
1606//   <a:camera prst="orthographicFront">
1607//     <a:rot lat="0" lon="0" rev="0"/>
1608//   </a:camera>
1609//   <a:lightRig rig="threePt" dir="t">
1610//     <a:rot lat="0" lon="0" rev="1200000"/>
1611//   </a:lightRig>
1612// </a:scene3d>
1613// <a:sp3d>
1614//   <a:bevelT w="63500" h="25400"/>
1615// </a:sp3d>
1616// ```
1617//
1618// 所有角度单位为"60000 分之一度"(OOXML ST_Angle),与 Transform::rot 一致。
1619
1620/// 三维旋转(`<a:rot>`,OOXML CT_SphereCoords)。
1621///
1622/// 三个角度均以 1/60000 度为单位:
1623/// - `lat`:纬度(-90°~90°,即 -5400000~5400000)
1624/// - `lon`:经度(0°~360°,即 0~21600000)
1625/// - `rev`:滚转(沿视线轴的旋转)
1626#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
1627pub struct Rotation3d {
1628    /// 纬度(1/60000 度)。
1629    pub lat: i32,
1630    /// 经度(1/60000 度)。
1631    pub lon: i32,
1632    /// 滚转(1/60000 度)。
1633    pub rev: i32,
1634}
1635
1636impl Rotation3d {
1637    /// 写出 `<a:rot lat="..." lon="..." rev="..."/>`(自闭合)。
1638    pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
1639        let lat = self.lat.to_string();
1640        let lon = self.lon.to_string();
1641        let rev = self.rev.to_string();
1642        w.empty_with(
1643            "a:rot",
1644            &[
1645                ("lat", lat.as_str()),
1646                ("lon", lon.as_str()),
1647                ("rev", rev.as_str()),
1648            ],
1649        );
1650    }
1651}
1652
1653/// 相机预设类型(OOXML ST_PresetCameraType,常用子集)。
1654///
1655/// 完整列表参见 ECMA-376 Part 1 §20.1.10.13。
1656#[derive(Clone, Debug, Default, PartialEq, Eq)]
1657pub enum CameraPreset {
1658    /// `orthographicFront`(默认,正交前视图)。
1659    #[default]
1660    OrthographicFront,
1661    /// `isometricOffAxis1` ~ `isometricOffAxis4`:等轴侧视图。
1662    IsometricOffAxis1,
1663    IsometricOffAxis2,
1664    IsometricOffAxis3,
1665    IsometricOffAxis4,
1666    /// `isometricLeftDown` / `isometricLeftUp` / `isometricRightDown` / `isometricRightUp`。
1667    IsometricLeftDown,
1668    IsometricLeftUp,
1669    IsometricRightDown,
1670    IsometricRightUp,
1671    /// `perspectiveFront`:透视前视图。
1672    PerspectiveFront,
1673    /// `perspectiveLeft` / `perspectiveRight`:透视侧视图。
1674    PerspectiveLeft,
1675    PerspectiveRight,
1676    /// 其它未显式枚举的预设(保留原始字符串)。
1677    Other(String),
1678}
1679
1680impl CameraPreset {
1681    /// 转 OOXML 字符串。
1682    pub fn as_str(&self) -> &str {
1683        match self {
1684            CameraPreset::OrthographicFront => "orthographicFront",
1685            CameraPreset::IsometricOffAxis1 => "isometricOffAxis1",
1686            CameraPreset::IsometricOffAxis2 => "isometricOffAxis2",
1687            CameraPreset::IsometricOffAxis3 => "isometricOffAxis3",
1688            CameraPreset::IsometricOffAxis4 => "isometricOffAxis4",
1689            CameraPreset::IsometricLeftDown => "isometricLeftDown",
1690            CameraPreset::IsometricLeftUp => "isometricLeftUp",
1691            CameraPreset::IsometricRightDown => "isometricRightDown",
1692            CameraPreset::IsometricRightUp => "isometricRightUp",
1693            CameraPreset::PerspectiveFront => "perspectiveFront",
1694            CameraPreset::PerspectiveLeft => "perspectiveLeft",
1695            CameraPreset::PerspectiveRight => "perspectiveRight",
1696            CameraPreset::Other(s) => s.as_str(),
1697        }
1698    }
1699
1700    /// 从字符串解析(不区分大小写、未识别则归入 `Other`)。
1701    ///
1702    /// 注:方法名为 `parse` 而非 `from_str`,以避免与 `std::str::FromStr` trait 冲突。
1703    pub fn parse(s: &str) -> Self {
1704        match s {
1705            "orthographicFront" => CameraPreset::OrthographicFront,
1706            "isometricOffAxis1" => CameraPreset::IsometricOffAxis1,
1707            "isometricOffAxis2" => CameraPreset::IsometricOffAxis2,
1708            "isometricOffAxis3" => CameraPreset::IsometricOffAxis3,
1709            "isometricOffAxis4" => CameraPreset::IsometricOffAxis4,
1710            "isometricLeftDown" => CameraPreset::IsometricLeftDown,
1711            "isometricLeftUp" => CameraPreset::IsometricLeftUp,
1712            "isometricRightDown" => CameraPreset::IsometricRightDown,
1713            "isometricRightUp" => CameraPreset::IsometricRightUp,
1714            "perspectiveFront" => CameraPreset::PerspectiveFront,
1715            "perspectiveLeft" => CameraPreset::PerspectiveLeft,
1716            "perspectiveRight" => CameraPreset::PerspectiveRight,
1717            other => CameraPreset::Other(other.to_string()),
1718        }
1719    }
1720}
1721
1722/// 相机(`<a:camera>`,OOXML CT_Camera)。
1723///
1724/// 定义观察形状的虚拟相机:预设类型、视野(FOV)、缩放、可选旋转。
1725#[derive(Clone, Debug, Default, PartialEq, Eq)]
1726pub struct Camera {
1727    /// 相机预设类型(默认 `orthographicFront`)。
1728    pub preset: CameraPreset,
1729    /// 视野角度(1/60000 度,0 表示使用预设默认)。
1730    pub fov: i32,
1731    /// 缩放(百分比 * 1000,100000 = 100%)。
1732    pub zoom: i32,
1733    /// 可选旋转(覆盖预设默认视角)。
1734    pub rotation: Option<Rotation3d>,
1735}
1736
1737impl Camera {
1738    /// 写出 `<a:camera prst="..." fov="..." zoom="...">...</a:camera>`。
1739    pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
1740        let prst = self.preset.as_str();
1741        let mut attrs: Vec<(&str, &str)> = vec![("prst", prst)];
1742        let fov_s;
1743        if self.fov != 0 {
1744            fov_s = self.fov.to_string();
1745            attrs.push(("fov", fov_s.as_str()));
1746        }
1747        let zoom_s;
1748        if self.zoom != 0 && self.zoom != 100000 {
1749            zoom_s = self.zoom.to_string();
1750            attrs.push(("zoom", zoom_s.as_str()));
1751        }
1752        if let Some(rot) = &self.rotation {
1753            w.open_with("a:camera", &attrs);
1754            rot.write_xml(w);
1755            w.close("a:camera");
1756        } else {
1757            w.empty_with("a:camera", &attrs);
1758        }
1759    }
1760}
1761
1762/// 光照设备预设类型(OOXML ST_LightRigType,常用子集)。
1763#[derive(Clone, Debug, Default, PartialEq, Eq)]
1764pub enum LightRigType {
1765    /// `balanced`(平衡光,默认)。
1766    #[default]
1767    Balanced,
1768    /// `bright`:明亮。
1769    Bright,
1770    /// `chilly`:冷光。
1771    Chilly,
1772    /// `contrasting`:对比光。
1773    Contrasting,
1774    /// `flat`:平光。
1775    Flat,
1776    /// `harsh`:强光。
1777    Harsh,
1778    /// `morning`:晨光。
1779    Morning,
1780    /// `soft`:柔光。
1781    Soft,
1782    /// `sunrise`:日出光。
1783    Sunrise,
1784    /// `sunset`:日落光。
1785    Sunset,
1786    /// `threePt`:三点光(Office 默认 effectStyle 中使用)。
1787    ThreePt,
1788    /// `twoPt`:两点光。
1789    TwoPt,
1790    /// 其它未显式枚举的光照类型(保留原始字符串)。
1791    Other(String),
1792}
1793
1794impl LightRigType {
1795    /// 转 OOXML 字符串。
1796    pub fn as_str(&self) -> &str {
1797        match self {
1798            LightRigType::Balanced => "balanced",
1799            LightRigType::Bright => "bright",
1800            LightRigType::Chilly => "chilly",
1801            LightRigType::Contrasting => "contrasting",
1802            LightRigType::Flat => "flat",
1803            LightRigType::Harsh => "harsh",
1804            LightRigType::Morning => "morning",
1805            LightRigType::Soft => "soft",
1806            LightRigType::Sunrise => "sunrise",
1807            LightRigType::Sunset => "sunset",
1808            LightRigType::ThreePt => "threePt",
1809            LightRigType::TwoPt => "twoPt",
1810            LightRigType::Other(s) => s.as_str(),
1811        }
1812    }
1813
1814    /// 从字符串解析。
1815    ///
1816    /// 注:方法名为 `parse` 而非 `from_str`,以避免与 `std::str::FromStr` trait 冲突。
1817    pub fn parse(s: &str) -> Self {
1818        match s {
1819            "balanced" => LightRigType::Balanced,
1820            "bright" => LightRigType::Bright,
1821            "chilly" => LightRigType::Chilly,
1822            "contrasting" => LightRigType::Contrasting,
1823            "flat" => LightRigType::Flat,
1824            "harsh" => LightRigType::Harsh,
1825            "morning" => LightRigType::Morning,
1826            "soft" => LightRigType::Soft,
1827            "sunrise" => LightRigType::Sunrise,
1828            "sunset" => LightRigType::Sunset,
1829            "threePt" => LightRigType::ThreePt,
1830            "twoPt" => LightRigType::TwoPt,
1831            other => LightRigType::Other(other.to_string()),
1832        }
1833    }
1834}
1835
1836/// 光照方向(OOXML ST_LightRigDirection)。
1837#[derive(Clone, Debug, Default, PartialEq, Eq)]
1838pub enum LightRigDirection {
1839    /// `tl`:左上。
1840    TopLeft,
1841    /// `t`:上(默认,Office effectStyle 中使用)。
1842    #[default]
1843    Top,
1844    /// `tr`:右上。
1845    TopRight,
1846    /// `l`:左。
1847    Left,
1848    /// `r`:右。
1849    Right,
1850    /// `bl`:左下。
1851    BottomLeft,
1852    /// `b`:下。
1853    Bottom,
1854    /// `br`:右下。
1855    BottomRight,
1856}
1857
1858impl LightRigDirection {
1859    /// 转 OOXML 字符串。
1860    pub fn as_str(&self) -> &str {
1861        match self {
1862            LightRigDirection::TopLeft => "tl",
1863            LightRigDirection::Top => "t",
1864            LightRigDirection::TopRight => "tr",
1865            LightRigDirection::Left => "l",
1866            LightRigDirection::Right => "r",
1867            LightRigDirection::BottomLeft => "bl",
1868            LightRigDirection::Bottom => "b",
1869            LightRigDirection::BottomRight => "br",
1870        }
1871    }
1872
1873    /// 从字符串解析。
1874    ///
1875    /// 注:方法名为 `parse` 而非 `from_str`,以避免与 `std::str::FromStr` trait 冲突。
1876    pub fn parse(s: &str) -> Self {
1877        match s {
1878            "tl" => LightRigDirection::TopLeft,
1879            "t" => LightRigDirection::Top,
1880            "tr" => LightRigDirection::TopRight,
1881            "l" => LightRigDirection::Left,
1882            "r" => LightRigDirection::Right,
1883            "bl" => LightRigDirection::BottomLeft,
1884            "b" => LightRigDirection::Bottom,
1885            "br" => LightRigDirection::BottomRight,
1886            _ => LightRigDirection::Top,
1887        }
1888    }
1889}
1890
1891/// 光照设备(`<a:lightRig>`,OOXML CT_LightRig)。
1892#[derive(Clone, Debug, Default, PartialEq, Eq)]
1893pub struct LightRig {
1894    /// 光照类型(默认 `balanced`)。
1895    pub rig: LightRigType,
1896    /// 光照方向(默认 `t`)。
1897    pub dir: LightRigDirection,
1898    /// 可选旋转(覆盖预设默认)。
1899    pub rotation: Option<Rotation3d>,
1900}
1901
1902impl LightRig {
1903    /// 写出 `<a:lightRig rig="..." dir="...">...</a:lightRig>`。
1904    pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
1905        let rig = self.rig.as_str();
1906        let dir = self.dir.as_str();
1907        if let Some(rot) = &self.rotation {
1908            w.open_with("a:lightRig", &[("rig", rig), ("dir", dir)]);
1909            rot.write_xml(w);
1910            w.close("a:lightRig");
1911        } else {
1912            w.empty_with("a:lightRig", &[("rig", rig), ("dir", dir)]);
1913        }
1914    }
1915}
1916
1917/// 三维场景(`<a:scene3d>`,OOXML CT_Scene3D)。
1918///
1919/// 包含相机与光照,定义观察形状的虚拟 3D 环境。
1920#[derive(Clone, Debug, Default, PartialEq, Eq)]
1921pub struct Scene3d {
1922    /// 相机(必填,默认 `orthographicFront`)。
1923    pub camera: Camera,
1924    /// 光照设备(必填,默认 `balanced` + `t`)。
1925    pub light_rig: LightRig,
1926    /// 三维场景背景(`<a:backdrop>`,可选,TODO-050)。
1927    ///
1928    /// 定义 6 个背景平面(地板/墙壁/左/右/顶/底),启用的平面会渲染为可见的背景面。
1929    /// `None` 表示不写出 `<a:backdrop>` 元素(与 Office 默认行为一致)。
1930    pub backdrop: Option<Backdrop>,
1931}
1932
1933impl Scene3d {
1934    /// 写出 `<a:scene3d>...</a:scene3d>`。
1935    ///
1936    /// # 元素顺序(OOXML CT_Scene3D)
1937    ///
1938    /// ```text
1939    /// <a:scene3d>
1940    ///   <a:camera>...</a:camera>        ← 必填
1941    ///   <a:lightRig>...</a:lightRig>    ← 必填
1942    ///   <a:backdrop>...</a:backdrop>    ← 可选(TODO-050)
1943    /// </a:scene3d>
1944    /// ```
1945    pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
1946        w.open("a:scene3d");
1947        self.camera.write_xml(w);
1948        self.light_rig.write_xml(w);
1949        if let Some(b) = &self.backdrop {
1950            b.write_xml(w);
1951        }
1952        w.close("a:scene3d");
1953    }
1954}
1955
1956/// 三维点(`<a:anchor>`,OOXML CT_Point3D)。
1957///
1958/// 用于 [`Backdrop`] 的锚点位置。三个坐标均以 EMU 为单位。
1959#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
1960pub struct Point3d {
1961    /// X 坐标(EMU)。
1962    pub x: i32,
1963    /// Y 坐标(EMU)。
1964    pub y: i32,
1965    /// Z 坐标(EMU)。
1966    pub z: i32,
1967}
1968
1969impl Point3d {
1970    /// 写出 `<a:anchor x="..." y="..." z="..."/>`(自闭合)。
1971    pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
1972        let xs = self.x.to_string();
1973        let ys = self.y.to_string();
1974        let zs = self.z.to_string();
1975        w.empty_with(
1976            "a:anchor",
1977            &[("x", xs.as_str()), ("y", ys.as_str()), ("z", zs.as_str())],
1978        );
1979    }
1980}
1981
1982/// 三维场景背景(`<a:backdrop>`,OOXML CT_Backdrop,TODO-050)。
1983///
1984/// 定义 3D 场景中的 6 个背景平面,每个平面可独立启用/禁用。
1985/// 启用的平面会渲染为可见的背景面(如地板/墙壁),用于营造空间感。
1986///
1987/// # OOXML 元素顺序
1988///
1989/// ```text
1990/// <a:backdrop>
1991///   <a:anchor x="..." y="..." z="..."/>   ← 可选:锚点位置
1992///   <a:floor/>                            ← 可选:地板平面
1993///   <a:wall/>                             ← 可选:后墙平面
1994///   <a:l/>                                ← 可选:左平面
1995///   <a:r/>                                ← 可选:右平面
1996///   <a:t/>                                ← 可选:顶平面
1997///   <a:b/>                                ← 可选:底平面
1998/// </a:backdrop>
1999/// ```
2000///
2001/// # 与 python-pptx 的对应
2002///
2003/// python-pptx 不支持 backdrop 编辑;本结构是 pptx-rs 的扩展能力。
2004#[derive(Clone, Debug, Default, PartialEq, Eq)]
2005pub struct Backdrop {
2006    /// 锚点位置(`<a:anchor>`)。
2007    ///
2008    /// 定义背景平面集合的参考原点(EMU 单位)。`None` 表示不写出 `<a:anchor>`。
2009    pub anchor: Option<Point3d>,
2010    /// 是否启用地板平面(`<a:floor/>`)。
2011    pub floor: bool,
2012    /// 是否启用后墙平面(`<a:wall/>`)。
2013    pub wall: bool,
2014    /// 是否启用左平面(`<a:l/>`)。
2015    pub left: bool,
2016    /// 是否启用右平面(`<a:r/>`)。
2017    pub right: bool,
2018    /// 是否启用顶平面(`<a:t/>`)。
2019    pub top: bool,
2020    /// 是否启用底平面(`<a:b/>`)。
2021    pub bottom: bool,
2022}
2023
2024impl Backdrop {
2025    /// 写出 `<a:backdrop>...</a:backdrop>`。
2026    ///
2027    /// 仅写出启用(`true`)的平面元素,未启用的平面不写出。
2028    pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
2029        w.open("a:backdrop");
2030        if let Some(a) = &self.anchor {
2031            a.write_xml(w);
2032        }
2033        if self.floor {
2034            w.empty("a:floor");
2035        }
2036        if self.wall {
2037            w.empty("a:wall");
2038        }
2039        if self.left {
2040            w.empty("a:l");
2041        }
2042        if self.right {
2043            w.empty("a:r");
2044        }
2045        if self.top {
2046            w.empty("a:t");
2047        }
2048        if self.bottom {
2049            w.empty("a:b");
2050        }
2051        w.close("a:backdrop");
2052    }
2053}
2054
2055/// 棱台(`<a:bevelT>` / `<a:bevelB>`,OOXML CT_Bevel)。
2056///
2057/// 宽高均以 EMU 为单位(典型值 63500 = 5pt)。
2058#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
2059pub struct Bevel {
2060    /// 棱台宽度(EMU)。
2061    pub w: i32,
2062    /// 棱台高度(EMU)。
2063    pub h: i32,
2064}
2065
2066impl Bevel {
2067    /// 写出 `<a:bevelT w="..." h="..."/>`(自闭合,tag 由调用方指定)。
2068    pub fn write_xml(&self, w: &mut super::writer::XmlWriter, tag: &str) {
2069        let w_s = self.w.to_string();
2070        let h_s = self.h.to_string();
2071        w.empty_with(tag, &[("w", w_s.as_str()), ("h", h_s.as_str())]);
2072    }
2073}
2074
2075/// 形状材质预设(OOXML ST_PresetMaterialType,常用子集)。
2076#[derive(Clone, Debug, Default, PartialEq, Eq)]
2077pub enum MaterialPreset {
2078    /// `warmMatte`(暖色哑光,默认)。
2079    #[default]
2080    WarmMatte,
2081    /// `clear`:透明。
2082    Clear,
2083    /// `dkEdge`:暗边缘。
2084    DarkEdge,
2085    /// `flat`:平面。
2086    Flat,
2087    /// `legacyMatte`:旧版哑光。
2088    LegacyMatte,
2089    /// `legacyMetallic`:旧版金属。
2090    LegacyMetallic,
2091    /// `legacyPlastic`:旧版塑料。
2092    LegacyPlastic,
2093    /// `legacyWireframe`:旧版线框。
2094    LegacyWireframe,
2095    /// `matte`:哑光。
2096    Matte,
2097    /// `metallic`:金属。
2098    Metallic,
2099    /// `plastic`:塑料。
2100    Plastic,
2101    /// `powder`:粉末。
2102    Powder,
2103    /// `softEdge`:柔边。
2104    SoftEdge,
2105    /// `softmetal`:软金属。
2106    SoftMetal,
2107    /// 其它未显式枚举的材质(保留原始字符串)。
2108    Other(String),
2109}
2110
2111impl MaterialPreset {
2112    /// 转 OOXML 字符串。
2113    pub fn as_str(&self) -> &str {
2114        match self {
2115            MaterialPreset::WarmMatte => "warmMatte",
2116            MaterialPreset::Clear => "clear",
2117            MaterialPreset::DarkEdge => "dkEdge",
2118            MaterialPreset::Flat => "flat",
2119            MaterialPreset::LegacyMatte => "legacyMatte",
2120            MaterialPreset::LegacyMetallic => "legacyMetallic",
2121            MaterialPreset::LegacyPlastic => "legacyPlastic",
2122            MaterialPreset::LegacyWireframe => "legacyWireframe",
2123            MaterialPreset::Matte => "matte",
2124            MaterialPreset::Metallic => "metallic",
2125            MaterialPreset::Plastic => "plastic",
2126            MaterialPreset::Powder => "powder",
2127            MaterialPreset::SoftEdge => "softEdge",
2128            MaterialPreset::SoftMetal => "softmetal",
2129            MaterialPreset::Other(s) => s.as_str(),
2130        }
2131    }
2132
2133    /// 从字符串解析。
2134    ///
2135    /// 注:方法名为 `parse` 而非 `from_str`,以避免与 `std::str::FromStr` trait 冲突。
2136    pub fn parse(s: &str) -> Self {
2137        match s {
2138            "warmMatte" => MaterialPreset::WarmMatte,
2139            "clear" => MaterialPreset::Clear,
2140            "dkEdge" => MaterialPreset::DarkEdge,
2141            "flat" => MaterialPreset::Flat,
2142            "legacyMatte" => MaterialPreset::LegacyMatte,
2143            "legacyMetallic" => MaterialPreset::LegacyMetallic,
2144            "legacyPlastic" => MaterialPreset::LegacyPlastic,
2145            "legacyWireframe" => MaterialPreset::LegacyWireframe,
2146            "matte" => MaterialPreset::Matte,
2147            "metallic" => MaterialPreset::Metallic,
2148            "plastic" => MaterialPreset::Plastic,
2149            "powder" => MaterialPreset::Powder,
2150            "softEdge" => MaterialPreset::SoftEdge,
2151            "softmetal" => MaterialPreset::SoftMetal,
2152            other => MaterialPreset::Other(other.to_string()),
2153        }
2154    }
2155}
2156
2157/// 形状 3D 属性(`<a:sp3d>`,OOXML CT_Shape3D)。
2158///
2159/// 定义形状本身的 3D 拉伸、棱台、材质。
2160#[derive(Clone, Debug, Default, PartialEq, Eq)]
2161pub struct Sp3d {
2162    /// 拉伸高度(EMU,典型值 38100 = 3pt)。
2163    pub extrusion_h: i32,
2164    /// 轮廓宽度(EMU)。
2165    pub contour_w: i32,
2166    /// 材质预设(默认 `warmMatte`)。
2167    pub prst_material: MaterialPreset,
2168    /// 顶部棱台(`<a:bevelT>`)。
2169    pub bevel_top: Option<Bevel>,
2170    /// 底部棱台(`<a:bevelB>`)。
2171    pub bevel_bottom: Option<Bevel>,
2172    /// 拉伸颜色(`<a:extrusionClr>`,`None` 表示不写出)。
2173    pub extrusion_color: Option<Color>,
2174    /// 轮廓颜色(`<a:contourClr>`,`None` 表示不写出)。
2175    pub contour_color: Option<Color>,
2176}
2177
2178impl Sp3d {
2179    /// 写出 `<a:sp3d>...</a:sp3d>`。
2180    pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
2181        let mut attrs: Vec<(&str, &str)> = Vec::new();
2182        let eh_s;
2183        if self.extrusion_h != 0 {
2184            eh_s = self.extrusion_h.to_string();
2185            attrs.push(("extrusionH", eh_s.as_str()));
2186        }
2187        let cw_s;
2188        if self.contour_w != 0 {
2189            cw_s = self.contour_w.to_string();
2190            attrs.push(("contourW", cw_s.as_str()));
2191        }
2192        // prstMaterial 仅在非默认时输出(避免覆盖默认值)
2193        let pm_s;
2194        if !matches!(self.prst_material, MaterialPreset::WarmMatte) {
2195            pm_s = self.prst_material.as_str().to_string();
2196            attrs.push(("prstMaterial", pm_s.as_str()));
2197        }
2198        let has_children = self.bevel_top.is_some()
2199            || self.bevel_bottom.is_some()
2200            || self.extrusion_color.is_some()
2201            || self.contour_color.is_some();
2202        if !has_children && attrs.is_empty() {
2203            w.empty("a:sp3d");
2204            return;
2205        }
2206        if !has_children {
2207            w.empty_with("a:sp3d", &attrs);
2208            return;
2209        }
2210        w.open_with("a:sp3d", &attrs);
2211        if let Some(b) = &self.bevel_top {
2212            b.write_xml(w, "a:bevelT");
2213        }
2214        if let Some(b) = &self.bevel_bottom {
2215            b.write_xml(w, "a:bevelB");
2216        }
2217        if let Some(c) = &self.extrusion_color {
2218            w.open("a:extrusionClr");
2219            c.write_solid_fill(w);
2220            w.close("a:extrusionClr");
2221        }
2222        if let Some(c) = &self.contour_color {
2223            w.open("a:contourClr");
2224            c.write_solid_fill(w);
2225            w.close("a:contourClr");
2226        }
2227        w.close("a:sp3d");
2228    }
2229}
2230
2231#[cfg(test)]
2232mod tests {
2233    use super::*;
2234    use crate::oxml::color::Color;
2235    use crate::oxml::writer::XmlWriter;
2236    use crate::units::RGBColor;
2237
2238    // --------------------- 调整值测试(TODO-038) ---------------------
2239
2240    /// `AdjustmentValue::new` 正确设置字段。
2241    #[test]
2242    fn adjustment_value_new() {
2243        let av = AdjustmentValue::new("adj", 16667);
2244        assert_eq!(av.name, "adj");
2245        assert_eq!(av.raw_value, 16667);
2246    }
2247
2248    /// `AdjustmentValue::from_normalized` 正确转换归一化值。
2249    #[test]
2250    fn adjustment_value_from_normalized() {
2251        let av = AdjustmentValue::from_normalized("adj", 0.5);
2252        assert_eq!(av.raw_value, 50000);
2253        let av2 = AdjustmentValue::from_normalized("adj1", 0.25);
2254        assert_eq!(av2.raw_value, 25000);
2255    }
2256
2257    /// `AdjustmentValue::effective_value` 正确返回归一化值。
2258    #[test]
2259    fn adjustment_value_effective_value() {
2260        let av = AdjustmentValue::new("adj", 16667);
2261        assert!((av.effective_value() - 0.16667).abs() < 0.0001);
2262    }
2263
2264    /// `AdjustmentValue::write_xml` 正确序列化 `<a:gd>` 元素。
2265    #[test]
2266    fn adjustment_value_write_xml() {
2267        let av = AdjustmentValue::new("adj", 16667);
2268        let mut w = XmlWriter::new();
2269        av.write_xml(&mut w);
2270        let xml = &w.buf;
2271        assert!(xml.contains("<a:gd"), "xml: {}", xml);
2272        assert!(xml.contains(r#"name="adj""#), "xml: {}", xml);
2273        assert!(xml.contains(r#"fmla="val 16667""#), "xml: {}", xml);
2274    }
2275
2276    /// `Geometry::Preset` 带调整值正确序列化 `<a:avLst>`。
2277    #[test]
2278    fn geometry_preset_with_adjustments_write_xml() {
2279        let geom = Geometry::Preset(
2280            PresetGeometry::RoundRectangle,
2281            vec![AdjustmentValue::new("adj", 16667)],
2282        );
2283        let mut w = XmlWriter::new();
2284        geom.write_xml(&mut w);
2285        let xml = &w.buf;
2286        assert!(xml.contains(r#"prst="roundRect""#), "xml: {}", xml);
2287        assert!(xml.contains("<a:avLst>"), "xml: {}", xml);
2288        assert!(xml.contains(r#"fmla="val 16667""#), "xml: {}", xml);
2289        assert!(xml.contains("</a:avLst>"), "xml: {}", xml);
2290    }
2291
2292    /// `Geometry::Preset` 无调整值时写出空 `<a:avLst/>`。
2293    #[test]
2294    fn geometry_preset_no_adjustments_write_xml() {
2295        let geom = Geometry::preset(PresetGeometry::Rectangle);
2296        let mut w = XmlWriter::new();
2297        geom.write_xml(&mut w);
2298        assert!(w.buf.contains("<a:avLst/>"), "xml: {}", w.buf);
2299    }
2300
2301    /// `Geometry::preset` 辅助方法创建无调整值的预设几何。
2302    #[test]
2303    fn geometry_preset_helper() {
2304        let geom = Geometry::preset(PresetGeometry::Ellipse);
2305        match geom {
2306            Geometry::Preset(p, adj) => {
2307                assert_eq!(p, PresetGeometry::Ellipse);
2308                assert!(adj.is_empty());
2309            }
2310            _ => panic!("应为 Preset 变体"),
2311        }
2312    }
2313
2314    // --------------------- 其他测试 ---------------------
2315
2316    /// 验证 `FillFormat::gradient()` 切到渐变模式并返回可变引用。
2317    #[test]
2318    fn fill_format_gradient_switches_mode() {
2319        let mut fill = Fill::Inherit;
2320        let mut fmt = FillFormat::new(&mut fill);
2321        let g = fmt.gradient();
2322        g.stops.push(GradientStop {
2323            pos: 0,
2324            color: Color::RGB(RGBColor(0xFF, 0x00, 0x00)),
2325        });
2326        g.stops.push(GradientStop {
2327            pos: 100_000,
2328            color: Color::RGB(RGBColor(0x00, 0x00, 0xFF)),
2329        });
2330        g.gradient_type = GradientType::Linear(2_700_000);
2331        assert!(matches!(fill, Fill::Gradient(_)));
2332        if let Fill::Gradient(g) = &fill {
2333            assert_eq!(g.stops.len(), 2);
2334            assert_eq!(g.stops[0].pos, 0);
2335            assert_eq!(g.stops[1].pos, 100_000);
2336            assert!(matches!(g.gradient_type, GradientType::Linear(2_700_000)));
2337        }
2338    }
2339
2340    /// 验证 `FillFormat::set_gradient_linear` 便捷方法。
2341    #[test]
2342    fn fill_format_set_gradient_linear() {
2343        let mut fill = Fill::Inherit;
2344        let mut fmt = FillFormat::new(&mut fill);
2345        let stops = vec![
2346            GradientStop {
2347                pos: 0,
2348                color: Color::RGB(RGBColor(0xFF, 0x00, 0x00)),
2349            },
2350            GradientStop {
2351                pos: 100_000,
2352                color: Color::RGB(RGBColor(0x00, 0xFF, 0x00)),
2353            },
2354        ];
2355        fmt.set_gradient_linear(stops, 5_400_000);
2356        assert!(matches!(fill, Fill::Gradient(_)));
2357        if let Fill::Gradient(g) = &fill {
2358            assert_eq!(g.stops.len(), 2);
2359            assert!(matches!(g.gradient_type, GradientType::Linear(5_400_000)));
2360        }
2361    }
2362
2363    /// 验证 `FillFormat::set_gradient_path` 便捷方法。
2364    #[test]
2365    fn fill_format_set_gradient_path() {
2366        let mut fill = Fill::Inherit;
2367        let mut fmt = FillFormat::new(&mut fill);
2368        let stops = vec![GradientStop {
2369            pos: 50_000,
2370            color: Color::RGB(RGBColor(0x80, 0x80, 0x80)),
2371        }];
2372        fmt.set_gradient_path(stops, GradientPath::Circle);
2373        if let Fill::Gradient(g) = &fill {
2374            assert!(matches!(
2375                g.gradient_type,
2376                GradientType::Path(GradientPath::Circle)
2377            ));
2378        }
2379    }
2380
2381    /// 验证 `FillFormat::pattern()` 切到图案模式并返回可变引用。
2382    #[test]
2383    fn fill_format_pattern_switches_mode() {
2384        let mut fill = Fill::Inherit;
2385        let mut fmt = FillFormat::new(&mut fill);
2386        let p = fmt.pattern();
2387        p.prst = "horz".to_string();
2388        p.fg_color = Color::RGB(RGBColor(0xFF, 0x00, 0x00));
2389        p.bg_color = Color::RGB(RGBColor(0xFF, 0xFF, 0xFF));
2390        assert!(matches!(fill, Fill::Pattern(_)));
2391        if let Fill::Pattern(p) = &fill {
2392            assert_eq!(p.prst, "horz");
2393        }
2394    }
2395
2396    /// 验证 `FillFormat::set_pattern` 便捷方法。
2397    #[test]
2398    fn fill_format_set_pattern() {
2399        let mut fill = Fill::Inherit;
2400        let mut fmt = FillFormat::new(&mut fill);
2401        fmt.set_pattern(
2402            "cross",
2403            Color::RGB(RGBColor(0x00, 0x00, 0x00)),
2404            Color::RGB(RGBColor(0xFF, 0xFF, 0xFF)),
2405        );
2406        if let Fill::Pattern(p) = &fill {
2407            assert_eq!(p.prst, "cross");
2408            assert!(matches!(p.fg_color, Color::RGB(_)));
2409            assert!(matches!(p.bg_color, Color::RGB(_)));
2410        } else {
2411            panic!("应为 Pattern 变体");
2412        }
2413    }
2414
2415    /// 验证 `fill_type()` 对渐变和图案返回正确的枚举值。
2416    #[test]
2417    fn fill_format_fill_type_for_gradient_and_pattern() {
2418        let mut fill = Fill::Gradient(GradientFill {
2419            stops: vec![],
2420            gradient_type: GradientType::Linear(0),
2421            flip: None,
2422            rot_with_shape: None,
2423        });
2424        let fmt = FillFormat::new(&mut fill);
2425        assert_eq!(
2426            fmt.fill_type(),
2427            crate::oxml::simpletypes::MsoFillType::Gradient
2428        );
2429
2430        let mut fill = Fill::Pattern(PatternFill::default());
2431        let fmt = FillFormat::new(&mut fill);
2432        assert_eq!(
2433            fmt.fill_type(),
2434            crate::oxml::simpletypes::MsoFillType::Pattern
2435        );
2436    }
2437
2438    /// 验证 `EffectList::is_empty` 和 `write_xml`(外阴影)。
2439    #[test]
2440    fn effect_list_outer_shadow_serialization() {
2441        let mut effects = EffectList::default();
2442        assert!(effects.is_empty());
2443        effects.outer_shadow = Some(ShadowEffect {
2444            dir: 2_700_000,
2445            dist: 38100,
2446            blur_rad: 40000,
2447            color: Color::RGB(RGBColor(0x00, 0x00, 0x00)),
2448            rot_with_shape: Some(false),
2449        });
2450        assert!(!effects.is_empty());
2451
2452        let mut w = XmlWriter::new();
2453        effects.write_xml(&mut w);
2454        let buf = &w.buf;
2455        assert!(buf.contains("<a:effectLst>"));
2456        assert!(buf.contains("<a:outerShdw"));
2457        assert!(buf.contains("dir=\"2700000\""));
2458        assert!(buf.contains("dist=\"38100\""));
2459        assert!(buf.contains("blurRad=\"40000\""));
2460        assert!(buf.contains("rotWithShape=\"0\""));
2461        assert!(buf.contains("<a:srgbClr val=\"000000\""));
2462        assert!(buf.contains("</a:effectLst>"));
2463    }
2464
2465    /// 验证 `EffectList` 发光和柔化边缘序列化。
2466    #[test]
2467    fn effect_list_glow_and_soft_edge() {
2468        let effects = EffectList {
2469            glow: Some(GlowEffect {
2470                rad: 50000,
2471                color: Color::RGB(RGBColor(0xFF, 0x00, 0xFF)),
2472            }),
2473            soft_edge: Some(SoftEdgeEffect { rad: 25000 }),
2474            ..Default::default()
2475        };
2476
2477        let mut w = XmlWriter::new();
2478        effects.write_xml(&mut w);
2479        let buf = &w.buf;
2480        assert!(buf.contains("<a:glow rad=\"50000\">"));
2481        assert!(buf.contains("<a:srgbClr val=\"FF00FF\""));
2482        assert!(buf.contains("<a:softEdge rad=\"25000\"/>"));
2483    }
2484
2485    /// 验证 `EffectList` 反射效果序列化(仅属性,无子元素)。
2486    #[test]
2487    fn effect_list_reflection_serialization() {
2488        let effects = EffectList {
2489            reflection: Some(ReflectionEffect {
2490                blur_rad: Some(50000),
2491                st_a: Some(52000),
2492                st_pos: Some(0),
2493                end_a: Some(30000),
2494                end_pos: Some(50000),
2495                dist: Some(38100),
2496                dir: Some(5_400_000),
2497                rot_with_shape: None,
2498            }),
2499            ..Default::default()
2500        };
2501
2502        let mut w = XmlWriter::new();
2503        effects.write_xml(&mut w);
2504        let buf = &w.buf;
2505        assert!(buf.contains("<a:reflection"));
2506        assert!(buf.contains("blurRad=\"50000\""));
2507        assert!(buf.contains("stA=\"52000\""));
2508        assert!(buf.contains("endPos=\"50000\""));
2509        assert!(buf.contains("dist=\"38100\""));
2510        assert!(buf.contains("/>")); // 自闭合
2511    }
2512
2513    /// 验证空 `EffectList` 不写出任何 XML。
2514    #[test]
2515    fn effect_list_empty_writes_nothing() {
2516        let effects = EffectList::default();
2517        let mut w = XmlWriter::new();
2518        effects.write_xml(&mut w);
2519        assert!(w.buf.is_empty());
2520    }
2521
2522    // --------------------- TODO-046: 图片填充模式测试 ---------------------
2523
2524    /// 验证 `BlipFillMode::Stretch` 序列化。
2525    #[test]
2526    fn blip_fill_mode_stretch_serialize() {
2527        let mode = BlipFillMode::Stretch;
2528        let mut w = XmlWriter::new();
2529        mode.write_xml(&mut w);
2530        let s = &w.buf;
2531        assert!(s.contains("<a:stretch>"), "应输出 a:stretch,实际: {s}");
2532        assert!(s.contains("<a:fillRect/>"), "应输出 a:fillRect,实际: {s}");
2533        assert!(s.contains("</a:stretch>"), "应关闭 a:stretch,实际: {s}");
2534    }
2535
2536    /// 验证 `BlipFillMode::Tile` 序列化(带全部属性)。
2537    #[test]
2538    fn blip_fill_mode_tile_serialize_full() {
2539        let mode = BlipFillMode::Tile {
2540            tx: Some(914400),
2541            ty: Some(457200),
2542            sx: Some(100_000),
2543            sy: Some(50_000),
2544            flip: Some("x".to_string()),
2545            algn: Some("tl".to_string()),
2546        };
2547        let mut w = XmlWriter::new();
2548        mode.write_xml(&mut w);
2549        let s = &w.buf;
2550        assert!(s.contains("<a:tile"), "应输出 a:tile,实际: {s}");
2551        assert!(s.contains("tx=\"914400\""), "应包含 tx,实际: {s}");
2552        assert!(s.contains("ty=\"457200\""), "应包含 ty,实际: {s}");
2553        assert!(s.contains("sx=\"100000\""), "应包含 sx,实际: {s}");
2554        assert!(s.contains("sy=\"50000\""), "应包含 sy,实际: {s}");
2555        assert!(s.contains("flip=\"x\""), "应包含 flip,实际: {s}");
2556        assert!(s.contains("algn=\"tl\""), "应包含 algn,实际: {s}");
2557        assert!(s.contains("/>"), "应为自闭合标签,实际: {s}");
2558    }
2559
2560    /// 验证 `BlipFillMode::Tile` 序列化(仅部分属性)。
2561    #[test]
2562    fn blip_fill_mode_tile_serialize_partial() {
2563        let mode = BlipFillMode::Tile {
2564            tx: None,
2565            ty: None,
2566            sx: Some(200_000),
2567            sy: None,
2568            flip: None,
2569            algn: Some("ctr".to_string()),
2570        };
2571        let mut w = XmlWriter::new();
2572        mode.write_xml(&mut w);
2573        let s = &w.buf;
2574        assert!(s.contains("<a:tile"), "应输出 a:tile,实际: {s}");
2575        assert!(!s.contains("tx="), "不应包含 tx,实际: {s}");
2576        assert!(!s.contains("ty="), "不应包含 ty,实际: {s}");
2577        assert!(s.contains("sx=\"200000\""), "应包含 sx,实际: {s}");
2578        assert!(!s.contains("sy="), "不应包含 sy,实际: {s}");
2579        assert!(s.contains("algn=\"ctr\""), "应包含 algn,实际: {s}");
2580    }
2581
2582    /// 验证 `BlipFillMode::None` 不写出任何 XML。
2583    #[test]
2584    fn blip_fill_mode_none_serialize() {
2585        let mode = BlipFillMode::None;
2586        let mut w = XmlWriter::new();
2587        mode.write_xml(&mut w);
2588        assert!(w.buf.is_empty(), "None 模式不应写出任何 XML");
2589    }
2590
2591    /// 验证 `Fill::Blip` 使用 `BlipFillMode::Stretch` 序列化。
2592    #[test]
2593    fn fill_blip_with_stretch_serialize() {
2594        let fill = Fill::Blip {
2595            rid: "rIdImg1".to_string(),
2596            mode: BlipFillMode::Stretch,
2597        };
2598        let mut w = XmlWriter::new();
2599        fill.write_xml(&mut w);
2600        let s = &w.buf;
2601        assert!(s.contains("<a:blipFill"), "应输出 a:blipFill,实际: {s}");
2602        assert!(
2603            s.contains("r:embed=\"rIdImg1\""),
2604            "应包含 r:embed,实际: {s}"
2605        );
2606        assert!(s.contains("<a:stretch>"), "应输出 a:stretch,实际: {s}");
2607        // BUG-001 回归检测:<a:blip 标签应只出现 1 次(不应有嵌套的双重标签)
2608        // 注意:不能用 matches("<a:blip"),因为它会同时匹配 <a:blipFill
2609        let blip_count = s.matches("<a:blip ").count()
2610            + s.matches("<a:blip/>").count()
2611            + s.matches("<a:blip>").count();
2612        assert_eq!(
2613            blip_count, 1,
2614            "应仅输出 1 个 <a:blip 标签,实际 {} 次(可能存在双重标签 bug): {s}",
2615            blip_count
2616        );
2617        // 同时验证不会出现嵌套的 <a:blip><a:blip
2618        assert!(
2619            !s.contains("<a:blip><a:blip"),
2620            "检测到嵌套的双重 <a:blip> 标签(BUG-001 回归): {s}"
2621        );
2622    }
2623
2624    /// 验证 `Fill::Blip` 使用 `BlipFillMode::Tile` 序列化。
2625    #[test]
2626    fn fill_blip_with_tile_serialize() {
2627        let fill = Fill::Blip {
2628            rid: "rIdImg2".to_string(),
2629            mode: BlipFillMode::Tile {
2630                tx: Some(100),
2631                ty: None,
2632                sx: None,
2633                sy: None,
2634                flip: Some("xy".to_string()),
2635                algn: None,
2636            },
2637        };
2638        let mut w = XmlWriter::new();
2639        fill.write_xml(&mut w);
2640        let s = &w.buf;
2641        assert!(s.contains("<a:blipFill"), "应输出 a:blipFill,实际: {s}");
2642        assert!(
2643            s.contains("r:embed=\"rIdImg2\""),
2644            "应包含 r:embed,实际: {s}"
2645        );
2646        assert!(s.contains("<a:tile"), "应输出 a:tile,实际: {s}");
2647        assert!(s.contains("tx=\"100\""), "应包含 tx,实际: {s}");
2648        assert!(s.contains("flip=\"xy\""), "应包含 flip,实际: {s}");
2649    }
2650
2651    /// 验证 `BlipFillMode` 默认值为 `Stretch`。
2652    #[test]
2653    fn blip_fill_mode_default_is_stretch() {
2654        let mode = BlipFillMode::default();
2655        assert!(matches!(mode, BlipFillMode::Stretch));
2656    }
2657
2658    // --------------------- TODO-050: 三维效果测试 ---------------------
2659
2660    /// 验证 `Rotation3d` 序列化为自闭合 `<a:rot/>`。
2661    #[test]
2662    fn rotation_3d_serialize() {
2663        let rot = Rotation3d {
2664            lat: 0,
2665            lon: 0,
2666            rev: 1200000,
2667        };
2668        let mut w = XmlWriter::new();
2669        rot.write_xml(&mut w);
2670        let s = &w.buf;
2671        assert!(s.contains("<a:rot"), "应输出 a:rot,实际: {s}");
2672        assert!(s.contains("lat=\"0\""), "应包含 lat,实际: {s}");
2673        assert!(s.contains("lon=\"0\""), "应包含 lon,实际: {s}");
2674        assert!(s.contains("rev=\"1200000\""), "应包含 rev,实际: {s}");
2675        assert!(s.contains("/>"), "应为自闭合,实际: {s}");
2676    }
2677
2678    /// 验证 `Camera` 序列化(默认预设、无旋转)。
2679    #[test]
2680    fn camera_default_serialize() {
2681        let camera = Camera::default();
2682        let mut w = XmlWriter::new();
2683        camera.write_xml(&mut w);
2684        let s = &w.buf;
2685        assert!(
2686            s.contains("<a:camera prst=\"orthographicFront\""),
2687            "应包含默认 prst,实际: {s}"
2688        );
2689        // 默认 fov=0/zoom=0 不应输出
2690        assert!(!s.contains("fov="), "默认 fov 不应输出,实际: {s}");
2691        assert!(!s.contains("zoom="), "默认 zoom 不应输出,实际: {s}");
2692    }
2693
2694    /// 验证 `Camera` 带旋转的序列化。
2695    #[test]
2696    fn camera_with_rotation_serialize() {
2697        let camera = Camera {
2698            preset: CameraPreset::PerspectiveFront,
2699            fov: 3600000,
2700            // 注意:zoom=100000 是 OOXML 默认值,write_xml 会省略;
2701            // 这里用 200000 测试非默认值的输出。
2702            zoom: 200000,
2703            rotation: Some(Rotation3d {
2704                lat: 30,
2705                lon: 45,
2706                rev: 0,
2707            }),
2708        };
2709        let mut w = XmlWriter::new();
2710        camera.write_xml(&mut w);
2711        let s = &w.buf;
2712        assert!(
2713            s.contains("<a:camera prst=\"perspectiveFront\""),
2714            "应包含 perspectiveFront,实际: {s}"
2715        );
2716        assert!(s.contains("fov=\"3600000\""), "应包含 fov,实际: {s}");
2717        assert!(s.contains("zoom=\"200000\""), "应包含 zoom,实际: {s}");
2718        assert!(s.contains("<a:rot"), "应包含 a:rot 子元素,实际: {s}");
2719        assert!(s.contains("</a:camera>"), "应关闭 a:camera,实际: {s}");
2720    }
2721
2722    /// 验证 `Scene3d` 完整序列化(相机 + 光照)。
2723    #[test]
2724    fn scene3d_full_serialize() {
2725        let scene = Scene3d {
2726            camera: Camera {
2727                preset: CameraPreset::OrthographicFront,
2728                fov: 0,
2729                zoom: 0,
2730                rotation: Some(Rotation3d {
2731                    lat: 0,
2732                    lon: 0,
2733                    rev: 0,
2734                }),
2735            },
2736            light_rig: LightRig {
2737                rig: LightRigType::ThreePt,
2738                dir: LightRigDirection::Top,
2739                rotation: Some(Rotation3d {
2740                    lat: 0,
2741                    lon: 0,
2742                    rev: 1200000,
2743                }),
2744            },
2745            backdrop: None,
2746        };
2747        let mut w = XmlWriter::new();
2748        scene.write_xml(&mut w);
2749        let s = &w.buf;
2750        assert!(s.contains("<a:scene3d>"), "应输出 a:scene3d,实际: {s}");
2751        assert!(
2752            s.contains("<a:camera prst=\"orthographicFront\">"),
2753            "应包含 camera 子元素,实际: {s}"
2754        );
2755        assert!(
2756            s.contains("<a:lightRig rig=\"threePt\" dir=\"t\">"),
2757            "应包含 lightRig 子元素,实际: {s}"
2758        );
2759        assert!(s.contains("</a:scene3d>"), "应关闭 a:scene3d,实际: {s}");
2760    }
2761
2762    /// 验证 `Sp3d` 默认序列化(仅 prstMaterial 默认值,输出空 `<a:sp3d/>`)。
2763    #[test]
2764    fn sp3d_default_serialize() {
2765        let sp3d = Sp3d::default();
2766        let mut w = XmlWriter::new();
2767        sp3d.write_xml(&mut w);
2768        let s = &w.buf;
2769        assert!(s.contains("<a:sp3d"), "应输出 a:sp3d,实际: {s}");
2770        // 默认 warmMatte 不输出 prstMaterial
2771        assert!(
2772            !s.contains("prstMaterial="),
2773            "默认 warmMatte 不应输出,实际: {s}"
2774        );
2775    }
2776
2777    /// 验证 `Sp3d` 带棱台和颜色的序列化。
2778    #[test]
2779    fn sp3d_with_bevel_and_colors_serialize() {
2780        let sp3d = Sp3d {
2781            extrusion_h: 38100,
2782            contour_w: 12700,
2783            prst_material: MaterialPreset::Metallic,
2784            bevel_top: Some(Bevel { w: 63500, h: 25400 }),
2785            bevel_bottom: None,
2786            extrusion_color: Some(Color::RGB(crate::units::RGBColor::BLACK)),
2787            contour_color: Some(Color::RGB(crate::units::RGBColor::WHITE)),
2788        };
2789        let mut w = XmlWriter::new();
2790        sp3d.write_xml(&mut w);
2791        let s = &w.buf;
2792        assert!(
2793            s.contains("extrusionH=\"38100\""),
2794            "应包含 extrusionH,实际: {s}"
2795        );
2796        assert!(
2797            s.contains("contourW=\"12700\""),
2798            "应包含 contourW,实际: {s}"
2799        );
2800        assert!(
2801            s.contains("prstMaterial=\"metallic\""),
2802            "应包含 prstMaterial,实际: {s}"
2803        );
2804        assert!(
2805            s.contains("<a:bevelT w=\"63500\" h=\"25400\"/>"),
2806            "应包含 bevelT,实际: {s}"
2807        );
2808        assert!(
2809            s.contains("<a:extrusionClr>"),
2810            "应包含 extrusionClr,实际: {s}"
2811        );
2812        assert!(s.contains("<a:contourClr>"), "应包含 contourClr,实际: {s}");
2813        assert!(s.contains("</a:sp3d>"), "应关闭 a:sp3d,实际: {s}");
2814    }
2815
2816    /// 验证 `ShapeProperties` 中 scene3d/sp3d 在 effectLst 之后输出。
2817    #[test]
2818    fn shape_properties_with_3d_serialize() {
2819        let sp = ShapeProperties {
2820            scene3d: Some(Scene3d::default()),
2821            sp3d: Some(Sp3d::default()),
2822            ..Default::default()
2823        };
2824        let mut w = XmlWriter::new();
2825        sp.write_xml(&mut w, "p:spPr");
2826        let s = &w.buf;
2827        // 验证 scene3d 与 sp3d 都被输出
2828        assert!(s.contains("<a:scene3d>"), "应输出 scene3d,实际: {s}");
2829        assert!(s.contains("<a:sp3d"), "应输出 sp3d,实际: {s}");
2830        // 验证顺序:scene3d 在 sp3d 之前
2831        let scene_pos = s.find("<a:scene3d>").expect("scene3d 应存在");
2832        let sp3d_pos = s.find("<a:sp3d").expect("sp3d 应存在");
2833        assert!(
2834            scene_pos < sp3d_pos,
2835            "scene3d 应在 sp3d 之前,实际 scene3d@{scene_pos} sp3d@{sp3d_pos}"
2836        );
2837    }
2838
2839    /// 验证 `CameraPreset::parse` 解析已知预设。
2840    #[test]
2841    fn camera_preset_from_str() {
2842        assert!(matches!(
2843            CameraPreset::parse("orthographicFront"),
2844            CameraPreset::OrthographicFront
2845        ));
2846        assert!(matches!(
2847            CameraPreset::parse("perspectiveFront"),
2848            CameraPreset::PerspectiveFront
2849        ));
2850        // 未知值归入 Other
2851        match CameraPreset::parse("customUnknown") {
2852            CameraPreset::Other(s) => assert_eq!(s, "customUnknown"),
2853            other => panic!("未知预设应归入 Other,实际: {other:?}"),
2854        }
2855    }
2856
2857    /// 验证 `MaterialPreset::parse` 解析已知材质。
2858    #[test]
2859    fn material_preset_from_str() {
2860        assert!(matches!(
2861            MaterialPreset::parse("warmMatte"),
2862            MaterialPreset::WarmMatte
2863        ));
2864        assert!(matches!(
2865            MaterialPreset::parse("metallic"),
2866            MaterialPreset::Metallic
2867        ));
2868        // 未知值归入 Other
2869        match MaterialPreset::parse("futureMaterial") {
2870            MaterialPreset::Other(s) => assert_eq!(s, "futureMaterial"),
2871            other => panic!("未知材质应归入 Other,实际: {other:?}"),
2872        }
2873    }
2874
2875    // ===================== TODO-050 backdrop 背景元素测试 =====================
2876
2877    /// 验证 `Backdrop` 默认序列化(无平面启用,仅空 backdrop)。
2878    #[test]
2879    fn backdrop_default_serialize() {
2880        let bd = Backdrop::default();
2881        let mut w = XmlWriter::new();
2882        bd.write_xml(&mut w);
2883        let s = &w.buf;
2884        assert!(s.contains("<a:backdrop>"), "应输出 a:backdrop,实际: {s}");
2885        assert!(s.contains("</a:backdrop>"), "应关闭 a:backdrop,实际: {s}");
2886        // 默认所有平面未启用,不应输出 a:floor/a:wall/a:l/a:r/a:t/a:b
2887        // 注意:用 <a:xxx/> 精确匹配,避免 <a:b 误匹配 <a:backdrop>
2888        assert!(!s.contains("<a:floor/>"), "默认不应输出 floor: {s}");
2889        assert!(!s.contains("<a:wall/>"), "默认不应输出 wall: {s}");
2890        assert!(!s.contains("<a:l/>"), "默认不应输出 l: {s}");
2891        assert!(!s.contains("<a:r/>"), "默认不应输出 r: {s}");
2892        assert!(!s.contains("<a:t/>"), "默认不应输出 t: {s}");
2893        assert!(!s.contains("<a:b/>"), "默认不应输出 b: {s}");
2894    }
2895
2896    /// 验证 `Backdrop` 带锚点 + 全平面启用的序列化。
2897    #[test]
2898    fn backdrop_with_anchor_and_all_planes_serialize() {
2899        let bd = Backdrop {
2900            anchor: Some(Point3d {
2901                x: 100000,
2902                y: 200000,
2903                z: 300000,
2904            }),
2905            floor: true,
2906            wall: true,
2907            left: true,
2908            right: true,
2909            top: true,
2910            bottom: true,
2911        };
2912        let mut w = XmlWriter::new();
2913        bd.write_xml(&mut w);
2914        let s = &w.buf;
2915        // 锚点
2916        assert!(
2917            s.contains("<a:anchor x=\"100000\" y=\"200000\" z=\"300000\"/>"),
2918            "应包含 anchor,实际: {s}"
2919        );
2920        // 6 个平面(按 OOXML 顺序)
2921        assert!(s.contains("<a:floor/>"), "应包含 floor: {s}");
2922        assert!(s.contains("<a:wall/>"), "应包含 wall: {s}");
2923        assert!(s.contains("<a:l/>"), "应包含 l: {s}");
2924        assert!(s.contains("<a:r/>"), "应包含 r: {s}");
2925        assert!(s.contains("<a:t/>"), "应包含 t: {s}");
2926        assert!(s.contains("<a:b/>"), "应包含 b: {s}");
2927        // 验证顺序:anchor → floor → wall → l → r → t → b
2928        // 注意:用 <a:xxx/> 精确匹配,避免 <a:b 误匹配 <a:backdrop>
2929        let anchor_pos = s.find("<a:anchor").expect("anchor 应存在");
2930        let floor_pos = s.find("<a:floor/>").expect("floor 应存在");
2931        let wall_pos = s.find("<a:wall/>").expect("wall 应存在");
2932        let l_pos = s.find("<a:l/>").expect("l 应存在");
2933        let r_pos = s.find("<a:r/>").expect("r 应存在");
2934        let t_pos = s.find("<a:t/>").expect("t 应存在");
2935        let b_pos = s.find("<a:b/>").expect("b 应存在");
2936        assert!(anchor_pos < floor_pos, "anchor 应在 floor 之前");
2937        assert!(floor_pos < wall_pos, "floor 应在 wall 之前");
2938        assert!(wall_pos < l_pos, "wall 应在 l 之前");
2939        assert!(l_pos < r_pos, "l 应在 r 之前");
2940        assert!(r_pos < t_pos, "r 应在 t 之前");
2941        assert!(t_pos < b_pos, "t 应在 b 之前");
2942    }
2943
2944    /// 验证 `Backdrop` 仅启用部分平面(floor + wall)。
2945    #[test]
2946    fn backdrop_partial_planes_serialize() {
2947        let bd = Backdrop {
2948            floor: true,
2949            wall: true,
2950            ..Default::default()
2951        };
2952        let mut w = XmlWriter::new();
2953        bd.write_xml(&mut w);
2954        let s = &w.buf;
2955        assert!(s.contains("<a:floor/>"), "应包含 floor: {s}");
2956        assert!(s.contains("<a:wall/>"), "应包含 wall: {s}");
2957        // 注意:用 <a:xxx/> 精确匹配,避免 <a:b 误匹配 <a:backdrop>
2958        assert!(!s.contains("<a:l/>"), "不应包含 l: {s}");
2959        assert!(!s.contains("<a:r/>"), "不应包含 r: {s}");
2960        assert!(!s.contains("<a:t/>"), "不应包含 t: {s}");
2961        assert!(!s.contains("<a:b/>"), "不应包含 b: {s}");
2962    }
2963
2964    /// 验证 `Scene3d` 带 backdrop 的完整序列化。
2965    #[test]
2966    fn scene3d_with_backdrop_serialize() {
2967        let scene = Scene3d {
2968            camera: Camera::default(),
2969            light_rig: LightRig::default(),
2970            backdrop: Some(Backdrop {
2971                floor: true,
2972                wall: true,
2973                ..Default::default()
2974            }),
2975        };
2976        let mut w = XmlWriter::new();
2977        scene.write_xml(&mut w);
2978        let s = &w.buf;
2979        assert!(s.contains("<a:scene3d>"), "应包含 scene3d: {s}");
2980        assert!(s.contains("<a:backdrop>"), "应包含 backdrop: {s}");
2981        assert!(s.contains("<a:floor/>"), "应包含 floor: {s}");
2982        assert!(s.contains("<a:wall/>"), "应包含 wall: {s}");
2983        // 验证顺序:camera → lightRig → backdrop
2984        let cam_pos = s.find("<a:camera").expect("camera 应存在");
2985        let rig_pos = s.find("<a:lightRig").expect("lightRig 应存在");
2986        let bd_pos = s.find("<a:backdrop>").expect("backdrop 应存在");
2987        assert!(cam_pos < rig_pos, "camera 应在 lightRig 之前");
2988        assert!(rig_pos < bd_pos, "lightRig 应在 backdrop 之前");
2989    }
2990
2991    /// 验证 `Scene3d` 无 backdrop 时不输出 backdrop 元素(向后兼容)。
2992    #[test]
2993    fn scene3d_without_backdrop_no_element() {
2994        let scene = Scene3d::default();
2995        let mut w = XmlWriter::new();
2996        scene.write_xml(&mut w);
2997        let s = &w.buf;
2998        assert!(s.contains("<a:scene3d>"), "应包含 scene3d: {s}");
2999        assert!(!s.contains("<a:backdrop>"), "无 backdrop 时不应输出: {s}");
3000    }
3001
3002    /// 验证 `Point3d` 序列化坐标值。
3003    #[test]
3004    fn point3d_serialize() {
3005        let p = Point3d {
3006            x: -100000,
3007            y: 0,
3008            z: 500000,
3009        };
3010        let mut w = XmlWriter::new();
3011        p.write_xml(&mut w);
3012        let s = &w.buf;
3013        assert!(s.contains("x=\"-100000\""), "应包含 x: {s}");
3014        assert!(s.contains("y=\"0\""), "应包含 y: {s}");
3015        assert!(s.contains("z=\"500000\""), "应包含 z: {s}");
3016    }
3017}