summaryrefslogtreecommitdiff
path: root/src/geom
diff options
context:
space:
mode:
authorMartin Haug <mhaug@live.de>2022-05-02 16:51:14 +0200
committerMartin Haug <mhaug@live.de>2022-05-02 17:20:47 +0200
commitf07395f9a47502c50f767f78a233d0e2a6e4445f (patch)
treec54fe2afeaaf265780279c893c272f1abfd12393 /src/geom
parent7b6f3a0ab9ae0dac19f62b62b9ecc96ea942a89e (diff)
Move rounding logic out of exporters
Diffstat (limited to 'src/geom')
-rw-r--r--src/geom/mod.rs46
-rw-r--r--src/geom/rect.rs212
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)
+ }
+}