mirror of
https://github.com/typst/typst
synced 2025-07-16 00:52:54 +08:00
feat: draw round caps on corners with radius
This commit is contained in:
parent
f9b01f595d
commit
c85240a8fe
@ -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);
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user