brush-paint: delete dead code (~250 lines)

- `lookahead_score`: unused; walker uses `lookahead_score_breakdown`.
- `measure_sweep`: unused; only `measure_sweep_full` is called.
- `paint_fill_sweep` / `_radius` / `_grid` + `ParamGrid`: pre-optimizer
  Cartesian-grid wrappers, only consumed by two `#[ignore]` exploration
  tests. The coordinate-descent optimizer in `brush_paint_opt` replaced
  this approach. Removed the tests too.
- Unused `brush_radius` arg from `pick_next_component`.
- Fixed two orphaned doc-comment fragments left from earlier rewrites.
This commit is contained in:
Mitchell Hansen
2026-05-06 23:18:53 -07:00
parent 890d4e9eed
commit da653eb881

View File

@@ -300,7 +300,6 @@ pub struct WalkCandidate {
pub score: f32,
}
/// Re-simulate the brush sweep over the final strokes and count how
/// Percentile of the SDF distribution over all ink pixels. `q` ∈ [0, 1].
/// At q=1.0 returns the max; at q=0.95 returns the 95th-percentile value.
/// We use this to pick a brush radius that ignores junction spikes (where
@@ -315,22 +314,12 @@ fn sdf_percentile(dist: &std::collections::HashMap<(u32, u32), f32>, q: f32) ->
vals[idx.min(vals.len() - 1)]
}
/// many ink vs background pixels would be covered. The "bg_painted"
/// number is what gets drawn outside the glyph on the actual plot —
/// that's the off-glyph ink that visible artifacts come from.
fn measure_sweep(strokes: &[Vec<(f32, f32)>], grid: &Grid, brush_radius: f32)
-> (u32, u32)
{
let (bg, total, _) = measure_sweep_full(strokes, grid, brush_radius);
(bg, total)
}
/// Like measure_sweep but also returns the *repaint* count: the total
/// number of *extra* disk-stamps on ink pixels beyond the first. A clean
/// single-pass sweep has some baseline repaint from adjacent-sample disk
/// overlap (~4× per pixel at step_size_factor=0.5). Higher values mean
/// the path is doubling back over itself. Reported as total extra hits
/// across all ink pixels covered.
/// Re-simulate the brush sweep over the final strokes and count
/// (bg_painted, total_swept, repaint) — bg+ink pixels under the disk,
/// total covered, and extra hits beyond the first per pixel. The
/// baseline single-pass sweep has some repaint from adjacent-sample
/// disk overlap (~4× per pixel at step_size_factor=0.5); higher
/// values mean the path is doubling back over itself.
fn measure_sweep_full(strokes: &[Vec<(f32, f32)>], grid: &Grid, brush_radius: f32)
-> (u32, u32, u32)
{
@@ -729,7 +718,7 @@ impl Grid {
/// Returns `None` once nothing remains worth painting, which lets
/// `paint_fill` exit cleanly instead of burning through max_strokes
/// on phantom 1-px gap attempts.
fn pick_next_component(&mut self, min_component_pixels: u32, brush_radius: f32)
fn pick_next_component(&mut self, min_component_pixels: u32)
-> Option<(f32, f32)>
{
let mut comp_id = vec![-1i32; self.unpainted.len()];
@@ -845,27 +834,6 @@ fn vec_dot(a: (f32, f32), b: (f32, f32)) -> f32 { a.0 * b.0 + a.1 * b.1 }
/// has a disk straddling both legs (lots of new ink), but the same disk
/// pokes into bg on the outside of the bend. With walk_bg_penalty
/// heavy, the cut loses to the inside-corner-following direction.
fn lookahead_score(start: (f32, f32), dir: (f32, f32),
grid: &Grid, params: &PaintParams,
brush_radius: f32, step_size: f32) -> f32
{
let mut total_new: f32 = 0.0;
let mut total_repaint: f32 = 0.0;
let mut total_bg: f32 = 0.0;
for k in 1..=params.lookahead_steps {
let p = (start.0 + dir.0 * step_size * k as f32,
start.1 + dir.1 * step_size * k as f32);
let (new, repaint, bg) = grid.evaluate_disk(p, brush_radius);
let weight = 1.0 / (k as f32);
total_new += new as f32 * weight;
total_repaint += repaint as f32 * weight;
total_bg += bg as f32 * weight;
}
total_new
- params.overpaint_penalty * total_repaint
- params.walk_bg_penalty * total_bg
}
/// Find the nearest unpainted ink pixel reachable from `start` by walking
/// only through ink (painted OR unpainted), using SDF-weighted Dijkstra
/// so the path hugs centerlines (high-SDF ridges are cheap, near-edge
@@ -1466,7 +1434,7 @@ pub fn paint_fill_with(hull: &Hull, params: &PaintParams) -> FillResult {
for stroke_idx in 0..params.max_strokes {
if grid.ink_remaining <= 0 { break; }
let start = match grid.pick_next_component(min_component_pixels, brush_radius) {
let start = match grid.pick_next_component(min_component_pixels) {
Some(s) => s, None => break,
};
let path = trace_stroke(start, &mut grid, params, brush_radius, None, stroke_idx);
@@ -1799,148 +1767,6 @@ pub fn score_weighted(m: &PaintMetrics, w: ScoreWeights) -> f32 {
- w.brush_size * m.brush_radius
}
/// Run `paint_fill_with` once per `params_variant` and return the best
/// result by `score` (lower wins). The metrics + score are returned
/// alongside so callers can diagnose / log.
pub fn paint_fill_sweep<F>(
hull: &Hull,
params_variants: &[PaintParams],
score: F,
) -> Option<(FillResult, PaintMetrics, f32)>
where F: Fn(&PaintMetrics) -> f32,
{
params_variants.iter().map(|p| {
let (r, m) = metrics_for(hull, p);
let s = score(&m);
(r, m, s)
}).min_by(|a, b| a.2.partial_cmp(&b.2).unwrap_or(std::cmp::Ordering::Equal))
}
/// Convenience wrapper: sweep the brush radius across a range of
/// absolute pixel values, holding all other params fixed. The
/// `brush_radius_factor`/`offset` knobs are repurposed inside each
/// variant so the resulting brush_radius equals the swept value.
pub fn paint_fill_sweep_radius<F>(
hull: &Hull,
base: &PaintParams,
radii_px: &[f32],
score: F,
) -> Option<(FillResult, PaintMetrics, f32)>
where F: Fn(&PaintMetrics) -> f32,
{
let pixel_set: HashSet<(u32, u32)> = hull.pixels.iter().copied().collect();
let dist = chamfer_distance(hull, &pixel_set);
let eff = sdf_percentile(&dist, base.brush_radius_percentile).max(0.5);
let variants: Vec<PaintParams> = radii_px.iter().map(|&r| {
let mut p = base.clone();
// factor*eff + offset = r → factor = (r - offset)/eff
p.brush_radius_factor = ((r - p.brush_radius_offset_px) / eff).max(0.01);
p
}).collect();
paint_fill_sweep(hull, &variants, score)
}
/// Multi-knob grid sweep. Each field holds the candidate values for that
/// knob; the Cartesian product is built and every combination is tried.
/// An empty Vec means "leave at base value" (one candidate, the base's).
#[derive(Debug, Clone, Default)]
pub struct ParamGrid {
pub brush_radii_px: Vec<f32>, // absolute brush radii in px (per-hull)
pub brush_radius_factors: Vec<f32>, // global × sdf_percentile
pub walk_bg_penalties: Vec<f32>,
pub bg_penalties: Vec<f32>, // polish bg penalty
pub polish_search_factors: Vec<f32>,
pub polish_iters_set: Vec<u32>,
pub overpaint_penalties: Vec<f32>,
pub momentum_weights: Vec<f32>,
}
impl ParamGrid {
/// Build all PaintParams variants from the Cartesian product of
/// candidate values (combined with `base` for any empty axis).
pub fn variants(&self, base: &PaintParams, hull: &Hull) -> Vec<PaintParams> {
let pixel_set: HashSet<(u32, u32)> = hull.pixels.iter().copied().collect();
let dist = chamfer_distance(hull, &pixel_set);
let eff = sdf_percentile(&dist, base.brush_radius_percentile).max(0.5);
// Collect knobs as (setter, candidates) pairs; empty axes fall back
// to the base value as a single candidate.
type Setter = Box<dyn Fn(&mut PaintParams, f32)>;
let knobs: Vec<(Vec<f32>, Setter)> = vec![
(
if self.brush_radii_px.is_empty() { vec![f32::NAN] } else { self.brush_radii_px.clone() },
Box::new(move |p: &mut PaintParams, r: f32| {
if !r.is_nan() {
p.brush_radius_factor = ((r - p.brush_radius_offset_px) / eff).max(0.01);
}
}),
),
(
if self.brush_radius_factors.is_empty() { vec![f32::NAN] } else { self.brush_radius_factors.clone() },
Box::new(|p, v| if !v.is_nan() { p.brush_radius_factor = v; }),
),
(
if self.polish_iters_set.is_empty() { vec![base.polish_iters as f32] } else {
self.polish_iters_set.iter().map(|&v| v as f32).collect()
},
Box::new(|p, v| p.polish_iters = v as u32),
),
(
if self.walk_bg_penalties.is_empty() { vec![base.walk_bg_penalty] } else { self.walk_bg_penalties.clone() },
Box::new(|p, v| p.walk_bg_penalty = v),
),
(
if self.bg_penalties.is_empty() { vec![base.bg_penalty] } else { self.bg_penalties.clone() },
Box::new(|p, v| p.bg_penalty = v),
),
(
if self.polish_search_factors.is_empty() { vec![base.polish_search_factor] } else { self.polish_search_factors.clone() },
Box::new(|p, v| p.polish_search_factor = v),
),
(
if self.overpaint_penalties.is_empty() { vec![base.overpaint_penalty] } else { self.overpaint_penalties.clone() },
Box::new(|p, v| p.overpaint_penalty = v),
),
(
if self.momentum_weights.is_empty() { vec![base.momentum_weight] } else { self.momentum_weights.clone() },
Box::new(|p, v| p.momentum_weight = v),
),
];
// Cartesian product.
let mut variants = vec![base.clone()];
for (vals, setter) in &knobs {
let mut next: Vec<PaintParams> = Vec::with_capacity(variants.len() * vals.len());
for v in &variants {
for &val in vals {
let mut nv = v.clone();
setter(&mut nv, val);
next.push(nv);
}
}
variants = next;
}
variants
}
}
/// Run a multi-knob grid sweep and return the best variant by score.
pub fn paint_fill_sweep_grid<F>(
hull: &Hull,
base: &PaintParams,
grid: &ParamGrid,
score: F,
) -> Option<(FillResult, PaintMetrics, f32, PaintParams)>
where F: Fn(&PaintMetrics) -> f32,
{
let variants = grid.variants(base, hull);
variants.iter().map(|p| {
let (r, m) = metrics_for(hull, p);
let s = score(&m);
(r, m, s, p.clone())
}).min_by(|a, b| a.2.partial_cmp(&b.2).unwrap_or(std::cmp::Ordering::Equal))
}
pub fn paint_fill_debug(hull: &Hull, params: &PaintParams) -> PaintDebug {
let bounds = [
hull.bounds.x_min as f32, hull.bounds.y_min as f32,
@@ -1962,7 +1788,7 @@ pub fn paint_fill_debug(hull: &Hull, params: &PaintParams) -> PaintDebug {
let mut walks: Vec<WalkTrace> = Vec::new();
for stroke_idx in 0..params.max_strokes {
if grid.ink_remaining <= 0 { break; }
let start = match grid.pick_next_component(min_component_pixels, brush_radius) {
let start = match grid.pick_next_component(min_component_pixels) {
Some(s) => s, None => break,
};
let path = trace_stroke(start, &mut grid, params, brush_radius,
@@ -2248,60 +2074,6 @@ mod tests {
}
}
/// Sweep the brush radius over a wide range and report which radius
/// produces the best score (default_score: heavy on stroke count,
/// light on length, light on bg). Run this on a curated set of
/// letters at 5mm/425dpi to see whether the radius decision is
/// well-calibrated and whether it varies meaningfully by glyph.
#[test]
#[ignore]
fn paint_radius_sweep_5mm_425dpi() {
let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let font_mm = 5.0_f32;
let dpi = 425;
let thick = 9;
let radii: Vec<f32> = (40..=120).step_by(10).map(|x| x as f32 / 10.0).collect();
// 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0
let base = PaintParams::default();
println!("\nbrush-radius sweep — {}mm @ {}dpi/{}px", font_mm, dpi, thick);
println!("char | best_r | strokes | len(px) | bg | score | default_r | default_strokes");
let mut total_default_strokes = 0u32;
let mut total_best_strokes = 0u32;
let mut total_default_len = 0.0_f32;
let mut total_best_len = 0.0_f32;
for ch in chars.chars() {
let hulls = rasterize_letter_at(ch, font_mm, dpi, thick);
let main = match hulls.iter().max_by_key(|h| h.area) {
Some(h) => h, None => continue
};
// Default-params baseline.
let (_default_result, default_m) = metrics_for(main, &base);
let default_r = default_m.brush_radius;
// Sweep.
let (best_r, best_m, best_s) = match paint_fill_sweep_radius(
main, &base, &radii, default_score)
{
Some(t) => t, None => continue,
};
let _ = best_r;
total_default_strokes += default_m.strokes;
total_best_strokes += best_m.strokes;
total_default_len += default_m.total_length;
total_best_len += best_m.total_length;
println!(" {} | {:5.2} | {:2} | {:5.0} | {:3} | {:6.0} | {:4.2} | {:2}",
ch, best_m.brush_radius, best_m.strokes, best_m.total_length,
best_m.bg_painted, best_s, default_r, default_m.strokes);
}
println!("\ntotals: default={} strokes / {:.0}px sweep={} strokes / {:.0}px",
total_default_strokes, total_default_len,
total_best_strokes, total_best_len);
}
/// Coordinate-descent optimization over (almost) the whole PaintParams
/// surface, parallel-evaluated against a corpus of letters at multiple
/// scales. Each axis has a list of candidate values; the optimizer
@@ -2628,79 +2400,6 @@ mod tests {
}
}
/// Multi-knob grid sweep on the letters that bg-paint most. For each
/// letter, search a Cartesian product of (brush_radius × walk_bg_penalty
/// × bg_penalty × polish_search_factor) and report the best config
/// found by `default_score`.
#[test]
#[ignore]
fn paint_grid_sweep_problem_letters() {
// (char, font_mm, dpi, thickness)
let cases: &[(char, f32, u32, u32)] = &[
('F', 5.0, 200, 4), // user-flagged: sides red
('M', 5.0, 425, 9),
('W', 5.0, 425, 9),
('A', 5.0, 425, 9),
('K', 5.0, 425, 9),
];
let base = PaintParams::default();
for &(ch, font_mm, dpi, thick) in cases {
let hulls = rasterize_letter_at(ch, font_mm, dpi, thick);
let main = match hulls.iter().max_by_key(|h| h.area) {
Some(h) => h, None => continue
};
let pixel_set: HashSet<(u32, u32)> = main.pixels.iter().copied().collect();
let dist = chamfer_distance(main, &pixel_set);
let sdf_max = dist.values().copied().fold(0.0_f32, f32::max);
let half_w = (thick as f32) / 2.0;
// Grid: ranges chosen around the geometric ideal.
let grid = ParamGrid {
brush_radii_px: (0..=8).map(|i| half_w - 1.0 + 0.5 * i as f32).collect(),
walk_bg_penalties: vec![0.0, 2.0, 5.0, 10.0, 20.0],
bg_penalties: vec![1.0, 3.0, 6.0, 10.0],
polish_search_factors: vec![0.5, 1.5, 3.0],
..ParamGrid::default()
};
let n_variants = grid.variants(&base, main).len();
// Baseline metrics.
let (_, base_m) = metrics_for(main, &base);
let base_s = default_score(&base_m);
// Best from sweep.
let (_, best_m, best_s, best_p) = paint_fill_sweep_grid(
main, &base, &grid, default_score
).unwrap();
// Find which knobs changed.
let mut diff = String::new();
if (best_p.walk_bg_penalty - base.walk_bg_penalty).abs() > 1e-3 {
diff.push_str(&format!("walk_bg={:.1} ", best_p.walk_bg_penalty));
}
if (best_p.bg_penalty - base.bg_penalty).abs() > 1e-3 {
diff.push_str(&format!("bg_pen={:.1} ", best_p.bg_penalty));
}
if (best_p.polish_search_factor - base.polish_search_factor).abs() > 1e-3 {
diff.push_str(&format!("polish_search={:.2} ", best_p.polish_search_factor));
}
println!("\n'{}' @ {}mm/{}dpi/{}px (sdf_max={:.2}, half_w={:.2}, {} variants)",
ch, font_mm, dpi, thick, sdf_max, half_w, n_variants);
println!(" baseline: r={:.2} strokes={} len={:.0} bg={} unp={} score={:.0}",
base_m.brush_radius, base_m.strokes, base_m.total_length,
base_m.bg_painted, base_m.ink_unpainted, base_s);
println!(" best: r={:.2} strokes={} len={:.0} bg={} unp={} score={:.0}",
best_m.brush_radius, best_m.strokes, best_m.total_length,
best_m.bg_painted, best_m.ink_unpainted, best_s);
println!(" knobs changed: {}", if diff.is_empty() { "(brush radius only)" } else { &diff });
println!(" bg drop: {:.0}% → {:.0}%",
100.0 * base_m.bg_painted as f32 / (base_m.bg_painted + base_m.total_length.round() as u32 + 1) as f32,
100.0 * best_m.bg_painted as f32 / (best_m.bg_painted + best_m.total_length.round() as u32 + 1) as f32);
}
}
#[test]
#[ignore]
fn paint_sdf_calibration() {