brush-paint: expose every algorithm-stage abstraction to the debug viewer
Rust additions to PaintDebug:
* skeleton_b64 — spur-pruned skeleton PNG, color-coded by
per-pixel degree (endpoint=red, junction=
green, path=grey).
* endpoint_arrows — Vec<(x, y, dx, dy)> for SVG init_dir
arrows at each skeleton endpoint.
* disk_offsets — raw Vec<(i32, i32)> for the brush's
precomputed pixel mask (frontend can
render an inset diagram).
* stroke_seedings — per-stroke list of all unpainted
components at that moment (bbox +
pixel_count + substantial flag + chosen
flag), plus the raw + post-snap start
positions and init_dir.
* unpainted_snapshots — pre-walk PNG of the unpainted mask
before each stroke begins.
HullData carries the bit-packed `skeleton` mask (alongside was_ink,
sdf, etc.). pick_next_component takes an optional Vec<SeedComponent>
out-param and returns the raw start in addition to snapped+init_dir.
Production path passes None — no overhead. metrics_for path
likewise (record_walks=false, render_pngs=false).
Frontend (PaintDebugView.jsx):
* Replaced the disabled 'skeleton (n/a)' step-layer toggle with
real layers in the main LAYERS list:
2. Skeleton (deg-coded)
3. Endpoint init_dirs
4. Pre-stroke components
5. Pre-walk unpainted (snapshot at stroke index)
6. Final missed-pixels (existing 'coverage', re-numbered)
* SVG renderers for the new vector overlays:
- endpoints as gold dots + tangent arrows scaled to ~1×brush
- components as bbox outlines (green=substantial,
grey=sub-threshold, yellow fill=chosen-by-this-stroke)
* Pre-walk snapshot picks the right PNG by the currently-selected
walk's stroke_idx, so the scrubber shows what the walker SAW
just before its current stroke began.
Bit-exact behavior preserved: alphabet report unchanged.
This commit is contained in:
@@ -10,11 +10,15 @@ const ZOOM_SENSITIVITY = IS_DARWIN ? 0.0015 : 0.015
|
|||||||
const LAYERS = [
|
const LAYERS = [
|
||||||
{ key: 'source', label: '0. Source pixels', on: true },
|
{ key: 'source', label: '0. Source pixels', on: true },
|
||||||
{ key: 'sdf', label: '1. SDF heatmap', on: false },
|
{ key: 'sdf', label: '1. SDF heatmap', on: false },
|
||||||
{ key: 'coverage', label: '2. Missed-pixel mask', on: false },
|
{ key: 'skeleton', label: '2. Skeleton (deg-coded)', on: false },
|
||||||
{ key: 'starts', label: '3. Start points', on: true },
|
{ key: 'endpoints', label: '3. Endpoint init_dirs', on: false },
|
||||||
{ key: 'brushSweep', label: '4. Brush sweep (radius)', on: false },
|
{ key: 'components', label: '4. Pre-stroke components', on: false },
|
||||||
{ key: 'trajectory', label: '5. Raw trajectories', on: true },
|
{ key: 'preSnapshot',label: '5. Pre-walk unpainted', on: false },
|
||||||
{ key: 'strokes', label: '6. Smoothed strokes', on: true },
|
{ key: 'coverage', label: '6. Final missed-pixels', on: false },
|
||||||
|
{ key: 'starts', label: '7. Start points', on: true },
|
||||||
|
{ key: 'brushSweep', label: '8. Brush sweep (radius)', on: false },
|
||||||
|
{ key: 'trajectory', label: '9. Raw trajectories', on: true },
|
||||||
|
{ key: 'strokes', label: '10. Smoothed strokes', on: true },
|
||||||
]
|
]
|
||||||
|
|
||||||
// Step-viz layers — toggled independently from the "final result" layers above.
|
// Step-viz layers — toggled independently from the "final result" layers above.
|
||||||
@@ -25,7 +29,6 @@ const STEP_LAYERS = [
|
|||||||
{ key: 'brushHere', label: 'd. Brush footprint', on: true },
|
{ key: 'brushHere', label: 'd. Brush footprint', on: true },
|
||||||
{ key: 'momentum', label: 'e. Momentum arrow', on: true },
|
{ key: 'momentum', label: 'e. Momentum arrow', on: true },
|
||||||
{ key: 'candidates', label: 'f. Candidates', on: true },
|
{ key: 'candidates', label: 'f. Candidates', on: true },
|
||||||
{ key: 'skeleton', label: 'g. Skeleton (n/a)', on: false },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
// Score weights — must match Rust's ScoreWeights::default().
|
// Score weights — must match Rust's ScoreWeights::default().
|
||||||
@@ -453,14 +456,10 @@ export default function PaintDebugView({ passIdx = 0 }) {
|
|||||||
<label key={l.key} className="flex items-center gap-2 cursor-pointer">
|
<label key={l.key} className="flex items-center gap-2 cursor-pointer">
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
checked={!!stepEnabled[l.key]}
|
checked={!!stepEnabled[l.key]}
|
||||||
onChange={() => toggleStepLayer(l.key)}
|
onChange={() => toggleStepLayer(l.key)} />
|
||||||
disabled={l.key === 'skeleton'} />
|
<span>{l.label}</span>
|
||||||
<span className={l.key === 'skeleton' ? 'text-neutral-600' : ''}>{l.label}</span>
|
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
{stepEnabled.skeleton && (
|
|
||||||
<div className="text-[10px] text-neutral-500 pl-5">skeleton not exposed yet</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -646,6 +645,90 @@ export default function PaintDebugView({ passIdx = 0 }) {
|
|||||||
preserveAspectRatio="none" />
|
preserveAspectRatio="none" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{enabled.skeleton && debug.skeleton_b64 && (
|
||||||
|
<image
|
||||||
|
href={debug.skeleton_b64} xlinkHref={debug.skeleton_b64}
|
||||||
|
x={debug.bounds[0]} y={debug.bounds[1]}
|
||||||
|
width={debug.bounds[2] - debug.bounds[0] + 1}
|
||||||
|
height={debug.bounds[3] - debug.bounds[1] + 1}
|
||||||
|
opacity={0.95}
|
||||||
|
style={{ imageRendering: 'pixelated' }}
|
||||||
|
preserveAspectRatio="none" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Per-endpoint init_dir arrows. Each arrow originates at the
|
||||||
|
endpoint and points along the skeleton tangent into the
|
||||||
|
letter — i.e., the direction the walker would head if it
|
||||||
|
seeded here. Arrow length scaled to ~1 brush radius. */}
|
||||||
|
{enabled.endpoints && (debug.endpoint_arrows ?? []).map((arr, i) => {
|
||||||
|
const [x, y, dx, dy] = arr
|
||||||
|
const L = Math.max(2, debug.brush_radius * 1.5)
|
||||||
|
const tx = x + dx * L
|
||||||
|
const ty = y + dy * L
|
||||||
|
return (
|
||||||
|
<g key={`ep${i}`}>
|
||||||
|
<line x1={x} y1={y} x2={tx} y2={ty}
|
||||||
|
stroke="#fbbf24" strokeWidth={0.6}
|
||||||
|
vectorEffect="non-scaling-stroke" />
|
||||||
|
<circle cx={x} cy={y} r={0.8}
|
||||||
|
fill="#fbbf24" stroke="#000" strokeWidth={0.3}
|
||||||
|
vectorEffect="non-scaling-stroke" />
|
||||||
|
<circle cx={tx} cy={ty} r={0.4}
|
||||||
|
fill="#fbbf24" stroke="none"
|
||||||
|
vectorEffect="non-scaling-stroke" />
|
||||||
|
</g>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* Pre-stroke component decomposition. For the currently-
|
||||||
|
selected walk's stroke, draws the bbox of every
|
||||||
|
connected unpainted component, color-coded:
|
||||||
|
green outline = substantial (got seeded or could)
|
||||||
|
grey outline = sub-threshold (sits in mask, no stroke)
|
||||||
|
yellow fill = the one this stroke chose. */}
|
||||||
|
{enabled.components && (() => {
|
||||||
|
const seedings = debug.stroke_seedings ?? []
|
||||||
|
// Find the seeding for the currently-selected walk's stroke_idx.
|
||||||
|
const sIdx = walks[walkIdx]?.stroke_idx ?? 0
|
||||||
|
const seeding = seedings.find(s => s.stroke_idx === sIdx) ?? seedings[0]
|
||||||
|
if (!seeding) return null
|
||||||
|
return (seeding.components ?? []).map((c, i) => {
|
||||||
|
const [xmin, ymin, xmax, ymax] = c.bbox
|
||||||
|
const fill = c.chosen ? '#facc1530' : 'none'
|
||||||
|
const stroke = c.substantial ? '#22c55e' : '#6b7280'
|
||||||
|
return (
|
||||||
|
<rect key={`cb${i}`}
|
||||||
|
x={xmin - 0.5} y={ymin - 0.5}
|
||||||
|
width={xmax - xmin + 1} height={ymax - ymin + 1}
|
||||||
|
fill={fill} stroke={stroke}
|
||||||
|
strokeWidth={0.4}
|
||||||
|
strokeOpacity={0.9}
|
||||||
|
vectorEffect="non-scaling-stroke" />
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})()}
|
||||||
|
|
||||||
|
{/* Pre-walk unpainted snapshot for the currently-selected
|
||||||
|
walk's stroke. Shows what the walker SAW just before it
|
||||||
|
started — distinct from the final coverage layer which
|
||||||
|
shows what's left after ALL strokes. */}
|
||||||
|
{enabled.preSnapshot && (() => {
|
||||||
|
const snaps = debug.unpainted_snapshots ?? []
|
||||||
|
const sIdx = walks[walkIdx]?.stroke_idx ?? 0
|
||||||
|
const png = snaps[sIdx]
|
||||||
|
if (!png) return null
|
||||||
|
return (
|
||||||
|
<image
|
||||||
|
href={png} xlinkHref={png}
|
||||||
|
x={debug.bounds[0]} y={debug.bounds[1]}
|
||||||
|
width={debug.bounds[2] - debug.bounds[0] + 1}
|
||||||
|
height={debug.bounds[3] - debug.bounds[1] + 1}
|
||||||
|
opacity={coverageOpacity}
|
||||||
|
style={{ imageRendering: 'pixelated' }}
|
||||||
|
preserveAspectRatio="none" />
|
||||||
|
)
|
||||||
|
})()}
|
||||||
|
|
||||||
{enabled.coverage && debug.coverage_b64 && (
|
{enabled.coverage && debug.coverage_b64 && (
|
||||||
<image
|
<image
|
||||||
href={debug.coverage_b64} xlinkHref={debug.coverage_b64}
|
href={debug.coverage_b64} xlinkHref={debug.coverage_b64}
|
||||||
|
|||||||
@@ -196,6 +196,55 @@ pub struct PaintDebug {
|
|||||||
/// scrub through the algorithm step by step and inspect every
|
/// scrub through the algorithm step by step and inspect every
|
||||||
/// candidate direction the walker considered.
|
/// candidate direction the walker considered.
|
||||||
pub walks: Vec<WalkTrace>,
|
pub walks: Vec<WalkTrace>,
|
||||||
|
/// Spur-pruned thinned skeleton, color-coded by per-pixel degree
|
||||||
|
/// (endpoint / junction / path). Same coord system as
|
||||||
|
/// `source_b64` / `sdf_b64` / `coverage_b64`.
|
||||||
|
pub skeleton_b64: String,
|
||||||
|
/// Skeleton endpoints as render-ready arrows: each tuple is
|
||||||
|
/// `(x, y, dx, dy)` where `(x, y)` is the endpoint position in
|
||||||
|
/// hull coords and `(dx, dy)` is the unit init_dir along the
|
||||||
|
/// skeleton (the direction the walker would head from this seed).
|
||||||
|
pub endpoint_arrows: Vec<(f32, f32, f32, f32)>,
|
||||||
|
/// Brush disk shape: integer (dx, dy) offsets that are inside the
|
||||||
|
/// brush mask at this radius. Lets the frontend render an inset
|
||||||
|
/// "this is the brush footprint" diagram.
|
||||||
|
pub disk_offsets: Vec<(i32, i32)>,
|
||||||
|
/// Per-stroke seeding info: one entry per `pick_next_component`
|
||||||
|
/// call that returned a seed. Each entry lists every connected
|
||||||
|
/// component in the unpainted mask at that moment, with bbox +
|
||||||
|
/// pixel count + flags for "would have been seeded" (substantial)
|
||||||
|
/// and "actually chosen as seed."
|
||||||
|
pub stroke_seedings: Vec<StrokeSeeding>,
|
||||||
|
/// Per-stroke pre-walk unpainted snapshot: the unpainted mask as
|
||||||
|
/// a PNG, captured just before each stroke's bidirectional walk
|
||||||
|
/// began. Same length as `trajectories`. Lets the scrubber show
|
||||||
|
/// "what the walker saw" at the start of stroke N.
|
||||||
|
pub unpainted_snapshots: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Per-stroke seeding diagnostics: one of these is recorded for each
|
||||||
|
/// `pick_next_component` call that produced a seed. Lists every
|
||||||
|
/// connected unpainted-ink component visible at that moment, with
|
||||||
|
/// flags for whether it was eligible (≥ min_component_pixels) and
|
||||||
|
/// whether the picker picked it.
|
||||||
|
#[derive(Debug, Clone, serde::Serialize)]
|
||||||
|
pub struct StrokeSeeding {
|
||||||
|
pub stroke_idx: u32,
|
||||||
|
pub min_component_pixels: u32,
|
||||||
|
pub raw_start: (f32, f32),
|
||||||
|
pub snapped_start: (f32, f32),
|
||||||
|
pub init_dir: (f32, f32),
|
||||||
|
pub components: Vec<SeedComponent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, serde::Serialize)]
|
||||||
|
pub struct SeedComponent {
|
||||||
|
/// Bbox in hull-local (relative to grid bx/by) coords:
|
||||||
|
/// `[x_min, y_min, x_max, y_max]`.
|
||||||
|
pub bbox: [i32; 4],
|
||||||
|
pub pixel_count: u32,
|
||||||
|
pub substantial: bool, // ≥ min_component_pixels — eligible to seed
|
||||||
|
pub chosen: bool, // picked by this pick_next_component call
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One full walk_brush invocation, recorded for stepping/visualization.
|
/// One full walk_brush invocation, recorded for stepping/visualization.
|
||||||
@@ -400,6 +449,73 @@ fn encode_sdf_b64(hull: &Hull) -> (String, f32) {
|
|||||||
(format!("data:image/png;base64,{}", b64), max_d)
|
(format!("data:image/png;base64,{}", b64), max_d)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Encode the spur-pruned skeleton as a per-pixel PNG, color-coded by
|
||||||
|
/// degree (count of in-skel 8-neighbors):
|
||||||
|
/// degree 1 → endpoint (red)
|
||||||
|
/// degree 2 → path (mid grey)
|
||||||
|
/// degree ≥ 3 → junction (green)
|
||||||
|
/// Empty pixels stay transparent. Sized to the hull's grid bbox so it
|
||||||
|
/// overlays directly on the source/sdf/coverage layers.
|
||||||
|
fn encode_skeleton_b64(hull_data: &HullData) -> String {
|
||||||
|
let bw = hull_data.width.max(1) as u32;
|
||||||
|
let bh = hull_data.height.max(1) as u32;
|
||||||
|
let mut img: image::RgbaImage = image::ImageBuffer::new(bw, bh);
|
||||||
|
for ly in 0..hull_data.height {
|
||||||
|
for lx in 0..hull_data.width {
|
||||||
|
let idx = (ly * hull_data.width + lx) as usize;
|
||||||
|
if !hull_data.skeleton.get(idx) { continue; }
|
||||||
|
// Count 8-connected in-skel neighbors.
|
||||||
|
let abs_x = (lx + hull_data.bx) as u32;
|
||||||
|
let abs_y = (ly + hull_data.by) as u32;
|
||||||
|
let nbrs = zs_neighbors(abs_x, abs_y);
|
||||||
|
let mut deg = 0;
|
||||||
|
for (nx, ny) in nbrs {
|
||||||
|
let nlx = nx as i32 - hull_data.bx;
|
||||||
|
let nly = ny as i32 - hull_data.by;
|
||||||
|
if nlx < 0 || nly < 0 || nlx >= hull_data.width || nly >= hull_data.height { continue; }
|
||||||
|
if hull_data.skeleton.get((nly * hull_data.width + nlx) as usize) {
|
||||||
|
deg += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let rgba = match deg {
|
||||||
|
0 | 1 => [244, 63, 94, 230], // endpoint — red
|
||||||
|
2 => [120, 120, 120, 200], // path — grey
|
||||||
|
_ => [ 34, 197, 94, 230], // junction — green
|
||||||
|
};
|
||||||
|
img.put_pixel(lx as u32, ly as u32, image::Rgba(rgba));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut buf = std::io::Cursor::new(Vec::new());
|
||||||
|
if img.write_to(&mut buf, image::ImageFormat::Png).is_err() { return String::new(); }
|
||||||
|
use base64::Engine as _;
|
||||||
|
let b64 = base64::engine::general_purpose::STANDARD.encode(buf.get_ref());
|
||||||
|
format!("data:image/png;base64,{}", b64)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Snapshot the current `unpainted` BitMask as a transparent PNG —
|
||||||
|
/// red pixels where ink is not yet painted, transparent elsewhere.
|
||||||
|
/// Same coord system as the other layers. Used by the per-stroke
|
||||||
|
/// scrubber so the viewer can see what was unpainted just before
|
||||||
|
/// stroke N began.
|
||||||
|
fn encode_grid_unpainted_b64(grid: &Grid) -> String {
|
||||||
|
let bw = grid.width.max(1) as u32;
|
||||||
|
let bh = grid.height.max(1) as u32;
|
||||||
|
let mut img: image::RgbaImage = image::ImageBuffer::new(bw, bh);
|
||||||
|
for ly in 0..grid.height {
|
||||||
|
for lx in 0..grid.width {
|
||||||
|
let idx = (ly * grid.width + lx) as usize;
|
||||||
|
if grid.unpainted.get(idx) {
|
||||||
|
img.put_pixel(lx as u32, ly as u32, image::Rgba([244, 63, 94, 200]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut buf = std::io::Cursor::new(Vec::new());
|
||||||
|
if img.write_to(&mut buf, image::ImageFormat::Png).is_err() { return String::new(); }
|
||||||
|
use base64::Engine as _;
|
||||||
|
let b64 = base64::engine::general_purpose::STANDARD.encode(buf.get_ref());
|
||||||
|
format!("data:image/png;base64,{}", b64)
|
||||||
|
}
|
||||||
|
|
||||||
fn encode_coverage_b64(grid: &Grid) -> String {
|
fn encode_coverage_b64(grid: &Grid) -> String {
|
||||||
let bw = grid.width.max(1) as u32;
|
let bw = grid.width.max(1) as u32;
|
||||||
let bh = grid.height.max(1) as u32;
|
let bh = grid.height.max(1) as u32;
|
||||||
@@ -494,6 +610,13 @@ struct HullData {
|
|||||||
/// (instead of trying to go down off the end of the foot, which
|
/// (instead of trying to go down off the end of the foot, which
|
||||||
/// is what the old hard-coded `(0, 1)` did).
|
/// is what the old hard-coded `(0, 1)` did).
|
||||||
skel_endpoints_init_dir: Vec<(f32, f32)>,
|
skel_endpoints_init_dir: Vec<(f32, f32)>,
|
||||||
|
/// Spur-pruned thinned skeleton, bit-packed in the same coord
|
||||||
|
/// system as `was_ink`. Kept around (small memory cost — ~1 bit
|
||||||
|
/// per ink pixel) so the debug viewer can render it overlaid on
|
||||||
|
/// the source. Per-pixel skeleton-degree (endpoint vs junction
|
||||||
|
/// vs path) is derived on demand by scanning 8-connected
|
||||||
|
/// neighbors of each skeleton pixel.
|
||||||
|
skeleton: BitMask,
|
||||||
skeleton_length: u32,
|
skeleton_length: u32,
|
||||||
ink_total: i32,
|
ink_total: i32,
|
||||||
}
|
}
|
||||||
@@ -601,9 +724,18 @@ fn compute_hull_data(hull: &Hull) -> HullData {
|
|||||||
skel_endpoints.push((x as i32, y as i32));
|
skel_endpoints.push((x as i32, y as i32));
|
||||||
skel_endpoints_init_dir.push((dx / mag, dy / mag));
|
skel_endpoints_init_dir.push((dx / mag, dy / mag));
|
||||||
}
|
}
|
||||||
|
// Bit-pack the skeleton in the same coord system as was_ink so
|
||||||
|
// the debug renderer can paint it as a per-pixel overlay.
|
||||||
|
let mut skeleton = BitMask::new(cells);
|
||||||
|
for &(x, y) in &skel {
|
||||||
|
let lx = x as i32 - bx; let ly = y as i32 - by;
|
||||||
|
if lx < 0 || ly < 0 || lx >= width || ly >= height { continue; }
|
||||||
|
skeleton.set((ly * width + lx) as usize);
|
||||||
|
}
|
||||||
let skeleton_length = skel.len() as u32;
|
let skeleton_length = skel.len() as u32;
|
||||||
HullData { bx, by, width, height, was_ink, sdf, sdf_values_sorted,
|
HullData { bx, by, width, height, was_ink, sdf, sdf_values_sorted,
|
||||||
skel_endpoints, skel_endpoints_init_dir, skeleton_length, ink_total: count }
|
skel_endpoints, skel_endpoints_init_dir, skeleton,
|
||||||
|
skeleton_length, ink_total: count }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Coverage grid: per-call mutable state, sized to the hull's bbox ─────
|
// ── Coverage grid: per-call mutable state, sized to the hull's bbox ─────
|
||||||
@@ -859,8 +991,9 @@ impl Grid {
|
|||||||
/// Returns `None` once nothing remains worth painting, which lets
|
/// Returns `None` once nothing remains worth painting, which lets
|
||||||
/// `paint_fill` exit cleanly instead of burning through max_strokes
|
/// `paint_fill` exit cleanly instead of burning through max_strokes
|
||||||
/// on phantom 1-px gap attempts.
|
/// on phantom 1-px gap attempts.
|
||||||
fn pick_next_component(&mut self, min_component_pixels: u32)
|
fn pick_next_component(&mut self, min_component_pixels: u32,
|
||||||
-> Option<((f32, f32), (f32, f32))>
|
debug_components: Option<&mut Vec<SeedComponent>>)
|
||||||
|
-> Option<((f32, f32), (f32, f32), (f32, f32))> // (snapped, init_dir, raw)
|
||||||
{
|
{
|
||||||
let mut comp_id = vec![-1i32; self.unpainted.len()];
|
let mut comp_id = vec![-1i32; self.unpainted.len()];
|
||||||
let mut components: Vec<(Vec<usize>, (i32, i32, i32, i32))> = Vec::new();
|
let mut components: Vec<(Vec<usize>, (i32, i32, i32, i32))> = Vec::new();
|
||||||
@@ -915,7 +1048,34 @@ impl Grid {
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let chosen = match best { Some((i, _)) => i, None => return None };
|
let chosen = match best { Some((i, _)) => i, None => {
|
||||||
|
// Even on None-return, fill the debug if requested so the
|
||||||
|
// viewer can see why nothing was seeded.
|
||||||
|
if let Some(out) = debug_components {
|
||||||
|
for (pixels, (top, left, bot, right)) in components.iter() {
|
||||||
|
out.push(SeedComponent {
|
||||||
|
bbox: [*left + self.bx, *top + self.by,
|
||||||
|
*right + self.bx, *bot + self.by],
|
||||||
|
pixel_count: pixels.len() as u32,
|
||||||
|
substantial: (pixels.len() as u32) >= min_component_pixels,
|
||||||
|
chosen: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return None;
|
||||||
|
} };
|
||||||
|
|
||||||
|
if let Some(out) = debug_components {
|
||||||
|
for (i, (pixels, (top, left, bot, right))) in components.iter().enumerate() {
|
||||||
|
out.push(SeedComponent {
|
||||||
|
bbox: [*left + self.bx, *top + self.by,
|
||||||
|
*right + self.bx, *bot + self.by],
|
||||||
|
pixel_count: pixels.len() as u32,
|
||||||
|
substantial: (pixels.len() as u32) >= min_component_pixels,
|
||||||
|
chosen: i == chosen,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Writing-order start: prefer a skeleton endpoint ("leg") that
|
// Writing-order start: prefer a skeleton endpoint ("leg") that
|
||||||
// falls inside the chosen component's still-unpainted ink. These
|
// falls inside the chosen component's still-unpainted ink. These
|
||||||
@@ -966,7 +1126,7 @@ impl Grid {
|
|||||||
((best_pixel.0 as f32, best_pixel.1 as f32), (0.0, 1.0))
|
((best_pixel.0 as f32, best_pixel.1 as f32), (0.0, 1.0))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
Some((self.snap_to_ridge(raw, 16), init_dir))
|
Some((self.snap_to_ridge(raw, 16), init_dir, raw))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1235,7 +1395,7 @@ pub fn paint_fill_with(hull: &Hull, params: &PaintParams) -> FillResult {
|
|||||||
|
|
||||||
for stroke_idx in 0..params.max_strokes {
|
for stroke_idx in 0..params.max_strokes {
|
||||||
if grid.ink_remaining <= 0 { break; }
|
if grid.ink_remaining <= 0 { break; }
|
||||||
let (start, init_dir) = match grid.pick_next_component(min_component_pixels) {
|
let (start, init_dir, _raw) = match grid.pick_next_component(min_component_pixels, None) {
|
||||||
Some(s) => s, None => break,
|
Some(s) => s, None => break,
|
||||||
};
|
};
|
||||||
let path = trace_stroke(start, init_dir, &mut grid, params, brush_radius, None, stroke_idx);
|
let path = trace_stroke(start, init_dir, &mut grid, params, brush_radius, None, stroke_idx);
|
||||||
@@ -1594,11 +1754,37 @@ fn paint_fill_debug_inner(hull: &Hull, params: &PaintParams,
|
|||||||
let min_component_pixels = (params.min_component_factor * brush_area).max(1.0) as u32;
|
let min_component_pixels = (params.min_component_factor * brush_area).max(1.0) as u32;
|
||||||
|
|
||||||
let mut walks: Vec<WalkTrace> = Vec::new();
|
let mut walks: Vec<WalkTrace> = Vec::new();
|
||||||
|
let mut stroke_seedings: Vec<StrokeSeeding> = Vec::new();
|
||||||
|
let mut unpainted_snapshots: Vec<String> = Vec::new();
|
||||||
for stroke_idx in 0..params.max_strokes {
|
for stroke_idx in 0..params.max_strokes {
|
||||||
if grid.ink_remaining <= 0 { break; }
|
if grid.ink_remaining <= 0 { break; }
|
||||||
let (start, init_dir) = match grid.pick_next_component(min_component_pixels) {
|
// Capture the unpainted mask BEFORE this stroke walks, so the
|
||||||
Some(s) => s, None => break,
|
// viewer can scrub through "what the walker saw at each step".
|
||||||
};
|
if render_pngs {
|
||||||
|
unpainted_snapshots.push(encode_grid_unpainted_b64(&grid));
|
||||||
|
}
|
||||||
|
let mut comps_dbg: Vec<SeedComponent> = Vec::new();
|
||||||
|
let comps_out: Option<&mut Vec<SeedComponent>> = if record_walks {
|
||||||
|
Some(&mut comps_dbg)
|
||||||
|
} else { None };
|
||||||
|
let pnc = grid.pick_next_component(min_component_pixels, comps_out);
|
||||||
|
if record_walks {
|
||||||
|
// Record the seeding decision (even if it returned None —
|
||||||
|
// tells the viewer "no substantial component left").
|
||||||
|
let (snapped, init_dir, raw) = match pnc {
|
||||||
|
Some(s) => s,
|
||||||
|
None => ((0.0, 0.0), (0.0, 0.0), (0.0, 0.0)),
|
||||||
|
};
|
||||||
|
stroke_seedings.push(StrokeSeeding {
|
||||||
|
stroke_idx,
|
||||||
|
min_component_pixels,
|
||||||
|
raw_start: raw,
|
||||||
|
snapped_start: snapped,
|
||||||
|
init_dir,
|
||||||
|
components: std::mem::take(&mut comps_dbg),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let (start, init_dir, _raw) = match pnc { Some(s) => s, None => break };
|
||||||
let walk_log = if record_walks { Some(&mut walks) } else { None };
|
let walk_log = if record_walks { Some(&mut walks) } else { None };
|
||||||
let path = trace_stroke(start, init_dir, &mut grid, params, brush_radius,
|
let path = trace_stroke(start, init_dir, &mut grid, params, brush_radius,
|
||||||
walk_log, stroke_idx);
|
walk_log, stroke_idx);
|
||||||
@@ -1623,11 +1809,19 @@ fn paint_fill_debug_inner(hull: &Hull, params: &PaintParams,
|
|||||||
let (bg_painted, total_swept, repaint) = measure_sweep_full(&strokes, &grid);
|
let (bg_painted, total_swept, repaint) = measure_sweep_full(&strokes, &grid);
|
||||||
let skeleton_length = grid.skeleton_length;
|
let skeleton_length = grid.skeleton_length;
|
||||||
let unpainted_clusters = grid.unpainted_cluster_sizes();
|
let unpainted_clusters = grid.unpainted_cluster_sizes();
|
||||||
let (source_b64, sdf_b64, coverage_b64) = if render_pngs {
|
let (source_b64, sdf_b64, coverage_b64, skeleton_b64) = if render_pngs {
|
||||||
(encode_hull_pixels_b64(hull), encode_sdf_b64(hull).0, encode_coverage_b64(&grid))
|
(encode_hull_pixels_b64(hull),
|
||||||
|
encode_sdf_b64(hull).0,
|
||||||
|
encode_coverage_b64(&grid),
|
||||||
|
encode_skeleton_b64(&grid.hull))
|
||||||
} else {
|
} else {
|
||||||
(String::new(), String::new(), String::new())
|
(String::new(), String::new(), String::new(), String::new())
|
||||||
};
|
};
|
||||||
|
let endpoint_arrows: Vec<(f32, f32, f32, f32)> = grid.hull.skel_endpoints.iter()
|
||||||
|
.zip(grid.hull.skel_endpoints_init_dir.iter())
|
||||||
|
.map(|(&(ex, ey), &(dx, dy))| (ex as f32, ey as f32, dx, dy))
|
||||||
|
.collect();
|
||||||
|
let disk_offsets = grid.disk_offsets.clone();
|
||||||
PaintDebug {
|
PaintDebug {
|
||||||
bounds,
|
bounds,
|
||||||
source_b64,
|
source_b64,
|
||||||
@@ -1646,6 +1840,11 @@ fn paint_fill_debug_inner(hull: &Hull, params: &PaintParams,
|
|||||||
strokes,
|
strokes,
|
||||||
start_points: starts,
|
start_points: starts,
|
||||||
walks,
|
walks,
|
||||||
|
skeleton_b64,
|
||||||
|
endpoint_arrows,
|
||||||
|
disk_offsets,
|
||||||
|
stroke_seedings,
|
||||||
|
unpainted_snapshots,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user