diff options
| author | Laurenz <laurmaedje@gmail.com> | 2024-10-27 19:04:55 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-10-27 18:04:55 +0000 |
| commit | be7cfc85d08c545abfac08098b7b33b4bd71f37e (patch) | |
| tree | f4137fa2aaa57babae1f7603a9b2ed7e688f43d8 /crates/typst-library/src/visualize/stroke.rs | |
| parent | b8034a343831e8609aec2ec81eb7eeda57aa5d81 (diff) | |
Split out four new crates (#5302)
Diffstat (limited to 'crates/typst-library/src/visualize/stroke.rs')
| -rw-r--r-- | crates/typst-library/src/visualize/stroke.rs | 617 |
1 files changed, 617 insertions, 0 deletions
diff --git a/crates/typst-library/src/visualize/stroke.rs b/crates/typst-library/src/visualize/stroke.rs new file mode 100644 index 00000000..4ca10920 --- /dev/null +++ b/crates/typst-library/src/visualize/stroke.rs @@ -0,0 +1,617 @@ +use ecow::EcoString; +use typst_utils::{Numeric, Scalar}; + +use crate::diag::{HintedStrResult, SourceResult}; +use crate::foundations::{ + cast, dict, func, scope, ty, Args, Cast, Dict, Fold, FromValue, NoneValue, Repr, + Resolve, Smart, StyleChain, Value, +}; +use crate::layout::{Abs, Length}; +use crate::visualize::{Color, Gradient, Paint, Pattern}; + +/// Defines how to draw a line. +/// +/// A stroke has a _paint_ (a solid color or gradient), a _thickness,_ a line +/// _cap,_ a line _join,_ a _miter limit,_ and a _dash_ pattern. All of these +/// values are optional and have sensible defaults. +/// +/// # Example +/// ```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: 2pt + gradient.linear(..color.map.rainbow)), +/// ) +/// ``` +/// +/// # Simple strokes +/// You can create a simple solid stroke from a color, a thickness, or a +/// combination of the two. Specifically, wherever a stroke is expected you can +/// pass any of the following values: +/// +/// - A length specifying the stroke's thickness. The color is inherited, +/// defaulting to black. +/// - A color to use for the stroke. The thickness is inherited, defaulting to +/// `{1pt}`. +/// - A stroke combined from color and thickness using the `+` operator as in +/// `{2pt + red}`. +/// +/// For full control, you can also provide a [dictionary] or a `{stroke}` object +/// to any function that expects a stroke. The dictionary's keys may include any +/// of the parameters for the constructor function, shown below. +/// +/// # Fields +/// On a stroke object, you can access any of the fields listed in the +/// constructor function. For example, `{(2pt + blue).thickness}` is `{2pt}`. +/// Meanwhile, `{stroke(red).cap}` is `{auto}` because it's unspecified. Fields +/// set to `{auto}` are inherited. +#[ty(scope, cast)] +#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] +pub struct Stroke<T: Numeric = Length> { + /// The stroke's paint. + pub paint: Smart<Paint>, + /// The stroke's thickness. + pub thickness: Smart<T>, + /// The stroke's line cap. + pub cap: Smart<LineCap>, + /// The stroke's line join. + pub join: Smart<LineJoin>, + /// The stroke's line dash pattern. + pub dash: Smart<Option<DashPattern<T>>>, + /// The miter limit. + pub miter_limit: Smart<Scalar>, +} + +impl Stroke { + /// Create a stroke from a paint and a thickness. + pub fn from_pair(paint: impl Into<Paint>, thickness: Length) -> Self { + Self { + paint: Smart::Custom(paint.into()), + thickness: Smart::Custom(thickness), + ..Default::default() + } + } +} + +#[scope] +impl Stroke { + /// Converts a value to a stroke or constructs a stroke with the given + /// parameters. + /// + /// Note that in most cases you do not need to convert values to strokes in + /// order to use them, as they will be converted automatically. However, + /// this constructor can be useful to ensure a value has all the fields of a + /// stroke. + /// + /// ```example + /// #let my-func(x) = { + /// x = stroke(x) // Convert to a stroke + /// [Stroke has thickness #x.thickness.] + /// } + /// #my-func(3pt) \ + /// #my-func(red) \ + /// #my-func(stroke(cap: "round", thickness: 1pt)) + /// ``` + #[func(constructor)] + pub fn construct( + /// The real arguments (the other arguments are just for the docs, this + /// function is a bit involved, so we parse the arguments manually). + args: &mut Args, + + /// The color or gradient to use for the stroke. + /// + /// If set to `{auto}`, the value is inherited, defaulting to `{black}`. + #[external] + paint: Smart<Paint>, + + /// The stroke's thickness. + /// + /// If set to `{auto}`, the value is inherited, defaulting to `{1pt}`. + #[external] + thickness: Smart<Length>, + + /// How the ends of the stroke are rendered. + /// + /// If set to `{auto}`, the value is inherited, defaulting to `{"butt"}`. + #[external] + cap: Smart<LineCap>, + + /// How sharp turns are rendered. + /// + /// If set to `{auto}`, the value is inherited, defaulting to `{"miter"}`. + #[external] + join: Smart<LineJoin>, + + /// The dash pattern to use. This can be: + /// + /// - One of the predefined patterns: + /// - `{"solid"}` or `{none}` + /// - `{"dotted"}` + /// - `{"densely-dotted"}` + /// - `{"loosely-dotted"}` + /// - `{"dashed"}` + /// - `{"densely-dashed"}` + /// - `{"loosely-dashed"}` + /// - `{"dash-dotted"}` + /// - `{"densely-dash-dotted"}` + /// - `{"loosely-dash-dotted"}` + /// - An [array] with alternating lengths for dashes and gaps. You can + /// also use the string `{"dot"}` for a length equal to the line + /// thickness. + /// - A [dictionary] with the keys `array` (same as the array above), + /// and `phase` (of type [length]), which defines where in the pattern + /// to start drawing. + /// + /// If set to `{auto}`, the value is inherited, defaulting to `{none}`. + /// + /// ```example + /// #set line(length: 100%, stroke: 2pt) + /// #stack( + /// spacing: 1em, + /// line(stroke: (dash: "dashed")), + /// line(stroke: (dash: (10pt, 5pt, "dot", 5pt))), + /// line(stroke: (dash: (array: (10pt, 5pt, "dot", 5pt), phase: 10pt))), + /// ) + /// ``` + #[external] + dash: Smart<Option<DashPattern>>, + + /// Number at which protruding sharp bends are rendered with a bevel + /// instead or a miter join. The higher the number, the sharper an angle + /// can be before it is bevelled. Only applicable if `join` is + /// `{"miter"}`. + /// + /// Specifically, the miter limit is the maximum ratio between the + /// corner's protrusion length and the stroke's thickness. + /// + /// If set to `{auto}`, the value is inherited, defaulting to `{4.0}`. + /// + /// ```example + /// #let points = ((15pt, 0pt), (0pt, 30pt), (30pt, 30pt), (10pt, 20pt)) + /// #set path(stroke: 6pt + blue) + /// #stack( + /// dir: ltr, + /// spacing: 1cm, + /// path(stroke: (miter-limit: 1), ..points), + /// path(stroke: (miter-limit: 4), ..points), + /// path(stroke: (miter-limit: 5), ..points), + /// ) + /// ``` + #[external] + miter_limit: Smart<f64>, + ) -> SourceResult<Stroke> { + if let Some(stroke) = args.eat::<Stroke>()? { + return Ok(stroke); + } + + fn take<T: FromValue>(args: &mut Args, arg: &str) -> SourceResult<Smart<T>> { + Ok(args.named::<Smart<T>>(arg)?.unwrap_or(Smart::Auto)) + } + + let paint = take::<Paint>(args, "paint")?; + let thickness = take::<Length>(args, "thickness")?; + let cap = take::<LineCap>(args, "cap")?; + let join = take::<LineJoin>(args, "join")?; + let dash = take::<Option<DashPattern>>(args, "dash")?; + let miter_limit = take::<f64>(args, "miter-limit")?.map(Scalar::new); + + Ok(Self { paint, thickness, cap, join, dash, miter_limit }) + } +} + +impl<T: Numeric> Stroke<T> { + /// Map the contained lengths with `f`. + pub fn map<F, U: Numeric>(self, f: F) -> Stroke<U> + where + F: Fn(T) -> U, + { + Stroke { + paint: self.paint, + thickness: self.thickness.map(&f), + cap: self.cap, + join: self.join, + dash: self.dash.map(|pattern| { + pattern.map(|pattern| DashPattern { + array: pattern + .array + .into_iter() + .map(|l| match l { + DashLength::Length(v) => DashLength::Length(f(v)), + DashLength::LineWidth => DashLength::LineWidth, + }) + .collect(), + phase: f(pattern.phase), + }) + }), + miter_limit: self.miter_limit, + } + } +} + +impl Stroke<Abs> { + /// Unpack the stroke, filling missing fields from the `default`. + pub fn unwrap_or(self, default: FixedStroke) -> FixedStroke { + let thickness = self.thickness.unwrap_or(default.thickness); + let dash = self + .dash + .map(|pattern| { + pattern.map(|pattern| DashPattern { + array: pattern + .array + .into_iter() + .map(|l| l.finish(thickness)) + .collect(), + phase: pattern.phase, + }) + }) + .unwrap_or(default.dash); + + FixedStroke { + paint: self.paint.unwrap_or(default.paint), + thickness, + cap: self.cap.unwrap_or(default.cap), + join: self.join.unwrap_or(default.join), + dash, + miter_limit: self.miter_limit.unwrap_or(default.miter_limit), + } + } + + /// Unpack the stroke, filling missing fields with the default values. + pub fn unwrap_or_default(self) -> FixedStroke { + // we want to do this; the Clippy lint is not type-aware + #[allow(clippy::unwrap_or_default)] + self.unwrap_or(FixedStroke::default()) + } +} + +impl<T: Numeric + Repr> Repr for Stroke<T> { + fn repr(&self) -> EcoString { + let mut r = EcoString::new(); + let Self { paint, thickness, cap, join, dash, miter_limit } = &self; + if cap.is_auto() && join.is_auto() && dash.is_auto() && miter_limit.is_auto() { + match (&self.paint, &self.thickness) { + (Smart::Custom(paint), Smart::Custom(thickness)) => { + r.push_str(&thickness.repr()); + r.push_str(" + "); + r.push_str(&paint.repr()); + } + (Smart::Custom(paint), Smart::Auto) => r.push_str(&paint.repr()), + (Smart::Auto, Smart::Custom(thickness)) => r.push_str(&thickness.repr()), + (Smart::Auto, Smart::Auto) => r.push_str("1pt + black"), + } + } else { + r.push('('); + let mut sep = ""; + if let Smart::Custom(paint) = &paint { + r.push_str(sep); + r.push_str("paint: "); + r.push_str(&paint.repr()); + sep = ", "; + } + if let Smart::Custom(thickness) = &thickness { + r.push_str(sep); + r.push_str("thickness: "); + r.push_str(&thickness.repr()); + sep = ", "; + } + if let Smart::Custom(cap) = &cap { + r.push_str(sep); + r.push_str("cap: "); + r.push_str(&cap.repr()); + sep = ", "; + } + if let Smart::Custom(join) = &join { + r.push_str(sep); + r.push_str("join: "); + r.push_str(&join.repr()); + sep = ", "; + } + if let Smart::Custom(dash) = &dash { + r.push_str(sep); + r.push_str("dash: "); + if let Some(dash) = dash { + r.push_str(&dash.repr()); + } else { + r.push_str(&NoneValue.repr()); + } + sep = ", "; + } + if let Smart::Custom(miter_limit) = &miter_limit { + r.push_str(sep); + r.push_str("miter-limit: "); + r.push_str(&miter_limit.get().repr()); + } + r.push(')'); + } + r + } +} + +impl<T: Numeric + Fold> Fold for Stroke<T> { + fn fold(self, outer: Self) -> Self { + Self { + paint: self.paint.or(outer.paint), + thickness: self.thickness.or(outer.thickness), + cap: self.cap.or(outer.cap), + join: self.join.or(outer.join), + dash: self.dash.or(outer.dash), + miter_limit: self.miter_limit.or(outer.miter_limit), + } + } +} + +impl Resolve for Stroke { + type Output = Stroke<Abs>; + + fn resolve(self, styles: StyleChain) -> Self::Output { + Stroke { + paint: self.paint, + thickness: self.thickness.resolve(styles), + cap: self.cap, + join: self.join, + dash: self.dash.resolve(styles), + miter_limit: self.miter_limit, + } + } +} + +cast! { + type Stroke, + thickness: Length => Self { + thickness: Smart::Custom(thickness), + ..Default::default() + }, + color: Color => Self { + paint: Smart::Custom(color.into()), + ..Default::default() + }, + gradient: Gradient => Self { + paint: Smart::Custom(gradient.into()), + ..Default::default() + }, + pattern: Pattern => Self { + paint: Smart::Custom(pattern.into()), + ..Default::default() + }, + mut dict: Dict => { + // Get a value by key, accepting either Auto or something convertible to type T. + fn take<T: FromValue>(dict: &mut Dict, key: &str) -> HintedStrResult<Smart<T>> { + Ok(dict.take(key).ok().map(Smart::<T>::from_value) + .transpose()?.unwrap_or(Smart::Auto)) + } + + let paint = take::<Paint>(&mut dict, "paint")?; + let thickness = take::<Length>(&mut dict, "thickness")?; + let cap = take::<LineCap>(&mut dict, "cap")?; + let join = take::<LineJoin>(&mut dict, "join")?; + let dash = take::<Option<DashPattern>>(&mut dict, "dash")?; + let miter_limit = take::<f64>(&mut dict, "miter-limit")?; + dict.finish(&["paint", "thickness", "cap", "join", "dash", "miter-limit"])?; + + Self { + paint, + thickness, + cap, + join, + dash, + miter_limit: miter_limit.map(Scalar::new), + } + }, +} + +cast! { + Stroke<Abs>, + self => self.map(Length::from).into_value(), +} + +/// The line cap of a stroke +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum LineCap { + /// Square stroke cap with the edge at the stroke's end point. + Butt, + /// Circular stroke cap centered at the stroke's end point. + Round, + /// Square stroke cap centered at the stroke's end point. + Square, +} + +impl Repr for LineCap { + fn repr(&self) -> EcoString { + match self { + Self::Butt => "butt".repr(), + Self::Round => "round".repr(), + Self::Square => "square".repr(), + } + } +} + +/// The line join of a stroke +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum LineJoin { + /// Segments are joined with sharp edges. Sharp bends exceeding the miter + /// limit are bevelled instead. + Miter, + /// Segments are joined with circular corners. + Round, + /// Segments are joined with a bevel (a straight edge connecting the butts + /// of the joined segments). + Bevel, +} + +impl Repr for LineJoin { + fn repr(&self) -> EcoString { + match self { + Self::Miter => "miter".repr(), + Self::Round => "round".repr(), + Self::Bevel => "bevel".repr(), + } + } +} + +/// A line dash pattern. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct DashPattern<T: Numeric = Length, DT = DashLength<T>> { + /// The dash array. + pub array: Vec<DT>, + /// The dash phase. + pub phase: T, +} + +impl<T: Numeric + Repr, DT: Repr> Repr for DashPattern<T, DT> { + fn repr(&self) -> EcoString { + let mut r = EcoString::from("(array: ("); + for (i, elem) in self.array.iter().enumerate() { + if i != 0 { + r.push_str(", ") + } + r.push_str(&elem.repr()) + } + r.push_str("), phase: "); + r.push_str(&self.phase.repr()); + r.push(')'); + r + } +} + +impl<T: Numeric + Default> From<Vec<DashLength<T>>> for DashPattern<T> { + fn from(array: Vec<DashLength<T>>) -> Self { + Self { array, phase: T::default() } + } +} + +impl Resolve for DashPattern { + type Output = DashPattern<Abs>; + + fn resolve(self, styles: StyleChain) -> Self::Output { + DashPattern { + array: self.array.into_iter().map(|l| l.resolve(styles)).collect(), + phase: self.phase.resolve(styles), + } + } +} + +// Same names as tikz: +// https://tex.stackexchange.com/questions/45275/tikz-get-values-for-predefined-dash-patterns +cast! { + DashPattern, + self => dict! { "array" => self.array, "phase" => self.phase }.into_value(), + + "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(), + "dash-dotted" => vec![Abs::pt(3.0).into(), Abs::pt(2.0).into(), DashLength::LineWidth, Abs::pt(2.0).into()].into(), + "densely-dash-dotted" => vec![Abs::pt(3.0).into(), Abs::pt(1.0).into(), DashLength::LineWidth, Abs::pt(1.0).into()].into(), + "loosely-dash-dotted" => vec![Abs::pt(3.0).into(), Abs::pt(4.0).into(), DashLength::LineWidth, Abs::pt(4.0).into()].into(), + + array: Vec<DashLength> => Self { array, phase: Length::zero() }, + mut dict: Dict => { + let array: Vec<DashLength> = dict.take("array")?.cast()?; + let phase = dict.take("phase").ok().map(Value::cast) + .transpose()?.unwrap_or(Length::zero()); + dict.finish(&["array", "phase"])?; + Self { + array, + phase, + } + }, +} + +/// The length of a dash in a line dash pattern. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum DashLength<T: Numeric = Length> { + LineWidth, + Length(T), +} + +impl<T: Numeric> DashLength<T> { + fn finish(self, line_width: T) -> T { + match self { + Self::LineWidth => line_width, + Self::Length(l) => l, + } + } +} + +impl<T: Numeric + Repr> Repr for DashLength<T> { + fn repr(&self) -> EcoString { + match self { + Self::LineWidth => "dot".repr(), + Self::Length(v) => v.repr(), + } + } +} + +impl Resolve for DashLength { + type Output = DashLength<Abs>; + + fn resolve(self, styles: StyleChain) -> Self::Output { + match self { + Self::LineWidth => DashLength::LineWidth, + Self::Length(v) => DashLength::Length(v.resolve(styles)), + } + } +} + +impl From<Abs> for DashLength { + fn from(l: Abs) -> Self { + DashLength::Length(l.into()) + } +} + +cast! { + DashLength, + self => match self { + Self::LineWidth => "dot".into_value(), + Self::Length(v) => v.into_value(), + }, + "dot" => Self::LineWidth, + v: Length => Self::Length(v), +} + +/// A fully specified stroke of a geometric shape. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct FixedStroke { + /// The stroke's paint. + pub paint: Paint, + /// The stroke's thickness. + pub thickness: Abs, + /// The stroke's line cap. + pub cap: LineCap, + /// The stroke's line join. + pub join: LineJoin, + /// The stroke's line dash pattern. + pub dash: Option<DashPattern<Abs, Abs>>, + /// The miter limit. Defaults to 4.0, same as `tiny-skia`. + pub miter_limit: Scalar, +} + +impl FixedStroke { + /// Create a stroke from a paint and a thickness. + pub fn from_pair(paint: impl Into<Paint>, thickness: Abs) -> Self { + Self { + paint: paint.into(), + thickness, + ..Default::default() + } + } +} + +impl Default for FixedStroke { + fn default() -> Self { + Self { + paint: Paint::Solid(Color::BLACK), + thickness: Abs::pt(1.0), + cap: LineCap::Butt, + join: LineJoin::Miter, + dash: None, + miter_limit: Scalar::new(4.0), + } + } +} |
