use crate::flatten::flatten;
use crate::path::{Path, PathBuilder};
use glam::Vec2;
#[derive(Debug, Clone, PartialEq)]
pub struct Dash {
pub intervals: Vec<f32>,
pub phase: f32,
}
impl Dash {
pub fn new(intervals: Vec<f32>, phase: f32) -> Self {
Self { intervals, phase }
}
pub fn is_usable(&self) -> bool {
!self.intervals.is_empty()
&& self.intervals.iter().all(|v| v.is_finite() && *v >= 0.0)
&& self.intervals.iter().sum::<f32>() > 0.0
&& self.phase.is_finite()
}
fn cycle(&self) -> Vec<f32> {
if self.intervals.len() % 2 == 0 {
self.intervals.clone()
} else {
let mut doubled = self.intervals.clone();
doubled.extend_from_slice(&self.intervals);
doubled
}
}
}
pub fn dash_path(path: &Path, dash: &Dash, tolerance: f32) -> Path {
if !dash.is_usable() {
return path.clone();
}
let cycle = dash.cycle();
let period: f32 = cycle.iter().sum();
let resolvable = period.is_finite() && period > tolerance;
if !resolvable {
return path.clone();
}
let mut builder = PathBuilder::new().with_fill_rule(path.fill_rule());
for polyline in flatten(path, tolerance) {
if polyline.len() < 2 {
continue;
}
walk(&polyline, &cycle, period, dash.phase, &mut builder);
}
builder.build()
}
fn walk(points: &[Vec2], cycle: &[f32], period: f32, phase: f32, out: &mut PathBuilder) {
let mut remaining = phase.rem_euclid(period);
let mut index = 0usize;
while remaining >= cycle[index] {
let next = remaining - cycle[index];
if next >= remaining {
break;
}
remaining = next;
index = (index + 1) % cycle.len();
}
let mut left = (cycle[index] - remaining).max(0.0);
let mut drawing = index % 2 == 0;
let mut pen_down = false;
for pair in points.windows(2) {
let (from, to) = (pair[0], pair[1]);
let segment = to - from;
let length = segment.length();
if !length.is_finite() || length <= 0.0 {
continue;
}
let mut traveled = 0.0f32;
while length - traveled > left {
let next = traveled + left;
if next <= traveled {
break;
}
traveled = next;
let at = from + segment * (traveled / length);
if drawing {
if !pen_down {
out.move_to(from + segment * ((traveled - left) / length));
}
out.line_to(at);
pen_down = false;
}
index = (index + 1) % cycle.len();
drawing = !drawing;
left = cycle[index];
if left <= 0.0 {
index = (index + 1) % cycle.len();
drawing = !drawing;
left = cycle[index];
}
}
let rest = length - traveled;
if drawing {
if !pen_down {
out.move_to(from + segment * (traveled / length));
pen_down = true;
}
out.line_to(to);
} else {
pen_down = false;
}
left -= rest;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn line(length: f32) -> Path {
let mut builder = PathBuilder::new();
builder.move_to(Vec2::ZERO);
builder.line_to(Vec2::new(length, 0.0));
builder.build()
}
fn drawn_length(path: &Path) -> f32 {
flatten(path, 0.25)
.iter()
.flat_map(|line| line.windows(2))
.map(|pair| (pair[1] - pair[0]).length())
.sum()
}
#[test]
fn a_pattern_draws_half_of_an_even_line() {
let dashed = dash_path(&line(100.0), &Dash::new(vec![10.0, 10.0], 0.0), 0.25);
assert!(
(drawn_length(&dashed) - 50.0).abs() < 0.01,
"drew {} of a hundred",
drawn_length(&dashed)
);
assert_eq!(flatten(&dashed, 0.25).len(), 5, "expected five dashes");
}
#[test]
fn an_odd_pattern_repeats_to_alternate() {
let dashed = dash_path(&line(100.0), &Dash::new(vec![10.0], 0.0), 0.25);
assert!(
(drawn_length(&dashed) - 50.0).abs() < 0.01,
"drew {}",
drawn_length(&dashed)
);
}
#[test]
fn the_phase_moves_the_pattern_along_the_line() {
let plain = dash_path(&line(100.0), &Dash::new(vec![10.0, 10.0], 0.0), 0.25);
let shifted = dash_path(&line(100.0), &Dash::new(vec![10.0, 10.0], 10.0), 0.25);
let first_of = |p: &Path| flatten(p, 0.25)[0][0].x;
assert!(first_of(&plain) < 0.01, "unshifted should start at zero");
assert!(
(first_of(&shifted) - 10.0).abs() < 0.01,
"a phase of ten should start ten along, not at {}",
first_of(&shifted)
);
}
#[test]
fn a_phase_beyond_one_period_wraps() {
let once = dash_path(&line(100.0), &Dash::new(vec![10.0, 10.0], 5.0), 0.25);
let again = dash_path(&line(100.0), &Dash::new(vec![10.0, 10.0], 25.0), 0.25);
assert_eq!(
flatten(&once, 0.25).len(),
flatten(&again, 0.25).len(),
"a phase one period further should repeat"
);
}
#[test]
fn a_dash_crossing_a_corner_stays_one_dash() {
let mut builder = PathBuilder::new();
builder.move_to(Vec2::ZERO);
builder.line_to(Vec2::new(10.0, 0.0));
builder.line_to(Vec2::new(10.0, 10.0));
let dashed = dash_path(&builder.build(), &Dash::new(vec![30.0, 5.0], 0.0), 0.25);
assert_eq!(
flatten(&dashed, 0.25).len(),
1,
"the dash was cut at the corner"
);
assert!((drawn_length(&dashed) - 20.0).abs() < 0.01);
}
#[test]
fn an_unusable_pattern_draws_the_path_whole() {
for intervals in [vec![], vec![0.0, 0.0], vec![-4.0, 2.0], vec![f32::NAN]] {
let dashed = dash_path(&line(100.0), &Dash::new(intervals.clone(), 0.0), 0.25);
assert!(
(drawn_length(&dashed) - 100.0).abs() < 0.01,
"{intervals:?} should draw the whole line, drew {}",
drawn_length(&dashed)
);
}
}
#[test]
fn a_zero_length_interval_inside_a_usable_pattern_terminates() {
let dashed = dash_path(
&line(100.0),
&Dash::new(vec![10.0, 0.0, 5.0, 5.0], 0.0),
0.25,
);
assert!(drawn_length(&dashed) > 0.0);
assert!(drawn_length(&dashed) < 100.0);
}
#[test]
fn a_pattern_longer_than_the_path_draws_what_it_reaches() {
let covered = dash_path(&line(10.0), &Dash::new(vec![100.0, 100.0], 0.0), 0.25);
assert!((drawn_length(&covered) - 10.0).abs() < 0.01);
let skipped = dash_path(&line(10.0), &Dash::new(vec![100.0, 100.0], 100.0), 0.25);
assert_eq!(drawn_length(&skipped), 0.0, "a line inside a gap drew");
}
#[test]
fn a_curve_is_dashed_along_its_length_rather_than_its_chord() {
let mut builder = PathBuilder::new();
builder.move_to(Vec2::new(10.0, 0.0));
builder.cubic_to(
Vec2::new(10.0, 5.523),
Vec2::new(5.523, 10.0),
Vec2::new(0.0, 10.0),
);
let arc = std::f32::consts::FRAC_PI_2 * 10.0;
let dashed = dash_path(&builder.build(), &Dash::new(vec![1.0, 1.0], 0.0), 0.05);
let drawn = drawn_length(&dashed);
assert!(
(drawn - arc / 2.0).abs() < 0.5,
"drew {drawn} of an arc of {arc}"
);
}
#[test]
fn a_pattern_finer_than_the_tolerance_is_left_solid() {
let path = line(100.0);
for interval in [f32::MIN_POSITIVE, 1e-30, 1e-20, 1e-9] {
let dash = Dash::new(vec![interval, interval], 0.0);
assert!(
dash.is_usable(),
"an interval of {interval:e} is what this test is about, and \
`is_usable` is expected to admit it"
);
let dashed = dash_path(&path, &dash, 0.1);
assert_eq!(
dashed.verbs().len(),
path.verbs().len(),
"an interval of {interval:e} is finer than the tolerance, so the \
path should come back as it went in"
);
}
let dashed = dash_path(&path, &Dash::new(vec![5.0, 5.0], 0.0), 0.1);
assert!(
dashed.verbs().len() > line(100.0).verbs().len(),
"a five-unit dash over a hundred units produced no extra verbs"
);
}
#[test]
fn an_odd_pattern_whose_doubled_cycle_overflows_is_refused() {
let path = line(100.0);
let dash = Dash::new(vec![f32::MAX, 118.494_29, 370.615_72], -1e20);
assert!(
dash.is_usable(),
"the intervals are finite, non-negative and sum above zero, which is \
the whole of what `is_usable` asks -- this test is about what it does \
not ask"
);
assert!(
!dash.cycle().iter().sum::<f32>().is_finite(),
"this case is only interesting while the doubled cycle overflows"
);
let dashed = dash_path(&path, &dash, 0.1);
assert_eq!(
dashed.verbs().len(),
path.verbs().len(),
"a pattern whose cycle does not sum to a length should come back \
undashed"
);
}
#[test]
fn a_phase_far_past_a_tiny_interval_still_settles() {
let path = line(100.0);
let dash = Dash::new(vec![f32::MIN_POSITIVE, 500.0], 499.999_97);
assert!(dash.is_usable());
let _ = dash_path(&path, &dash, 0.1);
}
}