Skip to main content

pptx_rs/shape/
autoshape.rs

1//! `AutoShape`:自选图形(矩形、椭圆、箭头、…)。
2//!
3//! [`AutoShape`] 是本库"几何 + 文本"的通用形状——文本框、矩形、椭圆、箭头
4//! 等在 python-pptx 中也都被归为 `Shape`(区别于 `TextBox` 是早期 OOXML 的
5//! 历史残留)。本库把 `TextBox` 单独拆出,主要为了类型可读性。
6//!
7//! # 与 python-pptx 的对应
8//!
9//! - `pptx.shapes.autoshape.Shape` ←→ [`AutoShape`];
10//! - `pptx.shapes.textbox.TextBox` ←→ [`crate::shape::TextBox`]。
11//!
12//! # 几何与位置
13//!
14//! - 几何由 [`PresetGeometry`] 决定(`rect` / `ellipse` / `arrow` / ...);
15//! - 位置 / 尺寸 / 旋转在 [`crate::oxml::sppr::Transform`] 上;
16//! - 文本由 [`TextBody`] 承载,可含多段多 Run。
17//!
18//! # 示例
19//!
20//! ```no_run
21//! use pptx_rs::shape::{AutoShape, Shape};  // 引入 Shape trait 以使用 set_* 方法
22//! use pptx_rs::oxml::simpletypes::PresetGeometry;
23//! use pptx_rs::EmuExt;
24//! use pptx_rs::Inches;
25//!
26//! let mut s = AutoShape::new("MyRect", PresetGeometry::Rectangle);
27//! s.set_left(Inches(1.0).emu());
28//! s.set_top(Inches(1.0).emu());
29//! s.set_width(Inches(3.0).emu());
30//! s.set_height(Inches(2.0).emu());
31//! let _ = s.text_frame_mut();
32//! ```
33
34use crate::oxml::color::Color;
35use crate::oxml::shape::Sp as OxmlSp;
36use crate::oxml::simpletypes::PresetGeometry;
37use crate::oxml::sppr::{Fill, Geometry, ShapeProperties};
38use crate::oxml::txbody::TextBody;
39use crate::shape::base::Shape;
40use crate::units::Emu;
41
42/// 自选形状(带或不带文本都可以)。
43#[derive(Clone, Debug, Default)]
44pub struct AutoShape {
45    /// 内部 oxml 句柄(`pub(crate)` 供 `slide::ShapesMut` mutate)。
46    pub(crate) sp: OxmlSp,
47}
48
49impl AutoShape {
50    /// 构造一个指定几何形状(不设置位置/尺寸,由调用方决定)。
51    #[allow(clippy::field_reassign_with_default)]
52    pub fn new(name: impl Into<String>, geometry: PresetGeometry) -> Self {
53        let mut sp = OxmlSp::default();
54        sp.id = 0;
55        sp.name = name.into();
56        sp.properties.geometry = Some(Geometry::preset(geometry));
57        sp.text = TextBody::new();
58        AutoShape { sp }
59    }
60
61    /// 从 oxml [`OxmlSp`] 构造包装。
62    pub fn from_sp(sp: OxmlSp) -> Self {
63        AutoShape { sp }
64    }
65
66    /// 取出内部 oxml 引用。
67    pub fn sp(&self) -> &OxmlSp {
68        &self.sp
69    }
70    /// 取出内部 oxml 可变引用。
71    pub fn sp_mut(&mut self) -> &mut OxmlSp {
72        &mut self.sp
73    }
74
75    /// 文本体不可变引用。
76    pub fn text_frame(&self) -> &TextBody {
77        &self.sp.text
78    }
79    /// 文本体可变引用。
80    pub fn text_frame_mut(&mut self) -> &mut TextBody {
81        &mut self.sp.text
82    }
83
84    /// 形状属性不可变引用。
85    pub fn properties(&self) -> &ShapeProperties {
86        &self.sp.properties
87    }
88    /// 形状属性可变引用。
89    pub fn properties_mut(&mut self) -> &mut ShapeProperties {
90        &mut self.sp.properties
91    }
92
93    /// 设填充。
94    pub fn set_fill(&mut self, fill: Fill) {
95        self.sp.properties.fill = fill;
96    }
97
98    /// 便捷方法:设填充为指定 RGB 颜色。
99    ///
100    /// 对应 python-pptx 中
101    /// `shape.fill.solid(); shape.fill.fore_color.rgb = RGBColor(r, g, b)`。
102    pub fn set_fill_color(&mut self, c: impl Into<Color>) {
103        self.sp.properties.fill = Fill::Solid(c.into());
104    }
105
106    /// 便捷方法:设描边颜色。
107    pub fn set_stroke_color(&mut self, c: impl Into<Color>) {
108        let mut line = self.sp.properties.line.clone().unwrap_or_default();
109        line.color = c.into();
110        self.sp.properties.line = Some(line);
111    }
112
113    /// 便捷方法:设描边宽度(EMU)。通常 `Pt(1.0).emu()`。
114    pub fn set_stroke_width(&mut self, w: Emu) {
115        let mut line = self.sp.properties.line.clone().unwrap_or_default();
116        line.width = Some(w);
117        self.sp.properties.line = Some(line);
118    }
119
120    // --------------------- 形状效果 API(TODO-011 高阶) ---------------------
121
122    /// 设置外阴影(`<a:outerShdw>`)。覆盖既有外阴影,保留其他效果。
123    ///
124    /// 对标 python-pptx `shape.shadow.inherit = False` + `shape.shadow.outerShadow`。
125    ///
126    /// # 示例
127    ///
128    /// ```no_run
129    /// # use pptx_rs::{Presentation, ShadowEffect, RGBColor, Color, Pt};
130    /// # let mut p = Presentation::new().unwrap();
131    /// # let counter = p.id_counter();
132    /// # let s = p.slides_mut().add_slide(counter).unwrap();
133    /// # let mut sh = s.shapes_mut().add_textbox(Pt(10.0), Pt(10.0), Pt(100.0), Pt(20.0)).unwrap();
134    /// sh.set_outer_shadow(ShadowEffect {
135    ///     dir: 2_700_000,        // 向下(1/60000 度)
136    ///     dist: 38_100,          // 3pt(EMU)
137    ///     blur_rad: 38_100,
138    ///     color: Color::RGB(RGBColor(0x80, 0x80, 0x80)),
139    ///     rot_with_shape: None,
140    /// });
141    /// ```
142    pub fn set_outer_shadow(&mut self, shadow: crate::oxml::sppr::ShadowEffect) {
143        self.sp.properties.set_outer_shadow(shadow);
144    }
145
146    /// 设置内阴影(`<a:innerShdw>`)。
147    pub fn set_inner_shadow(&mut self, shadow: crate::oxml::sppr::ShadowEffect) {
148        self.sp.properties.set_inner_shadow(shadow);
149    }
150
151    /// 设置发光(`<a:glow>`)。
152    pub fn set_glow(&mut self, glow: crate::oxml::sppr::GlowEffect) {
153        self.sp.properties.set_glow(glow);
154    }
155
156    /// 设置柔化边缘(`<a:softEdge>`)。
157    pub fn set_soft_edge(&mut self, rad: i64) {
158        self.sp.properties.set_soft_edge(rad);
159    }
160
161    /// 设置反射(`<a:reflection>`)。
162    pub fn set_reflection(&mut self, reflection: crate::oxml::sppr::ReflectionEffect) {
163        self.sp.properties.set_reflection(reflection);
164    }
165
166    /// 清除所有效果(删除整个 `<a:effectLst>` 元素)。
167    pub fn clear_effects(&mut self) {
168        self.sp.properties.clear_effects();
169    }
170
171    // --------------------- 三维效果 API(TODO-050 高阶) ---------------------
172    //
173    // 对标 PowerPoint "形状效果 → 三维旋转 / 三维格式"。
174    // oxml 层 Scene3d/Sp3d 模型已在 sppr.rs 完成,这里仅暴露便捷方法。
175    // 角度参数统一使用"度"(用户直觉),内部转换为 OOXML 的 1/60000 度。
176
177    /// 设置三维旋转(`<a:rot lat="..." lon="..." rev="..."/>`)。
178    ///
179    /// 三个角度均以**度**为单位(用户直觉),内部转换为 1/60000 度(OOXML ST_Angle)。
180    /// 若 `scene3d` 不存在则自动创建默认 Scene3d(正交前视图 + 平衡光)。
181    ///
182    /// # 参数
183    /// - `lat_deg`:纬度(-90°~90°,正值抬头)
184    /// - `lon_deg`:经度(0°~360°,正值右转)
185    /// - `rev_deg`:滚转(沿视线轴的旋转,正值顺时针)
186    ///
187    /// # 示例
188    ///
189    /// ```no_run
190    /// # use pptx_rs::{Presentation, Pt};
191    /// # let mut p = Presentation::new().unwrap();
192    /// # let counter = p.id_counter();
193    /// # let s = p.slides_mut().add_slide(counter).unwrap();
194    /// # let mut sh = s.shapes_mut().add_textbox(Pt(10.0), Pt(10.0), Pt(100.0), Pt(20.0)).unwrap();
195    /// sh.set_3d_rotation(30.0, 45.0, 0.0); // 抬头 30° + 右转 45°
196    /// ```
197    pub fn set_3d_rotation(&mut self, lat_deg: f64, lon_deg: f64, rev_deg: f64) {
198        let scene = self
199            .sp
200            .properties
201            .scene3d
202            .get_or_insert_with(crate::oxml::sppr::Scene3d::default);
203        scene.camera.rotation = Some(crate::oxml::sppr::Rotation3d {
204            lat: (lat_deg * 60_000.0) as i32,
205            lon: (lon_deg * 60_000.0) as i32,
206            rev: (rev_deg * 60_000.0) as i32,
207        });
208    }
209
210    /// 设置三维拉伸高度与可选拉伸颜色(`<a:sp3d extrusionH="...">` + `<a:extrusionClr>`)。
211    ///
212    /// # 参数
213    /// - `height_emu`:拉伸高度(EMU,38100 = 3pt)
214    /// - `color`:拉伸颜色(`None` 表示不写出 `<a:extrusionClr>`)
215    pub fn set_3d_extrusion(&mut self, height_emu: i32, color: Option<crate::oxml::color::Color>) {
216        let sp3d = self
217            .sp
218            .properties
219            .sp3d
220            .get_or_insert_with(crate::oxml::sppr::Sp3d::default);
221        sp3d.extrusion_h = height_emu;
222        sp3d.extrusion_color = color;
223    }
224
225    /// 设置三维棱台(`<a:bevelT>` + `<a:bevelB>`)。
226    ///
227    /// # 参数
228    /// - `top_w` / `top_h`:顶部棱台的宽高(EMU,63500 = 5pt)
229    /// - `bottom_w` / `bottom_h`:底部棱台的宽高(EMU)
230    pub fn set_3d_bevel(&mut self, top_w: i32, top_h: i32, bottom_w: i32, bottom_h: i32) {
231        let sp3d = self
232            .sp
233            .properties
234            .sp3d
235            .get_or_insert_with(crate::oxml::sppr::Sp3d::default);
236        sp3d.bevel_top = Some(crate::oxml::sppr::Bevel { w: top_w, h: top_h });
237        sp3d.bevel_bottom = Some(crate::oxml::sppr::Bevel {
238            w: bottom_w,
239            h: bottom_h,
240        });
241    }
242
243    /// 设置三维材质预设(`<a:sp3d prstMaterial="...">`)。
244    pub fn set_3d_material(&mut self, material: crate::oxml::sppr::MaterialPreset) {
245        let sp3d = self
246            .sp
247            .properties
248            .sp3d
249            .get_or_insert_with(crate::oxml::sppr::Sp3d::default);
250        sp3d.prst_material = material;
251    }
252
253    /// 清除所有三维效果(删除 `scene3d` + `sp3d`)。
254    pub fn clear_3d(&mut self) {
255        self.sp.properties.scene3d = None;
256        self.sp.properties.sp3d = None;
257    }
258
259    /// 取三维场景引用(camera + lightRig)。`None` 表示未设置 3D 场景。
260    pub fn scene_3d(&self) -> Option<&crate::oxml::sppr::Scene3d> {
261        self.sp.properties.scene3d.as_ref()
262    }
263
264    /// 取三维场景可变引用(可直接操作 camera / lightRig)。
265    pub fn scene_3d_mut(&mut self) -> &mut Option<crate::oxml::sppr::Scene3d> {
266        &mut self.sp.properties.scene3d
267    }
268
269    /// 取形状三维属性引用(拉伸/棱台/材质)。
270    pub fn sp_3d(&self) -> Option<&crate::oxml::sppr::Sp3d> {
271        self.sp.properties.sp3d.as_ref()
272    }
273
274    /// 取形状三维属性可变引用。
275    pub fn sp_3d_mut(&mut self) -> &mut Option<crate::oxml::sppr::Sp3d> {
276        &mut self.sp.properties.sp3d
277    }
278
279    // --------------------- 形状锁定 API(TODO-027 高阶) ---------------------
280
281    /// 读取形状锁定(`<a:spLocks>`)。`None` 表示未设置。
282    pub fn locks(&self) -> Option<&crate::oxml::shape::ShapeLocks> {
283        self.sp.locks.as_ref()
284    }
285
286    /// 读取形状锁定的可变引用。若未设置,自动初始化为空 `ShapeLocks`(无任何锁定)。
287    pub fn locks_mut(&mut self) -> &mut crate::oxml::shape::ShapeLocks {
288        self.sp.locks.get_or_insert_with(Default::default)
289    }
290
291    /// 便捷锁定:禁止选中(`noSelect="1"`)。
292    pub fn lock_select(&mut self, locked: bool) {
293        self.locks_mut().no_select = locked;
294    }
295
296    /// 便捷锁定:禁止移动(`noMove="1"`)。
297    pub fn lock_move(&mut self, locked: bool) {
298        self.locks_mut().no_move = locked;
299    }
300
301    /// 便捷锁定:禁止缩放(`noResize="1"`)。
302    pub fn lock_resize(&mut self, locked: bool) {
303        self.locks_mut().no_resize = locked;
304    }
305
306    /// 便捷锁定:禁止旋转(`noRot="1"`)。
307    pub fn lock_rotate(&mut self, locked: bool) {
308        self.locks_mut().no_rot = locked;
309    }
310
311    /// 便捷锁定:禁止组合(`noGrp="1"`)。
312    pub fn lock_group(&mut self, locked: bool) {
313        self.locks_mut().no_grp = locked;
314    }
315
316    /// 清除所有锁定。
317    pub fn clear_locks(&mut self) {
318        self.sp.locks = None;
319    }
320
321    /// 统一锁定入口:按 [`crate::oxml::shape::LockType`] 设置指定锁定(TODO-027 高阶 API)。
322    ///
323    /// 对标 python-pptx 风格 `shape.set_lock(MSO_SHAPE_LOCK_TYPE.Select, True)`。
324    /// 比 `lock_select` / `lock_move` 等具名方法更通用,可覆盖所有 12 种锁定类型。
325    ///
326    /// # 参数
327    /// - `lock_type`:锁定类型枚举;
328    /// - `locked`:`true` 启用,`false` 解除。
329    ///
330    /// # 示例
331    ///
332    /// ```no_run
333    /// # use pptx_rs::{Presentation, Pt, LockType};
334    /// # let mut p = Presentation::new().unwrap();
335    /// # let counter = p.id_counter();
336    /// # let s = p.slides_mut().add_slide(counter).unwrap();
337    /// # let mut sh = s.shapes_mut().add_textbox(Pt(10.0), Pt(10.0), Pt(100.0), Pt(20.0)).unwrap();
338    /// sh.set_lock(LockType::Select, true);
339    /// sh.set_lock(LockType::ChangeAspect, true);
340    /// assert!(sh.locks().unwrap().get_lock(LockType::Select));
341    /// ```
342    pub fn set_lock(&mut self, lock_type: crate::oxml::shape::LockType, locked: bool) {
343        self.locks_mut().set_lock(lock_type, locked);
344    }
345
346    // --------------------- ShapeStyle API(TODO-006 高阶) ---------------------
347
348    /// 读取主题样式引用(`<p:style>`)。`None` 表示未设置。
349    pub fn style(&self) -> Option<&crate::oxml::shape::ShapeStyle> {
350        self.sp.style.as_ref()
351    }
352
353    /// 设置主题样式引用(`<p:style>`)。
354    ///
355    /// 对标 python-pptx `shape.style` 的底层元素引用方式。
356    ///
357    /// # 参数
358    /// - `style`:主题样式(line_ref / fill_ref / effect_ref / font_ref)
359    pub fn set_style(&mut self, style: crate::oxml::shape::ShapeStyle) {
360        self.sp.style = Some(style);
361    }
362
363    /// 清除主题样式引用。
364    pub fn clear_style(&mut self) {
365        self.sp.style = None;
366    }
367
368    /// 便捷方法:设文本(**单段单 Run**)。多段请用 `text_frame_mut().add_paragraph`。
369    pub fn set_text(&mut self, t: impl Into<String>) {
370        let s: String = t.into();
371        self.sp.text.set_text(&s);
372    }
373
374    /// 文本快捷访问:返回首段首 Run 的文本(多段时只反映首段)。
375    pub fn text(&self) -> String {
376        self.sp.text.text()
377    }
378
379    // --------------------- 调整手柄 API(TODO-038) ---------------------
380
381    /// 取调整值列表(不可变)。
382    ///
383    /// 对标 python-pptx `shape.adjustments`。
384    pub fn adjustments(&self) -> &[crate::oxml::sppr::AdjustmentValue] {
385        match &self.sp.properties.geometry {
386            Some(Geometry::Preset(_, adj)) => adj,
387            _ => &[],
388        }
389    }
390
391    /// 取调整值列表(可变)。
392    ///
393    /// 如果几何不是 `Preset` 变体,返回空 Vec 的可变引用(实际是临时变量)。
394    /// 调用方应确保几何是 `Preset` 类型后再调用。
395    pub fn adjustments_mut(&mut self) -> &mut Vec<crate::oxml::sppr::AdjustmentValue> {
396        // 确保 geometry 是 Preset 变体
397        if !matches!(self.sp.properties.geometry, Some(Geometry::Preset(..))) {
398            self.sp.properties.geometry = Some(Geometry::default());
399        }
400        match &mut self.sp.properties.geometry {
401            Some(Geometry::Preset(_, adj)) => adj,
402            _ => unreachable!(),
403        }
404    }
405
406    /// 设置指定索引的调整值(归一化 0.0-1.0)。
407    ///
408    /// 对标 python-pptx `shape.adjustments[0] = 0.15`。
409    ///
410    /// # 参数
411    /// - `idx`:调整值索引(0-based);
412    /// - `value`:归一化值(0.0-1.0)。
413    ///
414    /// # 行为
415    /// - 若 `idx` 超出现有列表长度,自动追加新条目;
416    /// - 新条目的 `name` 默认为 `"adj"` (首个)或 `"adjN"`(后续)。
417    pub fn set_adjustment(&mut self, idx: usize, value: f64) {
418        let adj = self.adjustments_mut();
419        while adj.len() <= idx {
420            let name = if adj.is_empty() {
421                "adj".to_string()
422            } else {
423                format!("adj{}", adj.len() + 1)
424            };
425            adj.push(crate::oxml::sppr::AdjustmentValue::from_normalized(
426                &name, 0.0,
427            ));
428        }
429        adj[idx].raw_value = (value * 100000.0).round() as i64;
430    }
431
432    /// 取指定索引的调整值归一化值(0.0-1.0)。
433    ///
434    /// 对标 python-pptx `shape.adjustments[0]`。
435    pub fn adjustment_value(&self, idx: usize) -> Option<f64> {
436        self.adjustments().get(idx).map(|a| a.effective_value())
437    }
438}
439
440impl Shape for AutoShape {
441    fn id(&self) -> u32 {
442        self.sp.id
443    }
444    fn set_id(&mut self, id: u32) {
445        self.sp.id = id;
446    }
447    fn name(&self) -> &str {
448        &self.sp.name
449    }
450    fn set_name(&mut self, name: String) {
451        self.sp.name = name;
452    }
453    fn shape_type(&self) -> &'static str {
454        "auto_shape"
455    }
456
457    fn left(&self) -> Emu {
458        self.sp.properties.xfrm.off_x.unwrap_or_default()
459    }
460    fn set_left(&mut self, emu: Emu) {
461        self.sp.properties.xfrm.off_x = Some(emu);
462    }
463    fn top(&self) -> Emu {
464        self.sp.properties.xfrm.off_y.unwrap_or_default()
465    }
466    fn set_top(&mut self, emu: Emu) {
467        self.sp.properties.xfrm.off_y = Some(emu);
468    }
469    fn width(&self) -> Emu {
470        self.sp.properties.xfrm.ext_cx.unwrap_or_default()
471    }
472    fn set_width(&mut self, emu: Emu) {
473        self.sp.properties.xfrm.ext_cx = Some(emu);
474    }
475    fn height(&self) -> Emu {
476        self.sp.properties.xfrm.ext_cy.unwrap_or_default()
477    }
478    fn set_height(&mut self, emu: Emu) {
479        self.sp.properties.xfrm.ext_cy = Some(emu);
480    }
481
482    /// 旋转角度(度数,正向顺时针)。
483    fn rotation(&self) -> f64 {
484        self.sp.properties.rot_deg.unwrap_or(0.0)
485    }
486    /// 设置旋转角度。OOXML 内部单位是"60000 分之一度",
487    /// 本方法在读写时自动转换。
488    fn set_rotation(&mut self, deg: f64) {
489        self.sp.properties.rot_deg = Some(deg);
490        // OOXML 中 rot 单位是 1/60000 度
491        let rot = (deg * 60_000.0) as i32;
492        self.sp.properties.xfrm.rot = Some(rot);
493    }
494}
495
496#[cfg(test)]
497mod tests {
498    use super::*;
499    use crate::oxml::writer::XmlWriter;
500
501    /// `set_adjustment` 正确设置首个调整值(TODO-038)。
502    #[test]
503    fn set_adjustment_first_value() {
504        let mut s = AutoShape::new("RoundRect", PresetGeometry::RoundRectangle);
505        s.set_adjustment(0, 0.25);
506        assert_eq!(s.adjustments().len(), 1);
507        assert!((s.adjustment_value(0).unwrap() - 0.25).abs() < 1e-6);
508    }
509
510    /// `set_adjustment` 自动追加多个调整值(TODO-038)。
511    #[test]
512    fn set_adjustment_multiple_values() {
513        let mut s = AutoShape::new("Shape", PresetGeometry::Rectangle);
514        s.set_adjustment(0, 0.5);
515        s.set_adjustment(1, 0.3);
516        assert_eq!(s.adjustments().len(), 2);
517        assert!((s.adjustment_value(0).unwrap() - 0.5).abs() < 1e-6);
518        assert!((s.adjustment_value(1).unwrap() - 0.3).abs() < 1e-6);
519    }
520
521    /// `adjustment_value` 对越界索引返回 `None`(TODO-038)。
522    #[test]
523    fn adjustment_value_out_of_bounds_returns_none() {
524        let s = AutoShape::new("Rect", PresetGeometry::Rectangle);
525        assert!(s.adjustment_value(0).is_none());
526    }
527
528    /// 调整值在序列化后正确写出 `<a:gd>` 元素(TODO-038)。
529    #[test]
530    fn adjustment_serializes_to_xml() {
531        let mut s = AutoShape::new("RoundRect", PresetGeometry::RoundRectangle);
532        s.set_adjustment(0, 0.16667);
533        let mut w = XmlWriter::new();
534        s.sp.properties.geometry.as_ref().unwrap().write_xml(&mut w);
535        let xml = &w.buf;
536        assert!(
537            xml.contains("<a:prstGeom prst=\"roundRect\">"),
538            "xml: {}",
539            xml
540        );
541        assert!(
542            xml.contains("<a:gd name=\"adj\" fmla=\"val 16667\"/>"),
543            "xml: {}",
544            xml
545        );
546    }
547}