diff options
| author | Laurenz <laurmaedje@gmail.com> | 2023-07-02 19:59:52 +0200 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2023-07-02 20:07:43 +0200 |
| commit | ebfdb1dafa430786db10dad2ef7d5467c1bdbed1 (patch) | |
| tree | 2bbc24ddb4124c4bb14dec0e536129d4de37b056 /crates/typst-library/src/visualize/path.rs | |
| parent | 3ab19185093d7709f824b95b979060ce125389d8 (diff) | |
Move everything into `crates/` directory
Diffstat (limited to 'crates/typst-library/src/visualize/path.rs')
| -rw-r--r-- | crates/typst-library/src/visualize/path.rs | 211 |
1 files changed, 211 insertions, 0 deletions
diff --git a/crates/typst-library/src/visualize/path.rs b/crates/typst-library/src/visualize/path.rs new file mode 100644 index 00000000..d78abce1 --- /dev/null +++ b/crates/typst-library/src/visualize/path.rs @@ -0,0 +1,211 @@ +use kurbo::{CubicBez, ParamCurveExtrema}; +use typst::eval::Reflect; + +use crate::prelude::*; + +use PathVertex::{AllControlPoints, MirroredControlPoint, Vertex}; + +/// A path through a list of points, connected by Bezier curves. +/// +/// ## Example { #example } +/// ```example +/// #path( +/// fill: blue.lighten(80%), +/// stroke: blue, +/// closed: true, +/// (0pt, 50pt), +/// (100%, 50pt), +/// ((50%, 0pt), (40pt, 0pt)), +/// ) +/// ``` +/// +/// Display: Path +/// Category: visualize +#[element(Layout)] +pub struct PathElem { + /// How to fill the path. 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 path. This can be: + /// + /// See the [line's documentation]($func/line.stroke) for more details. Can + /// be set to `{none}` to disable the stroke or to `{auto}` for a stroke of + /// `{1pt}` black if and if only if no fill is given. + #[resolve] + #[fold] + pub stroke: Smart<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. + /// + /// Each vertex can be defined in 3 ways: + /// + /// - 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 + /// being the control point. The control point is expressed relative to + /// the vertex and is mirrored to get the second control point. The given + /// control point is the one that affects the curve coming _into_ this + /// vertex (even for the first point). The mirrored control point affects + /// 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] + pub vertices: Vec<PathVertex>, +} + +impl Layout for PathElem { + #[tracing::instrument(name = "PathElem::layout", skip_all)] + 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(); + if points.is_empty() { + return Ok(Fragment::frame(Frame::new(size))); + } + + // Only create a path if there are more than zero points. + // 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); + path.close_path(); + } + + // 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), + }; + + let mut frame = Frame::new(size); + 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! { + PathVertex, + self => match self { + Vertex(x) => x.into_value(), + MirroredControlPoint(x, c) => array![x, c].into_value(), + AllControlPoints(x, c1, c2) => array![x, c1, c2].into_value(), + }, + 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>>::castable(&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()?) + }, + _ => bail!("path vertex must have 1, 2, or 3 points"), + } + }, +} |
