mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-08 09:31:03 +00:00
Render: a long dashed stroke is outlined only near the view after an edit
A dashed stroke of a path with more than 1,024 path elements was outlined whole in every frame after an edit. It is now outlined in and around the view like a solid stroke. dash_kept runs the pattern along every segment and keeps only the dashes and dots on the segments near the view, so they sit where dashing the whole path puts them and the pixels are the same. A pattern too fine to draw is still drawn solid, and that solid stroke is cropped too. At 1600 x 1200 the frame after one anchor moves takes 14 ms instead of 102 ms on a filled closed path of 50,000 anchors with 6 pt dashes and 4 pt gaps. anchor_bench now includes that document.
This commit is contained in:
@@ -24,7 +24,7 @@ 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 dash::{Dashed, Dot, dash, dash_kept, dot_outline};
|
||||
pub use gradient::{Slice, WrittenSlices, gradient_meshes, gradient_slices, written_slices};
|
||||
pub use outline::{OUTLINE_TOL, Written, WrittenShape, for_writer, is_plain, outline_region};
|
||||
pub use width::width_outline;
|
||||
@@ -111,11 +111,7 @@ pub fn line_outline(line: &BezPath, st: &StrokeLayer, width: f64, tol: f64) -> B
|
||||
}
|
||||
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, Some(d)) => dashed_outline(&d, st, width, tol),
|
||||
(None, None) => {
|
||||
let mut out = plain_outline(line, &style(st, width), tol);
|
||||
out.extend(dot_outline(&point_dots(line), width, st.cap, tol).iter());
|
||||
@@ -124,6 +120,28 @@ pub fn line_outline(line: &BezPath, st: &StrokeLayer, width: f64, tol: f64) -> B
|
||||
}
|
||||
}
|
||||
|
||||
/// [`line_outline`] of a dashed line without a width profile, made of only the dashes and dots on
|
||||
/// the segments of `line` that `keep` accepts ([`dash_kept`]). `None` when `st` has no dash
|
||||
/// pattern or has a width profile, or when its pattern draws the line solid.
|
||||
pub fn dashed_line_outline_kept(line: &BezPath, st: &StrokeLayer, width: f64, tol: f64, keep: &dyn Fn(&PathSeg) -> bool) -> Option<BezPath> {
|
||||
if st.profile.is_some() {
|
||||
return None;
|
||||
}
|
||||
let d = dash_kept(line, st.dash.as_ref()?, keep)?;
|
||||
if width <= 0.0 || !width.is_finite() {
|
||||
return Some(BezPath::new());
|
||||
}
|
||||
Some(dashed_outline(&d, st, width, tol.max(1e-4)))
|
||||
}
|
||||
|
||||
/// The filled outline of the dashes and dots `d` of a stroke `st` without a width profile, at
|
||||
/// `width` and flattened/fitted to `tol`.
|
||||
fn dashed_outline(d: &Dashed, st: &StrokeLayer, width: f64, tol: f64) -> BezPath {
|
||||
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
|
||||
}
|
||||
|
||||
/// The subpaths of `line` that never leave their first point (`m` then `L` to the same point, as
|
||||
/// `.ai` files write dots, #1613), as dots: kurbo strokes nothing for them, where PDF and SVG
|
||||
/// viewers paint the cap. They lie along the x axis, which a projecting cap's square follows
|
||||
|
||||
@@ -45,6 +45,14 @@ pub struct Dashed {
|
||||
/// entry, as in SVG and PDF) or has no positive length (the line is solid), or is too fine to draw
|
||||
/// (then the line is drawn solid too).
|
||||
pub fn dash(bp: &BezPath, d: &Dash) -> Option<Dashed> {
|
||||
dash_kept(bp, d, &|_| true)
|
||||
}
|
||||
|
||||
/// [`dash`] with only the dashes and dots on the segments of `bp` that `keep` accepts. The pattern
|
||||
/// runs along every segment as in [`dash`], so a kept dash lies where [`dash`] puts it: a dash
|
||||
/// running onto a segment left out ends where that segment starts and goes on from where the next
|
||||
/// kept segment starts. Segments left out still count toward the limit on the number of dashes.
|
||||
pub fn dash_kept(bp: &BezPath, d: &Dash, keep: &dyn Fn(&PathSeg) -> bool) -> Option<Dashed> {
|
||||
if d.pattern.iter().any(|v| !v.is_finite() || *v < 0.0) || !d.offset.is_finite() {
|
||||
return None;
|
||||
}
|
||||
@@ -82,7 +90,7 @@ pub fn dash(bp: &BezPath, d: &Dash) -> Option<Dashed> {
|
||||
let phase = Phase { ix: ix0, rem: pat[ix0] - off, on: ix0 % 2 == 0 };
|
||||
let mut out = Dashed::default();
|
||||
for plan in &plans {
|
||||
dash_subpath(&mut out, plan, &pat, phase);
|
||||
dash_subpath(&mut out, plan, &pat, phase, keep);
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
@@ -175,7 +183,7 @@ impl Piece {
|
||||
}
|
||||
}
|
||||
|
||||
fn dash_subpath(out: &mut Dashed, plan: &Plan, pat: &[f64], mut ph: Phase) {
|
||||
fn dash_subpath(out: &mut Dashed, plan: &Plan, pat: &[f64], mut ph: Phase, keep: &dyn Fn(&PathSeg) -> bool) {
|
||||
let first_dot = out.dots.len();
|
||||
let total: f64 = plan.lens.iter().sum();
|
||||
// Fraction of the subpath's length at `x` along the plan.
|
||||
@@ -188,7 +196,7 @@ fn dash_subpath(out: &mut Dashed, plan: &Plan, pat: &[f64], mut ph: Phase) {
|
||||
};
|
||||
let mut dashes: Vec<Piece> = vec![];
|
||||
let mut cur = Piece::default();
|
||||
let (mut cuts, mut acc, mut starts_on) = (0, 0.0, false);
|
||||
let (mut cuts, mut acc, mut starts_on, mut left_out) = (0, 0.0, false, false);
|
||||
for (i, (seg, &len)) in plan.segs.iter().zip(&plan.lens).enumerate() {
|
||||
let k = plan.scale[i];
|
||||
if plan.restart[i] {
|
||||
@@ -199,21 +207,30 @@ fn dash_subpath(out: &mut Dashed, plan: &Plan, pat: &[f64], mut ph: Phase) {
|
||||
}
|
||||
ph = Phase { ix: 0, rem: pat[0] * k / 2.0, on: true };
|
||||
}
|
||||
// A segment left out ends the dash running onto it; the pattern still advances along it.
|
||||
let kept = keep(seg);
|
||||
if !kept {
|
||||
left_out = true;
|
||||
if !cur.segs.is_empty() {
|
||||
cur.end = acc;
|
||||
dashes.push(std::mem::take(&mut cur));
|
||||
}
|
||||
}
|
||||
if i == 0 {
|
||||
starts_on = ph.on && ph.rem > 0.0;
|
||||
starts_on = kept && ph.on && ph.rem > 0.0;
|
||||
}
|
||||
let mut s = 0.0;
|
||||
loop {
|
||||
let left = len - s;
|
||||
if ph.rem > left + EPS {
|
||||
if ph.on && left > EPS {
|
||||
if kept && ph.on && left > EPS {
|
||||
cur.push(sub(seg, len, s, len), acc + s);
|
||||
}
|
||||
ph.rem -= left;
|
||||
break;
|
||||
}
|
||||
let end = (s + ph.rem).min(len);
|
||||
if ph.on {
|
||||
if kept && ph.on {
|
||||
if pat[ph.ix] > 0.0 {
|
||||
if end > s {
|
||||
cur.push(sub(seg, len, s, end), acc + s);
|
||||
@@ -240,6 +257,7 @@ fn dash_subpath(out: &mut Dashed, plan: &Plan, pat: &[f64], mut ph: Phase) {
|
||||
&& plan.restart[0]
|
||||
&& pat[0] <= 0.0
|
||||
&& let Some(seg) = plan.segs.last()
|
||||
&& keep(seg)
|
||||
{
|
||||
push_dot(out, first_dot, Dot { at: seg.end(), dir: tangent(seg, 1.0), t: 1.0 });
|
||||
}
|
||||
@@ -249,7 +267,7 @@ fn dash_subpath(out: &mut Dashed, plan: &Plan, pat: &[f64], mut ph: Phase) {
|
||||
dashes.push(cur);
|
||||
}
|
||||
if plan.closed {
|
||||
if cuts == 0 && ends_on {
|
||||
if cuts == 0 && ends_on && !left_out {
|
||||
// One dash all the way round: keep it closed so the start gets a join, not caps.
|
||||
let d = dashes.pop().unwrap_or_default();
|
||||
write(&mut out.path, &d.segs);
|
||||
|
||||
@@ -110,6 +110,80 @@ fn dashes_follow_arc_length_and_join_across_the_start_of_closed_paths() {
|
||||
assert!(dash(&line(0.0, 1e6), &dashed(&[0.001, 0.001], 0.0)).is_none());
|
||||
}
|
||||
|
||||
/// The dashes of `d` as (start x, end x), for dashes along the x axis.
|
||||
fn spans_x(d: &Dashed) -> Vec<(f64, f64)> {
|
||||
let mut out = vec![];
|
||||
for el in d.path.elements() {
|
||||
match el {
|
||||
PathEl::MoveTo(p) => out.push((p.x, p.x)),
|
||||
PathEl::LineTo(p) => {
|
||||
if let Some(last) = out.last_mut() {
|
||||
last.1 = p.x;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Ten segments of 10 pt along the x axis from 0 to 100.
|
||||
fn steps() -> BezPath {
|
||||
let mut b = BezPath::new();
|
||||
b.move_to((0.0, 0.0));
|
||||
for i in 1..=10 {
|
||||
b.line_to((f64::from(i) * 10.0, 0.0));
|
||||
}
|
||||
b
|
||||
}
|
||||
|
||||
/// Whether a segment lies outside x 30..60.
|
||||
fn outside(s: &PathSeg) -> bool {
|
||||
s.start().x >= 60.0 || s.end().x <= 30.0
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dashes_kept_on_some_segments_keep_the_phase_of_the_whole_line() {
|
||||
// A period of 7 starting 2 into it: dashes at -2..2, 5..9, 12..16, …
|
||||
let pattern = dashed(&[4.0, 3.0], 2.0);
|
||||
let all = dash(&steps(), &pattern).unwrap();
|
||||
let kept = dash_kept(&steps(), &pattern, &|s| outside(s)).unwrap();
|
||||
// The dashes of the whole line, cut at x 30 and 60.
|
||||
let cut: Vec<(f64, f64)> =
|
||||
spans_x(&all).into_iter().flat_map(|(a, b)| [(a, b.min(30.0)), (a.max(60.0), b)]).filter(|(a, b)| b - a > 1e-9).collect();
|
||||
let got = spans_x(&kept);
|
||||
assert_eq!(got.len(), cut.len(), "{got:?} against {cut:?}");
|
||||
for ((a, b), (c, d)) in got.iter().zip(&cut) {
|
||||
assert!(near(*a, *c, 1e-9) && near(*b, *d, 1e-9), "{got:?} against {cut:?}");
|
||||
}
|
||||
// The dash after the gap starts where the whole line's does, at 61, not at 60.
|
||||
assert!(got.iter().any(|(a, _)| near(*a, 61.0, 1e-9)), "{got:?}");
|
||||
// Dots on segments left out are left out (the dot at 60 ends a segment left out).
|
||||
let dots = dash_kept(&steps(), &dashed(&[0.0, 5.0], 0.0), &|s| outside(s)).unwrap();
|
||||
let xs: Vec<f64> = dots.dots.iter().map(|d| d.at.x).collect();
|
||||
let want = [0.0, 5.0, 10.0, 15.0, 20.0, 25.0, 30.0, 65.0, 70.0, 75.0, 80.0, 85.0, 90.0, 95.0, 100.0];
|
||||
assert!(xs.len() == want.len() && xs.iter().zip(want).all(|(x, w)| near(*x, w, 1e-9)), "{xs:?}");
|
||||
// Keeping every segment is the whole line's dashing, and keeping none leaves nothing.
|
||||
assert_eq!(dash_kept(&steps(), &pattern, &|_| true).unwrap().path, all.path);
|
||||
let none = dash_kept(&steps(), &pattern, &|_| false).unwrap();
|
||||
assert!(none.path.elements().is_empty() && none.dots.is_empty());
|
||||
// A pattern too fine for the whole line draws it solid, however few segments are kept.
|
||||
assert!(dash_kept(&line(0.0, 1e6), &dashed(&[0.001, 0.001], 0.0), &|s| s.start().x < 1.0).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_closed_path_with_segments_left_out_is_dashed_in_open_pieces() {
|
||||
// One dash all the way round: with the right side left out it is no longer closed.
|
||||
let keep_left = |s: &PathSeg| s.start().x < 5.0 || s.end().x < 5.0;
|
||||
let whole = dash_kept(&square(10.0), &dashed(&[100.0, 1.0], 0.0), &keep_left).unwrap();
|
||||
assert!(!is_closed(&whole.path), "{:?}", whole.path.to_svg());
|
||||
// The bottom side left out: of the 10 dashes, the one through the start point stays one dash,
|
||||
// the one into the bottom side is cut and the two on it are left out.
|
||||
let kept = dash_kept(&square(100.0), &dashed(&[20.0, 20.0], 10.0), &|s| s.start().y < 50.0 || s.end().y < 50.0).unwrap();
|
||||
assert_eq!(moves(&kept.path), 8, "{}", kept.path.to_svg());
|
||||
assert!(!kept.path.elements().iter().any(|e| matches!(e, PathEl::MoveTo(p) if p.distance(Point::ZERO) < 1e-9)), "{}", kept.path.to_svg());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dot_outlines_take_the_cap_shape_and_wind_like_stroked_dashes() {
|
||||
let dots = [Dot { at: Point::new(10.0, 0.0), dir: Vec2::new(1.0, 0.0), t: 0.0 }];
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! `cargo run --release -p vectorcraft-engine --example anchor_bench` prints the wall time of the
|
||||
//! engine steps of editing anchors with Direct Selection, on three synthetic documents: one closed path of
|
||||
//! 50,000 anchors, 2,000 closed paths of 50 anchors each, and one open path of 200,000 anchors.
|
||||
//! engine steps of editing anchors with Direct Selection, on four synthetic documents: one closed path of
|
||||
//! 50,000 anchors, 2,000 closed paths of 50 anchors each, one open path of 200,000 anchors, and the closed
|
||||
//! path of 50,000 anchors filled and with a dashed stroke.
|
||||
//! `ITERS` sets the runs per step (default 7); each row is the median in ms.
|
||||
// Example (dev tool): unwrapping and panicking on failure is fine here, unlike in shipped code (AGENTS.md › Robustness).
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
@@ -10,7 +11,7 @@ use std::time::Instant;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_color::{Color, Paint};
|
||||
use vectorcraft_doc::{Appearance, Document, Node, NodeId};
|
||||
use vectorcraft_doc::{Appearance, AppearanceItem, Dash, Document, Node, NodeId};
|
||||
use vectorcraft_engine::tools::{Mods, PointerEvent, PointerKind};
|
||||
use vectorcraft_engine::{Session, ViewInfo};
|
||||
use vectorcraft_geom::{Affine, Anchor, PathData, Point, SubPath, Vec2};
|
||||
@@ -70,6 +71,20 @@ fn one_outline() -> Shape {
|
||||
}
|
||||
}
|
||||
|
||||
/// [`one_outline`] filled, with a 1 pt stroke of 6 pt dashes and 4 pt gaps.
|
||||
fn dashed_outline() -> Shape {
|
||||
let mut shape = one_outline();
|
||||
let mut look = Appearance::basic(Paint::solid(Color::rgb(0.8, 0.2, 0.2)), Paint::solid(Color::BLACK), 1.0);
|
||||
for item in &mut look.items {
|
||||
if let AppearanceItem::Stroke(st) = item {
|
||||
st.dash = Some(Dash { pattern: vec![6.0, 4.0], offset: 0.0, align_corners: false });
|
||||
}
|
||||
}
|
||||
shape.doc.node_mut(shape.ids[0]).unwrap().appearance = look.into();
|
||||
shape.name = "one closed path, 50,000 anchors, filled, dashed stroke";
|
||||
shape
|
||||
}
|
||||
|
||||
fn many_paths() -> Shape {
|
||||
let mut doc = Document::new(1600.0, 1200.0);
|
||||
let ids: Vec<NodeId> =
|
||||
@@ -317,4 +332,5 @@ fn main() {
|
||||
bench(one_outline());
|
||||
bench(many_paths());
|
||||
bench(long_open_path());
|
||||
bench(dashed_outline());
|
||||
}
|
||||
|
||||
@@ -269,10 +269,21 @@ impl Renderer {
|
||||
e.stamp = self.stamp;
|
||||
return Some(e.outline.clone());
|
||||
}
|
||||
// A long stroke without dashes or a width profile is outlined only in and around the view,
|
||||
// with a margin of the view's own size on each side ([`crop_line`]).
|
||||
// A long stroke without a width profile is outlined only in and around the view, with a
|
||||
// margin of the view's own size on each side ([`crop_dashed`] for a dashed one,
|
||||
// [`crop_line`] otherwise).
|
||||
let region = v.inflate(v.width(), v.height());
|
||||
let tol = 2f64.powi(level);
|
||||
if crops()
|
||||
&& line.elements().len() > CROP_ELEMENTS
|
||||
&& st.dash.is_some()
|
||||
&& st.profile.is_none()
|
||||
&& let Some(outline) = crop_dashed(line, st, width, region, crop_reach(st, width, f.px, tol), tol)
|
||||
{
|
||||
let outline = Arc::new(outline);
|
||||
self.strokes.insert(key, StrokeEntry { node, outline: outline.clone(), stamp: self.stamp, crop: Some(region) });
|
||||
return Some(outline);
|
||||
}
|
||||
let cropped = (crops() && line.elements().len() > CROP_ELEMENTS && st.dash.is_none() && st.profile.is_none())
|
||||
.then(|| crop_line(line, region, crop_reach(st, width, f.px, tol), width))
|
||||
.flatten();
|
||||
@@ -316,6 +327,38 @@ pub(crate) fn crop_reach(st: &StrokeLayer, width: f64, px: f64, tol: f64) -> f64
|
||||
width / 2.0 * corner + 2.0 * px + tol
|
||||
}
|
||||
|
||||
/// Whether the box of segment `s`'s control points meets `near`. A coordinate that is not a number
|
||||
/// counts as near.
|
||||
fn is_near(s: &kurbo::PathSeg, near: Rect) -> bool {
|
||||
use kurbo::PathSeg;
|
||||
let pts: &[kurbo::Point] = match s {
|
||||
PathSeg::Line(l) => &[l.p0, l.p1],
|
||||
PathSeg::Quad(q) => &[q.p0, q.p1, q.p2],
|
||||
PathSeg::Cubic(c) => &[c.p0, c.p1, c.p2, c.p3],
|
||||
};
|
||||
!(pts.iter().all(|p| p.x < near.x0)
|
||||
|| pts.iter().all(|p| p.x > near.x1)
|
||||
|| pts.iter().all(|p| p.y < near.y0)
|
||||
|| pts.iter().all(|p| p.y > near.y1))
|
||||
}
|
||||
|
||||
/// The outline of the line `line` of dashed stroke `st` (without a width profile) at `width`,
|
||||
/// flattened to `tol`, made from the segments within `reach` of `region` only, for drawing in
|
||||
/// `region`. The dashes and dots on those segments sit where the whole line puts them
|
||||
/// ([`effects::stroke::dash_kept`]); a pattern that draws the stroke solid is outlined as a solid
|
||||
/// line along [`crop_line`]. `None` when every segment is kept.
|
||||
fn crop_dashed(line: &BezPath, st: &StrokeLayer, width: f64, region: Rect, reach: f64, tol: f64) -> Option<BezPath> {
|
||||
let near = region.inflate(reach, reach);
|
||||
if kurbo::segments(line.elements().iter().copied()).all(|s| is_near(&s, near)) {
|
||||
return None;
|
||||
}
|
||||
if let Some(outline) = effects::stroke::dashed_line_outline_kept(line, st, width, tol, &|s| is_near(s, near)) {
|
||||
return Some(outline);
|
||||
}
|
||||
let solid = StrokeLayer { dash: None, ..st.clone() };
|
||||
crop_line(line, region, reach, width).map(|part| effects::stroke::line_outline(&part, &solid, width, tol))
|
||||
}
|
||||
|
||||
/// The stroke line `line` without the segments whose control points' box lies more than `reach`
|
||||
/// from `region`, for outlining a stroke of `width` that is drawn only in `region`. A subpath that
|
||||
/// loses some of its segments is replaced by its runs of kept segments, each an open subpath; a
|
||||
@@ -326,18 +369,7 @@ pub(crate) fn crop_reach(st: &StrokeLayer, width: f64, px: f64, tol: f64) -> f64
|
||||
pub(crate) fn crop_line(line: &BezPath, region: Rect, reach: f64, width: f64) -> Option<BezPath> {
|
||||
use kurbo::{PathEl, PathSeg};
|
||||
let near = region.inflate(reach, reach);
|
||||
let keep = |s: &PathSeg| {
|
||||
let pts: &[kurbo::Point] = match s {
|
||||
PathSeg::Line(l) => &[l.p0, l.p1],
|
||||
PathSeg::Quad(q) => &[q.p0, q.p1, q.p2],
|
||||
PathSeg::Cubic(c) => &[c.p0, c.p1, c.p2, c.p3],
|
||||
};
|
||||
// A coordinate that is not a number keeps the segment.
|
||||
!(pts.iter().all(|p| p.x < near.x0)
|
||||
|| pts.iter().all(|p| p.x > near.x1)
|
||||
|| pts.iter().all(|p| p.y < near.y0)
|
||||
|| pts.iter().all(|p| p.y > near.y1))
|
||||
};
|
||||
let keep = |s: &PathSeg| is_near(s, near);
|
||||
let els = line.elements();
|
||||
let mut starts: Vec<usize> = els.iter().enumerate().filter(|(_, e)| matches!(e, PathEl::MoveTo(_))).map(|(i, _)| i).collect();
|
||||
if starts.first() != Some(&0) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! A long stroke without dashes or a width profile is outlined in and around the view only
|
||||
//! (`crop_line`), and draws the same pixels as when it is outlined whole.
|
||||
//! A long stroke without a width profile is outlined in and around the view only (`crop_line`,
|
||||
//! and for dashes `dash_kept`), and draws the same pixels as when it is outlined whole.
|
||||
|
||||
use vectorcraft_color::{Color, Paint};
|
||||
use vectorcraft_doc::{Appearance, AppearanceItem, LineCap, LineJoin, Node, StrokeLayer};
|
||||
@@ -12,8 +12,13 @@ const SIZE: u32 = 200;
|
||||
|
||||
/// A document holding `path` stroked with `st` and no fill.
|
||||
fn doc(path: &BezPath, st: StrokeLayer) -> Document {
|
||||
doc_with(path, vec![AppearanceItem::Stroke(st)])
|
||||
}
|
||||
|
||||
/// A document holding `path` painted with `items`.
|
||||
fn doc_with(path: &BezPath, items: Vec<AppearanceItem>) -> Document {
|
||||
let mut d = Document::new(1000.0, 1000.0);
|
||||
let n = Node::path(d.alloc_id(), PathData::from_bezpath(path), Appearance { items: vec![AppearanceItem::Stroke(st)], ..Default::default() });
|
||||
let n = Node::path(d.alloc_id(), PathData::from_bezpath(path), Appearance { items, ..Default::default() });
|
||||
let l = d.layers[0].id;
|
||||
d.insert(Some(l), 0, n).unwrap();
|
||||
d
|
||||
@@ -98,6 +103,110 @@ fn a_long_stroke_draws_the_same_pixels_outlined_near_the_view() {
|
||||
}
|
||||
}
|
||||
|
||||
fn dashed(mut st: StrokeLayer, pattern: &[f64], offset: f64, align_corners: bool) -> StrokeLayer {
|
||||
st.dash = Some(vectorcraft_doc::Dash { pattern: pattern.to_vec(), offset, align_corners });
|
||||
st
|
||||
}
|
||||
|
||||
/// A document holding `path` filled red and stroked with `st`.
|
||||
fn filled(path: &BezPath, st: StrokeLayer) -> Document {
|
||||
let fill = AppearanceItem::Fill(vectorcraft_doc::FillLayer::new(Paint::solid(Color::rgb(1.0, 0.0, 0.0))));
|
||||
doc_with(path, vec![fill, AppearanceItem::Stroke(st)])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_long_dashed_stroke_draws_the_same_pixels_outlined_near_the_view() {
|
||||
let round = stroke(6.0, LineJoin::Round, LineCap::Round);
|
||||
let mut two_rings = ring(4000, 5000.0);
|
||||
two_rings.extend(ring(3000, 4980.0).iter());
|
||||
let cases = [
|
||||
(
|
||||
"zigzag, miter joins, square caps",
|
||||
doc(&zigzag(), dashed(stroke(6.0, LineJoin::Miter, LineCap::Square), &[30.0, 12.0], 7.0, false)),
|
||||
Point::new(12_000.0, 15.0),
|
||||
),
|
||||
(
|
||||
"ring through its start, a dash across it",
|
||||
doc(&ring(4000, 5000.0), dashed(round.clone(), &[40.0, 25.0], 0.0, false)),
|
||||
Point::new(5000.0, 0.0),
|
||||
),
|
||||
("ring, dots", doc(&ring(4000, 5000.0), dashed(round.clone(), &[0.0, 9.0], 0.0, false)), Point::new(0.0, 5000.0)),
|
||||
("ring filled, dashed", filled(&ring(4000, 5000.0), dashed(round.clone(), &[15.0, 10.0, 0.0, 10.0], 4.0, false)), Point::new(-5000.0, 0.0)),
|
||||
(
|
||||
"zigzag, dashes fitted to corners",
|
||||
doc(&zigzag(), dashed(stroke(4.0, LineJoin::Bevel, LineCap::Butt), &[5.0, 3.0], 0.0, true)),
|
||||
Point::new(8000.0, 15.0),
|
||||
),
|
||||
("two subpaths, dashed", doc(&two_rings, dashed(round.clone(), &[25.0, 15.0], 0.0, false)), Point::new(0.0, -4990.0)),
|
||||
("a pattern too fine, drawn solid", doc(&zigzag(), dashed(round, &[0.001, 0.001], 0.0, false)), Point::new(12_000.0, 15.0)),
|
||||
];
|
||||
for (what, d, at) in &cases {
|
||||
let mut r = Renderer::new();
|
||||
let img = render(&mut r, d, *at);
|
||||
assert!(r.strokes.values().any(|e| e.crop.is_some()), "{what}: outlined near the view");
|
||||
assert!(img.pixels == whole(d, *at).pixels, "{what}: the same pixels as the whole outline");
|
||||
assert!(img.pixels.chunks(4).any(|p| p[0] < 128), "{what}: the stroke shows");
|
||||
for step in [Point::new(60.0, 40.0), Point::new(450.0, 0.0), Point::new(-900.0, 300.0)] {
|
||||
let next = *at + step.to_vec2();
|
||||
assert!(render(&mut r, d, next).pixels == whole(d, next).pixels, "{what}, moved by {step:?}: the same pixels");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_long_dashed_stroke_draws_the_same_pixels_after_its_path_changes() {
|
||||
let st = dashed(stroke(5.0, LineJoin::Miter, LineCap::Butt), &[18.0, 7.0], 3.0, false);
|
||||
let at = Point::new(12_000.0, 15.0);
|
||||
let mut r = Renderer::new();
|
||||
render(&mut r, &doc(&zigzag(), st.clone()), at);
|
||||
// An anchor in the view moved, then one far before it, which shifts every dash after it.
|
||||
for (i, by) in [(1500, Vec2::new(0.0, 12.0)), (40, Vec2::new(-3.0, 0.0))] {
|
||||
let mut bp = zigzag();
|
||||
let mut els = bp.elements().to_vec();
|
||||
if let Some(kurbo::PathEl::LineTo(p)) = els.get_mut(i) {
|
||||
*p += by;
|
||||
}
|
||||
bp = BezPath::from_vec(els);
|
||||
let d = doc(&bp, st.clone());
|
||||
assert!(render(&mut r, &d, at).pixels == whole(&d, at).pixels, "anchor {i} moved: the same pixels");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn short_dashed_strokes_are_outlined_whole() {
|
||||
let st = dashed(stroke(4.0, LineJoin::Round, LineCap::Round), &[6.0, 4.0], 0.0, false);
|
||||
let one = BezPath::from_vec(vec![kurbo::PathEl::MoveTo(Point::new(100.0, 100.0))]);
|
||||
let mut dot = one.clone();
|
||||
dot.line_to((100.0, 100.0));
|
||||
for (what, bp) in
|
||||
[("empty", BezPath::new()), ("one anchor", one), ("one anchor, closed on itself", dot), ("a ring of 40 anchors", ring(40, 80.0))]
|
||||
{
|
||||
let d = doc(&bp, st.clone());
|
||||
let mut r = Renderer::new();
|
||||
let img = render(&mut r, &d, Point::new(100.0, 100.0));
|
||||
assert!(r.strokes.values().all(|e| e.crop.is_none()), "{what}: outlined whole");
|
||||
assert!(img.pixels == whole(&d, Point::new(100.0, 100.0)).pixels, "{what}: the same pixels");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_dashed_stroke_a_wide_stroke_crosses_over_draws_the_same_pixels() {
|
||||
// The closed sliver of `a_stroke_a_wide_stroke_crosses_over_stays_whole`, dashed.
|
||||
let mut bp = BezPath::new();
|
||||
bp.move_to((0.0, 0.0));
|
||||
for i in 1..=2000 {
|
||||
bp.line_to((f64::from(i) * 10.0, 0.0));
|
||||
}
|
||||
bp.line_to((20_000.0, 4.0));
|
||||
bp.line_to((0.0, 4.0));
|
||||
bp.close_path();
|
||||
let d = doc(&bp, dashed(stroke(10.0, LineJoin::Miter, LineCap::Butt), &[35.0, 15.0], 0.0, false));
|
||||
for at in [Point::new(20_000.0, 2.0), Point::new(9_000.0, 2.0), Point::new(0.0, 2.0)] {
|
||||
let mut r = Renderer::new();
|
||||
assert!(render(&mut r, &d, at).pixels == whole(&d, at).pixels, "at {at:?}: the same pixels");
|
||||
}
|
||||
}
|
||||
|
||||
/// A zigzag of 3,000 anchors 8 pt apart between y = 60 and y = 550, through the bottom of a view
|
||||
/// on y = 0.
|
||||
fn tall_zigzag() -> BezPath {
|
||||
|
||||
Reference in New Issue
Block a user