Skip to main content

ezu_features/ops/
buffer.rs

1//! Buffer / offset (Minkowski sum with a disk) of polygons, polylines,
2//! and points via [`i_overlay`].
3//!
4//! - Polygons: positive distance inflates, negative erodes. A heavy
5//!   negative buffer may erase or split a polygon — output is
6//!   therefore a `Vec<Polygon>`.
7//! - Polylines: stroked into a buffered polygon strip of width
8//!   `2 * distance`. Open paths.
9//! - Points: emitted as round polygons of radius `distance` (approx
10//!   via i_overlay's round join).
11//!
12//! Negative distance for polylines and points is ignored (an open
13//! path or a point has no interior to erode).
14
15use 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/// Join (corner) style applied at vertices of the offset path.
26#[derive(Debug, Clone, Copy)]
27pub enum BufferJoin {
28    /// Bevel — cut corners off flat.
29    Bevel,
30    /// Miter — sharp corners. `min_angle_rad` is the minimum interior
31    /// angle below which the join falls back to bevel.
32    Miter { min_angle_rad: f64 },
33    /// Round — corner approximated by an arc. `max_segment_angle_rad`
34    /// controls fineness (smaller → more segments).
35    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/// Configuration for [`buffer_polygons`].
51#[derive(Debug, Clone, Copy)]
52pub struct BufferOpts {
53    /// Signed offset distance (in the same units as the input
54    /// coordinates). Positive → inflate, negative → erode.
55    pub distance: f64,
56    /// Join style at corners.
57    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
71/// Inflate (positive) or erode (negative) every polygon. Heavy
72/// erosion can split a polygon; the result is therefore a single flat
73/// `Vec<Polygon>`.
74pub 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
88/// Stroke each polyline into a buffered polygon strip of width
89/// `2 * distance.abs()`. Open paths (start ≠ end). Negative distance
90/// has no meaning here and is treated as its absolute value.
91pub 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
109/// Emit a regular n-gon approximating a disk of radius
110/// `distance.abs()` per input point. Segment count is derived from
111/// `BufferJoin::Round`'s `max_segment_angle_rad`; other joins default
112/// to 32 segments.
113pub 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    // 0.25 rad ≈ ~14°: produces ~24 segments per full circle. Good
146    // enough for tile-scale rasterisation.
147    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        // grew outward
169        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}