Files
figcraft/crates/doc/src/vector_regions_tests.rs

241 lines
10 KiB
Rust

//! Regions as areas (`vector_regions.rs`): the inside test by each region's own fill rule, the last
//! region winning where regions overlap, curved edges, loops with segments stored backwards, a
//! region's points and empty or damaged networks.
use figcraft_geom::Point;
use crate::vector::{Region, Segment, VectorNetwork, Vertex, WindingRule};
/// Four points and four straight segments of a square at (`x0`, `y0`), clockwise on screen (y
/// down), appended to `n`. Returns the segment indexes in ring order.
fn square(n: &mut VectorNetwork, x0: f64, y0: f64, size: f64) -> Vec<usize> {
let v0 = n.vertices.len();
for (x, y) in [(x0, y0), (x0 + size, y0), (x0 + size, y0 + size), (x0, y0 + size)] {
n.vertices.push(Vertex::new(x, y));
}
let s0 = n.segments.len();
for k in 0..4 {
n.segments.push(Segment::line(v0 + k, v0 + (k + 1) % 4));
}
(s0..s0 + 4).collect()
}
/// The 7.1 network: a 200 square (points 0..3, segments 0..3) with a 80 square inside it (points
/// 4..7, segments 4..7), both wound the same way, and no regions yet.
fn nested() -> VectorNetwork {
let mut n = VectorNetwork::default();
square(&mut n, 0.0, 0.0, 200.0);
square(&mut n, 60.0, 60.0, 80.0);
n
}
fn region(loops: Vec<Vec<usize>>, winding: WindingRule) -> Region {
Region { loops, winding }
}
fn p(x: f64, y: f64) -> Point {
Point::new(x, y)
}
#[test]
fn the_inside_test_follows_each_regions_own_fill_rule() {
let mut n = nested();
n.regions = vec![region(vec![vec![0, 1, 2, 3], vec![4, 5, 6, 7]], WindingRule::NonZero)];
// 1. Non-zero: the centre counts (both rings wound the same way).
assert_eq!(n.region_at(p(100.0, 100.0)), Some(0));
// 4. Outside.
assert_eq!(n.region_at(p(300.0, 100.0)), None);
n.regions[0].winding = WindingRule::EvenOdd;
// 2. Even-odd: the centre is a hole; 3. the ring is inside.
assert_eq!(n.region_at(p(100.0, 100.0)), None);
assert_eq!(n.region_at(p(20.0, 100.0)), Some(0));
assert_eq!(n.region_at(p(300.0, 100.0)), None);
// 5. The inner ring wound the other way, non-zero: a hole.
let mut r = nested();
for s in &mut r.segments[4..8] {
std::mem::swap(&mut s.a, &mut s.b);
}
r.regions = vec![region(vec![vec![0, 1, 2, 3], vec![7, 6, 5, 4]], WindingRule::NonZero)];
assert_eq!(r.region_at(p(100.0, 100.0)), None);
assert_eq!(r.region_at(p(20.0, 100.0)), Some(0));
// Regions of one path may use different rules.
let mut m = nested();
m.regions = vec![
region(vec![vec![0, 1, 2, 3], vec![4, 5, 6, 7]], WindingRule::NonZero),
region(vec![vec![0, 1, 2, 3], vec![4, 5, 6, 7]], WindingRule::EvenOdd),
];
assert_eq!(m.region_at(p(100.0, 100.0)), Some(0), "the even-odd region has a hole there, the non-zero one doesn't");
assert_eq!(m.region_at(p(20.0, 100.0)), Some(1));
}
#[test]
fn the_last_overlapping_region_wins() {
let mut n = nested();
n.regions = vec![region(vec![vec![0, 1, 2, 3]], WindingRule::NonZero), region(vec![vec![4, 5, 6, 7]], WindingRule::NonZero)];
// 6, 7.
assert_eq!(n.region_at(p(100.0, 100.0)), Some(1));
assert_eq!(n.region_at(p(20.0, 100.0)), Some(0));
// 8. Swapped: the outer square (region 0 before, listed last now) wins in the middle.
n.regions.swap(0, 1);
assert_eq!(n.region_at(p(100.0, 100.0)), Some(1));
assert_eq!(n.region_points(1), vec![0, 1, 2, 3], "index 1 is the outer square now");
assert_eq!(n.region_at(p(20.0, 100.0)), Some(1));
}
#[test]
fn curved_edges_count_where_the_curve_runs() {
let mut n = VectorNetwork {
vertices: vec![Vertex::new(0.0, 0.0), Vertex::new(120.0, 0.0)],
segments: vec![Segment { a: 0, b: 1, ta: (0.0, -80.0), tb: (0.0, -80.0) }, Segment::line(1, 0)],
regions: vec![region(vec![vec![0, 1]], WindingRule::NonZero)],
};
// 9. Inside the bulge, above the chord; 10. below the chord; 11. above the curve's top (-60).
assert_eq!(n.region_at(p(60.0, -40.0)), Some(0));
assert_eq!(n.region_at(p(60.0, 20.0)), None);
assert_eq!(n.region_at(p(60.0, -70.0)), None);
assert_eq!(n.region_at(p(60.0, -58.0)), Some(0), "just under the top of the curve");
// The lens walks toward the point it shares with the second segment, whichever way that is.
n.segments.swap(0, 1);
n.regions = vec![region(vec![vec![0, 1]], WindingRule::NonZero)];
assert_eq!(n.region_at(p(60.0, -40.0)), Some(0));
assert_eq!(n.region_at(p(60.0, -70.0)), None);
}
#[test]
fn a_single_curved_segment_loop_encloses_its_area() {
let n = VectorNetwork {
vertices: vec![Vertex::new(0.0, 0.0)],
segments: vec![Segment { a: 0, b: 0, ta: (100.0, -100.0), tb: (-100.0, -100.0) }],
regions: vec![region(vec![vec![0]], WindingRule::NonZero)],
};
assert_eq!(n.region_at(p(0.0, -40.0)), Some(0));
assert_eq!(n.region_at(p(0.0, 40.0)), None);
}
#[test]
fn segments_stored_backwards_are_walked_the_right_way() {
let mut n = VectorNetwork::default();
square(&mut n, 0.0, 0.0, 200.0);
// Segment 1 stored as 2 → 1 and segment 3 as 0 → 3.
n.segments[1] = Segment::line(2, 1);
n.segments[3] = Segment::line(0, 3);
n.regions = vec![region(vec![vec![0, 1, 2, 3]], WindingRule::NonZero)];
// 12, 13.
assert_eq!(n.region_at(p(100.0, 100.0)), Some(0));
assert_eq!(n.region_at(p(300.0, 100.0)), None);
let walked = n.loop_walk(&[0, 1, 2, 3]);
assert_eq!(walked, vec![(0, false), (1, true), (2, false), (3, true)]);
// 14. The first segment reversed too: unchanged.
n.segments[0] = Segment::line(1, 0);
assert_eq!(n.region_at(p(100.0, 100.0)), Some(0));
assert_eq!(n.region_at(p(300.0, 100.0)), None);
// The walk equals the same loop stored consistently: one closed contour of the same area.
use figcraft_geom::Shape;
let mut consistent = VectorNetwork::default();
square(&mut consistent, 0.0, 0.0, 200.0);
consistent.regions = n.regions.clone();
let (a, b) = (n.region_path(0).unwrap().area().abs(), consistent.region_path(0).unwrap().area().abs());
assert!((a - 40_000.0).abs() < 1e-6 && (b - 40_000.0).abs() < 1e-6, "{a} {b}");
}
#[test]
fn a_loop_that_does_not_close_is_closed_implicitly() {
let mut n = VectorNetwork::default();
square(&mut n, 0.0, 0.0, 200.0);
n.regions = vec![region(vec![vec![0, 1, 2]], WindingRule::NonZero)];
// The walk ends at point 3 and closes straight back to point 0.
assert_eq!(n.region_at(p(100.0, 100.0)), Some(0));
assert_eq!(n.region_at(p(300.0, 100.0)), None);
}
#[test]
fn missing_segments_are_skipped_wherever_they_sit_in_a_loop() {
use figcraft_geom::Shape;
let mut n = VectorNetwork::default();
square(&mut n, 0.0, 0.0, 200.0);
n.regions = vec![region(vec![vec![0, 1, 2]], WindingRule::NonZero)];
let control = n.region_path(0).unwrap();
let control_walk = n.loop_walk(&[0, 1, 2]);
for lp in [vec![99, 0, 1, 2], vec![0, 99, 1, 2], vec![99, 98, 0, 1, 2]] {
n.regions = vec![region(vec![lp.clone()], WindingRule::NonZero)];
assert_eq!(n.loop_walk(&lp), control_walk, "{lp:?}");
assert_eq!(n.region_at(p(100.0, 100.0)), Some(0), "{lp:?}");
assert!(n.region_contains(0, p(100.0, 100.0)), "{lp:?}");
let path = n.region_path(0).unwrap();
assert_eq!(path.elements().len(), control.elements().len(), "{lp:?}");
assert!((path.area().abs() - control.area().abs()).abs() < 1e-6, "{lp:?}");
}
n.regions = vec![region(vec![vec![99]], WindingRule::NonZero)];
assert!(n.region_path(0).is_none());
assert!(!n.region_contains(0, p(100.0, 100.0)));
}
#[test]
fn a_regions_points_are_listed_once() {
let mut n = nested();
n.regions = vec![region(vec![vec![0, 1, 2, 3], vec![4, 5, 6, 7]], WindingRule::NonZero)];
// 15.
assert_eq!(n.region_points(0), vec![0, 1, 2, 3, 4, 5, 6, 7]);
// 16.
n.regions = vec![region(vec![vec![0, 1, 2, 3]], WindingRule::NonZero), region(vec![vec![4, 5, 6, 7]], WindingRule::NonZero)];
assert_eq!(n.region_points(1), vec![4, 5, 6, 7]);
assert!(n.region_points(9).is_empty());
// 17. A missing segment is ignored.
n.regions = vec![region(vec![vec![0, 1, 99, 2, 3]], WindingRule::NonZero)];
assert_eq!(n.region_points(0), vec![0, 1, 2, 3]);
assert_eq!(n.region_at(p(100.0, 100.0)), Some(0));
}
#[test]
fn empty_and_damaged_networks_have_no_regions_and_never_panic() {
// 18.
let mut n = nested();
assert_eq!(n.region_at(p(100.0, 100.0)), None);
let empty = VectorNetwork::default();
assert_eq!(empty.region_at(p(0.0, 0.0)), None);
assert!(empty.region_path(0).is_none());
assert!(empty.region_points(0).is_empty());
assert_eq!(empty.region_winding(0, p(0.0, 0.0)), None);
// Loops naming missing segments, segments naming missing points, empty loops, junk points.
n.regions = vec![region(vec![vec![], vec![42, 43]], WindingRule::EvenOdd), region(vec![vec![0, 1, 2, 3]], WindingRule::NonZero)];
n.segments.push(Segment::line(1, 500));
n.regions.push(region(vec![vec![8, 8, 8]], WindingRule::NonZero));
assert_eq!(n.region_at(p(100.0, 100.0)), Some(1));
assert!(n.region_path(0).is_none());
assert!(n.region_path(2).is_none());
for q in [p(f64::NAN, 0.0), p(f64::INFINITY, 1.0), p(1e300, -1e300)] {
let _ = n.region_at(q);
}
}
#[test]
fn region_points_leave_out_segment_ends_that_do_not_exist() {
let mut n = VectorNetwork::default();
let ring = square(&mut n, 0.0, 0.0, 100.0);
n.regions = vec![region(vec![ring], WindingRule::NonZero)];
assert_eq!(n.region_points(0), vec![0, 1, 2, 3]);
n.segments[3] = Segment::line(3, 9);
assert_eq!(n.region_points(0), vec![0, 1, 2, 3]);
assert!(n.region_path(0).is_some());
n.regions = vec![region(vec![vec![99]], WindingRule::NonZero)];
assert!(n.region_points(0).is_empty());
}
#[test]
fn the_topology_key_ignores_moved_points_but_not_new_segments() {
let mut n = nested();
n.regions = vec![region(vec![vec![0, 1, 2, 3]], WindingRule::NonZero)];
let k = n.topology_key();
n.translate(15.0, 25.0);
n.segments[0].ta = (5.0, 5.0);
assert_eq!(n.topology_key(), k, "moving points and handles keeps it");
n.regions.push(region(vec![vec![4, 5, 6, 7]], WindingRule::NonZero));
assert_ne!(n.topology_key(), k);
let k = n.topology_key();
n.regions.swap(0, 1);
assert_ne!(n.topology_key(), k, "reordered regions");
let k = n.topology_key();
n.segments.push(Segment::line(0, 2));
assert_ne!(n.topology_key(), k);
}