brush-paint: prefer bottommost-leftmost endpoint as stroke seed
Swapped the endpoint-priority comparison in pick_next_component from topmost-leftmost to bottommost-leftmost. For most Latin letters the natural pen-down anchor sits at the bottom (M/W/V/U feet, A's foot, vertical-stem letters' base). Top-of-glyph endpoints are still candidates — they just lose to a bottom one when both exist in the same component. For M at 5mm/425 specifically, the first stroke now starts at the bottom-left foot (108, 270) and walks up with init_dir=(0, -1), where it previously started at the top-left peak (108, 146) and walked down. Stroke count dropped 4 → 3 at that scale. Corpus metrics are roughly flat (±0.5% coverage per scale, ±1-2 total strokes per scale). The change is principled — pen-down anchors at the bottom for most letters — without regressing overall coverage. Adds a paint_diagnose_endpoints test (env-driven, #[ignore]) for inspecting endpoint positions + init_dirs + first-stroke start for any letter+scale.
This commit is contained in:
@@ -939,7 +939,13 @@ impl Grid {
|
|||||||
.get(i).copied().unwrap_or((0.0, 1.0));
|
.get(i).copied().unwrap_or((0.0, 1.0));
|
||||||
match best_endpoint {
|
match best_endpoint {
|
||||||
None => best_endpoint = Some(((ex, ey), init_dir)),
|
None => best_endpoint = Some(((ex, ey), init_dir)),
|
||||||
Some(((bx_e, by_e), _)) if ey < by_e || (ey == by_e && ex < bx_e) => {
|
// Bottommost first, leftmost tiebreaker. For most Latin
|
||||||
|
// letters the natural pen-down anchor is at the bottom
|
||||||
|
// (M/W/V/U feet, A's foot, vertical-stem letters' base).
|
||||||
|
// Top-of-glyph endpoints are still candidates — they
|
||||||
|
// just lose to a bottom one when both exist in the
|
||||||
|
// same component.
|
||||||
|
Some(((bx_e, by_e), _)) if ey > by_e || (ey == by_e && ex < bx_e) => {
|
||||||
best_endpoint = Some(((ex, ey), init_dir));
|
best_endpoint = Some(((ex, ey), init_dir));
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -1690,6 +1696,44 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Print the skeleton endpoints + first-stroke start for one letter.
|
||||||
|
/// Run as: cargo test --release --lib paint_diagnose_endpoints -- --ignored --nocapture
|
||||||
|
/// Pick char + scale via env: PD_CHAR=M PD_MM=8 PD_DPI=425 PD_THICK=9
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn paint_diagnose_endpoints() {
|
||||||
|
let ch: char = std::env::var("PD_CHAR").ok().and_then(|s| s.chars().next()).unwrap_or('M');
|
||||||
|
let font_mm: f32 = std::env::var("PD_MM").ok().and_then(|s| s.parse().ok()).unwrap_or(8.0);
|
||||||
|
let dpi: u32 = std::env::var("PD_DPI").ok().and_then(|s| s.parse().ok()).unwrap_or(425);
|
||||||
|
let thick: u32 = std::env::var("PD_THICK").ok().and_then(|s| s.parse().ok()).unwrap_or(9);
|
||||||
|
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 => { println!("no hull"); return; }
|
||||||
|
};
|
||||||
|
let h = get_or_compute_hull_data(main);
|
||||||
|
println!("\n=== '{}' @ {}mm/{}dpi/{}px ===", ch, font_mm, dpi, thick);
|
||||||
|
println!("bbox: x [{}, {}], y [{}, {}] w={} h={}",
|
||||||
|
main.bounds.x_min, main.bounds.x_max,
|
||||||
|
main.bounds.y_min, main.bounds.y_max,
|
||||||
|
h.width, h.height);
|
||||||
|
println!("skeleton endpoints ({}):", h.skel_endpoints.len());
|
||||||
|
for (i, &(ex, ey)) in h.skel_endpoints.iter().enumerate() {
|
||||||
|
let d = h.skel_endpoints_init_dir.get(i).copied().unwrap_or((0.0, 0.0));
|
||||||
|
println!(" #{} pos=({}, {}) init_dir=({:+.2}, {:+.2})", i, ex, ey, d.0, d.1);
|
||||||
|
}
|
||||||
|
// Run paint_fill_debug and report the first stroke's start.
|
||||||
|
let dbg = paint_fill_debug(main, &PaintParams::default());
|
||||||
|
println!("brush_radius = {:.2} px", dbg.brush_radius);
|
||||||
|
println!("first stroke starts: {:?}", dbg.start_points.first());
|
||||||
|
println!("first walk init_dir: {:?}", dbg.walks.first().map(|w| w.init_dir));
|
||||||
|
println!("strokes: {}", dbg.strokes.len());
|
||||||
|
for (i, s) in dbg.strokes.iter().enumerate().take(6) {
|
||||||
|
if s.is_empty() { continue; }
|
||||||
|
println!(" stroke #{}: {} pts, start ({:.1}, {:.1}), end ({:.1}, {:.1})",
|
||||||
|
i, s.len(), s[0].0, s[0].1, s.last().unwrap().0, s.last().unwrap().1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn paint_letter_I_is_one_stroke() {
|
fn paint_letter_I_is_one_stroke() {
|
||||||
let hulls = rasterize_letter_at('I', 8.0, 200, 4);
|
let hulls = rasterize_letter_at('I', 8.0, 200, 4);
|
||||||
|
|||||||
Reference in New Issue
Block a user