diff options
Diffstat (limited to 'crates/typst-library/src/visualize')
| -rw-r--r-- | crates/typst-library/src/visualize/image.rs | 271 | ||||
| -rw-r--r-- | crates/typst-library/src/visualize/line.rs | 86 | ||||
| -rw-r--r-- | crates/typst-library/src/visualize/mod.rs | 31 | ||||
| -rw-r--r-- | crates/typst-library/src/visualize/path.rs | 208 | ||||
| -rw-r--r-- | crates/typst-library/src/visualize/polygon.rs | 164 | ||||
| -rw-r--r-- | crates/typst-library/src/visualize/shape.rs | 547 |
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) - } -} |
