Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs
9353 views
1
//! Contains [`Bounded2d`] implementations for [geometric primitives](crate::primitives).
2
3
use crate::{
4
bounding::BoundingVolume,
5
ops,
6
primitives::{
7
Annulus, Arc2d, Capsule2d, Circle, CircularSector, CircularSegment, Ellipse, Line2d,
8
Plane2d, Primitive2d, Rectangle, RegularPolygon, Rhombus, Ring, Segment2d, Triangle2d,
9
},
10
Dir2, Isometry2d, Mat2, Rot2, Vec2,
11
};
12
use core::f32::consts::{FRAC_PI_2, PI, TAU};
13
14
#[cfg(feature = "alloc")]
15
use crate::primitives::{ConvexPolygon, Polygon, Polyline2d};
16
17
use arrayvec::ArrayVec;
18
19
use super::{Aabb2d, Bounded2d, BoundingCircle};
20
21
impl Bounded2d for Circle {
22
fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
23
let isometry = isometry.into();
24
Aabb2d::new(isometry.translation, Vec2::splat(self.radius))
25
}
26
27
fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
28
let isometry = isometry.into();
29
BoundingCircle::new(isometry.translation, self.radius)
30
}
31
}
32
33
// Compute the axis-aligned bounding points of a rotated arc, used for computing the AABB of arcs and derived shapes.
34
// The return type has room for 7 points so that the CircularSector code can add an additional point.
35
#[inline]
36
fn arc_bounding_points(arc: Arc2d, rotation: impl Into<Rot2>) -> ArrayVec<Vec2, 7> {
37
// Otherwise, the extreme points will always be either the endpoints or the axis-aligned extrema of the arc's circle.
38
// We need to compute which axis-aligned extrema are actually contained within the rotated arc.
39
let mut bounds = ArrayVec::<Vec2, 7>::new();
40
let rotation = rotation.into();
41
bounds.push(rotation * arc.left_endpoint());
42
bounds.push(rotation * arc.right_endpoint());
43
44
// The half-angles are measured from a starting point of π/2, being the angle of Vec2::Y.
45
// Compute the normalized angles of the endpoints with the rotation taken into account, and then
46
// check if we are looking for an angle that is between or outside them.
47
let left_angle = ops::rem_euclid(FRAC_PI_2 + arc.half_angle + rotation.as_radians(), TAU);
48
let right_angle = ops::rem_euclid(FRAC_PI_2 - arc.half_angle + rotation.as_radians(), TAU);
49
let inverted = left_angle < right_angle;
50
for extremum in [Vec2::X, Vec2::Y, Vec2::NEG_X, Vec2::NEG_Y] {
51
let angle = ops::rem_euclid(extremum.to_angle(), TAU);
52
// If inverted = true, then right_angle > left_angle, so we are looking for an angle that is not between them.
53
// There's a chance that this condition fails due to rounding error, if the endpoint angle is juuuust shy of the axis.
54
// But in that case, the endpoint itself is within rounding error of the axis and will define the bounds just fine.
55
let angle_within_parameters = if inverted {
56
angle >= right_angle || angle <= left_angle
57
} else {
58
angle >= right_angle && angle <= left_angle
59
};
60
if angle_within_parameters {
61
bounds.push(extremum * arc.radius);
62
}
63
}
64
bounds
65
}
66
67
impl Bounded2d for Arc2d {
68
fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
69
// If our arc covers more than a circle, just return the bounding box of the circle.
70
if self.half_angle >= PI {
71
return Circle::new(self.radius).aabb_2d(isometry);
72
}
73
74
let isometry = isometry.into();
75
76
Aabb2d::from_point_cloud(
77
Isometry2d::from_translation(isometry.translation),
78
&arc_bounding_points(*self, isometry.rotation),
79
)
80
}
81
82
fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
83
let isometry = isometry.into();
84
85
// There are two possibilities for the bounding circle.
86
if self.is_major() {
87
// If the arc is major, then the widest distance between two points is a diameter of the arc's circle;
88
// therefore, that circle is the bounding radius.
89
BoundingCircle::new(isometry.translation, self.radius)
90
} else {
91
// Otherwise, the widest distance between two points is the chord,
92
// so a circle of that diameter around the midpoint will contain the entire arc.
93
let center = isometry.rotation * self.chord_midpoint();
94
BoundingCircle::new(center + isometry.translation, self.half_chord_length())
95
}
96
}
97
}
98
99
impl Bounded2d for CircularSector {
100
fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
101
let isometry = isometry.into();
102
103
// If our sector covers more than a circle, just return the bounding box of the circle.
104
if self.half_angle() >= PI {
105
return Circle::new(self.radius()).aabb_2d(isometry);
106
}
107
108
// Otherwise, we use the same logic as for Arc2d, above, just with the circle's center as an additional possibility.
109
let mut bounds = arc_bounding_points(self.arc, isometry.rotation);
110
bounds.push(Vec2::ZERO);
111
112
Aabb2d::from_point_cloud(Isometry2d::from_translation(isometry.translation), &bounds)
113
}
114
115
fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
116
if self.arc.is_major() {
117
let isometry = isometry.into();
118
119
// If the arc is major, that is, greater than a semicircle,
120
// then bounding circle is just the circle defining the sector.
121
BoundingCircle::new(isometry.translation, self.arc.radius)
122
} else {
123
// However, when the arc is minor,
124
// we need our bounding circle to include both endpoints of the arc as well as the circle center.
125
// This means we need the circumcircle of those three points.
126
// The circumcircle will always have a greater curvature than the circle itself, so it will contain
127
// the entire circular sector.
128
Triangle2d::new(
129
Vec2::ZERO,
130
self.arc.left_endpoint(),
131
self.arc.right_endpoint(),
132
)
133
.bounding_circle(isometry)
134
}
135
}
136
}
137
138
impl Bounded2d for CircularSegment {
139
fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
140
self.arc.aabb_2d(isometry)
141
}
142
143
fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
144
self.arc.bounding_circle(isometry)
145
}
146
}
147
148
impl Bounded2d for Ellipse {
149
fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
150
let isometry = isometry.into();
151
152
// V = (hh * cos(beta), hh * sin(beta))
153
// #####*#####
154
// ### | ###
155
// # hh | #
156
// # *---------* U = (hw * cos(alpha), hw * sin(alpha))
157
// # hw #
158
// ### ###
159
// ###########
160
161
let (hw, hh) = (self.half_size.x, self.half_size.y);
162
163
// Sine and cosine of rotation angle alpha.
164
let (alpha_sin, alpha_cos) = isometry.rotation.sin_cos();
165
166
// Sine and cosine of alpha + pi/2. We can avoid the trigonometric functions:
167
// sin(beta) = sin(alpha + pi/2) = cos(alpha)
168
// cos(beta) = cos(alpha + pi/2) = -sin(alpha)
169
let (beta_sin, beta_cos) = (alpha_cos, -alpha_sin);
170
171
// Compute points U and V, the extremes of the ellipse
172
let (ux, uy) = (hw * alpha_cos, hw * alpha_sin);
173
let (vx, vy) = (hh * beta_cos, hh * beta_sin);
174
175
let half_size = Vec2::new(ops::hypot(ux, vx), ops::hypot(uy, vy));
176
177
Aabb2d::new(isometry.translation, half_size)
178
}
179
180
fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
181
let isometry = isometry.into();
182
BoundingCircle::new(isometry.translation, self.semi_major())
183
}
184
}
185
186
impl Bounded2d for Annulus {
187
fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
188
let isometry = isometry.into();
189
Aabb2d::new(isometry.translation, Vec2::splat(self.outer_circle.radius))
190
}
191
192
fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
193
let isometry = isometry.into();
194
BoundingCircle::new(isometry.translation, self.outer_circle.radius)
195
}
196
}
197
198
impl Bounded2d for Rhombus {
199
fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
200
let isometry = isometry.into();
201
202
let [rotated_x_half_diagonal, rotated_y_half_diagonal] = [
203
isometry.rotation * Vec2::new(self.half_diagonals.x, 0.0),
204
isometry.rotation * Vec2::new(0.0, self.half_diagonals.y),
205
];
206
let aabb_half_extent = rotated_x_half_diagonal
207
.abs()
208
.max(rotated_y_half_diagonal.abs());
209
210
Aabb2d {
211
min: -aabb_half_extent + isometry.translation,
212
max: aabb_half_extent + isometry.translation,
213
}
214
}
215
216
fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
217
let isometry = isometry.into();
218
BoundingCircle::new(isometry.translation, self.circumradius())
219
}
220
}
221
222
impl Bounded2d for Plane2d {
223
fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
224
let isometry = isometry.into();
225
226
let normal = isometry.rotation * *self.normal;
227
let facing_x = normal == Vec2::X || normal == Vec2::NEG_X;
228
let facing_y = normal == Vec2::Y || normal == Vec2::NEG_Y;
229
230
// Dividing `f32::MAX` by 2.0 is helpful so that we can do operations
231
// like growing or shrinking the AABB without breaking things.
232
let half_width = if facing_x { 0.0 } else { f32::MAX / 2.0 };
233
let half_height = if facing_y { 0.0 } else { f32::MAX / 2.0 };
234
let half_size = Vec2::new(half_width, half_height);
235
236
Aabb2d::new(isometry.translation, half_size)
237
}
238
239
fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
240
let isometry = isometry.into();
241
BoundingCircle::new(isometry.translation, f32::MAX / 2.0)
242
}
243
}
244
245
impl Bounded2d for Line2d {
246
fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
247
let isometry = isometry.into();
248
249
let direction = isometry.rotation * *self.direction;
250
251
// Dividing `f32::MAX` by 2.0 is helpful so that we can do operations
252
// like growing or shrinking the AABB without breaking things.
253
let max = f32::MAX / 2.0;
254
let half_width = if direction.x == 0.0 { 0.0 } else { max };
255
let half_height = if direction.y == 0.0 { 0.0 } else { max };
256
let half_size = Vec2::new(half_width, half_height);
257
258
Aabb2d::new(isometry.translation, half_size)
259
}
260
261
fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
262
let isometry = isometry.into();
263
BoundingCircle::new(isometry.translation, f32::MAX / 2.0)
264
}
265
}
266
267
impl Bounded2d for Segment2d {
268
fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
269
Aabb2d::from_point_cloud(isometry, &[self.point1(), self.point2()])
270
}
271
272
fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
273
let isometry: Isometry2d = isometry.into();
274
let local_center = self.center();
275
let radius = local_center.distance(self.point1());
276
let local_circle = BoundingCircle::new(local_center, radius);
277
local_circle.transformed_by(isometry.translation, isometry.rotation)
278
}
279
}
280
281
#[cfg(feature = "alloc")]
282
impl Bounded2d for Polyline2d {
283
fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
284
Aabb2d::from_point_cloud(isometry, &self.vertices)
285
}
286
287
fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
288
BoundingCircle::from_point_cloud(isometry, &self.vertices)
289
}
290
}
291
292
impl Bounded2d for Triangle2d {
293
fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
294
let isometry = isometry.into();
295
let [a, b, c] = self.vertices.map(|vtx| isometry.rotation * vtx);
296
297
let min = Vec2::new(a.x.min(b.x).min(c.x), a.y.min(b.y).min(c.y));
298
let max = Vec2::new(a.x.max(b.x).max(c.x), a.y.max(b.y).max(c.y));
299
300
Aabb2d {
301
min: min + isometry.translation,
302
max: max + isometry.translation,
303
}
304
}
305
306
fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
307
let isometry = isometry.into();
308
let [a, b, c] = self.vertices;
309
310
// The points of the segment opposite to the obtuse or right angle if one exists
311
let side_opposite_to_non_acute = if (b - a).dot(c - a) <= 0.0 {
312
Some((b, c))
313
} else if (c - b).dot(a - b) <= 0.0 {
314
Some((c, a))
315
} else if (a - c).dot(b - c) <= 0.0 {
316
Some((a, b))
317
} else {
318
// The triangle is acute.
319
None
320
};
321
322
// Find the minimum bounding circle. If the triangle is obtuse, the circle passes through two vertices.
323
// Otherwise, it's the circumcircle and passes through all three.
324
if let Some((point1, point2)) = side_opposite_to_non_acute {
325
// The triangle is obtuse or right, so the minimum bounding circle's diameter is equal to the longest side.
326
// We can compute the minimum bounding circle from the line segment of the longest side.
327
let segment = Segment2d::new(point1, point2);
328
segment.bounding_circle(isometry)
329
} else {
330
// The triangle is acute, so the smallest bounding circle is the circumcircle.
331
let (Circle { radius }, circumcenter) = self.circumcircle();
332
BoundingCircle::new(isometry * circumcenter, radius)
333
}
334
}
335
}
336
337
impl Bounded2d for Rectangle {
338
fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
339
let isometry = isometry.into();
340
341
// Compute the AABB of the rotated rectangle by transforming the half-extents
342
// by an absolute rotation matrix.
343
let (sin, cos) = isometry.rotation.sin_cos();
344
let abs_rot_mat =
345
Mat2::from_cols_array(&[ops::abs(cos), ops::abs(sin), ops::abs(sin), ops::abs(cos)]);
346
let half_size = abs_rot_mat * self.half_size;
347
348
Aabb2d::new(isometry.translation, half_size)
349
}
350
351
fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
352
let isometry = isometry.into();
353
let radius = self.half_size.length();
354
BoundingCircle::new(isometry.translation, radius)
355
}
356
}
357
358
#[cfg(feature = "alloc")]
359
impl Bounded2d for Polygon {
360
fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
361
Aabb2d::from_point_cloud(isometry, &self.vertices)
362
}
363
364
fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
365
BoundingCircle::from_point_cloud(isometry, &self.vertices)
366
}
367
}
368
369
#[cfg(feature = "alloc")]
370
impl Bounded2d for ConvexPolygon {
371
fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
372
Aabb2d::from_point_cloud(isometry, self.vertices())
373
}
374
375
fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
376
BoundingCircle::from_point_cloud(isometry, self.vertices())
377
}
378
}
379
380
impl Bounded2d for RegularPolygon {
381
fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
382
let isometry = isometry.into();
383
384
let mut min = Vec2::ZERO;
385
let mut max = Vec2::ZERO;
386
387
for vertex in self.vertices(isometry.rotation.as_radians()) {
388
min = min.min(vertex);
389
max = max.max(vertex);
390
}
391
392
Aabb2d {
393
min: min + isometry.translation,
394
max: max + isometry.translation,
395
}
396
}
397
398
fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
399
let isometry = isometry.into();
400
BoundingCircle::new(isometry.translation, self.circumcircle.radius)
401
}
402
}
403
404
impl Bounded2d for Capsule2d {
405
fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
406
let isometry = isometry.into();
407
408
// Get the line segment between the semicircles of the rotated capsule
409
let segment = Segment2d::from_direction_and_length(
410
isometry.rotation * Dir2::Y,
411
self.half_length * 2.,
412
);
413
let (a, b) = (segment.point1(), segment.point2());
414
415
// Expand the line segment by the capsule radius to get the capsule half-extents
416
let min = a.min(b) - Vec2::splat(self.radius);
417
let max = a.max(b) + Vec2::splat(self.radius);
418
419
Aabb2d {
420
min: min + isometry.translation,
421
max: max + isometry.translation,
422
}
423
}
424
425
fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
426
let isometry = isometry.into();
427
BoundingCircle::new(isometry.translation, self.radius + self.half_length)
428
}
429
}
430
431
impl<P: Bounded2d + Primitive2d> Bounded2d for Ring<P> {
432
fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
433
self.outer_shape.aabb_2d(isometry)
434
}
435
436
fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
437
self.outer_shape.bounding_circle(isometry)
438
}
439
}
440
441
#[cfg(test)]
442
#[expect(clippy::print_stdout, reason = "Allowed in tests.")]
443
mod tests {
444
use core::f32::consts::{FRAC_PI_2, FRAC_PI_3, FRAC_PI_4, FRAC_PI_6, TAU};
445
use std::println;
446
447
use approx::assert_abs_diff_eq;
448
use glam::Vec2;
449
450
use crate::{
451
bounding::Bounded2d,
452
ops::{self, FloatPow},
453
primitives::{
454
Annulus, Arc2d, Capsule2d, Circle, CircularSector, CircularSegment, Ellipse, Line2d,
455
Plane2d, Polygon, Polyline2d, Rectangle, RegularPolygon, Rhombus, Segment2d,
456
Triangle2d,
457
},
458
Dir2, Isometry2d, Rot2,
459
};
460
461
#[test]
462
fn circle() {
463
let circle = Circle { radius: 1.0 };
464
let translation = Vec2::new(2.0, 1.0);
465
let isometry = Isometry2d::from_translation(translation);
466
467
let aabb = circle.aabb_2d(isometry);
468
assert_eq!(aabb.min, Vec2::new(1.0, 0.0));
469
assert_eq!(aabb.max, Vec2::new(3.0, 2.0));
470
471
let bounding_circle = circle.bounding_circle(isometry);
472
assert_eq!(bounding_circle.center, translation);
473
assert_eq!(bounding_circle.radius(), 1.0);
474
}
475
476
#[test]
477
// Arcs and circular segments have the same bounding shapes so they share test cases.
478
fn arc_and_segment() {
479
struct TestCase {
480
name: &'static str,
481
arc: Arc2d,
482
translation: Vec2,
483
rotation: f32,
484
aabb_min: Vec2,
485
aabb_max: Vec2,
486
bounding_circle_center: Vec2,
487
bounding_circle_radius: f32,
488
}
489
490
impl TestCase {
491
fn isometry(&self) -> Isometry2d {
492
Isometry2d::new(self.translation, self.rotation.into())
493
}
494
}
495
496
// The apothem of an arc covering 1/6th of a circle.
497
let apothem = ops::sqrt(3.0) / 2.0;
498
let tests = [
499
// Test case: a basic minor arc
500
TestCase {
501
name: "1/6th circle untransformed",
502
arc: Arc2d::from_radians(1.0, FRAC_PI_3),
503
translation: Vec2::ZERO,
504
rotation: 0.0,
505
aabb_min: Vec2::new(-0.5, apothem),
506
aabb_max: Vec2::new(0.5, 1.0),
507
bounding_circle_center: Vec2::new(0.0, apothem),
508
bounding_circle_radius: 0.5,
509
},
510
// Test case: a smaller arc, verifying that radius scaling works
511
TestCase {
512
name: "1/6th circle with radius 0.5",
513
arc: Arc2d::from_radians(0.5, FRAC_PI_3),
514
translation: Vec2::ZERO,
515
rotation: 0.0,
516
aabb_min: Vec2::new(-0.25, apothem / 2.0),
517
aabb_max: Vec2::new(0.25, 0.5),
518
bounding_circle_center: Vec2::new(0.0, apothem / 2.0),
519
bounding_circle_radius: 0.25,
520
},
521
// Test case: a larger arc, verifying that radius scaling works
522
TestCase {
523
name: "1/6th circle with radius 2.0",
524
arc: Arc2d::from_radians(2.0, FRAC_PI_3),
525
translation: Vec2::ZERO,
526
rotation: 0.0,
527
aabb_min: Vec2::new(-1.0, 2.0 * apothem),
528
aabb_max: Vec2::new(1.0, 2.0),
529
bounding_circle_center: Vec2::new(0.0, 2.0 * apothem),
530
bounding_circle_radius: 1.0,
531
},
532
// Test case: translation of a minor arc
533
TestCase {
534
name: "1/6th circle translated",
535
arc: Arc2d::from_radians(1.0, FRAC_PI_3),
536
translation: Vec2::new(2.0, 3.0),
537
rotation: 0.0,
538
aabb_min: Vec2::new(1.5, 3.0 + apothem),
539
aabb_max: Vec2::new(2.5, 4.0),
540
bounding_circle_center: Vec2::new(2.0, 3.0 + apothem),
541
bounding_circle_radius: 0.5,
542
},
543
// Test case: rotation of a minor arc
544
TestCase {
545
name: "1/6th circle rotated",
546
arc: Arc2d::from_radians(1.0, FRAC_PI_3),
547
translation: Vec2::ZERO,
548
// Rotate left by 1/12 of a circle, so the right endpoint is on the y-axis.
549
rotation: FRAC_PI_6,
550
aabb_min: Vec2::new(-apothem, 0.5),
551
aabb_max: Vec2::new(0.0, 1.0),
552
// The exact coordinates here are not obvious, but can be computed by constructing
553
// an altitude from the midpoint of the chord to the y-axis and using the right triangle
554
// similarity theorem.
555
bounding_circle_center: Vec2::new(-apothem / 2.0, apothem.squared()),
556
bounding_circle_radius: 0.5,
557
},
558
// Test case: handling of axis-aligned extrema
559
TestCase {
560
name: "1/4er circle rotated to be axis-aligned",
561
arc: Arc2d::from_radians(1.0, FRAC_PI_2),
562
translation: Vec2::ZERO,
563
// Rotate right by 1/8 of a circle, so the right endpoint is on the x-axis and the left endpoint is on the y-axis.
564
rotation: -FRAC_PI_4,
565
aabb_min: Vec2::ZERO,
566
aabb_max: Vec2::splat(1.0),
567
bounding_circle_center: Vec2::splat(0.5),
568
bounding_circle_radius: ops::sqrt(2.0) / 2.0,
569
},
570
// Test case: a basic major arc
571
TestCase {
572
name: "5/6th circle untransformed",
573
arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3),
574
translation: Vec2::ZERO,
575
rotation: 0.0,
576
aabb_min: Vec2::new(-1.0, -apothem),
577
aabb_max: Vec2::new(1.0, 1.0),
578
bounding_circle_center: Vec2::ZERO,
579
bounding_circle_radius: 1.0,
580
},
581
// Test case: a translated major arc
582
TestCase {
583
name: "5/6th circle translated",
584
arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3),
585
translation: Vec2::new(2.0, 3.0),
586
rotation: 0.0,
587
aabb_min: Vec2::new(1.0, 3.0 - apothem),
588
aabb_max: Vec2::new(3.0, 4.0),
589
bounding_circle_center: Vec2::new(2.0, 3.0),
590
bounding_circle_radius: 1.0,
591
},
592
// Test case: a rotated major arc, with inverted left/right angles
593
TestCase {
594
name: "5/6th circle rotated",
595
arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3),
596
translation: Vec2::ZERO,
597
// Rotate left by 1/12 of a circle, so the left endpoint is on the y-axis.
598
rotation: FRAC_PI_6,
599
aabb_min: Vec2::new(-1.0, -1.0),
600
aabb_max: Vec2::new(1.0, 1.0),
601
bounding_circle_center: Vec2::ZERO,
602
bounding_circle_radius: 1.0,
603
},
604
];
605
606
for test in tests {
607
#[cfg(feature = "std")]
608
println!("subtest case: {}", test.name);
609
let segment: CircularSegment = test.arc.into();
610
611
let arc_aabb = test.arc.aabb_2d(test.isometry());
612
assert_abs_diff_eq!(test.aabb_min, arc_aabb.min);
613
assert_abs_diff_eq!(test.aabb_max, arc_aabb.max);
614
let segment_aabb = segment.aabb_2d(test.isometry());
615
assert_abs_diff_eq!(test.aabb_min, segment_aabb.min);
616
assert_abs_diff_eq!(test.aabb_max, segment_aabb.max);
617
618
let arc_bounding_circle = test.arc.bounding_circle(test.isometry());
619
assert_abs_diff_eq!(test.bounding_circle_center, arc_bounding_circle.center);
620
assert_abs_diff_eq!(test.bounding_circle_radius, arc_bounding_circle.radius());
621
let segment_bounding_circle = segment.bounding_circle(test.isometry());
622
assert_abs_diff_eq!(test.bounding_circle_center, segment_bounding_circle.center);
623
assert_abs_diff_eq!(
624
test.bounding_circle_radius,
625
segment_bounding_circle.radius()
626
);
627
}
628
}
629
630
#[test]
631
fn circular_sector() {
632
struct TestCase {
633
name: &'static str,
634
arc: Arc2d,
635
translation: Vec2,
636
rotation: f32,
637
aabb_min: Vec2,
638
aabb_max: Vec2,
639
bounding_circle_center: Vec2,
640
bounding_circle_radius: f32,
641
}
642
643
impl TestCase {
644
fn isometry(&self) -> Isometry2d {
645
Isometry2d::new(self.translation, self.rotation.into())
646
}
647
}
648
649
// The apothem of an arc covering 1/6th of a circle.
650
let apothem = ops::sqrt(3.0) / 2.0;
651
let inv_sqrt_3 = ops::sqrt(3.0).recip();
652
let tests = [
653
// Test case: A sector whose arc is minor, but whose bounding circle is not the circumcircle of the endpoints and center
654
TestCase {
655
name: "1/3rd circle",
656
arc: Arc2d::from_radians(1.0, TAU / 3.0),
657
translation: Vec2::ZERO,
658
rotation: 0.0,
659
aabb_min: Vec2::new(-apothem, 0.0),
660
aabb_max: Vec2::new(apothem, 1.0),
661
bounding_circle_center: Vec2::new(0.0, 0.5),
662
bounding_circle_radius: apothem,
663
},
664
// The remaining test cases are selected as for arc_and_segment.
665
TestCase {
666
name: "1/6th circle untransformed",
667
arc: Arc2d::from_radians(1.0, FRAC_PI_3),
668
translation: Vec2::ZERO,
669
rotation: 0.0,
670
aabb_min: Vec2::new(-0.5, 0.0),
671
aabb_max: Vec2::new(0.5, 1.0),
672
// The bounding circle is a circumcircle of an equilateral triangle with side length 1.
673
// The distance from the corner to the center of such a triangle is 1/sqrt(3).
674
bounding_circle_center: Vec2::new(0.0, inv_sqrt_3),
675
bounding_circle_radius: inv_sqrt_3,
676
},
677
TestCase {
678
name: "1/6th circle with radius 0.5",
679
arc: Arc2d::from_radians(0.5, FRAC_PI_3),
680
translation: Vec2::ZERO,
681
rotation: 0.0,
682
aabb_min: Vec2::new(-0.25, 0.0),
683
aabb_max: Vec2::new(0.25, 0.5),
684
bounding_circle_center: Vec2::new(0.0, inv_sqrt_3 / 2.0),
685
bounding_circle_radius: inv_sqrt_3 / 2.0,
686
},
687
TestCase {
688
name: "1/6th circle with radius 2.0",
689
arc: Arc2d::from_radians(2.0, FRAC_PI_3),
690
translation: Vec2::ZERO,
691
rotation: 0.0,
692
aabb_min: Vec2::new(-1.0, 0.0),
693
aabb_max: Vec2::new(1.0, 2.0),
694
bounding_circle_center: Vec2::new(0.0, 2.0 * inv_sqrt_3),
695
bounding_circle_radius: 2.0 * inv_sqrt_3,
696
},
697
TestCase {
698
name: "1/6th circle translated",
699
arc: Arc2d::from_radians(1.0, FRAC_PI_3),
700
translation: Vec2::new(2.0, 3.0),
701
rotation: 0.0,
702
aabb_min: Vec2::new(1.5, 3.0),
703
aabb_max: Vec2::new(2.5, 4.0),
704
bounding_circle_center: Vec2::new(2.0, 3.0 + inv_sqrt_3),
705
bounding_circle_radius: inv_sqrt_3,
706
},
707
TestCase {
708
name: "1/6th circle rotated",
709
arc: Arc2d::from_radians(1.0, FRAC_PI_3),
710
translation: Vec2::ZERO,
711
// Rotate left by 1/12 of a circle, so the right endpoint is on the y-axis.
712
rotation: FRAC_PI_6,
713
aabb_min: Vec2::new(-apothem, 0.0),
714
aabb_max: Vec2::new(0.0, 1.0),
715
// The x-coordinate is now the inradius of the equilateral triangle, which is sqrt(3)/2.
716
bounding_circle_center: Vec2::new(-inv_sqrt_3 / 2.0, 0.5),
717
bounding_circle_radius: inv_sqrt_3,
718
},
719
TestCase {
720
name: "1/4er circle rotated to be axis-aligned",
721
arc: Arc2d::from_radians(1.0, FRAC_PI_2),
722
translation: Vec2::ZERO,
723
// Rotate right by 1/8 of a circle, so the right endpoint is on the x-axis and the left endpoint is on the y-axis.
724
rotation: -FRAC_PI_4,
725
aabb_min: Vec2::ZERO,
726
aabb_max: Vec2::splat(1.0),
727
bounding_circle_center: Vec2::splat(0.5),
728
bounding_circle_radius: ops::sqrt(2.0) / 2.0,
729
},
730
TestCase {
731
name: "5/6th circle untransformed",
732
arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3),
733
translation: Vec2::ZERO,
734
rotation: 0.0,
735
aabb_min: Vec2::new(-1.0, -apothem),
736
aabb_max: Vec2::new(1.0, 1.0),
737
bounding_circle_center: Vec2::ZERO,
738
bounding_circle_radius: 1.0,
739
},
740
TestCase {
741
name: "5/6th circle translated",
742
arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3),
743
translation: Vec2::new(2.0, 3.0),
744
rotation: 0.0,
745
aabb_min: Vec2::new(1.0, 3.0 - apothem),
746
aabb_max: Vec2::new(3.0, 4.0),
747
bounding_circle_center: Vec2::new(2.0, 3.0),
748
bounding_circle_radius: 1.0,
749
},
750
TestCase {
751
name: "5/6th circle rotated",
752
arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3),
753
translation: Vec2::ZERO,
754
// Rotate left by 1/12 of a circle, so the left endpoint is on the y-axis.
755
rotation: FRAC_PI_6,
756
aabb_min: Vec2::new(-1.0, -1.0),
757
aabb_max: Vec2::new(1.0, 1.0),
758
bounding_circle_center: Vec2::ZERO,
759
bounding_circle_radius: 1.0,
760
},
761
];
762
763
for test in tests {
764
#[cfg(feature = "std")]
765
println!("subtest case: {}", test.name);
766
let sector: CircularSector = test.arc.into();
767
768
let aabb = sector.aabb_2d(test.isometry());
769
assert_abs_diff_eq!(test.aabb_min, aabb.min);
770
assert_abs_diff_eq!(test.aabb_max, aabb.max);
771
772
let bounding_circle = sector.bounding_circle(test.isometry());
773
assert_abs_diff_eq!(test.bounding_circle_center, bounding_circle.center);
774
assert_abs_diff_eq!(test.bounding_circle_radius, bounding_circle.radius());
775
}
776
}
777
778
#[test]
779
fn ellipse() {
780
let ellipse = Ellipse::new(1.0, 0.5);
781
let translation = Vec2::new(2.0, 1.0);
782
let isometry = Isometry2d::from_translation(translation);
783
784
let aabb = ellipse.aabb_2d(isometry);
785
assert_eq!(aabb.min, Vec2::new(1.0, 0.5));
786
assert_eq!(aabb.max, Vec2::new(3.0, 1.5));
787
788
let bounding_circle = ellipse.bounding_circle(isometry);
789
assert_eq!(bounding_circle.center, translation);
790
assert_eq!(bounding_circle.radius(), 1.0);
791
}
792
793
#[test]
794
fn annulus() {
795
let annulus = Annulus::new(1.0, 2.0);
796
let translation = Vec2::new(2.0, 1.0);
797
let rotation = Rot2::radians(1.0);
798
let isometry = Isometry2d::new(translation, rotation);
799
800
let aabb = annulus.aabb_2d(isometry);
801
assert_eq!(aabb.min, Vec2::new(0.0, -1.0));
802
assert_eq!(aabb.max, Vec2::new(4.0, 3.0));
803
804
let bounding_circle = annulus.bounding_circle(isometry);
805
assert_eq!(bounding_circle.center, translation);
806
assert_eq!(bounding_circle.radius(), 2.0);
807
}
808
809
#[test]
810
fn rhombus() {
811
let rhombus = Rhombus::new(2.0, 1.0);
812
let translation = Vec2::new(2.0, 1.0);
813
let rotation = Rot2::radians(FRAC_PI_4);
814
let isometry = Isometry2d::new(translation, rotation);
815
816
let aabb = rhombus.aabb_2d(isometry);
817
assert_eq!(aabb.min, Vec2::new(1.2928932, 0.29289323));
818
assert_eq!(aabb.max, Vec2::new(2.7071068, 1.7071068));
819
820
let bounding_circle = rhombus.bounding_circle(isometry);
821
assert_eq!(bounding_circle.center, translation);
822
assert_eq!(bounding_circle.radius(), 1.0);
823
824
let rhombus = Rhombus::new(0.0, 0.0);
825
let translation = Vec2::new(0.0, 0.0);
826
let isometry = Isometry2d::new(translation, rotation);
827
828
let aabb = rhombus.aabb_2d(isometry);
829
assert_eq!(aabb.min, Vec2::new(0.0, 0.0));
830
assert_eq!(aabb.max, Vec2::new(0.0, 0.0));
831
832
let bounding_circle = rhombus.bounding_circle(isometry);
833
assert_eq!(bounding_circle.center, translation);
834
assert_eq!(bounding_circle.radius(), 0.0);
835
}
836
837
#[test]
838
fn plane() {
839
let translation = Vec2::new(2.0, 1.0);
840
let isometry = Isometry2d::from_translation(translation);
841
842
let aabb1 = Plane2d::new(Vec2::X).aabb_2d(isometry);
843
assert_eq!(aabb1.min, Vec2::new(2.0, -f32::MAX / 2.0));
844
assert_eq!(aabb1.max, Vec2::new(2.0, f32::MAX / 2.0));
845
846
let aabb2 = Plane2d::new(Vec2::Y).aabb_2d(isometry);
847
assert_eq!(aabb2.min, Vec2::new(-f32::MAX / 2.0, 1.0));
848
assert_eq!(aabb2.max, Vec2::new(f32::MAX / 2.0, 1.0));
849
850
let aabb3 = Plane2d::new(Vec2::ONE).aabb_2d(isometry);
851
assert_eq!(aabb3.min, Vec2::new(-f32::MAX / 2.0, -f32::MAX / 2.0));
852
assert_eq!(aabb3.max, Vec2::new(f32::MAX / 2.0, f32::MAX / 2.0));
853
854
let bounding_circle = Plane2d::new(Vec2::Y).bounding_circle(isometry);
855
assert_eq!(bounding_circle.center, translation);
856
assert_eq!(bounding_circle.radius(), f32::MAX / 2.0);
857
}
858
859
#[test]
860
fn line() {
861
let translation = Vec2::new(2.0, 1.0);
862
let isometry = Isometry2d::from_translation(translation);
863
864
let aabb1 = Line2d { direction: Dir2::Y }.aabb_2d(isometry);
865
assert_eq!(aabb1.min, Vec2::new(2.0, -f32::MAX / 2.0));
866
assert_eq!(aabb1.max, Vec2::new(2.0, f32::MAX / 2.0));
867
868
let aabb2 = Line2d { direction: Dir2::X }.aabb_2d(isometry);
869
assert_eq!(aabb2.min, Vec2::new(-f32::MAX / 2.0, 1.0));
870
assert_eq!(aabb2.max, Vec2::new(f32::MAX / 2.0, 1.0));
871
872
let aabb3 = Line2d {
873
direction: Dir2::from_xy(1.0, 1.0).unwrap(),
874
}
875
.aabb_2d(isometry);
876
assert_eq!(aabb3.min, Vec2::new(-f32::MAX / 2.0, -f32::MAX / 2.0));
877
assert_eq!(aabb3.max, Vec2::new(f32::MAX / 2.0, f32::MAX / 2.0));
878
879
let bounding_circle = Line2d { direction: Dir2::Y }.bounding_circle(isometry);
880
assert_eq!(bounding_circle.center, translation);
881
assert_eq!(bounding_circle.radius(), f32::MAX / 2.0);
882
}
883
884
#[test]
885
fn segment() {
886
let segment = Segment2d::new(Vec2::new(-1.0, -0.5), Vec2::new(1.0, 0.5));
887
let translation = Vec2::new(2.0, 1.0);
888
let isometry = Isometry2d::from_translation(translation);
889
890
let aabb = segment.aabb_2d(isometry);
891
assert_eq!(aabb.min, Vec2::new(1.0, 0.5));
892
assert_eq!(aabb.max, Vec2::new(3.0, 1.5));
893
894
let bounding_circle = segment.bounding_circle(isometry);
895
assert_eq!(bounding_circle.center, translation);
896
assert_eq!(bounding_circle.radius(), ops::hypot(1.0, 0.5));
897
}
898
899
#[test]
900
fn polyline() {
901
let polyline = Polyline2d::new([
902
Vec2::ONE,
903
Vec2::new(-1.0, 1.0),
904
Vec2::NEG_ONE,
905
Vec2::new(1.0, -1.0),
906
]);
907
let translation = Vec2::new(2.0, 1.0);
908
let isometry = Isometry2d::from_translation(translation);
909
910
let aabb = polyline.aabb_2d(isometry);
911
assert_eq!(aabb.min, Vec2::new(1.0, 0.0));
912
assert_eq!(aabb.max, Vec2::new(3.0, 2.0));
913
914
let bounding_circle = polyline.bounding_circle(isometry);
915
assert_eq!(bounding_circle.center, translation);
916
assert_eq!(bounding_circle.radius(), core::f32::consts::SQRT_2);
917
}
918
919
#[test]
920
fn acute_triangle() {
921
let acute_triangle =
922
Triangle2d::new(Vec2::new(0.0, 1.0), Vec2::NEG_ONE, Vec2::new(1.0, -1.0));
923
let translation = Vec2::new(2.0, 1.0);
924
let isometry = Isometry2d::from_translation(translation);
925
926
let aabb = acute_triangle.aabb_2d(isometry);
927
assert_eq!(aabb.min, Vec2::new(1.0, 0.0));
928
assert_eq!(aabb.max, Vec2::new(3.0, 2.0));
929
930
// For acute triangles, the center is the circumcenter
931
let (Circle { radius }, circumcenter) = acute_triangle.circumcircle();
932
let bounding_circle = acute_triangle.bounding_circle(isometry);
933
assert_eq!(bounding_circle.center, circumcenter + translation);
934
assert_eq!(bounding_circle.radius(), radius);
935
}
936
937
#[test]
938
fn obtuse_triangle() {
939
let obtuse_triangle = Triangle2d::new(
940
Vec2::new(0.0, 1.0),
941
Vec2::new(-10.0, -1.0),
942
Vec2::new(10.0, -1.0),
943
);
944
let translation = Vec2::new(2.0, 1.0);
945
let isometry = Isometry2d::from_translation(translation);
946
947
let aabb = obtuse_triangle.aabb_2d(isometry);
948
assert_eq!(aabb.min, Vec2::new(-8.0, 0.0));
949
assert_eq!(aabb.max, Vec2::new(12.0, 2.0));
950
951
// For obtuse and right triangles, the center is the midpoint of the longest side (diameter of bounding circle)
952
let bounding_circle = obtuse_triangle.bounding_circle(isometry);
953
assert_eq!(bounding_circle.center, translation - Vec2::Y);
954
assert_eq!(bounding_circle.radius(), 10.0);
955
}
956
957
#[test]
958
fn rectangle() {
959
let rectangle = Rectangle::new(2.0, 1.0);
960
let translation = Vec2::new(2.0, 1.0);
961
962
let aabb = rectangle.aabb_2d(Isometry2d::new(translation, Rot2::radians(FRAC_PI_4)));
963
let expected_half_size = Vec2::splat(1.0606601);
964
assert_eq!(aabb.min, translation - expected_half_size);
965
assert_eq!(aabb.max, translation + expected_half_size);
966
967
let bounding_circle = rectangle.bounding_circle(Isometry2d::from_translation(translation));
968
assert_eq!(bounding_circle.center, translation);
969
assert_eq!(bounding_circle.radius(), ops::hypot(1.0, 0.5));
970
}
971
972
#[test]
973
fn polygon() {
974
let polygon = Polygon::new([
975
Vec2::ONE,
976
Vec2::new(-1.0, 1.0),
977
Vec2::NEG_ONE,
978
Vec2::new(1.0, -1.0),
979
]);
980
let translation = Vec2::new(2.0, 1.0);
981
let isometry = Isometry2d::from_translation(translation);
982
983
let aabb = polygon.aabb_2d(isometry);
984
assert_eq!(aabb.min, Vec2::new(1.0, 0.0));
985
assert_eq!(aabb.max, Vec2::new(3.0, 2.0));
986
987
let bounding_circle = polygon.bounding_circle(isometry);
988
assert_eq!(bounding_circle.center, translation);
989
assert_eq!(bounding_circle.radius(), core::f32::consts::SQRT_2);
990
}
991
992
#[test]
993
fn regular_polygon() {
994
let regular_polygon = RegularPolygon::new(1.0, 5);
995
let translation = Vec2::new(2.0, 1.0);
996
let isometry = Isometry2d::from_translation(translation);
997
998
let aabb = regular_polygon.aabb_2d(isometry);
999
assert!((aabb.min - (translation - Vec2::new(0.9510565, 0.8090169))).length() < 1e-6);
1000
assert!((aabb.max - (translation + Vec2::new(0.9510565, 1.0))).length() < 1e-6);
1001
1002
let bounding_circle = regular_polygon.bounding_circle(isometry);
1003
assert_eq!(bounding_circle.center, translation);
1004
assert_eq!(bounding_circle.radius(), 1.0);
1005
}
1006
1007
#[test]
1008
fn capsule() {
1009
let capsule = Capsule2d::new(0.5, 2.0);
1010
let translation = Vec2::new(2.0, 1.0);
1011
let isometry = Isometry2d::from_translation(translation);
1012
1013
let aabb = capsule.aabb_2d(isometry);
1014
assert_eq!(aabb.min, translation - Vec2::new(0.5, 1.5));
1015
assert_eq!(aabb.max, translation + Vec2::new(0.5, 1.5));
1016
1017
let bounding_circle = capsule.bounding_circle(isometry);
1018
assert_eq!(bounding_circle.center, translation);
1019
assert_eq!(bounding_circle.radius(), 1.5);
1020
}
1021
}
1022
1023