summaryrefslogtreecommitdiff
path: root/crates/typst-library/src/visualize
diff options
context:
space:
mode:
Diffstat (limited to 'crates/typst-library/src/visualize')
-rw-r--r--crates/typst-library/src/visualize/image.rs271
-rw-r--r--crates/typst-library/src/visualize/line.rs86
-rw-r--r--crates/typst-library/src/visualize/mod.rs31
-rw-r--r--crates/typst-library/src/visualize/path.rs208
-rw-r--r--crates/typst-library/src/visualize/polygon.rs164
-rw-r--r--crates/typst-library/src/visualize/shape.rs547
6 files changed, 0 insertions, 1307 deletions
diff --git a/crates/typst-library/src/visualize/image.rs b/crates/typst-library/src/visualize/image.rs
deleted file mode 100644
index 0996ae7c..00000000
--- a/crates/typst-library/src/visualize/image.rs
+++ /dev/null
@@ -1,271 +0,0 @@
-use std::ffi::OsStr;
-
-use typst::image::{Image, ImageFormat, RasterFormat, VectorFormat};
-use typst::util::option_eq;
-
-use crate::compute::Readable;
-use crate::meta::Figurable;
-use crate::prelude::*;
-use crate::text::families;
-
-/// A raster or vector graphic.
-///
-/// Supported formats are PNG, JPEG, GIF and SVG.
-///
-/// _Note:_ Work on SVG export is ongoing and there might be visual inaccuracies
-/// in the resulting PDF. Make sure to double-check embedded SVG images. If you
-/// have an issue, also feel free to report it on [GitHub][gh-svg].
-///
-/// # Example
-/// ```example
-/// #figure(
-/// image("molecular.jpg", width: 80%),
-/// caption: [
-/// A step in the molecular testing
-/// pipeline of our lab.
-/// ],
-/// )
-/// ```
-///
-/// [gh-svg]: https://github.com/typst/typst/issues?q=is%3Aopen+is%3Aissue+label%3Asvg
-#[elem(scope, Layout, LocalName, Figurable)]
-pub struct ImageElem {
- /// Path to an image file.
- #[required]
- #[parse(
- let Spanned { v: path, span } =
- args.expect::<Spanned<EcoString>>("path to image file")?;
- let id = vm.resolve_path(&path).at(span)?;
- let data = vm.world().file(id).at(span)?;
- path
- )]
- #[borrowed]
- pub path: EcoString,
-
- /// The raw file data.
- #[internal]
- #[required]
- #[parse(Readable::Bytes(data))]
- pub data: Readable,
-
- /// The image's format. Detected automatically by default.
- pub format: Smart<ImageFormat>,
-
- /// The width of the image.
- pub width: Smart<Rel<Length>>,
-
- /// The height of the image.
- pub height: Smart<Rel<Length>>,
-
- /// A text describing the image.
- pub alt: Option<EcoString>,
-
- /// How the image should adjust itself to a given area.
- #[default(ImageFit::Cover)]
- pub fit: ImageFit,
-}
-
-#[scope]
-impl ImageElem {
- /// Decode a raster or vector graphic from bytes or a string.
- ///
- /// ```example
- /// #let original = read("diagram.svg")
- /// #let changed = original.replace(
- /// "#2B80FF", // blue
- /// green.to-hex(),
- /// )
- ///
- /// #image.decode(original)
- /// #image.decode(changed)
- /// ```
- #[func(title = "Decode Image")]
- pub fn decode(
- /// The data to decode as an image. Can be a string for SVGs.
- data: Readable,
- /// The image's format. Detected automatically by default.
- #[named]
- format: Option<Smart<ImageFormat>>,
- /// The width of the image.
- #[named]
- width: Option<Smart<Rel<Length>>>,
- /// The height of the image.
- #[named]
- height: Option<Smart<Rel<Length>>>,
- /// A text describing the image.
- #[named]
- alt: Option<Option<EcoString>>,
- /// How the image should adjust itself to a given area.
- #[named]
- fit: Option<ImageFit>,
- ) -> StrResult<Content> {
- let mut elem = ImageElem::new(EcoString::new(), data);
- if let Some(format) = format {
- elem.push_format(format);
- }
- if let Some(width) = width {
- elem.push_width(width);
- }
- if let Some(height) = height {
- elem.push_height(height);
- }
- if let Some(alt) = alt {
- elem.push_alt(alt);
- }
- if let Some(fit) = fit {
- elem.push_fit(fit);
- }
- Ok(elem.pack())
- }
-}
-
-impl Layout for ImageElem {
- #[tracing::instrument(name = "ImageElem::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- // Take the format that was explicitly defined, or parse the extension,
- // or try to detect the format.
- let data = self.data();
- let format = match self.format(styles) {
- Smart::Custom(v) => v,
- Smart::Auto => {
- let ext = std::path::Path::new(self.path().as_str())
- .extension()
- .and_then(OsStr::to_str)
- .unwrap_or_default()
- .to_lowercase();
-
- match ext.as_str() {
- "png" => ImageFormat::Raster(RasterFormat::Png),
- "jpg" | "jpeg" => ImageFormat::Raster(RasterFormat::Jpg),
- "gif" => ImageFormat::Raster(RasterFormat::Gif),
- "svg" | "svgz" => ImageFormat::Vector(VectorFormat::Svg),
- _ => match &data {
- Readable::Str(_) => ImageFormat::Vector(VectorFormat::Svg),
- Readable::Bytes(bytes) => match RasterFormat::detect(bytes) {
- Some(f) => ImageFormat::Raster(f),
- None => bail!(self.span(), "unknown image format"),
- },
- },
- }
- }
- };
-
- let image = Image::with_fonts(
- data.clone().into(),
- format,
- self.alt(styles),
- vt.world,
- &families(styles).map(|s| s.into()).collect::<Vec<_>>(),
- )
- .at(self.span())?;
-
- let sizing = Axes::new(self.width(styles), self.height(styles));
- let region = sizing
- .zip_map(regions.base(), |s, r| s.map(|v| v.resolve(styles).relative_to(r)))
- .unwrap_or(regions.base());
-
- let expand = sizing.as_ref().map(Smart::is_custom) | regions.expand;
- let region_ratio = region.x / region.y;
-
- // Find out whether the image is wider or taller than the target size.
- let pxw = image.width() as f64;
- let pxh = image.height() as f64;
- let px_ratio = pxw / pxh;
- let wide = px_ratio > region_ratio;
-
- // The space into which the image will be placed according to its fit.
- let target = if expand.x && expand.y {
- region
- } else if expand.x || (!expand.y && wide && region.x.is_finite()) {
- Size::new(region.x, region.y.min(region.x.safe_div(px_ratio)))
- } else if region.y.is_finite() {
- Size::new(region.x.min(region.y * px_ratio), region.y)
- } else {
- Size::new(Abs::pt(pxw), Abs::pt(pxh))
- };
-
- // Compute the actual size of the fitted image.
- let fit = self.fit(styles);
- let fitted = match fit {
- ImageFit::Cover | ImageFit::Contain => {
- if wide == (fit == ImageFit::Contain) {
- Size::new(target.x, target.x / px_ratio)
- } else {
- Size::new(target.y * px_ratio, target.y)
- }
- }
- ImageFit::Stretch => target,
- };
-
- // First, place the image in a frame of exactly its size and then resize
- // the frame to the target size, center aligning the image in the
- // process.
- let mut frame = Frame::soft(fitted);
- frame.push(Point::zero(), FrameItem::Image(image, fitted, self.span()));
- frame.resize(target, Axes::splat(FixedAlign::Center));
-
- // Create a clipping group if only part of the image should be visible.
- if fit == ImageFit::Cover && !target.fits(fitted) {
- frame.clip(Path::rect(frame.size()));
- }
-
- // Apply metadata.
- frame.meta(styles, false);
-
- Ok(Fragment::frame(frame))
- }
-}
-
-impl LocalName for ImageElem {
- fn local_name(lang: Lang, region: Option<Region>) -> &'static str {
- match lang {
- Lang::ALBANIAN => "Figurë",
- Lang::ARABIC => "شكل",
- Lang::BOKMÅL => "Figur",
- Lang::CHINESE if option_eq(region, "TW") => "圖",
- Lang::CHINESE => "图",
- Lang::CZECH => "Obrázek",
- Lang::DANISH => "Figur",
- Lang::DUTCH => "Figuur",
- Lang::FILIPINO => "Pigura",
- Lang::FINNISH => "Kuva",
- Lang::FRENCH => "Fig.",
- Lang::GERMAN => "Abbildung",
- Lang::GREEK => "Σχήμα",
- Lang::HUNGARIAN => "Ábra",
- Lang::ITALIAN => "Figura",
- Lang::NYNORSK => "Figur",
- Lang::POLISH => "Rysunek",
- Lang::PORTUGUESE => "Figura",
- Lang::ROMANIAN => "Figura",
- Lang::RUSSIAN => "Рис.",
- Lang::SLOVENIAN => "Slika",
- Lang::SPANISH => "Figura",
- Lang::SWEDISH => "Figur",
- Lang::TURKISH => "Şekil",
- Lang::UKRAINIAN => "Рисунок",
- Lang::VIETNAMESE => "Hình",
- Lang::JAPANESE => "図",
- Lang::ENGLISH | _ => "Figure",
- }
- }
-}
-
-impl Figurable for ImageElem {}
-
-/// How an image should adjust itself to a given area.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
-pub enum ImageFit {
- /// The image should completely cover the area. This is the default.
- Cover,
- /// The image should be fully contained in the area.
- Contain,
- /// The image should be stretched so that it exactly fills the area, even if
- /// this means that the image will be distorted.
- Stretch,
-}
diff --git a/crates/typst-library/src/visualize/line.rs b/crates/typst-library/src/visualize/line.rs
deleted file mode 100644
index da497cf4..00000000
--- a/crates/typst-library/src/visualize/line.rs
+++ /dev/null
@@ -1,86 +0,0 @@
-use crate::prelude::*;
-
-/// A line from one point to another.
-///
-/// # Example
-/// ```example
-/// #set page(height: 100pt)
-///
-/// #line(length: 100%)
-/// #line(end: (50%, 50%))
-/// #line(
-/// length: 4cm,
-/// stroke: 2pt + maroon,
-/// )
-/// ```
-#[elem(Layout)]
-pub struct LineElem {
- /// The start point of the line.
- ///
- /// Must be an array of exactly two relative lengths.
- #[resolve]
- pub start: Axes<Rel<Length>>,
-
- /// The offset from `start` where the line ends.
- #[resolve]
- pub end: Option<Axes<Rel<Length>>>,
-
- /// The line's length. This is only respected if `end` is `none`.
- #[resolve]
- #[default(Abs::pt(30.0).into())]
- pub length: Rel<Length>,
-
- /// The angle at which the line points away from the origin. This is only
- /// respected if `end` is `none`.
- pub angle: Angle,
-
- /// How to [stroke]($stroke) the line.
- ///
- /// ```example
- /// #set line(length: 100%)
- /// #stack(
- /// spacing: 1em,
- /// line(stroke: 2pt + red),
- /// line(stroke: (paint: blue, thickness: 4pt, cap: "round")),
- /// line(stroke: (paint: blue, thickness: 1pt, dash: "dashed")),
- /// line(stroke: (paint: blue, thickness: 1pt, dash: ("dot", 2pt, 4pt, 2pt))),
- /// )
- /// ```
- #[resolve]
- #[fold]
- pub stroke: Stroke,
-}
-
-impl Layout for LineElem {
- #[tracing::instrument(name = "LineElem::layout", skip_all)]
- fn layout(
- &self,
- _: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- let resolve =
- |axes: Axes<Rel<Abs>>| axes.zip_map(regions.base(), Rel::relative_to);
- let start = resolve(self.start(styles));
- let delta =
- self.end(styles).map(|end| resolve(end) - start).unwrap_or_else(|| {
- let length = self.length(styles);
- let angle = self.angle(styles);
- let x = angle.cos() * length;
- let y = angle.sin() * length;
- resolve(Axes::new(x, y))
- });
-
- let stroke = self.stroke(styles).unwrap_or_default();
- let size = start.max(start + delta).max(Size::zero());
- let target = regions.expand.select(regions.size, size);
-
- if !target.is_finite() {
- bail!(error!(self.span(), "cannot create line with infinite length"));
- }
- let mut frame = Frame::soft(target);
- let shape = Geometry::Line(delta.to_point()).stroked(stroke);
- frame.push(start.to_point(), FrameItem::Shape(shape, self.span()));
- Ok(Fragment::frame(frame))
- }
-}
diff --git a/crates/typst-library/src/visualize/mod.rs b/crates/typst-library/src/visualize/mod.rs
deleted file mode 100644
index e39d50bc..00000000
--- a/crates/typst-library/src/visualize/mod.rs
+++ /dev/null
@@ -1,31 +0,0 @@
-//! Drawing and visualization.
-
-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::*;
-
-use crate::prelude::*;
-
-/// Hook up all visualize definitions.
-pub(super) fn define(global: &mut Scope) {
- global.category("visualize");
- global.define_type::<Color>();
- global.define_type::<Gradient>();
- global.define_type::<Stroke>();
- global.define_elem::<ImageElem>();
- global.define_elem::<LineElem>();
- global.define_elem::<RectElem>();
- global.define_elem::<SquareElem>();
- global.define_elem::<EllipseElem>();
- global.define_elem::<CircleElem>();
- global.define_elem::<PolygonElem>();
- global.define_elem::<PathElem>();
-}
diff --git a/crates/typst-library/src/visualize/path.rs b/crates/typst-library/src/visualize/path.rs
deleted file mode 100644
index 43b11526..00000000
--- a/crates/typst-library/src/visualize/path.rs
+++ /dev/null
@@ -1,208 +0,0 @@
-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
-/// #path(
-/// fill: blue.lighten(80%),
-/// stroke: blue,
-/// closed: true,
-/// (0pt, 50pt),
-/// (100%, 50pt),
-/// ((50%, 0pt), (40pt, 0pt)),
-/// )
-/// ```
-#[elem(Layout)]
-pub struct PathElem {
- /// How to fill the path.
- ///
- /// When setting a fill, the default stroke disappears. To create a
- /// rectangle with both fill and stroke, you have to configure both.
- ///
- /// 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]($stroke) the path. 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.
- #[resolve]
- #[fold]
- pub stroke: Smart<Option<Stroke>>,
-
- /// 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`]($line) or
- /// [`polygon`]($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_map(regions.base(), Rel::relative_to)
- .to_point()
- };
-
- let vertices = 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::soft(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(FixedStroke::default()),
- Smart::Auto => None,
- Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default),
- };
-
- let mut frame = Frame::soft(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"),
- }
- },
-}
diff --git a/crates/typst-library/src/visualize/polygon.rs b/crates/typst-library/src/visualize/polygon.rs
deleted file mode 100644
index b1ed9eaa..00000000
--- a/crates/typst-library/src/visualize/polygon.rs
+++ /dev/null
@@ -1,164 +0,0 @@
-use std::f64::consts::PI;
-
-use crate::prelude::*;
-
-/// A closed polygon.
-///
-/// The polygon is defined by its corner points and is closed automatically.
-///
-/// # Example
-/// ```example
-/// #polygon(
-/// fill: blue.lighten(80%),
-/// stroke: blue,
-/// (20%, 0pt),
-/// (60%, 0pt),
-/// (80%, 2cm),
-/// (0%, 2cm),
-/// )
-/// ```
-#[elem(scope, Layout)]
-pub struct PolygonElem {
- /// How to fill the polygon.
- ///
- /// When setting a fill, the default stroke disappears. To create a
- /// rectangle with both fill and stroke, you have to configure both.
- ///
- /// Currently all polygons are filled according to the
- /// [non-zero winding rule](https://en.wikipedia.org/wiki/Nonzero-rule).
- pub fill: Option<Paint>,
-
- /// How to [stroke]($stroke) the polygon. 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.
- #[resolve]
- #[fold]
- pub stroke: Smart<Option<Stroke>>,
-
- /// The vertices of the polygon. Each point is specified as an array of two
- /// [relative lengths]($relative).
- #[variadic]
- pub vertices: Vec<Axes<Rel<Length>>>,
-}
-
-#[scope]
-impl PolygonElem {
- /// A regular polygon, defined by its size and number of vertices.
- ///
- /// ```example
- /// #polygon.regular(
- /// fill: blue.lighten(80%),
- /// stroke: blue,
- /// size: 30pt,
- /// vertices: 3,
- /// )
- /// ```
- #[func(title = "Regular Polygon")]
- pub fn regular(
- /// How to fill the polygon. See the general
- /// [polygon's documentation]($polygon.fill) for more details.
- #[named]
- fill: Option<Option<Paint>>,
-
- /// How to stroke the polygon. See the general
- /// [polygon's documentation]($polygon.stroke) for more details.
- #[named]
- stroke: Option<Smart<Option<Stroke>>>,
-
- /// The diameter of the [circumcircle](https://en.wikipedia.org/wiki/Circumcircle)
- /// of the regular polygon.
- #[named]
- #[default(Em::one().into())]
- size: Length,
-
- /// The number of vertices in the polygon.
- #[named]
- #[default(3)]
- vertices: u64,
- ) -> Content {
- let radius = size / 2.0;
- let angle = |i: f64| {
- 2.0 * PI * i / (vertices as f64) + PI * (1.0 / 2.0 - 1.0 / vertices as f64)
- };
- let (horizontal_offset, vertical_offset) = (0..=vertices)
- .map(|v| {
- (
- (radius * angle(v as f64).cos()) + radius,
- (radius * angle(v as f64).sin()) + radius,
- )
- })
- .fold((radius, radius), |(min_x, min_y), (v_x, v_y)| {
- (
- if min_x < v_x { min_x } else { v_x },
- if min_y < v_y { min_y } else { v_y },
- )
- });
- let vertices = (0..=vertices)
- .map(|v| {
- let x = (radius * angle(v as f64).cos()) + radius - horizontal_offset;
- let y = (radius * angle(v as f64).sin()) + radius - vertical_offset;
- Axes::new(x, y).map(Rel::from)
- })
- .collect();
-
- let mut elem = PolygonElem::new(vertices);
- if let Some(fill) = fill {
- elem.push_fill(fill);
- }
- if let Some(stroke) = stroke {
- elem.push_stroke(stroke);
- }
- elem.pack()
- }
-}
-
-impl Layout for PolygonElem {
- #[tracing::instrument(name = "PolygonElem::layout", skip_all)]
- fn layout(
- &self,
- _: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- let points: Vec<Point> = self
- .vertices()
- .iter()
- .map(|c| {
- c.resolve(styles).zip_map(regions.base(), Rel::relative_to).to_point()
- })
- .collect();
-
- let size = points.iter().fold(Point::zero(), |max, c| c.max(max)).to_size();
- if !size.is_finite() {
- bail!(error!(self.span(), "cannot create polygon with infinite size"));
- }
- let mut frame = Frame::hard(size);
-
- // Only create a path if there are more than zero points.
- if points.is_empty() {
- return Ok(Fragment::frame(frame));
- }
-
- // Prepare fill and stroke.
- let fill = self.fill(styles);
- let stroke = match self.stroke(styles) {
- Smart::Auto if fill.is_none() => Some(FixedStroke::default()),
- Smart::Auto => None,
- Smart::Custom(stroke) => stroke.map(Stroke::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))
- }
-}
diff --git a/crates/typst-library/src/visualize/shape.rs b/crates/typst-library/src/visualize/shape.rs
deleted file mode 100644
index 779b07ff..00000000
--- a/crates/typst-library/src/visualize/shape.rs
+++ /dev/null
@@ -1,547 +0,0 @@
-use std::f64::consts::SQRT_2;
-
-use crate::prelude::*;
-
-/// A rectangle with optional content.
-///
-/// # Example
-/// ```example
-/// // Without content.
-/// #rect(width: 35%, height: 30pt)
-///
-/// // With content.
-/// #rect[
-/// Automatically sized \
-/// to fit the content.
-/// ]
-/// ```
-#[elem(title = "Rectangle", Layout)]
-pub struct RectElem {
- /// The rectangle's width, relative to its parent container.
- pub width: Smart<Rel<Length>>,
-
- /// The rectangle's height, relative to its parent container.
- pub height: Smart<Rel<Length>>,
-
- /// How to fill the rectangle.
- ///
- /// When setting a fill, the default stroke disappears. To create a
- /// rectangle with both fill and stroke, you have to configure both.
- ///
- /// ```example
- /// #rect(fill: blue)
- /// ```
- pub fill: Option<Paint>,
-
- /// How to stroke the rectangle. This can be:
- ///
- /// - `{none}` to disable stroking
- /// - `{auto}` for a stroke of `{1pt + black}` if and if only if no fill is
- /// given.
- /// - Any kind of [stroke]($stroke)
- /// - A dictionary describing the stroke for each side inidvidually. The
- /// dictionary can contain the following keys in order of precedence:
- /// - `top`: The top stroke.
- /// - `right`: The right stroke.
- /// - `bottom`: The bottom stroke.
- /// - `left`: The left stroke.
- /// - `x`: The horizontal stroke.
- /// - `y`: The vertical stroke.
- /// - `rest`: The stroke on all sides except those for which the
- /// dictionary explicitly sets a size.
- ///
- /// ```example
- /// #stack(
- /// dir: ltr,
- /// spacing: 1fr,
- /// rect(stroke: red),
- /// rect(stroke: 2pt),
- /// rect(stroke: 2pt + red),
- /// )
- /// ```
- #[resolve]
- #[fold]
- pub stroke: Smart<Sides<Option<Option<Stroke>>>>,
-
- /// How much to round the rectangle's corners, relative to the minimum of
- /// the width and height divided by two. This can be:
- ///
- /// - A relative length for a uniform corner radius.
- /// - A dictionary: With a dictionary, the stroke for each side can be set
- /// individually. The dictionary can contain the following keys in order
- /// of precedence:
- /// - `top-left`: The top-left corner radius.
- /// - `top-right`: The top-right corner radius.
- /// - `bottom-right`: The bottom-right corner radius.
- /// - `bottom-left`: The bottom-left corner radius.
- /// - `left`: The top-left and bottom-left corner radii.
- /// - `top`: The top-left and top-right corner radii.
- /// - `right`: The top-right and bottom-right corner radii.
- /// - `bottom`: The bottom-left and bottom-right corner radii.
- /// - `rest`: The radii for all corners except those for which the
- /// dictionary explicitly sets a size.
- ///
- /// ```example
- /// #set rect(stroke: 4pt)
- /// #rect(
- /// radius: (
- /// left: 5pt,
- /// top-right: 20pt,
- /// bottom-right: 10pt,
- /// ),
- /// stroke: (
- /// left: red,
- /// top: yellow,
- /// right: green,
- /// bottom: blue,
- /// ),
- /// )
- /// ```
- #[resolve]
- #[fold]
- pub radius: Corners<Option<Rel<Length>>>,
-
- /// How much to pad the rectangle's content.
- /// See the [box's documentation]($box.outset) for more details.
- #[resolve]
- #[fold]
- #[default(Sides::splat(Abs::pt(5.0).into()))]
- pub inset: Sides<Option<Rel<Length>>>,
-
- /// How much to expand the rectangle's size without affecting the layout.
- /// See the [box's documentation]($box.outset) for more details.
- #[resolve]
- #[fold]
- pub outset: Sides<Option<Rel<Length>>>,
-
- /// The content to place into the rectangle.
- ///
- /// When this is omitted, the rectangle takes on a default size of at most
- /// `{45pt}` by `{30pt}`.
- #[positional]
- pub body: Option<Content>,
-}
-
-impl Layout for RectElem {
- #[tracing::instrument(name = "RectElem::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- layout(
- vt,
- styles,
- regions,
- ShapeKind::Rect,
- &self.body(styles),
- Axes::new(self.width(styles), self.height(styles)),
- self.fill(styles),
- self.stroke(styles),
- self.inset(styles),
- self.outset(styles),
- self.radius(styles),
- self.span(),
- )
- }
-}
-
-/// A square with optional content.
-///
-/// # Example
-/// ```example
-/// // Without content.
-/// #square(size: 40pt)
-///
-/// // With content.
-/// #square[
-/// Automatically \
-/// sized to fit.
-/// ]
-/// ```
-#[elem(Layout)]
-pub struct SquareElem {
- /// The square's side length. This is mutually exclusive with `width` and
- /// `height`.
- #[external]
- pub size: Smart<Length>,
-
- /// The square's width. This is mutually exclusive with `size` and `height`.
- ///
- /// In contrast to `size`, this can be relative to the parent container's
- /// width.
- #[parse(
- let size = args.named::<Smart<Length>>("size")?.map(|s| s.map(Rel::from));
- match size {
- None => args.named("width")?,
- size => size,
- }
- )]
- pub width: Smart<Rel<Length>>,
-
- /// The square's height. This is mutually exclusive with `size` and `width`.
- ///
- /// In contrast to `size`, this can be relative to the parent container's
- /// height.
- #[parse(match size {
- None => args.named("height")?,
- size => size,
- })]
- pub height: Smart<Rel<Length>>,
-
- /// How to fill the square. See the [rectangle's documentation]($rect.fill)
- /// for more details.
- pub fill: Option<Paint>,
-
- /// How to stroke the square. See the
- /// [rectangle's documentation]($rect.stroke) for more details.
- #[resolve]
- #[fold]
- pub stroke: Smart<Sides<Option<Option<Stroke>>>>,
-
- /// How much to round the square's corners. See the
- /// [rectangle's documentation]($rect.radius) for more details.
- #[resolve]
- #[fold]
- pub radius: Corners<Option<Rel<Length>>>,
-
- /// How much to pad the square's content. See the
- /// [box's documentation]($box.inset) for more details.
- #[resolve]
- #[fold]
- #[default(Sides::splat(Abs::pt(5.0).into()))]
- pub inset: Sides<Option<Rel<Length>>>,
-
- /// How much to expand the square's size without affecting the layout. See
- /// the [box's documentation]($box.outset) for more details.
- #[resolve]
- #[fold]
- pub outset: Sides<Option<Rel<Length>>>,
-
- /// The content to place into the square. The square expands to fit this
- /// content, keeping the 1-1 aspect ratio.
- ///
- /// When this is omitted, the square takes on a default size of at most
- /// `{30pt}`.
- #[positional]
- pub body: Option<Content>,
-}
-
-impl Layout for SquareElem {
- #[tracing::instrument(name = "SquareElem::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- layout(
- vt,
- styles,
- regions,
- ShapeKind::Square,
- &self.body(styles),
- Axes::new(self.width(styles), self.height(styles)),
- self.fill(styles),
- self.stroke(styles),
- self.inset(styles),
- self.outset(styles),
- self.radius(styles),
- self.span(),
- )
- }
-}
-
-/// An ellipse with optional content.
-///
-/// # Example
-/// ```example
-/// // Without content.
-/// #ellipse(width: 35%, height: 30pt)
-///
-/// // With content.
-/// #ellipse[
-/// #set align(center)
-/// Automatically sized \
-/// to fit the content.
-/// ]
-/// ```
-#[elem(Layout)]
-pub struct EllipseElem {
- /// The ellipse's width, relative to its parent container.
- pub width: Smart<Rel<Length>>,
-
- /// The ellipse's height, relative to its parent container.
- pub height: Smart<Rel<Length>>,
-
- /// How to fill the ellipse. See the [rectangle's documentation]($rect.fill)
- /// for more details.
- pub fill: Option<Paint>,
-
- /// How to stroke the ellipse. See the
- /// [rectangle's documentation]($rect.stroke) for more details.
- #[resolve]
- #[fold]
- pub stroke: Smart<Option<Stroke>>,
-
- /// How much to pad the ellipse's content. See the
- /// [box's documentation]($box.inset) for more details.
- #[resolve]
- #[fold]
- #[default(Sides::splat(Abs::pt(5.0).into()))]
- pub inset: Sides<Option<Rel<Length>>>,
-
- /// How much to expand the ellipse's size without affecting the layout. See
- /// the [box's documentation]($box.outset) for more details.
- #[resolve]
- #[fold]
- pub outset: Sides<Option<Rel<Length>>>,
-
- /// The content to place into the ellipse.
- ///
- /// When this is omitted, the ellipse takes on a default size of at most
- /// `{45pt}` by `{30pt}`.
- #[positional]
- pub body: Option<Content>,
-}
-
-impl Layout for EllipseElem {
- #[tracing::instrument(name = "EllipseElem::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- layout(
- vt,
- styles,
- regions,
- ShapeKind::Ellipse,
- &self.body(styles),
- Axes::new(self.width(styles), self.height(styles)),
- self.fill(styles),
- self.stroke(styles).map(Sides::splat),
- self.inset(styles),
- self.outset(styles),
- Corners::splat(Rel::zero()),
- self.span(),
- )
- }
-}
-
-/// A circle with optional content.
-///
-/// # Example
-/// ```example
-/// // Without content.
-/// #circle(radius: 25pt)
-///
-/// // With content.
-/// #circle[
-/// #set align(center + horizon)
-/// Automatically \
-/// sized to fit.
-/// ]
-/// ```
-#[elem(Layout)]
-pub struct CircleElem {
- /// The circle's radius. This is mutually exclusive with `width` and
- /// `height`.
- #[external]
- pub radius: Length,
-
- /// The circle's width. This is mutually exclusive with `radius` and
- /// `height`.
- ///
- /// In contrast to `radius`, this can be relative to the parent container's
- /// width.
- #[parse(
- let size = args
- .named::<Smart<Length>>("radius")?
- .map(|s| s.map(|r| 2.0 * Rel::from(r)));
- match size {
- None => args.named("width")?,
- size => size,
- }
- )]
- pub width: Smart<Rel<Length>>,
-
- /// The circle's height. This is mutually exclusive with `radius` and
- /// `width`.
- ///
- /// In contrast to `radius`, this can be relative to the parent container's
- /// height.
- #[parse(match size {
- None => args.named("height")?,
- size => size,
- })]
- pub height: Smart<Rel<Length>>,
-
- /// How to fill the circle. See the [rectangle's documentation]($rect.fill)
- /// for more details.
- pub fill: Option<Paint>,
-
- /// How to stroke the circle. See the
- /// [rectangle's documentation]($rect.stroke) for more details.
- #[resolve]
- #[fold]
- #[default(Smart::Auto)]
- pub stroke: Smart<Option<Stroke>>,
-
- /// How much to pad the circle's content. See the
- /// [box's documentation]($box.inset) for more details.
- #[resolve]
- #[fold]
- #[default(Sides::splat(Abs::pt(5.0).into()))]
- pub inset: Sides<Option<Rel<Length>>>,
-
- /// How much to expand the circle's size without affecting the layout. See
- /// the [box's documentation]($box.outset) for more details.
- #[resolve]
- #[fold]
- pub outset: Sides<Option<Rel<Length>>>,
-
- /// The content to place into the circle. The circle expands to fit this
- /// content, keeping the 1-1 aspect ratio.
- #[positional]
- pub body: Option<Content>,
-}
-
-impl Layout for CircleElem {
- #[tracing::instrument(name = "CircleElem::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- layout(
- vt,
- styles,
- regions,
- ShapeKind::Circle,
- &self.body(styles),
- Axes::new(self.width(styles), self.height(styles)),
- self.fill(styles),
- self.stroke(styles).map(Sides::splat),
- self.inset(styles),
- self.outset(styles),
- Corners::splat(Rel::zero()),
- self.span(),
- )
- }
-}
-
-/// Layout a shape.
-#[tracing::instrument(name = "shape::layout", skip_all)]
-#[allow(clippy::too_many_arguments)]
-fn layout(
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- kind: ShapeKind,
- body: &Option<Content>,
- sizing: Axes<Smart<Rel<Length>>>,
- fill: Option<Paint>,
- stroke: Smart<Sides<Option<Stroke<Abs>>>>,
- mut inset: Sides<Rel<Abs>>,
- outset: Sides<Rel<Abs>>,
- radius: Corners<Rel<Abs>>,
- span: Span,
-) -> SourceResult<Fragment> {
- let resolved = sizing
- .zip_map(regions.base(), |s, r| s.map(|v| v.resolve(styles).relative_to(r)));
-
- let mut frame;
- if let Some(child) = body {
- let region = resolved.unwrap_or(regions.base());
- if kind.is_round() {
- inset = inset.map(|side| side + Ratio::new(0.5 - SQRT_2 / 4.0));
- }
-
- // Pad the child.
- let child = child.clone().padded(inset.map(|side| side.map(Length::from)));
- let expand = sizing.as_ref().map(Smart::is_custom);
- let pod = Regions::one(region, expand);
- frame = child.layout(vt, styles, pod)?.into_frame();
-
- // Enforce correct size.
- *frame.size_mut() = expand.select(region, frame.size());
-
- // Relayout with full expansion into square region to make sure
- // the result is really a square or circle.
- if kind.is_quadratic() {
- frame.set_size(Size::splat(frame.size().max_by_side()));
- let length = frame.size().max_by_side().min(region.min_by_side());
- let pod = Regions::one(Size::splat(length), Axes::splat(true));
- frame = child.layout(vt, styles, pod)?.into_frame();
- }
-
- // Enforce correct size again.
- *frame.size_mut() = expand.select(region, frame.size());
- if kind.is_quadratic() {
- frame.set_size(Size::splat(frame.size().max_by_side()));
- }
- } else {
- // The default size that a shape takes on if it has no child and
- // enough space.
- let default = Size::new(Abs::pt(45.0), Abs::pt(30.0));
- let mut size = resolved.unwrap_or(default.min(regions.base()));
- if kind.is_quadratic() {
- size = Size::splat(size.min_by_side());
- }
- frame = Frame::soft(size);
- }
-
- // Prepare stroke.
- let stroke = match stroke {
- Smart::Auto if fill.is_none() => Sides::splat(Some(FixedStroke::default())),
- Smart::Auto => Sides::splat(None),
- Smart::Custom(strokes) => strokes.map(|s| s.map(Stroke::unwrap_or_default)),
- };
-
- // Add fill and/or stroke.
- if fill.is_some() || stroke.iter().any(Option::is_some) {
- if kind.is_round() {
- let outset = outset.relative_to(frame.size());
- let size = frame.size() + outset.sum_by_axis();
- let pos = Point::new(-outset.left, -outset.top);
- let shape = ellipse(size, fill, stroke.left);
- frame.prepend(pos, FrameItem::Shape(shape, span));
- } else {
- frame.fill_and_stroke(fill, stroke, outset, radius, span);
- }
- }
-
- // Apply metadata.
- frame.meta(styles, false);
-
- Ok(Fragment::frame(frame))
-}
-
-/// A category of shape.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub enum ShapeKind {
- /// A rectangle with equal side lengths.
- Square,
- /// A quadrilateral with four right angles.
- Rect,
- /// An ellipse with coinciding foci.
- Circle,
- /// A curve around two focal points.
- Ellipse,
-}
-
-impl ShapeKind {
- /// Whether this shape kind is curvy.
- fn is_round(self) -> bool {
- matches!(self, Self::Circle | Self::Ellipse)
- }
-
- /// Whether this shape kind has equal side length.
- fn is_quadratic(self) -> bool {
- matches!(self, Self::Square | Self::Circle)
- }
-}