diff --git a/src/brush_paint.rs b/src/brush_paint.rs index dfe05cb2..92fcfdfc 100644 --- a/src/brush_paint.rs +++ b/src/brush_paint.rs @@ -322,7 +322,7 @@ fn measure_sweep_full(strokes: &[Vec<(f32, f32)>], grid: &Grid) for (i, &c) in count.iter().enumerate() { if c == 0 { continue; } total += 1; - if !grid.was_ink[i] { bg += 1; } + if !grid.was_ink.get(i) { bg += 1; } else { repaint += c - 1; } } (bg, total, repaint) @@ -415,7 +415,7 @@ fn encode_coverage_b64(grid: &Grid) -> String { // // For the coverage view, paint UNPAINTED ink red (= missed). // Painted/background stays transparent. - if grid.unpainted[idx] { + if grid.unpainted.get(idx) { img.put_pixel(lx as u32, ly as u32, image::Rgba([244, 63, 94, 200])); } } @@ -427,17 +427,51 @@ fn encode_coverage_b64(grid: &Grid) -> String { format!("data:image/png;base64,{}", b64) } +// ── Bit-packed mask: 1 bit per pixel ──────────────────────────────────── + +/// Compact boolean mask backed by `Vec`. Used for `was_ink` and +/// `unpainted` so a 200×200 letter mask is ~5 KB instead of ~40 KB — +/// fits L1 nicely, and word-at-a-time popcount is available when +/// scanning whole grids. All ops are `#[inline]` since they're called +/// from the disk-iteration hot path. +struct BitMask { + bits: Vec, + len: usize, +} + +impl BitMask { + fn new(n_bits: usize) -> Self { + let words = (n_bits + 63) / 64; + Self { bits: vec![0u64; words], len: n_bits } + } + #[inline] fn len(&self) -> usize { self.len } + #[inline] fn get(&self, i: usize) -> bool { + // Safety: caller guarantees i < self.len; we still bounds-check + // via the indexed Vec access (Rust will panic on OOB anyway). + (self.bits[i >> 6] >> (i & 63)) & 1 == 1 + } + #[inline] fn set(&mut self, i: usize) { + self.bits[i >> 6] |= 1u64 << (i & 63); + } + #[inline] fn clear(&mut self, i: usize) { + self.bits[i >> 6] &= !(1u64 << (i & 63)); + } + fn count_ones(&self) -> u32 { + self.bits.iter().map(|w| w.count_ones()).sum() + } +} + // ── Coverage grid: bool per pixel, sized to the hull's bbox ───────────── struct Grid { bx: i32, by: i32, width: i32, height: i32, /// `true` = ink pixel that hasn't been painted yet. - unpainted: Vec, + unpainted: BitMask, /// `true` = pixel was ink in the original glyph (immutable; never /// changes after construction). Lets relaxation tell "ink" apart from /// "background" without conflating it with painted state. - was_ink: Vec, + was_ink: BitMask, /// Chamfer 3-4 distance / 3 (≈ Euclidean px from boundary). Used to /// snap raw start points up the gradient onto the medial-axis ridge, /// so strokes begin at stroke-centerline rather than polygon-edge. @@ -484,16 +518,16 @@ impl Grid { let width = (hull.bounds.x_max as i32 - hull.bounds.x_min as i32 + 1 + 2 * PAD).max(1); let height = (hull.bounds.y_max as i32 - hull.bounds.y_min as i32 + 1 + 2 * PAD).max(1); let cells = (width * height) as usize; - let mut unpainted = vec![false; cells]; - let mut was_ink = vec![false; cells]; + let mut unpainted = BitMask::new(cells); + let mut was_ink = BitMask::new(cells); let mut sdf = vec![0.0_f32; cells]; let mut count = 0; for &(x, y) in &hull.pixels { let lx = x as i32 - bx; let ly = y as i32 - by; if lx < 0 || ly < 0 || lx >= width || ly >= height { continue; } let idx = (ly * width + lx) as usize; - unpainted[idx] = true; - was_ink[idx] = true; + unpainted.set(idx); + was_ink.set(idx); count += 1; } // Chamfer distance (per-pixel, in approximate Euclidean units) @@ -603,21 +637,21 @@ impl Grid { for sy in 0..self.height { for sx in 0..self.width { let s_idx = (sy * self.width + sx) as usize; - if !self.unpainted[s_idx] || comp_id[s_idx] >= 0 { continue; } + if !self.unpainted.get(s_idx) || comp_id[s_idx] >= 0 { continue; } let id = sizes.len() as i32; let mut size = 0u32; let mut stack: Vec<(i32, i32)> = vec![(sx, sy)]; while let Some((cx, cy)) = stack.pop() { let cidx = (cy * self.width + cx) as usize; if comp_id[cidx] >= 0 { continue; } - if !self.unpainted[cidx] { continue; } + if !self.unpainted.get(cidx) { continue; } comp_id[cidx] = id; size += 1; for (dx, dy) in [(1, 0i32), (-1, 0), (0, 1), (0, -1)] { let nx = cx + dx; let ny = cy + dy; if nx < 0 || ny < 0 || nx >= self.width || ny >= self.height { continue; } let nidx = (ny * self.width + nx) as usize; - if self.unpainted[nidx] && comp_id[nidx] < 0 { + if self.unpainted.get(nidx) && comp_id[nidx] < 0 { stack.push((nx, ny)); } } @@ -631,7 +665,7 @@ impl Grid { fn is_ink(&self, x: i32, y: i32) -> bool { let lx = x - self.bx; let ly = y - self.by; if lx < 0 || ly < 0 || lx >= self.width || ly >= self.height { return false; } - self.was_ink[(ly * self.width + lx) as usize] + self.was_ink.get((ly * self.width + lx) as usize) } /// Returns (new_ink, repaint_ink, bg) — pixel counts under disk(p, r): @@ -659,9 +693,9 @@ impl Grid { let ly = cy_i + dy - self.by; if lx < 0 || ly < 0 || lx >= self.width || ly >= self.height { continue; } let idx = (ly * self.width + lx) as usize; - if self.unpainted[idx] { + if self.unpainted.get(idx) { new_ink += 1; - } else if self.was_ink[idx] { + } else if self.was_ink.get(idx) { repaint_ink += 1; } else { bg += 1; @@ -685,8 +719,8 @@ impl Grid { let ly = cy_i + dy - self.by; if lx < 0 || ly < 0 || lx >= self.width || ly >= self.height { continue; } let idx = (ly * self.width + lx) as usize; - if self.unpainted[idx] { - self.unpainted[idx] = false; + if self.unpainted.get(idx) { + self.unpainted.clear(idx); newly += 1; } } @@ -698,7 +732,7 @@ impl Grid { fn is_unpainted(&self, x: i32, y: i32) -> bool { let lx = x - self.bx; let ly = y - self.by; if lx < 0 || ly < 0 || lx >= self.width || ly >= self.height { return false; } - self.unpainted[(ly * self.width + lx) as usize] + self.unpainted.get((ly * self.width + lx) as usize) } /// Pick the next stroke's start by analysing the connected components @@ -724,7 +758,7 @@ impl Grid { for sy in 0..self.height { for sx in 0..self.width { let s_idx = (sy * self.width + sx) as usize; - if !self.unpainted[s_idx] || comp_id[s_idx] >= 0 { continue; } + if !self.unpainted.get(s_idx) || comp_id[s_idx] >= 0 { continue; } let id = components.len() as i32; let mut pixels: Vec = Vec::new(); let (mut top, mut left, mut bot, mut right) = (sy, sx, sy, sx); @@ -732,7 +766,7 @@ impl Grid { while let Some((cx, cy)) = stack.pop() { let cidx = (cy * self.width + cx) as usize; if comp_id[cidx] >= 0 { continue; } - if !self.unpainted[cidx] { continue; } + if !self.unpainted.get(cidx) { continue; } comp_id[cidx] = id; pixels.push(cidx); if cy < top { top = cy; } @@ -743,7 +777,7 @@ impl Grid { let nx = cx + dx; let ny = cy + dy; if nx < 0 || ny < 0 || nx >= self.width || ny >= self.height { continue; } let nidx = (ny * self.width + nx) as usize; - if self.unpainted[nidx] && comp_id[nidx] < 0 { + if self.unpainted.get(nidx) && comp_id[nidx] < 0 { stack.push((nx, ny)); } }