use std::cmp::Ordering;
use crate::geometry::{Color, Rect};
use crate::painter::Painter;
const SAMPLES: i32 = 4;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum FillRule {
NonZero,
EvenOdd,
}
#[derive(Clone, Copy)]
pub struct SvgPolygon {
pub color: Color,
pub fill_rule: FillRule,
pub rings: &'static [&'static [(f32, f32)]],
}
#[derive(Clone, Copy)]
pub struct SvgImage {
pub width: f32,
pub height: f32,
pub polygons: &'static [SvgPolygon],
}
impl SvgImage {
pub fn draw(&self, painter: &mut Painter, rect: Rect) {
self.render(painter, rect, None);
}
pub fn draw_tinted(&self, painter: &mut Painter, rect: Rect, tint: Color) {
self.render(painter, rect, Some(tint));
}
fn render(&self, painter: &mut Painter, rect: Rect, tint: Option<Color>) {
if self.width <= 0.0 || self.height <= 0.0 || self.polygons.is_empty() {
return;
}
painter.physical(rect, |p, phys| self.fill_phys(p, phys, tint));
}
fn fill_phys(&self, painter: &mut Painter, phys: Rect, tint: Option<Color>) {
if phys.w <= 0 || phys.h <= 0 {
return;
}
let scale = (phys.w as f32 / self.width).min(phys.h as f32 / self.height);
if scale <= 0.0 {
return;
}
let tx = phys.x as f32 + (phys.w as f32 - self.width * scale) * 0.5;
let ty = phys.y as f32 + (phys.h as f32 - self.height * scale) * 0.5;
let mut raster = Rasterizer::new(phys);
for poly in self.polygons {
raster.fill(painter, poly, scale, tx, ty, tint);
}
}
}
struct Edge {
y_top: f32,
y_bot: f32,
x_top: f32,
dxdy: f32,
dir: i32,
}
struct Rasterizer {
x0: i32,
y0: i32,
x1: i32,
y1: i32,
coverage: Vec<f32>,
edges: Vec<Edge>,
crossings: Vec<(f32, i32)>,
}
impl Rasterizer {
fn new(phys: Rect) -> Self {
let width = phys.w.max(0) as usize;
Self {
x0: phys.x,
y0: phys.y,
x1: phys.x + phys.w,
y1: phys.y + phys.h,
coverage: vec![0.0; width],
edges: Vec::new(),
crossings: Vec::new(),
}
}
fn fill(
&mut self,
painter: &mut Painter,
poly: &SvgPolygon,
scale: f32,
tx: f32,
ty: f32,
tint: Option<Color>,
) {
self.edges.clear();
let map = |&(x, y): &(f32, f32)| (tx + x * scale, ty + y * scale);
let (mut min_x, mut min_y) = (f32::MAX, f32::MAX);
let (mut max_x, mut max_y) = (f32::MIN, f32::MIN);
for ring in poly.rings {
let n = ring.len();
if n < 3 {
continue;
}
for i in 0..n {
let (x, y) = map(&ring[i]);
min_x = min_x.min(x);
min_y = min_y.min(y);
max_x = max_x.max(x);
max_y = max_y.max(y);
let (xa, ya) = (x, y);
let (xb, yb) = map(&ring[(i + 1) % n]);
if ya == yb {
continue; }
let (y_top, x_top, y_bot, x_bot, dir) = if ya < yb {
(ya, xa, yb, xb, 1)
} else {
(yb, xb, ya, xa, -1)
};
self.edges.push(Edge {
y_top,
y_bot,
x_top,
dxdy: (x_bot - x_top) / (y_bot - y_top),
dir,
});
}
}
if self.edges.is_empty() {
return;
}
let row_lo = (min_y.floor() as i32).max(self.y0);
let row_hi = (max_y.ceil() as i32).min(self.y1);
let col_lo = (min_x.floor() as i32).max(self.x0);
let col_hi = (max_x.ceil() as i32).min(self.x1);
if row_hi <= row_lo || col_hi <= col_lo {
return;
}
let width = (col_hi - col_lo) as usize;
if self.coverage.len() < width {
self.coverage.resize(width, 0.0);
}
let cov = &mut self.coverage[..width];
let color = tint.unwrap_or(poly.color);
let max_alpha = color.alpha() as f32;
let weight = 1.0 / SAMPLES as f32;
for iy in row_lo..row_hi {
for c in cov.iter_mut() {
*c = 0.0;
}
for s in 0..SAMPLES {
let sy = iy as f32 + (s as f32 + 0.5) * weight;
self.crossings.clear();
for e in &self.edges {
if sy >= e.y_top && sy < e.y_bot {
self.crossings
.push((e.x_top + (sy - e.y_top) * e.dxdy, e.dir));
}
}
if self.crossings.len() < 2 {
continue;
}
self.crossings
.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(Ordering::Equal));
let mut winding = 0i32;
for i in 0..self.crossings.len() - 1 {
winding += self.crossings[i].1;
let inside = match poly.fill_rule {
FillRule::NonZero => winding != 0,
FillRule::EvenOdd => (i as i32 + 1) & 1 == 1,
};
if !inside {
continue;
}
let xa = self.crossings[i].0.max(col_lo as f32);
let xb = self.crossings[i + 1].0.min(col_hi as f32);
add_span(cov, col_lo, xa, xb, weight);
}
}
for (k, &cv) in cov.iter().enumerate() {
if cv <= 0.0 {
continue;
}
let alpha = (max_alpha * cv.min(1.0)).round() as i32;
if alpha <= 0 {
continue;
}
painter.blend_pixel_phys(col_lo + k as i32, iy, color, alpha.min(255) as u8);
}
}
}
}
fn add_span(cov: &mut [f32], base_x: i32, xa: f32, xb: f32, weight: f32) {
if xb <= xa {
return;
}
let start = xa.floor() as i32;
let end = xb.ceil() as i32;
for ix in start..end {
let left = (ix as f32).max(xa);
let right = ((ix + 1) as f32).min(xb);
let frac = (right - left).clamp(0.0, 1.0);
if frac <= 0.0 {
continue;
}
let idx = ix - base_x;
if idx >= 0 && (idx as usize) < cov.len() {
cov[idx as usize] += frac * weight;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::FontSet;
fn render(image: &SvgImage, size: i32) -> Vec<u32> {
let mut pixels = vec![Color::WHITE.0; (size * size) as usize];
{
let mut p = Painter::new(&mut pixels, size, size, 1.0, 0, 0, FontSet::default());
image.draw(&mut p, Rect::new(0, 0, size, size));
}
pixels
}
const SQUARE: SvgImage = SvgImage {
width: 1.0,
height: 1.0,
polygons: &[SvgPolygon {
color: Color::BLACK,
fill_rule: FillRule::NonZero,
rings: &[&[(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)]],
}],
};
#[test]
fn fills_the_whole_box_opaquely() {
let px = render(&SQUARE, 16);
let at = |x: i32, y: i32| Color(px[(y * 16 + x) as usize]);
for (x, y) in [(0, 0), (15, 0), (0, 15), (15, 15), (8, 8)] {
assert_eq!(at(x, y), Color::BLACK, "({x},{y}) should be filled");
}
}
#[test]
fn draw_tinted_recolors_every_polygon() {
let size = 8;
let mut px = vec![Color::WHITE.0; (size * size) as usize];
{
let mut p = Painter::new(&mut px, size, size, 1.0, 0, 0, FontSet::default());
SQUARE.draw_tinted(&mut p, Rect::new(0, 0, size, size), Color::RED);
}
let at = |x: i32, y: i32| Color(px[(y * size + x) as usize]);
for (x, y) in [(0, 0), (7, 7), (4, 4)] {
assert_eq!(at(x, y), Color::RED, "({x},{y}) should be tinted red");
}
}
#[test]
fn empty_or_degenerate_images_draw_nothing() {
let blank = SvgImage {
width: 1.0,
height: 1.0,
polygons: &[],
};
assert!(render(&blank, 8).iter().all(|&p| Color(p) == Color::WHITE));
let sliver = SvgImage {
width: 1.0,
height: 1.0,
polygons: &[SvgPolygon {
color: Color::BLACK,
fill_rule: FillRule::NonZero,
rings: &[&[(0.0, 0.0), (1.0, 1.0)]],
}],
};
assert!(render(&sliver, 8).iter().all(|&p| Color(p) == Color::WHITE));
}
#[test]
fn even_odd_rule_punches_a_hole() {
let donut = SvgImage {
width: 9.0,
height: 9.0,
polygons: &[SvgPolygon {
color: Color::BLACK,
fill_rule: FillRule::EvenOdd,
rings: &[
&[(0.0, 0.0), (9.0, 0.0), (9.0, 9.0), (0.0, 9.0)],
&[(3.0, 3.0), (6.0, 3.0), (6.0, 6.0), (3.0, 6.0)],
],
}],
};
let px = render(&donut, 9);
let at = |x: i32, y: i32| Color(px[(y * 9 + x) as usize]);
assert_eq!(at(1, 1), Color::BLACK, "outer band is filled");
assert_eq!(at(4, 4), Color::WHITE, "even-odd leaves the center hole");
}
#[test]
fn blends_partial_coverage_at_edges() {
let half = SvgImage {
width: 3.0,
height: 1.0,
polygons: &[SvgPolygon {
color: Color::BLACK,
fill_rule: FillRule::NonZero,
rings: &[&[(0.0, 0.0), (1.5, 0.0), (1.5, 1.0), (0.0, 1.0)]],
}],
};
let mut pixels = vec![Color::WHITE.0; 3];
{
let mut p = Painter::new(&mut pixels, 3, 1, 1.0, 0, 0, FontSet::default());
half.draw(&mut p, Rect::new(0, 0, 3, 1));
}
assert_eq!(Color(pixels[0]), Color::BLACK, "fully covered column");
assert_eq!(Color(pixels[2]), Color::WHITE, "untouched column");
let mid = Color(pixels[1]);
assert!(
mid != Color::BLACK && mid != Color::WHITE,
"half-covered column should be a gray blend, got {mid:?}",
);
}
}