summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnton Wetzel <59712243+antonWetzel@users.noreply.github.com>2023-09-25 13:54:58 +0200
committerGitHub <noreply@github.com>2023-09-25 13:54:58 +0200
commit98e5d9750923fa05ff2ab39a55e3b48ddd5b9ed8 (patch)
tree90c85096fa314b5739dffded8d55bf1a38664863
parent44bbfded5fb270a0b6c38b1d4dc8923dc44d7e7e (diff)
Better rect edges (#1956)
-rw-r--r--crates/typst/src/doc.rs5
-rw-r--r--crates/typst/src/geom/corners.rs42
-rw-r--r--crates/typst/src/geom/mod.rs4
-rw-r--r--crates/typst/src/geom/rect.rs541
-rw-r--r--crates/typst/src/geom/rounded.rs182
-rw-r--r--tests/ref/compiler/show-selector.pngbin20394 -> 17066 bytes
-rw-r--r--tests/ref/meta/figure.pngbin66110 -> 54197 bytes
-rw-r--r--tests/ref/visualize/shape-fill-stroke.pngbin5821 -> 10272 bytes
-rw-r--r--tests/ref/visualize/shape-rect.pngbin247247 -> 247599 bytes
-rw-r--r--tests/ref/visualize/shape-rounded.pngbin876 -> 1975 bytes
-rw-r--r--tests/ref/visualize/stroke.pngbin4254 -> 4465 bytes
-rw-r--r--tests/typ/visualize/shape-fill-stroke.typ51
-rw-r--r--tests/typ/visualize/shape-rect.typ2
-rw-r--r--tests/typ/visualize/shape-rounded.typ53
14 files changed, 685 insertions, 195 deletions
diff --git a/crates/typst/src/doc.rs b/crates/typst/src/doc.rs
index 16f8eb03..e3913c1c 100644
--- a/crates/typst/src/doc.rs
+++ b/crates/typst/src/doc.rs
@@ -12,7 +12,7 @@ use crate::eval::{cast, dict, ty, Dict, Value};
use crate::export::PdfPageLabel;
use crate::font::Font;
use crate::geom::{
- self, rounded_rect, Abs, Axes, Color, Corners, Dir, Em, FixedAlign, FixedStroke,
+ self, styled_rect, Abs, Axes, Color, Corners, Dir, Em, FixedAlign, FixedStroke,
Geometry, Length, Numeric, Paint, Point, Rel, Shape, Sides, Size, Transform,
};
use crate::image::Image;
@@ -301,9 +301,8 @@ impl Frame {
let outset = outset.relative_to(self.size());
let size = self.size() + outset.sum_by_axis();
let pos = Point::new(-outset.left, -outset.top);
- let radius = radius.map(|side| side.relative_to(size.x.min(size.y) / 2.0));
self.prepend_multiple(
- rounded_rect(size, radius, fill, stroke)
+ styled_rect(size, radius, fill, stroke)
.into_iter()
.map(|x| (pos, FrameItem::Shape(x, span))),
)
diff --git a/crates/typst/src/geom/corners.rs b/crates/typst/src/geom/corners.rs
index d21dc22e..a7cd0eed 100644
--- a/crates/typst/src/geom/corners.rs
+++ b/crates/typst/src/geom/corners.rs
@@ -110,6 +110,48 @@ pub enum Corner {
BottomLeft,
}
+impl Corner {
+ /// The next corner, clockwise.
+ pub fn next_cw(self) -> Self {
+ match self {
+ Self::TopLeft => Self::TopRight,
+ Self::TopRight => Self::BottomRight,
+ Self::BottomRight => Self::BottomLeft,
+ Self::BottomLeft => Self::TopLeft,
+ }
+ }
+
+ /// The next corner, counter-clockwise.
+ pub fn next_ccw(self) -> Self {
+ match self {
+ Self::TopLeft => Self::BottomLeft,
+ Self::TopRight => Self::TopLeft,
+ Self::BottomRight => Self::TopRight,
+ Self::BottomLeft => Self::BottomRight,
+ }
+ }
+
+ /// The next side, clockwise.
+ pub fn side_cw(self) -> Side {
+ match self {
+ Self::TopLeft => Side::Top,
+ Self::TopRight => Side::Right,
+ Self::BottomRight => Side::Bottom,
+ Self::BottomLeft => Side::Left,
+ }
+ }
+
+ /// The next side, counter-clockwise.
+ pub fn side_ccw(self) -> Side {
+ match self {
+ Self::TopLeft => Side::Left,
+ Self::TopRight => Side::Top,
+ Self::BottomRight => Side::Right,
+ Self::BottomLeft => Side::Bottom,
+ }
+ }
+}
+
impl<T: Reflect> Reflect for Corners<Option<T>> {
fn input() -> CastInfo {
T::input() + Dict::input()
diff --git a/crates/typst/src/geom/mod.rs b/crates/typst/src/geom/mod.rs
index c5bcf84e..0d685607 100644
--- a/crates/typst/src/geom/mod.rs
+++ b/crates/typst/src/geom/mod.rs
@@ -17,8 +17,8 @@ mod paint;
mod path;
mod point;
mod ratio;
+mod rect;
mod rel;
-mod rounded;
mod scalar;
mod shape;
mod sides;
@@ -42,8 +42,8 @@ pub use self::paint::Paint;
pub use self::path::{Path, PathItem};
pub use self::point::Point;
pub use self::ratio::Ratio;
+pub use self::rect::styled_rect;
pub use self::rel::Rel;
-pub use self::rounded::rounded_rect;
pub use self::scalar::Scalar;
pub use self::shape::{Geometry, Shape};
pub use self::sides::{Side, Sides};
diff --git a/crates/typst/src/geom/rect.rs b/crates/typst/src/geom/rect.rs
new file mode 100644
index 00000000..108f5add
--- /dev/null
+++ b/crates/typst/src/geom/rect.rs
@@ -0,0 +1,541 @@
+use super::*;
+
+/// Helper to draw arcs with bezier curves.
+trait PathExtension {
+ fn arc(&mut self, start: Point, center: Point, end: Point);
+ fn arc_move(&mut self, start: Point, center: Point, end: Point);
+ fn arc_line(&mut self, start: Point, center: Point, end: Point);
+}
+
+/// Get the control points for a bezier curve that approximates a circular arc for
+/// a start point, an end point and a center of the circle whose arc connects
+/// the two.
+fn bezier_arc_control(start: Point, center: Point, end: Point) -> [Point; 2] {
+ // https://stackoverflow.com/a/44829356/1567835
+ let a = start - center;
+ let b = end - center;
+
+ let q1 = a.x.to_raw() * a.x.to_raw() + a.y.to_raw() * a.y.to_raw();
+ let q2 = q1 + a.x.to_raw() * b.x.to_raw() + a.y.to_raw() * b.y.to_raw();
+ let k2 = (4.0 / 3.0) * ((2.0 * q1 * q2).sqrt() - q2)
+ / (a.x.to_raw() * b.y.to_raw() - a.y.to_raw() * b.x.to_raw());
+
+ let control_1 = Point::new(center.x + a.x - k2 * a.y, center.y + a.y + k2 * a.x);
+ let control_2 = Point::new(center.x + b.x + k2 * b.y, center.y + b.y - k2 * b.x);
+
+ [control_1, control_2]
+}
+
+impl PathExtension for Path {
+ fn arc(&mut self, start: Point, center: Point, end: Point) {
+ let arc = bezier_arc_control(start, center, end);
+ self.cubic_to(arc[0], arc[1], end);
+ }
+
+ fn arc_move(&mut self, start: Point, center: Point, end: Point) {
+ self.move_to(start);
+ self.arc(start, center, end);
+ }
+ fn arc_line(&mut self, start: Point, center: Point, end: Point) {
+ self.line_to(start);
+ self.arc(start, center, end);
+ }
+}
+
+/// Create a styled rectangle with shapes.
+/// - use rect primitive for simple rectangles
+/// - stroke sides if possible
+/// - use fill for sides for best looks
+pub fn styled_rect(
+ size: Size,
+ radius: Corners<Rel<Abs>>,
+ fill: Option<Paint>,
+ stroke: Sides<Option<FixedStroke>>,
+) -> Vec<Shape> {
+ if stroke.is_uniform() && radius.iter().cloned().all(Rel::is_zero) {
+ simple_rect(size, fill, stroke.top)
+ } else {
+ segmented_rect(size, radius, fill, stroke)
+ }
+}
+
+/// Use rect primitive for the rectangle
+fn simple_rect(
+ size: Size,
+ fill: Option<Paint>,
+ stroke: Option<FixedStroke>,
+) -> Vec<Shape> {
+ vec![Shape { geometry: Geometry::Rect(size), fill, stroke }]
+}
+
+/// Use stroke and fill for the rectangle
+fn segmented_rect(
+ size: Size,
+ radius: Corners<Rel<Abs>>,
+ fill: Option<Paint>,
+ strokes: Sides<Option<FixedStroke>>,
+) -> Vec<Shape> {
+ let mut res = vec![];
+ let stroke_widths = strokes
+ .clone()
+ .map(|s| s.map(|s| s.thickness / 2.0).unwrap_or(Abs::zero()));
+
+ let max_radius = (size.x.min(size.y)) / 2.0
+ + stroke_widths.iter().cloned().min().unwrap_or(Abs::zero());
+
+ let radius = radius.map(|side| side.relative_to(max_radius * 2.0).min(max_radius));
+
+ let corners = Corners {
+ top_left: Corner::TopLeft,
+ top_right: Corner::TopRight,
+ bottom_right: Corner::BottomRight,
+ bottom_left: Corner::BottomLeft,
+ }
+ .map(|corner| ControlPoints {
+ radius: radius.get(corner),
+ stroke_before: stroke_widths.get(corner.side_ccw()),
+ stroke_after: stroke_widths.get(corner.side_cw()),
+ corner,
+ size,
+ same: match (
+ strokes.get_ref(corner.side_ccw()),
+ strokes.get_ref(corner.side_cw()),
+ ) {
+ (Some(a), Some(b)) => a.paint == b.paint && a.dash_pattern == b.dash_pattern,
+ (None, None) => true,
+ _ => false,
+ },
+ });
+
+ // insert stroked sides below filled sides
+ let mut stroke_insert = 0;
+
+ // fill shape with inner curve
+ if let Some(fill) = fill {
+ let mut path = Path::new();
+ let c = corners.get_ref(Corner::TopLeft);
+ if c.arc() {
+ path.arc_move(c.start(), c.center(), c.end());
+ } else {
+ path.move_to(c.center());
+ };
+
+ for corner in [Corner::TopRight, Corner::BottomRight, Corner::BottomLeft] {
+ let c = corners.get_ref(corner);
+ if c.arc() {
+ path.arc_line(c.start(), c.center(), c.end());
+ } else {
+ path.line_to(c.center());
+ }
+ }
+ path.close_path();
+ res.push(Shape {
+ geometry: Geometry::Path(path),
+ fill: Some(fill),
+ stroke: None,
+ });
+ stroke_insert += 1;
+ }
+
+ let current = corners.iter().find(|c| !c.same).map(|c| c.corner);
+ if let Some(mut current) = current {
+ // multiple segments
+ // start at a corner with a change between sides and iterate clockwise all other corners
+ let mut last = current;
+ for _ in 0..4 {
+ current = current.next_cw();
+ if corners.get_ref(current).same {
+ continue;
+ }
+ // create segment
+ let start = last;
+ let end = current;
+ last = current;
+ let stroke = match strokes.get_ref(start.side_cw()) {
+ None => continue,
+ Some(stroke) => stroke.clone(),
+ };
+ let (shape, ontop) = segment(start, end, &corners, stroke);
+ if ontop {
+ res.push(shape);
+ } else {
+ res.insert(stroke_insert, shape);
+ stroke_insert += 1;
+ }
+ }
+ } else if let Some(stroke) = strokes.top {
+ // single segment
+ let (shape, _) = segment(Corner::TopLeft, Corner::TopLeft, &corners, stroke);
+ res.push(shape);
+ }
+ res
+}
+
+/// Returns the shape for the segment and whether the shape should be drawn on top.
+fn segment(
+ start: Corner,
+ end: Corner,
+ corners: &Corners<ControlPoints>,
+ stroke: FixedStroke,
+) -> (Shape, bool) {
+ fn fill_corner(corner: &ControlPoints) -> bool {
+ corner.stroke_before != corner.stroke_after
+ || corner.radius() < corner.stroke_before
+ }
+
+ fn fill_corners(
+ start: Corner,
+ end: Corner,
+ corners: &Corners<ControlPoints>,
+ ) -> bool {
+ if fill_corner(corners.get_ref(start)) {
+ return true;
+ }
+ if fill_corner(corners.get_ref(end)) {
+ return true;
+ }
+ let mut current = start.next_cw();
+ while current != end {
+ if fill_corner(corners.get_ref(current)) {
+ return true;
+ }
+ current = current.next_cw();
+ }
+ false
+ }
+
+ let solid = stroke
+ .dash_pattern
+ .as_ref()
+ .map(|pattern| pattern.array.is_empty())
+ .unwrap_or(true);
+
+ let use_fill = solid && fill_corners(start, end, corners);
+
+ let shape = if use_fill {
+ fill_segment(start, end, corners, stroke)
+ } else {
+ stroke_segment(start, end, corners, stroke)
+ };
+ (shape, use_fill)
+}
+
+/// Stroke the sides from `start` to `end` clockwise.
+fn stroke_segment(
+ start: Corner,
+ end: Corner,
+ corners: &Corners<ControlPoints>,
+ stroke: FixedStroke,
+) -> Shape {
+ // create start corner
+ let c = corners.get_ref(start);
+ let mut path = Path::new();
+ if start == end || !c.arc() {
+ path.move_to(c.end());
+ } else {
+ path.arc_move(c.mid(), c.center(), c.end());
+ }
+
+ // create corners between start and end
+ let mut current = start.next_cw();
+ while current != end {
+ let c = corners.get_ref(current);
+ if c.arc() {
+ path.arc_line(c.start(), c.center(), c.end());
+ } else {
+ path.line_to(c.end());
+ }
+ current = current.next_cw();
+ }
+
+ // create end corner
+ let c = corners.get_ref(end);
+ if !c.arc() {
+ path.line_to(c.start());
+ } else if start == end {
+ path.arc_line(c.start(), c.center(), c.end());
+ } else {
+ path.arc_line(c.start(), c.center(), c.mid());
+ }
+
+ Shape {
+ geometry: Geometry::Path(path),
+ stroke: Some(stroke),
+ fill: None,
+ }
+}
+
+/// Fill the sides from `start` to `end` clockwise.
+fn fill_segment(
+ start: Corner,
+ end: Corner,
+ corners: &Corners<ControlPoints>,
+ stroke: FixedStroke,
+) -> Shape {
+ let mut path = Path::new();
+
+ // create the start corner
+ // begin on the inside and finish on the outside
+ // no corner if start and end are equal
+ // half corner if different
+ if start == end {
+ let c = corners.get_ref(start);
+ path.move_to(c.end_inner());
+ path.line_to(c.end_outer());
+ } else {
+ let c = corners.get_ref(start);
+
+ if c.arc_inner() {
+ path.arc_move(c.end_inner(), c.center_inner(), c.mid_inner());
+ } else {
+ path.move_to(c.end_inner());
+ }
+
+ if c.arc_outer() {
+ path.arc_line(c.mid_outer(), c.center_outer(), c.end_outer());
+ } else {
+ path.line_to(c.outer());
+ path.line_to(c.end_outer());
+ }
+ }
+
+ // create the clockwise outside path for the corners between start and end
+ let mut current = start.next_cw();
+ while current != end {
+ let c = corners.get_ref(current);
+ if c.arc_outer() {
+ path.arc_line(c.start_outer(), c.center_outer(), c.end_outer());
+ } else {
+ path.line_to(c.outer());
+ }
+ current = current.next_cw();
+ }
+
+ // create the end corner
+ // begin on the outside and finish on the inside
+ // full corner if start and end are equal
+ // half corner if different
+ if start == end {
+ let c = corners.get_ref(end);
+ if c.arc_outer() {
+ path.arc_line(c.start_outer(), c.center_outer(), c.end_outer());
+ } else {
+ path.line_to(c.outer());
+ path.line_to(c.end_outer());
+ }
+ if c.arc_inner() {
+ path.arc_line(c.end_inner(), c.center_inner(), c.start_inner());
+ } else {
+ path.line_to(c.center_inner());
+ }
+ } else {
+ let c = corners.get_ref(end);
+ if c.arc_outer() {
+ path.arc_line(c.start_outer(), c.center_outer(), c.mid_outer());
+ } else {
+ path.line_to(c.outer());
+ }
+ if c.arc_inner() {
+ path.arc_line(c.mid_inner(), c.center_inner(), c.start_inner());
+ } else {
+ path.line_to(c.center_inner());
+ }
+ }
+
+ // create the counterclockwise inside path for the corners between start and end
+ let mut current = end.next_ccw();
+ while current != start {
+ let c = corners.get_ref(current);
+ if c.arc_inner() {
+ path.arc_line(c.end_inner(), c.center_inner(), c.start_inner());
+ } else {
+ path.line_to(c.center_inner());
+ }
+ current = current.next_ccw();
+ }
+
+ path.close_path();
+
+ Shape {
+ geometry: Geometry::Path(path),
+ stroke: None,
+ fill: Some(stroke.paint),
+ }
+}
+
+/// Helper to calculate different control points for the corners.
+/// Clockwise orientation from start to end.
+/// ```text
+/// O-------------------EO --- - Z: Zero/Origin ({x: 0, y: 0} for top left corner)
+/// |\ ___----''' | | - O: Outer: intersection between the straight outer lines
+/// | \ / | | - S_: start
+/// | MO | | - M_: midpoint
+/// | /Z\ __-----------E | - E_: end
+/// |/ \M | ro - r_: radius
+/// | /\ | | - middle of the stroke
+/// | / \ | | - arc from S through M to E with center C and radius r
+/// | | MI--EI------- | - outer curve
+/// | | / \ | - arc from SO through MO to EO with center CO and radius ro
+/// SO | | \ CO --- - inner curve
+/// | | | \ - arc from SI through MI to EI with center CI and radius ri
+/// |--S-SI-----CI C
+/// |--ri--|
+/// |-------r--------|
+/// ```
+struct ControlPoints {
+ radius: Abs,
+ stroke_after: Abs,
+ stroke_before: Abs,
+ corner: Corner,
+ size: Size,
+ same: bool,
+}
+
+impl ControlPoints {
+ /// Move and rotate the point from top-left to the required corner.
+ fn rotate(&self, point: Point) -> Point {
+ match self.corner {
+ Corner::TopLeft => point,
+ Corner::TopRight => Point { x: self.size.x - point.y, y: point.x },
+ Corner::BottomRight => {
+ Point { x: self.size.x - point.x, y: self.size.y - point.y }
+ }
+ Corner::BottomLeft => Point { x: point.y, y: self.size.y - point.x },
+ }
+ }
+
+ /// Outside intersection of the sides.
+ pub fn outer(&self) -> Point {
+ self.rotate(Point { x: -self.stroke_before, y: -self.stroke_after })
+ }
+
+ /// Center for the outer arc.
+ pub fn center_outer(&self) -> Point {
+ let r = self.radius_outer();
+ self.rotate(Point {
+ x: r - self.stroke_before,
+ y: r - self.stroke_after,
+ })
+ }
+
+ /// Center for the middle arc.
+ pub fn center(&self) -> Point {
+ let r = self.radius();
+ self.rotate(Point { x: r, y: r })
+ }
+
+ /// Center for the inner arc.
+ pub fn center_inner(&self) -> Point {
+ let r = self.radius_inner();
+
+ self.rotate(Point {
+ x: self.stroke_before + r,
+ y: self.stroke_after + r,
+ })
+ }
+
+ /// Radius of the outer arc.
+ pub fn radius_outer(&self) -> Abs {
+ self.radius
+ }
+
+ /// Radius of the middle arc.
+ pub fn radius(&self) -> Abs {
+ (self.radius - self.stroke_before.min(self.stroke_after)).max(Abs::zero())
+ }
+
+ /// Radius of the inner arc.
+ pub fn radius_inner(&self) -> Abs {
+ (self.radius - 2.0 * self.stroke_before.max(self.stroke_after)).max(Abs::zero())
+ }
+
+ /// Middle of the corner on the outside of the stroke.
+ pub fn mid_outer(&self) -> Point {
+ let c_i = self.center_inner();
+ let c_o = self.center_outer();
+ let o = self.outer();
+ let r = self.radius_outer();
+
+ // https://math.stackexchange.com/a/311956
+ // intersection between the line from inner center to outside and the outer arc
+ let a = (o.x - c_i.x).to_raw().powi(2) + (o.y - c_i.y).to_raw().powi(2);
+ let b = 2.0 * (o.x - c_i.x).to_raw() * (c_i.x - c_o.x).to_raw()
+ + 2.0 * (o.y - c_i.y).to_raw() * (c_i.y - c_o.y).to_raw();
+ let c = (c_i.x - c_o.x).to_raw().powi(2) + (c_i.y - c_o.y).to_raw().powi(2)
+ - r.to_raw().powi(2);
+ let t = (-b + (b * b - 4.0 * a * c).sqrt()) / (2.0 * a);
+ c_i + t * (o - c_i)
+ }
+
+ /// Middle of the corner in the middle of the stroke.
+ pub fn mid(&self) -> Point {
+ let center = self.center_outer();
+ let outer = self.outer();
+ let diff = outer - center;
+ center + diff / diff.hypot().to_raw() * self.radius().to_raw()
+ }
+
+ /// Middle of the corner on the inside of the stroke.
+ pub fn mid_inner(&self) -> Point {
+ let center = self.center_inner();
+ let outer = self.outer();
+ let diff = outer - center;
+ center + diff / diff.hypot().to_raw() * self.radius_inner().to_raw()
+ }
+
+ /// If an outer arc is required.
+ pub fn arc_outer(&self) -> bool {
+ self.radius_outer() > Abs::zero()
+ }
+
+ pub fn arc(&self) -> bool {
+ self.radius() > Abs::zero()
+ }
+
+ /// If an inner arc is required.
+ pub fn arc_inner(&self) -> bool {
+ self.radius_inner() > Abs::zero()
+ }
+
+ /// Start of the corner on the outside of the stroke.
+ pub fn start_outer(&self) -> Point {
+ self.rotate(Point {
+ x: -self.stroke_before,
+ y: self.radius_outer() - self.stroke_after,
+ })
+ }
+
+ /// Start of the corner in the center of the stroke.
+ pub fn start(&self) -> Point {
+ self.rotate(Point::with_y(self.radius()))
+ }
+
+ /// Start of the corner on the inside of the stroke.
+ pub fn start_inner(&self) -> Point {
+ self.rotate(Point {
+ x: self.stroke_before,
+ y: self.stroke_after + self.radius_inner(),
+ })
+ }
+
+ /// End of the corner on the outside of the stroke.
+ pub fn end_outer(&self) -> Point {
+ self.rotate(Point {
+ x: self.radius_outer() - self.stroke_before,
+ y: -self.stroke_after,
+ })
+ }
+
+ /// End of the corner in the center of the stroke.
+ pub fn end(&self) -> Point {
+ self.rotate(Point::with_x(self.radius()))
+ }
+
+ /// End of the corner on the inside of the stroke.
+ pub fn end_inner(&self) -> Point {
+ self.rotate(Point {
+ x: self.stroke_before + self.radius_inner(),
+ y: self.stroke_after,
+ })
+ }
+}
diff --git a/crates/typst/src/geom/rounded.rs b/crates/typst/src/geom/rounded.rs
deleted file mode 100644
index abaf46de..00000000
--- a/crates/typst/src/geom/rounded.rs
+++ /dev/null
@@ -1,182 +0,0 @@
-use super::*;
-
-/// Produce shapes that together make up a rounded rectangle.
-pub fn rounded_rect(
- size: Size,
- radius: Corners<Abs>,
- fill: Option<Paint>,
- stroke: Sides<Option<FixedStroke>>,
-) -> Vec<Shape> {
- let mut res = vec![];
- if fill.is_some() || (stroke.iter().any(Option::is_some) && stroke.is_uniform()) {
- res.push(Shape {
- geometry: fill_geometry(size, radius),
- fill,
- stroke: if stroke.is_uniform() { stroke.top.clone() } else { None },
- });
- }
-
- if !stroke.is_uniform() {
- for (path, stroke) in stroke_segments(size, radius, stroke) {
- if stroke.is_some() {
- res.push(Shape { geometry: Geometry::Path(path), fill: None, stroke });
- }
- }
- }
-
- res
-}
-
-/// Output the shape of the rectangle as a path or primitive rectangle,
-/// depending on whether it is rounded.
-fn fill_geometry(size: Size, radius: Corners<Abs>) -> Geometry {
- if radius.iter().copied().all(Abs::is_zero) {
- Geometry::Rect(size)
- } else {
- let mut paths = stroke_segments(size, radius, Sides::splat(None));
- assert_eq!(paths.len(), 1);
- Geometry::Path(paths.pop().unwrap().0)
- }
-}
-
-/// Output the minimum number of paths along the rectangles border.
-fn stroke_segments(
- size: Size,
- radius: Corners<Abs>,
- stroke: Sides<Option<FixedStroke>>,
-) -> Vec<(Path, Option<FixedStroke>)> {
- let mut res = vec![];
-
- let mut connection = Connection::default();
- let mut path = Path::new();
- let mut always_continuous = true;
- let max_radius = size.x.min(size.y).max(Abs::zero()) / 2.0;
-
- for side in [Side::Top, Side::Right, Side::Bottom, Side::Left] {
- let continuous = stroke.get_ref(side) == stroke.get_ref(side.next_cw());
- connection = connection.advance(continuous && side != Side::Left);
- always_continuous &= continuous;
-
- draw_side(
- &mut path,
- side,
- size,
- radius.get(side.start_corner()).clamp(Abs::zero(), max_radius),
- radius.get(side.end_corner()).clamp(Abs::zero(), max_radius),
- connection,
- );
-
- if !continuous {
- res.push((std::mem::take(&mut path), stroke.get_ref(side).clone()));
- }
- }
-
- if always_continuous {
- path.close_path();
- }
-
- if !path.0.is_empty() {
- res.push((path, stroke.left));
- }
-
- res
-}
-
-/// Draws one side of the rounded rectangle. Will always draw the left arc. The
-/// right arc will be drawn halfway if and only if there is no connection.
-fn draw_side(
- path: &mut Path,
- side: Side,
- size: Size,
- start_radius: Abs,
- end_radius: Abs,
- connection: Connection,
-) {
- let angle_left = Angle::deg(if connection.prev { 90.0 } else { 45.0 });
- let angle_right = Angle::deg(if connection.next { 90.0 } else { 45.0 });
- let length = size.get(side.axis());
-
- // The arcs for a border of the rectangle along the x-axis, starting at (0,0).
- let p1 = Point::with_x(start_radius);
- let mut arc1 = bezier_arc(
- p1 + Point::new(
- -angle_left.sin() * start_radius,
- (1.0 - angle_left.cos()) * start_radius,
- ),
- Point::new(start_radius, start_radius),
- p1,
- );
-
- let p2 = Point::with_x(length - end_radius);
- let mut arc2 = bezier_arc(
- p2,
- Point::new(length - end_radius, end_radius),
- p2 + Point::new(
- angle_right.sin() * end_radius,
- (1.0 - angle_right.cos()) * end_radius,
- ),
- );
-
- let transform = match side {
- Side::Left => Transform::rotate(Angle::deg(-90.0))
- .post_concat(Transform::translate(Abs::zero(), size.y)),
- Side::Bottom => Transform::rotate(Angle::deg(180.0))
- .post_concat(Transform::translate(size.x, size.y)),
- Side::Right => Transform::rotate(Angle::deg(90.0))
- .post_concat(Transform::translate(size.x, Abs::zero())),
- _ => Transform::identity(),
- };
-
- arc1 = arc1.map(|x| x.transform(transform));
- arc2 = arc2.map(|x| x.transform(transform));
-
- if !connection.prev {
- path.move_to(if start_radius.is_zero() { arc1[3] } else { arc1[0] });
- }
-
- if !start_radius.is_zero() {
- path.cubic_to(arc1[1], arc1[2], arc1[3]);
- }
-
- path.line_to(arc2[0]);
-
- if !connection.next && !end_radius.is_zero() {
- path.cubic_to(arc2[1], arc2[2], arc2[3]);
- }
-}
-
-/// Get the control points for a bezier curve that describes a circular arc for
-/// a start point, an end point and a center of the circle whose arc connects
-/// the two.
-fn bezier_arc(start: Point, center: Point, end: Point) -> [Point; 4] {
- // https://stackoverflow.com/a/44829356/1567835
- let a = start - center;
- let b = end - center;
-
- let q1 = a.x.to_raw() * a.x.to_raw() + a.y.to_raw() * a.y.to_raw();
- let q2 = q1 + a.x.to_raw() * b.x.to_raw() + a.y.to_raw() * b.y.to_raw();
- let k2 = (4.0 / 3.0) * ((2.0 * q1 * q2).sqrt() - q2)
- / (a.x.to_raw() * b.y.to_raw() - a.y.to_raw() * b.x.to_raw());
-
- let control_1 = Point::new(center.x + a.x - k2 * a.y, center.y + a.y + k2 * a.x);
- let control_2 = Point::new(center.x + b.x + k2 * b.y, center.y + b.y - k2 * b.x);
-
- [start, control_1, control_2, end]
-}
-
-/// Indicates which sides of the border strokes in a 2D polygon are connected to
-/// their neighboring sides.
-#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)]
-struct Connection {
- prev: bool,
- next: bool,
-}
-
-impl Connection {
- /// Advance to the next clockwise side of the polygon. The argument
- /// indicates whether the border is connected on the right side of the next
- /// edge.
- pub fn advance(self, next: bool) -> Self {
- Self { prev: self.next, next }
- }
-}
diff --git a/tests/ref/compiler/show-selector.png b/tests/ref/compiler/show-selector.png
index be5ba463..f8259698 100644
--- a/tests/ref/compiler/show-selector.png
+++ b/tests/ref/compiler/show-selector.png
Binary files differ
diff --git a/tests/ref/meta/figure.png b/tests/ref/meta/figure.png
index 37386698..83bd7b7f 100644
--- a/tests/ref/meta/figure.png
+++ b/tests/ref/meta/figure.png
Binary files differ
diff --git a/tests/ref/visualize/shape-fill-stroke.png b/tests/ref/visualize/shape-fill-stroke.png
index cabbfb2e..d4a4817a 100644
--- a/tests/ref/visualize/shape-fill-stroke.png
+++ b/tests/ref/visualize/shape-fill-stroke.png
Binary files differ
diff --git a/tests/ref/visualize/shape-rect.png b/tests/ref/visualize/shape-rect.png
index 48d40447..3eda642f 100644
--- a/tests/ref/visualize/shape-rect.png
+++ b/tests/ref/visualize/shape-rect.png
Binary files differ
diff --git a/tests/ref/visualize/shape-rounded.png b/tests/ref/visualize/shape-rounded.png
index 5e761a4d..ec926d0a 100644
--- a/tests/ref/visualize/shape-rounded.png
+++ b/tests/ref/visualize/shape-rounded.png
Binary files differ
diff --git a/tests/ref/visualize/stroke.png b/tests/ref/visualize/stroke.png
index 3aa87c4b..bdfcae9f 100644
--- a/tests/ref/visualize/stroke.png
+++ b/tests/ref/visualize/stroke.png
Binary files differ
diff --git a/tests/typ/visualize/shape-fill-stroke.typ b/tests/typ/visualize/shape-fill-stroke.typ
index 0d133171..8d187400 100644
--- a/tests/typ/visualize/shape-fill-stroke.typ
+++ b/tests/typ/visualize/shape-fill-stroke.typ
@@ -43,8 +43,51 @@
// Test stroke composition.
#set square(stroke: 4pt)
#set text(font: "Roboto")
-#square(
- stroke: (left: red, top: yellow, right: green, bottom: blue),
- radius: 100%, align(center+horizon)[*G*],
- inset: 8pt
+#stack(
+ dir: ltr,
+ square(
+ stroke: (left: red, top: yellow, right: green, bottom: blue),
+ radius: 50%, align(center+horizon)[*G*],
+ inset: 8pt
+ ),
+ h(0.5cm),
+ square(
+ stroke: (left: red, top: yellow + 8pt, right: green, bottom: blue + 2pt),
+ radius: 50%, align(center+horizon)[*G*],
+ inset: 8pt
+ ),
+ h(0.5cm),
+ square(
+ stroke: (left: red, top: yellow, right: green, bottom: blue),
+ radius: 100%, align(center+horizon)[*G*],
+ inset: 8pt
+ ),
+)
+
+// Join between different solid strokes
+#set square(size: 20pt, stroke: 2pt)
+#set square(stroke: (left: green + 4pt, top: black + 2pt, right: blue, bottom: black + 2pt))
+#stack(
+ dir: ltr,
+ square(),
+ h(0.2cm),
+ square(radius: (top-left: 0pt, rest: 1pt)),
+ h(0.2cm),
+ square(radius: (top-left: 0pt, rest: 8pt)),
+ h(0.2cm),
+ square(radius: (top-left: 0pt, rest: 100pt)),
+)
+
+
+// Join between solid and dotted strokes
+#set square(stroke: (left: green + 4pt, top: black + 2pt, right: (paint: blue, dash: "dotted"), bottom: (paint: black, dash: "dotted")))
+#stack(
+ dir: ltr,
+ square(),
+ h(0.2cm),
+ square(radius: (top-left: 0pt, rest: 1pt)),
+ h(0.2cm),
+ square(radius: (top-left: 0pt, rest: 8pt)),
+ h(0.2cm),
+ square(radius: (top-left: 0pt, rest: 100pt)),
)
diff --git a/tests/typ/visualize/shape-rect.typ b/tests/typ/visualize/shape-rect.typ
index 17dd0f92..98450c80 100644
--- a/tests/typ/visualize/shape-rect.typ
+++ b/tests/typ/visualize/shape-rect.typ
@@ -32,7 +32,7 @@
#stack(
dir: ltr,
spacing: 1fr,
- rect(width: 2cm, radius: 60%),
+ rect(width: 2cm, radius: 30%),
rect(width: 1cm, radius: (left: 10pt, right: 5pt)),
rect(width: 1.25cm, radius: (
top-left: 2pt,
diff --git a/tests/typ/visualize/shape-rounded.typ b/tests/typ/visualize/shape-rounded.typ
index 862141ba..42432dc9 100644
--- a/tests/typ/visualize/shape-rounded.typ
+++ b/tests/typ/visualize/shape-rounded.typ
@@ -1,6 +1,53 @@
// Test rounded rectangles and squares.
---
-// Ensure that radius is clamped.
-#rect(radius: -20pt)
-#square(radius: 30pt)
+#set square(size: 20pt, stroke: 4pt)
+
+// no radius for non-rounded corners
+#stack(
+ dir: ltr,
+ square(),
+ h(10pt),
+ square(radius: 0pt),
+ h(10pt),
+ square(radius: -10pt),
+)
+
+#stack(
+ dir: ltr,
+ square(),
+ h(10pt),
+ square(radius: 0%),
+ h(10pt),
+ square(radius: -10%),
+)
+
+
+// small values for small radius
+#stack(
+ dir: ltr,
+ square(radius: 1pt),
+ h(10pt),
+ square(radius: 5%),
+ h(10pt),
+ square(radius: 2pt),
+)
+
+// large values for large radius or circle
+#stack(
+ dir: ltr,
+ square(radius: 8pt),
+ h(10pt),
+ square(radius: 10pt),
+ h(10pt),
+ square(radius: 12pt),
+)
+
+#stack(
+ dir: ltr,
+ square(radius: 45%),
+ h(10pt),
+ square(radius: 50%),
+ h(10pt),
+ square(radius: 55%),
+)