1use crate::oxml::shape::Sp as OxmlSp;
36use crate::oxml::sppr::{CustomGeometry, GeomRect, Geometry, Path, PathSegment, ShapeProperties};
37use crate::oxml::txbody::TextBody;
38use crate::shape::autoshape::AutoShape;
39use crate::shape::base::Shape;
40use crate::units::Emu;
41
42#[derive(Copy, Clone, Debug, Default)]
44pub struct Point {
45 pub x: Emu,
47 pub y: Emu,
49}
50
51#[derive(Clone, Debug, Default)]
53pub struct FreeformBuilder {
54 points: Vec<Point>,
55 auto_close: bool,
56}
57
58impl FreeformBuilder {
59 pub fn new() -> Self {
61 FreeformBuilder {
62 points: Vec::new(),
63 auto_close: false,
64 }
65 }
66
67 pub fn move_to(&mut self, x: Emu, y: Emu) -> &mut Self {
69 self.points.push(Point { x, y });
70 self
71 }
72
73 pub fn line_to(&mut self, x: Emu, y: Emu) -> &mut Self {
75 self.points.push(Point { x, y });
76 self
77 }
78
79 pub fn close(&mut self) -> &mut Self {
81 self.auto_close = true;
82 self
83 }
84
85 pub fn points(&self) -> &[Point] {
87 &self.points
88 }
89
90 #[allow(clippy::field_reassign_with_default)]
105 pub fn build(self, name: impl Into<String>) -> Freeform {
106 let mut sp = OxmlSp::default();
107 sp.id = 0;
108 sp.name = name.into();
109 sp.properties = ShapeProperties::default();
110 sp.text = TextBody::new();
111
112 let mut segments: Vec<PathSegment> = Vec::with_capacity(self.points.len() + 1);
114 for (i, p) in self.points.iter().enumerate() {
115 let x = p.x.value();
116 let y = p.y.value();
117 if i == 0 {
118 segments.push(PathSegment::MoveTo { x, y });
119 } else {
120 segments.push(PathSegment::LineTo { x, y });
121 }
122 }
123 if self.auto_close {
124 segments.push(PathSegment::Close);
125 }
126
127 let (path_w, path_h, min_x, min_y) = compute_bbox(&self.points);
130
131 let path = Path {
132 width: path_w,
133 height: path_h,
134 fill: None,
135 stroke: None,
136 segments,
137 };
138 let geom = CustomGeometry {
139 fill: None,
140 stroke: None,
141 rect: Some(GeomRect {
143 l: min_x.to_string(),
144 t: min_y.to_string(),
145 r: (min_x + path_w).to_string(),
146 b: (min_y + path_h).to_string(),
147 }),
148 path_list: vec![path],
149 };
150 sp.properties.geometry = Some(Geometry::Custom(geom));
151
152 sp.properties.xfrm.off_x = Some(Emu(min_x));
155 sp.properties.xfrm.off_y = Some(Emu(min_y));
156 sp.properties.xfrm.ext_cx = Some(Emu(path_w));
157 sp.properties.xfrm.ext_cy = Some(Emu(path_h));
158
159 Freeform {
160 shape: AutoShape::from_sp(sp),
161 }
162 }
163}
164
165fn compute_bbox(points: &[Point]) -> (i64, i64, i64, i64) {
169 if points.is_empty() {
170 return (0, 0, 0, 0);
171 }
172 let mut min_x = points[0].x.value();
173 let mut min_y = points[0].y.value();
174 let mut max_x = min_x;
175 let mut max_y = min_y;
176 for p in &points[1..] {
177 let x = p.x.value();
178 let y = p.y.value();
179 if x < min_x {
180 min_x = x;
181 }
182 if x > max_x {
183 max_x = x;
184 }
185 if y < min_y {
186 min_y = y;
187 }
188 if y > max_y {
189 max_y = y;
190 }
191 }
192 (max_x - min_x, max_y - min_y, min_x, min_y)
193}
194
195#[derive(Clone, Debug, Default)]
197pub struct Freeform {
198 pub(crate) shape: AutoShape,
200}
201
202impl Freeform {
203 pub fn as_shape(&self) -> &AutoShape {
205 &self.shape
206 }
207 pub fn as_shape_mut(&mut self) -> &mut AutoShape {
209 &mut self.shape
210 }
211}
212
213impl Shape for Freeform {
214 fn id(&self) -> u32 {
215 self.shape.id()
216 }
217 fn set_id(&mut self, id: u32) {
218 self.shape.set_id(id);
219 }
220 fn name(&self) -> &str {
221 self.shape.name()
222 }
223 fn set_name(&mut self, name: String) {
224 self.shape.set_name(name);
225 }
226 fn shape_type(&self) -> &'static str {
227 "freeform"
228 }
229 fn left(&self) -> Emu {
230 self.shape.left()
231 }
232 fn set_left(&mut self, emu: Emu) {
233 self.shape.set_left(emu);
234 }
235 fn top(&self) -> Emu {
236 self.shape.top()
237 }
238 fn set_top(&mut self, emu: Emu) {
239 self.shape.set_top(emu);
240 }
241 fn width(&self) -> Emu {
242 self.shape.width()
243 }
244 fn set_width(&mut self, emu: Emu) {
245 self.shape.set_width(emu);
246 }
247 fn height(&self) -> Emu {
248 self.shape.height()
249 }
250 fn set_height(&mut self, emu: Emu) {
251 self.shape.set_height(emu);
252 }
253 fn rotation(&self) -> f64 {
254 self.shape.rotation()
255 }
256 fn set_rotation(&mut self, deg: f64) {
257 self.shape.set_rotation(deg);
258 }
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264 use crate::oxml::writer::XmlWriter;
265 use crate::EmuExt;
266 use crate::Inches;
267
268 #[test]
270 fn build_outputs_cust_geom() {
271 let mut b = FreeformBuilder::new();
272 b.move_to(Inches(1.0).emu(), Inches(1.0).emu())
273 .line_to(Inches(3.0).emu(), Inches(1.0).emu())
274 .line_to(Inches(2.0).emu(), Inches(2.0).emu())
275 .close();
276 let f = b.build("triangle");
277 let mut w = XmlWriter::new();
278 f.shape.sp().properties.write_xml(&mut w, "p:spPr");
279 let xml = &w.buf;
280 assert!(
281 xml.contains("<a:custGeom>"),
282 "must output custGeom, xml: {}",
283 xml
284 );
285 assert!(
286 !xml.contains("<a:prstGeom"),
287 "must not output prstGeom, xml: {}",
288 xml
289 );
290 assert!(xml.contains("<a:moveTo>"), "xml: {}", xml);
291 assert!(xml.contains("<a:lnTo>"), "xml: {}", xml);
292 assert!(xml.contains("<a:close/>"), "xml: {}", xml);
293 }
294
295 #[test]
297 fn build_with_empty_points() {
298 let b = FreeformBuilder::new();
299 let f = b.build("empty");
300 let mut w = XmlWriter::new();
301 f.shape.sp().properties.write_xml(&mut w, "p:spPr");
302 let xml = &w.buf;
303 assert!(xml.contains("<a:custGeom>"), "xml: {}", xml);
304 }
305
306 #[test]
308 fn bbox_two_points() {
309 let pts = [
310 Point {
311 x: Inches(1.0).emu(),
312 y: Inches(2.0).emu(),
313 },
314 Point {
315 x: Inches(4.0).emu(),
316 y: Inches(6.0).emu(),
317 },
318 ];
319 let (w, h, min_x, min_y) = compute_bbox(&pts);
320 assert_eq!(w, Inches(3.0).emu().value());
321 assert_eq!(h, Inches(4.0).emu().value());
322 assert_eq!(min_x, Inches(1.0).emu().value());
323 assert_eq!(min_y, Inches(2.0).emu().value());
324 }
325}