@@ -118,24 +118,10 @@ pub struct PaintParams {
/// the brush is wider than the stroke.
pub bg_penalty : f32 ,
/// Minimum unpainted-ink component size (as a multiplier of brush
/// area = π·r²) to start a new stroke. Components smaller than this
/// are leftovers from the previous stroke's brush sweep that the
/// relaxation didn't catch — we paint them with a single disk and
/// move on instead of attempting a doomed walk. 1.0 = "must be at
/// least one full brush-disc worth of unpainted ink."
/// area = π·r²) to be eligible as a stroke seed. Sub-threshold
/// components stay in the unpainted mask and count against
/// coverage but don't get a stroke of their own.
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.
pub max_steps_per_stroke : u32 ,
pub max_strokes : u32 ,
@@ -167,8 +153,6 @@ impl Default for PaintParams {
polish_search_factor : 0.5 ,
bg_penalty : 2.0 ,
min_component_factor : 1.49 ,
pen_lift_penalty : 0.0 ,
pen_lift_reach : 3.0 ,
max_steps_per_stroke : 4000 ,
max_strokes : 12 ,
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
/// pokes into bg on the outside of the bend. With walk_bg_penalty
/// 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! [ ( - 1 i32 , - 1 i32 ) ; 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 , 0 i32 ) , ( - 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.
/// `init_dir` seeds the momentum so the brush prefers a specific
/// 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
} ;
let chosen_dir = 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 {
if score < min_score | | would_be_stuck {
exit_reason = if score < min_score { " score_below_min " . into ( ) } else { " stuck " . into ( ) } ;
if let Some ( t ) = trace . as_deref_mut ( ) {
t . steps . push ( WalkStep {
@@ -1038,22 +931,7 @@ fn walk_brush(start: (f32, f32), init_dir: Option<(f32, f32)>,
}
break ;
}
let max_radius = params . pen_lift_reach * brush_radius ;
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 chosen_dir = dir ;
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_search_factor = {:.2} " , best . params . polish_search_factor ) ;
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 ) ;
}
@@ -2211,10 +2088,6 @@ mod tests {
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 ,
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 ,
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! ( " 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! ( " 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 ) ;
// Per-letter breakdown at 5mm/425dpi for the constraint set.