brush-paint: remove Dijkstra repaint fallback (~110 lines)
Walker had a fallback path for "stuck" cases: SDF-weighted Dijkstra through painted ink to reach the nearest unpainted pixel, gated by `pen_lift_penalty`. Default was 0, which effectively disabled it (any nonzero path cost broke the walker). The optimizer never landed on a meaningfully-large value either, and last attempt to push it (=50) produced visually-bad smear behaviour. Removing: - `nearest_unpainted_through_ink` function (~85 lines) - `pen_lift_penalty`, `pen_lift_reach` from PaintParams + Default - Both axes from brush_paint_opt and the duplicate-axes test - `pen_lift_*` from frontend mirror and lib.rs's per-hull optimizer - The Dijkstra branch in walk_brush — now `score < min_score || would_be_stuck` just BREAKs cleanly. Strokes now end where the walker decides they're done; there's no "keep going through painted ink to find more unpainted." Multi-leg letters (P, q, M's right downstroke) will need to be reached by multiple strokes from `pick_next_component`.
This commit is contained in:
@@ -52,8 +52,6 @@ export const DEFAULT_PAINT_PARAMS = {
|
|||||||
polish_search_factor: 0.5,
|
polish_search_factor: 0.5,
|
||||||
bg_penalty: 2.0,
|
bg_penalty: 2.0,
|
||||||
min_component_factor: 1.49,
|
min_component_factor: 1.49,
|
||||||
pen_lift_penalty: 0.0,
|
|
||||||
pen_lift_reach: 3.0,
|
|
||||||
max_steps_per_stroke: 4000,
|
max_steps_per_stroke: 4000,
|
||||||
max_strokes: 12,
|
max_strokes: 12,
|
||||||
output_rdp_eps: 1.98,
|
output_rdp_eps: 1.98,
|
||||||
|
|||||||
@@ -118,24 +118,10 @@ pub struct PaintParams {
|
|||||||
/// the brush is wider than the stroke.
|
/// the brush is wider than the stroke.
|
||||||
pub bg_penalty: f32,
|
pub bg_penalty: f32,
|
||||||
/// Minimum unpainted-ink component size (as a multiplier of brush
|
/// Minimum unpainted-ink component size (as a multiplier of brush
|
||||||
/// area = π·r²) to start a new stroke. Components smaller than this
|
/// area = π·r²) to be eligible as a stroke seed. Sub-threshold
|
||||||
/// are leftovers from the previous stroke's brush sweep that the
|
/// components stay in the unpainted mask and count against
|
||||||
/// relaxation didn't catch — we paint them with a single disk and
|
/// coverage but don't get a stroke of their own.
|
||||||
/// move on instead of attempting a doomed walk. 1.0 = "must be at
|
|
||||||
/// least one full brush-disc worth of unpainted ink."
|
|
||||||
pub min_component_factor: f32,
|
pub min_component_factor: f32,
|
||||||
/// "Pen lift" cost: how many ink-pixel-equivalents of overpaint+bg cost
|
|
||||||
/// the walker is willing to absorb to reach unpainted ink without
|
|
||||||
/// terminating the stroke. 0 = always terminate when local lookahead
|
|
||||||
/// dries up (= stroke per blob). Higher values let the walker double
|
|
||||||
/// back across already-painted ink to bridge to a new ink region —
|
|
||||||
/// e.g. M's bottom-V apex, where one stroke can naturally cover both
|
|
||||||
/// diagonals if it can pass through the painted apex.
|
|
||||||
pub pen_lift_penalty: f32,
|
|
||||||
/// How far (in brush radii) the bridge lookahead can reach when the
|
|
||||||
/// normal lookahead's best score is below `min_score`. Bridging only
|
|
||||||
/// kicks in when this is > step_size_factor × lookahead_steps.
|
|
||||||
pub pen_lift_reach: f32,
|
|
||||||
/// Cap.
|
/// Cap.
|
||||||
pub max_steps_per_stroke: u32,
|
pub max_steps_per_stroke: u32,
|
||||||
pub max_strokes: u32,
|
pub max_strokes: u32,
|
||||||
@@ -167,8 +153,6 @@ impl Default for PaintParams {
|
|||||||
polish_search_factor: 0.5,
|
polish_search_factor: 0.5,
|
||||||
bg_penalty: 2.0,
|
bg_penalty: 2.0,
|
||||||
min_component_factor: 1.49,
|
min_component_factor: 1.49,
|
||||||
pen_lift_penalty: 0.0,
|
|
||||||
pen_lift_reach: 3.0,
|
|
||||||
max_steps_per_stroke: 4000,
|
max_steps_per_stroke: 4000,
|
||||||
max_strokes: 12,
|
max_strokes: 12,
|
||||||
output_rdp_eps: 1.98,
|
output_rdp_eps: 1.98,
|
||||||
@@ -834,93 +818,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.
|
||||||
/// 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
|
|
||||||
/// pixels are expensive). Caller passes `max_pixel_radius` — the search
|
|
||||||
/// is cut off beyond that euclidean distance from start. Returns the
|
|
||||||
/// next-step direction toward the target, or None if no target is
|
|
||||||
/// reachable inside the budget. Cost is in "step pixels" so the caller
|
|
||||||
/// can compare it directly against `pen_lift_penalty`.
|
|
||||||
fn nearest_unpainted_through_ink(start: (f32, f32), grid: &Grid,
|
|
||||||
max_pixel_radius: f32)
|
|
||||||
-> Option<((f32, f32), f32)>
|
|
||||||
{
|
|
||||||
use std::collections::BinaryHeap;
|
|
||||||
use std::cmp::Reverse;
|
|
||||||
|
|
||||||
let sx = start.0.round() as i32;
|
|
||||||
let sy = start.1.round() as i32;
|
|
||||||
if !grid.is_ink(sx, sy) { return None; }
|
|
||||||
if grid.is_unpainted(sx, sy) {
|
|
||||||
// Nothing to do — caller would have a positive score in that case.
|
|
||||||
return Some(((0.0, 0.0), 0.0));
|
|
||||||
}
|
|
||||||
|
|
||||||
let r = max_pixel_radius.ceil() as i32;
|
|
||||||
let r2 = max_pixel_radius * max_pixel_radius;
|
|
||||||
let bx = sx - r;
|
|
||||||
let by = sy - r;
|
|
||||||
let span = 2 * r as usize + 1;
|
|
||||||
let cells = span * span;
|
|
||||||
let mut dist = vec![f32::INFINITY; cells];
|
|
||||||
let mut prev = vec![(-1i32, -1i32); cells];
|
|
||||||
let local = |x: i32, y: i32| -> Option<usize> {
|
|
||||||
let lx = x - bx; let ly = y - by;
|
|
||||||
if lx < 0 || ly < 0 || lx as usize >= span || ly as usize >= span { return None; }
|
|
||||||
Some(ly as usize * span + lx as usize)
|
|
||||||
};
|
|
||||||
|
|
||||||
let s_idx = local(sx, sy)?;
|
|
||||||
dist[s_idx] = 0.0;
|
|
||||||
let mut heap: BinaryHeap<(Reverse<u32>, i32, i32)> = BinaryHeap::new();
|
|
||||||
heap.push((Reverse(0), sx, sy));
|
|
||||||
|
|
||||||
while let Some((Reverse(d_int), x, y)) = heap.pop() {
|
|
||||||
let here = local(x, y)?;
|
|
||||||
let d = d_int as f32 / 1024.0;
|
|
||||||
if d > dist[here] + 1e-3 { continue; }
|
|
||||||
|
|
||||||
if (x, y) != (sx, sy) && grid.is_unpainted(x, y) {
|
|
||||||
// Reconstruct: walk prev[] back to start, take FIRST step.
|
|
||||||
let mut cx = x; let mut cy = y;
|
|
||||||
loop {
|
|
||||||
let idx = local(cx, cy).unwrap();
|
|
||||||
let (px, py) = prev[idx];
|
|
||||||
if (px, py) == (sx, sy) || (px, py) == (-1, -1) {
|
|
||||||
let dx = (cx - sx) as f32;
|
|
||||||
let dy = (cy - sy) as f32;
|
|
||||||
let mag = (dx * dx + dy * dy).sqrt().max(1e-6);
|
|
||||||
return Some(((dx / mag, dy / mag), d));
|
|
||||||
}
|
|
||||||
cx = px; cy = py;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for &(dx, dy) in &[(1,0i32),(-1,0),(0,1),(0,-1),(1,1),(1,-1),(-1,1),(-1,-1)] {
|
|
||||||
let nx = x + dx; let ny = y + dy;
|
|
||||||
if !grid.is_ink(nx, ny) { continue; }
|
|
||||||
// Stay inside the radius budget.
|
|
||||||
let rdx = (nx - sx) as f32; let rdy = (ny - sy) as f32;
|
|
||||||
if rdx * rdx + rdy * rdy > r2 { continue; }
|
|
||||||
// Step cost: euclidean length × ridge-aversion factor. High
|
|
||||||
// SDF (ridge interior) → cheap. Low SDF (near edge) → expensive.
|
|
||||||
// The +0.5 keeps the factor finite at boundary pixels.
|
|
||||||
let step_len = if dx != 0 && dy != 0 { 1.41421356 } else { 1.0 };
|
|
||||||
let ridge = grid.sdf_at(nx, ny);
|
|
||||||
let factor = 1.0 + 1.5 / (ridge + 0.5);
|
|
||||||
let nd = d + step_len * factor;
|
|
||||||
let nidx = match local(nx, ny) { Some(i) => i, None => continue };
|
|
||||||
if nd < dist[nidx] {
|
|
||||||
dist[nidx] = nd;
|
|
||||||
prev[nidx] = (x, y);
|
|
||||||
heap.push((Reverse((nd * 1024.0) as u32), nx, ny));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Walk the brush in one direction from `start` until it dead-ends.
|
/// Walk the brush in one direction from `start` until it dead-ends.
|
||||||
/// `init_dir` seeds the momentum so the brush prefers a specific
|
/// `init_dir` seeds the momentum so the brush prefers a specific
|
||||||
/// direction at the first step (used for the "walk backwards" pass).
|
/// direction at the first step (used for the "walk backwards" pass).
|
||||||
@@ -1023,11 +920,7 @@ fn walk_brush(start: (f32, f32), init_dir: Option<(f32, f32)>,
|
|||||||
nc == 0 && grid.evaluate_disk(p, brush_radius).0 == 0
|
nc == 0 && grid.evaluate_disk(p, brush_radius).0 == 0
|
||||||
};
|
};
|
||||||
|
|
||||||
let chosen_dir = if score >= min_score && !would_be_stuck {
|
if score < min_score || would_be_stuck {
|
||||||
dir
|
|
||||||
} else {
|
|
||||||
// Repaint Dijkstra fallback (disabled when pen_lift_penalty=0).
|
|
||||||
if params.pen_lift_penalty <= 0.0 {
|
|
||||||
exit_reason = if score < min_score { "score_below_min".into() } else { "stuck".into() };
|
exit_reason = if score < min_score { "score_below_min".into() } else { "stuck".into() };
|
||||||
if let Some(t) = trace.as_deref_mut() {
|
if let Some(t) = trace.as_deref_mut() {
|
||||||
t.steps.push(WalkStep {
|
t.steps.push(WalkStep {
|
||||||
@@ -1038,22 +931,7 @@ fn walk_brush(start: (f32, f32), init_dir: Option<(f32, f32)>,
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let max_radius = params.pen_lift_reach * brush_radius;
|
let chosen_dir = dir;
|
||||||
match nearest_unpainted_through_ink(p, grid, max_radius) {
|
|
||||||
Some((rd, cost)) if cost <= params.pen_lift_penalty => rd,
|
|
||||||
_ => {
|
|
||||||
exit_reason = "repaint_search_failed".into();
|
|
||||||
if let Some(t) = trace.as_deref_mut() {
|
|
||||||
t.steps.push(WalkStep {
|
|
||||||
idx: step_idx, p, prev_dir,
|
|
||||||
candidates: recorded,
|
|
||||||
chosen: None, new_p: None,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let new_p = (p.0 + chosen_dir.0 * step_size, p.1 + chosen_dir.1 * step_size);
|
let new_p = (p.0 + chosen_dir.0 * step_size, p.1 + chosen_dir.1 * step_size);
|
||||||
|
|
||||||
@@ -2138,7 +2016,6 @@ mod tests {
|
|||||||
println!(" polish_iters = {}", best.params.polish_iters);
|
println!(" polish_iters = {}", best.params.polish_iters);
|
||||||
println!(" polish_search_factor = {:.2}", best.params.polish_search_factor);
|
println!(" polish_search_factor = {:.2}", best.params.polish_search_factor);
|
||||||
println!(" bg_penalty = {:.2}", best.params.bg_penalty);
|
println!(" bg_penalty = {:.2}", best.params.bg_penalty);
|
||||||
println!(" pen_lift_penalty = {:.1}", best.params.pen_lift_penalty);
|
|
||||||
println!(" min_component_factor = {:.2}", best.params.min_component_factor);
|
println!(" min_component_factor = {:.2}", best.params.min_component_factor);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2211,10 +2088,6 @@ mod tests {
|
|||||||
set: |p, v| p.min_score_factor = v, get: |p| p.min_score_factor },
|
set: |p, v| p.min_score_factor = v, get: |p| p.min_score_factor },
|
||||||
Axis { name: "min_component_factor", lo: 0.10, hi: 1.50, is_int: false,
|
Axis { name: "min_component_factor", lo: 0.10, hi: 1.50, is_int: false,
|
||||||
set: |p, v| p.min_component_factor = v, get: |p| p.min_component_factor },
|
set: |p, v| p.min_component_factor = v, get: |p| p.min_component_factor },
|
||||||
Axis { name: "pen_lift_penalty", lo: 0.0, hi: 200.0, is_int: false,
|
|
||||||
set: |p, v| p.pen_lift_penalty = v, get: |p| p.pen_lift_penalty },
|
|
||||||
Axis { name: "pen_lift_reach", lo: 0.5, hi: 16.0, is_int: false,
|
|
||||||
set: |p, v| p.pen_lift_reach = v, get: |p| p.pen_lift_reach },
|
|
||||||
Axis { name: "output_rdp_eps", lo: 0.0, hi: 2.0, is_int: false,
|
Axis { name: "output_rdp_eps", lo: 0.0, hi: 2.0, is_int: false,
|
||||||
set: |p, v| p.output_rdp_eps = v, get: |p| p.output_rdp_eps },
|
set: |p, v| p.output_rdp_eps = v, get: |p| p.output_rdp_eps },
|
||||||
];
|
];
|
||||||
@@ -2375,8 +2248,6 @@ mod tests {
|
|||||||
println!(" polish_search_factor = {:.2} (default {:.2})", current.polish_search_factor, base.polish_search_factor);
|
println!(" polish_search_factor = {:.2} (default {:.2})", current.polish_search_factor, base.polish_search_factor);
|
||||||
println!(" bg_penalty = {:.2} (default {:.2})", current.bg_penalty, base.bg_penalty);
|
println!(" bg_penalty = {:.2} (default {:.2})", current.bg_penalty, base.bg_penalty);
|
||||||
println!(" min_component_factor = {:.2} (default {:.2})", current.min_component_factor, base.min_component_factor);
|
println!(" min_component_factor = {:.2} (default {:.2})", current.min_component_factor, base.min_component_factor);
|
||||||
println!(" pen_lift_penalty = {:.1} (default {:.1})", current.pen_lift_penalty, base.pen_lift_penalty);
|
|
||||||
println!(" pen_lift_reach = {:.1} (default {:.1})", current.pen_lift_reach, base.pen_lift_reach);
|
|
||||||
println!(" output_rdp_eps = {:.2} (default {:.2})", current.output_rdp_eps, base.output_rdp_eps);
|
println!(" output_rdp_eps = {:.2} (default {:.2})", current.output_rdp_eps, base.output_rdp_eps);
|
||||||
|
|
||||||
// Per-letter breakdown at 5mm/425dpi for the constraint set.
|
// Per-letter breakdown at 5mm/425dpi for the constraint set.
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ pub fn default_axes() -> Vec<Axis> {
|
|||||||
set: |p, v| p.overpaint_penalty = v, get: |p| p.overpaint_penalty },
|
set: |p, v| p.overpaint_penalty = v, get: |p| p.overpaint_penalty },
|
||||||
Axis { name: "step_size_factor", lo: 0.20, hi: 0.90, is_int: false,
|
Axis { name: "step_size_factor", lo: 0.20, hi: 0.90, is_int: false,
|
||||||
set: |p, v| p.step_size_factor = v, get: |p| p.step_size_factor },
|
set: |p, v| p.step_size_factor = v, get: |p| p.step_size_factor },
|
||||||
Axis { name: "lookahead_steps", lo: 5.0, hi: 10.0, is_int: true,
|
Axis { name: "lookahead_steps", lo: 3.0, hi: 8.0, is_int: true,
|
||||||
set: |p, v| p.lookahead_steps = v as usize, get: |p| p.lookahead_steps as f32 },
|
set: |p, v| p.lookahead_steps = v as usize, get: |p| p.lookahead_steps as f32 },
|
||||||
Axis { name: "n_directions", lo: 8.0, hi: 64.0, is_int: true,
|
Axis { name: "n_directions", lo: 8.0, hi: 64.0, is_int: true,
|
||||||
set: |p, v| p.n_directions = v as usize, get: |p| p.n_directions as f32 },
|
set: |p, v| p.n_directions = v as usize, get: |p| p.n_directions as f32 },
|
||||||
@@ -71,10 +71,6 @@ pub fn default_axes() -> Vec<Axis> {
|
|||||||
set: |p, v| p.back_dir_cutoff = v, get: |p| p.back_dir_cutoff },
|
set: |p, v| p.back_dir_cutoff = v, get: |p| p.back_dir_cutoff },
|
||||||
Axis { name: "min_component_factor", lo: 0.10, hi: 1.50, is_int: false,
|
Axis { name: "min_component_factor", lo: 0.10, hi: 1.50, is_int: false,
|
||||||
set: |p, v| p.min_component_factor = v, get: |p| p.min_component_factor },
|
set: |p, v| p.min_component_factor = v, get: |p| p.min_component_factor },
|
||||||
Axis { name: "pen_lift_penalty", lo: 0.0, hi: 25.0, is_int: false,
|
|
||||||
set: |p, v| p.pen_lift_penalty = v, get: |p| p.pen_lift_penalty },
|
|
||||||
Axis { name: "pen_lift_reach", lo: 1.0, hi: 6.0, is_int: false,
|
|
||||||
set: |p, v| p.pen_lift_reach = v, get: |p| p.pen_lift_reach },
|
|
||||||
Axis { name: "output_rdp_eps", lo: 0.0, hi: 2.0, is_int: false,
|
Axis { name: "output_rdp_eps", lo: 0.0, hi: 2.0, is_int: false,
|
||||||
set: |p, v| p.output_rdp_eps = v, get: |p| p.output_rdp_eps },
|
set: |p, v| p.output_rdp_eps = v, get: |p| p.output_rdp_eps },
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1107,10 +1107,6 @@ fn optimize_paint_params(
|
|||||||
|p, v| p.min_score_factor = v),
|
|p, v| p.min_score_factor = v),
|
||||||
("min_component_factor", vec![0.2, 0.4, 0.6, 0.8, 1.2],
|
("min_component_factor", vec![0.2, 0.4, 0.6, 0.8, 1.2],
|
||||||
|p, v| p.min_component_factor = v),
|
|p, v| p.min_component_factor = v),
|
||||||
("pen_lift_penalty", vec![0.0, 10.0, 30.0, 60.0, 120.0],
|
|
||||||
|p, v| p.pen_lift_penalty = v),
|
|
||||||
("pen_lift_reach", vec![1.0, 3.0, 6.0, 10.0, 16.0],
|
|
||||||
|p, v| p.pen_lift_reach = v),
|
|
||||||
("output_rdp_eps", vec![0.0, 0.25, 0.5, 1.0],
|
("output_rdp_eps", vec![0.0, 0.25, 0.5, 1.0],
|
||||||
|p, v| p.output_rdp_eps = v),
|
|p, v| p.output_rdp_eps = v),
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user