use core::{borrow::Borrow, f64::consts::PI};
use alloc::vec::Vec;
use smallvec::SmallVec;
#[cfg(not(feature = "std"))]
use crate::common::FloatFuncs;
use crate::{
common::solve_quadratic, fit_to_bezpath, fit_to_bezpath_opt, offset::CubicOffset, Affine, Arc,
BezPath, CubicBez, Line, ParamCurve, ParamCurveArclen, PathEl, PathSeg, Point, QuadBez, Vec2,
};
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Join {
Bevel,
Miter,
Round,
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Cap {
Butt,
Square,
Round,
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Stroke {
pub width: f64,
pub join: Join,
pub miter_limit: f64,
pub start_cap: Cap,
pub end_cap: Cap,
pub dash_pattern: Dashes,
pub dash_offset: f64,
}
pub struct StrokeOpts {
opt_level: StrokeOptLevel,
}
pub enum StrokeOptLevel {
Subdivide,
Optimized,
}
impl Default for StrokeOpts {
fn default() -> Self {
let opt_level = StrokeOptLevel::Subdivide;
StrokeOpts { opt_level }
}
}
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,
}
}
}
impl Stroke {
pub fn new(width: f64) -> Self {
Self {
width,
..Default::default()
}
}
pub fn with_join(mut self, join: Join) -> Self {
self.join = join;
self
}
pub fn with_miter_limit(mut self, limit: f64) -> Self {
self.miter_limit = limit;
self
}
pub fn with_start_cap(mut self, cap: Cap) -> Self {
self.start_cap = cap;
self
}
pub fn with_end_cap(mut self, cap: Cap) -> Self {
self.end_cap = cap;
self
}
pub fn with_caps(mut self, cap: Cap) -> Self {
self.start_cap = cap;
self.end_cap = cap;
self
}
pub fn with_dashes<P>(mut self, offset: f64, pattern: P) -> Self
where
P: IntoIterator,
P::Item: Borrow<f64>,
{
self.dash_offset = offset;
self.dash_pattern.clear();
self.dash_pattern
.extend(pattern.into_iter().map(|dash| *dash.borrow()));
self
}
}
impl StrokeOpts {
pub fn opt_level(mut self, opt_level: StrokeOptLevel) -> Self {
self.opt_level = opt_level;
self
}
}
pub type Dashes = SmallVec<[f64; 4]>;
#[derive(Default)]
struct StrokeCtx {
output: BezPath,
forward_path: BezPath,
backward_path: BezPath,
start_pt: Point,
start_norm: Vec2,
start_tan: Vec2,
last_pt: Point,
last_tan: Vec2,
join_thresh: f64,
}
pub fn stroke(
path: impl IntoIterator<Item = PathEl>,
style: &Stroke,
opts: &StrokeOpts,
tolerance: f64,
) -> BezPath {
if style.dash_pattern.is_empty() {
stroke_undashed(path, style, tolerance, opts)
} else {
let dashed = dash(path.into_iter(), style.dash_offset, &style.dash_pattern);
stroke_undashed(dashed, style, tolerance, opts)
}
}
fn stroke_undashed(
path: impl IntoIterator<Item = PathEl>,
style: &Stroke,
tolerance: f64,
opts: &StrokeOpts,
) -> BezPath {
let mut ctx = StrokeCtx {
join_thresh: 2.0 * tolerance / style.width,
..Default::default()
};
for el in path {
let p0 = ctx.last_pt;
match el {
PathEl::MoveTo(p) => {
ctx.finish(style);
ctx.start_pt = p;
ctx.last_pt = p;
}
PathEl::LineTo(p1) => {
if p1 != p0 {
let tangent = p1 - p0;
ctx.do_join(style, tangent);
ctx.last_tan = tangent;
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_join(style, tan0);
ctx.do_cubic(style, q.raise(), tolerance, opts);
ctx.last_tan = tan1;
}
}
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_join(style, tan0);
ctx.do_cubic(style, c, tolerance, opts);
ctx.last_tan = tan1;
}
}
PathEl::ClosePath => {
if p0 != ctx.start_pt {
let tangent = ctx.start_pt - p0;
ctx.do_join(style, tangent);
ctx.last_tan = tangent;
ctx.do_line(style, tangent, ctx.start_pt);
}
ctx.finish_closed(style);
}
}
}
ctx.finish(style);
ctx.output
}
fn round_cap(out: &mut BezPath, tolerance: f64, center: Point, norm: Vec2) {
round_join(out, tolerance, center, norm, PI);
}
fn round_join(out: &mut BezPath, tolerance: f64, center: Point, norm: Vec2, angle: f64) {
let a = Affine::new([norm.x, norm.y, -norm.y, norm.x, center.x, center.y]);
let arc = Arc::new(Point::ORIGIN, (1.0, 1.0), PI - angle, angle, 0.0);
arc.to_cubic_beziers(tolerance, |p1, p2, p3| out.curve_to(a * p1, a * p2, a * p3));
}
fn round_join_rev(out: &mut BezPath, tolerance: f64, center: Point, norm: Vec2, angle: f64) {
let a = Affine::new([norm.x, norm.y, norm.y, -norm.x, center.x, center.y]);
let arc = Arc::new(Point::ORIGIN, (1.0, 1.0), PI - angle, angle, 0.0);
arc.to_cubic_beziers(tolerance, |p1, p2, p3| out.curve_to(a * p1, a * p2, a * p3));
}
fn square_cap(out: &mut BezPath, close: bool, center: Point, norm: Vec2) {
let a = Affine::new([norm.x, norm.y, -norm.y, norm.x, center.x, center.y]);
out.line_to(a * Point::new(1.0, 1.0));
out.line_to(a * Point::new(-1.0, 1.0));
if close {
out.close_path();
} else {
out.line_to(a * Point::new(-1.0, 0.0));
}
}
fn extend_reversed(out: &mut BezPath, elements: &[PathEl]) {
for i in (1..elements.len()).rev() {
let end = elements[i - 1].end_point().unwrap();
match elements[i] {
PathEl::LineTo(_) => out.line_to(end),
PathEl::QuadTo(p1, _) => out.quad_to(p1, end),
PathEl::CurveTo(p1, p2, _) => out.curve_to(p2, p1, end),
_ => unreachable!(),
}
}
}
fn fit_with_opts(co: &CubicOffset, tolerance: f64, opts: &StrokeOpts) -> BezPath {
match opts.opt_level {
StrokeOptLevel::Subdivide => fit_to_bezpath(co, tolerance),
StrokeOptLevel::Optimized => fit_to_bezpath_opt(co, tolerance),
}
}
impl StrokeCtx {
fn finish(&mut self, style: &Stroke) {
let tolerance = 1e-3;
if self.forward_path.is_empty() {
return;
}
self.output.extend(&self.forward_path);
let back_els = self.backward_path.elements();
let return_p = back_els[back_els.len() - 1].end_point().unwrap();
let d = self.last_pt - return_p;
match style.end_cap {
Cap::Butt => self.output.line_to(return_p),
Cap::Round => round_cap(&mut self.output, tolerance, self.last_pt, d),
Cap::Square => square_cap(&mut self.output, false, self.last_pt, d),
}
extend_reversed(&mut self.output, back_els);
match style.start_cap {
Cap::Butt => self.output.close_path(),
Cap::Round => round_cap(&mut self.output, tolerance, self.start_pt, self.start_norm),
Cap::Square => square_cap(&mut self.output, true, self.start_pt, self.start_norm),
}
self.forward_path.truncate(0);
self.backward_path.truncate(0);
}
fn finish_closed(&mut self, style: &Stroke) {
if self.forward_path.is_empty() {
return;
}
self.do_join(style, self.start_tan);
self.output.extend(&self.forward_path);
self.output.close_path();
let back_els = self.backward_path.elements();
let last_pt = back_els[back_els.len() - 1].end_point().unwrap();
self.output.move_to(last_pt);
extend_reversed(&mut self.output, back_els);
self.output.close_path();
self.forward_path.truncate(0);
self.backward_path.truncate(0);
}
fn do_join(&mut self, style: &Stroke, tan0: Vec2) {
let tolerance = 1e-3;
let scale = 0.5 * style.width / tan0.hypot();
let norm = scale * Vec2::new(-tan0.y, tan0.x);
let p0 = self.last_pt;
if self.forward_path.elements().is_empty() {
self.forward_path.move_to(p0 - norm);
self.backward_path.move_to(p0 + norm);
self.start_tan = tan0;
self.start_norm = norm;
} else {
let ab = self.last_tan;
let cd = tan0;
let cross = ab.cross(cd);
let dot = ab.dot(cd);
let hypot = cross.hypot(dot);
if dot <= 0.0 || cross.abs() >= hypot * self.join_thresh {
match style.join {
Join::Bevel => {
self.forward_path.line_to(p0 - norm);
self.backward_path.line_to(p0 + norm);
}
Join::Miter => {
if 2.0 * hypot < (hypot + dot) * style.miter_limit.powi(2) {
let last_scale = 0.5 * style.width / ab.hypot();
let last_norm = last_scale * Vec2::new(-ab.y, ab.x);
if cross > 0.0 {
let fp_last = p0 - last_norm;
let fp_this = p0 - norm;
let h = ab.cross(fp_this - fp_last) / cross;
let miter_pt = fp_this - cd * h;
self.forward_path.line_to(miter_pt);
} else if cross < 0.0 {
let fp_last = p0 + last_norm;
let fp_this = p0 + norm;
let h = ab.cross(fp_this - fp_last) / cross;
let miter_pt = fp_this - cd * h;
self.backward_path.line_to(miter_pt);
}
}
self.forward_path.line_to(p0 - norm);
self.backward_path.line_to(p0 + norm);
}
Join::Round => {
let angle = cross.atan2(dot);
if angle > 0.0 {
self.backward_path.line_to(p0 + norm);
round_join(&mut self.forward_path, tolerance, p0, norm, angle);
} else {
self.forward_path.line_to(p0 - norm);
round_join_rev(&mut self.backward_path, tolerance, p0, -norm, -angle);
}
}
}
}
}
}
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, opts: &StrokeOpts) {
let chord = c.p3 - c.p0;
let mut chord_ref = chord;
let mut chord_ref_hypot2 = chord_ref.hypot2();
let d01 = c.p1 - c.p0;
if d01.hypot2() > chord_ref_hypot2 {
chord_ref = d01;
chord_ref_hypot2 = chord_ref.hypot2();
}
let d23 = c.p3 - c.p2;
if d23.hypot2() > chord_ref_hypot2 {
chord_ref = d23;
chord_ref_hypot2 = chord_ref.hypot2();
}
let p0 = c.p0.to_vec2().dot(chord_ref);
let p1 = c.p1.to_vec2().dot(chord_ref);
let p2 = c.p2.to_vec2().dot(chord_ref);
let p3 = c.p3.to_vec2().dot(chord_ref);
const ENDPOINT_D: f64 = 0.01;
if p3 <= p0
|| p1 > p2
|| p1 < p0 + ENDPOINT_D * (p3 - p0)
|| p2 > p3 - ENDPOINT_D * (p3 - p0)
{
let x01 = d01.cross(chord_ref);
let x23 = d23.cross(chord_ref);
let x03 = chord.cross(chord_ref);
let thresh = tolerance.powi(2) * chord_ref_hypot2;
if x01 * x01 < thresh && x23 * x23 < thresh && x03 * x03 < thresh {
let midpoint = c.p0.midpoint(c.p3);
let ref_vec = chord_ref / chord_ref_hypot2;
let ref_pt = midpoint - 0.5 * (p0 + p3) * ref_vec;
self.do_linear(style, c, [p0, p1, p2, p3], ref_pt, ref_vec);
return;
}
}
const DIM_TUNE: f64 = 0.25;
let dimension = tolerance * DIM_TUNE;
let co = CubicOffset::new_regularized(c, -0.5 * style.width, dimension);
let forward = fit_with_opts(&co, tolerance, opts);
self.forward_path.extend(forward.into_iter().skip(1));
let co = CubicOffset::new_regularized(c, 0.5 * style.width, dimension);
let backward = fit_with_opts(&co, tolerance, opts);
self.backward_path.extend(backward.into_iter().skip(1));
self.last_pt = c.p3;
}
fn do_linear(
&mut self,
style: &Stroke,
c: CubicBez,
p: [f64; 4],
ref_pt: Point,
ref_vec: Vec2,
) {
let style = Stroke::new(style.width).with_join(Join::Round);
let (tan0, tan1) = PathSeg::Cubic(c).tangents();
self.last_tan = tan0;
let c0 = p[1] - p[0];
let c1 = 2.0 * p[2] - 4.0 * p[1] + 2.0 * p[0];
let c2 = p[3] - 3.0 * p[2] + 3.0 * p[1] - p[0];
let roots = solve_quadratic(c0, c1, c2);
const EPSILON: f64 = 1e-6;
for t in roots {
if t > EPSILON && t < 1.0 - EPSILON {
let mt = 1.0 - t;
let z = mt * (mt * mt * p[0] + 3.0 * t * (mt * p[1] + t * p[2])) + t * t * t * p[3];
let p = ref_pt + z * ref_vec;
let tan = p - self.last_pt;
self.do_join(&style, tan);
self.do_line(&style, tan, p);
self.last_tan = tan;
}
}
let tan = c.p3 - self.last_pt;
self.do_join(&style, tan);
self.do_line(&style, tan, c.p3);
self.last_tan = tan;
self.do_join(&style, tan1);
}
}
#[doc(hidden)]
pub struct DashIterator<'a, T> {
inner: T,
input_done: bool,
closepath_pending: bool,
dashes: &'a [f64],
dash_ix: usize,
init_dash_ix: usize,
init_dash_remaining: f64,
init_is_active: bool,
is_active: bool,
state: DashState,
current_seg: PathSeg,
t: f64,
dash_remaining: f64,
seg_remaining: f64,
start_pt: Point,
last_pt: Point,
stash: Vec<PathEl>,
stash_ix: usize,
}
#[derive(PartialEq, Eq)]
enum DashState {
NeedInput,
ToStash,
Working,
FromStash,
}
impl<'a, T: Iterator<Item = PathEl>> Iterator for DashIterator<'a, T> {
type Item = PathEl;
fn next(&mut self) -> Option<PathEl> {
loop {
match self.state {
DashState::NeedInput => {
if self.input_done {
return None;
}
self.get_input();
if self.input_done {
return None;
}
self.state = DashState::ToStash;
}
DashState::ToStash => {
if let Some(el) = self.step() {
self.stash.push(el);
}
}
DashState::Working => {
if let Some(el) = self.step() {
return Some(el);
}
}
DashState::FromStash => {
if let Some(el) = self.stash.get(self.stash_ix) {
self.stash_ix += 1;
return Some(*el);
} else {
self.stash.clear();
self.stash_ix = 0;
if self.input_done {
return None;
}
if self.closepath_pending {
self.closepath_pending = false;
self.state = DashState::NeedInput;
} else {
self.state = DashState::ToStash;
}
}
}
}
}
}
}
fn seg_to_el(el: &PathSeg) -> PathEl {
match el {
PathSeg::Line(l) => PathEl::LineTo(l.p1),
PathSeg::Quad(q) => PathEl::QuadTo(q.p1, q.p2),
PathSeg::Cubic(c) => PathEl::CurveTo(c.p1, c.p2, c.p3),
}
}
const DASH_ACCURACY: f64 = 1e-6;
pub fn dash<'a>(
inner: impl Iterator<Item = PathEl> + 'a,
dash_offset: f64,
dashes: &'a [f64],
) -> impl Iterator<Item = PathEl> + 'a {
dash_impl(inner, dash_offset, dashes)
}
fn dash_impl<T: Iterator<Item = PathEl>>(
inner: T,
dash_offset: f64,
dashes: &[f64],
) -> DashIterator<T> {
let mut dash_ix = 0;
let mut dash_remaining = dashes[dash_ix] - dash_offset;
let mut is_active = true;
while dash_remaining < 0.0 {
dash_ix = (dash_ix + 1) % dashes.len();
dash_remaining += dashes[dash_ix];
is_active = !is_active;
}
DashIterator {
inner,
input_done: false,
closepath_pending: false,
dashes,
dash_ix,
init_dash_ix: dash_ix,
init_dash_remaining: dash_remaining,
init_is_active: is_active,
is_active,
state: DashState::NeedInput,
current_seg: PathSeg::Line(Line::new(Point::ORIGIN, Point::ORIGIN)),
t: 0.0,
dash_remaining,
seg_remaining: 0.0,
start_pt: Point::ORIGIN,
last_pt: Point::ORIGIN,
stash: Vec::new(),
stash_ix: 0,
}
}
impl<'a, T: Iterator<Item = PathEl>> DashIterator<'a, T> {
#[doc(hidden)]
#[deprecated(since = "0.10.4", note = "use dash() instead")]
pub fn new(inner: T, dash_offset: f64, dashes: &'a [f64]) -> Self {
dash_impl(inner, dash_offset, dashes)
}
fn get_input(&mut self) {
loop {
if self.closepath_pending {
self.handle_closepath();
break;
}
let Some(next_el) = self.inner.next() else {
self.input_done = true;
self.state = DashState::FromStash;
return;
};
let p0 = self.last_pt;
match next_el {
PathEl::MoveTo(p) => {
if !self.stash.is_empty() {
self.state = DashState::FromStash;
}
self.start_pt = p;
self.last_pt = p;
self.reset_phase();
continue;
}
PathEl::LineTo(p1) => {
let l = Line::new(p0, p1);
self.seg_remaining = l.arclen(DASH_ACCURACY);
self.current_seg = PathSeg::Line(l);
self.last_pt = p1;
}
PathEl::QuadTo(p1, p2) => {
let q = QuadBez::new(p0, p1, p2);
self.seg_remaining = q.arclen(DASH_ACCURACY);
self.current_seg = PathSeg::Quad(q);
self.last_pt = p2;
}
PathEl::CurveTo(p1, p2, p3) => {
let c = CubicBez::new(p0, p1, p2, p3);
self.seg_remaining = c.arclen(DASH_ACCURACY);
self.current_seg = PathSeg::Cubic(c);
self.last_pt = p3;
}
PathEl::ClosePath => {
self.closepath_pending = true;
if p0 != self.start_pt {
let l = Line::new(p0, self.start_pt);
self.seg_remaining = l.arclen(DASH_ACCURACY);
self.current_seg = PathSeg::Line(l);
self.last_pt = self.start_pt;
} else {
self.handle_closepath();
}
}
}
break;
}
self.t = 0.0;
}
fn step(&mut self) -> Option<PathEl> {
let mut result = None;
if self.state == DashState::ToStash && self.stash.is_empty() {
if self.is_active {
result = Some(PathEl::MoveTo(self.current_seg.start()));
} else {
self.state = DashState::Working;
}
} else if self.dash_remaining < self.seg_remaining {
let seg = self.current_seg.subsegment(self.t..1.0);
let t1 = seg.inv_arclen(self.dash_remaining, DASH_ACCURACY);
if self.is_active {
let subseg = seg.subsegment(0.0..t1);
result = Some(seg_to_el(&subseg));
self.state = DashState::Working;
} else {
let p = seg.eval(t1);
result = Some(PathEl::MoveTo(p));
}
self.is_active = !self.is_active;
self.t += t1 * (1.0 - self.t);
self.seg_remaining -= self.dash_remaining;
self.dash_ix += 1;
if self.dash_ix == self.dashes.len() {
self.dash_ix = 0;
}
self.dash_remaining = self.dashes[self.dash_ix];
} else {
if self.is_active {
let seg = self.current_seg.subsegment(self.t..1.0);
result = Some(seg_to_el(&seg));
}
self.dash_remaining -= self.seg_remaining;
self.get_input();
}
result
}
fn handle_closepath(&mut self) {
if self.state == DashState::ToStash {
self.stash.push(PathEl::ClosePath);
} else if self.is_active {
self.stash_ix = 1;
}
self.state = DashState::FromStash;
self.reset_phase();
}
fn reset_phase(&mut self) {
self.dash_ix = self.init_dash_ix;
self.dash_remaining = self.init_dash_remaining;
self.is_active = self.init_is_active;
}
}
#[cfg(test)]
mod tests {
use crate::{
dash, segments, stroke, Cap::Butt, CubicBez, Join::Miter, Line, PathSeg, Shape, Stroke,
};
#[test]
fn pathological_stroke() {
let curve = CubicBez::new(
(602.469, 286.585),
(641.975, 286.585),
(562.963, 286.585),
(562.963, 286.585),
);
let path = curve.into_path(0.1);
let stroke_style = Stroke::new(1.);
let stroked = stroke(path, &stroke_style, &Default::default(), 0.001);
assert!(stroked.is_finite());
}
#[test]
fn broken_strokes() {
let broken_cubics = [
[
(465.24423, 107.11105),
(475.50754, 107.11105),
(475.50754, 107.11105),
(475.50754, 107.11105),
],
[(0., -0.01), (128., 128.001), (128., -0.01), (0., 128.001)], [(0., 0.), (0., -10.), (0., -10.), (0., 10.)], [(10., 0.), (0., 0.), (20., 0.), (10., 0.)], [(39., -39.), (40., -40.), (40., -40.), (0., 0.)], [(40., 40.), (0., 0.), (200., 200.), (0., 0.)], [(0., 0.), (1e-2, 0.), (-1e-2, 0.), (0., 0.)], [
(400.75, 100.05),
(400.75, 100.05),
(100.05, 300.95),
(100.05, 300.95),
],
[(0.5, 0.), (0., 0.), (20., 0.), (10., 0.)], [(10., 0.), (0., 0.), (10., 0.), (10., 0.)], ];
let stroke_style = Stroke::new(30.).with_caps(Butt).with_join(Miter);
for cubic in &broken_cubics {
let path = CubicBez::new(cubic[0], cubic[1], cubic[2], cubic[3]).into_path(0.1);
let stroked = stroke(path, &stroke_style, &Default::default(), 0.001);
assert!(stroked.is_finite());
}
}
#[test]
fn dash_sequence() {
let shape = Line::new((0.0, 0.0), (21.0, 0.0));
let dashes = [1., 5., 2., 5.];
let expansion = [
PathSeg::Line(Line::new((6., 0.), (8., 0.))),
PathSeg::Line(Line::new((13., 0.), (14., 0.))),
PathSeg::Line(Line::new((19., 0.), (21., 0.))),
PathSeg::Line(Line::new((0., 0.), (1., 0.))),
];
let iter = segments(dash(shape.path_elements(0.), 0., &dashes));
assert_eq!(iter.collect::<Vec<PathSeg>>(), expansion);
}
#[test]
fn dash_sequence_offset() {
let shape = Line::new((0.0, 0.0), (21.0, 0.0));
let dashes = [1., 5., 2., 5.];
let expansion = [
PathSeg::Line(Line::new((3., 0.), (5., 0.))),
PathSeg::Line(Line::new((10., 0.), (11., 0.))),
PathSeg::Line(Line::new((16., 0.), (18., 0.))),
];
let iter = segments(dash(shape.path_elements(0.), 3., &dashes));
assert_eq!(iter.collect::<Vec<PathSeg>>(), expansion);
}
}