mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Image Trace: the notch rule reads only its own piece
Where the pixel at a sharp corner falls to the other colour, the outline cuts the corner in two one-pixel steps, and between two straight edges of three cracks or more subpixel::outline makes those steps one point (a notch). Whether the edges were long enough was measured on the whole loop. An edge can run on past a junction in one region's loop while the region across it, whose loop turns at that junction, sees a shorter edge: one side made the notch and the other did not, so the shared piece got different points on each side and the abutting curves left a gap. The notch now also requires the three cracks of each edge next to it to lie in the piece (no junction among them), as the corner-pixel shift correction above it already does. Test (code-drawn): noisy_colour_images_leave_no_sample_uncovered traces 20 seeded 36 px images of soft overlapping blobs in four colours plus pixel noise (Color, 5 colours, Noise 1 and 2, Paths 0/50/100) and needs every one of five sample points per pixel to lie in some path: 93 uncovered before (seeds 9, 11, 13), 0 after. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018tBcdwNhptqtmX32JCGFzB
This commit is contained in:
committed by
bflatastic
co-authored by
Claude Opus 5.5
parent
f8af51a341
commit
1d72a6f895
@@ -222,6 +222,11 @@ pub(crate) fn outline(
|
||||
&& free(s1)
|
||||
&& free(s2)
|
||||
&& free(b)
|
||||
// The three cracks of `a` and of `b` next to the notch must lie in this piece. An edge
|
||||
// that runs on past a junction is shorter in the loop of the region across it, which
|
||||
// turns there: that region makes no notch, and the two would get different points.
|
||||
&& !(1..=2).any(|k| junction(edges[a].first + edges[a].len - k))
|
||||
&& !(1..=2).any(|k| junction(edges[b].first + k))
|
||||
{
|
||||
let (ea, eb) = (&edges[a], &edges[b]);
|
||||
// Edge `a` carried on one pixel further meets edge `b` carried back one.
|
||||
|
||||
@@ -1550,3 +1550,55 @@ fn a_crack_between_thin_slivers_has_the_same_shift_from_either_side() {
|
||||
assert!(probe.iter().all(|s| s[0] == -s[1]), "threshold {threshold}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Noisy multi-colour images (soft blobs, slivers, specks) at several Paths and Noise values: no
|
||||
/// sample point is left uncovered by the abutting paths. A notch (a corner pixel cut in two steps)
|
||||
/// is made only where the straight edges beside it lie in the piece itself: measured over the whole
|
||||
/// loop, an edge that runs on past a junction made one region cut the corner and the region across
|
||||
/// the edge, whose loop turns at that junction, not (seeds 9, 11 and 13 here).
|
||||
#[test]
|
||||
fn noisy_colour_images_leave_no_sample_uncovered() {
|
||||
use kurbo::Shape;
|
||||
let hash =
|
||||
|x: u32, y: u32, s: u32| (x.wrapping_mul(73_856_093) ^ y.wrapping_mul(19_349_663) ^ s.wrapping_mul(83_492_791)).wrapping_mul(2_654_435_761);
|
||||
let mut uncovered = vec![];
|
||||
for seed in 0..20u32 {
|
||||
let (w, h) = (36u32, 36u32);
|
||||
// Overlapping soft blobs of four colours plus pixel noise.
|
||||
let img = Raster::from_fn(w, h, |x, y| {
|
||||
let mut c = [235.0f64, 230.0, 220.0];
|
||||
for b in 0..5u32 {
|
||||
let cx = f64::from(hash(b, 1, seed) % w);
|
||||
let cy = f64::from(hash(b, 2, seed) % h);
|
||||
let r = 3.0 + f64::from(hash(b, 3, seed) % 9);
|
||||
let d = ((f64::from(x) + 0.5 - cx).powi(2) + (f64::from(y) + 0.5 - cy).powi(2)).sqrt();
|
||||
let a = (r + 0.7 - d).clamp(0.0, 1.4) / 1.4;
|
||||
let col = [[20.0, 20.0, 30.0], [200.0, 40.0, 50.0], [40.0, 90.0, 200.0], [120.0, 120.0, 120.0]][(b % 4) as usize];
|
||||
for k in 0..3 {
|
||||
c[k] = c[k] * (1.0 - a) + col[k] * a;
|
||||
}
|
||||
}
|
||||
let n = f64::from(hash(x, y, seed) % 41) - 20.0;
|
||||
let p = c.map(|v| (v + n).clamp(0.0, 255.0) as u8);
|
||||
[p[0], p[1], p[2], 255]
|
||||
});
|
||||
for noise in [1, 2] {
|
||||
for paths in [0.0, 50.0, 100.0] {
|
||||
let params = TraceParams { mode: Mode::Color, colors: 5, noise, paths, ..TraceParams::default() };
|
||||
let res = trace(&img, ¶ms);
|
||||
let shapes: Vec<_> = res.paths.iter().map(|p| p.path.to_bezpath()).collect();
|
||||
for y in 1..h - 1 {
|
||||
for x in 1..w - 1 {
|
||||
for (dx, dy) in [(0.5, 0.5), (0.25, 0.25), (0.75, 0.25), (0.25, 0.75), (0.75, 0.75)] {
|
||||
let pt = kurbo::Point::new(f64::from(x) + dx, f64::from(y) + dy);
|
||||
if !shapes.iter().any(|b| b.winding(pt) != 0) {
|
||||
uncovered.push((seed, noise, paths, pt));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(uncovered.is_empty(), "{} uncovered (seed, noise, paths, point), e.g. {:?}", uncovered.len(), &uncovered[..uncovered.len().min(5)]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user