diff --git a/crates/trace/src/quantize.rs b/crates/trace/src/quantize.rs index 4a0225ac2..ce51bf72d 100644 --- a/crates/trace/src/quantize.rs +++ b/crates/trace/src/quantize.rs @@ -1271,21 +1271,7 @@ fn rare_colours(img: &Raster, own: &[bool], choose: &[bool], hk: &HistKey, cente *o = best.and_then(|b| b.2); } } - let mut out: Vec<[f64; 3]> = vec![]; - let mut traced_as = vec![]; - for (((_, _, c, _), o), &from) in chosen.iter().zip(&onto).zip(&other_of) { - if let Some(o) = o { - traced_as.push((*c, *o, from)); - continue; - } - if out.len() >= RARE_BUDGET { - break; - } - let lab = oklab(*c); - if out.iter().all(|o| dist(oklab(*o), lab) >= RARE_APART) { - out.push(*c); - } - } + let (out, traced_as) = kept_rare(chosen.iter().zip(&onto).zip(&other_of).map(|(((_, _, c, _), o), &from)| (*c, *o, from))); // The index in `centers` followed by `out` of a palette color or a rare color, or of the kept // rare color one that is not kept traces as. let index = |from: usize| -> Option { @@ -1309,6 +1295,33 @@ fn rare_colours(img: &Raster, own: &[bool], choose: &[bool], hk: &HistKey, cente (out, traced_as) } +/// Of `colors` (a rare color, the rare color it traces as, and where the other color of its pair +/// comes from), largest region first: the rare colors kept, at most [`RARE_BUDGET`] and each at +/// least [`RARE_APART`] from the ones before it, and every color that traces as another, past the +/// budget too. +pub(crate) fn kept_rare(colors: impl IntoIterator, Option)>) -> (Vec<[f64; 3]>, Vec) { + let dist = |a: [f64; 3], b: [f64; 3]| (0..3).map(|k| (a[k] - b[k]).powi(2)).sum::().sqrt(); + let mut out: Vec<[f64; 3]> = vec![]; + let mut traced_as = vec![]; + for (c, o, from) in colors { + if let Some(o) = o { + traced_as.push((c, o, from)); + continue; + } + if out.len() >= RARE_BUDGET { + continue; + } + let lab = oklab(c); + if out.iter().all(|o| dist(oklab(*o), lab) >= RARE_APART) { + out.push(c); + } + } + (out, traced_as) +} + +/// A color, the rare color it traces as, and where the other color of its pair comes from. +pub(crate) type TracedAsColor = ([f64; 3], [f64; 3], Option); + /// Whether `c` (sRGB) lies between two of `centers`, as anti-aliasing mixes them: within 12 levels /// of the line between them, and at least an eighth of the way from each. fn is_mix(c: [f64; 3], centers: &[[f64; 3]]) -> bool { diff --git a/crates/trace/src/tests.rs b/crates/trace/src/tests.rs index 74bf738a1..6ad909495 100644 --- a/crates/trace/src/tests.rs +++ b/crates/trace/src/tests.rs @@ -2461,13 +2461,24 @@ fn automatic_traces_an_outline_over_a_cut_edge_in_the_color_of_other_art() { .iter() .any(|&(x0, y0, x1, y1)| (x0..x1).contains(&x) && (y0..y1).contains(&y)) }; + // The dark paths reach the top and the bottom of the disc, which no bar comes near: the outline + // is traced, not only the bars. + use kurbo::Shape; + let target = quantize::tests_oklab([110, 80, 40]); + let dark_reaches = |res: &TraceResult, edge: &dyn Fn(kurbo::Rect) -> bool| { + res.paths.iter().any(|p| { + let o = quantize::tests_oklab(p.color); + (0..3).map(|k| (o[k] - target[k]).powi(2)).sum::().sqrt() <= 3.0 && edge(p.path.to_bezpath().bounding_box()) + }) + }; let mut failed = vec![]; for width in [1.5, 2.0, 2.5] { let outline = move |x: f64, y: f64| (r(x, y) - 140.0).abs() < width / 2.0; let img = cut_out_layers(&[(&disc, light, 1.0), (&block, blue, 1.0), (&bars, dark, 1.0), (&outline, dark, 0.8)]); for detail in [85.0, 50.0] { let res = trace(&img, &TraceParams { color_detail: detail, ..preset("Auto Color").unwrap() }); - if distinct_colours(&res) != 3 || !has_colour(&res, [110, 80, 40], 3.0) || res.paths.len() > 24 { + let ring = dark_reaches(&res, &|b| b.y0 < 15.0) && dark_reaches(&res, &|b| b.y1 > 285.0); + if distinct_colours(&res) != 3 || !has_colour(&res, [110, 80, 40], 3.0) || !ring || res.paths.len() > 24 { failed.push(format!("outline {width} px, color detail {detail}: {} paths, {:?}", res.paths.len(), res.palette)); } } @@ -2546,6 +2557,26 @@ fn automatic_keeps_an_opaque_line_between_two_kept_colors() { assert!(has_colour(&res, [204, 190, 155], 3.0), "{:?}", res.palette); } +#[test] +fn a_cut_edge_blend_past_the_rare_budget_still_traces_as_its_kept_color() { + // Seven distinct rare colors, largest first, fill the budget of six, and then the blend of the + // first one along a cut edge: the blend still traces as that kept color. + let rare: [[f64; 3]; 7] = [ + [200.0, 30.0, 40.0], + [30.0, 160.0, 60.0], + [240.0, 200.0, 20.0], + [120.0, 40.0, 160.0], + [20.0, 200.0, 210.0], + [250.0, 120.0, 180.0], + [90.0, 60.0, 20.0], + ]; + let blend = [220.0, 120.0, 120.0]; + let colors = rare.iter().map(|&c| (c, None, None)).chain([(blend, Some(rare[0]), None)]); + let (out, traced_as) = quantize::kept_rare(colors); + assert_eq!(out, rare[..6]); + assert_eq!(traced_as, [(blend, rare[0], None)]); +} + #[test] fn noise_keeps_a_kept_rare_colours_regions_but_not_its_specks() { // Labels: 0 background, 1 a rare colour kept on purpose (four islands of 4, 6, 9 and 2