summaryrefslogtreecommitdiff
path: root/library
diff options
context:
space:
mode:
authorSekoiaTree <51149447+SekoiaTree@users.noreply.github.com>2023-04-06 12:10:49 +0200
committerGitHub <noreply@github.com>2023-04-06 12:10:49 +0200
commit52b92a9d352a47421e83649ece585882053a4ec7 (patch)
tree2619f52b1d27bd15b788fd082492ddcdbed466dd /library
parent085282c138899dd5aaa06bc6ae7bd2f79d75d7e1 (diff)
Path support (#536)
Diffstat (limited to 'library')
-rw-r--r--library/src/lib.rs1
-rw-r--r--library/src/visualize/mod.rs2
-rw-r--r--library/src/visualize/path.rs210
-rw-r--r--library/src/visualize/polygon.rs3
4 files changed, 214 insertions, 2 deletions
diff --git a/library/src/lib.rs b/library/src/lib.rs
index cabafd8c..c11b818e 100644
--- a/library/src/lib.rs
+++ b/library/src/lib.rs
@@ -84,6 +84,7 @@ fn global(math: Module, calc: Module) -> Module {
global.define("ellipse", visualize::EllipseElem::func());
global.define("circle", visualize::CircleElem::func());
global.define("polygon", visualize::PolygonElem::func());
+ global.define("path", visualize::PathElem::func());
// Meta.
global.define("document", meta::DocumentElem::func());
diff --git a/library/src/visualize/mod.rs b/library/src/visualize/mod.rs
index 06eec23d..4cf5d04f 100644
--- a/library/src/visualize/mod.rs
+++ b/library/src/visualize/mod.rs
@@ -2,10 +2,12 @@
mod image;
mod line;
+mod path;
mod polygon;
mod shape;
pub use self::image::*;
pub use self::line::*;
+pub use self::path::*;
pub use self::polygon::*;
pub use self::shape::*;
diff --git a/library/src/visualize/path.rs b/library/src/visualize/path.rs
new file mode 100644
index 00000000..44f506e8
--- /dev/null
+++ b/library/src/visualize/path.rs
@@ -0,0 +1,210 @@
+use self::PathVertex::{AllControlPoints, MirroredControlPoint, Vertex};
+use crate::prelude::*;
+use kurbo::{CubicBez, ParamCurveExtrema};
+
+/// A path going through a list of points, connected through Bezier curves.
+///
+/// ## Example
+/// ```example
+/// #set page(height: 100pt)
+/// #path((10%, 10%), ((20%, 20%), (5%, 5%)))
+/// #path((10%, 10%), (10%, 15%))
+/// ```
+///
+/// Display: Path
+/// Category: visualize
+#[element(Layout)]
+pub struct PathElem {
+ /// Whether to close this path with one last bezier curve. This last curve
+ /// 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.
+ ///
+ /// Currently all paths are filled according to the
+ /// [non-zero winding rule](https://en.wikipedia.org/wiki/Nonzero-rule).
+ pub fill: Option<Paint>,
+
+ /// How to stroke the polygon. See the [lines's
+ /// documentation]($func/line.stroke) for more details.
+ #[resolve]
+ #[fold]
+ #[default(Some(PartialStroke::default()))]
+ pub stroke: Option<PartialStroke>,
+
+ /// The vertices of the path.
+ ///
+ /// Each vertex can be defined in 3 ways:
+ ///
+ /// - A regular point, like [line]($func/line)
+ /// - An array of two points, the first being the vertex and the second
+ /// being the control point.
+ /// The control point is expressed relative to the vertex and is mirrored
+ /// to get the second control point.
+ /// The control point itself refers to the control point that affects the curve coming _into_ this vertex, including for the first point.
+ /// - 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]
+ pub vertices: Vec<PathVertex>,
+}
+
+impl Layout for PathElem {
+ fn layout(
+ &self,
+ _: &mut Vt,
+ styles: StyleChain,
+ regions: Regions,
+ ) -> SourceResult<Fragment> {
+ let resolve = |axes: Axes<Rel<Length>>| {
+ axes.resolve(styles)
+ .zip(regions.base())
+ .map(|(l, b)| l.relative_to(b))
+ .to_point()
+ };
+
+ let vertices: Vec<PathVertex> = self.vertices();
+ let points: Vec<Point> = vertices.iter().map(|c| resolve(c.vertex())).collect();
+
+ let mut size = Size::zero();
+
+ // Only create a path if there are more than zero points.
+ let path = if points.len() > 0 {
+ // Construct a closed path given all points.
+ let mut path = Path::new();
+ path.move_to(points[0]);
+
+ let mut add_cubic = |from_point: Point,
+ to_point: Point,
+ from: PathVertex,
+ to: PathVertex| {
+ let from_control_point = resolve(from.control_point_from()) + from_point;
+ let to_control_point = resolve(to.control_point_to()) + 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 p1 = kurbo::Point::new(
+ from_control_point.x.to_raw(),
+ from_control_point.y.to_raw(),
+ );
+ let p2 = kurbo::Point::new(
+ to_control_point.x.to_raw(),
+ to_control_point.y.to_raw(),
+ );
+ let p3 = kurbo::Point::new(to_point.x.to_raw(), to_point.y.to_raw());
+ let extrema = CubicBez::new(p0, p1, p2, p3).bounding_box();
+ size.x.set_max(Abs::raw(extrema.x1));
+ size.y.set_max(Abs::raw(extrema.y1));
+ };
+
+ for (vertex_window, point_window) in
+ vertices.windows(2).zip(points.windows(2))
+ {
+ let from = vertex_window[0];
+ let to = vertex_window[1];
+ let from_point = point_window[0];
+ let to_point = point_window[1];
+
+ add_cubic(from_point, to_point, from, to);
+ }
+
+ if self.closed(styles) {
+ let from = *vertices.last().unwrap(); // We checked that we have at least one element.
+ let to = vertices[0];
+ let from_point = *points.last().unwrap();
+ let to_point = points[0];
+
+ add_cubic(from_point, to_point, from, to);
+ }
+
+ Some(path)
+ } else {
+ None
+ };
+
+ let mut frame = Frame::new(size);
+ if let Some(path) = path {
+ let fill = self.fill(styles);
+ 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))
+ }
+}
+
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub enum PathVertex {
+ Vertex(Axes<Rel<Length>>),
+ MirroredControlPoint(Axes<Rel<Length>>, Axes<Rel<Length>>),
+ AllControlPoints(Axes<Rel<Length>>, Axes<Rel<Length>>, Axes<Rel<Length>>),
+}
+
+impl PathVertex {
+ pub fn vertex(&self) -> Axes<Rel<Length>> {
+ match self {
+ Vertex(x) => *x,
+ MirroredControlPoint(x, _) => *x,
+ AllControlPoints(x, _, _) => *x,
+ }
+ }
+
+ pub fn control_point_from(&self) -> Axes<Rel<Length>> {
+ match self {
+ Vertex(_) => Axes::new(Rel::zero(), Rel::zero()),
+ MirroredControlPoint(_, a) => a.map(|x| -x),
+ AllControlPoints(_, _, b) => *b,
+ }
+ }
+
+ pub fn control_point_to(&self) -> Axes<Rel<Length>> {
+ match self {
+ Vertex(_) => Axes::new(Rel::zero(), Rel::zero()),
+ MirroredControlPoint(_, a) => *a,
+ AllControlPoints(_, a, _) => *a,
+ }
+ }
+}
+
+cast_from_value! {
+ PathVertex,
+ array: Array => {
+ let mut iter = array.into_iter();
+ match (iter.next(), iter.next(), iter.next(), iter.next()) {
+ (Some(a), None, None, None) => {
+ Vertex(a.cast()?)
+ },
+ (Some(a), Some(b), None, None) => {
+ if Axes::<Rel<Length>>::is(&a) {
+ MirroredControlPoint(a.cast()?, b.cast()?)
+ } else {
+ Vertex(Axes::new(a.cast()?, b.cast()?))
+ }
+ },
+ (Some(a), Some(b), Some(c), None) => {
+ AllControlPoints(a.cast()?, b.cast()?, c.cast()?)
+ },
+ _ => Err("path vertex must have 1, 2, or 3 points")?,
+ }
+ },
+}
+
+cast_to_value! {
+ v: PathVertex => {
+ match v {
+ PathVertex::Vertex(x) => {
+ Value::from(x)
+ },
+ PathVertex::MirroredControlPoint(x, c) => {
+ Value::Array(array![x, c])
+ },
+ PathVertex::AllControlPoints(x, c1, c2) => {
+ Value::Array(array![x, c1, c2])
+ },
+ }
+ }
+}
diff --git a/library/src/visualize/polygon.rs b/library/src/visualize/polygon.rs
index 642349fa..4b4adf7b 100644
--- a/library/src/visualize/polygon.rs
+++ b/library/src/visualize/polygon.rs
@@ -58,8 +58,7 @@ impl Layout for PolygonElem {
.collect();
let size = points.iter().fold(Point::zero(), |max, c| c.max(max)).to_size();
- let target = regions.expand.select(regions.size, size);
- let mut frame = Frame::new(target);
+ let mut frame = Frame::new(size);
// Only create a path if there are more than zero points.
if !points.is_empty() {