diff --git a/Cargo.toml b/Cargo.toml index 0bb01e80..ba8ba2fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,9 @@ features = ["mint", "schemars", "serde"] default = ["std"] std = [] +[dependencies] +smallvec = "1.10" + [dependencies.arrayvec] version = "0.7.1" default-features = false diff --git a/src/lib.rs b/src/lib.rs index 203e367b..3c09b4ab 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -107,6 +107,7 @@ mod rounded_rect_radii; mod shape; pub mod simplify; mod size; +mod stroke; #[cfg(feature = "std")] mod svg; mod translate_scale; @@ -131,6 +132,7 @@ pub use crate::rounded_rect_radii::*; pub use crate::shape::*; pub use crate::size::*; #[cfg(feature = "std")] +pub use crate::stroke::*; pub use crate::svg::*; pub use crate::translate_scale::*; pub use crate::vec2::*; diff --git a/src/stroke.rs b/src/stroke.rs new file mode 100644 index 00000000..657982bf --- /dev/null +++ b/src/stroke.rs @@ -0,0 +1,256 @@ +// Copyright 2023 the Kurbo Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use core::borrow::Borrow; + +use smallvec::SmallVec; + +use crate::{PathEl, BezPath, Point, Vec2, PathSeg, CubicBez, offset::CubicOffset, fit_to_bezpath, QuadBez}; + +/// Defines the connection between two segments of a stroke. +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum Join { + /// A straight line connecting the segments. + Bevel, + /// The segments are extended to their natural intersection point. + Miter, + /// An arc between the segments. + Round, +} + +/// Defines the shape to be drawn at the ends of a stroke. +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum Cap { + /// Flat cap. + Butt, + /// Square cap with dimensions equal to half the stroke width. + Square, + /// Rounded cap with radius equal to half the stroke width. + Round, +} + +/// Describes the visual style of a stroke. +#[derive(Clone, Debug)] +pub struct Stroke { + /// Width of the stroke. + pub width: f64, + /// Style for connecting segments of the stroke. + pub join: Join, + /// Limit for miter joins. + pub miter_limit: f64, + /// Style for capping the beginning of an open subpath. + pub start_cap: Cap, + /// Style for capping the end of an open subpath. + pub end_cap: Cap, + /// Lengths of dashes in alternating on/off order. + pub dash_pattern: Dashes, + /// Offset of the first dash. + pub dash_offset: f64, + /// True if the stroke width should be affected by the scale of a + /// transform. + /// + /// Discussion question: does this make sense here? + pub scale: bool, +} + +impl Default for Stroke { + fn default() -> Self { + Self { + width: 1.0, + join: Join::Round, + miter_limit: 4.0, + start_cap: Cap::Round, + end_cap: Cap::Round, + dash_pattern: Default::default(), + dash_offset: 0.0, + scale: true, + } + } +} + +impl Stroke { + /// Creates a new stroke with the specified width. + pub fn new(width: f64) -> Self { + Self { + width, + ..Default::default() + } + } + + /// Builder method for setting the join style. + pub fn with_join(mut self, join: Join) -> Self { + self.join = join; + self + } + + /// Builder method for setting the limit for miter joins. + pub fn with_miter_limit(mut self, limit: f64) -> Self { + self.miter_limit = limit; + self + } + + /// Builder method for setting the cap style for the start of the stroke. + pub fn with_start_cap(mut self, cap: Cap) -> Self { + self.start_cap = cap; + self + } + + /// Builder method for setting the cap style for the end of the stroke. + pub fn with_end_cap(mut self, cap: Cap) -> Self { + self.end_cap = cap; + self + } + + /// Builder method for setting the cap style. + pub fn with_caps(mut self, cap: Cap) -> Self { + self.start_cap = cap; + self.end_cap = cap; + self + } + + /// Builder method for setting the dashing parameters. + pub fn with_dashes

(mut self, offset: f64, pattern: P) -> Self + where + P: IntoIterator, + P::Item: Borrow, + { + self.dash_offset = offset; + self.dash_pattern.clear(); + self.dash_pattern + .extend(pattern.into_iter().map(|dash| *dash.borrow())); + self + } + + /// Builder method for setting whether or not the stroke should be affected + /// by the scale of any applied transform. + pub fn with_scale(mut self, yes: bool) -> Self { + self.scale = yes; + self + } +} + +/// Collection of values representing lengths in a dash pattern. +pub type Dashes = SmallVec<[f64; 4]>; + +/// Internal structure used for creating strokes. +#[derive(Default)] +struct StrokeCtx { + // Probably don't need both output and forward, can just concat + output: BezPath, + forward_path: BezPath, + backward_path: BezPath, + last_pt: Point, + last_tan: Vec2, +} + +/// Expand a stroke into a fill. +pub fn stroke( + path: impl IntoIterator, + style: &Stroke, + tolerance: f64, +) -> BezPath { + let mut ctx = StrokeCtx::default(); + for el in path { + let p0 = ctx.last_pt; + match el { + PathEl::MoveTo(p) => { + ctx.finish(); + ctx.last_pt = p; + } + PathEl::LineTo(p1) => { + if p1 != ctx.last_pt { + let tangent = p1 - p0; + ctx.do_tangents(style, tangent, tangent, p1); + ctx.do_line(style, tangent, p1); + } + } + PathEl::QuadTo(p1, p2) => { + if p1 != p0 && p2 != p0 { + let q = QuadBez::new(p0, p1, p2); + let (tan0, tan1) = PathSeg::Quad(q).tangents(); + ctx.do_tangents(style, tan0, tan1, p2); + ctx.do_cubic(style, q.raise(), tolerance); + } + } + PathEl::CurveTo(p1, p2, p3) => { + if p1 != p0 && p2 != p0 && p3 != p0 { + let c = CubicBez::new(p0, p1, p2, p3); + let (tan0, tan1) = PathSeg::Cubic(c).tangents(); + ctx.do_tangents(style, tan0, tan1, p3); + ctx.do_cubic(style, c, tolerance); + } + } + _ => todo!(), + } + } + todo!() +} + +fn get_end(el: &PathEl) -> Point { + match el { + PathEl::MoveTo(p) => *p, + PathEl::LineTo(p1) => *p1, + PathEl::QuadTo(_, p2) => *p2, + PathEl::CurveTo(_, _, p3) => *p3, + _ => unreachable!(), + } +} + +impl StrokeCtx { + /// Append forward and backward paths to output. + fn finish(&mut self) { + if self.forward_path.is_empty() { + return; + } + self.output.extend(&self.forward_path); + let back_els = self.backward_path.elements(); + // TODO: this is "butt" end, but we want to do other styles + self.output.line_to(get_end(&back_els[back_els.len() - 1])); + for i in (1..back_els.len()).rev() { + let end = get_end(&back_els[i - 1]); + match back_els[i] { + PathEl::LineTo(_) => self.output.line_to(end), + PathEl::QuadTo(p1, _) => self.output.quad_to(p1, end), + PathEl::CurveTo(p1, p2, _) => self.output.curve_to(p2, p1, end), + _ => unreachable!(), + } + } + // Same, this is butt end + self.output.close_path(); + + self.forward_path.truncate(0); + self.backward_path.truncate(0); + } + + fn do_tangents(&mut self, style: &Stroke, tan0: Vec2, tan1: Vec2, p1: Point) { + let scale = 0.5 * style.width / tan0.hypot(); + let norm = scale * Vec2::new(-tan0.y, tan0.x); + if self.forward_path.is_empty() { + self.forward_path.move_to(p1 + norm); + self.backward_path.move_to(p1 - norm); + } else { + // TODO: this represents miter joins, handle other styles + self.forward_path.line_to(p1 + norm); + self.backward_path.line_to(p1 - norm); + } + self.last_tan = tan1; + } + + fn do_line(&mut self, style: &Stroke, tangent: Vec2, p1: Point) { + let scale = 0.5 * style.width / tangent.hypot(); + let norm = scale * Vec2::new(-tangent.y, tangent.x); + self.forward_path.line_to(p1 + norm); + self.backward_path.line_to(p1 - norm); + self.last_pt = p1; + } + + fn do_cubic(&mut self, style: &Stroke, c: CubicBez, tolerance: f64) { + let co = CubicOffset::new(c, 0.5 * style.width); + let forward = fit_to_bezpath(&co, tolerance); + self.forward_path.extend(forward.into_iter().skip(1)); + let co = CubicOffset::new(c, -0.5 * style.width); + let backward = fit_to_bezpath(&co, tolerance); + self.backward_path.extend(backward.into_iter().skip(1)); + self.last_pt = c.p3; + } +}