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:
@@ -300,7 +300,6 @@ pub struct WalkCandidate {
|
|||||||
pub score: f32,
|
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].
|
/// 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.
|
/// 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
|
/// 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)]
|
vals[idx.min(vals.len() - 1)]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// many ink vs background pixels would be covered. The "bg_painted"
|
/// Re-simulate the brush sweep over the final strokes and count
|
||||||
/// number is what gets drawn outside the glyph on the actual plot —
|
/// (bg_painted, total_swept, repaint) — bg+ink pixels under the disk,
|
||||||
/// that's the off-glyph ink that visible artifacts come from.
|
/// total covered, and extra hits beyond the first per pixel. The
|
||||||
fn measure_sweep(strokes: &[Vec<(f32, f32)>], grid: &Grid, brush_radius: f32)
|
/// baseline single-pass sweep has some repaint from adjacent-sample
|
||||||
-> (u32, u32)
|
/// disk overlap (~4× per pixel at step_size_factor=0.5); higher
|
||||||
{
|
/// values mean the path is doubling back over itself.
|
||||||
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.
|
|
||||||
fn measure_sweep_full(strokes: &[Vec<(f32, f32)>], grid: &Grid, brush_radius: f32)
|
fn measure_sweep_full(strokes: &[Vec<(f32, f32)>], grid: &Grid, brush_radius: f32)
|
||||||
-> (u32, u32, u32)
|
-> (u32, u32, u32)
|
||||||
{
|
{
|
||||||
@@ -729,7 +718,7 @@ impl Grid {
|
|||||||
/// Returns `None` once nothing remains worth painting, which lets
|
/// Returns `None` once nothing remains worth painting, which lets
|
||||||
/// `paint_fill` exit cleanly instead of burning through max_strokes
|
/// `paint_fill` exit cleanly instead of burning through max_strokes
|
||||||
/// on phantom 1-px gap attempts.
|
/// 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)>
|
-> Option<(f32, f32)>
|
||||||
{
|
{
|
||||||
let mut comp_id = vec![-1i32; self.unpainted.len()];
|
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
|
/// 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
|
/// pokes into bg on the outside of the bend. With walk_bg_penalty
|
||||||
/// heavy, the cut loses to the inside-corner-following direction.
|
/// 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
|
/// Find the nearest unpainted ink pixel reachable from `start` by walking
|
||||||
/// only through ink (painted OR unpainted), using SDF-weighted Dijkstra
|
/// only through ink (painted OR unpainted), using SDF-weighted Dijkstra
|
||||||
/// so the path hugs centerlines (high-SDF ridges are cheap, near-edge
|
/// 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 {
|
for stroke_idx in 0..params.max_strokes {
|
||||||
if grid.ink_remaining <= 0 { break; }
|
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,
|
Some(s) => s, None => break,
|
||||||
};
|
};
|
||||||
let path = trace_stroke(start, &mut grid, params, brush_radius, None, stroke_idx);
|
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
|
- 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 {
|
pub fn paint_fill_debug(hull: &Hull, params: &PaintParams) -> PaintDebug {
|
||||||
let bounds = [
|
let bounds = [
|
||||||
hull.bounds.x_min as f32, hull.bounds.y_min as f32,
|
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();
|
let mut walks: Vec<WalkTrace> = Vec::new();
|
||||||
for stroke_idx in 0..params.max_strokes {
|
for stroke_idx in 0..params.max_strokes {
|
||||||
if grid.ink_remaining <= 0 { break; }
|
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,
|
Some(s) => s, None => break,
|
||||||
};
|
};
|
||||||
let path = trace_stroke(start, &mut grid, params, brush_radius,
|
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
|
/// Coordinate-descent optimization over (almost) the whole PaintParams
|
||||||
/// surface, parallel-evaluated against a corpus of letters at multiple
|
/// surface, parallel-evaluated against a corpus of letters at multiple
|
||||||
/// scales. Each axis has a list of candidate values; the optimizer
|
/// 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]
|
#[test]
|
||||||
#[ignore]
|
#[ignore]
|
||||||
fn paint_sdf_calibration() {
|
fn paint_sdf_calibration() {
|
||||||
|
|||||||
Reference in New Issue
Block a user