mirror of
https://github.com/visioncortex/vtracer.git
synced 2026-09-29 15:31:19 -07:00
A new pipeline slot between curve fitting and composition: CurvePasses rewrite each fitted contour, so mosaic mode transforms every shared boundary segment exactly once and the tessellation stays seam-free by construction. SimplifyCurves re-fits each smooth run between corners with the fewest cubics within the tolerance (Schneider's algorithm via a current flo_curves — visioncortex's copy is pinned to an old one and block-splits at 200 points), with tangents from the chain's own ends, corners kept in place, junction endpoints pinned bit-for-bit, and rings seamed at their sharpest junction. Off by default; polylines pass through untouched. Cityscape at tolerance 1: 229 -> 138 KB stacked, 103 -> 36 KB watershed cutout, with render diffs under golden noise. CurveFitter now returns Vec<FittedGeom> (promoted from mosaic::fit) so stacked contours flow through the same pass machinery; the optimizer's SimplifyPass is renamed CleanupPass to free the word.
94 lines
2.8 KiB
Rust
94 lines
2.8 KiB
Rust
//! The curve-simplification stage, end to end: `Config::simplify` must cut
|
|
//! anchor counts in both compositing modes without changing geometry kind,
|
|
//! and leave output untouched when off (the goldens enforce the byte-level
|
|
//! version of that).
|
|
|
|
use vtracer::ir::PathCmd;
|
|
use vtracer::{ColorImage, Config, FitMode, Hierarchical, VectorDoc};
|
|
|
|
/// A filled disc — one long smooth boundary, the best case for merging the
|
|
/// per-splice cubics the spline fitter emits.
|
|
fn disc_image(size: usize) -> ColorImage {
|
|
let mut pixels = Vec::with_capacity(size * size * 4);
|
|
let (c, r) = (size as f64 / 2.0, size as f64 * 0.4);
|
|
for y in 0..size {
|
|
for x in 0..size {
|
|
let (dx, dy) = (x as f64 + 0.5 - c, y as f64 + 0.5 - c);
|
|
let (rr, gg, bb) = if (dx * dx + dy * dy).sqrt() < r {
|
|
(200, 60, 60)
|
|
} else {
|
|
(240, 240, 240)
|
|
};
|
|
pixels.extend_from_slice(&[rr, gg, bb, 255]);
|
|
}
|
|
}
|
|
ColorImage {
|
|
pixels,
|
|
width: size,
|
|
height: size,
|
|
}
|
|
}
|
|
|
|
fn cubic_count(doc: &VectorDoc) -> usize {
|
|
doc.shapes
|
|
.iter()
|
|
.flat_map(|s| &s.path.subpaths)
|
|
.flat_map(|sub| &sub.commands)
|
|
.filter(|c| matches!(c, PathCmd::CubicTo(..)))
|
|
.count()
|
|
}
|
|
|
|
fn run(config: &Config) -> VectorDoc {
|
|
config.build().unwrap().run(&disc_image(128)).unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn simplify_reduces_cubics_in_stacked_mode() {
|
|
let base = Config::default();
|
|
let simplified = Config {
|
|
simplify: Some(2.0),
|
|
..Config::default()
|
|
};
|
|
let (before, after) = (cubic_count(&run(&base)), cubic_count(&run(&simplified)));
|
|
assert!(before > 0, "the disc must be traced with cubics");
|
|
assert!(
|
|
after < before,
|
|
"simplify must reduce anchors: {before} -> {after}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn simplify_reduces_cubics_in_cutout_mode() {
|
|
let cutout = |simplify| Config {
|
|
hierarchical: Hierarchical::Cutout,
|
|
simplify,
|
|
..Config::default()
|
|
};
|
|
let (before, after) = (
|
|
cubic_count(&run(&cutout(None))),
|
|
cubic_count(&run(&cutout(Some(2.0)))),
|
|
);
|
|
assert!(before > 0, "the disc must be traced with cubics");
|
|
assert!(
|
|
after < before,
|
|
"simplify must reduce anchors: {before} -> {after}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn simplify_leaves_polyline_modes_untouched() {
|
|
for mode in [FitMode::Pixel, FitMode::Polygon] {
|
|
let base = Config {
|
|
mode,
|
|
..Config::default()
|
|
};
|
|
let simplified = Config {
|
|
simplify: Some(2.0),
|
|
..base.clone()
|
|
};
|
|
let a = base.build().unwrap().to_svg(&disc_image(64)).unwrap();
|
|
let b = simplified.build().unwrap().to_svg(&disc_image(64)).unwrap();
|
|
assert_eq!(a, b, "{mode:?} output must not change");
|
|
}
|
|
}
|