From da653eb881c2a471dea3ffae358626006e3ca2ce Mon Sep 17 00:00:00 2001 From: Mitchell Hansen Date: Wed, 6 May 2026 23:18:53 -0700 Subject: [PATCH] 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. --- src/brush_paint.rs | 319 ++------------------------------------------- 1 file changed, 9 insertions(+), 310 deletions(-) diff --git a/src/brush_paint.rs b/src/brush_paint.rs index b335b445..73e25fc9 100644 --- a/src/brush_paint.rs +++ b/src/brush_paint.rs @@ -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( - 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( - 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 = 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, // absolute brush radii in px (per-hull) - pub brush_radius_factors: Vec, // global × sdf_percentile - pub walk_bg_penalties: Vec, - pub bg_penalties: Vec, // polish bg penalty - pub polish_search_factors: Vec, - pub polish_iters_set: Vec, - pub overpaint_penalties: Vec, - pub momentum_weights: Vec, -} - -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 { - 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; - let knobs: Vec<(Vec, 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 = 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( - 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 = 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 = (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() {