summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--crates/typst-layout/src/shapes.rs167
-rw-r--r--crates/typst-library/src/layout/point.rs10
2 files changed, 99 insertions, 78 deletions
diff --git a/crates/typst-layout/src/shapes.rs b/crates/typst-layout/src/shapes.rs
index 1a9b38f2..0d19c346 100644
--- a/crates/typst-layout/src/shapes.rs
+++ b/crates/typst-layout/src/shapes.rs
@@ -714,12 +714,11 @@ pub fn clip_rect(
let outset = outset.relative_to(size);
let size = size + outset.sum_by_axis();
- let stroke_widths = stroke
- .as_ref()
- .map(|s| s.as_ref().map_or(Abs::zero(), |s| s.thickness / 2.0));
+ let stroke_widths = stroke.as_ref().map(|s| s.as_ref().map(|s| s.thickness / 2.0));
- let max_radius = (size.x.min(size.y)) / 2.0
- + stroke_widths.iter().cloned().min().unwrap_or(Abs::zero());
+ let min_stroke_width =
+ stroke_widths.iter().flatten().cloned().min().unwrap_or(Abs::zero());
+ let max_radius = (size.x.min(size.y)) / 2.0 + min_stroke_width;
let radius = radius.map(|side| side.relative_to(max_radius * 2.0).min(max_radius));
let corners = corners_control_points(size, &radius, stroke, &stroke_widths);
@@ -804,7 +803,7 @@ fn corners_control_points(
size: Size,
radius: &Corners<Abs>,
strokes: &Sides<Option<FixedStroke>>,
- stroke_widths: &Sides<Abs>,
+ stroke_widths: &Sides<Option<Abs>>,
) -> Corners<ControlPoints> {
Corners {
top_left: Corner::TopLeft,
@@ -837,12 +836,11 @@ fn segmented_rect(
strokes: &Sides<Option<FixedStroke>>,
) -> Vec<Shape> {
let mut res = vec![];
- let stroke_widths = strokes
- .as_ref()
- .map(|s| s.as_ref().map_or(Abs::zero(), |s| s.thickness / 2.0));
+ let stroke_widths = strokes.as_ref().map(|s| s.as_ref().map(|s| s.thickness / 2.0));
- let max_radius = (size.x.min(size.y)) / 2.0
- + stroke_widths.iter().cloned().min().unwrap_or(Abs::zero());
+ let min_stroke_width =
+ stroke_widths.iter().flatten().cloned().min().unwrap_or(Abs::zero());
+ let max_radius = (size.x.min(size.y)) / 2.0 + min_stroke_width;
let radius = radius.map(|side| side.relative_to(max_radius * 2.0).min(max_radius));
let corners = corners_control_points(size, &radius, strokes, &stroke_widths);
@@ -970,7 +968,7 @@ fn segment(
) -> (Shape, bool) {
fn fill_corner(corner: &ControlPoints) -> bool {
corner.stroke_before != corner.stroke_after
- || corner.radius() < corner.stroke_before
+ || corner.radius() < corner.stroke_width_before()
}
fn fill_corners(
@@ -1053,10 +1051,9 @@ fn fill_segment(
curve.move_(c.end_inner());
}
+ c.start_cap(&mut curve, start_cap);
if c.arc_outer() {
curve.arc_line(c.mid_outer(), c.center_outer(), c.end_outer());
- } else {
- c.start_cap(&mut curve, start_cap);
}
}
@@ -1096,10 +1093,9 @@ fn fill_segment(
} else {
curve.line(c.outer());
}
+ c.end_cap(&mut curve, end_cap);
if c.arc_inner() {
curve.arc_line(c.mid_inner(), c.center_inner(), c.start_inner());
- } else {
- c.end_cap(&mut curve, end_cap);
}
}
@@ -1146,24 +1142,14 @@ fn fill_segment(
/// ```
struct ControlPoints {
radius: Abs,
- stroke_after: Abs,
- stroke_before: Abs,
+ stroke_after: Option<Abs>,
+ stroke_before: Option<Abs>,
corner: Corner,
size: Size,
same: bool,
}
impl ControlPoints {
- /// Rotate point around the origin, relative to the top-left.
- fn rotate_centered(&self, point: Point) -> Point {
- match self.corner {
- Corner::TopLeft => point,
- Corner::TopRight => Point { x: -point.y, y: point.x },
- Corner::BottomRight => Point { x: -point.x, y: -point.y },
- Corner::BottomLeft => Point { x: point.y, y: -point.x },
- }
- }
-
/// Move and rotate the point from top-left to the required corner.
fn rotate(&self, point: Point) -> Point {
match self.corner {
@@ -1176,17 +1162,36 @@ impl ControlPoints {
}
}
+ /// If no stroke is specified before and there is a radius, use the same
+ /// width as on the side after. Otherwise default to zero.
+ fn stroke_width_before(&self) -> Abs {
+ self.stroke_before
+ .or(self.stroke_after.filter(|_| self.radius != Abs::zero()))
+ .unwrap_or(Abs::zero())
+ }
+
+ /// If no stroke is specified after and there is a radius, use the same
+ /// width as on the side before. Otherwise default to zero.
+ fn stroke_width_after(&self) -> Abs {
+ self.stroke_after
+ .or(self.stroke_before.filter(|_| self.radius != Abs::zero()))
+ .unwrap_or(Abs::zero())
+ }
+
/// Outside intersection of the sides.
pub fn outer(&self) -> Point {
- self.rotate(Point { x: -self.stroke_before, y: -self.stroke_after })
+ self.rotate(Point {
+ x: -self.stroke_width_before(),
+ y: -self.stroke_width_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,
+ x: r - self.stroke_width_before(),
+ y: r - self.stroke_width_after(),
})
}
@@ -1201,8 +1206,8 @@ impl ControlPoints {
let r = self.radius_inner();
self.rotate(Point {
- x: self.stroke_before + r,
- y: self.stroke_after + r,
+ x: self.stroke_width_before() + r,
+ y: self.stroke_width_after() + r,
})
}
@@ -1213,12 +1218,14 @@ impl ControlPoints {
/// Radius of the middle arc.
pub fn radius(&self) -> Abs {
- (self.radius - self.stroke_before.min(self.stroke_after)).max(Abs::zero())
+ (self.radius - self.stroke_width_before().min(self.stroke_width_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())
+ (self.radius - 2.0 * self.stroke_width_before().max(self.stroke_width_after()))
+ .max(Abs::zero())
}
/// Middle of the corner on the outside of the stroke.
@@ -1272,8 +1279,8 @@ impl ControlPoints {
/// 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,
+ x: -self.stroke_width_before(),
+ y: self.radius_outer() - self.stroke_width_after(),
})
}
@@ -1285,16 +1292,16 @@ impl ControlPoints {
/// 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(),
+ x: self.stroke_width_before(),
+ y: self.stroke_width_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,
+ x: self.radius_outer() - self.stroke_width_before(),
+ y: -self.stroke_width_after(),
})
}
@@ -1306,8 +1313,8 @@ impl ControlPoints {
/// 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,
+ x: self.stroke_width_before() + self.radius_inner(),
+ y: self.stroke_width_after(),
})
}
@@ -1315,74 +1322,78 @@ impl ControlPoints {
///
/// If this corner has a stroke before it,
/// a default "butt" cap is used.
- ///
- /// NOTE: doesn't support the case where the corner has a radius.
pub fn start_cap(&self, curve: &mut Curve, cap_type: LineCap) {
- if self.stroke_before != Abs::zero()
- || self.radius != Abs::zero()
+ // Avoid misshaped caps on small radii.
+ let small_radius = self.radius < 2.0 * self.stroke_width_after();
+ if self.stroke_before.is_some()
|| cap_type == LineCap::Butt
+ || self.radius != Abs::zero() && small_radius
{
// Just the default cap.
- curve.line(self.outer());
+ curve.line(self.mid_outer());
} else if cap_type == LineCap::Square {
+ let butt_start = self.mid_inner();
+ let butt_end = self.mid_outer();
// Extend by the stroke width.
- let offset =
- self.rotate_centered(Point { x: -self.stroke_after, y: Abs::zero() });
- curve.line(self.end_inner() + offset);
- curve.line(self.outer() + offset);
+ let offset_dir = line_normal(butt_start, butt_end);
+ let offset = self.stroke_width_after().to_raw() * offset_dir;
+ curve.line(butt_start + offset);
+ curve.line(butt_end + offset);
+ curve.line(butt_end);
} else if cap_type == LineCap::Round {
+ let arc_start = self.mid_inner();
+ let arc_end = self.mid_outer();
// We push the center by a little bit to ensure the correct
// half of the circle gets drawn. If it is perfectly centered
// the `arc` function just degenerates into a line, which we
// do not want in this case.
- curve.arc(
- self.end_inner(),
- (self.end_inner()
- + self.rotate_centered(Point { x: Abs::raw(1.0), y: Abs::zero() })
- + self.outer())
- / 2.,
- self.outer(),
- );
+ let offset_dir = -line_normal(arc_start, arc_end);
+ let arc_center = (arc_start + arc_end) / 2.0 + offset_dir;
+ curve.arc(arc_start, arc_center, arc_end);
}
- curve.line(self.end_outer());
}
/// Draw the cap at the end of the segment.
///
/// If this corner has a stroke before it,
/// a default "butt" cap is used.
- ///
- /// NOTE: doesn't support the case where the corner has a radius.
pub fn end_cap(&self, curve: &mut Curve, cap_type: LineCap) {
- if self.stroke_after != Abs::zero()
- || self.radius != Abs::zero()
+ // Avoid misshaped caps on small radii.
+ let small_radius = self.radius < 2.0 * self.stroke_width_before();
+ if self.stroke_after.is_some()
|| cap_type == LineCap::Butt
+ || self.radius != Abs::zero() && small_radius
{
// Just the default cap.
- curve.line(self.center_inner());
+ curve.line(self.mid_inner());
} else if cap_type == LineCap::Square {
+ let butt_start = self.mid_outer();
+ let butt_end = self.mid_inner();
// Extend by the stroke width.
- let offset =
- self.rotate_centered(Point { x: Abs::zero(), y: -self.stroke_before });
- curve.line(self.outer() + offset);
- curve.line(self.center_inner() + offset);
+ let offset_dir = line_normal(butt_start, butt_end);
+ let offset = self.stroke_width_before().to_raw() * offset_dir;
+ curve.line(butt_start + offset);
+ curve.line(butt_end + offset);
+ curve.line(butt_end);
} else if cap_type == LineCap::Round {
+ let arc_start = self.mid_outer();
+ let arc_end = self.mid_inner();
// We push the center by a little bit to ensure the correct
// half of the circle gets drawn. If it is perfectly centered
// the `arc` function just degenerates into a line, which we
// do not want in this case.
- curve.arc(
- self.outer(),
- (self.outer()
- + self.rotate_centered(Point { x: Abs::zero(), y: Abs::raw(1.0) })
- + self.center_inner())
- / 2.,
- self.center_inner(),
- );
+ let arc_center_offset = -line_normal(arc_start, arc_end);
+ let arc_center = (arc_start + arc_end) / 2.0 + arc_center_offset;
+ curve.arc(arc_start, arc_center, arc_end);
}
}
}
+/// Computes the normal vector towards the left of the line.
+fn line_normal(start: Point, end: Point) -> Point {
+ (end - start).rot90ccw().normalized()
+}
+
/// Helper to draw arcs with Bézier curves.
trait CurveExt {
fn arc(&mut self, start: Point, center: Point, end: Point);
diff --git a/crates/typst-library/src/layout/point.rs b/crates/typst-library/src/layout/point.rs
index c3d1594e..2e49c482 100644
--- a/crates/typst-library/src/layout/point.rs
+++ b/crates/typst-library/src/layout/point.rs
@@ -60,6 +60,16 @@ impl Point {
Abs::raw(self.x.to_raw().hypot(self.y.to_raw()))
}
+ // TODO: this is a bit awkward on a point struct.
+ pub fn normalized(self) -> Self {
+ self / self.hypot().to_raw()
+ }
+
+ /// Rotate the point 90 degrees counter-clockwise.
+ pub fn rot90ccw(self) -> Self {
+ Self { x: self.y, y: -self.x }
+ }
+
/// Transform the point with the given transformation.
///
/// In the event that one of the coordinates is infinite, the result will