ezu_features/ops/
buffer.rs1use core::f64::consts::PI;
16
17use i_overlay::mesh::outline::offset::OutlineOffset;
18use i_overlay::mesh::stroke::offset::StrokeOffset;
19use i_overlay::mesh::style::{LineCap, LineJoin, OutlineStyle, StrokeStyle};
20
21use crate::Polygon;
22
23use super::convert::{polygon_to_f, polygons_from_shapes};
24
25#[derive(Debug, Clone, Copy)]
27pub enum BufferJoin {
28 Bevel,
30 Miter { min_angle_rad: f64 },
33 Round { max_segment_angle_rad: f64 },
36}
37
38impl BufferJoin {
39 fn to_i_overlay(self) -> LineJoin<f64> {
40 match self {
41 BufferJoin::Bevel => LineJoin::Bevel,
42 BufferJoin::Miter { min_angle_rad } => LineJoin::Miter(min_angle_rad),
43 BufferJoin::Round {
44 max_segment_angle_rad,
45 } => LineJoin::Round(max_segment_angle_rad),
46 }
47 }
48}
49
50#[derive(Debug, Clone, Copy)]
52pub struct BufferOpts {
53 pub distance: f64,
56 pub join: BufferJoin,
58}
59
60impl Default for BufferOpts {
61 fn default() -> Self {
62 Self {
63 distance: 0.0,
64 join: BufferJoin::Miter {
65 min_angle_rad: 5.0 * PI / 180.0,
66 },
67 }
68 }
69}
70
71pub fn buffer_polygons(polys: &[Polygon], opts: &BufferOpts) -> Vec<Polygon> {
75 if polys.is_empty() || opts.distance == 0.0 {
76 return polys.to_vec();
77 }
78 let style = OutlineStyle::new(opts.distance).line_join(opts.join.to_i_overlay());
79 let mut out = Vec::new();
80 for p in polys {
81 let shape: Vec<Vec<[f64; 2]>> = polygon_to_f(p);
82 let shapes = shape.outline(&style);
83 out.extend(polygons_from_shapes(&shapes));
84 }
85 out
86}
87
88pub fn buffer_lines(lines: &[Vec<(i32, i32)>], opts: &BufferOpts) -> Vec<Polygon> {
92 let width = 2.0 * opts.distance.abs();
93 if lines.is_empty() || width == 0.0 {
94 return Vec::new();
95 }
96 let style = StrokeStyle::new(width)
97 .line_join(opts.join.to_i_overlay())
98 .start_cap(round_cap_default())
99 .end_cap(round_cap_default());
100 let mut out = Vec::new();
101 for line in lines {
102 let path: Vec<[f64; 2]> = line.iter().map(|&(x, y)| [x as f64, y as f64]).collect();
103 let shapes = path.stroke(style.clone(), false);
104 out.extend(polygons_from_shapes(&shapes));
105 }
106 out
107}
108
109pub fn buffer_points(points: &[(i32, i32)], opts: &BufferOpts) -> Vec<Polygon> {
114 let radius = opts.distance.abs();
115 if points.is_empty() || radius == 0.0 {
116 return Vec::new();
117 }
118 let seg = match opts.join {
119 BufferJoin::Round {
120 max_segment_angle_rad,
121 } if max_segment_angle_rad > 0.0 => {
122 ((2.0 * PI / max_segment_angle_rad).ceil() as usize).max(8)
123 }
124 _ => 32,
125 };
126 let mut out = Vec::with_capacity(points.len());
127 for &(cx, cy) in points {
128 let mut ring = Vec::with_capacity(seg + 1);
129 for i in 0..seg {
130 let t = (i as f64) / (seg as f64) * 2.0 * PI;
131 let x = cx as f64 + radius * t.cos();
132 let y = cy as f64 + radius * t.sin();
133 ring.push((x.round() as i32, y.round() as i32));
134 }
135 ring.push(ring[0]);
136 out.push(Polygon {
137 exterior: ring,
138 holes: vec![],
139 });
140 }
141 out
142}
143
144fn round_cap_default() -> LineCap<[f64; 2]> {
145 LineCap::Round(0.25)
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn polygon_inflate_grows() {
156 let p = Polygon {
157 exterior: vec![(0, 0), (10, 0), (10, 10), (0, 10), (0, 0)],
158 holes: vec![],
159 };
160 let out = buffer_polygons(
161 &[p],
162 &BufferOpts {
163 distance: 2.0,
164 join: BufferJoin::Miter { min_angle_rad: 0.1 },
165 },
166 );
167 assert_eq!(out.len(), 1);
168 assert!(out[0].exterior.iter().any(|&(x, _)| x < 0));
170 }
171
172 #[test]
173 fn polygon_heavy_erode_disappears() {
174 let p = Polygon {
175 exterior: vec![(0, 0), (4, 0), (4, 4), (0, 4), (0, 0)],
176 holes: vec![],
177 };
178 let out = buffer_polygons(
179 &[p],
180 &BufferOpts {
181 distance: -10.0,
182 join: BufferJoin::Bevel,
183 },
184 );
185 assert!(out.is_empty());
186 }
187
188 #[test]
189 fn line_buffer_produces_polygon() {
190 let line = vec![(0, 0), (10, 0)];
191 let out = buffer_lines(
192 &[line],
193 &BufferOpts {
194 distance: 2.0,
195 join: BufferJoin::Bevel,
196 },
197 );
198 assert_eq!(out.len(), 1);
199 }
200
201 #[test]
202 fn point_buffer_produces_disk() {
203 let out = buffer_points(
204 &[(5, 5)],
205 &BufferOpts {
206 distance: 3.0,
207 join: BufferJoin::Bevel,
208 },
209 );
210 assert_eq!(out.len(), 1);
211 assert!(out[0].exterior.len() >= 8);
212 }
213}