Tests: random edits of long paths keep what making the path again makes

Two seeded tests run 300 random edits each on a long path and a long
compound path: anchors added and deleted at the start, middle and end of a
subpath and where a run of segments ends, subpaths closed and opened, anchors
and handles moved, and in the compound path members added and removed and
subpaths added to a member. The subpaths range from 2 anchors to 3,000, open
and closed. After each edit the kept path's elements, subpaths, bounds and
miter tips equal those of a new `KeptPath`, its runs hold each subpath's
segments, and each run's box and tips equal those measured again.
This commit is contained in:
dotCooCoo
2026-11-05 14:12:17 +07:00
committed by bflatastic
parent e3d30a7047
commit 924ec6f0d6
+180
View File
@@ -384,6 +384,186 @@ fn deleting_an_anchor_of_an_open_path_and_clearing_the_handles_past_its_ends_mea
assert_eq!(changed, vec![1499 / crate::kept_paths::RUN], "only the run that held the anchor changed: {before:?} -> {after:?}");
}
/// Numbers from a fixed seed (a linear congruential generator).
struct Seeded(u64);
impl Seeded {
/// A number below `m` (0 when `m` is 0).
fn below(&mut self, m: usize) -> usize {
self.0 = self.0.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1_442_695_040_888_963_407);
(self.0 >> 33) as usize % m.max(1)
}
/// A whole number of points from -`r` to `r`.
fn offset(&mut self, r: usize) -> f64 {
self.below(2 * r + 1) as f64 - r as f64
}
/// Where in a subpath of `n` anchors to edit: at the start, at the end, where a run of segments
/// ends, or anywhere.
fn at(&mut self, n: usize) -> usize {
match self.below(4) {
0 => self.below(3).min(n),
1 => n.saturating_sub(self.below(3)),
2 => (crate::kept_paths::RUN * (1 + self.below(24)) + self.below(3)).min(n),
_ => self.below(n + 1),
}
}
}
/// An anchor to add after `before`: on it (a segment without length), a corner near it, or a
/// smooth anchor near it.
fn anchor_after(g: &mut Seeded, before: Point) -> Anchor {
let p = before + Vec2::new(g.offset(20), g.offset(40));
match g.below(5) {
0 => Anchor::corner(before),
1 => Anchor::smooth(p, p + Vec2::new(g.offset(6), g.offset(6))),
_ => Anchor::corner(p),
}
}
/// Edit `p` at random: anchors added or deleted at the start, middle or end of a subpath, a
/// subpath closed or opened, an anchor or a handle moved, or many anchors moved. Says what it did.
fn random_edit(g: &mut Seeded, p: &mut PathData) -> String {
let s = g.below(p.subpaths.len());
let Some(sp) = p.subpaths.get_mut(s) else { return "nothing".into() };
let n = sp.anchors.len();
match g.below(6) {
0 | 1 => {
let (at, k) = (g.at(n), 1 + g.below(3));
for j in 0..k {
let before = sp.anchors.get((at + j).saturating_sub(1)).map_or(Point::new(12_000.0, 15.0), |a| a.p);
let a = anchor_after(g, before);
sp.anchors.insert(at + j, a);
}
format!("{k} anchors added at {at} of subpath {s} of {n}")
}
2 | 3 if n > 0 => {
let at = g.at(n - 1);
let k = (1 + g.below(3)).min(n - at);
sp.anchors.drain(at..at + k);
format!("{k} anchors deleted at {at} of subpath {s} of {n}")
}
4 => {
sp.closed = !sp.closed;
format!("subpath {s} of {n} closed: {}", sp.closed)
}
_ if n > 0 => {
let at = g.below(n);
match g.below(3) {
0 => moved(&mut sp.anchors[at], Vec2::new(g.offset(30), g.offset(30))),
1 => sp.anchors[at].h_out = sp.anchors[at].p + Vec2::new(g.offset(10), g.offset(10)),
_ => sp.anchors[at..].iter_mut().take(n / 10).for_each(|a| moved(a, Vec2::new(1.0, -1.0))),
}
format!("anchors moved at {at} of subpath {s} of {n}")
}
_ => "nothing".into(),
}
}
/// Bring `k` up to date with `n` and check that it holds what [`KeptPath::new`] makes from `n`:
/// the same path elements and subpaths, runs that hold each subpath's segments, the boxes and miter
/// tips of `st` measured again over its runs, and the same bounds and tips in all.
fn same_as_made_again(k: &mut crate::kept_paths::KeptPath, n: &Arc<Node>, st: &StrokeLayer, what: &str) {
assert!(k.update(n), "{what}: brought up to date");
let mut fresh = crate::kept_paths::KeptPath::new(n, 0).unwrap_or_else(|| panic!("{what}: kept"));
assert!(*k.path == *fresh.path, "{what}: the same path elements");
let (layout, whole) = (k.layout(), fresh.layout());
assert!(layout.iter().map(|l| l.0).eq(whole.iter().map(|l| l.0)), "{what}: the same subpaths: {layout:?}");
for ((sub, runs), ((_, segs, ..), _)) in layout.iter().zip(&whole) {
assert_eq!(runs.iter().sum::<usize>(), *segs, "{what}: the runs of {sub:?} hold its segments");
assert!(runs.iter().all(|&s| (1..=2 * crate::kept_paths::RUN).contains(&s)), "{what}: the runs of {sub:?}: {runs:?}");
}
let (runs, again, _) = k.runs();
assert!(runs == again, "{what}: the same boxes and tips as measured again");
assert_eq!(k.geometric(), fresh.geometric(), "{what}: the same bounds");
assert_eq!(k.tips(st), fresh.tips(st), "{what}: the same miter tips");
}
#[test]
fn random_edits_of_a_long_path_keep_what_making_its_path_again_makes() {
let st = stroke(6.0, LineJoin::Miter, LineCap::Butt);
let mut g = Seeded(5744);
let closed = |sp: SubPath| SubPath::new(sp.anchors, true);
let path = PathData::new(vec![
zigzag(2, 11_990.0),
closed(zigzag(3, 12_000.0)),
zigzag(7, 11_960.0),
closed(zigzag(40, 11_800.0)),
ring(300, Point::new(12_000.0, 15.0), 60.0),
zigzag(3000, 0.0),
closed(zigzag(2000, 4000.0)),
]);
let mut n = Arc::new(Node::path(NodeId(1), path, look(vec![st.clone()])));
let mut k = crate::kept_paths::KeptPath::new(&n, 0).unwrap();
k.tips(&st);
for step in 0..300 {
let mut next = (*n).clone();
let what = random_edit(&mut g, next.path_data_mut().unwrap());
n = Arc::new(next);
same_as_made_again(&mut k, &n, &st, &format!("step {step}, {what}"));
}
}
#[test]
fn random_edits_of_a_long_compound_path_keep_what_making_its_path_again_makes() {
let st = stroke(4.0, LineJoin::Miter, LineCap::Square);
let mut g = Seeded(1050);
let ids = std::cell::Cell::new(10);
let member = |p: PathData| {
ids.set(ids.get() + 1);
Arc::new(Node::path(NodeId(ids.get()), p, Appearance::default()))
};
let children = vec![
member(PathData::new(vec![SubPath::new(zigzag(3, 11_990.0).anchors, true), zigzag(5, 12_010.0)])),
member(PathData::single(zigzag(3000, 0.0))),
member(PathData::single(ring(40, Point::new(12_000.0, 15.0), 30.0))),
member(PathData::single(zigzag(2, 12_020.0))),
];
let mut n = Node::new(NodeId(1), NodeKind::Compound { children, rule: FillRule::EvenOdd });
n.appearance = look(vec![st.clone()]).into();
let mut n = Arc::new(n);
let mut k = crate::kept_paths::KeptPath::new(&n, 0).unwrap();
k.tips(&st);
for step in 0..300 {
let mut next = (*n).clone();
let NodeKind::Compound { children, .. } = &mut next.kind else { panic!("a compound path") };
let what = match g.below(8) {
0 => {
let at = [0, children.len(), g.below(children.len() + 1)][g.below(3)];
let size = [1, 2, 4, 30, 200][g.below(5)];
let mut sp = zigzag(size, 11_900.0 + g.offset(100));
sp.closed = g.below(2) == 0;
children.insert(at, member(PathData::single(sp)));
format!("a member of {size} anchors added at {at}")
}
// The member of 3,000 anchors stays, so that the path stays kept.
1 if children.len() > 2 => {
let at = [0, children.len() - 1, g.below(children.len())][g.below(3)];
if children[at].path_data().is_some_and(|p| p.subpaths.iter().map(|s| s.anchors.len()).sum::<usize>() > 1000) {
"nothing".into()
} else {
children.remove(at);
format!("member {at} removed")
}
}
2 => {
let at = g.below(children.len());
let m = Arc::make_mut(&mut children[at]).path_data_mut().unwrap();
m.subpaths.push(zigzag(1 + g.below(4), 12_030.0));
format!("a subpath added to member {at}")
}
_ => {
let at = g.below(children.len());
let what = random_edit(&mut g, Arc::make_mut(&mut children[at]).path_data_mut().unwrap());
format!("member {at}: {what}")
}
};
n = Arc::new(next);
same_as_made_again(&mut k, &n, &st, &format!("step {step}, {what}"));
}
}
#[test]
fn moving_one_anchor_of_a_long_path_changes_its_kept_path_in_place() {
let mut d = Document::new(1000.0, 1000.0);