use crate::oxml::shape::Sp as OxmlSp;
use crate::oxml::sppr::{CustomGeometry, GeomRect, Geometry, Path, PathSegment, ShapeProperties};
use crate::oxml::txbody::TextBody;
use crate::shape::autoshape::AutoShape;
use crate::shape::base::Shape;
use crate::units::Emu;
#[derive(Copy, Clone, Debug, Default)]
pub struct Point {
pub x: Emu,
pub y: Emu,
}
#[derive(Clone, Debug, Default)]
pub struct FreeformBuilder {
points: Vec<Point>,
auto_close: bool,
}
impl FreeformBuilder {
pub fn new() -> Self {
FreeformBuilder {
points: Vec::new(),
auto_close: false,
}
}
pub fn move_to(&mut self, x: Emu, y: Emu) -> &mut Self {
self.points.push(Point { x, y });
self
}
pub fn line_to(&mut self, x: Emu, y: Emu) -> &mut Self {
self.points.push(Point { x, y });
self
}
pub fn close(&mut self) -> &mut Self {
self.auto_close = true;
self
}
pub fn points(&self) -> &[Point] {
&self.points
}
#[allow(clippy::field_reassign_with_default)]
pub fn build(self, name: impl Into<String>) -> Freeform {
let mut sp = OxmlSp::default();
sp.id = 0;
sp.name = name.into();
sp.properties = ShapeProperties::default();
sp.text = TextBody::new();
let mut segments: Vec<PathSegment> = Vec::with_capacity(self.points.len() + 1);
for (i, p) in self.points.iter().enumerate() {
let x = p.x.value();
let y = p.y.value();
if i == 0 {
segments.push(PathSegment::MoveTo { x, y });
} else {
segments.push(PathSegment::LineTo { x, y });
}
}
if self.auto_close {
segments.push(PathSegment::Close);
}
let (path_w, path_h, min_x, min_y) = compute_bbox(&self.points);
let path = Path {
width: path_w,
height: path_h,
fill: None,
stroke: None,
segments,
};
let geom = CustomGeometry {
fill: None,
stroke: None,
rect: Some(GeomRect {
l: min_x.to_string(),
t: min_y.to_string(),
r: (min_x + path_w).to_string(),
b: (min_y + path_h).to_string(),
}),
path_list: vec![path],
};
sp.properties.geometry = Some(Geometry::Custom(geom));
sp.properties.xfrm.off_x = Some(Emu(min_x));
sp.properties.xfrm.off_y = Some(Emu(min_y));
sp.properties.xfrm.ext_cx = Some(Emu(path_w));
sp.properties.xfrm.ext_cy = Some(Emu(path_h));
Freeform {
shape: AutoShape::from_sp(sp),
}
}
}
fn compute_bbox(points: &[Point]) -> (i64, i64, i64, i64) {
if points.is_empty() {
return (0, 0, 0, 0);
}
let mut min_x = points[0].x.value();
let mut min_y = points[0].y.value();
let mut max_x = min_x;
let mut max_y = min_y;
for p in &points[1..] {
let x = p.x.value();
let y = p.y.value();
if x < min_x {
min_x = x;
}
if x > max_x {
max_x = x;
}
if y < min_y {
min_y = y;
}
if y > max_y {
max_y = y;
}
}
(max_x - min_x, max_y - min_y, min_x, min_y)
}
#[derive(Clone, Debug, Default)]
pub struct Freeform {
pub(crate) shape: AutoShape,
}
impl Freeform {
pub fn as_shape(&self) -> &AutoShape {
&self.shape
}
pub fn as_shape_mut(&mut self) -> &mut AutoShape {
&mut self.shape
}
}
impl Shape for Freeform {
fn id(&self) -> u32 {
self.shape.id()
}
fn set_id(&mut self, id: u32) {
self.shape.set_id(id);
}
fn name(&self) -> &str {
self.shape.name()
}
fn set_name(&mut self, name: String) {
self.shape.set_name(name);
}
fn shape_type(&self) -> &'static str {
"freeform"
}
fn left(&self) -> Emu {
self.shape.left()
}
fn set_left(&mut self, emu: Emu) {
self.shape.set_left(emu);
}
fn top(&self) -> Emu {
self.shape.top()
}
fn set_top(&mut self, emu: Emu) {
self.shape.set_top(emu);
}
fn width(&self) -> Emu {
self.shape.width()
}
fn set_width(&mut self, emu: Emu) {
self.shape.set_width(emu);
}
fn height(&self) -> Emu {
self.shape.height()
}
fn set_height(&mut self, emu: Emu) {
self.shape.set_height(emu);
}
fn rotation(&self) -> f64 {
self.shape.rotation()
}
fn set_rotation(&mut self, deg: f64) {
self.shape.set_rotation(deg);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::oxml::writer::XmlWriter;
use crate::EmuExt;
use crate::Inches;
#[test]
fn build_outputs_cust_geom() {
let mut b = FreeformBuilder::new();
b.move_to(Inches(1.0).emu(), Inches(1.0).emu())
.line_to(Inches(3.0).emu(), Inches(1.0).emu())
.line_to(Inches(2.0).emu(), Inches(2.0).emu())
.close();
let f = b.build("triangle");
let mut w = XmlWriter::new();
f.shape.sp().properties.write_xml(&mut w, "p:spPr");
let xml = &w.buf;
assert!(
xml.contains("<a:custGeom>"),
"must output custGeom, xml: {}",
xml
);
assert!(
!xml.contains("<a:prstGeom"),
"must not output prstGeom, xml: {}",
xml
);
assert!(xml.contains("<a:moveTo>"), "xml: {}", xml);
assert!(xml.contains("<a:lnTo>"), "xml: {}", xml);
assert!(xml.contains("<a:close/>"), "xml: {}", xml);
}
#[test]
fn build_with_empty_points() {
let b = FreeformBuilder::new();
let f = b.build("empty");
let mut w = XmlWriter::new();
f.shape.sp().properties.write_xml(&mut w, "p:spPr");
let xml = &w.buf;
assert!(xml.contains("<a:custGeom>"), "xml: {}", xml);
}
#[test]
fn bbox_two_points() {
let pts = [
Point {
x: Inches(1.0).emu(),
y: Inches(2.0).emu(),
},
Point {
x: Inches(4.0).emu(),
y: Inches(6.0).emu(),
},
];
let (w, h, min_x, min_y) = compute_bbox(&pts);
assert_eq!(w, Inches(3.0).emu().value());
assert_eq!(h, Inches(4.0).emu().value());
assert_eq!(min_x, Inches(1.0).emu().value());
assert_eq!(min_y, Inches(2.0).emu().value());
}
}