feat: draw round caps on corners with radius

This commit is contained in:
Tobias Schmitz 2025-07-08 17:15:16 +02:00
parent f9b01f595d
commit c85240a8fe
No known key found for this signature in database
2 changed files with 99 additions and 78 deletions

View File

@ -714,12 +714,11 @@ pub fn clip_rect(
let outset = outset.relative_to(size); let outset = outset.relative_to(size);
let size = size + outset.sum_by_axis(); let size = size + outset.sum_by_axis();
let stroke_widths = stroke let stroke_widths = stroke.as_ref().map(|s| s.as_ref().map(|s| s.thickness / 2.0));
.as_ref()
.map(|s| s.as_ref().map_or(Abs::zero(), |s| s.thickness / 2.0));
let max_radius = (size.x.min(size.y)) / 2.0 let min_stroke_width =
+ stroke_widths.iter().cloned().min().unwrap_or(Abs::zero()); 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 radius = radius.map(|side| side.relative_to(max_radius * 2.0).min(max_radius));
let corners = corners_control_points(size, &radius, stroke, &stroke_widths); let corners = corners_control_points(size, &radius, stroke, &stroke_widths);
@ -804,7 +803,7 @@ fn corners_control_points(
size: Size, size: Size,
radius: &Corners<Abs>, radius: &Corners<Abs>,
strokes: &Sides<Option<FixedStroke>>, strokes: &Sides<Option<FixedStroke>>,
stroke_widths: &Sides<Abs>, stroke_widths: &Sides<Option<Abs>>,
) -> Corners<ControlPoints> { ) -> Corners<ControlPoints> {
Corners { Corners {
top_left: Corner::TopLeft, top_left: Corner::TopLeft,
@ -837,12 +836,11 @@ fn segmented_rect(
strokes: &Sides<Option<FixedStroke>>, strokes: &Sides<Option<FixedStroke>>,
) -> Vec<Shape> { ) -> Vec<Shape> {
let mut res = vec![]; let mut res = vec![];
let stroke_widths = strokes let stroke_widths = strokes.as_ref().map(|s| s.as_ref().map(|s| s.thickness / 2.0));
.as_ref()
.map(|s| s.as_ref().map_or(Abs::zero(), |s| s.thickness / 2.0));
let max_radius = (size.x.min(size.y)) / 2.0 let min_stroke_width =
+ stroke_widths.iter().cloned().min().unwrap_or(Abs::zero()); 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 radius = radius.map(|side| side.relative_to(max_radius * 2.0).min(max_radius));
let corners = corners_control_points(size, &radius, strokes, &stroke_widths); let corners = corners_control_points(size, &radius, strokes, &stroke_widths);
@ -970,7 +968,7 @@ fn segment(
) -> (Shape, bool) { ) -> (Shape, bool) {
fn fill_corner(corner: &ControlPoints) -> bool { fn fill_corner(corner: &ControlPoints) -> bool {
corner.stroke_before != corner.stroke_after corner.stroke_before != corner.stroke_after
|| corner.radius() < corner.stroke_before || corner.radius() < corner.stroke_width_before()
} }
fn fill_corners( fn fill_corners(
@ -1053,10 +1051,9 @@ fn fill_segment(
curve.move_(c.end_inner()); curve.move_(c.end_inner());
} }
c.start_cap(&mut curve, start_cap);
if c.arc_outer() { if c.arc_outer() {
curve.arc_line(c.mid_outer(), c.center_outer(), c.end_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 { } else {
curve.line(c.outer()); curve.line(c.outer());
} }
c.end_cap(&mut curve, end_cap);
if c.arc_inner() { if c.arc_inner() {
curve.arc_line(c.mid_inner(), c.center_inner(), c.start_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 { struct ControlPoints {
radius: Abs, radius: Abs,
stroke_after: Abs, stroke_after: Option<Abs>,
stroke_before: Abs, stroke_before: Option<Abs>,
corner: Corner, corner: Corner,
size: Size, size: Size,
same: bool, same: bool,
} }
impl ControlPoints { 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. /// Move and rotate the point from top-left to the required corner.
fn rotate(&self, point: Point) -> Point { fn rotate(&self, point: Point) -> Point {
match self.corner { 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. /// Outside intersection of the sides.
pub fn outer(&self) -> Point { 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. /// Center for the outer arc.
pub fn center_outer(&self) -> Point { pub fn center_outer(&self) -> Point {
let r = self.radius_outer(); let r = self.radius_outer();
self.rotate(Point { self.rotate(Point {
x: r - self.stroke_before, x: r - self.stroke_width_before(),
y: r - self.stroke_after, y: r - self.stroke_width_after(),
}) })
} }
@ -1201,8 +1206,8 @@ impl ControlPoints {
let r = self.radius_inner(); let r = self.radius_inner();
self.rotate(Point { self.rotate(Point {
x: self.stroke_before + r, x: self.stroke_width_before() + r,
y: self.stroke_after + r, y: self.stroke_width_after() + r,
}) })
} }
@ -1213,12 +1218,14 @@ impl ControlPoints {
/// Radius of the middle arc. /// Radius of the middle arc.
pub fn radius(&self) -> Abs { 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. /// Radius of the inner arc.
pub fn radius_inner(&self) -> Abs { 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. /// 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. /// Start of the corner on the outside of the stroke.
pub fn start_outer(&self) -> Point { pub fn start_outer(&self) -> Point {
self.rotate(Point { self.rotate(Point {
x: -self.stroke_before, x: -self.stroke_width_before(),
y: self.radius_outer() - self.stroke_after, y: self.radius_outer() - self.stroke_width_after(),
}) })
} }
@ -1285,16 +1292,16 @@ impl ControlPoints {
/// Start of the corner on the inside of the stroke. /// Start of the corner on the inside of the stroke.
pub fn start_inner(&self) -> Point { pub fn start_inner(&self) -> Point {
self.rotate(Point { self.rotate(Point {
x: self.stroke_before, x: self.stroke_width_before(),
y: self.stroke_after + self.radius_inner(), y: self.stroke_width_after() + self.radius_inner(),
}) })
} }
/// End of the corner on the outside of the stroke. /// End of the corner on the outside of the stroke.
pub fn end_outer(&self) -> Point { pub fn end_outer(&self) -> Point {
self.rotate(Point { self.rotate(Point {
x: self.radius_outer() - self.stroke_before, x: self.radius_outer() - self.stroke_width_before(),
y: -self.stroke_after, y: -self.stroke_width_after(),
}) })
} }
@ -1306,8 +1313,8 @@ impl ControlPoints {
/// End of the corner on the inside of the stroke. /// End of the corner on the inside of the stroke.
pub fn end_inner(&self) -> Point { pub fn end_inner(&self) -> Point {
self.rotate(Point { self.rotate(Point {
x: self.stroke_before + self.radius_inner(), x: self.stroke_width_before() + self.radius_inner(),
y: self.stroke_after, y: self.stroke_width_after(),
}) })
} }
@ -1315,74 +1322,78 @@ impl ControlPoints {
/// ///
/// If this corner has a stroke before it, /// If this corner has a stroke before it,
/// a default "butt" cap is used. /// 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) { pub fn start_cap(&self, curve: &mut Curve, cap_type: LineCap) {
if self.stroke_before != Abs::zero() // Avoid misshaped caps on small radii.
|| self.radius != Abs::zero() let small_radius = self.radius < 2.0 * self.stroke_width_after();
if self.stroke_before.is_some()
|| cap_type == LineCap::Butt || cap_type == LineCap::Butt
|| self.radius != Abs::zero() && small_radius
{ {
// Just the default cap. // Just the default cap.
curve.line(self.outer()); curve.line(self.mid_outer());
} else if cap_type == LineCap::Square { } else if cap_type == LineCap::Square {
let butt_start = self.mid_inner();
let butt_end = self.mid_outer();
// Extend by the stroke width. // Extend by the stroke width.
let offset = let offset_dir = line_normal(butt_start, butt_end);
self.rotate_centered(Point { x: -self.stroke_after, y: Abs::zero() }); let offset = self.stroke_width_after().to_raw() * offset_dir;
curve.line(self.end_inner() + offset); curve.line(butt_start + offset);
curve.line(self.outer() + offset); curve.line(butt_end + offset);
curve.line(butt_end);
} else if cap_type == LineCap::Round { } 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 // We push the center by a little bit to ensure the correct
// half of the circle gets drawn. If it is perfectly centered // half of the circle gets drawn. If it is perfectly centered
// the `arc` function just degenerates into a line, which we // the `arc` function just degenerates into a line, which we
// do not want in this case. // do not want in this case.
curve.arc( let offset_dir = -line_normal(arc_start, arc_end);
self.end_inner(), let arc_center = (arc_start + arc_end) / 2.0 + offset_dir;
(self.end_inner() curve.arc(arc_start, arc_center, arc_end);
+ self.rotate_centered(Point { x: Abs::raw(1.0), y: Abs::zero() })
+ self.outer())
/ 2.,
self.outer(),
);
} }
curve.line(self.end_outer());
} }
/// Draw the cap at the end of the segment. /// Draw the cap at the end of the segment.
/// ///
/// If this corner has a stroke before it, /// If this corner has a stroke before it,
/// a default "butt" cap is used. /// 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) { pub fn end_cap(&self, curve: &mut Curve, cap_type: LineCap) {
if self.stroke_after != Abs::zero() // Avoid misshaped caps on small radii.
|| self.radius != Abs::zero() let small_radius = self.radius < 2.0 * self.stroke_width_before();
if self.stroke_after.is_some()
|| cap_type == LineCap::Butt || cap_type == LineCap::Butt
|| self.radius != Abs::zero() && small_radius
{ {
// Just the default cap. // Just the default cap.
curve.line(self.center_inner()); curve.line(self.mid_inner());
} else if cap_type == LineCap::Square { } else if cap_type == LineCap::Square {
let butt_start = self.mid_outer();
let butt_end = self.mid_inner();
// Extend by the stroke width. // Extend by the stroke width.
let offset = let offset_dir = line_normal(butt_start, butt_end);
self.rotate_centered(Point { x: Abs::zero(), y: -self.stroke_before }); let offset = self.stroke_width_before().to_raw() * offset_dir;
curve.line(self.outer() + offset); curve.line(butt_start + offset);
curve.line(self.center_inner() + offset); curve.line(butt_end + offset);
curve.line(butt_end);
} else if cap_type == LineCap::Round { } 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 // We push the center by a little bit to ensure the correct
// half of the circle gets drawn. If it is perfectly centered // half of the circle gets drawn. If it is perfectly centered
// the `arc` function just degenerates into a line, which we // the `arc` function just degenerates into a line, which we
// do not want in this case. // do not want in this case.
curve.arc( let arc_center_offset = -line_normal(arc_start, arc_end);
self.outer(), let arc_center = (arc_start + arc_end) / 2.0 + arc_center_offset;
(self.outer() curve.arc(arc_start, arc_center, arc_end);
+ self.rotate_centered(Point { x: Abs::zero(), y: Abs::raw(1.0) })
+ self.center_inner())
/ 2.,
self.center_inner(),
);
} }
} }
} }
/// 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. /// Helper to draw arcs with Bézier curves.
trait CurveExt { trait CurveExt {
fn arc(&mut self, start: Point, center: Point, end: Point); fn arc(&mut self, start: Point, center: Point, end: Point);

View File

@ -60,6 +60,16 @@ impl Point {
Abs::raw(self.x.to_raw().hypot(self.y.to_raw())) 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. /// Transform the point with the given transformation.
/// ///
/// In the event that one of the coordinates is infinite, the result will /// In the event that one of the coordinates is infinite, the result will