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 = [
|
||||
{ key: 'source', label: '0. Source pixels', on: true },
|
||||
{ key: 'sdf', label: '1. SDF heatmap', on: false },
|
||||
{ key: 'coverage', label: '2. Missed-pixel mask', on: false },
|
||||
{ key: 'starts', label: '3. Start points', on: true },
|
||||
{ key: 'brushSweep', label: '4. Brush sweep (radius)', on: false },
|
||||
{ key: 'trajectory', label: '5. Raw trajectories', on: true },
|
||||
{ key: 'strokes', label: '6. Smoothed strokes', on: true },
|
||||
{ key: 'skeleton', label: '2. Skeleton (deg-coded)', on: false },
|
||||
{ key: 'endpoints', label: '3. Endpoint init_dirs', on: false },
|
||||
{ key: 'components', label: '4. Pre-stroke components', on: false },
|
||||
{ key: 'preSnapshot',label: '5. Pre-walk unpainted', on: false },
|
||||
{ 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.
|
||||
@@ -25,7 +29,6 @@ const STEP_LAYERS = [
|
||||
{ key: 'brushHere', label: 'd. Brush footprint', on: true },
|
||||
{ key: 'momentum', label: 'e. Momentum arrow', 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().
|
||||
@@ -453,14 +456,10 @@ export default function PaintDebugView({ passIdx = 0 }) {
|
||||
<label key={l.key} className="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox"
|
||||
checked={!!stepEnabled[l.key]}
|
||||
onChange={() => toggleStepLayer(l.key)}
|
||||
disabled={l.key === 'skeleton'} />
|
||||
<span className={l.key === 'skeleton' ? 'text-neutral-600' : ''}>{l.label}</span>
|
||||
onChange={() => toggleStepLayer(l.key)} />
|
||||
<span>{l.label}</span>
|
||||
</label>
|
||||
))}
|
||||
{stepEnabled.skeleton && (
|
||||
<div className="text-[10px] text-neutral-500 pl-5">skeleton not exposed yet</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -646,6 +645,90 @@ export default function PaintDebugView({ passIdx = 0 }) {
|
||||
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 && (
|
||||
<image
|
||||
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
|
||||
/// candidate direction the walker considered.
|
||||
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.
|
||||
@@ -400,6 +449,73 @@ fn encode_sdf_b64(hull: &Hull) -> (String, f32) {
|
||||
(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 {
|
||||
let bw = grid.width.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
|
||||
/// is what the old hard-coded `(0, 1)` did).
|
||||
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,
|
||||
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_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;
|
||||
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 ─────
|
||||
@@ -859,8 +991,9 @@ impl Grid {
|
||||
/// Returns `None` once nothing remains worth painting, which lets
|
||||
/// `paint_fill` exit cleanly instead of burning through max_strokes
|
||||
/// on phantom 1-px gap attempts.
|
||||
fn pick_next_component(&mut self, min_component_pixels: u32)
|
||||
-> Option<((f32, f32), (f32, f32))>
|
||||
fn pick_next_component(&mut self, min_component_pixels: u32,
|
||||
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 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
|
||||
// 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))
|
||||
}
|
||||
};
|
||||
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 {
|
||||
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,
|
||||
};
|
||||
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 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 {
|
||||
if grid.ink_remaining <= 0 { break; }
|
||||
let (start, init_dir) = match grid.pick_next_component(min_component_pixels) {
|
||||
Some(s) => s, None => break,
|
||||
// Capture the unpainted mask BEFORE this stroke walks, so the
|
||||
// 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 path = trace_stroke(start, init_dir, &mut grid, params, brush_radius,
|
||||
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 skeleton_length = grid.skeleton_length;
|
||||
let unpainted_clusters = grid.unpainted_cluster_sizes();
|
||||
let (source_b64, sdf_b64, coverage_b64) = if render_pngs {
|
||||
(encode_hull_pixels_b64(hull), encode_sdf_b64(hull).0, encode_coverage_b64(&grid))
|
||||
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_skeleton_b64(&grid.hull))
|
||||
} 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 {
|
||||
bounds,
|
||||
source_b64,
|
||||
@@ -1646,6 +1840,11 @@ fn paint_fill_debug_inner(hull: &Hull, params: &PaintParams,
|
||||
strokes,
|
||||
start_points: starts,
|
||||
walks,
|
||||
skeleton_b64,
|
||||
endpoint_arrows,
|
||||
disk_offsets,
|
||||
stroke_seedings,
|
||||
unpainted_snapshots,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user