diff options
| author | Martin Haug <mhaug@live.de> | 2022-05-02 16:51:14 +0200 |
|---|---|---|
| committer | Martin Haug <mhaug@live.de> | 2022-05-02 17:20:47 +0200 |
| commit | f07395f9a47502c50f767f78a233d0e2a6e4445f (patch) | |
| tree | c54fe2afeaaf265780279c893c272f1abfd12393 /src/geom | |
| parent | 7b6f3a0ab9ae0dac19f62b62b9ecc96ea942a89e (diff) | |
Move rounding logic out of exporters
Diffstat (limited to 'src/geom')
| -rw-r--r-- | src/geom/mod.rs | 46 | ||||
| -rw-r--r-- | src/geom/rect.rs | 212 |
2 files changed, 258 insertions, 0 deletions
diff --git a/src/geom/mod.rs b/src/geom/mod.rs index a6f53c87..bdd08fe5 100644 --- a/src/geom/mod.rs +++ b/src/geom/mod.rs @@ -13,6 +13,7 @@ mod paint; mod path; mod point; mod ratio; +mod rect; mod relative; mod scalar; mod sides; @@ -30,6 +31,7 @@ pub use paint::*; pub use path::*; pub use point::*; pub use ratio::*; +pub use rect::*; pub use relative::*; pub use scalar::*; pub use sides::*; @@ -60,6 +62,50 @@ pub trait Get<Index> { } } +/// A geometric shape with optional fill and stroke. +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Shape { + /// The shape's geometry. + pub geometry: Geometry, + /// The shape's background fill. + pub fill: Option<Paint>, + /// The shape's border stroke. + pub stroke: Option<Stroke>, +} + +/// A shape's geometry. +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum Geometry { + /// A line to a point (relative to its position). + Line(Point), + /// A rectangle with its origin in the topleft corner. + Rect(Size), + /// A ellipse with its origin in the topleft corner. + Ellipse(Size), + /// A bezier path. + Path(Path), +} + +impl Geometry { + /// Fill the geometry without a stroke. + pub fn filled(self, fill: Paint) -> Shape { + Shape { + geometry: self, + fill: Some(fill), + stroke: None, + } + } + + /// Stroke the geometry without a fill. + pub fn stroked(self, stroke: Stroke) -> Shape { + Shape { + geometry: self, + fill: None, + stroke: Some(stroke), + } + } +} + /// A numeric type. pub trait Numeric: Sized diff --git a/src/geom/rect.rs b/src/geom/rect.rs new file mode 100644 index 00000000..839feda4 --- /dev/null +++ b/src/geom/rect.rs @@ -0,0 +1,212 @@ +use super::*; + +use std::mem; + +/// A rectangle with rounded corners. +#[derive(Debug, Clone)] +pub struct Rect { + size: Size, + radius: Sides<Length>, +} + +impl Rect { + /// Create a new rectangle. + pub fn new(size: Size, radius: Sides<Length>) -> Self { + Self { size, radius } + } + + /// Output all constituent shapes of the rectangle in order. The last one is + /// in the foreground. The function will output multiple items if the stroke + /// properties differ by side. + pub fn shapes( + &self, + fill: Option<Paint>, + stroke: Sides<Option<Stroke>>, + ) -> Vec<Shape> { + let mut res = vec![]; + if fill.is_some() || (stroke.iter().any(Option::is_some) && stroke.is_uniform()) { + res.push(Shape { + geometry: self.fill_geometry(), + fill, + stroke: stroke.left, + }); + } + + if !stroke.is_uniform() { + for (path, stroke) in self.stroke_segments(Some(stroke)) { + if !stroke.is_some() { + continue; + } + res.push(Shape { + geometry: Geometry::Path(path), + fill: None, + stroke, + }); + } + } + + res + } + + /// Output the minimum number of paths along the rectangles border. + fn stroke_segments( + &self, + strokes: Option<Sides<Option<Stroke>>>, + ) -> Vec<(Path, Option<Stroke>)> { + let strokes = strokes.unwrap_or_else(|| Sides::splat(None)); + let mut res = vec![]; + + let mut connection = Connection::None; + let mut path = Path::new(); + let mut always_continuous = true; + + for side in [Side::Top, Side::Right, Side::Bottom, Side::Left] { + let radius = [self.radius.get(side.next_ccw()), self.radius.get(side)]; + + let stroke_continuity = strokes.get(side) == strokes.get(side.next_cw()); + connection = connection.advance(stroke_continuity && side != Side::Left); + always_continuous &= stroke_continuity; + + draw_side(&mut path, side, self.size, radius[0], radius[1], connection); + + if !stroke_continuity { + res.push((mem::take(&mut path), strokes.get(side))); + } + } + + if always_continuous { + path.close_path(); + } + + if !path.0.is_empty() { + res.push((path, strokes.left)); + } + + res + } + + /// Output the shape of the rectangle as a path or primitive rectangle, + /// depending on whether it is rounded. + fn fill_geometry(&self) -> Geometry { + if self.radius.iter().copied().all(Length::is_zero) { + Geometry::Rect(self.size) + } else { + let mut paths = self.stroke_segments(None); + assert_eq!(paths.len(), 1); + + Geometry::Path(paths.pop().unwrap().0) + } + } +} + +/// Draws one side of the rounded rectangle. Will always draw the left arc. The +/// right arc will be drawn halfway iff there is no connection. +fn draw_side( + path: &mut Path, + side: Side, + size: Size, + radius_left: Length, + radius_right: Length, + connection: Connection, +) { + let reversed = |angle: Angle, radius, rotate, mirror_x, mirror_y| { + let [a, b, c, d] = angle.bezier_arc(radius, rotate, mirror_x, mirror_y); + [d, c, b, a] + }; + + let angle_left = Angle::deg(if connection.left() { 90.0 } else { 45.0 }); + let angle_right = Angle::deg(if connection.right() { 90.0 } else { 45.0 }); + + let (arc1, arc2) = match side { + Side::Top => { + let arc1 = reversed(angle_left, radius_left, true, true, false) + .map(|x| x + Point::with_x(radius_left)); + let arc2 = (-angle_right) + .bezier_arc(radius_right, true, true, false) + .map(|x| x + Point::with_x(size.x - radius_right)); + + (arc1, arc2) + } + Side::Right => { + let arc1 = reversed(-angle_left, radius_left, false, false, false) + .map(|x| x + Point::new(size.x, radius_left)); + + let arc2 = angle_right + .bezier_arc(radius_right, false, false, false) + .map(|x| x + Point::new(size.x, size.y - radius_right)); + + (arc1, arc2) + } + Side::Bottom => { + let arc1 = reversed(-angle_left, radius_left, true, false, false) + .map(|x| x + Point::new(size.x - radius_left, size.y)); + + let arc2 = angle_right + .bezier_arc(radius_right, true, false, false) + .map(|x| x + Point::new(radius_right, size.y)); + + (arc1, arc2) + } + Side::Left => { + let arc1 = reversed(angle_left, radius_left, false, false, true) + .map(|x| x + Point::with_y(size.y - radius_left)); + + let arc2 = (-angle_right) + .bezier_arc(radius_right, false, false, true) + .map(|x| x + Point::with_y(radius_right)); + + (arc1, arc2) + } + }; + + if !connection.left() { + path.move_to(if radius_left.is_zero() { arc1[3] } else { arc1[0] }); + } + + if !radius_left.is_zero() { + path.cubic_to(arc1[1], arc1[2], arc1[3]); + } + + path.line_to(arc2[0]); + + if !connection.right() && !radius_right.is_zero() { + path.cubic_to(arc2[1], arc2[2], arc2[3]); + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +enum Connection { + None, + Left, + Right, + Both, +} + +impl Connection { + pub fn advance(self, right: bool) -> Self { + match self { + Self::Right | Self::Both => { + if right { + Self::Both + } else { + Self::Left + } + } + Self::Left | Self::None => { + if right { + Self::Right + } else { + Self::None + } + } + } + } + + fn left(self) -> bool { + matches!(self, Self::Left | Self::Both) + } + + fn right(self) -> bool { + matches!(self, Self::Right | Self::Both) + } +} |
