Skip to main content

pptx_rs/shape/
connector.rs

1//! `Connector`:连接器(直线/折线/曲线)。
2//!
3//! 连接器在 OOXML 中是一等公民——`p:cxnSp` 与 `p:sp` 平级,但用途上
4//! 专门表达"两点之间"的几何关系。PowerPoint 允许在连接器上"挂接"形状,
5//! 使其在源/目标形状移动时自动跟随(流程图常用)。
6//!
7//! # 与 python-pptx 的对应
8//!
9//! - `pptx.shapes.connector.Connector` ←→ [`Connector`];
10//! - `Slide.shapes.add_connector(connector_type, begin_x, begin_y, end_x, end_y)`
11//!   返回 [`Connector`](本库当前未实现 `add_connector` 高阶方法,
12//!   仅暴露 `Connector::new` 供高级用户手动构造)。
13//!
14//! # 几何
15//!
16//! 连接器几何通常是 `prstGeom=line` / `straightConnector1` 等;通过
17//! [`Connector::cxn_mut`] 可直接 mutate 内部 oxml 字段。
18//!
19//! # 限制
20//!
21//! - **未实现** 自动连接源/目标形状的"挂接点"语义(`stCxn` / `endCxn`);
22//! - 弯曲连接器(curvedConnector)序列化后 PowerPoint 端能识别但本库未专门测试。
23
24use crate::oxml::shape::Connector as OxmlCxn;
25use crate::oxml::simpletypes::MsoConnectorType;
26use crate::oxml::simpletypes::PresetGeometry;
27use crate::oxml::sppr::Geometry;
28use crate::shape::base::Shape;
29use crate::units::{Emu, EmuPoint};
30
31/// 连接器(直线 / 折线 / 曲线)。
32#[derive(Clone, Debug, Default)]
33pub struct Connector {
34    /// 内部 oxml 句柄。
35    pub(crate) cxn: OxmlCxn,
36}
37
38impl Connector {
39    /// 从 oxml [`OxmlCxn`] 构造包装。
40    pub fn from_cxn(c: OxmlCxn) -> Self {
41        Connector { cxn: c }
42    }
43
44    /// 新建一个连接器(默认名为 `name`,其余字段由调用方填充)。
45    pub fn new(name: impl Into<String>) -> Self {
46        Connector {
47            cxn: OxmlCxn {
48                name: name.into(),
49                ..Default::default()
50            },
51        }
52    }
53
54    /// 新建一个**指定类型**的连接器。
55    ///
56    /// 对应 python-pptx 中 `Connector(connector_type, begin_x, begin_y, end_x, end_y)`
57    /// 的"类型"部分。几何由 `connector_type` 决定。
58    ///
59    /// # 修订历史
60    /// 早期版本**不会**自动计算 xfrm(bounding box),导致 begin/end 在序列化时
61    /// 被丢弃 / 位置跑到 (0, 0)。现已**自动**根据 begin/end 计算 xfrm:
62    /// - off_x = min(begin_x, end_x)
63    /// - off_y = min(begin_y, end_y)
64    /// - ext_cx = |end_x - begin_x|
65    /// - ext_cy = |end_y - begin_y|
66    ///
67    /// # 注意
68    /// 若 begin/end 还未设置,xfrm 保持为 0/0/0/0;可在调用 [`Self::set_begin`]
69    /// 与 [`Self::set_end`] 之后用 [`Self::recompute_xfrm`] 重算。
70    pub fn new_with_type(name: impl Into<String>, connector_type: MsoConnectorType) -> Self {
71        let mut cxn = OxmlCxn {
72            name: name.into(),
73            ..Default::default()
74        };
75        cxn.connector_type = Some(connector_type);
76        // 把 connector_type 同步到 spPr.geometry(OOXML 中 xfrm 只放变换,几何在 spPr 直接子级)。
77        cxn.properties.geometry =
78            Some(Geometry::preset(connector_type_to_geometry(connector_type)));
79        Connector { cxn }
80    }
81
82    /// 重新计算 xfrm 边界盒(基于 begin/end)。
83    ///
84    /// 若 begin/end 都没设置,xfrm 会被重置为 0/0/0/0。
85    ///
86    /// # OOXML 语义
87    /// `p:spPr/a:xfrm` 表达"此连接器在 slide 上的占位矩形";`a:off` 是
88    /// 该矩形的左上角,`a:ext` 是宽高。**`p:cxnSp` 本身没有 begin/end 元素**,
89    /// 端点是用 `<a:xfrm>` 内嵌 `<a:off x=0 y=0/><a:ext cx=... cy=.../>` +
90    /// `prstGeom` 的 `prst="line"`(或 bent/curved)配合"反转坐标系"实现的。
91    ///
92    /// 简化策略:把 begin/end 转成 (offset, extent) 的 bounding box 写入 xfrm,
93    /// 由 PowerPoint 端按 line 几何反算端点。这样**位置信息不丢**。
94    pub fn recompute_xfrm(&mut self) {
95        match (self.cxn.begin, self.cxn.end) {
96            (Some((bx, by)), Some((ex, ey))) => {
97                let min_x = bx.value().min(ex.value());
98                let min_y = by.value().min(ey.value());
99                let max_x = bx.value().max(ex.value());
100                let max_y = by.value().max(ey.value());
101                self.cxn.properties.xfrm.off_x = Some(Emu(min_x));
102                self.cxn.properties.xfrm.off_y = Some(Emu(min_y));
103                self.cxn.properties.xfrm.ext_cx = Some(Emu(max_x - min_x));
104                self.cxn.properties.xfrm.ext_cy = Some(Emu(max_y - min_y));
105            }
106            _ => {
107                self.cxn.properties.xfrm.off_x = Some(Emu(0));
108                self.cxn.properties.xfrm.off_y = Some(Emu(0));
109                self.cxn.properties.xfrm.ext_cx = Some(Emu(0));
110                self.cxn.properties.xfrm.ext_cy = Some(Emu(0));
111            }
112        }
113    }
114
115    /// 取出 oxml 引用。
116    pub fn cxn(&self) -> &OxmlCxn {
117        &self.cxn
118    }
119    /// 取出 oxml 可变引用。
120    pub fn cxn_mut(&mut self) -> &mut OxmlCxn {
121        &mut self.cxn
122    }
123
124    /// 形状属性(spPr)不可变引用。
125    pub fn properties(&self) -> &crate::oxml::sppr::ShapeProperties {
126        &self.cxn.properties
127    }
128    /// 形状属性(spPr)可变引用(python-pptx 风格,方便 `LineFormat::from`)。
129    pub fn properties_mut(&mut self) -> &mut crate::oxml::sppr::ShapeProperties {
130        &mut self.cxn.properties
131    }
132
133    /// 起点坐标(EMU)。
134    pub fn begin(&self) -> Option<EmuPoint> {
135        self.cxn.begin.map(|(x, y)| EmuPoint(x.value(), y.value()))
136    }
137    /// 设置起点坐标。
138    ///
139    /// **副作用**:会**自动**重算 xfrm 边界盒(除非你显式调用了
140    /// `Self::set_no_xfrm_recompute` 关闭)。这是为了与 OOXML 序列化要求一致——
141    /// `p:cxnSp` 的端点位置在 xfrm 的 off/ext 中体现。
142    pub fn set_begin(&mut self, p: EmuPoint) {
143        self.cxn.begin = Some((Emu(p.0), Emu(p.1)));
144        self.recompute_xfrm();
145    }
146
147    /// 终点坐标(EMU)。
148    pub fn end(&self) -> Option<EmuPoint> {
149        self.cxn.end.map(|(x, y)| EmuPoint(x.value(), y.value()))
150    }
151    /// 设置终点坐标。**自动**重算 xfrm。参见 [`Self::set_begin`]。
152    pub fn set_end(&mut self, p: EmuPoint) {
153        self.cxn.end = Some((Emu(p.0), Emu(p.1)));
154        self.recompute_xfrm();
155    }
156
157    /// 连接器类型。
158    pub fn connector_type(&self) -> Option<MsoConnectorType> {
159        self.cxn.connector_type
160    }
161    /// 设置连接器类型。
162    pub fn set_connector_type(&mut self, t: MsoConnectorType) {
163        self.cxn.connector_type = Some(t);
164        self.cxn.properties.geometry = Some(Geometry::preset(connector_type_to_geometry(t)));
165    }
166
167    /// 起点挂接。
168    pub fn begin_connection(&self) -> Option<(u32, u32)> {
169        self.cxn.st_cxn
170    }
171    /// 设置起点挂接。
172    pub fn set_begin_connection(&mut self, shape_id: u32, idx: u32) {
173        self.cxn.st_cxn = Some((shape_id, idx));
174    }
175
176    /// 终点挂接。
177    pub fn end_connection(&self) -> Option<(u32, u32)> {
178        self.cxn.end_cxn
179    }
180    /// 设置终点挂接。
181    pub fn set_end_connection(&mut self, shape_id: u32, idx: u32) {
182        self.cxn.end_cxn = Some((shape_id, idx));
183    }
184}
185
186/// 把 `MsoConnectorType` 映射到对应的 `PresetGeometry` 几何。
187///
188/// 几何直接落在 `spPr/prstGeom` 上(OOXML 不允许 xfrm 内嵌 prstGeom)。
189fn connector_type_to_geometry(t: MsoConnectorType) -> PresetGeometry {
190    match t {
191        MsoConnectorType::Straight => PresetGeometry::Line,
192        MsoConnectorType::Elbow => PresetGeometry::BentConnector2,
193        MsoConnectorType::Curve => PresetGeometry::CurvedConnector2,
194        MsoConnectorType::BentConnector3 => PresetGeometry::BentConnector3,
195        MsoConnectorType::BentConnector4 => PresetGeometry::BentConnector4,
196        MsoConnectorType::BentConnector5 => PresetGeometry::BentConnector5,
197        MsoConnectorType::CurvedConnector3 => PresetGeometry::CurvedConnector3,
198        MsoConnectorType::CurvedConnector4 => PresetGeometry::CurvedConnector4,
199        MsoConnectorType::CurvedConnector5 => PresetGeometry::CurvedConnector5,
200    }
201}
202
203impl Shape for Connector {
204    fn id(&self) -> u32 {
205        self.cxn.id
206    }
207    fn set_id(&mut self, id: u32) {
208        self.cxn.id = id;
209    }
210    fn name(&self) -> &str {
211        &self.cxn.name
212    }
213    fn set_name(&mut self, name: String) {
214        self.cxn.name = name;
215    }
216    fn shape_type(&self) -> &'static str {
217        "connector"
218    }
219
220    fn left(&self) -> Emu {
221        self.cxn.properties.xfrm.off_x.unwrap_or_default()
222    }
223    fn set_left(&mut self, emu: Emu) {
224        self.cxn.properties.xfrm.off_x = Some(emu);
225    }
226    fn top(&self) -> Emu {
227        self.cxn.properties.xfrm.off_y.unwrap_or_default()
228    }
229    fn set_top(&mut self, emu: Emu) {
230        self.cxn.properties.xfrm.off_y = Some(emu);
231    }
232    fn width(&self) -> Emu {
233        self.cxn.properties.xfrm.ext_cx.unwrap_or_default()
234    }
235    fn set_width(&mut self, emu: Emu) {
236        self.cxn.properties.xfrm.ext_cx = Some(emu);
237    }
238    fn height(&self) -> Emu {
239        self.cxn.properties.xfrm.ext_cy.unwrap_or_default()
240    }
241    fn set_height(&mut self, emu: Emu) {
242        self.cxn.properties.xfrm.ext_cy = Some(emu);
243    }
244
245    fn rotation(&self) -> f64 {
246        self.cxn.properties.rot_deg.unwrap_or(0.0)
247    }
248    fn set_rotation(&mut self, deg: f64) {
249        self.cxn.properties.rot_deg = Some(deg);
250        let rot = (deg * 60_000.0) as i32;
251        self.cxn.properties.xfrm.rot = Some(rot);
252    }
253}