diff --git a/src-frontend/src/hooks/useTauri.js b/src-frontend/src/hooks/useTauri.js index 8a5c204f..a2ba6719 100644 --- a/src-frontend/src/hooks/useTauri.js +++ b/src-frontend/src/hooks/useTauri.js @@ -52,8 +52,6 @@ export const DEFAULT_PAINT_PARAMS = { 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, diff --git a/src/brush_paint.rs b/src/brush_paint.rs index 73e25fc9..2ce8a9bb 100644 --- a/src/brush_paint.rs +++ b/src/brush_paint.rs @@ -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![(-1i32, -1i32); cells]; - let local = |x: i32, y: i32| -> Option { - 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, 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. /// `init_dir` seeds the momentum so the brush prefers a specific /// direction at the first step (used for the "walk backwards" pass). @@ -1023,37 +920,18 @@ 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 { - 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 { - idx: step_idx, p, prev_dir, - candidates: recorded, - chosen: None, new_p: None, - }); - } - break; + 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 { + idx: step_idx, p, prev_dir, + candidates: recorded, + chosen: None, new_p: None, + }); } - 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; - } - } - }; + 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. diff --git a/src/brush_paint_opt.rs b/src/brush_paint_opt.rs index 3960ad2b..0ae99ad5 100644 --- a/src/brush_paint_opt.rs +++ b/src/brush_paint_opt.rs @@ -59,7 +59,7 @@ pub fn default_axes() -> Vec { 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, 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 }, 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 }, @@ -71,10 +71,6 @@ pub fn default_axes() -> Vec { 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, 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, set: |p, v| p.output_rdp_eps = v, get: |p| p.output_rdp_eps }, ] diff --git a/src/lib.rs b/src/lib.rs index 50c1f001..0635073e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1107,10 +1107,6 @@ fn optimize_paint_params( |p, v| p.min_score_factor = v), ("min_component_factor", vec![0.2, 0.4, 0.6, 0.8, 1.2], |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], |p, v| p.output_rdp_eps = v), ];