use core::f64::consts::PI;
use i_overlay::mesh::outline::offset::OutlineOffset;
use i_overlay::mesh::stroke::offset::StrokeOffset;
use i_overlay::mesh::style::{LineCap, LineJoin, OutlineStyle, StrokeStyle};
use crate::Polygon;
use super::convert::{polygon_to_f, polygons_from_shapes};
#[derive(Debug, Clone, Copy)]
pub enum BufferJoin {
Bevel,
Miter { min_angle_rad: f64 },
Round { max_segment_angle_rad: f64 },
}
impl BufferJoin {
fn to_i_overlay(self) -> LineJoin<f64> {
match self {
BufferJoin::Bevel => LineJoin::Bevel,
BufferJoin::Miter { min_angle_rad } => LineJoin::Miter(min_angle_rad),
BufferJoin::Round {
max_segment_angle_rad,
} => LineJoin::Round(max_segment_angle_rad),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct BufferOpts {
pub distance: f64,
pub join: BufferJoin,
}
impl Default for BufferOpts {
fn default() -> Self {
Self {
distance: 0.0,
join: BufferJoin::Miter {
min_angle_rad: 5.0 * PI / 180.0,
},
}
}
}
pub fn buffer_polygons(polys: &[Polygon], opts: &BufferOpts) -> Vec<Polygon> {
if polys.is_empty() || opts.distance == 0.0 {
return polys.to_vec();
}
let style = OutlineStyle::new(opts.distance).line_join(opts.join.to_i_overlay());
let mut out = Vec::new();
for p in polys {
let shape: Vec<Vec<[f64; 2]>> = polygon_to_f(p);
let shapes = shape.outline(&style);
out.extend(polygons_from_shapes(&shapes));
}
out
}
pub fn buffer_lines(lines: &[Vec<(i32, i32)>], opts: &BufferOpts) -> Vec<Polygon> {
let width = 2.0 * opts.distance.abs();
if lines.is_empty() || width == 0.0 {
return Vec::new();
}
let style = StrokeStyle::new(width)
.line_join(opts.join.to_i_overlay())
.start_cap(round_cap_default())
.end_cap(round_cap_default());
let mut out = Vec::new();
for line in lines {
let path: Vec<[f64; 2]> = line.iter().map(|&(x, y)| [x as f64, y as f64]).collect();
let shapes = path.stroke(style.clone(), false);
out.extend(polygons_from_shapes(&shapes));
}
out
}
pub fn buffer_points(points: &[(i32, i32)], opts: &BufferOpts) -> Vec<Polygon> {
let radius = opts.distance.abs();
if points.is_empty() || radius == 0.0 {
return Vec::new();
}
let seg = match opts.join {
BufferJoin::Round {
max_segment_angle_rad,
} if max_segment_angle_rad > 0.0 => {
((2.0 * PI / max_segment_angle_rad).ceil() as usize).max(8)
}
_ => 32,
};
let mut out = Vec::with_capacity(points.len());
for &(cx, cy) in points {
let mut ring = Vec::with_capacity(seg + 1);
for i in 0..seg {
let t = (i as f64) / (seg as f64) * 2.0 * PI;
let x = cx as f64 + radius * t.cos();
let y = cy as f64 + radius * t.sin();
ring.push((x.round() as i32, y.round() as i32));
}
ring.push(ring[0]);
out.push(Polygon {
exterior: ring,
holes: vec![],
});
}
out
}
fn round_cap_default() -> LineCap<[f64; 2]> {
LineCap::Round(0.25)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn polygon_inflate_grows() {
let p = Polygon {
exterior: vec![(0, 0), (10, 0), (10, 10), (0, 10), (0, 0)],
holes: vec![],
};
let out = buffer_polygons(
&[p],
&BufferOpts {
distance: 2.0,
join: BufferJoin::Miter { min_angle_rad: 0.1 },
},
);
assert_eq!(out.len(), 1);
assert!(out[0].exterior.iter().any(|&(x, _)| x < 0));
}
#[test]
fn polygon_heavy_erode_disappears() {
let p = Polygon {
exterior: vec![(0, 0), (4, 0), (4, 4), (0, 4), (0, 0)],
holes: vec![],
};
let out = buffer_polygons(
&[p],
&BufferOpts {
distance: -10.0,
join: BufferJoin::Bevel,
},
);
assert!(out.is_empty());
}
#[test]
fn line_buffer_produces_polygon() {
let line = vec![(0, 0), (10, 0)];
let out = buffer_lines(
&[line],
&BufferOpts {
distance: 2.0,
join: BufferJoin::Bevel,
},
);
assert_eq!(out.len(), 1);
}
#[test]
fn point_buffer_produces_disk() {
let out = buffer_points(
&[(5, 5)],
&BufferOpts {
distance: 3.0,
join: BufferJoin::Bevel,
},
);
assert_eq!(out.len(), 1);
assert!(out[0].exterior.len() >= 8);
}
}