brush-paint: add wall_follow_weight — right-hand wall-following bonus
Per-candidate-direction bonus to the walker's score, computed via a single perpendicular-CW raycast on the was_ink BitMask. Bonus peaks at brush_area when the wall-distance equals brush_radius (pen riding the wall at its natural offset), falls off linearly to 0 at distance 0 or 2×brush_radius. The hypothesis: at M/W/A apexes the lookahead-coverage score collapses because the V's interior is past the lookahead horizon. Wall-following gives the walker an alternative signal that "this direction follows a stroke corridor" even when greedy lookahead sees mostly painted ink + bg. Default 0.0 → no behavior change → alphabet report bit-exact. The optimizer's 14th axis now (range 0.0–2.0, same as momentum_weight). If the optimizer dials it nonzero and metrics improve, the term is doing useful work; if it lands at 0, harmless.
This commit is contained in:
@@ -44,6 +44,7 @@ export const DEFAULT_PAINT_PARAMS = {
|
||||
n_directions: 48,
|
||||
lookahead_steps: 3,
|
||||
momentum_weight: 0.20,
|
||||
wall_follow_weight: 0.0,
|
||||
overpaint_penalty: 0.10,
|
||||
walk_bg_penalty: 0.69,
|
||||
min_score_factor: 0.20,
|
||||
|
||||
@@ -81,6 +81,15 @@ pub struct PaintParams {
|
||||
/// Bonus weight on direction alignment with current velocity.
|
||||
/// 0 = no momentum, 1 = momentum equally weighted with new coverage.
|
||||
pub momentum_weight: f32,
|
||||
/// Bonus weight for "right-hand on the wall" wall-following. For each
|
||||
/// candidate direction the walker raycasts perpendicular CW; if the
|
||||
/// resulting wall distance is near `brush_radius` (= the pen would be
|
||||
/// riding the wall at its natural offset), the candidate's score gets
|
||||
/// a `wall_follow_weight × brush_area` bonus that decays linearly to
|
||||
/// 0 as the wall distance moves away from `brush_radius`. Helps the
|
||||
/// walker take the down-into-V step at M/W/A apexes where greedy
|
||||
/// lookahead-coverage alone collapses. 0 = off, no behavior change.
|
||||
pub wall_follow_weight: f32,
|
||||
/// Per-overpainted-pixel penalty in scoring (relative to new coverage
|
||||
/// which is +1 per pixel). Applied to ink we already painted (mild —
|
||||
/// just discourages backtracking).
|
||||
@@ -131,6 +140,7 @@ impl Default for PaintParams {
|
||||
n_directions: 48,
|
||||
lookahead_steps: 3,
|
||||
momentum_weight: 0.20,
|
||||
wall_follow_weight: 0.0,
|
||||
overpaint_penalty: 0.10,
|
||||
walk_bg_penalty: 0.69,
|
||||
min_score_factor: 0.20,
|
||||
@@ -263,7 +273,11 @@ pub struct WalkCandidate {
|
||||
/// `momentum_weight × max(0, dot(dir, prev_dir)) × brush_area` (0 if
|
||||
/// no prev_dir).
|
||||
pub momentum_bonus: f32,
|
||||
/// Final score = new_ink − overpaint·repaint − walk_bg·bg + momentum_bonus.
|
||||
/// `wall_follow_weight × wall_proximity × brush_area`. Peaks when
|
||||
/// the right-perpendicular raycast hits a wall at distance ≈
|
||||
/// brush_radius (pen riding the wall at natural offset).
|
||||
pub wall_bonus: f32,
|
||||
/// Final score = new_ink − overpaint·repaint − walk_bg·bg + momentum_bonus + wall_bonus.
|
||||
pub score: f32,
|
||||
}
|
||||
|
||||
@@ -946,6 +960,43 @@ fn vec_unit(v: (f32, f32)) -> (f32, f32) {
|
||||
}
|
||||
fn vec_dot(a: (f32, f32), b: (f32, f32)) -> f32 { a.0 * b.0 + a.1 * b.1 }
|
||||
|
||||
/// Cast a ray from `p` in direction `dir` (unit vector) over the grid's
|
||||
/// `was_ink` mask. Returns the distance, in pixels, to the first non-ink
|
||||
/// pixel — capped at `max_steps`. The ray walks in 1-px increments; for
|
||||
/// our use case (max_steps ≈ 2× brush_radius ≈ 5–10 px), that's a
|
||||
/// handful of `BitMask::get` lookups per ray.
|
||||
fn raycast_to_wall(grid: &Grid, p: (f32, f32), dir: (f32, f32), max_steps: u32) -> f32 {
|
||||
for k in 1..=max_steps {
|
||||
let kf = k as f32;
|
||||
let probe_x = (p.0 + dir.0 * kf).round() as i32;
|
||||
let probe_y = (p.1 + dir.1 * kf).round() as i32;
|
||||
if !grid.is_ink(probe_x, probe_y) {
|
||||
return kf - 1.0;
|
||||
}
|
||||
}
|
||||
max_steps as f32
|
||||
}
|
||||
|
||||
/// Right-hand wall-following bonus for one candidate direction.
|
||||
/// Computes the perpendicular CW raycast distance and rewards positions
|
||||
/// where it sits near `brush_radius` (the pen's natural offset for
|
||||
/// riding the wall). Falls off linearly to 0 as the distance drifts
|
||||
/// away from `brush_radius` by ±brush_radius. Returns a value in
|
||||
/// [0, brush_area] so it scales the same as `momentum_bonus`.
|
||||
fn wall_follow_bonus(grid: &Grid, p: (f32, f32), dir: (f32, f32),
|
||||
brush_radius: f32, brush_area: f32) -> f32 {
|
||||
if brush_radius <= 0.0 { return 0.0; }
|
||||
// Perpendicular CW: rotate (dx, dy) by -90° → (dy, -dx).
|
||||
let perp_cw = (dir.1, -dir.0);
|
||||
// Cap the raycast at 2× brush_radius — anything farther means the
|
||||
// pen isn't on a wall at all, so wall-bonus is 0 anyway.
|
||||
let max_steps = (2.0 * brush_radius).ceil().max(1.0) as u32;
|
||||
let wall_dist = raycast_to_wall(grid, p, perp_cw, max_steps);
|
||||
let offset = (wall_dist - brush_radius).abs();
|
||||
let proximity = (1.0 - offset / brush_radius).max(0.0);
|
||||
proximity * brush_area
|
||||
}
|
||||
|
||||
// ── Trace a single stroke ───────────────────────────────────────────────
|
||||
|
||||
/// Score one candidate direction by simulating `lookahead_steps` walks
|
||||
@@ -1017,16 +1068,20 @@ fn walk_brush(start: (f32, f32), init_dir: Option<(f32, f32)>,
|
||||
let momentum_bonus = if has_momentum {
|
||||
params.momentum_weight * vec_dot(dir, prev_dir_unit).max(0.0) * brush_area
|
||||
} else { 0.0 };
|
||||
let wall_bonus = if params.wall_follow_weight > 0.0 {
|
||||
params.wall_follow_weight * wall_follow_bonus(grid, p, dir, brush_radius, brush_area)
|
||||
} else { 0.0 };
|
||||
let score = new_ink
|
||||
- params.overpaint_penalty * repaint
|
||||
- params.walk_bg_penalty * bg
|
||||
+ momentum_bonus;
|
||||
+ momentum_bonus
|
||||
+ wall_bonus;
|
||||
|
||||
if recording {
|
||||
recorded.push(WalkCandidate {
|
||||
theta, dir, probe,
|
||||
rejected_back, rejected_off_ink,
|
||||
new_ink, repaint, bg, momentum_bonus, score,
|
||||
new_ink, repaint, bg, momentum_bonus, wall_bonus, score,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -59,6 +59,8 @@ pub fn default_axes() -> Vec<Axis> {
|
||||
set: |p, v| p.n_directions = v as usize, get: |p| p.n_directions as f32 },
|
||||
Axis { name: "momentum_weight", lo: 0.0, hi: 2.0, is_int: false,
|
||||
set: |p, v| p.momentum_weight = v, get: |p| p.momentum_weight },
|
||||
Axis { name: "wall_follow_weight", lo: 0.0, hi: 2.0, is_int: false,
|
||||
set: |p, v| p.wall_follow_weight = v, get: |p| p.wall_follow_weight },
|
||||
Axis { name: "min_score_factor", lo: 0.05, hi: 0.30, is_int: false,
|
||||
set: |p, v| p.min_score_factor = v, get: |p| p.min_score_factor },
|
||||
Axis { name: "back_dir_cutoff", lo: -0.95, hi: -0.3, is_int: false,
|
||||
|
||||
Reference in New Issue
Block a user