summaryrefslogtreecommitdiff
path: root/crates/typst-library/src/visualize
diff options
context:
space:
mode:
authorEmmanuel Lesueur <48604057+Emm54321@users.noreply.github.com>2024-12-18 16:58:57 +0100
committerGitHub <noreply@github.com>2024-12-18 15:58:57 +0000
commit257764181e52332a00079b9e3af03823fde1a15d (patch)
tree203378b303f1969e684b26dbefb77f7a5979ec9f /crates/typst-library/src/visualize
parent24c08a7ec0aaf87d6dc2e1a2c47b7beb6d5ad2a4 (diff)
New `curve` element that supersedes `path` (#5323)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
Diffstat (limited to 'crates/typst-library/src/visualize')
-rw-r--r--crates/typst-library/src/visualize/curve.rs532
-rw-r--r--crates/typst-library/src/visualize/image/mod.rs6
-rw-r--r--crates/typst-library/src/visualize/line.rs2
-rw-r--r--crates/typst-library/src/visualize/mod.rs3
-rw-r--r--crates/typst-library/src/visualize/path.rs146
-rw-r--r--crates/typst-library/src/visualize/polygon.rs2
-rw-r--r--crates/typst-library/src/visualize/shape.rs12
-rw-r--r--crates/typst-library/src/visualize/stroke.rs20
8 files changed, 563 insertions, 160 deletions
diff --git a/crates/typst-library/src/visualize/curve.rs b/crates/typst-library/src/visualize/curve.rs
new file mode 100644
index 00000000..607d92ab
--- /dev/null
+++ b/crates/typst-library/src/visualize/curve.rs
@@ -0,0 +1,532 @@
+use kurbo::ParamCurveExtrema;
+use typst_macros::{scope, Cast};
+use typst_utils::Numeric;
+
+use crate::diag::{bail, HintedStrResult, HintedString, SourceResult};
+use crate::engine::Engine;
+use crate::foundations::{
+ cast, elem, Content, NativeElement, Packed, Show, Smart, StyleChain,
+};
+use crate::layout::{Abs, Axes, BlockElem, Length, Point, Rel, Size};
+use crate::visualize::{FillRule, Paint, Stroke};
+
+/// A curve consisting of movements, lines, and Beziér segments.
+///
+/// At any point in time, there is a conceptual pen or cursor.
+/// - Move elements move the cursor without drawing.
+/// - Line/Quadratic/Cubic elements draw a segment from the cursor to a new
+/// position, potentially with control point for a Beziér curve.
+/// - Close elements draw a straight or smooth line back to the start of the
+/// curve or the latest preceding move segment.
+///
+/// For layout purposes, the bounding box of the curve is a tight rectangle
+/// containing all segments as well as the point `{(0pt, 0pt)}`.
+///
+/// Positions may be specified absolutely (i.e. relatively to `{(0pt, 0pt)}`),
+/// or relative to the current pen/cursor position, that is, the position where
+/// the previous segment ended.
+///
+/// Beziér curve control points can be skipped by passing `{none}` or
+/// automatically mirrored from the preceding segment by passing `{auto}`.
+///
+/// # Example
+/// ```example
+/// #curve(
+/// fill: blue.lighten(80%),
+/// stroke: blue,
+/// curve.move((0pt, 50pt)),
+/// curve.line((100pt, 50pt)),
+/// curve.cubic(none, (90pt, 0pt), (50pt, 0pt)),
+/// curve.close(),
+/// )
+/// ```
+#[elem(scope, Show)]
+pub struct CurveElem {
+ /// How to fill the curve.
+ ///
+ /// When setting a fill, the default stroke disappears. To create a
+ /// rectangle with both fill and stroke, you have to configure both.
+ pub fill: Option<Paint>,
+
+ /// The drawing rule used to fill the curve.
+ ///
+ /// ```example
+ /// // We use `.with` to get a new
+ /// // function that has the common
+ /// // arguments pre-applied.
+ /// #let star = curve.with(
+ /// fill: red,
+ /// curve.move((25pt, 0pt)),
+ /// curve.line((10pt, 50pt)),
+ /// curve.line((50pt, 20pt)),
+ /// curve.line((0pt, 20pt)),
+ /// curve.line((40pt, 50pt)),
+ /// curve.close(),
+ /// )
+ ///
+ /// #star(fill-rule: "non-zero")
+ /// #star(fill-rule: "even-odd")
+ /// ```
+ #[default]
+ pub fill_rule: FillRule,
+
+ /// How to [stroke] the curve. This can be:
+ ///
+ /// 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.
+ ///
+ /// ```example
+ /// #let down = curve.line((40pt, 40pt), relative: true)
+ /// #let up = curve.line((40pt, -40pt), relative: true)
+ ///
+ /// #curve(
+ /// stroke: 4pt + gradient.linear(red, blue),
+ /// down, up, down, up, down,
+ /// )
+ /// ```
+ #[resolve]
+ #[fold]
+ pub stroke: Smart<Option<Stroke>>,
+
+ /// The components of the curve, in the form of moves, line and Beziér
+ /// segment, and closes.
+ #[variadic]
+ pub components: Vec<CurveComponent>,
+}
+
+impl Show for Packed<CurveElem> {
+ fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> {
+ Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_curve)
+ .pack()
+ .spanned(self.span()))
+ }
+}
+
+#[scope]
+impl CurveElem {
+ #[elem]
+ type CurveMove;
+
+ #[elem]
+ type CurveLine;
+
+ #[elem]
+ type CurveQuad;
+
+ #[elem]
+ type CurveCubic;
+
+ #[elem]
+ type CurveClose;
+}
+
+/// A component used for curve creation.
+#[derive(Debug, Clone, PartialEq, Hash)]
+pub enum CurveComponent {
+ Move(Packed<CurveMove>),
+ Line(Packed<CurveLine>),
+ Quad(Packed<CurveQuad>),
+ Cubic(Packed<CurveCubic>),
+ Close(Packed<CurveClose>),
+}
+
+cast! {
+ CurveComponent,
+ self => match self {
+ Self::Move(element) => element.into_value(),
+ Self::Line(element) => element.into_value(),
+ Self::Quad(element) => element.into_value(),
+ Self::Cubic(element) => element.into_value(),
+ Self::Close(element) => element.into_value(),
+ },
+ v: Content => {
+ v.try_into()?
+ }
+}
+
+impl TryFrom<Content> for CurveComponent {
+ type Error = HintedString;
+
+ fn try_from(value: Content) -> HintedStrResult<Self> {
+ value
+ .into_packed::<CurveMove>()
+ .map(Self::Move)
+ .or_else(|value| value.into_packed::<CurveLine>().map(Self::Line))
+ .or_else(|value| value.into_packed::<CurveQuad>().map(Self::Quad))
+ .or_else(|value| value.into_packed::<CurveCubic>().map(Self::Cubic))
+ .or_else(|value| value.into_packed::<CurveClose>().map(Self::Close))
+ .or_else(|_| bail!("expecting a curve element"))
+ }
+}
+
+/// Starts a new curve component.
+///
+/// If no `curve.move` element is passed, the curve will start at
+/// `{(0pt, 0pt)}`.
+///
+/// ```example
+/// #curve(
+/// fill: blue.lighten(80%),
+/// fill-rule: "even-odd",
+/// stroke: blue,
+/// curve.line((50pt, 0pt)),
+/// curve.line((50pt, 50pt)),
+/// curve.line((0pt, 50pt)),
+/// curve.close(),
+/// curve.move((10pt, 10pt)),
+/// curve.line((40pt, 10pt)),
+/// curve.line((40pt, 40pt)),
+/// curve.line((10pt, 40pt)),
+/// curve.close(),
+/// )
+/// ```
+#[elem(name = "move", title = "Curve Move")]
+pub struct CurveMove {
+ /// The starting point for the new component.
+ #[required]
+ pub start: Axes<Rel<Length>>,
+
+ /// Whether the coordinates are relative to the previous point.
+ #[default(false)]
+ pub relative: bool,
+}
+
+/// Adds a straight line from the current point to a following one.
+///
+/// ```example
+/// #curve(
+/// stroke: blue,
+/// curve.line((50pt, 0pt)),
+/// curve.line((50pt, 50pt)),
+/// curve.line((100pt, 50pt)),
+/// curve.line((100pt, 0pt)),
+/// curve.line((150pt, 0pt)),
+/// )
+/// ```
+#[elem(name = "line", title = "Curve Line")]
+pub struct CurveLine {
+ /// The point at which the line shall end.
+ #[required]
+ pub end: Axes<Rel<Length>>,
+
+ /// Whether the coordinates are relative to the previous point.
+ ///
+ /// ```example
+ /// #curve(
+ /// stroke: blue,
+ /// curve.line((50pt, 0pt), relative: true),
+ /// curve.line((0pt, 50pt), relative: true),
+ /// curve.line((50pt, 0pt), relative: true),
+ /// curve.line((0pt, -50pt), relative: true),
+ /// curve.line((50pt, 0pt), relative: true),
+ /// )
+ /// ```
+ #[default(false)]
+ pub relative: bool,
+}
+
+/// Adds a quadratic Beziér curve segment from the last point to `end`, using
+/// `control` as the control point.
+///
+/// ```example
+/// // Function to illustrate where the control point is.
+/// #let mark((x, y)) = place(
+/// dx: x - 1pt, dy: y - 1pt,
+/// circle(fill: aqua, radius: 2pt),
+/// )
+///
+/// #mark((20pt, 20pt))
+///
+/// #curve(
+/// stroke: blue,
+/// curve.move((0pt, 100pt)),
+/// curve.quad((20pt, 20pt), (100pt, 0pt)),
+/// )
+/// ```
+#[elem(name = "quad", title = "Curve Quadratic Segment")]
+pub struct CurveQuad {
+ /// The control point of the quadratic Beziér curve.
+ ///
+ /// - If `{auto}` and this segment follows another quadratic Beziér curve,
+ /// the previous control point will be mirrored.
+ /// - If `{none}`, the control point defaults to `end`, and the curve will
+ /// be a straight line.
+ ///
+ /// ```example
+ /// #curve(
+ /// stroke: 2pt,
+ /// curve.quad((20pt, 40pt), (40pt, 40pt), relative: true),
+ /// curve.quad(auto, (40pt, -40pt), relative: true),
+ /// )
+ /// ```
+ #[required]
+ pub control: Smart<Option<Axes<Rel<Length>>>>,
+
+ /// The point at which the segment shall end.
+ #[required]
+ pub end: Axes<Rel<Length>>,
+
+ /// Whether the `control` and `end` coordinates are relative to the previous
+ /// point.
+ #[default(false)]
+ pub relative: bool,
+}
+
+/// Adds a cubic Beziér curve segment from the last point to `end`, using
+/// `control-start` and `control-end` as the control points.
+///
+/// ```example
+/// // Function to illustrate where the control points are.
+/// #let handle(start, end) = place(
+/// line(stroke: red, start: start, end: end)
+/// )
+///
+/// #handle((0pt, 80pt), (10pt, 20pt))
+/// #handle((90pt, 60pt), (100pt, 0pt))
+///
+/// #curve(
+/// stroke: blue,
+/// curve.move((0pt, 80pt)),
+/// curve.cubic((10pt, 20pt), (90pt, 60pt), (100pt, 0pt)),
+/// )
+/// ```
+#[elem(name = "cubic", title = "Curve Cubic Segment")]
+pub struct CurveCubic {
+ /// The control point going out from the start of the curve segment.
+ ///
+ /// - If `{auto}` and this element follows another `curve.cubic` element,
+ /// the last control point will be mirrored. In SVG terms, this makes
+ /// `curve.cubic` behave like the `S` operator instead of the `C` operator.
+ ///
+ /// - If `{none}`, the curve has no first control point, or equivalently,
+ /// the control point defaults to the curve's starting point.
+ ///
+ /// ```example
+ /// #curve(
+ /// stroke: blue,
+ /// curve.move((0pt, 50pt)),
+ /// // - No start control point
+ /// // - End control point at `(20pt, 0pt)`
+ /// // - End point at `(50pt, 0pt)`
+ /// curve.cubic(none, (20pt, 0pt), (50pt, 0pt)),
+ /// // - No start control point
+ /// // - No end control point
+ /// // - End point at `(50pt, 0pt)`
+ /// curve.cubic(none, none, (100pt, 50pt)),
+ /// )
+ ///
+ /// #curve(
+ /// stroke: blue,
+ /// curve.move((0pt, 50pt)),
+ /// curve.cubic(none, (20pt, 0pt), (50pt, 0pt)),
+ /// // Passing `auto` instead of `none` means the start control point
+ /// // mirrors the end control point of the previous curve. Mirror of
+ /// // `(20pt, 0pt)` w.r.t `(50pt, 0pt)` is `(80pt, 0pt)`.
+ /// curve.cubic(auto, none, (100pt, 50pt)),
+ /// )
+ ///
+ /// #curve(
+ /// stroke: blue,
+ /// curve.move((0pt, 50pt)),
+ /// curve.cubic(none, (20pt, 0pt), (50pt, 0pt)),
+ /// // `(80pt, 0pt)` is the same as `auto` in this case.
+ /// curve.cubic((80pt, 0pt), none, (100pt, 50pt)),
+ /// )
+ /// ```
+ #[required]
+ pub control_start: Option<Smart<Axes<Rel<Length>>>>,
+
+ /// The control point going into the end point of the curve segment.
+ ///
+ /// If set to `{none}`, the curve has no end control point, or equivalently,
+ /// the control point defaults to the curve's end point.
+ #[required]
+ pub control_end: Option<Axes<Rel<Length>>>,
+
+ /// The point at which the curve segment shall end.
+ #[required]
+ pub end: Axes<Rel<Length>>,
+
+ /// Whether the `control-start`, `control-end`, and `end` coordinates are
+ /// relative to the previous point.
+ #[default(false)]
+ pub relative: bool,
+}
+
+/// Closes the curve by adding a segment from the last point to the start of the
+/// curve (or the last preceding `curve.move` point).
+///
+/// ```example
+/// // We define a function to show the same shape with
+/// // both closing modes.
+/// #let shape(mode: "smooth") = curve(
+/// fill: blue.lighten(80%),
+/// stroke: blue,
+/// curve.move((0pt, 50pt)),
+/// curve.line((100pt, 50pt)),
+/// curve.cubic(auto, (90pt, 0pt), (50pt, 0pt)),
+/// curve.close(mode: mode),
+/// )
+///
+/// #shape(mode: "smooth")
+/// #shape(mode: "straight")
+/// ```
+#[elem(name = "close", title = "Curve Close")]
+pub struct CurveClose {
+ /// How to close the curve.
+ pub mode: CloseMode,
+}
+
+/// How to close a curve.
+#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Hash, Cast)]
+pub enum CloseMode {
+ /// Closes the curve with a smooth segment that takes into account the
+ /// control point opposite the start point.
+ #[default]
+ Smooth,
+ /// Closes the curve with a straight line.
+ Straight,
+}
+
+/// A curve consisting of movements, lines, and Beziér segments.
+#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
+pub struct Curve(pub Vec<CurveItem>);
+
+/// An item in a curve.
+#[derive(Debug, Clone, Eq, PartialEq, Hash)]
+pub enum CurveItem {
+ Move(Point),
+ Line(Point),
+ Cubic(Point, Point, Point),
+ Close,
+}
+
+impl Curve {
+ /// Creates an empty curve.
+ pub const fn new() -> Self {
+ Self(vec![])
+ }
+
+ /// Creates a curve that describes a rectangle.
+ pub fn rect(size: Size) -> Self {
+ let z = Abs::zero();
+ let point = Point::new;
+ let mut curve = Self::new();
+ curve.move_(point(z, z));
+ curve.line(point(size.x, z));
+ curve.line(point(size.x, size.y));
+ curve.line(point(z, size.y));
+ curve.close();
+ curve
+ }
+
+ /// Creates a curve that describes an axis-aligned ellipse.
+ pub fn ellipse(size: Size) -> Self {
+ // https://stackoverflow.com/a/2007782
+ let z = Abs::zero();
+ let rx = size.x / 2.0;
+ let ry = size.y / 2.0;
+ let m = 0.551784;
+ let mx = m * rx;
+ let my = m * ry;
+ let point = |x, y| Point::new(x + rx, y + ry);
+
+ let mut curve = Curve::new();
+ curve.move_(point(-rx, z));
+ curve.cubic(point(-rx, -my), point(-mx, -ry), point(z, -ry));
+ curve.cubic(point(mx, -ry), point(rx, -my), point(rx, z));
+ curve.cubic(point(rx, my), point(mx, ry), point(z, ry));
+ curve.cubic(point(-mx, ry), point(-rx, my), point(-rx, z));
+ curve
+ }
+
+ /// Push a [`Move`](CurveItem::Move) item.
+ pub fn move_(&mut self, p: Point) {
+ self.0.push(CurveItem::Move(p));
+ }
+
+ /// Push a [`Line`](CurveItem::Line) item.
+ pub fn line(&mut self, p: Point) {
+ self.0.push(CurveItem::Line(p));
+ }
+
+ /// Push a [`Cubic`](CurveItem::Cubic) item.
+ pub fn cubic(&mut self, p1: Point, p2: Point, p3: Point) {
+ self.0.push(CurveItem::Cubic(p1, p2, p3));
+ }
+
+ /// Push a [`Close`](CurveItem::Close) item.
+ pub fn close(&mut self) {
+ self.0.push(CurveItem::Close);
+ }
+
+ /// Check if the curve is empty.
+ pub fn is_empty(&self) -> bool {
+ self.0.is_empty()
+ }
+
+ /// Translate all points in this curve by the given offset.
+ pub fn translate(&mut self, offset: Point) {
+ if offset.is_zero() {
+ return;
+ }
+ for item in self.0.iter_mut() {
+ match item {
+ CurveItem::Move(p) => *p += offset,
+ CurveItem::Line(p) => *p += offset,
+ CurveItem::Cubic(p1, p2, p3) => {
+ *p1 += offset;
+ *p2 += offset;
+ *p3 += offset;
+ }
+ CurveItem::Close => (),
+ }
+ }
+ }
+
+ /// Computes the size of the bounding box of this curve.
+ pub fn bbox_size(&self) -> Size {
+ let mut min_x = Abs::inf();
+ let mut min_y = Abs::inf();
+ let mut max_x = -Abs::inf();
+ let mut max_y = -Abs::inf();
+
+ let mut cursor = Point::zero();
+ for item in self.0.iter() {
+ match item {
+ CurveItem::Move(to) => {
+ min_x = min_x.min(cursor.x);
+ min_y = min_y.min(cursor.y);
+ max_x = max_x.max(cursor.x);
+ max_y = max_y.max(cursor.y);
+ cursor = *to;
+ }
+ CurveItem::Line(to) => {
+ min_x = min_x.min(cursor.x);
+ min_y = min_y.min(cursor.y);
+ max_x = max_x.max(cursor.x);
+ max_y = max_y.max(cursor.y);
+ cursor = *to;
+ }
+ CurveItem::Cubic(c0, c1, end) => {
+ let cubic = kurbo::CubicBez::new(
+ kurbo::Point::new(cursor.x.to_pt(), cursor.y.to_pt()),
+ kurbo::Point::new(c0.x.to_pt(), c0.y.to_pt()),
+ kurbo::Point::new(c1.x.to_pt(), c1.y.to_pt()),
+ kurbo::Point::new(end.x.to_pt(), end.y.to_pt()),
+ );
+
+ let bbox = cubic.bounding_box();
+ min_x = min_x.min(Abs::pt(bbox.x0)).min(Abs::pt(bbox.x1));
+ min_y = min_y.min(Abs::pt(bbox.y0)).min(Abs::pt(bbox.y1));
+ max_x = max_x.max(Abs::pt(bbox.x0)).max(Abs::pt(bbox.x1));
+ max_y = max_y.max(Abs::pt(bbox.y0)).max(Abs::pt(bbox.y1));
+ cursor = *end;
+ }
+ CurveItem::Close => (),
+ }
+ }
+
+ Size::new(max_x - min_x, max_y - min_y)
+ }
+}
diff --git a/crates/typst-library/src/visualize/image/mod.rs b/crates/typst-library/src/visualize/image/mod.rs
index fddb4acb..452bb65c 100644
--- a/crates/typst-library/src/visualize/image/mod.rs
+++ b/crates/typst-library/src/visualize/image/mod.rs
@@ -95,9 +95,9 @@ pub struct ImageElem {
#[default(ImageFit::Cover)]
pub fit: ImageFit,
- /// Whether text in SVG images should be converted into paths before
- /// embedding. This will result in the text becoming unselectable in
- /// the output.
+ /// Whether text in SVG images should be converted into curves before
+ /// embedding. This will result in the text becoming unselectable in the
+ /// output.
#[default(false)]
pub flatten_text: bool,
}
diff --git a/crates/typst-library/src/visualize/line.rs b/crates/typst-library/src/visualize/line.rs
index d9ddab84..689321f1 100644
--- a/crates/typst-library/src/visualize/line.rs
+++ b/crates/typst-library/src/visualize/line.rs
@@ -25,7 +25,7 @@ pub struct LineElem {
#[resolve]
pub start: Axes<Rel<Length>>,
- /// The offset from `start` where the line ends.
+ /// The point where the line ends.
#[resolve]
pub end: Option<Axes<Rel<Length>>>,
diff --git a/crates/typst-library/src/visualize/mod.rs b/crates/typst-library/src/visualize/mod.rs
index 61c56a61..43119149 100644
--- a/crates/typst-library/src/visualize/mod.rs
+++ b/crates/typst-library/src/visualize/mod.rs
@@ -1,6 +1,7 @@
//! Drawing and visualization.
mod color;
+mod curve;
mod gradient;
mod image;
mod line;
@@ -12,6 +13,7 @@ mod stroke;
mod tiling;
pub use self::color::*;
+pub use self::curve::*;
pub use self::gradient::*;
pub use self::image::*;
pub use self::line::*;
@@ -46,6 +48,7 @@ pub(super) fn define(global: &mut Scope) {
global.define_elem::<EllipseElem>();
global.define_elem::<CircleElem>();
global.define_elem::<PolygonElem>();
+ global.define_elem::<CurveElem>();
global.define_elem::<PathElem>();
// Compatibility.
diff --git a/crates/typst-library/src/visualize/path.rs b/crates/typst-library/src/visualize/path.rs
index f592a712..6aacb319 100644
--- a/crates/typst-library/src/visualize/path.rs
+++ b/crates/typst-library/src/visualize/path.rs
@@ -1,6 +1,3 @@
-use kurbo::ParamCurveExtrema;
-use typst_utils::Numeric;
-
use self::PathVertex::{AllControlPoints, MirroredControlPoint, Vertex};
use crate::diag::{bail, SourceResult};
use crate::engine::Engine;
@@ -8,7 +5,7 @@ use crate::foundations::{
array, cast, elem, Array, Content, NativeElement, Packed, Reflect, Show, Smart,
StyleChain,
};
-use crate::layout::{Abs, Axes, BlockElem, Length, Point, Rel, Size};
+use crate::layout::{Axes, BlockElem, Length, Rel};
use crate::visualize::{FillRule, Paint, Stroke};
/// A path through a list of points, connected by Bezier curves.
@@ -24,6 +21,9 @@ use crate::visualize::{FillRule, Paint, Stroke};
/// ((50%, 0pt), (40pt, 0pt)),
/// )
/// ```
+///
+/// # Deprecation
+/// This element is deprecated. The [`curve`] element should be used instead.
#[elem(Show)]
pub struct PathElem {
/// How to fill the path.
@@ -156,141 +156,3 @@ cast! {
}
},
}
-
-/// A bezier path.
-#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
-pub struct Path(pub Vec<PathItem>);
-
-/// An item in a bezier path.
-#[derive(Debug, Clone, Eq, PartialEq, Hash)]
-pub enum PathItem {
- MoveTo(Point),
- LineTo(Point),
- CubicTo(Point, Point, Point),
- ClosePath,
-}
-
-impl Path {
- /// Create an empty path.
- pub const fn new() -> Self {
- Self(vec![])
- }
-
- /// Create a path that describes a rectangle.
- pub fn rect(size: Size) -> Self {
- let z = Abs::zero();
- let point = Point::new;
- let mut path = Self::new();
- path.move_to(point(z, z));
- path.line_to(point(size.x, z));
- path.line_to(point(size.x, size.y));
- path.line_to(point(z, size.y));
- path.close_path();
- path
- }
-
- /// Create a path that describes an axis-aligned ellipse.
- pub fn ellipse(size: Size) -> Self {
- // https://stackoverflow.com/a/2007782
- let z = Abs::zero();
- let rx = size.x / 2.0;
- let ry = size.y / 2.0;
- let m = 0.551784;
- let mx = m * rx;
- let my = m * ry;
- let point = |x, y| Point::new(x + rx, y + ry);
-
- let mut path = Path::new();
- path.move_to(point(-rx, z));
- path.cubic_to(point(-rx, -my), point(-mx, -ry), point(z, -ry));
- path.cubic_to(point(mx, -ry), point(rx, -my), point(rx, z));
- path.cubic_to(point(rx, my), point(mx, ry), point(z, ry));
- path.cubic_to(point(-mx, ry), point(-rx, my), point(-rx, z));
- path
- }
-
- /// Push a [`MoveTo`](PathItem::MoveTo) item.
- pub fn move_to(&mut self, p: Point) {
- self.0.push(PathItem::MoveTo(p));
- }
-
- /// Push a [`LineTo`](PathItem::LineTo) item.
- pub fn line_to(&mut self, p: Point) {
- self.0.push(PathItem::LineTo(p));
- }
-
- /// Push a [`CubicTo`](PathItem::CubicTo) item.
- pub fn cubic_to(&mut self, p1: Point, p2: Point, p3: Point) {
- self.0.push(PathItem::CubicTo(p1, p2, p3));
- }
-
- /// Push a [`ClosePath`](PathItem::ClosePath) item.
- pub fn close_path(&mut self) {
- self.0.push(PathItem::ClosePath);
- }
-
- /// Translate all points in this path by the given offset.
- pub fn translate(&mut self, offset: Point) {
- if offset.is_zero() {
- return;
- }
- for item in self.0.iter_mut() {
- match item {
- PathItem::MoveTo(p) => *p += offset,
- PathItem::LineTo(p) => *p += offset,
- PathItem::CubicTo(p1, p2, p3) => {
- *p1 += offset;
- *p2 += offset;
- *p3 += offset;
- }
- PathItem::ClosePath => (),
- }
- }
- }
-
- /// Computes the size of bounding box of this path.
- pub fn bbox_size(&self) -> Size {
- let mut min_x = Abs::inf();
- let mut min_y = Abs::inf();
- let mut max_x = -Abs::inf();
- let mut max_y = -Abs::inf();
-
- let mut cursor = Point::zero();
- for item in self.0.iter() {
- match item {
- PathItem::MoveTo(to) => {
- min_x = min_x.min(cursor.x);
- min_y = min_y.min(cursor.y);
- max_x = max_x.max(cursor.x);
- max_y = max_y.max(cursor.y);
- cursor = *to;
- }
- PathItem::LineTo(to) => {
- min_x = min_x.min(cursor.x);
- min_y = min_y.min(cursor.y);
- max_x = max_x.max(cursor.x);
- max_y = max_y.max(cursor.y);
- cursor = *to;
- }
- PathItem::CubicTo(c0, c1, end) => {
- let cubic = kurbo::CubicBez::new(
- kurbo::Point::new(cursor.x.to_pt(), cursor.y.to_pt()),
- kurbo::Point::new(c0.x.to_pt(), c0.y.to_pt()),
- kurbo::Point::new(c1.x.to_pt(), c1.y.to_pt()),
- kurbo::Point::new(end.x.to_pt(), end.y.to_pt()),
- );
-
- let bbox = cubic.bounding_box();
- min_x = min_x.min(Abs::pt(bbox.x0)).min(Abs::pt(bbox.x1));
- min_y = min_y.min(Abs::pt(bbox.y0)).min(Abs::pt(bbox.y1));
- max_x = max_x.max(Abs::pt(bbox.x0)).max(Abs::pt(bbox.x1));
- max_y = max_y.max(Abs::pt(bbox.y0)).max(Abs::pt(bbox.y1));
- cursor = *end;
- }
- PathItem::ClosePath => (),
- }
- }
-
- Size::new(max_x - min_x, max_y - min_y)
- }
-}
diff --git a/crates/typst-library/src/visualize/polygon.rs b/crates/typst-library/src/visualize/polygon.rs
index 33e4fd32..465f2c1a 100644
--- a/crates/typst-library/src/visualize/polygon.rs
+++ b/crates/typst-library/src/visualize/polygon.rs
@@ -35,7 +35,7 @@ pub struct PolygonElem {
/// The drawing rule used to fill the polygon.
///
- /// See the [path documentation]($path.fill-rule) for an example.
+ /// See the [curve documentation]($curve.fill-rule) for an example.
#[default]
pub fill_rule: FillRule,
diff --git a/crates/typst-library/src/visualize/shape.rs b/crates/typst-library/src/visualize/shape.rs
index 01e316a8..3c62b210 100644
--- a/crates/typst-library/src/visualize/shape.rs
+++ b/crates/typst-library/src/visualize/shape.rs
@@ -4,7 +4,7 @@ use crate::foundations::{
elem, Cast, Content, NativeElement, Packed, Show, Smart, StyleChain,
};
use crate::layout::{Abs, BlockElem, Corners, Length, Point, Rel, Sides, Size, Sizing};
-use crate::visualize::{FixedStroke, Paint, Path, Stroke};
+use crate::visualize::{Curve, FixedStroke, Paint, Stroke};
/// A rectangle with optional content.
///
@@ -395,7 +395,7 @@ pub struct Shape {
pub stroke: Option<FixedStroke>,
}
-/// A path filling rule.
+/// A fill rule for curve drawing.
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Cast)]
pub enum FillRule {
/// Specifies that "inside" is computed by a non-zero sum of signed edge crossings.
@@ -412,8 +412,8 @@ pub enum Geometry {
Line(Point),
/// A rectangle with its origin in the topleft corner.
Rect(Size),
- /// A bezier path.
- Path(Path),
+ /// A curve consisting of movements, lines, and Bezier segments.
+ Curve(Curve),
}
impl Geometry {
@@ -441,8 +441,8 @@ impl Geometry {
pub fn bbox_size(&self) -> Size {
match self {
Self::Line(line) => Size::new(line.x, line.y),
- Self::Rect(s) => *s,
- Self::Path(p) => p.bbox_size(),
+ Self::Rect(rect) => *rect,
+ Self::Curve(curve) => curve.bbox_size(),
}
}
}
diff --git a/crates/typst-library/src/visualize/stroke.rs b/crates/typst-library/src/visualize/stroke.rs
index 2ab493a5..97a1535d 100644
--- a/crates/typst-library/src/visualize/stroke.rs
+++ b/crates/typst-library/src/visualize/stroke.rs
@@ -170,14 +170,20 @@ impl Stroke {
/// If set to `{auto}`, the value is inherited, defaulting to `{4.0}`.
///
/// ```example
- /// #let points = ((15pt, 0pt), (0pt, 30pt), (30pt, 30pt), (10pt, 20pt))
- /// #set path(stroke: 6pt + blue)
+ /// #let items = (
+ /// curve.move((15pt, 0pt)),
+ /// curve.line((0pt, 30pt)),
+ /// curve.line((30pt, 30pt)),
+ /// curve.line((10pt, 20pt)),
+ /// )
+ ///
+ /// #set curve(stroke: 6pt + blue)
/// #stack(
- /// dir: ltr,
- /// spacing: 1cm,
- /// path(stroke: (miter-limit: 1), ..points),
- /// path(stroke: (miter-limit: 4), ..points),
- /// path(stroke: (miter-limit: 5), ..points),
+ /// dir: ltr,
+ /// spacing: 1cm,
+ /// curve(stroke: (miter-limit: 1), ..items),
+ /// curve(stroke: (miter-limit: 4), ..items),
+ /// curve(stroke: (miter-limit: 5), ..items),
/// )
/// ```
#[external]