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 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);
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user