oxigdal_algorithms/vector/
min_bounds.rs1use crate::error::Result;
20use crate::vector::convex_hull;
21use oxigdal_core::vector::Coordinate;
22
23#[derive(Debug, Clone, PartialEq)]
29pub struct RotatedRect {
30 pub center: Coordinate,
32 pub width: f64,
34 pub height: f64,
36 pub angle_rad: f64,
38}
39
40impl RotatedRect {
41 pub fn corners(&self) -> [Coordinate; 4] {
43 let (cos_a, sin_a) = (self.angle_rad.cos(), self.angle_rad.sin());
44 let hw = self.width * 0.5;
45 let hh = self.height * 0.5;
46 let local: [(f64, f64); 4] = [(hw, hh), (-hw, hh), (-hw, -hh), (hw, -hh)];
48 local.map(|(lx, ly)| {
49 Coordinate::new_2d(
50 self.center.x + lx * cos_a - ly * sin_a,
51 self.center.y + lx * sin_a + ly * cos_a,
52 )
53 })
54 }
55
56 #[inline]
58 pub fn area(&self) -> f64 {
59 self.width * self.height
60 }
61}
62
63#[derive(Debug, Clone, PartialEq)]
65pub struct Circle {
66 pub center: Coordinate,
68 pub radius: f64,
70}
71
72impl Circle {
73 #[inline]
78 pub fn contains(&self, p: Coordinate) -> bool {
79 let dx = p.x - self.center.x;
80 let dy = p.y - self.center.y;
81 dx * dx + dy * dy <= (self.radius + 1e-10) * (self.radius + 1e-10)
82 }
83
84 #[inline]
86 pub fn zero() -> Self {
87 Self {
88 center: Coordinate::new_2d(0.0, 0.0),
89 radius: 0.0,
90 }
91 }
92}
93
94pub fn aabb(points: &[Coordinate]) -> Option<(f64, f64, f64, f64)> {
102 if points.is_empty() {
103 return None;
104 }
105 let mut min_x = points[0].x;
106 let mut min_y = points[0].y;
107 let mut max_x = min_x;
108 let mut max_y = min_y;
109 for c in &points[1..] {
110 if c.x < min_x {
111 min_x = c.x;
112 }
113 if c.y < min_y {
114 min_y = c.y;
115 }
116 if c.x > max_x {
117 max_x = c.x;
118 }
119 if c.y > max_y {
120 max_y = c.y;
121 }
122 }
123 Some((min_x, min_y, max_x, max_y))
124}
125
126pub fn min_area_rotated_rect(points: &[Coordinate]) -> Option<RotatedRect> {
139 if points.is_empty() {
140 return None;
141 }
142 if points.len() == 1 {
143 return Some(RotatedRect {
144 center: points[0],
145 width: 0.0,
146 height: 0.0,
147 angle_rad: 0.0,
148 });
149 }
150 if points.len() == 2 {
151 let (p, q) = (points[0], points[1]);
152 let dx = q.x - p.x;
153 let dy = q.y - p.y;
154 let length = (dx * dx + dy * dy).sqrt();
155 let angle = dy.atan2(dx);
156 return Some(RotatedRect {
157 center: Coordinate::new_2d((p.x + q.x) * 0.5, (p.y + q.y) * 0.5),
158 width: length,
159 height: 0.0,
160 angle_rad: angle,
161 });
162 }
163
164 let hull: Vec<Coordinate> = match convex_hull(points) {
167 Ok(h) => h,
168 Err(_) => return None,
169 };
170 let n = hull.len();
171 if n < 2 {
172 return None;
173 }
174 let mut best_area = f64::INFINITY;
179 let mut best_rect: Option<RotatedRect> = None;
180
181 for i in 0..n {
182 let j = (i + 1) % n;
183 let edge_x = hull[j].x - hull[i].x;
184 let edge_y = hull[j].y - hull[i].y;
185 let edge_len = (edge_x * edge_x + edge_y * edge_y).sqrt();
186 if edge_len < 1e-12 {
187 continue;
188 }
189 let ux = edge_x / edge_len;
191 let uy = edge_y / edge_len;
192 let vx = -uy;
194 let vy = ux;
195
196 let mut min_u = f64::INFINITY;
198 let mut max_u = f64::NEG_INFINITY;
199 let mut min_v = f64::INFINITY;
200 let mut max_v = f64::NEG_INFINITY;
201 for c in &hull {
202 let u = c.x * ux + c.y * uy;
203 let v = c.x * vx + c.y * vy;
204 if u < min_u {
205 min_u = u;
206 }
207 if u > max_u {
208 max_u = u;
209 }
210 if v < min_v {
211 min_v = v;
212 }
213 if v > max_v {
214 max_v = v;
215 }
216 }
217
218 let w = max_u - min_u;
219 let h = max_v - min_v;
220 let area = w * h;
221
222 if area < best_area {
223 best_area = area;
224
225 let cu = (min_u + max_u) * 0.5;
228 let cv = (min_v + max_v) * 0.5;
229 let cx = cu * ux + cv * vx;
230 let cy = cu * uy + cv * vy;
231
232 best_rect = Some(RotatedRect {
233 center: Coordinate::new_2d(cx, cy),
234 width: w,
235 height: h,
236 angle_rad: uy.atan2(ux),
238 });
239 }
240 }
241
242 best_rect
243}
244
245#[inline]
251fn circle_from_1(a: Coordinate) -> Circle {
252 Circle {
253 center: a,
254 radius: 0.0,
255 }
256}
257
258#[inline]
260fn circle_from_2(a: Coordinate, b: Coordinate) -> Circle {
261 let cx = (a.x + b.x) * 0.5;
262 let cy = (a.y + b.y) * 0.5;
263 let dx = b.x - a.x;
264 let dy = b.y - a.y;
265 Circle {
266 center: Coordinate::new_2d(cx, cy),
267 radius: (dx * dx + dy * dy).sqrt() * 0.5,
268 }
269}
270
271fn circumcircle(a: Coordinate, b: Coordinate, c: Coordinate) -> Option<Circle> {
273 let ax = b.x - a.x;
274 let ay = b.y - a.y;
275 let bx = c.x - a.x;
276 let by = c.y - a.y;
277 let d = 2.0 * (ax * by - ay * bx);
278 if d.abs() < 1e-12 {
279 return None;
280 }
281 let a_sq = ax * ax + ay * ay;
282 let b_sq = bx * bx + by * by;
283 let ux = (by * a_sq - ay * b_sq) / d;
284 let uy = (ax * b_sq - bx * a_sq) / d;
285 let r = (ux * ux + uy * uy).sqrt();
286 Some(Circle {
287 center: Coordinate::new_2d(a.x + ux, a.y + uy),
288 radius: r,
289 })
290}
291
292#[inline]
294fn trivial_circle(boundary: &[Coordinate]) -> Circle {
295 match boundary.len() {
296 0 => Circle::zero(),
297 1 => circle_from_1(boundary[0]),
298 2 => circle_from_2(boundary[0], boundary[1]),
299 3 => circumcircle(boundary[0], boundary[1], boundary[2]).unwrap_or_else(|| {
300 let mut best = circle_from_2(boundary[0], boundary[1]);
303 let c02 = circle_from_2(boundary[0], boundary[2]);
304 let c12 = circle_from_2(boundary[1], boundary[2]);
305 if c02.radius > best.radius {
306 best = c02;
307 }
308 if c12.radius > best.radius {
309 best = c12;
310 }
311 best
312 }),
313 _ => unreachable!("boundary set may not exceed 3 points"),
314 }
315}
316
317fn welzl(pts: &[Coordinate], boundary: &mut Vec<Coordinate>, n: usize) -> Circle {
323 if n == 0 || boundary.len() == 3 {
324 return trivial_circle(boundary);
325 }
326 let p = pts[n - 1];
327 let d = welzl(pts, boundary, n - 1);
328 if d.contains(p) {
329 return d;
330 }
331 boundary.push(p);
332 let result = welzl(pts, boundary, n - 1);
333 boundary.pop();
334 result
335}
336
337fn lcg_shuffle(v: &mut [Coordinate], seed: u64) {
341 let n = v.len();
342 if n <= 1 {
343 return;
344 }
345 let mut state = seed;
346 for i in (1..n).rev() {
347 state = state
349 .wrapping_mul(6_364_136_223_846_793_005)
350 .wrapping_add(1_442_695_040_888_963_407);
351 let j = (state >> 33) as usize % (i + 1);
353 v.swap(i, j);
354 }
355}
356
357pub fn smallest_enclosing_circle(points: &[Coordinate]) -> Circle {
370 match points.len() {
371 0 => Circle::zero(),
372 1 => circle_from_1(points[0]),
373 2 => circle_from_2(points[0], points[1]),
374 _ => {
375 let mut pts: Vec<Coordinate> = points.to_vec();
376 let seed: u64 = pts.iter().fold(0u64, |acc, c| {
379 acc.wrapping_add(c.x.to_bits() ^ c.y.to_bits())
380 });
381 lcg_shuffle(&mut pts, seed.wrapping_add(42));
382 let mut boundary: Vec<Coordinate> = Vec::with_capacity(3);
383 welzl(&pts, &mut boundary, pts.len())
384 }
385 }
386}
387
388#[cfg(test)]
393mod tests {
394 use super::*;
395
396 #[test]
397 fn test_aabb_basic() {
398 let pts = vec![
399 Coordinate::new_2d(1.0, 2.0),
400 Coordinate::new_2d(3.0, 0.0),
401 Coordinate::new_2d(0.0, 4.0),
402 ];
403 let (min_x, min_y, max_x, max_y) = aabb(&pts).expect("non-empty");
404 assert!((min_x - 0.0).abs() < 1e-12);
405 assert!((min_y - 0.0).abs() < 1e-12);
406 assert!((max_x - 3.0).abs() < 1e-12);
407 assert!((max_y - 4.0).abs() < 1e-12);
408 }
409
410 #[test]
411 fn test_rotated_rect_corners_roundtrip() {
412 let rect = RotatedRect {
413 center: Coordinate::new_2d(1.0, 1.0),
414 width: 4.0,
415 height: 2.0,
416 angle_rad: 0.0,
417 };
418 let corners = rect.corners();
419 assert!((corners[0].x - 3.0).abs() < 1e-10); assert!((corners[0].y - 2.0).abs() < 1e-10); }
423
424 #[test]
425 fn test_circle_contains_boundary() {
426 let c = Circle {
427 center: Coordinate::new_2d(0.0, 0.0),
428 radius: 1.0,
429 };
430 assert!(c.contains(Coordinate::new_2d(1.0, 0.0)));
432 assert!(!c.contains(Coordinate::new_2d(1.1, 0.0)));
433 }
434}