mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
538 lines
22 KiB
Rust
538 lines
22 KiB
Rust
//! Shared boundaries in Abutting traces: every edge between two neighbouring regions is drawn
|
||
//! once, so the two filled paths on either side of it follow one curve (one of them reversed), and
|
||
//! together the paths cover the image with no gap and no overlap.
|
||
//!
|
||
//! That must hold for edges between two junctions, for closed edges with no junction at all (an
|
||
//! island inside another region, a hole), and for an edge that leaves a junction and comes back to
|
||
//! it (two parts of a region touching only at a pixel corner). No region may vanish on its own: a
|
||
//! one-pixel island or hole is kept or dropped together with the hole or island it leaves, and two
|
||
//! separate areas of one colour stay two shapes.
|
||
//!
|
||
//! Every image here is drawn in code from simple geometry (squares, discs, sine waves, stripes),
|
||
//! hard-edged (each pixel the colour at its centre) and, where it means something, anti-aliased
|
||
//! (each pixel the mean of 8 × 8 samples). The check is geometric and needs no renderer: points
|
||
//! along each path's outline must lie on another path's outline (to [`TOL`] px), the paths' areas
|
||
//! must add up to the image's, and points on a fine grid must each lie in exactly one path.
|
||
//!
|
||
//! Behaviour reference: Image Trace's Advanced › Method, Abutting (shapes cut out of each other,
|
||
//! meeting edge to edge) as against Overlapping (stacked)
|
||
//! (<https://helpx.adobe.com/illustrator/using/image-trace.html>).
|
||
// Integration tests: unwrapping and panicking on failure is fine here, unlike in shipped code.
|
||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||
|
||
use kurbo::{BezPath, CubicBez, ParamCurve, ParamCurveExtrema, ParamCurveNearest, Point, Rect, Shape};
|
||
use vectorcraft_trace::{Method, Mode, Raster, TraceParams, TraceResult, trace};
|
||
|
||
/// How far (px) a point of one path's outline may lie from its neighbour's outline.
|
||
const TOL: f64 = 1e-3;
|
||
|
||
/// The drawing colours, by index. Far apart, and no blend of two is near a third.
|
||
const COLOURS: [[u8; 3]; 3] = [[255, 255, 255], [200, 40, 50], [30, 90, 200]];
|
||
|
||
/// Black and White's two colours: index 0 is paper, 1 is ink.
|
||
const BW: [[u8; 3]; 2] = [[255, 255, 255], [0, 0, 0]];
|
||
|
||
/// A drawing: the colour index at a point of a `w × h` image.
|
||
struct Art {
|
||
name: &'static str,
|
||
w: u32,
|
||
h: u32,
|
||
at: Box<dyn Fn(f64, f64) -> usize>,
|
||
}
|
||
|
||
impl Art {
|
||
fn new(name: &'static str, w: u32, h: u32, at: impl Fn(f64, f64) -> usize + 'static) -> Self {
|
||
Self { name, w, h, at: Box::new(at) }
|
||
}
|
||
/// Each pixel the colour at its centre.
|
||
fn labels(&self) -> Vec<usize> {
|
||
(0..self.h).flat_map(|y| (0..self.w).map(move |x| (x, y))).map(|(x, y)| (self.at)(f64::from(x) + 0.5, f64::from(y) + 0.5)).collect()
|
||
}
|
||
fn hard(&self, colours: &[[u8; 3]]) -> Raster {
|
||
Raster::from_fn(self.w, self.h, |x, y| {
|
||
let c = colours[(self.at)(f64::from(x) + 0.5, f64::from(y) + 0.5)];
|
||
[c[0], c[1], c[2], 255]
|
||
})
|
||
}
|
||
/// Anti-aliased: each pixel the mean of its 8 × 8 samples.
|
||
fn soft(&self, colours: &[[u8; 3]]) -> Raster {
|
||
Raster::from_fn(self.w, self.h, |x, y| {
|
||
let mut sum = [0.0f64; 3];
|
||
for i in 0..64 {
|
||
let c = colours[(self.at)(f64::from(x) + (f64::from(i % 8) + 0.5) / 8.0, f64::from(y) + (f64::from(i / 8) + 0.5) / 8.0)];
|
||
for k in 0..3 {
|
||
sum[k] += f64::from(c[k]) / 64.0;
|
||
}
|
||
}
|
||
[sum[0].round() as u8, sum[1].round() as u8, sum[2].round() as u8, 255]
|
||
})
|
||
}
|
||
}
|
||
|
||
/// The 4-connected components of each colour of `labels`.
|
||
fn components(labels: &[usize], w: usize, h: usize, colours: usize) -> Vec<usize> {
|
||
let mut seen = vec![false; labels.len()];
|
||
let mut count = vec![0; colours];
|
||
for start in 0..labels.len() {
|
||
if seen[start] {
|
||
continue;
|
||
}
|
||
count[labels[start]] += 1;
|
||
seen[start] = true;
|
||
let mut stack = vec![start];
|
||
while let Some(i) = stack.pop() {
|
||
let (x, y) = (i % w, i / w);
|
||
let near = [(x > 0).then(|| i - 1), (x + 1 < w).then_some(i + 1), (y > 0).then(|| i - w), (y + 1 < h).then_some(i + w)];
|
||
for j in near.into_iter().flatten() {
|
||
if !seen[j] && labels[j] == labels[i] {
|
||
seen[j] = true;
|
||
stack.push(j);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
count
|
||
}
|
||
|
||
/// The size of the smallest 4-connected one-colour component of `labels`.
|
||
fn smallest(labels: &[usize], w: usize, h: usize) -> usize {
|
||
let mut seen = vec![false; labels.len()];
|
||
let mut least = usize::MAX;
|
||
for start in 0..labels.len() {
|
||
if seen[start] {
|
||
continue;
|
||
}
|
||
seen[start] = true;
|
||
let (mut stack, mut size) = (vec![start], 0);
|
||
while let Some(i) = stack.pop() {
|
||
size += 1;
|
||
let (x, y) = (i % w, i / w);
|
||
let near = [(x > 0).then(|| i - 1), (x + 1 < w).then_some(i + 1), (y > 0).then(|| i - w), (y + 1 < h).then_some(i + w)];
|
||
for j in near.into_iter().flatten() {
|
||
if !seen[j] && labels[j] == labels[i] {
|
||
seen[j] = true;
|
||
stack.push(j);
|
||
}
|
||
}
|
||
}
|
||
least = least.min(size);
|
||
}
|
||
least
|
||
}
|
||
|
||
/// Color mode, Abutting, `n` colours, Noise `noise` (0 and 1 remove nothing).
|
||
fn color(n: u32, noise: u32) -> TraceParams {
|
||
TraceParams { mode: Mode::Color, colors: n, noise, method: Method::Abutting, ignore_white: false, ..TraceParams::default() }
|
||
}
|
||
|
||
/// Black and White with white traced too, Noise `noise`.
|
||
fn black_and_white(noise: u32) -> TraceParams {
|
||
TraceParams { mode: Mode::BlackAndWhite, noise, method: Method::Abutting, ignore_white: false, ..TraceParams::default() }
|
||
}
|
||
|
||
/// What the check found.
|
||
#[derive(Debug)]
|
||
struct Seams {
|
||
/// Paths of each drawing colour (each path goes to the nearest one).
|
||
faces: Vec<usize>,
|
||
/// The farthest a point of one path's outline lies from every other path's outline (px), and
|
||
/// where.
|
||
gap: f64,
|
||
at: Point,
|
||
/// The paths' areas summed, less the image's (px²): below 0 a gap, above 0 an overlap.
|
||
area: f64,
|
||
/// Points of a 4 × 4 grid per pixel that no path covers, and that more than one path covers.
|
||
uncovered: usize,
|
||
doubled: usize,
|
||
}
|
||
|
||
impl Seams {
|
||
fn ok(&self) -> bool {
|
||
self.gap <= TOL && self.area.abs() <= TOL && self.uncovered == 0 && self.doubled == 0
|
||
}
|
||
}
|
||
|
||
fn segments(p: &vectorcraft_trace::TracedPath) -> Vec<CubicBez> {
|
||
p.path.subpaths.iter().flat_map(|sp| (0..sp.segment_count()).map(move |i| sp.segment(i))).collect()
|
||
}
|
||
|
||
fn check(res: &TraceResult, w: u32, h: u32, colours: &[[u8; 3]]) -> Seams {
|
||
let (wf, hf) = (f64::from(w), f64::from(h));
|
||
let mut faces = vec![0; colours.len()];
|
||
for p in &res.paths {
|
||
assert!(p.stroke.is_none(), "Abutting fills only");
|
||
let d = |c: &[u8; 3]| (0..3).map(|k| (f64::from(c[k]) - f64::from(p.color[k])).powi(2)).sum::<f64>();
|
||
let nearest = (0..colours.len()).min_by(|&a, &b| d(&colours[a]).total_cmp(&d(&colours[b]))).unwrap();
|
||
faces[nearest] += 1;
|
||
}
|
||
let segs: Vec<Vec<(CubicBez, Rect)>> =
|
||
res.paths.iter().map(|p| segments(p).into_iter().map(|c| (c, ParamCurveExtrema::bounding_box(&c))).collect()).collect();
|
||
// Points along each outline, against every other path's outline (the frame has no neighbour).
|
||
let on_frame = |p: Point| p.x.abs() < 1e-9 || p.y.abs() < 1e-9 || (p.x - wf).abs() < 1e-9 || (p.y - hf).abs() < 1e-9;
|
||
let (mut gap, mut at) = (0.0f64, Point::ORIGIN);
|
||
for (i, own) in segs.iter().enumerate() {
|
||
for (c, _) in own {
|
||
for k in 0..32 {
|
||
let p = c.eval(f64::from(k) / 32.0);
|
||
if on_frame(p) {
|
||
continue;
|
||
}
|
||
let mut best = f64::INFINITY;
|
||
for other in segs.iter().enumerate().filter(|(j, _)| *j != i).map(|(_, o)| o) {
|
||
for (o, b) in other {
|
||
let dx = (b.x0 - p.x).max(p.x - b.x1).max(0.0);
|
||
let dy = (b.y0 - p.y).max(p.y - b.y1).max(0.0);
|
||
if dx * dx + dy * dy >= best * best {
|
||
continue;
|
||
}
|
||
best = best.min(o.nearest(p, 1e-12).distance_sq.sqrt());
|
||
}
|
||
}
|
||
if best > gap {
|
||
(gap, at) = (best, p);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
let area = res.paths.iter().map(|p| p.path.subpaths.iter().map(|s| s.area()).sum::<f64>()).sum::<f64>() - wf * hf;
|
||
let beziers: Vec<BezPath> = res.paths.iter().map(|p| p.path.to_bezpath()).collect();
|
||
let (mut uncovered, mut doubled) = (0, 0);
|
||
for yi in 0..h * 4 {
|
||
for xi in 0..w * 4 {
|
||
// (Off the half and quarter pixels, where hard edges lie.)
|
||
let p = Point::new(0.1093 + f64::from(xi) * 0.25, 0.1371 + f64::from(yi) * 0.25);
|
||
match beziers.iter().filter(|b| b.winding(p) != 0).count() {
|
||
0 => uncovered += 1,
|
||
1 => {}
|
||
_ => doubled += 1,
|
||
}
|
||
}
|
||
}
|
||
Seams { faces, gap, at, area, uncovered, doubled }
|
||
}
|
||
|
||
/// One trace of `art` checked: a line for the record, and what is wrong (empty when nothing is).
|
||
/// `expect`: the paths of each colour, where every region must survive.
|
||
fn run(art: &Art, soft: bool, bw: bool, params: &TraceParams, setting: &str, expect: Option<&[usize]>) -> (String, Vec<String>) {
|
||
let colours: &[[u8; 3]] = if bw { &BW } else { &COLOURS[..params.colors as usize] };
|
||
let img = if soft { art.soft(colours) } else { art.hard(colours) };
|
||
let res = trace(&img, params);
|
||
let s = check(&res, art.w, art.h, colours);
|
||
let label = format!("{} {} {} noise {} {setting}", art.name, if soft { "soft" } else { "hard" }, if bw { "B&W" } else { "Color" }, params.noise);
|
||
let line = format!(
|
||
"{label}: faces {:?}, gap {:.6} px at ({:.3}, {:.3}), area {:+.6} px², uncovered {}, doubled {}",
|
||
s.faces, s.gap, s.at.x, s.at.y, s.area, s.uncovered, s.doubled
|
||
);
|
||
let mut wrong = vec![];
|
||
if !s.ok() {
|
||
wrong.push(line.clone());
|
||
}
|
||
if let Some(e) = expect
|
||
&& s.faces != e
|
||
{
|
||
wrong.push(format!("{label}: faces {:?}, expected {e:?}", s.faces));
|
||
}
|
||
(line, wrong)
|
||
}
|
||
|
||
/// A fitting setting: its name, and what it changes.
|
||
type Setting = (&'static str, fn(&mut TraceParams));
|
||
|
||
const DEFAULT: Setting = ("default", |_| {});
|
||
const LOOSEST: Setting = ("paths 0 corners 0", |p| (p.paths, p.corners) = (0.0, 0.0));
|
||
const TIGHTEST: Setting = ("paths 100 corners 100", |p| (p.paths, p.corners) = (100.0, 100.0));
|
||
const SNAP: Setting = ("snap", |p| p.snap_curves_to_lines = true);
|
||
const SHAPES: Setting = ("shapes", |p| p.shapes = true);
|
||
|
||
/// The defaults, both ends of Paths and Corners, Snap Curves To Lines, and Shapes.
|
||
const ALL: [Setting; 5] = [DEFAULT, LOOSEST, TIGHTEST, SNAP, SHAPES];
|
||
|
||
/// Runs every variant of `art` with each of `settings`: hard-edged always, anti-aliased when
|
||
/// `soft`, Color with `n` colours, and Black and White when the art has two. Hard-edged, every
|
||
/// region must survive (Noise 1 removes nothing), except that at Paths 0 a region of under four
|
||
/// pixels may go, if what it leaves goes with it (the geometric checks see to that).
|
||
fn variants(art: &Art, n: usize, soft: bool, settings: &[Setting]) -> Vec<String> {
|
||
let labels = art.labels();
|
||
let counts = components(&labels, art.w as usize, art.h as usize, n);
|
||
let tiny = smallest(&labels, art.w as usize, art.h as usize) < 4;
|
||
let mut wrong = vec![];
|
||
let mut runs = vec![(false, false, color(n as u32, 1))];
|
||
if n == 2 {
|
||
runs.push((false, true, black_and_white(1)));
|
||
}
|
||
if soft {
|
||
runs.push((true, false, color(n as u32, 1)));
|
||
if n == 2 {
|
||
runs.push((true, true, black_and_white(1)));
|
||
}
|
||
}
|
||
for (soft, bw, params) in runs {
|
||
for &(name, set) in settings {
|
||
let mut params = params.clone();
|
||
set(&mut params);
|
||
let expect = (!soft && !(tiny && name == LOOSEST.0)).then_some(counts.as_slice());
|
||
let (line, w) = run(art, soft, bw, ¶ms, name, expect);
|
||
println!("{line}");
|
||
wrong.extend(w);
|
||
}
|
||
}
|
||
wrong
|
||
}
|
||
|
||
fn all_variants(art: &Art, n: usize, soft: bool) -> Vec<String> {
|
||
variants(art, n, soft, &ALL)
|
||
}
|
||
|
||
fn assert_none(wrong: Vec<String>) {
|
||
assert!(wrong.is_empty(), "shared boundaries broken:\n{}", wrong.join("\n"));
|
||
}
|
||
|
||
// --- The drawings ---------------------------------------------------------------------------
|
||
|
||
fn in_disc(x: f64, y: f64, cx: f64, cy: f64, r: f64) -> bool {
|
||
(x - cx).powi(2) + (y - cy).powi(2) <= r * r
|
||
}
|
||
|
||
fn square_island() -> Art {
|
||
Art::new("square island", 32, 32, |x, y| usize::from((9.0..23.0).contains(&x) && (8.0..22.0).contains(&y)))
|
||
}
|
||
|
||
fn disc_island() -> Art {
|
||
Art::new("disc island", 32, 32, |x, y| usize::from(in_disc(x, y, 16.3, 15.8, 8.6)))
|
||
}
|
||
|
||
/// A ring with a core of a third colour in its hole.
|
||
fn nested() -> Art {
|
||
Art::new("nested ring and core", 40, 40, |x, y| {
|
||
if in_disc(x, y, 20.2, 19.7, 6.3) {
|
||
2
|
||
} else if in_disc(x, y, 20.2, 19.7, 13.4) {
|
||
1
|
||
} else {
|
||
0
|
||
}
|
||
})
|
||
}
|
||
|
||
/// A ring whose hole shows the background: one colour, two separate areas of it.
|
||
fn ring() -> Art {
|
||
Art::new("ring with a hole", 40, 40, |x, y| usize::from(in_disc(x, y, 20.2, 19.7, 13.4) && !in_disc(x, y, 20.2, 19.7, 6.3)))
|
||
}
|
||
|
||
fn square_ring() -> Art {
|
||
Art::new("square ring", 32, 32, |x, y| {
|
||
let outer = (6.0..26.0).contains(&x) && (6.0..26.0).contains(&y);
|
||
let inner = (12.0..20.0).contains(&x) && (12.0..20.0).contains(&y);
|
||
usize::from(outer && !inner)
|
||
})
|
||
}
|
||
|
||
fn wavy_seam() -> Art {
|
||
Art::new("wavy seam", 32, 32, |x, y| usize::from(x > 16.0 + 3.2 * (y * std::f64::consts::TAU / 14.0 + 0.4).sin()))
|
||
}
|
||
|
||
/// Left, upper right and lower right meet at one point inside the image.
|
||
fn three_regions() -> Art {
|
||
Art::new("three-region junction", 32, 32, |x, y| {
|
||
if x < 15.4 + 0.15 * (y - 16.0) {
|
||
0
|
||
} else if y < 16.3 + 0.2 * (x - 16.0) {
|
||
1
|
||
} else {
|
||
2
|
||
}
|
||
})
|
||
}
|
||
|
||
/// The 3 × 3 pattern 111 / 101 / 110, each cell `k` pixels square: the middle hole and the
|
||
/// corner of the background touch at one pixel corner, so the outline round the middle leaves that
|
||
/// corner and comes back to it.
|
||
fn returning(k: u32) -> Art {
|
||
let name: &'static str = match k {
|
||
1 => "returning loop 1 px",
|
||
2 => "returning loop 2 px",
|
||
3 => "returning loop 3 px",
|
||
_ => "returning loop 6 px",
|
||
};
|
||
let kf = f64::from(k);
|
||
Art::new(name, 3 * k, 3 * k, move |x, y| {
|
||
let (cx, cy) = ((x / kf) as usize, (y / kf) as usize);
|
||
usize::from(!matches!((cx, cy), (1, 1) | (2, 2)))
|
||
})
|
||
}
|
||
|
||
/// The same pattern inside a frame of the ink colour: the returning outline is an inner one.
|
||
fn returning_framed() -> Art {
|
||
Art::new("returning loop framed", 24, 24, |x, y| {
|
||
let (cx, cy) = (((x - 3.0) / 6.0).floor() as i32, ((y - 3.0) / 6.0).floor() as i32);
|
||
usize::from(!matches!((cx, cy), (1, 1) | (2, 2)))
|
||
})
|
||
}
|
||
|
||
fn one_pixel_hole() -> Art {
|
||
Art::new("one-pixel hole", 24, 24, |x, y| {
|
||
usize::from((5.0..19.0).contains(&x) && (5.0..19.0).contains(&y) && !(x as u32 == 11 && y as u32 == 12))
|
||
})
|
||
}
|
||
|
||
fn one_pixel_island() -> Art {
|
||
Art::new("one-pixel island", 15, 15, |x, y| usize::from(x as u32 == 7 && y as u32 == 7))
|
||
}
|
||
|
||
fn disconnected_squares() -> Art {
|
||
Art::new("disconnected squares", 24, 20, |x, y| {
|
||
usize::from(((3.0..7.0).contains(&x) && (3.0..7.0).contains(&y)) || ((16.0..20.0).contains(&x) && (12.0..16.0).contains(&y)))
|
||
})
|
||
}
|
||
|
||
fn disconnected_discs() -> Art {
|
||
Art::new("disconnected discs", 40, 24, |x, y| usize::from(in_disc(x, y, 10.3, 11.7, 6.2) || in_disc(x, y, 28.6, 12.2, 5.4)))
|
||
}
|
||
|
||
/// Two squares of one colour that touch only at a corner: two regions, not one.
|
||
fn corner_contact() -> Art {
|
||
Art::new("corner contact", 24, 24, |x, y| {
|
||
usize::from(((4.0..12.0).contains(&x) && (4.0..12.0).contains(&y)) || ((12.0..20.0).contains(&x) && (12.0..20.0).contains(&y)))
|
||
})
|
||
}
|
||
|
||
/// Slanted hatch strokes two pixels wide, one pixel of paper between them (along each row).
|
||
fn hatches() -> Art {
|
||
Art::new("parallel hatches", 32, 32, |x, y| {
|
||
let inside = (4.0..28.0).contains(&x) && (4.0..28.0).contains(&y);
|
||
usize::from(inside && (x - (y / 2.0).floor()).rem_euclid(3.0) < 2.0)
|
||
})
|
||
}
|
||
|
||
/// Upright strokes two pixels wide with one-pixel paper channels, closed at both ends by a bar:
|
||
/// each channel is a hole.
|
||
fn closed_channels() -> Art {
|
||
Art::new("closed paper channels", 32, 24, |x, y| {
|
||
let frame = (4.0..28.0).contains(&x) && (4.0..20.0).contains(&y);
|
||
let bar = !(6.0..18.0).contains(&y);
|
||
usize::from(frame && (bar || (x - 4.0).rem_euclid(3.0) < 2.0 || x >= 25.0))
|
||
})
|
||
}
|
||
|
||
// --- The tests ------------------------------------------------------------------------------
|
||
|
||
#[test]
|
||
fn a_closed_island_shares_its_whole_outline_with_the_background() {
|
||
let mut wrong = all_variants(&square_island(), 2, false);
|
||
wrong.extend(all_variants(&disc_island(), 2, true));
|
||
assert_none(wrong);
|
||
}
|
||
|
||
#[test]
|
||
fn a_ring_shares_its_outline_and_its_hole_with_its_neighbours() {
|
||
let mut wrong = all_variants(&nested(), 3, true);
|
||
wrong.extend(all_variants(&ring(), 2, true));
|
||
wrong.extend(all_variants(&square_ring(), 2, false));
|
||
assert_none(wrong);
|
||
}
|
||
|
||
#[test]
|
||
fn a_wavy_seam_is_one_curve_for_both_sides() {
|
||
assert_none(all_variants(&wavy_seam(), 2, true));
|
||
}
|
||
|
||
#[test]
|
||
fn three_regions_share_their_junction_and_edges() {
|
||
assert_none(all_variants(&three_regions(), 3, true));
|
||
}
|
||
|
||
/// The drawings whose outlines leave a junction and come back to it.
|
||
fn returning_arts() -> Vec<Art> {
|
||
vec![returning(1), returning(2), returning(3), returning(6), returning_framed(), corner_contact()]
|
||
}
|
||
|
||
#[test]
|
||
fn an_outline_returning_to_its_junction_is_shared() {
|
||
let mut wrong = vec![];
|
||
for art in returning_arts() {
|
||
wrong.extend(variants(&art, 2, false, &[DEFAULT, TIGHTEST, SNAP]));
|
||
}
|
||
assert_none(wrong);
|
||
}
|
||
|
||
/// At Paths 0 and Corners 0, a piece that leaves a junction and returns to it is fitted with fewer
|
||
/// than two curves: the region it is the whole outline of is dropped (a closed subpath needs two),
|
||
/// and its neighbour keeps a single curve from the junction back to itself, so a hole the size of
|
||
/// the region is left uncovered (1 to 6.7 px wide here, 0.3 to 108 px²).
|
||
#[test]
|
||
fn an_outline_returning_to_its_junction_survives_the_loosest_fit() {
|
||
let mut wrong = vec![];
|
||
for art in returning_arts() {
|
||
wrong.extend(variants(&art, 2, false, &[LOOSEST]));
|
||
}
|
||
assert_none(wrong);
|
||
}
|
||
|
||
/// With Shapes, a region whose outline is a shape becomes that exact shape, but where its outline
|
||
/// runs through a junction (two parts of the other colour touch at a corner) the neighbour keeps
|
||
/// the traced curve: in Black and White (where the threshold moves hard edges by 1/256 px) the
|
||
/// square and its neighbour part by up to 0.0039 px, and a square on the image's corner reaches
|
||
/// 0.002 px past the frame.
|
||
#[test]
|
||
#[ignore = "Shapes replaces a region touching a junction with an exact shape that its neighbour does not share (lib.rs Layers::layer, the Shapes branch): up to 0.0039 px apart in Black and White"]
|
||
fn shapes_keep_the_edges_a_region_shares_at_a_junction() {
|
||
let mut wrong = vec![];
|
||
for art in returning_arts() {
|
||
wrong.extend(variants(&art, 2, false, &[SHAPES]));
|
||
}
|
||
assert_none(wrong);
|
||
}
|
||
|
||
#[test]
|
||
fn a_one_pixel_hole_and_island_survive_with_what_they_leave() {
|
||
let mut wrong = all_variants(&one_pixel_hole(), 2, false);
|
||
wrong.extend(all_variants(&one_pixel_island(), 2, false));
|
||
// At the default Noise both go, each together with the hole it would leave.
|
||
// (The hole goes into the square around it; the island into the background, leaving no hole.)
|
||
for (art, expect_island) in [(one_pixel_hole(), 1), (one_pixel_island(), 0)] {
|
||
for (bw, params) in [(false, color(2, 25)), (true, black_and_white(25))] {
|
||
let (line, w) = run(&art, false, bw, ¶ms, "default", Some(&[1, expect_island]));
|
||
println!("{line}");
|
||
wrong.extend(w);
|
||
}
|
||
}
|
||
assert_none(wrong);
|
||
}
|
||
|
||
#[test]
|
||
fn separate_areas_of_one_colour_stay_separate() {
|
||
let mut wrong = all_variants(&disconnected_squares(), 2, false);
|
||
wrong.extend(all_variants(&disconnected_discs(), 2, true));
|
||
// (Squares touching at a corner: see the returning-outline tests.)
|
||
assert_none(wrong);
|
||
}
|
||
|
||
#[test]
|
||
fn one_pixel_paper_channels_between_hatches_are_shared() {
|
||
let mut wrong = all_variants(&hatches(), 2, true);
|
||
wrong.extend(all_variants(&closed_channels(), 2, false));
|
||
assert_none(wrong);
|
||
}
|
||
|
||
/// The check itself: a path moved by a hundredth of a pixel, and stacked (Overlapping) layers, are
|
||
/// found.
|
||
#[test]
|
||
fn the_check_finds_a_hairline_gap_and_an_overlap() {
|
||
let art = square_island();
|
||
let colours = &COLOURS[..2];
|
||
let mut res = trace(&art.hard(colours), &color(2, 1));
|
||
let s = check(&res, art.w, art.h, colours);
|
||
assert!(s.ok(), "{s:?}");
|
||
let island = res.paths.iter_mut().find(|p| p.color == colours[1]).unwrap();
|
||
for a in &mut island.path.subpaths[0].anchors {
|
||
for p in [&mut a.p, &mut a.h_in, &mut a.h_out] {
|
||
p.x += 0.01;
|
||
}
|
||
}
|
||
let s = check(&res, art.w, art.h, colours);
|
||
assert!(!s.ok() && (s.gap - 0.01).abs() < 1e-6, "{s:?}");
|
||
let stacked = TraceParams { method: Method::Overlapping, ..color(2, 1) };
|
||
let s = check(&trace(&art.hard(colours), &stacked), art.w, art.h, colours);
|
||
assert!(s.doubled > 0 && s.area > 100.0, "{s:?}");
|
||
}
|