Better rect edges (#1956)
@ -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))),
|
||||
)
|
||||
|
@ -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()
|
||||
|
@ -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};
|
||||
|
541
crates/typst/src/geom/rect.rs
Normal file
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
@ -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 }
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 242 KiB After Width: | Height: | Size: 242 KiB |
Before Width: | Height: | Size: 876 B After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.4 KiB |
@ -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)),
|
||||
)
|
||||
|
@ -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,
|
||||
|
@ -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%),
|
||||
)
|
||||
|