mirror of
https://github.com/typst/typst
synced 2025-05-14 04:56:26 +08:00
Add support for more complex strokes (#505)
This commit is contained in:
parent
46ce9c94e3
commit
d1cd814ef8
@ -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,
|
||||
),
|
||||
|
@ -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,
|
||||
),
|
||||
);
|
||||
|
@ -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;
|
||||
|
@ -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]
|
||||
|
@ -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.
|
||||
|
@ -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(),
|
||||
),
|
||||
|
@ -108,6 +108,7 @@ pub fn add(lhs: Value, rhs: Value) -> StrResult<Value> {
|
||||
Value::dynamic(PartialStroke {
|
||||
paint: Smart::Custom(color.into()),
|
||||
thickness: Smart::Custom(thickness),
|
||||
..PartialStroke::default()
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<Color> 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);
|
||||
|
||||
|
@ -188,22 +188,30 @@ where
|
||||
|
||||
fn cast(mut value: Value) -> StrResult<Self> {
|
||||
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 {
|
||||
<Self as Cast>::error(value)
|
||||
|
@ -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<DashPattern<Abs, Abs>>,
|
||||
/// 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<T = Length> {
|
||||
pub paint: Smart<Paint>,
|
||||
/// The stroke's thickness.
|
||||
pub thickness: Smart<T>,
|
||||
/// The stroke's line cap.
|
||||
pub line_cap: Smart<LineCap>,
|
||||
/// The stroke's line join.
|
||||
pub line_join: Smart<LineJoin>,
|
||||
/// The stroke's line dash pattern.
|
||||
pub dash_pattern: Smart<Option<DashPattern<T>>>,
|
||||
/// The miter limit.
|
||||
pub miter_limit: Smart<Scalar>,
|
||||
}
|
||||
|
||||
impl PartialStroke<Abs> {
|
||||
/// 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<Abs> {
|
||||
|
||||
impl<T: Debug> Debug for PartialStroke<T> {
|
||||
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("<stroke>"),
|
||||
}
|
||||
(Smart::Custom(paint), Smart::Auto) => paint.fmt(f),
|
||||
(Smart::Auto, Smart::Custom(thickness)) => thickness.fmt(f),
|
||||
(Smart::Auto, Smart::Auto) => f.pad("<stroke>"),
|
||||
} 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<T = Length, DT = DashLength<T>> {
|
||||
/// The dash array.
|
||||
pub array: Vec<DT>,
|
||||
/// The dash phase.
|
||||
pub phase: T,
|
||||
}
|
||||
|
||||
impl<T: Debug, DT: Debug> Debug for DashPattern<T, DT> {
|
||||
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<T: Default> From<Vec<DashLength<T>>> for DashPattern<T> {
|
||||
fn from(array: Vec<DashLength<T>>) -> 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<T = Length> {
|
||||
LineWidth,
|
||||
Length(T),
|
||||
}
|
||||
|
||||
impl From<Abs> for DashLength {
|
||||
fn from(l: Abs) -> Self {
|
||||
DashLength::Length(l.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> DashLength<T> {
|
||||
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<Abs>;
|
||||
|
||||
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<DashLength> => {
|
||||
Self {
|
||||
array,
|
||||
phase: Length::zero(),
|
||||
}
|
||||
},
|
||||
mut dict: Dict => {
|
||||
let array: Vec<DashLength> = 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<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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<T: Cast<Value>>(dict: &mut Dict, key: &str) -> StrResult<Smart<T>> {
|
||||
Ok(dict.take(key).ok().map(T::cast)
|
||||
.transpose()?.map(Smart::Custom).unwrap_or(Smart::Auto))
|
||||
}
|
||||
|
||||
let paint = take::<Paint>(&mut dict, "color")?;
|
||||
let thickness = take::<Length>(&mut dict, "thickness")?;
|
||||
let line_cap = take::<LineCap>(&mut dict, "cap")?;
|
||||
let line_join = take::<LineJoin>(&mut dict, "join")?;
|
||||
let dash_pattern = take::<Option<DashPattern>>(&mut dict, "dash")?;
|
||||
let miter_limit = take::<f64>(&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<Abs> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
BIN
tests/ref/visualize/stroke.png
Normal file
BIN
tests/ref/visualize/stroke.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
@ -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))
|
||||
|
71
tests/typ/visualize/stroke.typ
Normal file
71
tests/typ/visualize/stroke.typ
Normal file
@ -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"))
|
Loading…
x
Reference in New Issue
Block a user