use crate::coord::WindowRect;
use crate::tile::{AlphaF32, AlphaF32Data, AlphaMask, Tile, TILE_SIZE};
impl AlphaMask {
pub fn boolean_add(&mut self, other: &AlphaMask) {
for ((tx, ty), other_tile) in other.iter() {
let tile = self.get_or_create(tx, ty);
let dst = tile.write();
let src = other_tile.data();
for i in 0..dst.0.len() {
dst.0[i] = (dst.0[i] + src.0[i]).min(1.0);
}
}
}
pub fn boolean_subtract(&mut self, other: &AlphaMask) {
for ((tx, ty), other_tile) in other.iter() {
if let Some(tile) = self.get_mut(tx, ty) {
let dst = tile.write();
let src = other_tile.data();
for i in 0..dst.0.len() {
dst.0[i] = (dst.0[i] - src.0[i]).max(0.0);
}
}
}
}
pub fn boolean_intersect(&mut self, other: &AlphaMask) {
let self_keys: Vec<(i32, i32)> = self.iter().map(|(k, _)| k).collect();
for (tx, ty) in self_keys {
match other.get(tx, ty) {
Some(other_tile) => {
let tile = self.get_or_create(tx, ty);
let dst = tile.write();
let src = other_tile.data();
for i in 0..dst.0.len() {
dst.0[i] = dst.0[i].min(src.0[i]);
}
}
None => {
let tile = self.get_or_create(tx, ty);
let dst = tile.write();
dst.0.fill(0.0);
}
}
}
}
fn get_mut(&mut self, tx: i32, ty: i32) -> Option<&mut Tile<AlphaF32>> {
if self.get(tx, ty).is_some() {
Some(self.get_or_create(tx, ty))
} else {
None
}
}
pub fn clear(&mut self) {
*self = AlphaMask::new();
}
pub fn invert(&mut self, canvas_w: u32, canvas_h: u32) {
let ts = TILE_SIZE as i32;
let tx_max = ((canvas_w as i32) - 1).div_euclid(ts);
let ty_max = ((canvas_h as i32) - 1).div_euclid(ts);
for ty in 0..=ty_max {
for tx in 0..=tx_max {
self.get_or_create(tx, ty);
}
}
let keys: Vec<(i32, i32)> = self.iter().map(|(k, _)| k).collect();
for (tx, ty) in keys {
let tile = self.get_or_create(tx, ty);
let data = tile.write();
for v in data.0.iter_mut() {
*v = 1.0 - *v;
}
}
}
pub fn bounding_rect(&self) -> Option<(i32, i32, i32, i32)> {
let mut min_x = i32::MAX;
let mut min_y = i32::MAX;
let mut max_x = i32::MIN;
let mut max_y = i32::MIN;
let mut any = false;
for ((tx, ty), _) in self.iter() {
min_x = min_x.min(tx);
min_y = min_y.min(ty);
max_x = max_x.max(tx);
max_y = max_y.max(ty);
any = true;
}
if any {
Some((min_x, min_y, max_x, max_y))
} else {
None
}
}
pub fn pixel_bounding_rect(&self) -> Option<[u32; 4]> {
let ts = TILE_SIZE as i32;
let mut px_min_x = i32::MAX;
let mut px_min_y = i32::MAX;
let mut px_max_x = i32::MIN;
let mut px_max_y = i32::MIN;
for ((tx, ty), tile) in self.iter() {
let data = tile.data();
let origin_x = tx * ts;
let origin_y = ty * ts;
for ly in 0..TILE_SIZE {
for lx in 0..TILE_SIZE {
if data.0[ly * TILE_SIZE + lx] > 0.0 {
let px = origin_x + lx as i32;
let py = origin_y + ly as i32;
px_min_x = px_min_x.min(px);
px_min_y = px_min_y.min(py);
px_max_x = px_max_x.max(px);
px_max_y = px_max_y.max(py);
}
}
}
}
if px_min_x <= px_max_x {
let x = px_min_x.max(0) as u32;
let y = px_min_y.max(0) as u32;
let w = (px_max_x - px_min_x + 1) as u32;
let h = (px_max_y - px_min_y + 1) as u32;
Some([x, y, w, h])
} else {
None
}
}
pub fn sample(&self, px: i32, py: i32) -> f32 {
let tile_size = TILE_SIZE as i32;
let (tx, ty) = Self::tile_coords_for_pixel(px, py);
match self.get(tx, ty) {
Some(tile) => {
let lx = (px - tx * tile_size) as usize;
let ly = (py - ty * tile_size) as usize;
tile.data().get(lx, ly)
}
None => 0.0,
}
}
}
impl AlphaMask {
pub fn rasterize(
&mut self,
bounds: (i32, i32, i32, i32),
sdf_fn: impl Fn(f32, f32) -> f32,
antialias: bool,
feather: f32,
) {
let (bx, by, bw, bh) = bounds;
let margin = if feather > 0.0 {
feather.ceil() as i32
} else if antialias {
1
} else {
0
};
let x0 = bx - margin;
let y0 = by - margin;
let x1 = bx + bw + margin;
let y1 = by + bh + margin;
let ts = TILE_SIZE as i32;
let (tx0, ty0) = Self::tile_coords_for_pixel(x0, y0);
let (tx1, ty1) = Self::tile_coords_for_pixel(x1 - 1, y1 - 1);
let edge_band = if feather > 0.0 {
feather * 0.5
} else if antialias {
0.5
} else {
0.0
};
for tty in ty0..=ty1 {
for ttx in tx0..=tx1 {
let base_px = ttx * ts;
let base_py = tty * ts;
let corners = [
sdf_fn(base_px as f32 + 0.5, base_py as f32 + 0.5),
sdf_fn((base_px + ts - 1) as f32 + 0.5, base_py as f32 + 0.5),
sdf_fn(base_px as f32 + 0.5, (base_py + ts - 1) as f32 + 0.5),
sdf_fn(
(base_px + ts - 1) as f32 + 0.5,
(base_py + ts - 1) as f32 + 0.5,
),
];
let max_corner = corners.iter().copied().fold(f32::NEG_INFINITY, f32::max);
let min_corner = corners.iter().copied().fold(f32::INFINITY, f32::min);
if max_corner < -edge_band {
let tile = self.get_or_create(ttx, tty);
tile.write().0.fill(1.0);
continue;
}
let half_diag = (ts as f32) * std::f32::consts::FRAC_1_SQRT_2;
if min_corner > edge_band + half_diag {
continue;
}
let tile = self.get_or_create(ttx, tty);
let data = tile.write();
for ly in 0..TILE_SIZE {
let py = base_py + ly as i32;
if py < y0 || py >= y1 {
continue;
}
for lx in 0..TILE_SIZE {
let px = base_px + lx as i32;
if px < x0 || px >= x1 {
continue;
}
let sdf = sdf_fn(px as f32 + 0.5, py as f32 + 0.5);
let coverage = crate::sdf::sdf_coverage(sdf, antialias, feather);
if coverage > 0.0 {
data.set(lx, ly, coverage);
}
}
}
}
}
}
}
pub struct RasterizedMask {
pub data: Vec<u8>,
pub x: u32,
pub y: u32,
pub width: u32,
pub height: u32,
}
pub fn rasterize_sdf_r8(
canvas_width: u32,
canvas_height: u32,
bounds: (i32, i32, i32, i32),
sdf_fn: impl Fn(f32, f32) -> f32,
antialias: bool,
feather: f32,
) -> RasterizedMask {
let (bx, by, bw, bh) = bounds;
let margin = if feather > 0.0 {
feather.ceil() as i32
} else if antialias {
1
} else {
0
};
let window = WindowRect::from_xywh(0, 0, canvas_width, canvas_height);
let region = match WindowRect::from_corners(
bx - margin,
by - margin,
bx + bw + margin,
by + bh + margin,
)
.intersect(window)
{
Some(r) => r,
None => {
return RasterizedMask {
data: Vec::new(),
x: 0,
y: 0,
width: 0,
height: 0,
}
}
};
let (x0, y0) = (region.x0() as u32, region.y0() as u32);
let (rw, rh) = (region.width, region.height);
let (x1, y1) = (x0 + rw, y0 + rh);
let mut pixels = vec![0u8; (rw * rh) as usize];
for py in y0..y1 {
for px in x0..x1 {
let sdf = sdf_fn(px as f32 + 0.5, py as f32 + 0.5);
let coverage = crate::sdf::sdf_coverage(sdf, antialias, feather);
if coverage > 0.0 {
pixels[((py - y0) * rw + (px - x0)) as usize] = (coverage * 255.0) as u8;
}
}
}
RasterizedMask {
data: pixels,
x: x0,
y: y0,
width: rw,
height: rh,
}
}
pub fn rasterize_polygon_r8(
canvas_width: u32,
canvas_height: u32,
vertices: &[[f32; 2]],
antialias: bool,
) -> RasterizedMask {
if vertices.len() < 3 {
return RasterizedMask {
data: Vec::new(),
x: 0,
y: 0,
width: 0,
height: 0,
};
}
let mut min_x = f32::INFINITY;
let mut min_y = f32::INFINITY;
let mut max_x = f32::NEG_INFINITY;
let mut max_y = f32::NEG_INFINITY;
for v in vertices {
min_x = min_x.min(v[0]);
min_y = min_y.min(v[1]);
max_x = max_x.max(v[0]);
max_y = max_y.max(v[1]);
}
let margin = if antialias { 1.0 } else { 0.0 };
let window = WindowRect::from_xywh(0, 0, canvas_width, canvas_height);
let region = match window.clamp_f32(
min_x - margin,
min_y - margin,
max_x + margin + 1.0,
max_y + margin + 1.0,
) {
Some(r) => r,
None => {
return RasterizedMask {
data: Vec::new(),
x: 0,
y: 0,
width: 0,
height: 0,
}
}
};
let (x0, y0) = (region.x0() as u32, region.y0() as u32);
let (rw, rh) = (region.width, region.height);
let (x1, y1) = (x0 + rw, y0 + rh);
let n = vertices.len();
let sub_samples: &[f32] = if antialias {
&[0.125, 0.375, 0.625, 0.875]
} else {
&[0.5]
};
let scale = if antialias {
255.0 / sub_samples.len() as f32
} else {
255.0
};
let mut accum = vec![0u16; (rw * rh) as usize];
let mut intersections = Vec::with_capacity(n / 2 + 4);
for py in y0..y1 {
let local_y = (py - y0) as usize;
for &sub_offset in sub_samples {
let scan_y = py as f32 + sub_offset;
intersections.clear();
let mut j = n - 1;
for i in 0..n {
let yi = vertices[i][1];
let yj = vertices[j][1];
if (yi <= scan_y && yj > scan_y) || (yj <= scan_y && yi > scan_y) {
let t = (scan_y - yi) / (yj - yi);
let x = vertices[i][0] + t * (vertices[j][0] - vertices[i][0]);
intersections.push(x);
}
j = i;
}
intersections.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap());
for pair in intersections.chunks_exact(2) {
let xl = pair[0];
let xr = pair[1];
let px_start = (xl.ceil() as i32).clamp(x0 as i32, x1 as i32) as u32;
let px_end = (xr.floor() as i32 + 1).clamp(x0 as i32, x1 as i32) as u32;
for px in px_start..px_end {
accum[local_y * rw as usize + (px - x0) as usize] += 1;
}
if antialias {
let left_px = (xl.floor() as i32).max(x0 as i32) as u32;
if left_px < px_start && left_px >= x0 && left_px < x1 {
let coverage = (left_px as f32 + 1.0 - xl).clamp(0.0, 1.0);
accum[local_y * rw as usize + (left_px - x0) as usize] +=
(coverage * 1.0) as u16; }
let right_px = (xr.floor() as i32).max(x0 as i32) as u32;
if right_px >= px_end && right_px >= x0 && right_px < x1 {
let coverage = (xr - right_px as f32).clamp(0.0, 1.0);
accum[local_y * rw as usize + (right_px - x0) as usize] +=
(coverage * 1.0) as u16;
}
}
}
}
}
let data: Vec<u8> = accum
.iter()
.map(|&v| (v as f32 * scale).round().min(255.0) as u8)
.collect();
RasterizedMask {
data,
x: x0,
y: y0,
width: rw,
height: rh,
}
}
pub fn contour_segments_r8(
pixels: &[u8],
width: u32,
height: u32,
threshold: u8,
) -> Vec<([f32; 2], [f32; 2])> {
let segments = contour_raw_segments_r8(pixels, width, height, threshold);
simplify_segments(merge_collinear(segments))
}
pub fn contour_polylines_r8(
pixels: &[u8],
width: u32,
height: u32,
threshold: u8,
) -> Vec<Vec<[f32; 2]>> {
let segments = contour_raw_segments_r8(pixels, width, height, threshold);
build_polylines(merge_collinear(segments))
}
fn contour_raw_segments_r8(
pixels: &[u8],
width: u32,
height: u32,
threshold: u8,
) -> Vec<([f32; 2], [f32; 2])> {
let [bx, by, bw, bh] = match pixel_bounds_r8(pixels, width, height) {
Some(b) => b,
None => return Vec::new(),
};
let px_min = bx as i32 - 1;
let py_min = by as i32 - 1;
let px_max = (bx + bw) as i32;
let py_max = (by + bh) as i32;
let sample = |x: i32, y: i32| -> f32 {
if x < 0 || y < 0 || x >= width as i32 || y >= height as i32 {
return 0.0;
}
pixels[(y as u32 * width + x as u32) as usize] as f32 / 255.0
};
let threshold_f = threshold as f32 / 255.0;
let mut segments = Vec::new();
for py in py_min..py_max {
for px in px_min..px_max {
let tl = sample(px, py) > threshold_f;
let tr = sample(px + 1, py) > threshold_f;
let bl = sample(px, py + 1) > threshold_f;
let br = sample(px + 1, py + 1) > threshold_f;
let index = (tl as u8) | ((tr as u8) << 1) | ((bl as u8) << 2) | ((br as u8) << 3);
if index == 0 || index == 15 {
continue;
}
let x = px as f32;
let y = py as f32;
let top = lerp_edge(sample(px, py), sample(px + 1, py), threshold_f);
let bottom = lerp_edge(sample(px, py + 1), sample(px + 1, py + 1), threshold_f);
let left = lerp_edge(sample(px, py), sample(px, py + 1), threshold_f);
let right = lerp_edge(sample(px + 1, py), sample(px + 1, py + 1), threshold_f);
let t = [x + top, y];
let b = [x + bottom, y + 1.0];
let l = [x, y + left];
let r = [x + 1.0, y + right];
match index {
1 => segments.push((l, t)),
2 => segments.push((t, r)),
3 => segments.push((l, r)),
4 => segments.push((b, l)),
5 => segments.push((b, t)),
6 => {
segments.push((t, r));
segments.push((b, l));
}
7 => segments.push((b, r)),
8 => segments.push((r, b)),
9 => {
segments.push((l, t));
segments.push((r, b));
}
10 => segments.push((t, b)),
11 => segments.push((l, b)),
12 => segments.push((r, l)),
13 => segments.push((r, t)),
14 => segments.push((t, l)),
_ => unreachable!(),
}
}
}
segments
}
pub fn pixel_bounds_r8(pixels: &[u8], width: u32, height: u32) -> Option<[u32; 4]> {
let mut min_x = u32::MAX;
let mut min_y = u32::MAX;
let mut max_x = 0u32;
let mut max_y = 0u32;
let mut found = false;
for y in 0..height {
for x in 0..width {
if pixels[(y * width + x) as usize] > 0 {
found = true;
min_x = min_x.min(x);
min_y = min_y.min(y);
max_x = max_x.max(x);
max_y = max_y.max(y);
}
}
}
if found {
Some([min_x, min_y, max_x - min_x + 1, max_y - min_y + 1])
} else {
None
}
}
fn gaussian_kernel(radius: f32) -> Vec<f32> {
let sigma = radius * 0.5;
let half = radius.ceil() as usize;
let size = 2 * half + 1;
let mut kernel = Vec::with_capacity(size);
let two_sigma_sq = 2.0 * sigma * sigma;
let mut sum = 0.0;
for i in 0..size {
let x = i as f32 - half as f32;
let val = (-x * x / two_sigma_sq).exp();
kernel.push(val);
sum += val;
}
for v in &mut kernel {
*v /= sum;
}
kernel
}
impl AlphaMask {
pub fn feather(&mut self, radius: f32) {
if radius < 0.5 {
return;
}
let kernel = gaussian_kernel(radius);
let half = (kernel.len() / 2) as i32;
let Some((tx_min, ty_min, tx_max, ty_max)) = self.bounding_rect() else {
return;
};
let ts = TILE_SIZE as i32;
let tile_expand = (half as usize).div_ceil(TILE_SIZE);
let te = tile_expand as i32;
let mut intermediate = AlphaMask::new();
for tty in ty_min..=ty_max {
for ttx in (tx_min - te)..=(tx_max + te) {
let base_px = ttx * ts;
let base_py = tty * ts;
let mut tile_data = AlphaF32Data::default();
let mut any = false;
for ly in 0..TILE_SIZE {
let py = base_py + ly as i32;
for lx in 0..TILE_SIZE {
let px = base_px + lx as i32;
let mut sum = 0.0;
for (ki, &weight) in kernel.iter().enumerate() {
let sx = px + ki as i32 - half;
sum += self.sample(sx, py) * weight;
}
if sum > 1e-6 {
tile_data.set(lx, ly, sum);
any = true;
}
}
}
if any {
let tile = intermediate.get_or_create(ttx, tty);
*tile.write() = tile_data;
}
}
}
let Some((ix_min, iy_min, ix_max, iy_max)) = intermediate.bounding_rect() else {
self.clear();
return;
};
let mut result = AlphaMask::new();
for tty in (iy_min - te)..=(iy_max + te) {
for ttx in ix_min..=ix_max {
let base_px = ttx * ts;
let base_py = tty * ts;
let mut tile_data = AlphaF32Data::default();
let mut any = false;
for ly in 0..TILE_SIZE {
let py = base_py + ly as i32;
for lx in 0..TILE_SIZE {
let px = base_px + lx as i32;
let mut sum = 0.0;
for (ki, &weight) in kernel.iter().enumerate() {
let sy = py + ki as i32 - half;
sum += intermediate.sample(px, sy) * weight;
}
if sum > 1e-6 {
tile_data.set(lx, ly, sum.min(1.0));
any = true;
}
}
}
if any {
let tile = result.get_or_create(ttx, tty);
*tile.write() = tile_data;
}
}
}
*self = result;
}
}
impl AlphaMask {
pub fn contour_segments(&self, threshold: f32) -> Vec<([f32; 2], [f32; 2])> {
let Some((tx_min, ty_min, tx_max, ty_max)) = self.bounding_rect() else {
return Vec::new();
};
let ts = TILE_SIZE as i32;
let px_min = tx_min * ts - 1;
let py_min = ty_min * ts - 1;
let px_max = (tx_max + 1) * ts;
let py_max = (ty_max + 1) * ts;
let mut segments = Vec::new();
for py in py_min..py_max {
for px in px_min..px_max {
let tl = self.sample(px, py) > threshold;
let tr = self.sample(px + 1, py) > threshold;
let bl = self.sample(px, py + 1) > threshold;
let br = self.sample(px + 1, py + 1) > threshold;
let index = (tl as u8) | ((tr as u8) << 1) | ((bl as u8) << 2) | ((br as u8) << 3);
if index == 0 || index == 15 {
continue;
}
let x = px as f32;
let y = py as f32;
let top = lerp_edge(self.sample(px, py), self.sample(px + 1, py), threshold);
let bottom = lerp_edge(
self.sample(px, py + 1),
self.sample(px + 1, py + 1),
threshold,
);
let left = lerp_edge(self.sample(px, py), self.sample(px, py + 1), threshold);
let right = lerp_edge(
self.sample(px + 1, py),
self.sample(px + 1, py + 1),
threshold,
);
let t = [x + top, y]; let b = [x + bottom, y + 1.0]; let l = [x, y + left]; let r = [x + 1.0, y + right];
match index {
1 => segments.push((l, t)), 2 => segments.push((t, r)), 3 => segments.push((l, r)), 4 => segments.push((b, l)), 5 => segments.push((b, t)), 6 => {
segments.push((t, r));
segments.push((b, l));
} 7 => segments.push((b, r)), 8 => segments.push((r, b)), 9 => {
segments.push((l, t));
segments.push((r, b));
} 10 => segments.push((t, b)), 11 => segments.push((l, b)), 12 => segments.push((r, l)), 13 => segments.push((r, t)), 14 => segments.push((t, l)), _ => unreachable!(),
}
}
}
simplify_segments(merge_collinear(segments))
}
}
fn merge_collinear(segments: Vec<([f32; 2], [f32; 2])>) -> Vec<([f32; 2], [f32; 2])> {
use std::collections::BTreeMap;
fn key(v: f32) -> i32 {
v.to_bits() as i32
}
let mut horiz: BTreeMap<(i32, bool), Vec<(f32, f32)>> = BTreeMap::new();
let mut vert: BTreeMap<(i32, bool), Vec<(f32, f32)>> = BTreeMap::new();
let mut other: Vec<([f32; 2], [f32; 2])> = Vec::new();
for (a, b) in segments {
if a[1] == b[1] {
let reversed = a[0] > b[0];
let (lo, hi) = if reversed { (b[0], a[0]) } else { (a[0], b[0]) };
horiz
.entry((key(a[1]), reversed))
.or_default()
.push((lo, hi));
} else if a[0] == b[0] {
let reversed = a[1] > b[1];
let (lo, hi) = if reversed { (b[1], a[1]) } else { (a[1], b[1]) };
vert.entry((key(a[0]), reversed))
.or_default()
.push((lo, hi));
} else {
other.push((a, b));
}
}
let mut result = Vec::new();
for ((y_bits, reversed), mut spans) in horiz {
let y = f32::from_bits(y_bits as u32);
spans.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
let (mut lo, mut hi) = spans[0];
for &(s_lo, s_hi) in &spans[1..] {
if s_lo == hi {
hi = s_hi;
} else {
if reversed {
result.push(([hi, y], [lo, y]));
} else {
result.push(([lo, y], [hi, y]));
}
lo = s_lo;
hi = s_hi;
}
}
if reversed {
result.push(([hi, y], [lo, y]));
} else {
result.push(([lo, y], [hi, y]));
}
}
for ((x_bits, reversed), mut spans) in vert {
let x = f32::from_bits(x_bits as u32);
spans.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
let (mut lo, mut hi) = spans[0];
for &(s_lo, s_hi) in &spans[1..] {
if s_lo == hi {
hi = s_hi;
} else {
if reversed {
result.push(([x, hi], [x, lo]));
} else {
result.push(([x, lo], [x, hi]));
}
lo = s_lo;
hi = s_hi;
}
}
if reversed {
result.push(([x, hi], [x, lo]));
} else {
result.push(([x, lo], [x, hi]));
}
}
result.extend(other);
result
}
fn build_polylines(segments: Vec<([f32; 2], [f32; 2])>) -> Vec<Vec<[f32; 2]>> {
if segments.is_empty() {
return Vec::new();
}
use std::collections::HashMap;
fn qkey(p: [f32; 2]) -> (i64, i64) {
((p[0] * 1024.0) as i64, (p[1] * 1024.0) as i64)
}
let mut adj: HashMap<(i64, i64), Vec<(usize, bool)>> = HashMap::new();
for (i, (a, b)) in segments.iter().enumerate() {
adj.entry(qkey(*a)).or_default().push((i, false)); adj.entry(qkey(*b)).or_default().push((i, true)); }
let mut used = vec![false; segments.len()];
let mut chains: Vec<Vec<[f32; 2]>> = Vec::new();
for start_idx in 0..segments.len() {
if used[start_idx] {
continue;
}
used[start_idx] = true;
let (a, b) = segments[start_idx];
let mut chain = vec![a, b];
loop {
let tail = *chain.last().unwrap();
let key = qkey(tail);
let next = adj
.get(&key)
.and_then(|neighbors| neighbors.iter().find(|&&(idx, _)| !used[idx]));
match next {
Some(&(idx, is_end)) => {
used[idx] = true;
let (sa, sb) = segments[idx];
if is_end {
chain.push(sa);
} else {
chain.push(sb);
}
}
None => break,
}
}
loop {
let head = chain[0];
let key = qkey(head);
let next = adj
.get(&key)
.and_then(|neighbors| neighbors.iter().find(|&&(idx, _)| !used[idx]));
match next {
Some(&(idx, is_end)) => {
used[idx] = true;
let (sa, sb) = segments[idx];
if is_end {
chain.insert(0, sa);
} else {
chain.insert(0, sb);
}
}
None => break,
}
}
chains.push(chain);
}
if segments.len() <= 32 {
return chains;
}
chains.into_iter().map(|c| rdp_simplify(&c, 1.0)).collect()
}
fn simplify_segments(segments: Vec<([f32; 2], [f32; 2])>) -> Vec<([f32; 2], [f32; 2])> {
let polylines = build_polylines(segments);
let mut result = Vec::new();
for poly in &polylines {
for w in poly.windows(2) {
result.push((w[0], w[1]));
}
}
result
}
fn rdp_simplify(points: &[[f32; 2]], epsilon: f32) -> Vec<[f32; 2]> {
if points.len() <= 2 {
return points.to_vec();
}
let first = points[0];
let last = points[points.len() - 1];
let mut max_dist = 0.0f32;
let mut max_idx = 0;
for (i, p) in points.iter().enumerate().skip(1).take(points.len() - 2) {
let d = point_to_line_dist(*p, first, last);
if d > max_dist {
max_dist = d;
max_idx = i;
}
}
if max_dist > epsilon {
let mut left = rdp_simplify(&points[..=max_idx], epsilon);
let right = rdp_simplify(&points[max_idx..], epsilon);
left.pop(); left.extend(right);
left
} else {
vec![first, last]
}
}
fn point_to_line_dist(p: [f32; 2], a: [f32; 2], b: [f32; 2]) -> f32 {
let dx = b[0] - a[0];
let dy = b[1] - a[1];
let len_sq = dx * dx + dy * dy;
if len_sq < 1e-12 {
let ex = p[0] - a[0];
let ey = p[1] - a[1];
return (ex * ex + ey * ey).sqrt();
}
((p[0] - a[0]) * dy - (p[1] - a[1]) * dx).abs() / len_sq.sqrt()
}
fn lerp_edge(v0: f32, v1: f32, threshold: f32) -> f32 {
let dv = v1 - v0;
if dv.abs() < 1e-6 {
0.5
} else {
((threshold - v0) / dv).clamp(0.0, 1.0)
}
}
impl AlphaMask {
pub fn rasterize_r8(
&self,
origin: (i32, i32),
width: u32,
height: u32,
default_value: u8,
) -> Vec<u8> {
let mut pixels = vec![default_value; (width * height) as usize];
let (ox, oy) = origin;
let ts = TILE_SIZE;
for ((tx, ty), tile) in self.iter() {
let base_x = tx * ts as i32;
let base_y = ty * ts as i32;
let data = tile.data();
for ly in 0..ts {
for lx in 0..ts {
let px = base_x + lx as i32 - ox;
let py = base_y + ly as i32 - oy;
if px >= 0 && py >= 0 && (px as u32) < width && (py as u32) < height {
let v = (data.get(lx, ly).clamp(0.0, 1.0) * 255.0) as u8;
pixels[(py as u32 * width + px as u32) as usize] = v;
}
}
}
}
pixels
}
pub fn from_r8(pixels: &[u8], width: u32, height: u32) -> Self {
let ts = TILE_SIZE;
let mut mask = AlphaMask::new();
for py in 0..height {
for px in 0..width {
let v = pixels[(py * width + px) as usize];
if v > 0 {
let tx = (px / ts as u32) as i32;
let ty = (py / ts as u32) as i32;
let lx = (px % ts as u32) as usize;
let ly = (py % ts as u32) as usize;
mask.get_or_create(tx, ty)
.write()
.set(lx, ly, v as f32 / 255.0);
}
}
}
mask
}
}
#[cfg(test)]
impl AlphaMask {
fn fill_rect_test(&mut self, x: i32, y: i32, w: i32, h: i32, value: f32) {
let tile_size = TILE_SIZE as i32;
for py in y..y + h {
for px in x..x + w {
let (tx, ty) = Self::tile_coords_for_pixel(px, py);
let tile = self.get_or_create(tx, ty);
let lx = (px - tx * tile_size) as usize;
let ly = (py - ty * tile_size) as usize;
tile.write().set(lx, ly, value);
}
}
}
}
#[cfg(test)]
mod tests {
use crate::tile::AlphaMask;
#[test]
fn boolean_add() {
let mut a = AlphaMask::new();
let mut b = AlphaMask::new();
a.fill_rect_test(0, 0, 10, 10, 0.5);
b.fill_rect_test(5, 0, 10, 10, 0.5);
a.boolean_add(&b);
assert_eq!(a.sample(7, 5), 1.0);
assert_eq!(a.sample(2, 5), 0.5);
assert_eq!(a.sample(12, 5), 0.5);
}
#[test]
fn boolean_subtract() {
let mut a = AlphaMask::new();
let mut b = AlphaMask::new();
a.fill_rect_test(0, 0, 20, 10, 1.0);
b.fill_rect_test(10, 0, 20, 10, 1.0);
a.boolean_subtract(&b);
assert_eq!(a.sample(5, 5), 1.0);
assert_eq!(a.sample(15, 5), 0.0);
}
#[test]
fn boolean_intersect() {
let mut a = AlphaMask::new();
let mut b = AlphaMask::new();
a.fill_rect_test(0, 0, 20, 10, 1.0);
b.fill_rect_test(10, 0, 20, 10, 0.5);
a.boolean_intersect(&b);
assert_eq!(a.sample(5, 5), 0.0);
assert_eq!(a.sample(15, 5), 0.5);
}
#[test]
fn invert() {
let mut mask = AlphaMask::new();
mask.fill_rect_test(0, 0, 10, 10, 0.75);
mask.invert(64, 64);
assert!((mask.sample(5, 5) - 0.25).abs() < 1e-6);
assert!((mask.sample(32, 32) - 1.0).abs() < 1e-6);
}
#[test]
fn clear() {
let mut mask = AlphaMask::new();
mask.fill_rect_test(0, 0, 64, 64, 1.0);
assert!(!mask.is_empty());
mask.clear();
assert!(mask.is_empty());
assert_eq!(mask.sample(5, 5), 0.0);
}
#[test]
fn bounding_rect() {
let mut mask = AlphaMask::new();
assert!(mask.bounding_rect().is_none());
let (tx, ty) = AlphaMask::tile_coords_for_pixel(100, 200);
mask.get_or_create(tx, ty).write().set(0, 0, 1.0);
let (tx_min, ty_min, tx_max, ty_max) = mask.bounding_rect().unwrap();
assert_eq!(tx_min, 1); assert_eq!(ty_min, 3); assert_eq!(tx_max, 1);
assert_eq!(ty_max, 3);
}
#[test]
fn sample_empty() {
let mask = AlphaMask::new();
assert_eq!(mask.sample(0, 0), 0.0);
assert_eq!(mask.sample(1000, 1000), 0.0);
}
#[test]
fn rasterize_rect_hard_edge() {
let mut mask = AlphaMask::new();
mask.rasterize(
(5, 5, 20, 10),
|px, py| crate::sdf::sdf_rect(px, py, 15.0, 10.0, 10.0, 5.0),
false,
0.0,
);
assert_eq!(mask.sample(10, 8), 1.0);
assert_eq!(mask.sample(15, 10), 1.0);
assert_eq!(mask.sample(3, 8), 0.0);
assert_eq!(mask.sample(10, 20), 0.0);
}
#[test]
fn rasterize_rect_antialiased() {
let mut mask = AlphaMask::new();
mask.rasterize(
(10, 10, 41, 30),
|px, py| crate::sdf::sdf_rect(px, py, 30.25, 25.0, 20.0, 15.0),
true,
0.0,
);
assert_eq!(mask.sample(25, 20), 1.0);
assert_eq!(mask.sample(0, 0), 0.0);
let edge = mask.sample(50, 20);
assert!(
edge > 0.0 && edge < 1.0,
"edge pixel should be partially covered, got {edge}"
);
}
#[test]
fn rasterize_circle_hard_edge() {
let mut mask = AlphaMask::new();
mask.rasterize(
(0, 0, 100, 100),
|px, py| crate::sdf::sdf_circle(px, py, 50.0, 50.0, 30.0),
false,
0.0,
);
assert_eq!(mask.sample(50, 50), 1.0);
assert_eq!(mask.sample(50, 22), 1.0);
assert_eq!(mask.sample(50, 15), 0.0);
}
#[test]
fn rasterize_feathered() {
let mut mask = AlphaMask::new();
mask.rasterize(
(10, 10, 40, 30),
|px, py| crate::sdf::sdf_rect(px, py, 30.0, 25.0, 20.0, 15.0),
false,
4.0,
);
assert_eq!(mask.sample(25, 20), 1.0);
let near_edge = mask.sample(49, 20);
assert!(
near_edge > 0.0 && near_edge < 1.0,
"near-boundary pixel should be partially covered, got {near_edge}"
);
let just_outside = mask.sample(50, 20);
assert!(
just_outside > 0.0 && just_outside < 1.0,
"just-outside pixel should be partially covered, got {just_outside}"
);
let far_outside = mask.sample(52, 20);
assert!(
far_outside < 0.05,
"far outside should be ~0, got {far_outside}"
);
}
#[test]
fn rasterize_polygon() {
let mut mask = AlphaMask::new();
let verts = [[10.0, 10.0], [50.0, 10.0], [50.0, 50.0], [10.0, 50.0]];
mask.rasterize(
(10, 10, 40, 40),
|px, py| crate::sdf::sdf_polygon(px, py, &verts),
false,
0.0,
);
assert_eq!(mask.sample(30, 30), 1.0);
assert_eq!(mask.sample(5, 5), 0.0);
}
#[test]
fn rasterize_ellipse() {
let mut mask = AlphaMask::new();
mask.rasterize(
(0, 0, 100, 60),
|px, py| crate::sdf::sdf_ellipse(px, py, 50.0, 30.0, 40.0, 20.0),
false,
0.0,
);
assert_eq!(mask.sample(50, 30), 1.0);
assert_eq!(mask.sample(95, 30), 0.0);
assert_eq!(mask.sample(50, 55), 0.0);
}
#[test]
fn rasterize_sdf_r8_fully_below_right_is_empty() {
let mask = crate::mask::rasterize_sdf_r8(
100,
100,
(5000, 5000, 50, 50),
|px, py| crate::sdf::sdf_rect(px, py, 5025.0, 5025.0, 25.0, 25.0),
true,
0.0,
);
assert_eq!(mask.width, 0);
assert_eq!(mask.height, 0);
}
#[test]
fn rasterize_sdf_r8_fully_above_left_is_empty() {
let mask = crate::mask::rasterize_sdf_r8(
100,
100,
(-5000, -5000, 50, 50),
|px, py| crate::sdf::sdf_rect(px, py, -4975.0, -4975.0, 25.0, 25.0),
true,
0.0,
);
assert_eq!(mask.width, 0);
assert_eq!(mask.height, 0);
}
#[test]
fn rasterize_sdf_r8_partial_clamps_to_canvas() {
let mask = crate::mask::rasterize_sdf_r8(
100,
100,
(80, 80, 50, 50),
|px, py| crate::sdf::sdf_rect(px, py, 105.0, 105.0, 25.0, 25.0),
false,
0.0,
);
assert!(mask.width > 0 && mask.height > 0);
assert!(mask.x + mask.width <= 100);
assert!(mask.y + mask.height <= 100);
}
#[test]
fn pixel_bounds_r8_zero_dimension_is_none() {
assert!(crate::mask::pixel_bounds_r8(&[], 0, 5).is_none());
assert!(crate::mask::pixel_bounds_r8(&[], 5, 0).is_none());
assert!(crate::mask::pixel_bounds_r8(&[], 0, 0).is_none());
}
#[test]
fn pixel_bounds_r8_all_zero_is_none() {
let data = vec![0u8; 4 * 4];
assert!(crate::mask::pixel_bounds_r8(&data, 4, 4).is_none());
}
#[test]
fn rasterize_polygon_r8_fully_off_canvas_is_empty() {
let verts = [[5000.0, 5000.0], [5050.0, 5000.0], [5025.0, 5050.0]];
let mask = crate::mask::rasterize_polygon_r8(100, 100, &verts, true);
assert_eq!(mask.width, 0);
assert_eq!(mask.height, 0);
}
#[test]
fn rasterize_polygon_r8_negative_span_does_not_panic() {
let verts = [[0.0_f32, 0.0], [10.0, 0.0], [-100.0, 100.0]];
let mask = crate::mask::rasterize_polygon_r8(100, 100, &verts, true);
assert!(mask.x + mask.width <= 100);
assert!(mask.y + mask.height <= 100);
}
#[test]
fn feather_expands_mask() {
let mut mask = AlphaMask::new();
mask.fill_rect_test(20, 20, 20, 20, 1.0);
assert_eq!(mask.sample(18, 30), 0.0);
mask.feather(4.0);
let outside = mask.sample(18, 30);
assert!(
outside > 0.01,
"feather should expand mask beyond original boundary, got {outside}"
);
let center = mask.sample(30, 30);
assert!(
center > 0.9,
"center should remain near 1.0 after feather, got {center}"
);
}
#[test]
fn feather_zero_radius_noop() {
let mut mask = AlphaMask::new();
mask.fill_rect_test(10, 10, 10, 10, 0.75);
let before = mask.sample(15, 15);
mask.feather(0.0);
assert_eq!(mask.sample(15, 15), before);
}
#[test]
fn feather_empty_mask() {
let mut mask = AlphaMask::new();
mask.feather(5.0); assert!(mask.is_empty());
}
#[test]
fn contour_empty_mask() {
let mask = AlphaMask::new();
assert!(mask.contour_segments(0.5).is_empty());
}
#[test]
fn contour_rect_produces_segments() {
let mut mask = AlphaMask::new();
mask.fill_rect_test(10, 10, 20, 20, 1.0);
let segs = mask.contour_segments(0.5);
assert!(
!segs.is_empty(),
"contour should produce segments for a filled rect"
);
for (a, b) in &segs {
let near_boundary = (a[0] >= 9.0 && a[0] <= 31.0)
&& (a[1] >= 9.0 && a[1] <= 31.0)
&& (b[0] >= 9.0 && b[0] <= 31.0)
&& (b[1] >= 9.0 && b[1] <= 31.0);
assert!(
near_boundary,
"segment [{},{}]-[{},{}] should be near boundary",
a[0], a[1], b[0], b[1]
);
}
}
fn rect_buffer_r8(stride: u32, rect_x: u32, rect_y: u32, rect_w: u32, rect_h: u32) -> Vec<u8> {
let mut buf = vec![0u8; (stride * stride) as usize];
for y in rect_y..rect_y + rect_h {
for x in rect_x..rect_x + rect_w {
buf[(y * stride + x) as usize] = 255;
}
}
buf
}
#[test]
fn polylines_empty_mask() {
let buf = vec![0u8; 16 * 16];
assert!(crate::mask::contour_polylines_r8(&buf, 16, 16, 127).is_empty());
}
#[test]
fn polylines_rect_forms_closed_loop() {
let buf = rect_buffer_r8(20, 5, 5, 8, 8);
let polylines = crate::mask::contour_polylines_r8(&buf, 20, 20, 127);
assert_eq!(
polylines.len(),
1,
"a single filled rectangle should produce one polyline, got {}",
polylines.len()
);
let poly = &polylines[0];
assert!(
poly.len() >= 4,
"expected at least 4 points, got {}",
poly.len()
);
let first = poly[0];
let last = *poly.last().unwrap();
let dx = first[0] - last[0];
let dy = first[1] - last[1];
assert!(
dx * dx + dy * dy < 1e-3,
"polyline should be a closed loop: first={:?} last={:?}",
first,
last
);
}
#[test]
fn polylines_segments_are_chained() {
let buf = rect_buffer_r8(20, 5, 5, 8, 8);
let polylines = crate::mask::contour_polylines_r8(&buf, 20, 20, 127);
for poly in &polylines {
for i in 1..poly.len() {
let a = poly[i - 1];
let b = poly[i];
let len_sq = (b[0] - a[0]).powi(2) + (b[1] - a[1]).powi(2);
assert!(len_sq > 0.0, "degenerate segment in polyline at index {i}");
}
}
}
#[test]
fn polylines_full_canvas_traces_border() {
let w = 12u32;
let h = 12u32;
let buf = vec![255u8; (w * h) as usize];
let polylines = crate::mask::contour_polylines_r8(&buf, w, h, 127);
assert_eq!(
polylines.len(),
1,
"a fully-filled mask should trace one border loop, got {}",
polylines.len()
);
let mut arc = 0.0_f32;
for win in polylines[0].windows(2) {
let dx = win[1][0] - win[0][0];
let dy = win[1][1] - win[0][1];
arc += (dx * dx + dy * dy).sqrt();
}
let expected = 2.0 * (w as f32 + h as f32);
assert!(
(arc - expected).abs() < 4.0,
"border perimeter ≈ {expected}, got {arc}"
);
}
#[test]
fn polylines_inverted_rect_has_border_and_hole() {
let stride = 20u32;
let mut buf = vec![255u8; (stride * stride) as usize];
for y in 6..12 {
for x in 6..12 {
buf[(y * stride + x) as usize] = 0;
}
}
let polylines = crate::mask::contour_polylines_r8(&buf, stride, stride, 127);
assert_eq!(
polylines.len(),
2,
"inverted-rect mask should yield a border loop and a hole loop, got {}",
polylines.len()
);
}
#[test]
fn polylines_perimeter_matches_rect_size() {
let w = 8.0;
let h = 8.0;
let buf = rect_buffer_r8(20, 5, 5, w as u32, h as u32);
let polylines = crate::mask::contour_polylines_r8(&buf, 20, 20, 127);
assert_eq!(polylines.len(), 1);
let mut arc = 0.0_f32;
for win in polylines[0].windows(2) {
let dx = win[1][0] - win[0][0];
let dy = win[1][1] - win[0][1];
arc += (dx * dx + dy * dy).sqrt();
}
let expected = 2.0 * (w + h);
assert!(
(arc - expected).abs() < 4.0,
"expected perimeter ≈ {expected}, got {arc}"
);
}
}