diff --git a/library/src/math/frac.rs b/library/src/math/frac.rs index 1712a1fc8..7520e10d1 100644 --- a/library/src/math/frac.rs +++ b/library/src/math/frac.rs @@ -135,6 +135,7 @@ fn layout( Geometry::Line(Point::with_x(line_width)).stroked(Stroke { paint: TextElem::fill_in(ctx.styles()), thickness, + ..Stroke::default() }), span, ), diff --git a/library/src/math/root.rs b/library/src/math/root.rs index 037c6ce76..cc48cd747 100644 --- a/library/src/math/root.rs +++ b/library/src/math/root.rs @@ -121,8 +121,11 @@ fn layout( frame.push( line_pos, FrameItem::Shape( - Geometry::Line(Point::with_x(radicand.width())) - .stroked(Stroke { paint: TextElem::fill_in(ctx.styles()), thickness }), + Geometry::Line(Point::with_x(radicand.width())).stroked(Stroke { + paint: TextElem::fill_in(ctx.styles()), + thickness, + ..Stroke::default() + }), span, ), ); diff --git a/library/src/text/deco.rs b/library/src/text/deco.rs index 79917641c..ab89e6b53 100644 --- a/library/src/text/deco.rs +++ b/library/src/text/deco.rs @@ -271,6 +271,7 @@ pub(super) fn decorate( let stroke = deco.stroke.clone().unwrap_or(Stroke { paint: text.fill.clone(), thickness: metrics.thickness.at(text.size), + ..Stroke::default() }); let gap_padding = 0.08 * text.size; diff --git a/library/src/visualize/line.rs b/library/src/visualize/line.rs index 0932a9f17..362f1a891 100644 --- a/library/src/visualize/line.rs +++ b/library/src/visualize/line.rs @@ -40,9 +40,32 @@ pub struct LineElem { /// to `{1pt}`. /// - A stroke combined from color and thickness using the `+` operator as /// in `{2pt + red}`. + /// - A stroke described by a dictionary with any of the following keys: + /// - `color`: the color to use for the stroke + /// - `thickness`: the stroke's thickness + /// - `cap`: one of `"butt"`, `"round"` or `"square"`, the line cap of the stroke + /// - `join`: one of `"miter"`, `"round"` or `"bevel"`, the line join of the stroke + /// - `miter-limit`: the miter limit to use if `join` is `"miter"`, defaults to 4.0 + /// - `dash`: the dash pattern to use. Can be any of the following: + /// - One of the strings `"solid"`, `"dotted"`, `"densely-dotted"`, `"loosely-dotted"`, + /// `"dashed"`, `"densely-dashed"`, `"loosely-dashed"`, `"dashdotted"`, + /// `"densely-dashdotted"` or `"loosely-dashdotted"` + /// - An array with elements that specify the lengths of dashes and gaps, alternating. + /// Elements can also be the string `"dot"` for a length equal to the line thickness. + /// - A dict with the keys `array`, same as the array above, and `phase`, the offset to + /// the start of the first dash. + /// /// /// ```example - /// #line(length: 100%, stroke: 2pt + red) + /// #stack( + /// line(length: 100%, stroke: 2pt + red), + /// v(1em), + /// line(length: 100%, stroke: (color: blue, thickness: 4pt, cap: "round")), + /// v(1em), + /// line(length: 100%, stroke: (color: blue, thickness: 1pt, dash: "dashed")), + /// v(1em), + /// line(length: 100%, stroke: (color: blue, thickness: 1pt, dash: ("dot", 2pt, 4pt, 2pt))), + /// ) /// ``` #[resolve] #[fold] diff --git a/library/src/visualize/shape.rs b/library/src/visualize/shape.rs index 51dbabd80..e0214f035 100644 --- a/library/src/visualize/shape.rs +++ b/library/src/visualize/shape.rs @@ -47,8 +47,22 @@ pub struct RectElem { /// to `{1pt}`. /// - A stroke combined from color and thickness using the `+` operator as /// in `{2pt + red}`. - /// - A dictionary: With a dictionary, the stroke for each side can be set - /// individually. The dictionary can contain the following keys in order + /// - A stroke described by a dictionary with any of the following keys: + /// - `color`: the color to use for the stroke + /// - `thickness`: the stroke's thickness + /// - `cap`: one of `"butt"`, `"round"` or `"square"`, the line cap of the stroke + /// - `join`: one of `"miter"`, `"round"` or `"bevel"`, the line join of the stroke + /// - `miter-limit`: the miter limit to use if `join` is `"miter"`, defaults to 4.0 + /// - `dash`: the dash pattern to use. Can be any of the following: + /// - One of the strings `"solid"`, `"dotted"`, `"densely-dotted"`, `"loosely-dotted"`, + /// `"dashed"`, `"densely-dashed"`, `"loosely-dashed"`, `"dashdotted"`, + /// `"densely-dashdotted"` or `"loosely-dashdotted"` + /// - An array with elements that specify the lengths of dashes and gaps, alternating. + /// Elements can also be the string `"dot"` for a length equal to the line thickness. + /// - A dict with the keys `array`, same as the array above, and `phase`, the offset to + /// the start of the first dash. + /// - Another 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. diff --git a/src/doc.rs b/src/doc.rs index 76d46f4a2..4f0428fb7 100644 --- a/src/doc.rs +++ b/src/doc.rs @@ -359,6 +359,7 @@ impl Frame { Geometry::Line(Point::with_x(self.size.x)).stroked(Stroke { paint: Color::RED.into(), thickness: Abs::pt(1.0), + ..Stroke::default() }), Span::detached(), ), @@ -386,6 +387,7 @@ impl Frame { Geometry::Line(Point::with_x(self.size.x)).stroked(Stroke { paint: Color::GREEN.into(), thickness: Abs::pt(1.0), + ..Stroke::default() }), Span::detached(), ), diff --git a/src/eval/ops.rs b/src/eval/ops.rs index 948243d12..43ee6ceb6 100644 --- a/src/eval/ops.rs +++ b/src/eval/ops.rs @@ -108,6 +108,7 @@ pub fn add(lhs: Value, rhs: Value) -> StrResult { Value::dynamic(PartialStroke { paint: Smart::Custom(color.into()), thickness: Smart::Custom(thickness), + ..PartialStroke::default() }) } diff --git a/src/export/pdf/page.rs b/src/export/pdf/page.rs index 636d42c77..d6ead124d 100644 --- a/src/export/pdf/page.rs +++ b/src/export/pdf/page.rs @@ -1,5 +1,7 @@ use ecow::eco_format; -use pdf_writer::types::{ActionType, AnnotationType, ColorSpaceOperand}; +use pdf_writer::types::{ + ActionType, AnnotationType, ColorSpaceOperand, LineCapStyle, LineJoinStyle, +}; use pdf_writer::writers::ColorSpace; use pdf_writer::{Content, Filter, Finish, Name, Rect, Ref, Str}; @@ -7,8 +9,8 @@ use super::{deflate, AbsExt, EmExt, PdfContext, RefExt, D65_GRAY, SRGB}; use crate::doc::{Destination, Frame, FrameItem, GroupItem, Meta, TextItem}; use crate::font::Font; use crate::geom::{ - self, Abs, Color, Em, Geometry, Numeric, Paint, Point, Ratio, Shape, Size, Stroke, - Transform, + self, Abs, Color, Em, Geometry, LineCap, LineJoin, Numeric, Paint, Point, Ratio, + Shape, Size, Stroke, Transform, }; use crate::image::Image; @@ -250,8 +252,17 @@ impl PageContext<'_, '_> { fn set_stroke(&mut self, stroke: &Stroke) { if self.state.stroke.as_ref() != Some(stroke) { + let Stroke { + paint, + thickness, + line_cap, + line_join, + dash_pattern, + miter_limit, + } = stroke; + let f = |c| c as f32 / 255.0; - let Paint::Solid(color) = stroke.paint; + let Paint::Solid(color) = paint; match color { Color::Luma(c) => { self.set_stroke_color_space(D65_GRAY); @@ -267,7 +278,26 @@ impl PageContext<'_, '_> { } } - self.content.set_line_width(stroke.thickness.to_f32()); + self.content.set_line_width(thickness.to_f32()); + if self.state.stroke.as_ref().map(|s| &s.line_cap) != Some(line_cap) { + self.content.set_line_cap(line_cap.into()); + } + if self.state.stroke.as_ref().map(|s| &s.line_join) != Some(line_join) { + self.content.set_line_join(line_join.into()); + } + if self.state.stroke.as_ref().map(|s| &s.dash_pattern) != Some(dash_pattern) { + if let Some(pattern) = dash_pattern { + self.content.set_dash_pattern( + pattern.array.iter().map(|l| l.to_f32()), + pattern.phase.to_f32(), + ); + } else { + self.content.set_dash_pattern([], 0.0); + } + } + if self.state.stroke.as_ref().map(|s| &s.miter_limit) != Some(miter_limit) { + self.content.set_miter_limit(miter_limit.0 as f32); + } self.state.stroke = Some(stroke.clone()); } } @@ -486,3 +516,23 @@ fn write_link(ctx: &mut PageContext, pos: Point, dest: &Destination, size: Size) ctx.links.push((dest.clone(), rect)); } + +impl From<&LineCap> for LineCapStyle { + fn from(line_cap: &LineCap) -> Self { + match line_cap { + LineCap::Butt => LineCapStyle::ButtCap, + LineCap::Round => LineCapStyle::RoundCap, + LineCap::Square => LineCapStyle::ProjectingSquareCap, + } + } +} + +impl From<&LineJoin> for LineJoinStyle { + fn from(line_join: &LineJoin) -> Self { + match line_join { + LineJoin::Miter => LineJoinStyle::MiterJoin, + LineJoin::Round => LineJoinStyle::RoundJoin, + LineJoin::Bevel => LineJoinStyle::BevelJoin, + } + } +} diff --git a/src/export/render.rs b/src/export/render.rs index 8cee3aa65..f3c72ba0f 100644 --- a/src/export/render.rs +++ b/src/export/render.rs @@ -11,7 +11,8 @@ use usvg::{FitTo, NodeExt}; use crate::doc::{Frame, FrameItem, GroupItem, Meta, TextItem}; use crate::geom::{ - self, Abs, Color, Geometry, Paint, PathItem, Shape, Size, Stroke, Transform, + self, Abs, Color, Geometry, LineCap, LineJoin, Paint, PathItem, Shape, Size, Stroke, + Transform, }; use crate::image::{DecodedImage, Image}; @@ -392,9 +393,36 @@ fn render_shape( canvas.fill_path(&path, &paint, rule, ts, mask); } - if let Some(Stroke { paint, thickness }) = &shape.stroke { + if let Some(Stroke { + paint, + thickness, + line_cap, + line_join, + dash_pattern, + miter_limit, + }) = &shape.stroke + { + let dash = dash_pattern.as_ref().and_then(|pattern| { + // tiny-skia only allows dash patterns with an even number of elements, + // while pdf allows any number. + let len = if pattern.array.len() % 2 == 1 { + pattern.array.len() * 2 + } else { + pattern.array.len() + }; + let dash_array = + pattern.array.iter().map(|l| l.to_f32()).cycle().take(len).collect(); + + sk::StrokeDash::new(dash_array, pattern.phase.to_f32()) + }); let paint = paint.into(); - let stroke = sk::Stroke { width: thickness.to_f32(), ..Default::default() }; + let stroke = sk::Stroke { + width: thickness.to_f32(), + line_cap: line_cap.into(), + line_join: line_join.into(), + dash, + miter_limit: miter_limit.0 as f32, + }; canvas.stroke_path(&path, &paint, &stroke, ts, mask); } @@ -525,6 +553,26 @@ impl From for sk::Color { } } +impl From<&LineCap> for sk::LineCap { + fn from(line_cap: &LineCap) -> Self { + match line_cap { + LineCap::Butt => sk::LineCap::Butt, + LineCap::Round => sk::LineCap::Round, + LineCap::Square => sk::LineCap::Square, + } + } +} + +impl From<&LineJoin> for sk::LineJoin { + fn from(line_join: &LineJoin) -> Self { + match line_join { + LineJoin::Miter => sk::LineJoin::Miter, + LineJoin::Round => sk::LineJoin::Round, + LineJoin::Bevel => sk::LineJoin::Bevel, + } + } +} + /// Allows to build tiny-skia paths from glyph outlines. struct WrappedPathBuilder(sk::PathBuilder); diff --git a/src/geom/sides.rs b/src/geom/sides.rs index 25b1fab50..d9a020da8 100644 --- a/src/geom/sides.rs +++ b/src/geom/sides.rs @@ -188,22 +188,30 @@ where fn cast(mut value: Value) -> StrResult { if let Value::Dict(dict) = &mut value { - let mut take = |key| dict.take(key).ok().map(T::cast).transpose(); + let mut try_cast = || -> StrResult<_> { + let mut take = |key| dict.take(key).ok().map(T::cast).transpose(); - let rest = take("rest")?; - let x = take("x")?.or_else(|| rest.clone()); - let y = take("y")?.or_else(|| rest.clone()); - let sides = Sides { - left: take("left")?.or_else(|| x.clone()), - top: take("top")?.or_else(|| y.clone()), - right: take("right")?.or_else(|| x.clone()), - bottom: take("bottom")?.or_else(|| y.clone()), + let rest = take("rest")?; + let x = take("x")?.or_else(|| rest.clone()); + let y = take("y")?.or_else(|| rest.clone()); + let sides = Sides { + left: take("left")?.or_else(|| x.clone()), + top: take("top")?.or_else(|| y.clone()), + right: take("right")?.or_else(|| x.clone()), + bottom: take("bottom")?.or_else(|| y.clone()), + }; + + dict.finish(&["left", "top", "right", "bottom", "x", "y", "rest"])?; + + Ok(sides) }; - dict.finish(&["left", "top", "right", "bottom", "x", "y", "rest"])?; + if let Ok(res) = try_cast() { + return Ok(res); + } + } - Ok(sides) - } else if T::is(&value) { + if T::is(&value) { Ok(Self::splat(Some(T::cast(value)?))) } else { ::error(value) diff --git a/src/geom/stroke.rs b/src/geom/stroke.rs index 4dba06d9c..344da3c53 100644 --- a/src/geom/stroke.rs +++ b/src/geom/stroke.rs @@ -7,6 +7,14 @@ pub struct Stroke { pub paint: Paint, /// The stroke's thickness. pub thickness: Abs, + /// The stroke's line cap. + pub line_cap: LineCap, + /// The stroke's line join. + pub line_join: LineJoin, + /// The stroke's line dash pattern. + pub dash_pattern: Option>, + /// The miter limit. Defaults to 4.0, same as `tiny-skia`. + pub miter_limit: Scalar, } impl Default for Stroke { @@ -14,6 +22,10 @@ impl Default for Stroke { Self { paint: Paint::Solid(Color::BLACK), thickness: Abs::pt(1.0), + line_cap: LineCap::Butt, + line_join: LineJoin::Miter, + dash_pattern: None, + miter_limit: Scalar(4.0), } } } @@ -29,14 +41,41 @@ pub struct PartialStroke { pub paint: Smart, /// The stroke's thickness. pub thickness: Smart, + /// The stroke's line cap. + pub line_cap: Smart, + /// The stroke's line join. + pub line_join: Smart, + /// The stroke's line dash pattern. + pub dash_pattern: Smart>>, + /// The miter limit. + pub miter_limit: Smart, } impl PartialStroke { /// Unpack the stroke, filling missing fields from the `default`. pub fn unwrap_or(self, default: Stroke) -> Stroke { + let thickness = self.thickness.unwrap_or(default.thickness); + let dash_pattern = self + .dash_pattern + .map(|pattern| { + pattern.map(|pattern| DashPattern { + array: pattern + .array + .into_iter() + .map(|l| l.finish(thickness)) + .collect(), + phase: pattern.phase, + }) + }) + .unwrap_or(default.dash_pattern); + Stroke { paint: self.paint.unwrap_or(default.paint), - thickness: self.thickness.unwrap_or(default.thickness), + thickness, + line_cap: self.line_cap.unwrap_or(default.line_cap), + line_join: self.line_join.unwrap_or(default.line_join), + dash_pattern, + miter_limit: self.miter_limit.unwrap_or(default.miter_limit), } } @@ -48,13 +87,205 @@ impl PartialStroke { impl Debug for PartialStroke { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match (&self.paint, &self.thickness) { - (Smart::Custom(paint), Smart::Custom(thickness)) => { - write!(f, "{thickness:?} + {paint:?}") + let Self { + paint, + thickness, + line_cap, + line_join, + dash_pattern, + miter_limit, + } = &self; + if line_cap.is_auto() + && line_join.is_auto() + && dash_pattern.is_auto() + && miter_limit.is_auto() + { + match (&self.paint, &self.thickness) { + (Smart::Custom(paint), Smart::Custom(thickness)) => { + write!(f, "{thickness:?} + {paint:?}") + } + (Smart::Custom(paint), Smart::Auto) => paint.fmt(f), + (Smart::Auto, Smart::Custom(thickness)) => thickness.fmt(f), + (Smart::Auto, Smart::Auto) => f.pad(""), } - (Smart::Custom(paint), Smart::Auto) => paint.fmt(f), - (Smart::Auto, Smart::Custom(thickness)) => thickness.fmt(f), - (Smart::Auto, Smart::Auto) => f.pad(""), + } else { + write!(f, "(")?; + let mut sep = ""; + if let Smart::Custom(paint) = &paint { + write!(f, "{}color: {:?}", sep, paint)?; + sep = ", "; + } + if let Smart::Custom(thickness) = &thickness { + write!(f, "{}thickness: {:?}", sep, thickness)?; + sep = ", "; + } + if let Smart::Custom(cap) = &line_cap { + write!(f, "{}cap: {:?}", sep, cap)?; + sep = ", "; + } + if let Smart::Custom(join) = &line_join { + write!(f, "{}join: {:?}", sep, join)?; + sep = ", "; + } + if let Smart::Custom(dash) = &dash_pattern { + write!(f, "{}dash: {:?}", sep, dash)?; + sep = ", "; + } + if let Smart::Custom(miter_limit) = &miter_limit { + write!(f, "{}miter-limit: {:?}", sep, miter_limit)?; + } + write!(f, ")")?; + Ok(()) + } + } +} + +/// The line cap of a stroke +#[derive(Cast, Clone, Eq, PartialEq, Hash)] +pub enum LineCap { + Butt, + Round, + Square, +} + +impl Debug for LineCap { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + LineCap::Butt => write!(f, "\"butt\""), + LineCap::Round => write!(f, "\"round\""), + LineCap::Square => write!(f, "\"square\""), + } + } +} + +/// The line join of a stroke +#[derive(Cast, Clone, Eq, PartialEq, Hash)] +pub enum LineJoin { + Miter, + Round, + Bevel, +} + +impl Debug for LineJoin { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + LineJoin::Miter => write!(f, "\"miter\""), + LineJoin::Round => write!(f, "\"round\""), + LineJoin::Bevel => write!(f, "\"bevel\""), + } + } +} + +/// A line dash pattern +#[derive(Clone, Eq, PartialEq, Hash)] +pub struct DashPattern> { + /// The dash array. + pub array: Vec
, + /// The dash phase. + pub phase: T, +} + +impl Debug for DashPattern { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "(array: (")?; + for (i, elem) in self.array.iter().enumerate() { + if i == 0 { + write!(f, "{:?}", elem)?; + } else { + write!(f, ", {:?}", elem)?; + } + } + write!(f, "), phase: {:?})", self.phase)?; + Ok(()) + } +} + +impl From>> for DashPattern { + fn from(array: Vec>) -> Self { + Self { array, phase: T::default() } + } +} + +/// The length of a dash in a line dash pattern +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum DashLength { + LineWidth, + Length(T), +} + +impl From for DashLength { + fn from(l: Abs) -> Self { + DashLength::Length(l.into()) + } +} + +impl DashLength { + fn finish(self, line_width: T) -> T { + match self { + Self::LineWidth => line_width, + Self::Length(l) => l, + } + } +} + +cast_from_value! { + DashLength: "dash length", + "dot" => Self::LineWidth, + l: Length => Self::Length(l), +} + +impl Resolve for DashLength { + type Output = DashLength; + + fn resolve(self, styles: StyleChain) -> Self::Output { + match self { + Self::LineWidth => DashLength::LineWidth, + Self::Length(l) => DashLength::Length(l.resolve(styles)), + } + } +} + +cast_from_value! { + DashPattern: "dash pattern", + // Use same names as tikz: + // https://tex.stackexchange.com/questions/45275/tikz-get-values-for-predefined-dash-patterns + "solid" => Vec::new().into(), + "dotted" => vec![DashLength::LineWidth, Abs::pt(2.0).into()].into(), + "densely-dotted" => vec![DashLength::LineWidth, Abs::pt(1.0).into()].into(), + "loosely-dotted" => vec![DashLength::LineWidth, Abs::pt(4.0).into()].into(), + "dashed" => vec![Abs::pt(3.0).into(), Abs::pt(3.0).into()].into(), + "densely-dashed" => vec![Abs::pt(3.0).into(), Abs::pt(2.0).into()].into(), + "loosely-dashed" => vec![Abs::pt(3.0).into(), Abs::pt(6.0).into()].into(), + "dashdotted" => vec![Abs::pt(3.0).into(), Abs::pt(2.0).into(), DashLength::LineWidth, Abs::pt(2.0).into()].into(), + "densely-dashdotted" => vec![Abs::pt(3.0).into(), Abs::pt(1.0).into(), DashLength::LineWidth, Abs::pt(1.0).into()].into(), + "loosely-dashdotted" => vec![Abs::pt(3.0).into(), Abs::pt(4.0).into(), DashLength::LineWidth, Abs::pt(4.0).into()].into(), + array: Vec => { + Self { + array, + phase: Length::zero(), + } + }, + mut dict: Dict => { + let array: Vec = dict.take("array")?.cast()?; + let phase = dict.take("phase").ok().map(Length::cast) + .transpose()?.unwrap_or(Length::zero()); + + dict.finish(&["array", "phase"])?; + + Self { + array, + phase, + } + }, +} + +impl Resolve for DashPattern { + type Output = DashPattern; + + fn resolve(self, styles: StyleChain) -> Self::Output { + DashPattern { + array: self.array.into_iter().map(|l| l.resolve(styles)).collect(), + phase: self.phase.resolve(styles), } } } @@ -64,10 +295,42 @@ cast_from_value! { thickness: Length => Self { paint: Smart::Auto, thickness: Smart::Custom(thickness), + line_cap: Smart::Auto, + line_join: Smart::Auto, + dash_pattern: Smart::Auto, + miter_limit: Smart::Auto, }, color: Color => Self { paint: Smart::Custom(color.into()), thickness: Smart::Auto, + line_cap: Smart::Auto, + line_join: Smart::Auto, + dash_pattern: Smart::Auto, + miter_limit: Smart::Auto, + }, + mut dict: Dict => { + fn take>(dict: &mut Dict, key: &str) -> StrResult> { + Ok(dict.take(key).ok().map(T::cast) + .transpose()?.map(Smart::Custom).unwrap_or(Smart::Auto)) + } + + let paint = take::(&mut dict, "color")?; + let thickness = take::(&mut dict, "thickness")?; + let line_cap = take::(&mut dict, "cap")?; + let line_join = take::(&mut dict, "join")?; + let dash_pattern = take::>(&mut dict, "dash")?; + let miter_limit = take::(&mut dict, "miter-limit")?; + + dict.finish(&["color", "thickness", "cap", "join", "dash", "miter-limit"])?; + + Self { + paint, + thickness, + line_cap, + line_join, + dash_pattern, + miter_limit: miter_limit.map(Scalar), + } }, } @@ -78,6 +341,10 @@ impl Resolve for PartialStroke { PartialStroke { paint: self.paint, thickness: self.thickness.resolve(styles), + line_cap: self.line_cap, + line_join: self.line_join, + dash_pattern: self.dash_pattern.resolve(styles), + miter_limit: self.miter_limit, } } } @@ -89,6 +356,10 @@ impl Fold for PartialStroke { Self { paint: self.paint.or(outer.paint), thickness: self.thickness.or(outer.thickness), + line_cap: self.line_cap.or(outer.line_cap), + line_join: self.line_join.or(outer.line_join), + dash_pattern: self.dash_pattern.or(outer.dash_pattern), + miter_limit: self.miter_limit, } } } diff --git a/tests/ref/visualize/stroke.png b/tests/ref/visualize/stroke.png new file mode 100644 index 000000000..312eabec6 Binary files /dev/null and b/tests/ref/visualize/stroke.png differ diff --git a/tests/typ/visualize/shape-rect.typ b/tests/typ/visualize/shape-rect.typ index a95f2750a..951d5beaa 100644 --- a/tests/typ/visualize/shape-rect.typ +++ b/tests/typ/visualize/shape-rect.typ @@ -51,5 +51,5 @@ #rect(radius: (left: 10pt, cake: 5pt)) --- -// Error: 15-21 expected length, color, stroke, none, dictionary, or auto, found array +// Error: 15-21 expected length, color, dictionary, stroke, none, or auto, found array #rect(stroke: (1, 2)) diff --git a/tests/typ/visualize/stroke.typ b/tests/typ/visualize/stroke.typ new file mode 100644 index 000000000..2443d27a9 --- /dev/null +++ b/tests/typ/visualize/stroke.typ @@ -0,0 +1,71 @@ +// Test lines. + +--- +// Some simple test lines + +#line(length: 60pt, stroke: red) +#v(3pt) +#line(length: 60pt, stroke: 2pt) +#v(3pt) +#line(length: 60pt, stroke: blue + 1.5pt) +#v(3pt) +#line(length: 60pt, stroke: (color: red, thickness: 1pt, dash: "dashed")) +#v(3pt) +#line(length: 60pt, stroke: (color: red, thickness: 4pt, cap: "round")) + +--- +// Set rules with stroke + +#set line(stroke: (color: red, thickness: 1pt, cap: "butt", dash: "dashdotted")) + +#line(length: 60pt) +#v(3pt) +#line(length: 60pt, stroke: blue) +#v(3pt) +#line(length: 60pt, stroke: (dash: none)) + +--- +// Rectangle strokes + +#rect(width: 20pt, height: 20pt, stroke: red) +#v(3pt) +#rect(width: 20pt, height: 20pt, stroke: (rest: red, top: (color: blue, dash: "dashed"))) +#v(3pt) +#rect(width: 20pt, height: 20pt, stroke: (thickness: 5pt, join: "round")) + +--- +// Dashing +#line(length: 60pt, stroke: (color: red, thickness: 1pt, dash: ("dot", 1pt))) +#v(3pt) +#line(length: 60pt, stroke: (color: red, thickness: 1pt, dash: ("dot", 1pt, 4pt, 2pt))) +#v(3pt) +#line(length: 60pt, stroke: (color: red, thickness: 1pt, dash: (array: ("dot", 1pt, 4pt, 2pt), phase: 5pt))) +#v(3pt) +#line(length: 60pt, stroke: (color: red, thickness: 1pt, dash: ())) +#v(3pt) +#line(length: 60pt, stroke: (color: red, thickness: 1pt, dash: (1pt, 3pt, 9pt))) + +--- +// Line joins +#stack(dir: ltr, + polygon(stroke: (thickness: 4pt, color: blue, join: "round"), + (0pt, 20pt), (15pt, 0pt), (0pt, 40pt), (15pt, 45pt)), + h(1em), + polygon(stroke: (thickness: 4pt, color: blue, join: "bevel"), + (0pt, 20pt), (15pt, 0pt), (0pt, 40pt), (15pt, 45pt)), + h(1em), + polygon(stroke: (thickness: 4pt, color: blue, join: "miter"), + (0pt, 20pt), (15pt, 0pt), (0pt, 40pt), (15pt, 45pt)), + h(1em), + polygon(stroke: (thickness: 4pt, color: blue, join: "miter", miter-limit: 20.0), + (0pt, 20pt), (15pt, 0pt), (0pt, 40pt), (15pt, 45pt)), +) +--- + +// Error: 29-56 unexpected key "thicknes", valid keys are "color", "thickness", "cap", "join", "dash", and "miter-limit" +#line(length: 60pt, stroke: (color: red, thicknes: 1pt)) + +--- + +// Error: 29-55 expected "solid", "dotted", "densely-dotted", "loosely-dotted", "dashed", "densely-dashed", "loosely-dashed", "dashdotted", "densely-dashdotted", "loosely-dashdotted", array, dictionary, dash pattern, or none +#line(length: 60pt, stroke: (color: red, dash: "dash"))