Fix path and polygon strokes

This commit is contained in:
Laurenz 2023-04-06 12:57:26 +02:00
parent f9b9be16f9
commit 4f4af02ace
5 changed files with 113 additions and 87 deletions

View File

@ -2,51 +2,59 @@ use self::PathVertex::{AllControlPoints, MirroredControlPoint, Vertex};
use crate::prelude::*; use crate::prelude::*;
use kurbo::{CubicBez, ParamCurveExtrema}; use kurbo::{CubicBez, ParamCurveExtrema};
/// A path going through a list of points, connected through Bezier curves. /// A path through a list of points, connected by Bezier curves.
/// ///
/// ## Example /// ## Example
/// ```example /// ```example
/// #set page(height: 100pt) /// #path(
/// #path((10%, 10%), ((20%, 20%), (5%, 5%))) /// fill: blue.lighten(80%),
/// #path((10%, 10%), (10%, 15%)) /// stroke: blue,
/// closed: true,
/// (0pt, 50pt),
/// (100%, 50pt),
/// ((50%, 0pt), (40pt, 0pt)),
/// )
/// ``` /// ```
/// ///
/// Display: Path /// Display: Path
/// Category: visualize /// Category: visualize
#[element(Layout)] #[element(Layout)]
pub struct PathElem { pub struct PathElem {
/// Whether to close this path with one last bezier curve. This last curve /// How to fill the path. See the
/// still takes into account the control points.
/// If you want to close with a straight line, simply add one last point
/// that's the same as the start point.
#[default(false)]
pub closed: bool,
/// How to fill the polygon. See the
/// [rectangle's documentation]($func/rect.fill) for more details. /// [rectangle's documentation]($func/rect.fill) for more details.
/// ///
/// Currently all paths are filled according to the /// Currently all paths are filled according to the
/// [non-zero winding rule](https://en.wikipedia.org/wiki/Nonzero-rule). /// [non-zero winding rule](https://en.wikipedia.org/wiki/Nonzero-rule).
pub fill: Option<Paint>, pub fill: Option<Paint>,
/// How to stroke the polygon. See the [lines's /// How to stroke the path. See the
/// documentation]($func/line.stroke) for more details. /// [polygon's documentation]($func/polygon.stroke) for more details.
#[resolve] #[resolve]
#[fold] #[fold]
#[default(Some(PartialStroke::default()))] pub stroke: Smart<Option<PartialStroke>>,
pub stroke: Option<PartialStroke>,
/// Whether to close this path with one last bezier curve. This curve will
/// takes into account the adjacent control points. If you want to close
/// with a straight line, simply add one last point that's the same as the
/// start point.
#[default(false)]
pub closed: bool,
/// The vertices of the path. /// The vertices of the path.
/// ///
/// Each vertex can be defined in 3 ways: /// Each vertex can be defined in 3 ways:
/// ///
/// - A regular point, like [line]($func/line) /// - A regular point, as given to the [`line`]($func/line) or
/// [`polygon`]($func/polygon) function.
/// - An array of two points, the first being the vertex and the second /// - An array of two points, the first being the vertex and the second
/// being the control point. /// being the control point. The control point is expressed relative to
/// The control point is expressed relative to the vertex and is mirrored /// the vertex and is mirrored to get the second control point. The given
/// to get the second control point. /// control point is the one that affects the curve coming _into_ this
/// The control point itself refers to the control point that affects the curve coming _into_ this vertex, including for the first point. /// vertex (even for the first point). The mirrored control point affects
/// - An array of three points, the first being the vertex and the next being the control points (control point for curves coming in and out respectively) /// the curve going out of this vertex.
/// - An array of three points, the first being the vertex and the next
/// being the control points (control point for curves coming in and out,
/// respectively)
#[variadic] #[variadic]
pub vertices: Vec<PathVertex>, pub vertices: Vec<PathVertex>,
} }
@ -69,20 +77,19 @@ impl Layout for PathElem {
let points: Vec<Point> = vertices.iter().map(|c| resolve(c.vertex())).collect(); let points: Vec<Point> = vertices.iter().map(|c| resolve(c.vertex())).collect();
let mut size = Size::zero(); let mut size = Size::zero();
if points.is_empty() {
return Ok(Fragment::frame(Frame::new(size)));
}
// Only create a path if there are more than zero points. // Only create a path if there are more than zero points.
let path = if points.len() > 0 { // Construct a closed path given all points.
// Construct a closed path given all points. let mut path = Path::new();
let mut path = Path::new(); path.move_to(points[0]);
path.move_to(points[0]);
let mut add_cubic = |from_point: Point, let mut add_cubic =
to_point: Point, |from_point: Point, to_point: Point, from: PathVertex, to: PathVertex| {
from: PathVertex,
to: PathVertex| {
let from_control_point = resolve(from.control_point_from()) + from_point; let from_control_point = resolve(from.control_point_from()) + from_point;
let to_control_point = resolve(to.control_point_to()) + to_point; let to_control_point = resolve(to.control_point_to()) + to_point;
path.cubic_to(from_control_point, to_control_point, to_point); path.cubic_to(from_control_point, to_control_point, to_point);
let p0 = kurbo::Point::new(from_point.x.to_raw(), from_point.y.to_raw()); let p0 = kurbo::Point::new(from_point.x.to_raw(), from_point.y.to_raw());
@ -100,38 +107,35 @@ impl Layout for PathElem {
size.y.set_max(Abs::raw(extrema.y1)); size.y.set_max(Abs::raw(extrema.y1));
}; };
for (vertex_window, point_window) in for (vertex_window, point_window) in vertices.windows(2).zip(points.windows(2)) {
vertices.windows(2).zip(points.windows(2)) let from = vertex_window[0];
{ let to = vertex_window[1];
let from = vertex_window[0]; let from_point = point_window[0];
let to = vertex_window[1]; let to_point = point_window[1];
let from_point = point_window[0];
let to_point = point_window[1];
add_cubic(from_point, to_point, from, to); add_cubic(from_point, to_point, from, to);
} }
if self.closed(styles) { if self.closed(styles) {
let from = *vertices.last().unwrap(); // We checked that we have at least one element. let from = *vertices.last().unwrap(); // We checked that we have at least one element.
let to = vertices[0]; let to = vertices[0];
let from_point = *points.last().unwrap(); let from_point = *points.last().unwrap();
let to_point = points[0]; let to_point = points[0];
add_cubic(from_point, to_point, from, to); add_cubic(from_point, to_point, from, to);
} }
Some(path) // Prepare fill and stroke.
} else { let fill = self.fill(styles);
None let stroke = match self.stroke(styles) {
Smart::Auto if fill.is_none() => Some(Stroke::default()),
Smart::Auto => None,
Smart::Custom(stroke) => stroke.map(PartialStroke::unwrap_or_default),
}; };
let mut frame = Frame::new(size); let mut frame = Frame::new(size);
if let Some(path) = path { let shape = Shape { geometry: Geometry::Path(path), stroke, fill };
let fill = self.fill(styles); frame.push(Point::zero(), FrameItem::Shape(shape, self.span()));
let stroke = self.stroke(styles).map(PartialStroke::unwrap_or_default);
let shape = Shape { geometry: Geometry::Path(path), stroke, fill };
frame.push(Point::zero(), FrameItem::Shape(shape, self.span()));
}
Ok(Fragment::frame(frame)) Ok(Fragment::frame(frame))
} }

View File

@ -7,12 +7,12 @@ use crate::prelude::*;
/// ## Example /// ## Example
/// ```example /// ```example
/// #polygon( /// #polygon(
/// fill: red, /// fill: blue.lighten(80%),
/// stroke: 2pt + black, /// stroke: blue,
/// (0pt, 0pt), /// (20%, 0pt),
/// (50%, 0pt), /// (60%, 0pt),
/// (50%, 4cm), /// (80%, 2cm),
/// (20%, 4cm), /// (0%, 2cm),
/// ) /// )
/// ``` /// ```
/// ///
@ -27,11 +27,20 @@ pub struct PolygonElem {
/// [non-zero winding rule](https://en.wikipedia.org/wiki/Nonzero-rule). /// [non-zero winding rule](https://en.wikipedia.org/wiki/Nonzero-rule).
pub fill: Option<Paint>, pub fill: Option<Paint>,
/// How to stroke the polygon. See the [lines's /// How to stroke the polygon. This can be:
/// documentation]($func/line.stroke) for more details. ///
/// - `{none}` to disable the stroke.
/// - `{auto}` for a stroke of `{1pt}` black if and if only if no fill is
/// given.
/// - A length specifying the stroke's thickness. The color is inherited,
/// defaulting to black.
/// - A color to use for the stroke. The thickness is inherited, defaulting
/// to `{1pt}`.
/// - A stroke combined from color and thickness using the `+` operator as
/// in `{2pt + red}`.
#[resolve] #[resolve]
#[fold] #[fold]
pub stroke: Option<PartialStroke>, pub stroke: Smart<Option<PartialStroke>>,
/// The vertices of the polygon. Each point is specified as an array of two /// The vertices of the polygon. Each point is specified as an array of two
/// [relative lengths]($type/relative-length). /// [relative lengths]($type/relative-length).
@ -61,22 +70,29 @@ impl Layout for PolygonElem {
let mut frame = Frame::new(size); let mut frame = Frame::new(size);
// Only create a path if there are more than zero points. // Only create a path if there are more than zero points.
if !points.is_empty() { if points.is_empty() {
let fill = self.fill(styles); return Ok(Fragment::frame(frame));
let stroke = self.stroke(styles).map(PartialStroke::unwrap_or_default);
// Construct a closed path given all points.
let mut path = Path::new();
path.move_to(points[0]);
for &point in &points[1..] {
path.line_to(point);
}
path.close_path();
let shape = Shape { geometry: Geometry::Path(path), stroke, fill };
frame.push(Point::zero(), FrameItem::Shape(shape, self.span()));
} }
// Prepare fill and stroke.
let fill = self.fill(styles);
let stroke = match self.stroke(styles) {
Smart::Auto if fill.is_none() => Some(Stroke::default()),
Smart::Auto => None,
Smart::Custom(stroke) => stroke.map(PartialStroke::unwrap_or_default),
};
// Construct a closed path given all points.
let mut path = Path::new();
path.move_to(points[0]);
for &point in &points[1..] {
path.line_to(point);
}
path.close_path();
let shape = Shape { geometry: Geometry::Path(path), stroke, fill };
frame.push(Point::zero(), FrameItem::Shape(shape, self.span()));
Ok(Fragment::frame(frame)) Ok(Fragment::frame(frame))
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -11,13 +11,18 @@ Apart
--- ---
// Test box sizing. // Test box sizing.
#box(width: 50pt, height: 50pt, fill: yellow, path( #box(
fill: purple, width: 50pt,
(0pt, 0pt), height: 50pt,
(30pt, 30pt), fill: yellow,
(0pt, 30pt), path(
(30pt, 0pt), fill: purple,
)) (0pt, 0pt),
(30pt, 30pt),
(0pt, 30pt),
(30pt, 0pt),
),
)
--- ---
// Test fr box. // Test fr box.

View File

@ -8,7 +8,6 @@
align: center + horizon, align: center + horizon,
path( path(
fill: red, fill: red,
stroke: none,
closed: true, closed: true,
((0%, 0%), (4%, -4%)), ((0%, 0%), (4%, -4%)),
((50%, 50%), (4%, -4%)), ((50%, 50%), (4%, -4%)),
@ -17,6 +16,7 @@
), ),
path( path(
fill: purple, fill: purple,
stroke: 1pt,
(0pt, 0pt), (0pt, 0pt),
(30pt, 30pt), (30pt, 30pt),
(0pt, 30pt), (0pt, 30pt),
@ -24,6 +24,7 @@
), ),
path( path(
fill: blue, fill: blue,
stroke: 1pt,
closed: true, closed: true,
((30%, 0%), (35%, 30%), (-20%, 0%)), ((30%, 0%), (35%, 30%), (-20%, 0%)),
((30%, 60%), (-20%, 0%), (0%, 0%)), ((30%, 60%), (-20%, 0%), (0%, 0%)),