use crate::framebuffer::Framebuffer;
use oxiui_core::Color;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
pub enum FillRule {
EvenOdd,
#[default]
NonZero,
}
#[derive(Clone, Debug)]
pub(crate) struct Edge {
x: f32,
dx: f32,
y_max: i32,
winding: i32,
}
pub struct RasterizerScratch {
pub(crate) global_edges: Vec<(i32, Edge)>,
pub(crate) active: Vec<Edge>,
}
impl RasterizerScratch {
pub fn new() -> Self {
Self {
global_edges: Vec::new(),
active: Vec::new(),
}
}
pub fn clear(&mut self) {
self.global_edges.clear();
self.active.clear();
}
}
impl Default for RasterizerScratch {
fn default() -> Self {
Self::new()
}
}
pub struct Rasterizer {
scratch: RasterizerScratch,
}
impl Rasterizer {
pub fn new() -> Self {
Self {
scratch: RasterizerScratch::new(),
}
}
pub fn fill_polygon(
&mut self,
fb: &mut Framebuffer,
points: &[(f32, f32)],
color: Color,
fill_rule: FillRule,
aa: bool,
) {
fill_polygon_with_scratch(fb, points, color, fill_rule, aa, &mut self.scratch);
}
pub fn fill_polygon_clipped(
&mut self,
fb: &mut Framebuffer,
points: &[(f32, f32)],
color: Color,
fill_rule: FillRule,
aa: bool,
clip: crate::clip::ClipRect,
) {
fill_polygon_clipped_with_scratch(
fb,
points,
color,
fill_rule,
aa,
clip,
&mut self.scratch,
);
}
}
impl Default for Rasterizer {
fn default() -> Self {
Self::new()
}
}
fn build_edges_into(points: &[(f32, f32)], out: &mut Vec<(i32, Edge)>) {
let n = points.len();
if n < 2 {
return;
}
out.reserve(n);
for i in 0..n {
let (x0, y0) = points[i];
let (x1, y1) = points[(i + 1) % n];
if (y0 - y1).abs() < f32::EPSILON {
continue;
}
let (top_y, bot_y, top_x, bot_x, winding) = if y0 < y1 {
(y0, y1, x0, x1, 1i32)
} else {
(y1, y0, x1, x0, -1i32)
};
let dy = bot_y - top_y;
let dx = (bot_x - top_x) / dy;
let y_start = top_y.ceil() as i32;
let x_at_start = top_x + dx * (y_start as f32 - top_y);
let y_max = bot_y.ceil() as i32;
out.push((
y_start,
Edge {
x: x_at_start,
dx,
y_max,
winding,
},
));
}
}
#[inline]
fn span_coverage(left: f32, right: f32, px: f32) -> f32 {
let pl = px;
let pr = px + 1.0;
if right <= pl || left >= pr {
return 0.0;
}
let cl = left.max(pl);
let cr = right.min(pr);
(cr - cl).clamp(0.0, 1.0)
}
fn fill_polygon_with_scratch(
fb: &mut Framebuffer,
points: &[(f32, f32)],
color: Color,
fill_rule: FillRule,
aa: bool,
scratch: &mut RasterizerScratch,
) {
if points.len() < 3 {
return;
}
let (mut min_y, mut max_y) = (f32::INFINITY, f32::NEG_INFINITY);
for &(_, y) in points {
if y < min_y {
min_y = y;
}
if y > max_y {
max_y = y;
}
}
let y_start = min_y.floor() as i32;
let y_end = max_y.ceil() as i32;
scratch.clear();
build_edges_into(points, &mut scratch.global_edges);
scratch.global_edges.sort_by(|a, b| {
a.0.cmp(&b.0).then(
a.1.x
.partial_cmp(&b.1.x)
.unwrap_or(core::cmp::Ordering::Equal),
)
});
let mut gi = 0usize;
for y in y_start..y_end {
while gi < scratch.global_edges.len() && scratch.global_edges[gi].0 == y {
scratch.active.push(scratch.global_edges[gi].1.clone());
gi += 1;
}
scratch.active.retain(|e| e.y_max > y);
scratch
.active
.sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap_or(core::cmp::Ordering::Equal));
fill_spans(
fb,
&scratch.active,
y as u32,
y as f32,
color,
fill_rule,
aa,
);
for e in &mut scratch.active {
e.x += e.dx;
}
}
}
pub fn fill_polygon(
fb: &mut Framebuffer,
points: &[(f32, f32)],
color: Color,
fill_rule: FillRule,
aa: bool,
) {
let mut scratch = RasterizerScratch::new();
fill_polygon_with_scratch(fb, points, color, fill_rule, aa, &mut scratch);
}
fn fill_spans(
fb: &mut Framebuffer,
active: &[Edge],
y: u32,
_y_float: f32,
color: Color,
fill_rule: FillRule,
aa: bool,
) {
if y >= fb.height() || active.is_empty() {
return;
}
match fill_rule {
FillRule::EvenOdd => fill_even_odd(fb, active, y, color, aa),
FillRule::NonZero => fill_non_zero(fb, active, y, color, aa),
}
}
fn fill_even_odd(fb: &mut Framebuffer, active: &[Edge], y: u32, color: Color, aa: bool) {
let mut i = 0;
while i + 1 < active.len() {
let left = active[i].x;
let right = active[i + 1].x;
if right > left {
paint_span(fb, left, right, y, color, aa);
}
i += 2;
}
}
fn fill_non_zero(fb: &mut Framebuffer, active: &[Edge], y: u32, color: Color, aa: bool) {
let mut winding = 0i32;
let mut fill_start: Option<f32> = None;
for edge in active {
let prev_winding = winding;
winding += edge.winding;
if prev_winding == 0 && winding != 0 {
fill_start = Some(edge.x);
} else if prev_winding != 0 && winding == 0 {
if let Some(start) = fill_start.take() {
let end = edge.x;
if end > start {
paint_span(fb, start, end, y, color, aa);
}
}
}
}
}
fn paint_span(fb: &mut Framebuffer, left: f32, right: f32, y: u32, color: Color, aa: bool) {
if y >= fb.height() {
return;
}
let x0 = left.floor() as i32;
let x1 = right.ceil() as i32;
let Color(cr, cg, cb, ca) = color;
for px in x0..x1 {
if px < 0 || px as u32 >= fb.width() {
continue;
}
let coverage = if aa {
span_coverage(left, right, px as f32)
} else {
let centre = px as f32 + 0.5;
if centre >= left && centre < right {
1.0
} else {
0.0
}
};
if coverage <= 0.0 {
continue;
}
let alpha = (ca as f32 * coverage).round() as u8;
fb.blend(
px as u32,
y,
crate::framebuffer::pack_rgba(cr, cg, cb, alpha),
);
}
}
fn fill_polygon_clipped_with_scratch(
fb: &mut Framebuffer,
points: &[(f32, f32)],
color: Color,
fill_rule: FillRule,
aa: bool,
clip: crate::clip::ClipRect,
scratch: &mut RasterizerScratch,
) {
if points.len() < 3 {
return;
}
let (mut min_y, mut max_y) = (f32::INFINITY, f32::NEG_INFINITY);
for &(_, y) in points {
if y < min_y {
min_y = y;
}
if y > max_y {
max_y = y;
}
}
let y_clip_start = (min_y.floor() as i64).max(clip.y0) as i32;
let y_end = (max_y.ceil() as i64).min(clip.y1) as i32;
scratch.clear();
build_edges_into(points, &mut scratch.global_edges);
scratch.global_edges.sort_by(|a, b| {
a.0.cmp(&b.0).then(
a.1.x
.partial_cmp(&b.1.x)
.unwrap_or(core::cmp::Ordering::Equal),
)
});
let mut gi = 0usize;
for y in (min_y.floor() as i32)..y_end {
while gi < scratch.global_edges.len() && scratch.global_edges[gi].0 == y {
scratch.active.push(scratch.global_edges[gi].1.clone());
gi += 1;
}
scratch.active.retain(|e| e.y_max > y);
scratch
.active
.sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap_or(core::cmp::Ordering::Equal));
if y >= y_clip_start {
fill_spans_clipped(fb, &scratch.active, y as u32, color, fill_rule, aa, &clip);
}
for e in &mut scratch.active {
e.x += e.dx;
}
}
}
pub fn fill_polygon_clipped(
fb: &mut Framebuffer,
points: &[(f32, f32)],
color: Color,
fill_rule: FillRule,
aa: bool,
clip: crate::clip::ClipRect,
) {
let mut scratch = RasterizerScratch::new();
fill_polygon_clipped_with_scratch(fb, points, color, fill_rule, aa, clip, &mut scratch);
}
fn fill_spans_clipped(
fb: &mut Framebuffer,
active: &[Edge],
y: u32,
color: Color,
fill_rule: FillRule,
aa: bool,
clip: &crate::clip::ClipRect,
) {
if y >= fb.height() || (y as i64) < clip.y0 || (y as i64) >= clip.y1 || active.is_empty() {
return;
}
match fill_rule {
FillRule::EvenOdd => {
let mut i = 0;
while i + 1 < active.len() {
let left = active[i].x.max(clip.x0 as f32);
let right = active[i + 1].x.min(clip.x1 as f32);
if right > left {
paint_span(fb, left, right, y, color, aa);
}
i += 2;
}
}
FillRule::NonZero => {
let mut winding = 0i32;
let mut fill_start: Option<f32> = None;
for edge in active {
let prev_winding = winding;
winding += edge.winding;
if prev_winding == 0 && winding != 0 {
fill_start = Some(edge.x.max(clip.x0 as f32));
} else if prev_winding != 0 && winding == 0 {
if let Some(start) = fill_start.take() {
let end = edge.x.min(clip.x1 as f32);
if end > start {
paint_span(fb, start, end, y, color, aa);
}
}
}
}
}
}
}
pub fn fill_triangle(
fb: &mut Framebuffer,
p0: (f32, f32),
p1: (f32, f32),
p2: (f32, f32),
color: Color,
) {
fill_polygon(fb, &[p0, p1, p2], color, FillRule::NonZero, true);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::framebuffer::Framebuffer;
fn fresh(w: u32, h: u32) -> Framebuffer {
Framebuffer::with_fill(w, h, Color(0, 0, 0, 255))
}
#[test]
fn triangle_fill_coverage() {
let mut fb = fresh(20, 20);
fill_triangle(
&mut fb,
(0.0, 0.0),
(20.0, 0.0),
(10.0, 20.0),
Color(255, 255, 255, 255),
);
let mut count = 0u32;
for y in 0..20 {
for x in 0..20 {
let (r, _, _, _) = fb.get_rgba(x, y).unwrap_or((0, 0, 0, 0));
if r > 0 {
count += 1;
}
}
}
assert!(count >= 50, "expected >= 50 filled pixels, got {count}");
}
#[test]
fn polygon_fill_even_odd() {
let cx = 35.0f32;
let cy = 35.0f32;
let r = 30.0f32;
let star: Vec<(f32, f32)> = (0..5)
.map(|i| {
let angle = std::f32::consts::PI * (2.0 * (2 * i) as f32 / 5.0 - 0.5);
(cx + r * angle.cos(), cy + r * angle.sin())
})
.collect();
let mut fb_eo = fresh(70, 70);
let mut fb_nz = fresh(70, 70);
fill_polygon(
&mut fb_eo,
&star,
Color(255, 0, 0, 255),
FillRule::EvenOdd,
false,
);
fill_polygon(
&mut fb_nz,
&star,
Color(255, 0, 0, 255),
FillRule::NonZero,
false,
);
let (r_eo_tip, _, _, _) = fb_eo.get_rgba(35, 8).unwrap_or((0, 0, 0, 0));
let (r_nz_tip, _, _, _) = fb_nz.get_rgba(35, 8).unwrap_or((0, 0, 0, 0));
assert!(r_eo_tip > 0, "EvenOdd: star arm should be painted");
assert!(r_nz_tip > 0, "NonZero: star arm should be painted");
let (r_nz_ctr, _, _, _) = fb_nz.get_rgba(35, 35).unwrap_or((0, 0, 0, 0));
assert!(r_nz_ctr > 0, "NonZero: star center must be filled");
let (r_eo_ctr, _, _, _) = fb_eo.get_rgba(35, 35).unwrap_or((0, 0, 0, 0));
assert_eq!(
r_eo_ctr, 0,
"EvenOdd: star center should be a hole (r={r_eo_ctr})"
);
}
#[test]
fn fill_rect_via_polygon() {
let mut fb = fresh(10, 10);
let pts = [(2.0f32, 2.0), (8.0, 2.0), (8.0, 8.0), (2.0, 8.0)];
fill_polygon(
&mut fb,
&pts,
Color(0, 255, 0, 255),
FillRule::NonZero,
false,
);
assert_eq!(fb.get_rgba(5, 5), Some((0, 255, 0, 255)));
assert_eq!(fb.get_rgba(0, 0), Some((0, 0, 0, 255)));
}
#[test]
fn fill_rule_noop_on_empty_polygon() {
let mut fb = fresh(5, 5);
fill_polygon(
&mut fb,
&[],
Color(255, 0, 0, 255),
FillRule::NonZero,
false,
);
assert_eq!(fb.get_rgba(2, 2), Some((0, 0, 0, 255)));
}
#[test]
fn test_scanline_reuse_output_identical() {
let triangle: &[(f32, f32)] = &[(10.0, 90.0), (50.0, 10.0), (90.0, 90.0)];
let color = Color(200, 100, 50, 255);
let mut fb1 = fresh(100, 100);
let mut ras = Rasterizer::new();
ras.fill_polygon(&mut fb1, triangle, color, FillRule::NonZero, true);
let mut fb2 = fresh(100, 100);
ras.fill_polygon(&mut fb2, triangle, color, FillRule::NonZero, true);
for y in 0..100 {
for x in 0..100 {
let p1 = fb1.get_rgba(x, y);
let p2 = fb2.get_rgba(x, y);
assert_eq!(
p1, p2,
"pixel ({x},{y}) differs: first={p1:?} second={p2:?}"
);
}
}
let mut fb3 = fresh(100, 100);
fill_polygon(&mut fb3, triangle, color, FillRule::NonZero, true);
for y in 0..100 {
for x in 0..100 {
let p1 = fb1.get_rgba(x, y);
let p3 = fb3.get_rgba(x, y);
assert_eq!(
p1, p3,
"pixel ({x},{y}): Rasterizer={p1:?} vs free fn={p3:?}"
);
}
}
}
}