mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-31 05:46:46 -06:00
M4: a dashed line starts and ends with a half-length dash (#1718)
This commit is contained in:
+2
-1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,114 @@
|
||||
//! A dashed line starts and ends with a half-length dash, whatever its pattern: a whole number of
|
||||
//! patterns fits the line, dashes and gaps scaled together. A closed path has no ends and runs its
|
||||
//! pattern as it is. The numbers in [`the_public_example_comes_out_the_same`] are from a public
|
||||
//! explanation of how the fit works.
|
||||
|
||||
use figcraft_doc::{StrokeAlign, StrokeProps, WidthProfile};
|
||||
use figcraft_geom::Point;
|
||||
use figcraft_geom::kurbo::{BezPath, Rect, Shape};
|
||||
|
||||
use crate::width::{dash_pattern, line_dashes, subpath_dashes};
|
||||
use crate::{stroke_center, stroke_outline};
|
||||
|
||||
fn line(len: f64) -> BezPath {
|
||||
let mut p = BezPath::new();
|
||||
p.move_to((0.0, 0.0));
|
||||
p.line_to((len, 0.0));
|
||||
p
|
||||
}
|
||||
|
||||
fn dashed(dashes: &[f64]) -> StrokeProps {
|
||||
StrokeProps { weight: 2.0, dashes: dashes.to_vec(), align: StrokeAlign::Center, ..Default::default() }
|
||||
}
|
||||
|
||||
fn covers(p: &BezPath, x: f64) -> bool {
|
||||
p.winding(Point::new(x, 0.0)) != 0
|
||||
}
|
||||
|
||||
/// Which of `xs` are on a dash.
|
||||
fn on(p: &BezPath, xs: &[f64]) -> Vec<bool> {
|
||||
xs.iter().map(|x| covers(p, *x)).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_line_starts_and_ends_with_half_a_dash() {
|
||||
// 10 on, 10 off along 100: 5 on, 10 off, 10 on, … 10 off, 5 on.
|
||||
let s = stroke_center(&line(100.0), 2.0, &dashed(&[10.0, 10.0]));
|
||||
assert_eq!(
|
||||
on(&s, &[2.0, 7.0, 13.0, 16.0, 24.0, 26.0, 84.0, 86.0, 94.0, 98.0]),
|
||||
[true, false, false, true, true, false, true, false, false, true]
|
||||
);
|
||||
assert!((s.area().abs() - 100.0).abs() < 0.01, "half of it is dashes: {}", s.area());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_whole_number_of_patterns_fits_the_line() {
|
||||
// 107 is 5 patterns and a bit: scaled to 21.4 each, so it still ends on half a dash.
|
||||
let (pattern, offset) = line_dashes(107.0, false, &[10.0, 10.0]);
|
||||
assert!((pattern[0] - 10.7).abs() < 1e-9 && (pattern[1] - 10.7).abs() < 1e-9, "{pattern:?}");
|
||||
assert!((offset - 5.35).abs() < 1e-9);
|
||||
let s = stroke_center(&line(107.0), 2.0, &dashed(&[10.0, 10.0]));
|
||||
assert_eq!(on(&s, &[5.0, 6.0, 101.0, 102.0]), [true, false, false, true]);
|
||||
// A line shorter than one pattern takes one, scaled down.
|
||||
assert_eq!(line_dashes(5.0, false, &[10.0, 10.0]), (vec![2.5, 2.5], 1.25));
|
||||
// A pattern that already fits isn't lost to rounding.
|
||||
assert_eq!(line_dashes(0.3, false, &[0.1, 0.05]).0.len(), 2);
|
||||
let (fitted, _) = line_dashes(3.0 * 0.15, false, &[0.1, 0.05]);
|
||||
assert!((fitted[0] - 0.1).abs() < 1e-9, "three patterns, not two: {fitted:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_public_example_comes_out_the_same() {
|
||||
// A 19 px line of [1, 2] draws six patterns of about 3.167.
|
||||
let (pattern, offset) = line_dashes(19.0, false, &[1.0, 2.0]);
|
||||
assert!((pattern[0] - 1.056).abs() < 1e-3 && (pattern[1] - 2.111).abs() < 1e-3, "{pattern:?}");
|
||||
assert!((offset - 0.528).abs() < 1e-3);
|
||||
}
|
||||
|
||||
/// Custom patterns too: the half is of the first dash, which the pattern comes round to at the end.
|
||||
#[test]
|
||||
fn a_custom_pattern_ends_on_half_its_first_dash() {
|
||||
let s = stroke_center(&line(100.0), 2.0, &dashed(&[12.0, 4.0, 2.0, 2.0]));
|
||||
// 6 on, 4 off, 2 on, 2 off, 12 on, …, and the last 6 on.
|
||||
assert_eq!(on(&s, &[3.0, 8.0, 11.0, 13.0, 20.0, 97.0, 93.0]), [true, false, true, false, true, true, false]);
|
||||
// An odd pattern draws repeated, so it fits as the doubled one.
|
||||
let doubled = dash_pattern(&dashed(&[4.0, 2.0, 1.0])).unwrap();
|
||||
assert_eq!(doubled, [4.0, 2.0, 1.0, 4.0, 2.0, 1.0]);
|
||||
let s = stroke_center(&line(70.0), 2.0, &dashed(&[4.0, 2.0, 1.0]));
|
||||
assert_eq!(on(&s, &[1.0, 3.0, 67.5, 69.0]), [true, false, false, true], "half of 4 at each end");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_closed_path_runs_its_pattern_as_it_is() {
|
||||
let rect = Rect::new(0.0, 0.0, 100.0, 50.0).to_path(0.1);
|
||||
assert_eq!(subpath_dashes(&rect, &[10.0, 7.0]), (vec![10.0, 7.0], 0.0));
|
||||
// The open one measures the same and is fitted.
|
||||
let (open, offset) = line_dashes(300.0, false, &[10.0, 7.0]);
|
||||
assert!(open[0] != 10.0 && offset > 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_subpath_fits_the_pattern_to_its_own_length() {
|
||||
let mut two = line(100.0);
|
||||
two.move_to((0.0, 50.0));
|
||||
two.line_to((107.0, 50.0));
|
||||
let s = stroke_center(&two, 2.0, &dashed(&[10.0, 10.0]));
|
||||
assert!(s.winding(Point::new(98.0, 0.0)) != 0 && s.winding(Point::new(102.0, 50.0)) != 0, "both end on half a dash");
|
||||
assert!(s.winding(Point::new(92.0, 0.0)) == 0 && s.winding(Point::new(99.0, 50.0)) == 0);
|
||||
}
|
||||
|
||||
/// A variable-width stroke is drawn by its own code: its dashes land in the same places.
|
||||
#[test]
|
||||
fn a_variable_width_stroke_dashes_the_same_way() {
|
||||
let props = StrokeProps { width_profile: Some(WidthProfile::uniform()), ..dashed(&[10.0, 10.0]) };
|
||||
let s = stroke_outline(&line(100.0), &props, false);
|
||||
assert_eq!(on(&s, &[2.0, 7.0, 13.0, 16.0, 98.0]), [true, false, false, true, true]);
|
||||
}
|
||||
|
||||
/// Nothing to fit: an empty line or a pattern of nothing stays as it is.
|
||||
#[test]
|
||||
fn nothing_to_fit_stays_as_it_is() {
|
||||
assert_eq!(line_dashes(0.0, false, &[10.0, 10.0]), (vec![10.0, 10.0], 0.0));
|
||||
assert_eq!(line_dashes(100.0, false, &[0.0, 0.0]), (vec![0.0, 0.0], 0.0));
|
||||
assert!(stroke_center(&BezPath::new(), 2.0, &dashed(&[10.0, 10.0])).elements().is_empty());
|
||||
}
|
||||
@@ -16,6 +16,8 @@ mod budget;
|
||||
#[cfg(test)]
|
||||
mod budget_tests;
|
||||
#[cfg(test)]
|
||||
mod dash_ends_tests;
|
||||
#[cfg(test)]
|
||||
mod dot_dash_tests;
|
||||
#[cfg(test)]
|
||||
mod ops_tests;
|
||||
@@ -337,7 +339,7 @@ pub fn stroke_center(path: &BezPath, width: f64, props: &StrokeProps) -> BezPath
|
||||
return BezPath::new();
|
||||
}
|
||||
let width = width.min(MAX_STROKE_WIDTH);
|
||||
let mut style = kurbo::Stroke::new(width)
|
||||
let style = kurbo::Stroke::new(width)
|
||||
.with_join(kjoin(props.join))
|
||||
.with_miter_limit(props.miter_limit.clamp(1.0, 100.0))
|
||||
.with_start_cap(kcap(props.cap_start))
|
||||
@@ -346,10 +348,17 @@ pub fn stroke_center(path: &BezPath, width: f64, props: &StrokeProps) -> BezPath
|
||||
let subpaths = path.elements().iter().filter(|e| matches!(e, PathEl::MoveTo(_))).count() as f64;
|
||||
let round = [props.cap_start, props.cap_end].into_iter().any(|c| matches!(kcap(c), Cap::Round));
|
||||
let pattern = width::dash_pattern(props).map(|p| p.into_iter().map(|d| if round && d == 0.0 { 1e-3 } else { d }).collect::<Vec<_>>());
|
||||
if let Some(pat) = pattern.filter(|p| width::dashes_fit(path.perimeter(1.0) + subpaths * p.iter().sum::<f64>(), p)) {
|
||||
style = style.with_dashes(0.0, pat);
|
||||
let opts = kurbo::StrokeOpts::default();
|
||||
let Some(pat) = pattern.filter(|p| width::dashes_fit(path.perimeter(1.0) + subpaths * p.iter().sum::<f64>(), p)) else {
|
||||
return kurbo::stroke(path.iter(), &style, &opts, 0.05);
|
||||
};
|
||||
// Each subpath fits the pattern to its own length.
|
||||
let mut out = BezPath::new();
|
||||
for sub in figcraft_geom::split_subpaths(path) {
|
||||
let (dashes, offset) = width::subpath_dashes(&sub, &pat);
|
||||
out.extend(kurbo::stroke(sub.iter(), &style.clone().with_dashes(offset, dashes), &opts, 0.05));
|
||||
}
|
||||
kurbo::stroke(path.iter(), &style, &kurbo::StrokeOpts::default(), 0.05)
|
||||
out
|
||||
}
|
||||
|
||||
/// The painted stroke area of a shape as a fill path (local coordinates). `closed` says whether the
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
//! fill it with the non-zero rule.
|
||||
|
||||
use figcraft_doc::{StrokeCap, StrokeJoin, StrokeProps, WidthProfile};
|
||||
use figcraft_geom::kurbo::{self, BezPath, Point, Vec2};
|
||||
use figcraft_geom::kurbo::{self, BezPath, PathEl, Point, Shape as _, Vec2};
|
||||
use figcraft_geom::measure::FLATTEN_TOL;
|
||||
/// The most points the outline works with; longer paths fall back to a uniform stroke.
|
||||
pub use figcraft_geom::measure::MAX_SAMPLES as MAX_WIDTH_SAMPLES;
|
||||
@@ -193,11 +193,12 @@ fn outline_piece(path: &mut BezPath, pts: &[Point], hw: &[Half], closed: bool, s
|
||||
push_contour(path, ring);
|
||||
}
|
||||
|
||||
/// Split a sampled polyline into dashes (`pattern` alternates dash and gap lengths).
|
||||
fn dashes(pts: &[Point], hw: &[Half], pattern: &[f64]) -> Vec<(Vec<Point>, Vec<Half>)> {
|
||||
/// Split a sampled polyline into dashes (`pattern` alternates dash and gap lengths), `offset` into
|
||||
/// the pattern at its start.
|
||||
fn dashes(pts: &[Point], hw: &[Half], pattern: &[f64], offset: f64) -> Vec<(Vec<Point>, Vec<Half>)> {
|
||||
let mut out = Vec::new();
|
||||
let Some(&first) = pattern.first() else { return out };
|
||||
let (mut idx, mut left, mut on) = (0usize, first, true);
|
||||
let (mut idx, mut left, mut on) = (0usize, (first - offset).max(0.0), true);
|
||||
let mut cur: (Vec<Point>, Vec<Half>) = (pts.iter().take(1).copied().collect(), hw.iter().take(1).copied().collect());
|
||||
for (p, h) in pts.windows(2).zip(hw.windows(2)) {
|
||||
let (a, b, ha, hb) = (p[0], p[1], h[0], h[1]);
|
||||
@@ -242,6 +243,32 @@ pub fn dash_pattern(props: &StrokeProps) -> Option<Vec<f64>> {
|
||||
Some(if dashes.len() % 2 == 1 { [dashes.clone(), dashes].concat() } else { dashes })
|
||||
}
|
||||
|
||||
/// How a line `len` long draws `pattern` (from [`dash_pattern`]): the pattern and how far into it
|
||||
/// the line starts. An open line starts and ends with a half-length dash, so a whole number of
|
||||
/// patterns fits it exactly, dashes and gaps scaled together, and it starts halfway through the
|
||||
/// first dash. A closed line has no ends: it runs the pattern as it is.
|
||||
pub fn line_dashes(len: f64, closed: bool, pattern: &[f64]) -> (Vec<f64>, f64) {
|
||||
let total: f64 = pattern.iter().sum();
|
||||
if closed || !(len > 1e-9 && total > 0.0) {
|
||||
return (pattern.to_vec(), 0.0);
|
||||
}
|
||||
// A hair under a whole number is that number: a pattern already fitted stays as it is, also
|
||||
// after a trip through a file that rounds it.
|
||||
let scale = len / ((len / total + 0.01).floor().max(1.0) * total);
|
||||
let fitted: Vec<f64> = pattern.iter().map(|d| d * scale).collect();
|
||||
let offset = fitted.first().map_or(0.0, |d| d / 2.0);
|
||||
(fitted, offset)
|
||||
}
|
||||
|
||||
/// [`line_dashes`] for a subpath.
|
||||
pub fn subpath_dashes(sub: &BezPath, pattern: &[f64]) -> (Vec<f64>, f64) {
|
||||
let closed = matches!(sub.elements().last(), Some(PathEl::ClosePath));
|
||||
line_dashes(sub.perimeter(DASH_ACCURACY), closed, pattern)
|
||||
}
|
||||
|
||||
/// How exactly a subpath is measured to fit its dashes (the stroke's own tolerance).
|
||||
const DASH_ACCURACY: f64 = 0.05;
|
||||
|
||||
/// Whether `pattern` (from [`dash_pattern`]) makes at most [`MAX_DASHES`] dashes along `len`;
|
||||
/// past that a stroke draws solid instead of one outline per dash (#1060).
|
||||
pub(crate) fn dashes_fit(len: f64, pattern: &[f64]) -> bool {
|
||||
@@ -301,7 +328,8 @@ pub fn variable_outline(path: &BezPath, width: f64, props: &StrokeProps, profile
|
||||
.collect();
|
||||
match &pattern {
|
||||
Some(pat) if dashes_fit(len, pat) => {
|
||||
for (p, h) in dashes(&pts, &hw, pat) {
|
||||
let (pat, offset) = line_dashes(len, line.closed, pat);
|
||||
for (p, h) in dashes(&pts, &hw, &pat, offset) {
|
||||
outline_piece(&mut out, &p, &h, false, style);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,11 +104,11 @@ fn joins_and_dashes() {
|
||||
let dashed = StrokeProps { dashes: vec![10.0, 10.0], ..props(2.0, Some(WidthProfile::uniform())) };
|
||||
let s = stroke_outline(&line(100.0), &dashed, false);
|
||||
assert!((s.area().abs() - 100.0).abs() < 1.0, "{}", s.area());
|
||||
assert!(covers(&s, 5.0, 0.0) && !covers(&s, 15.0, 0.0));
|
||||
assert!(covers(&s, 2.0, 0.0) && !covers(&s, 10.0, 0.0) && covers(&s, 20.0, 0.0));
|
||||
// Dashes keep the width of where they are on the path.
|
||||
let tapered = StrokeProps { dashes: vec![10.0, 10.0], ..props(10.0, WidthPreset::Wedge.profile()) };
|
||||
let s = stroke_outline(&line(100.0), &tapered, false);
|
||||
assert!(covers(&s, 5.0, 4.0) && !covers(&s, 85.0, 4.0));
|
||||
assert!(covers(&s, 2.0, 4.0) && !covers(&s, 80.0, 4.0));
|
||||
// Zero-length dashes with round caps are dots.
|
||||
let dots =
|
||||
StrokeProps { dashes: vec![0.0, 10.0], cap_start: StrokeCap::Round, cap_end: StrokeCap::Round, ..props(4.0, Some(WidthProfile::uniform())) };
|
||||
|
||||
@@ -529,7 +529,11 @@ pub(crate) fn stroke_props(s: &usvg::Stroke, k: f64) -> figcraft_doc::StrokeProp
|
||||
};
|
||||
let mut dashes: Vec<f64> = s.dasharray().unwrap_or(&[]).iter().map(|v| f64::from(*v) * k).collect();
|
||||
if !dashes.is_empty() {
|
||||
dashes = dash_offset(&dashes, f64::from(s.dashoffset()) * k, cap == StrokeCap::None).unwrap_or(dashes);
|
||||
let offset = f64::from(s.dashoffset()) * k;
|
||||
// Half a dash in is how a line starts by itself (and how export writes one).
|
||||
if dashes.first().is_none_or(|d| (d / 2.0 - offset).abs() > 1e-3) {
|
||||
dashes = dash_offset(&dashes, offset, cap == StrokeCap::None).unwrap_or(dashes);
|
||||
}
|
||||
}
|
||||
figcraft_doc::StrokeProps {
|
||||
weight: f64::from(s.width().get()) * k,
|
||||
|
||||
+19
-12
@@ -138,6 +138,20 @@ fn path_d(p: &BezPath) -> String {
|
||||
s
|
||||
}
|
||||
|
||||
/// A stroke's path as `d` values, each with its dash attributes. A dashed one takes a `<path>` for
|
||||
/// each subpath, as each fits the pattern to its own length (see
|
||||
/// [`figcraft_pathops::width::line_dashes`]), so a viewer draws what the canvas does.
|
||||
fn dashed_pieces(path: BezPath, k: &figcraft_doc::StrokeProps) -> Vec<(String, String)> {
|
||||
let list = |v: &[f64]| v.iter().map(|d| num(*d)).collect::<Vec<_>>().join(" ");
|
||||
let Some(pattern) = figcraft_pathops::width::dash_pattern(k) else { return vec![(path_d(&path), String::new())] };
|
||||
let piece = |sub: BezPath| {
|
||||
let (dashes, offset) = figcraft_pathops::width::subpath_dashes(&sub, &pattern);
|
||||
let start = if offset > 0.0 { format!(" stroke-dashoffset=\"{}\"", num(offset)) } else { String::new() };
|
||||
(path_d(&sub), format!(" stroke-dasharray=\"{}\"{start}", list(&dashes)))
|
||||
};
|
||||
figcraft_geom::split_subpaths(&path).into_iter().map(piece).collect()
|
||||
}
|
||||
|
||||
/// A matrix scale/shear coefficient to 6 significant digits (unlike [`num`], small values survive).
|
||||
fn coeff(v: f64) -> String {
|
||||
if !v.is_finite() || v == 0.0 {
|
||||
@@ -816,22 +830,15 @@ impl W<'_> {
|
||||
StrokeJoin::Round => " stroke-linejoin=\"round\"".into(),
|
||||
StrokeJoin::Bevel => " stroke-linejoin=\"bevel\"".into(),
|
||||
};
|
||||
let dash = if k.dashes.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" stroke-dasharray=\"{}\"", k.dashes.iter().map(|d| num(*d)).collect::<Vec<_>>().join(" "))
|
||||
};
|
||||
let pieces = dashed_pieces(path, k);
|
||||
let mut out = String::new();
|
||||
for p in &n.strokes {
|
||||
if let Some(a) = self.paint_attr(p, n.width, n.height) {
|
||||
let a = a.replace("fill-opacity=", "stroke-opacity=").replacen("fill=", "stroke=", 1);
|
||||
let _ = write!(
|
||||
out,
|
||||
"<path transform=\"{}\" d=\"{}\" fill=\"none\" {a} stroke-width=\"{}\"{cap}{join}{dash}/>",
|
||||
matrix(xf),
|
||||
path_d(&path),
|
||||
num(w)
|
||||
);
|
||||
for (d, dash) in &pieces {
|
||||
let _ =
|
||||
write!(out, "<path transform=\"{}\" d=\"{d}\" fill=\"none\" {a} stroke-width=\"{}\"{cap}{join}{dash}/>", matrix(xf), num(w));
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(out)
|
||||
|
||||
@@ -324,3 +324,61 @@ fn matrix_snaps_rotation_noise_and_tiny_values() {
|
||||
assert_eq!(matrix(r), "matrix(0 1 -1 0 0 0)");
|
||||
assert_eq!(matrix(Affine::scale(1e-320)), "matrix(0 0 0 0 0 0)");
|
||||
}
|
||||
|
||||
/// A dashed stroke's `<path>`s in `svg`, each as (dash pattern, dash offset).
|
||||
fn dash_attrs(svg: &str) -> Vec<(String, Option<String>)> {
|
||||
let attr = |e: &str, name: &str| e.split(&format!(" {name}=\"")).nth(1).and_then(|r| r.split('"').next()).map(str::to_string);
|
||||
svg.split("<path").filter_map(|e| Some((attr(e, "stroke-dasharray")?, attr(e, "stroke-dashoffset")))).collect()
|
||||
}
|
||||
|
||||
/// A page with one centred dashed stroke along `network`'s path or around a rectangle.
|
||||
fn dashed(kind: NodeKind, dashes: Vec<f64>) -> String {
|
||||
let mut d = Document::new("dashes");
|
||||
let p = d.first_page().unwrap();
|
||||
let n = d.create(p, kind, "Dashed", 0.0, 0.0, 100.0, 50.0).unwrap();
|
||||
let node = d.get_mut(n).unwrap();
|
||||
node.fills.clear();
|
||||
node.strokes = vec![Paint::solid(Color::from_hex("000000").unwrap())];
|
||||
node.stroke.align = figcraft_doc::StrokeAlign::Center;
|
||||
node.stroke.dashes = dashes;
|
||||
export_svg(&d, &[n], &SvgOptions { simplify_stroke: true, ..SvgOptions::default() }).unwrap()
|
||||
}
|
||||
|
||||
/// A viewer draws what the canvas does: an open line's dashes fitted and started half a dash in.
|
||||
#[test]
|
||||
fn a_dashed_line_exports_as_the_canvas_draws_it() {
|
||||
let line = Segment::line(0, 1);
|
||||
let open = VectorNetwork { vertices: vec![Vertex::new(0.0, 0.0), Vertex::new(107.0, 0.0)], segments: vec![line], ..Default::default() };
|
||||
let svg = dashed(NodeKind::Vector { network: open }, vec![10.0, 10.0]);
|
||||
assert_eq!(dash_attrs(&svg), [("10.7 10.7".to_string(), Some("5.35".to_string()))], "{svg}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_closed_dashed_path_exports_its_pattern_as_it_is() {
|
||||
let svg = dashed(NodeKind::Rectangle, vec![10.0, 7.0]);
|
||||
assert_eq!(dash_attrs(&svg), [("10 7".to_string(), None)], "{svg}");
|
||||
}
|
||||
|
||||
/// Each subpath fits the pattern to its own length, so each is a `<path>` of its own.
|
||||
#[test]
|
||||
fn each_subpath_of_a_dashed_vector_exports_its_own_fit() {
|
||||
let two = VectorNetwork {
|
||||
vertices: vec![Vertex::new(0.0, 0.0), Vertex::new(100.0, 0.0), Vertex::new(0.0, 20.0), Vertex::new(107.0, 20.0)],
|
||||
segments: vec![Segment::line(0, 1), Segment::line(2, 3)],
|
||||
..Default::default()
|
||||
};
|
||||
let svg = dashed(NodeKind::Vector { network: two }, vec![10.0, 10.0]);
|
||||
assert_eq!(dash_attrs(&svg).len(), 2, "{svg}");
|
||||
}
|
||||
|
||||
/// What export writes opens as the same line, the offset not counted twice.
|
||||
#[test]
|
||||
fn an_exported_dashed_line_opens_as_the_same_line() {
|
||||
let src = r##"<svg xmlns="http://www.w3.org/2000/svg" width="200" height="20"><line x2="107" stroke="#000" stroke-dasharray="10.7 10.7" stroke-dashoffset="5.35"/></svg>"##;
|
||||
let mut d = Document::new("back");
|
||||
let p = d.first_page().unwrap();
|
||||
let frame = import_svg(&mut d, src.as_bytes(), p, 0.0, 0.0, "Back").unwrap();
|
||||
let dashes: Vec<Vec<f64>> =
|
||||
d.subtree(frame).iter().filter_map(|i| d.node(*i)).filter(|n| !n.stroke.dashes.is_empty()).map(|n| n.stroke.dashes.clone()).collect();
|
||||
assert!(matches!(dashes.as_slice(), [d] if d.len() == 2 && d.iter().all(|v| (v - 10.7).abs() < 1e-4)), "{dashes:?}");
|
||||
}
|
||||
|
||||
@@ -7,9 +7,10 @@
|
||||
<polyline id="miter" points="20,100 50,70 80,100" fill="none" stroke="#000000" stroke-width="10" stroke-miterlimit="4"/>
|
||||
<polyline id="round-join" points="90,100 120,70 150,100" fill="none" stroke="#000000" stroke-width="10" stroke-linejoin="round"/>
|
||||
<polyline id="bevel" points="160,100 190,70 220,100" fill="none" stroke="#000000" stroke-width="10" stroke-linejoin="bevel"/>
|
||||
<!-- A dashed line starts and ends with half a dash: 10 on, 10 off, 20 on, 10 off, … -->
|
||||
<!-- probe 15 10 #000000ff -->
|
||||
<!-- probe 35 10 #00000000 -->
|
||||
<!-- probe 45 10 #000000ff -->
|
||||
<!-- probe 25 10 #00000000 -->
|
||||
<!-- probe 40 10 #000000ff -->
|
||||
<!-- probe 16 40 #ff0000ff -->
|
||||
<!-- probe 63 40 #ff0000ff -->
|
||||
<!-- probe 78 40 #00000000 -->
|
||||
|
||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
+2
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user