Skip to main content

brepkit_math/
polygon2d.rs

1//! 2D polygon operations: clipping, filleting, chamfering, and segment detection.
2
3use crate::vec::Point2;
4
5/// Cross product of vectors (b-a) and (c-a).
6#[must_use]
7pub fn cross_2d(a: Point2, b: Point2, c: Point2) -> f64 {
8    (b.x() - a.x()) * (c.y() - a.y()) - (b.y() - a.y()) * (c.x() - a.x())
9}
10
11fn line_intersect_2d(a1: Point2, a2: Point2, b1: Point2, b2: Point2) -> Option<Point2> {
12    let dx_a = a2.x() - a1.x();
13    let dy_a = a2.y() - a1.y();
14    let dx_b = b2.x() - b1.x();
15    let dy_b = b2.y() - b1.y();
16    let denom = dx_a * dy_b - dy_a * dx_b;
17    if denom.abs() < 1e-15 {
18        return None;
19    }
20    let t = ((b1.x() - a1.x()) * dy_b - (b1.y() - a1.y()) * dx_b) / denom;
21    Some(Point2::new(a1.x() + t * dx_a, a1.y() + t * dy_a))
22}
23
24fn point_to_line_dist_sq_2d(p: Point2, a: Point2, b: Point2) -> f64 {
25    let dx = b.x() - a.x();
26    let dy = b.y() - a.y();
27    let len_sq = dx * dx + dy * dy;
28    if len_sq < 1e-30 {
29        let ex = p.x() - a.x();
30        let ey = p.y() - a.y();
31        return ex * ex + ey * ey;
32    }
33    let cross = (p.x() - a.x()) * dy - (p.y() - a.y()) * dx;
34    (cross * cross) / len_sq
35}
36
37/// Sutherland-Hodgman polygon clipping algorithm.
38#[must_use]
39pub fn sutherland_hodgman_clip(subject: &[Point2], clip: &[Point2]) -> Vec<Point2> {
40    let mut output: Vec<Point2> = subject.to_vec();
41
42    for i in 0..clip.len() {
43        if output.is_empty() {
44            return output;
45        }
46        let edge_start = clip[i];
47        let edge_end = clip[(i + 1) % clip.len()];
48        let input = output;
49        output = Vec::new();
50
51        for j in 0..input.len() {
52            let current = input[j];
53            let previous = input[(j + input.len() - 1) % input.len()];
54
55            let curr_inside = cross_2d(edge_start, edge_end, current) >= 0.0;
56            let prev_inside = cross_2d(edge_start, edge_end, previous) >= 0.0;
57
58            if curr_inside {
59                if !prev_inside
60                    && let Some(p) = line_intersect_2d(previous, current, edge_start, edge_end)
61                {
62                    output.push(p);
63                }
64                output.push(current);
65            } else if prev_inside
66                && let Some(p) = line_intersect_2d(previous, current, edge_start, edge_end)
67            {
68                output.push(p);
69            }
70        }
71    }
72
73    output
74}
75
76/// Find common (collinear, overlapping) edges between two polygons.
77#[must_use]
78pub fn find_common_segments(a: &[Point2], b: &[Point2], tolerance: f64) -> Vec<(Point2, Point2)> {
79    let mut results = Vec::new();
80    let tol_sq = tolerance * tolerance;
81
82    for i in 0..a.len() {
83        let a1 = a[i];
84        let a2 = a[(i + 1) % a.len()];
85        for j in 0..b.len() {
86            let b1 = b[j];
87            let b2 = b[(j + 1) % b.len()];
88
89            // Check if edge A and edge B are collinear and overlapping.
90            // Both endpoints of B must be close to line through A, or vice versa.
91            let dist_b1 = point_to_line_dist_sq_2d(b1, a1, a2);
92            let dist_b2 = point_to_line_dist_sq_2d(b2, a1, a2);
93
94            if dist_b1 < tol_sq && dist_b2 < tol_sq {
95                // Edges are collinear. Check for overlap by projecting onto A's direction.
96                let dx = a2.x() - a1.x();
97                let dy = a2.y() - a1.y();
98                let len_sq = dx * dx + dy * dy;
99                if len_sq < tol_sq {
100                    continue;
101                }
102                let t1 = ((b1.x() - a1.x()) * dx + (b1.y() - a1.y()) * dy) / len_sq;
103                let t2 = ((b2.x() - a1.x()) * dx + (b2.y() - a1.y()) * dy) / len_sq;
104                let t_min = t1.min(t2).max(0.0);
105                let t_max = t1.max(t2).min(1.0);
106                if t_max - t_min > tolerance / len_sq.sqrt() {
107                    results.push((
108                        Point2::new(a1.x() + t_min * dx, a1.y() + t_min * dy),
109                        Point2::new(a1.x() + t_max * dx, a1.y() + t_max * dy),
110                    ));
111                }
112            }
113        }
114    }
115    results
116}
117
118/// Round all corners of a 2D polygon with arc approximations.
119#[must_use]
120pub fn fillet_polygon_2d(polygon: &[Point2], radius: f64) -> Vec<Point2> {
121    let n = polygon.len();
122    if n < 3 {
123        return polygon.to_vec();
124    }
125
126    let arc_segments = 8; // Number of segments per fillet arc
127    let mut result = Vec::with_capacity(n * (arc_segments + 1));
128
129    for i in 0..n {
130        let prev = polygon[(i + n - 1) % n];
131        let curr = polygon[i];
132        let next = polygon[(i + 1) % n];
133
134        let d_prev = ((prev.x() - curr.x()).powi(2) + (prev.y() - curr.y()).powi(2)).sqrt();
135        let d_next = ((next.x() - curr.x()).powi(2) + (next.y() - curr.y()).powi(2)).sqrt();
136
137        let max_r = (d_prev.min(d_next) / 2.0).min(radius);
138
139        if max_r < 1e-10 {
140            result.push(curr);
141            continue;
142        }
143
144        // Direction vectors from corner to adjacent vertices
145        let dir_prev_x = (prev.x() - curr.x()) / d_prev;
146        let dir_prev_y = (prev.y() - curr.y()) / d_prev;
147        let dir_next_x = (next.x() - curr.x()) / d_next;
148        let dir_next_y = (next.y() - curr.y()) / d_next;
149
150        // Tangent points on edges
151        let t1 = Point2::new(curr.x() + dir_prev_x * max_r, curr.y() + dir_prev_y * max_r);
152        let t2 = Point2::new(curr.x() + dir_next_x * max_r, curr.y() + dir_next_y * max_r);
153
154        // Generate arc points from t1 to t2
155        for k in 0..=arc_segments {
156            let t = k as f64 / arc_segments as f64;
157            let x = t2.x().mul_add(t, t1.x() * (1.0 - t));
158            let y = t2.y().mul_add(t, t1.y() * (1.0 - t));
159
160            // Push point toward the arc center for a circular approximation
161            let mid_x = f64::midpoint(t1.x(), t2.x());
162            let mid_y = f64::midpoint(t1.y(), t2.y());
163            let to_corner_x = curr.x() - mid_x;
164            let to_corner_y = curr.y() - mid_y;
165            let corner_dist = (to_corner_x * to_corner_x + to_corner_y * to_corner_y).sqrt();
166
167            if corner_dist > 1e-10 {
168                // Compute the bulge: how much to push along the corner bisector
169                let chord_half =
170                    ((t2.x() - t1.x()).powi(2) + (t2.y() - t1.y()).powi(2)).sqrt() / 2.0;
171                let sagitta = if max_r > chord_half {
172                    max_r - (max_r * max_r - chord_half * chord_half).sqrt()
173                } else {
174                    0.0
175                };
176
177                // Blend factor: maximum at midpoint (t=0.5), zero at endpoints
178                let blend = 4.0 * t * (1.0 - t); // parabolic blend
179                let push = sagitta * blend;
180
181                let nx = to_corner_x / corner_dist;
182                let ny = to_corner_y / corner_dist;
183                result.push(Point2::new(x + nx * push, y + ny * push));
184            } else {
185                result.push(Point2::new(x, y));
186            }
187        }
188    }
189
190    result
191}
192
193/// Cut all corners of a 2D polygon with flat bevels.
194#[must_use]
195pub fn chamfer_polygon_2d(polygon: &[Point2], distance: f64) -> Vec<Point2> {
196    let n = polygon.len();
197    if n < 3 {
198        return polygon.to_vec();
199    }
200
201    let mut result = Vec::with_capacity(n * 2);
202
203    for i in 0..n {
204        let prev = polygon[(i + n - 1) % n];
205        let curr = polygon[i];
206        let next = polygon[(i + 1) % n];
207
208        let d_prev = ((prev.x() - curr.x()).powi(2) + (prev.y() - curr.y()).powi(2)).sqrt();
209        let d_next = ((next.x() - curr.x()).powi(2) + (next.y() - curr.y()).powi(2)).sqrt();
210
211        let d = (d_prev.min(d_next) / 2.0).min(distance);
212
213        if d < 1e-10 {
214            result.push(curr);
215            continue;
216        }
217
218        // Two chamfer points: one on previous edge, one on next edge
219        result.push(Point2::new(
220            curr.x() + (prev.x() - curr.x()) / d_prev * d,
221            curr.y() + (prev.y() - curr.y()) / d_prev * d,
222        ));
223        result.push(Point2::new(
224            curr.x() + (next.x() - curr.x()) / d_next * d,
225            curr.y() + (next.y() - curr.y()) / d_next * d,
226        ));
227    }
228
229    result
230}