use alloc::vec::Vec;
use crate::types::{Fixed, Point};
use super::path::{Path, PathCmd};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct LineSeg {
pub p1: Point,
pub p2: Point,
}
#[derive(Clone, Debug)]
pub(crate) struct SubPath {
pub segs: Vec<LineSeg>,
pub closed: bool,
}
const QUAD_STEPS: i32 = 8;
const CUBIC_STEPS: i32 = 16;
pub(crate) fn flatten(path: &Path) -> Vec<LineSeg> {
let mut out = Vec::new();
let mut subpath_start = Point::ZERO;
let mut current = Point::ZERO;
for cmd in &path.cmds {
match cmd {
PathCmd::MoveTo(p) => {
subpath_start = *p;
current = *p;
}
PathCmd::LineTo(p) => {
out.push(LineSeg {
p1: current,
p2: *p,
});
current = *p;
}
PathCmd::QuadTo { ctrl, end } => {
let p0 = current;
for i in 1..=QUAD_STEPS {
let t = Fixed::from_int(i) / Fixed::from_int(QUAD_STEPS);
let next = quad_at(p0, *ctrl, *end, t);
out.push(LineSeg {
p1: current,
p2: next,
});
current = next;
}
}
PathCmd::CubicTo { ctrl1, ctrl2, end } => {
let p0 = current;
for i in 1..=CUBIC_STEPS {
let t = Fixed::from_int(i) / Fixed::from_int(CUBIC_STEPS);
let next = cubic_at(p0, *ctrl1, *ctrl2, *end, t);
out.push(LineSeg {
p1: current,
p2: next,
});
current = next;
}
}
PathCmd::Close => {
if current != subpath_start {
out.push(LineSeg {
p1: current,
p2: subpath_start,
});
}
current = subpath_start;
}
}
}
out
}
pub(crate) fn flatten_subpaths(path: &Path) -> Vec<SubPath> {
let mut out: Vec<SubPath> = Vec::new();
let mut subpath_start = Point::ZERO;
let mut current = Point::ZERO;
let mut current_segs: Vec<LineSeg> = Vec::new();
let mut has_moveto = false;
let flush = |segs: &mut Vec<LineSeg>, out: &mut Vec<SubPath>, closed: bool| {
if !segs.is_empty() {
out.push(SubPath {
segs: core::mem::take(segs),
closed,
});
}
};
for cmd in &path.cmds {
match cmd {
PathCmd::MoveTo(p) => {
flush(&mut current_segs, &mut out, false);
subpath_start = *p;
current = *p;
has_moveto = true;
}
PathCmd::LineTo(p) => {
if !has_moveto {
continue;
}
current_segs.push(LineSeg {
p1: current,
p2: *p,
});
current = *p;
}
PathCmd::QuadTo { ctrl, end } => {
if !has_moveto {
continue;
}
let p0 = current;
for i in 1..=QUAD_STEPS {
let t = Fixed::from_int(i) / Fixed::from_int(QUAD_STEPS);
let next = quad_at(p0, *ctrl, *end, t);
current_segs.push(LineSeg {
p1: current,
p2: next,
});
current = next;
}
}
PathCmd::CubicTo { ctrl1, ctrl2, end } => {
if !has_moveto {
continue;
}
let p0 = current;
for i in 1..=CUBIC_STEPS {
let t = Fixed::from_int(i) / Fixed::from_int(CUBIC_STEPS);
let next = cubic_at(p0, *ctrl1, *ctrl2, *end, t);
current_segs.push(LineSeg {
p1: current,
p2: next,
});
current = next;
}
}
PathCmd::Close => {
if current != subpath_start {
current_segs.push(LineSeg {
p1: current,
p2: subpath_start,
});
}
current = subpath_start;
flush(&mut current_segs, &mut out, true);
has_moveto = false;
}
}
}
flush(&mut current_segs, &mut out, false);
out
}
const SUB_SCANLINES: i32 = 4;
pub(crate) fn scanline_fill(
segs: &[LineSeg],
px_x0: i32,
py_y0: i32,
px_x1: i32,
py_y1: i32,
mut emit: impl FnMut(i32, i32, Fixed),
) {
if segs.is_empty() || px_x1 <= px_x0 || py_y1 <= py_y0 {
return;
}
let row_w = (px_x1 - px_x0) as usize;
let mut acc: Vec<Fixed> = alloc::vec![Fixed::ZERO; row_w];
let mut crossings: Vec<Fixed> = Vec::with_capacity(segs.len());
let sub_weight = Fixed::ONE / SUB_SCANLINES;
for py in py_y0..py_y1 {
for a in acc.iter_mut() {
*a = Fixed::ZERO;
}
for sub in 0..SUB_SCANLINES {
let y_sample =
Fixed::from_int(py) + (Fixed::from_int(sub) + Fixed::ONE / 2) / SUB_SCANLINES;
crossings.clear();
for s in segs {
let (y_lo, y_hi) = if s.p1.y <= s.p2.y {
(s.p1.y, s.p2.y)
} else {
(s.p2.y, s.p1.y)
};
if y_sample < y_lo || y_sample >= y_hi {
continue;
}
let dy = s.p2.y - s.p1.y;
if dy == Fixed::ZERO {
continue;
}
let t = (y_sample - s.p1.y) / dy;
let x_cross = s.p1.x + (s.p2.x - s.p1.x) * t;
crossings.push(x_cross);
}
crossings.sort();
let mut i = 0;
while i + 1 < crossings.len() {
let xa = crossings[i];
let xb = crossings[i + 1];
accumulate_interval(&mut acc, px_x0, px_x1, xa, xb, sub_weight);
i += 2;
}
}
for (i, cov) in acc.iter().enumerate() {
if *cov > Fixed::ZERO {
emit(px_x0 + i as i32, py, *cov);
}
}
}
}
fn accumulate_interval(
acc: &mut [Fixed],
px_x0: i32,
px_x1: i32,
xa: Fixed,
xb: Fixed,
weight: Fixed,
) {
let (xlo, xhi) = if xa <= xb { (xa, xb) } else { (xb, xa) };
if xhi <= Fixed::from_int(px_x0) || xlo >= Fixed::from_int(px_x1) {
return;
}
let lo_int = xlo.to_int().max(px_x0);
let hi_int = xhi.ceil().to_int().min(px_x1);
for px in lo_int..hi_int {
let pixel_left = Fixed::from_int(px);
let pixel_right = Fixed::from_int(px + 1);
let left = xlo.max(pixel_left);
let right = xhi.min(pixel_right);
let frac = right - left;
if frac > Fixed::ZERO {
acc[(px - px_x0) as usize] += frac * weight;
}
}
}
#[allow(dead_code)] pub(crate) fn point_in_segments(pt: Point, segs: &[LineSeg]) -> bool {
let mut crossings = 0u32;
for s in segs {
let (y_lo, y_hi) = if s.p1.y <= s.p2.y {
(s.p1.y, s.p2.y)
} else {
(s.p2.y, s.p1.y)
};
if pt.y < y_lo || pt.y >= y_hi {
continue;
}
let dy = s.p2.y - s.p1.y;
if dy == Fixed::ZERO {
continue;
}
let t = (pt.y - s.p1.y) / dy;
let x_cross = s.p1.x + (s.p2.x - s.p1.x) * t;
if x_cross > pt.x {
crossings += 1;
}
}
crossings & 1 == 1
}
#[allow(dead_code)] pub(crate) fn min_dist_to_segments_capped(pt: Point, segs: &[LineSeg], cap: Fixed) -> Fixed {
let mut best = cap;
let mut found = false;
for s in segs {
let (xlo, xhi) = if s.p1.x <= s.p2.x {
(s.p1.x, s.p2.x)
} else {
(s.p2.x, s.p1.x)
};
let (ylo, yhi) = if s.p1.y <= s.p2.y {
(s.p1.y, s.p2.y)
} else {
(s.p2.y, s.p1.y)
};
let dx_box = if pt.x < xlo {
xlo - pt.x
} else if pt.x > xhi {
pt.x - xhi
} else {
Fixed::ZERO
};
let dy_box = if pt.y < ylo {
ylo - pt.y
} else if pt.y > yhi {
pt.y - yhi
} else {
Fixed::ZERO
};
if dx_box >= best || dy_box >= best {
continue;
}
let d = dist_sq_point_to_segment(pt, s.p1, s.p2).sqrt();
if d < best {
best = d;
found = true;
}
}
if found { best } else { cap }
}
fn dist_sq_point_to_segment(p: Point, a: Point, b: Point) -> Fixed {
let abx = b.x - a.x;
let aby = b.y - a.y;
let len_sq = abx * abx + aby * aby;
if len_sq == Fixed::ZERO {
let dx = p.x - a.x;
let dy = p.y - a.y;
return dx * dx + dy * dy;
}
let apx = p.x - a.x;
let apy = p.y - a.y;
let t_raw = (apx * abx + apy * aby) / len_sq;
let t = t_raw.max(Fixed::ZERO).min(Fixed::ONE);
let cx = a.x + abx * t;
let cy = a.y + aby * t;
let dx = p.x - cx;
let dy = p.y - cy;
dx * dx + dy * dy
}
const MITER_LIMIT: Fixed = Fixed::from_int(4);
pub(crate) fn offset_polygon(path: &Path, width: Fixed) -> Path {
let mut out = Path::new();
if width <= Fixed::ZERO {
return out;
}
let half = width / 2;
for sub in flatten_subpaths(path) {
if sub.segs.is_empty() {
continue;
}
let n = compute_normals(&sub.segs, half);
if sub.closed {
let left = build_ring(&sub.segs, &n, half, true);
let mut right = build_ring(&sub.segs, &n, half, false);
right.reverse();
append_closed_polyline(&mut out, &left);
append_closed_polyline(&mut out, &right);
} else {
let left = build_open_rail(&sub.segs, &n, half, true);
let right = build_open_rail(&sub.segs, &n, half, false);
append_open_ribbon(&mut out, &left, &right);
}
}
out
}
fn compute_normals(segs: &[LineSeg], half: Fixed) -> Vec<Point> {
let mut out = Vec::with_capacity(segs.len());
for s in segs {
let dx = s.p2.x - s.p1.x;
let dy = s.p2.y - s.p1.y;
let len_sq = dx * dx + dy * dy;
if len_sq == Fixed::ZERO {
out.push(Point::ZERO);
continue;
}
let len = len_sq.sqrt();
let nx = -dy / len * half;
let ny = dx / len * half;
out.push(Point { x: nx, y: ny });
}
out
}
fn build_ring(segs: &[LineSeg], n: &[Point], half: Fixed, left: bool) -> Vec<Point> {
let sign = if left { Fixed::ONE } else { -Fixed::ONE };
let count = segs.len();
let mut out = Vec::with_capacity(count + 1);
for i in 0..count {
let prev = if i == 0 { count - 1 } else { i - 1 };
let p_prev = segs[prev];
let p_curr = segs[i];
let n_prev = scaled(n[prev], sign);
let n_curr = scaled(n[i], sign);
let joint = compute_join(
p_prev.p2, p_prev.p1, n_prev, p_curr.p1, p_curr.p2, n_curr, half,
);
out.push(joint);
}
if let Some(&first) = out.first() {
out.push(first);
}
out
}
fn build_open_rail(segs: &[LineSeg], n: &[Point], half: Fixed, left: bool) -> Vec<Point> {
let sign = if left { Fixed::ONE } else { -Fixed::ONE };
let count = segs.len();
let mut out = Vec::with_capacity(count + 1);
let n0 = scaled(n[0], sign);
out.push(offset(segs[0].p1, n0));
for i in 1..count {
let p_prev = segs[i - 1];
let p_curr = segs[i];
let n_prev = scaled(n[i - 1], sign);
let n_curr = scaled(n[i], sign);
let joint = compute_join(
p_prev.p2, p_prev.p1, n_prev, p_curr.p1, p_curr.p2, n_curr, half,
);
out.push(joint);
}
let n_last = scaled(n[count - 1], sign);
out.push(offset(segs[count - 1].p2, n_last));
out
}
fn compute_join(
_a: Point,
b: Point,
n_prev: Point,
c: Point,
_d: Point,
n_curr: Point,
half: Fixed,
) -> Point {
let p_in = offset(b, n_prev);
let p_out = offset(c, n_curr);
if approx_eq(p_in, p_out) {
return p_in;
}
let dir_prev = Point {
x: b.x - _a.x,
y: b.y - _a.y,
};
let dir_curr = Point {
x: _d.x - c.x,
y: _d.y - c.y,
};
if let Some(miter) = line_intersect(p_in, dir_prev, p_out, dir_curr) {
let corner = Point {
x: (b.x + c.x) / 2,
y: (b.y + c.y) / 2,
};
let dx = miter.x - corner.x;
let dy = miter.y - corner.y;
let dist_sq = dx * dx + dy * dy;
let limit = half * MITER_LIMIT;
if dist_sq <= limit * limit {
return miter;
}
}
Point {
x: (p_in.x + p_out.x) / 2,
y: (p_in.y + p_out.y) / 2,
}
}
fn line_intersect(p1: Point, d1: Point, p2: Point, d2: Point) -> Option<Point> {
let denom = d1.x * d2.y - d1.y * d2.x;
if denom == Fixed::ZERO {
return None;
}
let dx = p2.x - p1.x;
let dy = p2.y - p1.y;
let t = (dx * d2.y - dy * d2.x) / denom;
Some(Point {
x: p1.x + d1.x * t,
y: p1.y + d1.y * t,
})
}
fn append_closed_polyline(out: &mut Path, pts: &[Point]) {
if pts.len() < 2 {
return;
}
out.move_to(pts[0]);
for p in &pts[1..] {
out.line_to(*p);
}
out.close();
}
fn append_open_ribbon(out: &mut Path, left: &[Point], right: &[Point]) {
if left.is_empty() || right.is_empty() {
return;
}
out.move_to(left[0]);
for p in &left[1..] {
out.line_to(*p);
}
for p in right.iter().rev() {
out.line_to(*p);
}
out.close();
}
fn scaled(p: Point, s: Fixed) -> Point {
Point {
x: p.x * s,
y: p.y * s,
}
}
fn offset(p: Point, n: Point) -> Point {
Point {
x: p.x + n.x,
y: p.y + n.y,
}
}
fn approx_eq(a: Point, b: Point) -> bool {
let dx = a.x - b.x;
let dy = a.y - b.y;
dx.abs() + dy.abs() < Fixed::ONE / 128
}
fn lerp(a: Point, b: Point, t: Fixed) -> Point {
Point {
x: a.x + (b.x - a.x) * t,
y: a.y + (b.y - a.y) * t,
}
}
fn quad_at(p0: Point, p1: Point, p2: Point, t: Fixed) -> Point {
lerp(lerp(p0, p1, t), lerp(p1, p2, t), t)
}
fn cubic_at(p0: Point, p1: Point, p2: Point, p3: Point, t: Fixed) -> Point {
let q0 = lerp(p0, p1, t);
let q1 = lerp(p1, p2, t);
let q2 = lerp(p2, p3, t);
lerp(lerp(q0, q1, t), lerp(q1, q2, t), t)
}
#[cfg(test)]
mod tests {
use alloc::vec;
use super::*;
fn pt(x: i32, y: i32) -> Point {
Point {
x: Fixed::from_int(x),
y: Fixed::from_int(y),
}
}
#[test]
fn flatten_empty_path() {
assert!(flatten(&Path::new()).is_empty());
}
#[test]
fn flatten_single_line() {
let mut p = Path::new();
p.move_to(pt(0, 0)).line_to(pt(10, 0));
let segs = flatten(&p);
assert_eq!(segs.len(), 1);
assert_eq!(segs[0].p1, pt(0, 0));
assert_eq!(segs[0].p2, pt(10, 0));
}
#[test]
fn flatten_closed_triangle_emits_closing_edge() {
let mut p = Path::new();
p.move_to(pt(0, 0))
.line_to(pt(10, 0))
.line_to(pt(5, 10))
.close();
let segs = flatten(&p);
assert_eq!(segs.len(), 3);
assert_eq!(segs[2].p2, pt(0, 0));
}
#[test]
fn flatten_close_is_noop_when_current_equals_start() {
let mut p = Path::new();
p.move_to(pt(0, 0))
.line_to(pt(10, 0))
.line_to(pt(0, 0))
.close();
let segs = flatten(&p);
assert_eq!(segs.len(), 2);
}
#[test]
fn flatten_quad_emits_n_segments_connected() {
let mut p = Path::new();
p.move_to(pt(0, 0)).quad_to(pt(10, 10), pt(20, 0));
let segs = flatten(&p);
assert_eq!(segs.len(), QUAD_STEPS as usize);
for i in 0..segs.len() - 1 {
assert_eq!(segs[i].p2, segs[i + 1].p1);
}
assert_eq!(segs.last().unwrap().p2, pt(20, 0));
}
#[test]
fn flatten_cubic_emits_n_segments_connected() {
let mut p = Path::new();
p.move_to(pt(0, 0))
.cubic_to(pt(0, 10), pt(10, 10), pt(10, 0));
let segs = flatten(&p);
assert_eq!(segs.len(), CUBIC_STEPS as usize);
for i in 0..segs.len() - 1 {
assert_eq!(segs[i].p2, segs[i + 1].p1);
}
assert_eq!(segs.last().unwrap().p2, pt(10, 0));
}
#[test]
fn flatten_multi_subpath() {
let mut p = Path::new();
p.move_to(pt(0, 0)).line_to(pt(10, 0));
p.move_to(pt(50, 50)).line_to(pt(60, 50)).close();
let segs = flatten(&p);
assert_eq!(segs.len(), 3);
assert_eq!(segs.last().unwrap().p2, pt(50, 50));
}
fn square_segs() -> Vec<LineSeg> {
vec![
LineSeg {
p1: pt(0, 0),
p2: pt(10, 0),
},
LineSeg {
p1: pt(10, 0),
p2: pt(10, 10),
},
LineSeg {
p1: pt(10, 10),
p2: pt(0, 10),
},
LineSeg {
p1: pt(0, 10),
p2: pt(0, 0),
},
]
}
#[test]
fn point_in_square_interior_detected() {
assert!(point_in_segments(pt(5, 5), &square_segs()));
}
#[test]
fn point_outside_square_rejected() {
assert!(!point_in_segments(pt(-1, 5), &square_segs()));
assert!(!point_in_segments(pt(11, 5), &square_segs()));
assert!(!point_in_segments(pt(5, -1), &square_segs()));
assert!(!point_in_segments(pt(5, 11), &square_segs()));
}
#[test]
fn point_on_horizontal_edge_half_open() {
assert!(point_in_segments(pt(5, 0), &square_segs()));
assert!(!point_in_segments(pt(5, 10), &square_segs()));
}
fn big_cap() -> Fixed {
Fixed::from_int(1000)
}
#[test]
fn dist_on_segment_is_zero() {
let segs = square_segs();
let d = min_dist_to_segments_capped(pt(5, 0), &segs, big_cap());
assert_eq!(d, Fixed::ZERO);
}
#[test]
fn dist_center_to_square_is_half_width() {
let segs = square_segs();
let d = min_dist_to_segments_capped(pt(5, 5), &segs, big_cap()).to_f32();
assert!((d - 5.0).abs() < 0.01, "d = {d}");
}
#[test]
fn dist_beyond_endpoint_uses_endpoint() {
let segs = vec![LineSeg {
p1: pt(0, 0),
p2: pt(10, 0),
}];
let d = min_dist_to_segments_capped(pt(15, 0), &segs, big_cap()).to_f32();
assert!((d - 5.0).abs() < 0.01);
}
#[test]
fn dist_to_degenerate_segment() {
let segs = vec![LineSeg {
p1: pt(3, 4),
p2: pt(3, 4),
}];
let d = min_dist_to_segments_capped(pt(0, 0), &segs, big_cap()).to_f32();
assert!((d - 5.0).abs() < 0.01);
}
#[test]
fn dist_capped_returns_cap_when_far() {
let segs = vec![LineSeg {
p1: pt(100, 100),
p2: pt(110, 100),
}];
let cap = Fixed::from_int(5);
let d = min_dist_to_segments_capped(pt(0, 0), &segs, cap);
assert_eq!(d, cap);
}
#[test]
fn flatten_subpaths_marks_closed_and_open() {
let mut p = Path::new();
p.move_to(pt(0, 0))
.line_to(pt(10, 0))
.line_to(pt(0, 10))
.close();
p.move_to(pt(50, 50)).line_to(pt(60, 50));
let subs = flatten_subpaths(&p);
assert_eq!(subs.len(), 2);
assert!(subs[0].closed);
assert!(!subs[1].closed);
}
#[test]
fn offset_polygon_rectangle_closed_produces_two_rings() {
let path = Path::rect(
Fixed::from_int(0),
Fixed::from_int(0),
Fixed::from_int(10),
Fixed::from_int(10),
);
let outline = offset_polygon(&path, Fixed::from_int(2));
let closes = outline
.cmds
.iter()
.filter(|c| matches!(c, PathCmd::Close))
.count();
assert_eq!(closes, 2);
}
#[test]
fn offset_polygon_open_line_makes_one_ribbon() {
let mut path = Path::new();
path.move_to(pt(0, 0)).line_to(pt(10, 0));
let outline = offset_polygon(&path, Fixed::from_int(2));
let closes = outline
.cmds
.iter()
.filter(|c| matches!(c, PathCmd::Close))
.count();
assert_eq!(closes, 1);
}
#[test]
fn offset_polygon_zero_width_is_empty() {
let mut path = Path::new();
path.move_to(pt(0, 0)).line_to(pt(10, 0));
let outline = offset_polygon(&path, Fixed::ZERO);
assert!(outline.cmds.is_empty());
}
#[test]
fn flatten_rounded_rect_produces_expected_count() {
let p = Path::rounded_rect(
Fixed::from_int(0),
Fixed::from_int(0),
Fixed::from_int(20),
Fixed::from_int(20),
Fixed::from_int(4),
);
let segs = flatten(&p);
let expected_min = 4 + 4 * QUAD_STEPS as usize;
assert!(
segs.len() >= expected_min,
"got {} segs, expected ≥{}",
segs.len(),
expected_min
);
}
}