mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-03 14:40:24 -06:00
M12: preserve tiny traced components and holes
This commit is contained in:
@@ -139,6 +139,13 @@ pub(crate) fn fit_closed(pts: &[Point], style: &Style) -> Option<Vec<Cubic>> {
|
||||
if stats.reverted > 0 && !all_within(&sample_cubics(&segs, 0.25), &resample_closed(&pts, 0.5).map_or_else(|| pts.clone(), |(p, _)| p), 1.5) {
|
||||
segs = polygon(&pts, style.tol);
|
||||
}
|
||||
// A coarse fit of a tiny loop can be a single cubic. A closed editable
|
||||
// subpath needs at least two anchors, so that fit would silently discard
|
||||
// a component (or hole) which survived Noise. Keep the positioned outline
|
||||
// instead; do not simplify it again with the tolerance that collapsed it.
|
||||
if segs.len() < 2 {
|
||||
segs = polygon(&pts, 0.0);
|
||||
}
|
||||
if style.upright {
|
||||
snap_upright(&mut segs, true, false);
|
||||
}
|
||||
@@ -244,3 +251,62 @@ fn polygon(pts: &[Point], tol: f64) -> Vec<Cubic> {
|
||||
let kept: Vec<Point> = (0..n).filter(|&i| keep[i]).map(|i| pts[i]).collect();
|
||||
(0..kept.len()).map(|i| [kept[i], kept[i], kept[(i + 1) % kept.len()], kept[(i + 1) % kept.len()]]).collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod component_survival_tests {
|
||||
use crate::{Raster, TraceParams, preset, trace};
|
||||
|
||||
fn params(corners: f64, noise: u32) -> TraceParams {
|
||||
TraceParams { paths: 50.0, corners, noise, ignore_white: true, ..preset("Black and White Logo").unwrap() }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_one_pixel_component_survives_fitting_at_every_corner_setting() {
|
||||
let image = Raster::from_fn(32, 32, |x, y| if x == 16 && y == 16 { [0, 0, 0, 255] } else { [255; 4] });
|
||||
for corners in [0.0, 25.0, 50.0, 75.0, 100.0] {
|
||||
for noise in [0, 1] {
|
||||
let result = trace(&image, ¶ms(corners, noise));
|
||||
assert_eq!(result.paths.len(), 1, "corners={corners}, noise={noise}");
|
||||
assert!(result.paths[0].path.subpaths[0].area().abs() > 0.1);
|
||||
}
|
||||
assert!(trace(&image, ¶ms(corners, 2)).paths.is_empty(), "Noise must still remove the one-pixel component");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_one_pixel_hole_survives_fitting_without_overriding_noise() {
|
||||
let image =
|
||||
Raster::from_fn(
|
||||
32,
|
||||
32,
|
||||
|x, y| if (4..28).contains(&x) && (4..28).contains(&y) && (x, y) != (16, 16) { [0, 0, 0, 255] } else { [255; 4] },
|
||||
);
|
||||
for corners in [0.0, 25.0, 50.0, 75.0, 100.0] {
|
||||
for noise in [0, 1] {
|
||||
let result = trace(&image, ¶ms(corners, noise));
|
||||
assert_eq!(result.paths.len(), 1);
|
||||
assert_eq!(result.paths[0].path.subpaths.len(), 2, "hole lost at corners={corners}, noise={noise}");
|
||||
let subs = &result.paths[0].path.subpaths;
|
||||
assert!(subs[0].area() * subs[1].area() < 0.0, "hole must keep opposite winding");
|
||||
}
|
||||
let cleaned = trace(&image, ¶ms(corners, 2));
|
||||
assert_eq!(cleaned.paths.len(), 1);
|
||||
assert_eq!(cleaned.paths[0].path.subpaths.len(), 1, "Noise must still fill the one-pixel hole");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_small_stepped_component_is_not_dropped_by_low_corners() {
|
||||
let image =
|
||||
Raster::from_fn(
|
||||
32,
|
||||
32,
|
||||
|x, y| if (10..14).contains(&x) && y == 10 || (10..13).contains(&x) && y == 11 { [0, 0, 0, 255] } else { [255; 4] },
|
||||
);
|
||||
for corners in [0.0, 25.0, 50.0, 75.0, 100.0] {
|
||||
let result = trace(&image, ¶ms(corners, 1));
|
||||
assert_eq!(result.paths.len(), 1, "stepped region lost at corners={corners}");
|
||||
assert!(result.paths[0].path.subpaths[0].area().abs() > 1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
# Image Trace quality
|
||||
|
||||
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (Gradients: grown over noise and soft rims)
|
||||
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (tiny fitted contours retain islands and holes; Gradients: grown over noise and soft rims)
|
||||
|
||||
What Image Trace (`crates/trace`) makes of images whose right answer is known, measured by a harness and
|
||||
compared with the numbers before each change. Nothing here is a judgement by eye, and nothing is compared
|
||||
with any other product except the open tracer named under [Yardstick](#4-yardstick).
|
||||
|
||||
## Tiny closed contours
|
||||
|
||||
A coarse fit can reduce a small closed outline to one cubic, which the editable-path builder cannot
|
||||
represent as a closed subpath with two anchors. Previously that dropped the component or hole after
|
||||
it survived Noise. The fitter now retains the positioned contour without further simplification when
|
||||
that minimum is not met. Corners and Noise defaults are unchanged.
|
||||
|
||||
Three source-generated regressions cover a one-pixel island, a one-pixel hole with opposite winding,
|
||||
and a stepped seven-pixel region across Corners values. They also check that Noise still removes the
|
||||
island and fills the hole when its threshold requests that. These are survival and winding checks,
|
||||
not a new quality-harness baseline or a general claim about small-feature fidelity.
|
||||
|
||||
## How it is measured
|
||||
|
||||
`crates/trace/tests/quality` draws its own inputs from vector art (no image files), traces each with every
|
||||
@@ -955,6 +967,7 @@ One trace at a time on this machine (all cores); memory is the peak allocated ab
|
||||
|
||||
| date | change |
|
||||
|:--|:--|
|
||||
| 2026-10-14 | Tiny closed fits retain their positioned outlines instead of losing islands/holes; three generated regression tests; quality-harness baseline unchanged |
|
||||
| 2026-10-14 | Gradients: a gradient that noise or a soft rim broke into pieces is found in its largest piece and grown over the rest (blurred and noisy linear gradients: 3 to 11 paths beside the gradient down to 0.5 with 6 Colors, 24 to 56 down to 0.5 to 1.5 with High Fidelity Photo) |
|
||||
| 2026-10-14 | Enhanced versions of the presets (Auto Grouping, and Shapes in all but High Fidelity Photo; Flat Logo has none), each measured beside its preset; a hole that one colour fills is the same shape as what fills it, rectangles are sized by where their sides lie, and a group keeps its paths in the order they were traced in (grouped, a letter with a counter was painted with a light seam: Default with Auto Grouping, edge ΔE 10.24 to 9.88) |
|
||||
| 2026-10-14 | Create: Strokes: a line whose ink is cut square at its ends gets square caps, and a wide line whose skeleton hooks towards a corner of its end ends on the line it was following (Outline: largest distance 2.94 px to 0.88; aliased lines 0.88 to 0.81) |
|
||||
|
||||
+3
-2
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user