Add constructor for stroke. (#2518)

This commit is contained in:
Joseph Wilson 2023-11-08 23:08:38 +13:00 committed by GitHub
parent 3a1e47913d
commit 2ce9d76025
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 176 additions and 41 deletions

View File

@ -368,7 +368,7 @@ pub struct CircleElem {
)]
pub width: Smart<Rel<Length>>,
/// The circle's height.This is mutually exclusive with `radius` and
/// 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

View File

@ -1,10 +1,11 @@
use super::*;
use crate::eval::{dict, Cast, FromValue, NoneValue};
use crate::diag::SourceResult;
use crate::eval::{dict, Args, Cast, FromValue, NoneValue};
/// Defines how to draw a line.
///
/// A stroke has a _paint_ (typically a solid color), a _thickness,_ a line
/// _cap,_ a line _join,_ a _miter-limit,_ and a _dash_ pattern. All of these
/// 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
@ -31,42 +32,17 @@ use crate::eval::{dict, Cast, FromValue, NoneValue};
/// - A stroke combined from color and thickness using the `+` operator as in
/// `{2pt + red}`.
///
/// # Complex strokes
/// For full control, you can also pass a [dictionary]($dictionary) to any
/// function that expects a stroke. This dictionary has the following keys:
///
/// - `paint`: The [color]($color) to use for the stroke.
///
/// - `thickness`: The stroke's thickness as a [length]($length).
///
/// - `cap`: How the line terminates. One of `{"butt"}`, `{"round"}`, or
/// `{"square"}`.
///
/// - `join`: How sharp turns of a contour are rendered. One of `{"miter"}`,
/// `{"round"}`, or `{"bevel"}`. Not applicable to lines but to
/// [polygons]($polygon) or [paths]($path).
///
/// - `miter-limit`: Number at which protruding sharp angles are rendered with a
/// bevel instead. The higher the number, the sharper an angle can be before
/// it is bevelled. Only applicable if `join` is `{"miter"}`. Defaults to
/// `{4.0}`.
///
/// - `dash`: The dash pattern to use. Can be any of the following:
/// - One of the predefined patterns `{"solid"}`, `{"dotted"}`,
/// `{"densely-dotted"}`, `{"loosely-dotted"}`, `{"dashed"}`,
/// `{"densely-dashed"}`, `{"loosely-dashed"}`, `{"dash-dotted"}`,
/// `{"densely-dash-dotted"}` or `{"loosely-dash-dotted"}`
/// - An [array]($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]($dictionary) with the keys `array` (same as the array
/// above), and `phase` (of type [length]($length)), which defines where in
/// the pattern to start drawing.
/// For full control, you can also provide a [dictionary]($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 mentioned in the
/// dictionary format above. For example, `{(2pt + blue).thickness}` is `{2pt}`.
/// Meanwhile, `{(2pt + blue).cap}` is `{auto}` because it's unspecified.
#[ty]
/// 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)]
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Stroke<T: Numeric = Length> {
/// The stroke's paint.
@ -83,6 +59,138 @@ pub struct Stroke<T: Numeric = Length> {
pub miter_limit: Smart<Scalar>,
}
#[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]($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]($dictionary) with the keys `array` (same as the array
/// above), and `phase` (of type [length]($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 line_cap = take::<LineCap>(args, "cap")?;
let line_join = take::<LineJoin>(args, "join")?;
let dash_pattern = take::<Option<DashPattern>>(args, "dash")?;
let miter_limit = take::<f64>(args, "miter-limit")?.map(Scalar::new);
Ok(Self {
paint,
thickness,
line_cap,
line_join,
dash_pattern,
miter_limit,
})
}
}
impl<T: Numeric> Stroke<T> {
/// Map the contained lengths with `f`.
pub fn map<F, U: Numeric>(self, f: F) -> Stroke<U>
@ -267,9 +375,10 @@ cast! {
..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) -> StrResult<Smart<T>> {
Ok(dict.take(key).ok().map(T::from_value)
.transpose()?.map(Smart::Custom).unwrap_or(Smart::Auto))
Ok(dict.take(key).ok().map(Smart::<T>::from_value)
.transpose()?.unwrap_or(Smart::Auto))
}
let paint = take::<Paint>(&mut dict, "paint")?;
@ -299,8 +408,11 @@ cast! {
/// 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,
}
@ -317,8 +429,13 @@ impl Repr for LineCap {
/// 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,
}

View File

@ -60,7 +60,7 @@
#line(length: 60pt, stroke: (paint: red, thicknes: 1pt))
---
// Error: 29-55 expected "solid", "dotted", "densely-dotted", "loosely-dotted", "dashed", "densely-dashed", "loosely-dashed", "dash-dotted", "densely-dash-dotted", "loosely-dash-dotted", array, dictionary, or none
// Error: 29-55 expected "solid", "dotted", "densely-dotted", "loosely-dotted", "dashed", "densely-dashed", "loosely-dashed", "dash-dotted", "densely-dash-dotted", "loosely-dash-dotted", array, dictionary, none, or auto
#line(length: 60pt, stroke: (paint: red, dash: "dash"))
---
@ -95,3 +95,21 @@
((0%, 50%), (4%, 4%)),
((50%, 0%), (4%, 4%)),
)
---
// Converting to stroke
#assert.eq(stroke(red).paint, red)
#assert.eq(stroke(red).thickness, auto)
#assert.eq(stroke(2pt).paint, auto)
#assert.eq(stroke((cap: "round", paint: blue)).cap, "round")
#assert.eq(stroke((cap: auto, paint: blue)).cap, auto)
#assert.eq(stroke((cap: auto, paint: blue)).thickness, auto)
// Error: 9-21 unexpected key "foo", valid keys are "paint", "thickness", "cap", "join", "dash", and "miter-limit"
#stroke((foo: "bar"))
// Constructing with named arguments
#assert.eq(stroke(paint: blue, thickness: 8pt), 8pt + blue)
#assert.eq(stroke(thickness: 2pt), stroke(2pt))
#assert.eq(stroke(cap: "round").thickness, auto)
#assert.eq(stroke(cap: "round", thickness: auto).thickness, auto)