137 lines
5.5 KiB
Rust
137 lines
5.5 KiB
Rust
//! Stroke geometry shared by the canvas, the PDF/SVG exporters and Outline Stroke, so every
|
|
//! consumer paints the same shapes:
|
|
//!
|
|
//! - [`stroke_pieces`]: the centre line (trimmed under the arrowheads) and the arrowheads as
|
|
//! filled outlines (hollow kinds are rings), placed by [`ArrowAlign`](vectorcraft_doc::ArrowAlign);
|
|
//! - [`dash`]: the dash pattern, exact or fitted to corners and path ends; zero-length dashes
|
|
//! become [`Dot`]s that a round or projecting cap turns into discs or squares ([`dot_outline`]);
|
|
//! - [`width_outline`]: variable-width (profile) strokes, with the stroke's joins at corners;
|
|
//! - [`line_outline`]: the line part of a stroke as one filled outline (profile, dashes, dots,
|
|
//! caps and joins);
|
|
//! - [`for_writer`] and [`outline_region`]: strokes for the SVG/PDF writers and Outline Stroke.
|
|
|
|
mod arrow;
|
|
mod dash;
|
|
mod outline;
|
|
mod width;
|
|
|
|
use std::borrow::Cow;
|
|
|
|
use kurbo::{BezPath, PathEl, PathSeg};
|
|
use vectorcraft_doc::{LineCap, LineJoin, StrokeAlign, StrokeLayer};
|
|
pub(crate) use vectorcraft_geom::bez::{kink, segments, subpaths, tangent, unit};
|
|
|
|
pub use arrow::Arrow;
|
|
pub use dash::{Dashed, Dot, dash, dot_outline};
|
|
pub use outline::{OUTLINE_TOL, Written, WrittenShape, for_writer, is_plain, outline_region};
|
|
pub use width::width_outline;
|
|
|
|
/// Arc-length accuracy for dashing and trimming (document points).
|
|
const ARCLEN_ACCURACY: f64 = 1e-6;
|
|
|
|
/// The geometry one stroke paints, in the coordinate space of its path.
|
|
#[derive(Clone, Debug)]
|
|
pub struct StrokePieces<'a> {
|
|
/// The centre line to stroke: the path with its ends trimmed under the arrowheads (borrowed
|
|
/// when nothing is trimmed). Empty when the heads cover the whole path.
|
|
pub line: Cow<'a, BezPath>,
|
|
/// The arrowheads, start first. Closed ends get none.
|
|
pub heads: Vec<Arrow>,
|
|
}
|
|
|
|
/// The centre line and arrowheads of stroke `st` along `bp`.
|
|
pub fn stroke_pieces<'a>(bp: &'a BezPath, st: &StrokeLayer) -> StrokePieces<'a> {
|
|
if st.start_arrow.is_none() && st.end_arrow.is_none() {
|
|
return StrokePieces { line: Cow::Borrowed(bp), heads: vec![] };
|
|
}
|
|
arrow::pieces(bp, st)
|
|
}
|
|
|
|
/// Does `bp` end with a `ClosePath`?
|
|
pub fn is_closed(bp: &BezPath) -> bool {
|
|
bp.elements().last().is_some_and(|e| matches!(e, PathEl::ClosePath))
|
|
}
|
|
|
|
/// The width actually stroked: inside/outside strokes of closed paths are drawn twice as wide
|
|
/// and clipped to one side; open paths always stroke centred.
|
|
pub fn aligned_width(st: &StrokeLayer, closed: bool) -> f64 {
|
|
match st.align {
|
|
StrokeAlign::Inside | StrokeAlign::Outside if closed => st.width * 2.0,
|
|
_ => st.width,
|
|
}
|
|
}
|
|
|
|
/// How far a cap reaches past the end of a stroke of `width`.
|
|
pub fn cap_extent(cap: LineCap, width: f64) -> f64 {
|
|
match cap {
|
|
LineCap::Butt => 0.0,
|
|
LineCap::Round | LineCap::Square => width / 2.0,
|
|
}
|
|
}
|
|
|
|
/// The stroke style (cap, join, miter limit) of `st` at `width`, without dashes.
|
|
pub fn style(st: &StrokeLayer, width: f64) -> kurbo::Stroke {
|
|
kurbo::Stroke::new(width)
|
|
.with_join(match st.join {
|
|
LineJoin::Miter => kurbo::Join::Miter,
|
|
LineJoin::Round => kurbo::Join::Round,
|
|
LineJoin::Bevel => kurbo::Join::Bevel,
|
|
})
|
|
.with_caps(match st.cap {
|
|
LineCap::Butt => kurbo::Cap::Butt,
|
|
LineCap::Round => kurbo::Cap::Round,
|
|
LineCap::Square => kurbo::Cap::Square,
|
|
})
|
|
.with_miter_limit(st.miter_limit)
|
|
}
|
|
|
|
/// The filled outline of the line part of `st` along `line` (usually [`StrokePieces::line`]) at
|
|
/// `width` (see [`aligned_width`]), flattened/fitted to `tol`: the dash pattern with its dots and
|
|
/// the width profile (each dash and dot as wide as the profile where it sits along the path), with
|
|
/// the stroke's caps and joins. Fill it with the non-zero rule. Arrowheads are not included.
|
|
pub fn line_outline(line: &BezPath, st: &StrokeLayer, width: f64, tol: f64) -> BezPath {
|
|
if line.elements().is_empty() || width <= 0.0 || !width.is_finite() {
|
|
return BezPath::new();
|
|
}
|
|
let tol = tol.max(1e-4);
|
|
let dashed = st.dash.as_ref().and_then(|d| dash(line, d));
|
|
match (st.profile.as_ref(), dashed) {
|
|
(Some(profile), None) => width_outline(line, width, profile, st, tol),
|
|
(Some(profile), Some(d)) => {
|
|
let mut out = width::outline_spans(&d.path, &d.spans, width, profile, st, tol);
|
|
for dot in &d.dots {
|
|
// The dot spans the profile's left and right widths there.
|
|
let (l, r) = profile.at(dot.t);
|
|
let (l, r) = (l.max(0.0), r.max(0.0));
|
|
let at = dot.at + width::left(dot.dir) * (width * (l - r) / 4.0);
|
|
out.extend(dot_outline(&[Dot { at, ..*dot }], width * (l + r) / 2.0, st.cap, tol).iter());
|
|
}
|
|
out
|
|
}
|
|
(None, Some(d)) => {
|
|
let mut out = kurbo::stroke(d.path.iter(), &style(st, width), &kurbo::StrokeOpts::default(), tol);
|
|
out.extend(dot_outline(&d.dots, width, st.cap, tol).iter());
|
|
out
|
|
}
|
|
(None, None) => kurbo::stroke(line.iter(), &style(st, width), &kurbo::StrokeOpts::default(), tol),
|
|
}
|
|
}
|
|
|
|
/// Append `seg` to `out` (which must already have a current point at its start).
|
|
pub(crate) fn push_seg(out: &mut BezPath, seg: &PathSeg) {
|
|
match seg {
|
|
PathSeg::Line(l) => out.line_to(l.p1),
|
|
PathSeg::Quad(q) => out.quad_to(q.p1, q.p2),
|
|
PathSeg::Cubic(c) => out.curve_to(c.p1, c.p2, c.p3),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests;
|
|
#[cfg(test)]
|
|
mod tests_arrows;
|
|
#[cfg(test)]
|
|
mod tests_fit;
|
|
#[cfg(test)]
|
|
mod tests_reach;
|