Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_math/src/primitives/dim3.rs
9366 views
1
use core::f32::consts::{FRAC_PI_3, PI};
2
3
use super::{Circle, Measured2d, Measured3d, Primitive2d, Primitive3d};
4
use crate::{
5
ops::{self, FloatPow},
6
Dir3, InvalidDirectionError, Isometry3d, Mat3, Ray3d, Vec2, Vec3,
7
};
8
9
#[cfg(feature = "bevy_reflect")]
10
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
11
#[cfg(all(feature = "serialize", feature = "bevy_reflect"))]
12
use bevy_reflect::{ReflectDeserialize, ReflectSerialize};
13
use glam::Quat;
14
15
#[cfg(feature = "alloc")]
16
use alloc::vec::Vec;
17
18
/// A sphere primitive, representing the set of all points some distance from the origin
19
#[derive(Clone, Copy, Debug, PartialEq)]
20
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
21
#[cfg_attr(
22
feature = "bevy_reflect",
23
derive(Reflect),
24
reflect(Debug, PartialEq, Default, Clone)
25
)]
26
#[cfg_attr(
27
all(feature = "serialize", feature = "bevy_reflect"),
28
reflect(Serialize, Deserialize)
29
)]
30
pub struct Sphere {
31
/// The radius of the sphere
32
pub radius: f32,
33
}
34
35
impl Primitive3d for Sphere {}
36
37
impl Default for Sphere {
38
/// Returns the default [`Sphere`] with a radius of `0.5`.
39
fn default() -> Self {
40
Self { radius: 0.5 }
41
}
42
}
43
44
impl Sphere {
45
/// Create a new [`Sphere`] from a `radius`
46
#[inline]
47
pub const fn new(radius: f32) -> Self {
48
Self { radius }
49
}
50
51
/// Get the diameter of the sphere
52
#[inline]
53
pub const fn diameter(&self) -> f32 {
54
2.0 * self.radius
55
}
56
57
/// Finds the point on the sphere that is closest to the given `point`.
58
///
59
/// If the point is outside the sphere, the returned point will be on the surface of the sphere.
60
/// Otherwise, it will be inside the sphere and returned as is.
61
#[inline]
62
pub fn closest_point(&self, point: Vec3) -> Vec3 {
63
let distance_squared = point.length_squared();
64
65
if distance_squared <= self.radius.squared() {
66
// The point is inside the sphere.
67
point
68
} else {
69
// The point is outside the sphere.
70
// Find the closest point on the surface of the sphere.
71
let dir_to_point = point / ops::sqrt(distance_squared);
72
self.radius * dir_to_point
73
}
74
}
75
}
76
77
impl Measured3d for Sphere {
78
/// Get the surface area of the sphere
79
#[inline]
80
fn area(&self) -> f32 {
81
4.0 * PI * self.radius.squared()
82
}
83
84
/// Get the volume of the sphere
85
#[inline]
86
fn volume(&self) -> f32 {
87
4.0 * FRAC_PI_3 * self.radius.cubed()
88
}
89
}
90
91
/// A bounded plane in 3D space. It forms a surface starting from the origin with a defined height and width.
92
#[derive(Clone, Copy, Debug, PartialEq)]
93
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
94
#[cfg_attr(
95
feature = "bevy_reflect",
96
derive(Reflect),
97
reflect(Debug, PartialEq, Default, Clone)
98
)]
99
#[cfg_attr(
100
all(feature = "serialize", feature = "bevy_reflect"),
101
reflect(Serialize, Deserialize)
102
)]
103
pub struct Plane3d {
104
/// The normal of the plane. The plane will be placed perpendicular to this direction
105
pub normal: Dir3,
106
/// Half of the width and height of the plane
107
pub half_size: Vec2,
108
}
109
110
impl Primitive3d for Plane3d {}
111
112
impl Default for Plane3d {
113
/// Returns the default [`Plane3d`] with a normal pointing in the `+Y` direction, width and height of `1.0`.
114
fn default() -> Self {
115
Self {
116
normal: Dir3::Y,
117
half_size: Vec2::splat(0.5),
118
}
119
}
120
}
121
122
impl Plane3d {
123
/// Create a new `Plane3d` from a normal and a half size
124
///
125
/// # Panics
126
///
127
/// Panics if the given `normal` is zero (or very close to zero), or non-finite.
128
#[inline]
129
pub fn new(normal: Vec3, half_size: Vec2) -> Self {
130
Self {
131
normal: Dir3::new(normal).expect("normal must be nonzero and finite"),
132
half_size,
133
}
134
}
135
136
/// Create a new `Plane3d` based on three points and compute the geometric center
137
/// of those points.
138
///
139
/// The direction of the plane normal is determined by the winding order
140
/// of the triangular shape formed by the points.
141
///
142
/// # Panics
143
///
144
/// Panics if a valid normal can not be computed, for example when the points
145
/// are *collinear* and lie on the same line.
146
#[inline]
147
pub fn from_points(a: Vec3, b: Vec3, c: Vec3) -> (Self, Vec3) {
148
let normal = Dir3::new((b - a).cross(c - a)).expect(
149
"finite plane must be defined by three finite points that don't lie on the same line",
150
);
151
let translation = (a + b + c) / 3.0;
152
153
(
154
Self {
155
normal,
156
..Default::default()
157
},
158
translation,
159
)
160
}
161
}
162
impl Measured2d for Plane3d {
163
#[inline]
164
fn area(&self) -> f32 {
165
self.half_size.element_product() * 4.0
166
}
167
168
#[inline]
169
fn perimeter(&self) -> f32 {
170
self.half_size.element_sum() * 4.0
171
}
172
}
173
174
/// An unbounded plane in 3D space. It forms a separating surface through the origin,
175
/// stretching infinitely far
176
#[derive(Clone, Copy, Debug, PartialEq)]
177
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
178
#[cfg_attr(
179
feature = "bevy_reflect",
180
derive(Reflect),
181
reflect(Debug, PartialEq, Default, Clone)
182
)]
183
#[cfg_attr(
184
all(feature = "serialize", feature = "bevy_reflect"),
185
reflect(Serialize, Deserialize)
186
)]
187
pub struct InfinitePlane3d {
188
/// The normal of the plane. The plane will be placed perpendicular to this direction
189
pub normal: Dir3,
190
}
191
192
impl Primitive3d for InfinitePlane3d {}
193
194
impl Default for InfinitePlane3d {
195
/// Returns the default [`InfinitePlane3d`] with a normal pointing in the `+Y` direction.
196
fn default() -> Self {
197
Self { normal: Dir3::Y }
198
}
199
}
200
201
impl InfinitePlane3d {
202
/// Create a new `InfinitePlane3d` from a normal
203
///
204
/// # Panics
205
///
206
/// Panics if the given `normal` is zero (or very close to zero), or non-finite.
207
#[inline]
208
pub fn new<T: TryInto<Dir3>>(normal: T) -> Self
209
where
210
<T as TryInto<Dir3>>::Error: core::fmt::Debug,
211
{
212
Self {
213
normal: normal
214
.try_into()
215
.expect("normal must be nonzero and finite"),
216
}
217
}
218
219
/// Create a new `InfinitePlane3d` based on three points and compute the geometric center
220
/// of those points.
221
///
222
/// The direction of the plane normal is determined by the winding order
223
/// of the triangular shape formed by the points.
224
///
225
/// # Panics
226
///
227
/// Panics if a valid normal can not be computed, for example when the points
228
/// are *collinear* and lie on the same line.
229
#[inline]
230
pub fn from_points(a: Vec3, b: Vec3, c: Vec3) -> (Self, Vec3) {
231
let normal = Dir3::new((b - a).cross(c - a)).expect(
232
"infinite plane must be defined by three finite points that don't lie on the same line",
233
);
234
let translation = (a + b + c) / 3.0;
235
236
(Self { normal }, translation)
237
}
238
239
/// Computes the shortest distance between a plane transformed with the given `isometry` and a
240
/// `point`. The result is a signed value; it's positive if the point lies in the half-space
241
/// that the plane's normal vector points towards.
242
#[inline]
243
pub fn signed_distance(&self, isometry: impl Into<Isometry3d>, point: Vec3) -> f32 {
244
let isometry = isometry.into();
245
self.normal.dot(isometry.inverse() * point)
246
}
247
248
/// Injects the `point` into this plane transformed with the given `isometry`.
249
///
250
/// This projects the point orthogonally along the shortest path onto the plane.
251
#[inline]
252
pub fn project_point(&self, isometry: impl Into<Isometry3d>, point: Vec3) -> Vec3 {
253
point - self.normal * self.signed_distance(isometry, point)
254
}
255
256
/// Computes an [`Isometry3d`] which transforms points from the plane in 3D space with the given
257
/// `origin` to the XY-plane.
258
///
259
/// ## Guarantees
260
///
261
/// * the transformation is a [congruence] meaning it will preserve all distances and angles of
262
/// the transformed geometry
263
/// * uses the least rotation possible to transform the geometry
264
/// * if two geometries are transformed with the same isometry, then the relations between
265
/// them, like distances, are also preserved
266
/// * compared to projections, the transformation is lossless (up to floating point errors)
267
/// reversible
268
///
269
/// ## Non-Guarantees
270
///
271
/// * the rotation used is generally not unique
272
/// * the orientation of the transformed geometry in the XY plane might be arbitrary, to
273
/// enforce some kind of alignment the user has to use an extra transformation ontop of this
274
/// one
275
///
276
/// See [`isometries_xy`] for example usescases.
277
///
278
/// [congruence]: https://en.wikipedia.org/wiki/Congruence_(geometry)
279
/// [`isometries_xy`]: `InfinitePlane3d::isometries_xy`
280
#[inline]
281
pub fn isometry_into_xy(&self, origin: Vec3) -> Isometry3d {
282
let rotation = Quat::from_rotation_arc(self.normal.as_vec3(), Vec3::Z);
283
let transformed_origin = rotation * origin;
284
Isometry3d::new(-Vec3::Z * transformed_origin.z, rotation)
285
}
286
287
/// Computes an [`Isometry3d`] which transforms points from the XY-plane to this plane with the
288
/// given `origin`.
289
///
290
/// ## Guarantees
291
///
292
/// * the transformation is a [congruence] meaning it will preserve all distances and angles of
293
/// the transformed geometry
294
/// * uses the least rotation possible to transform the geometry
295
/// * if two geometries are transformed with the same isometry, then the relations between
296
/// them, like distances, are also preserved
297
/// * compared to projections, the transformation is lossless (up to floating point errors)
298
/// reversible
299
///
300
/// ## Non-Guarantees
301
///
302
/// * the rotation used is generally not unique
303
/// * the orientation of the transformed geometry in the XY plane might be arbitrary, to
304
/// enforce some kind of alignment the user has to use an extra transformation ontop of this
305
/// one
306
///
307
/// See [`isometries_xy`] for example usescases.
308
///
309
/// [congruence]: https://en.wikipedia.org/wiki/Congruence_(geometry)
310
/// [`isometries_xy`]: `InfinitePlane3d::isometries_xy`
311
#[inline]
312
pub fn isometry_from_xy(&self, origin: Vec3) -> Isometry3d {
313
self.isometry_into_xy(origin).inverse()
314
}
315
316
/// Computes both [isometries] which transforms points from the plane in 3D space with the
317
/// given `origin` to the XY-plane and back.
318
///
319
/// [isometries]: `Isometry3d`
320
///
321
/// # Example
322
///
323
/// The projection and its inverse can be used to run 2D algorithms on flat shapes in 3D. The
324
/// workflow would usually look like this:
325
///
326
/// ```
327
/// # use bevy_math::{Vec3, Dir3};
328
/// # use bevy_math::primitives::InfinitePlane3d;
329
///
330
/// let triangle_3d @ [a, b, c] = [Vec3::X, Vec3::Y, Vec3::Z];
331
/// let center = (a + b + c) / 3.0;
332
///
333
/// let plane = InfinitePlane3d::new(Vec3::ONE);
334
///
335
/// let (to_xy, from_xy) = plane.isometries_xy(center);
336
///
337
/// let triangle_2d = triangle_3d.map(|vec3| to_xy * vec3).map(|vec3| vec3.truncate());
338
///
339
/// // apply some algorithm to `triangle_2d`
340
///
341
/// let triangle_3d = triangle_2d.map(|vec2| vec2.extend(0.0)).map(|vec3| from_xy * vec3);
342
/// ```
343
#[inline]
344
pub fn isometries_xy(&self, origin: Vec3) -> (Isometry3d, Isometry3d) {
345
let projection = self.isometry_into_xy(origin);
346
(projection, projection.inverse())
347
}
348
}
349
350
/// An infinite line going through the origin along a direction in 3D space.
351
///
352
/// For a finite line: [`Segment3d`]
353
#[derive(Clone, Copy, Debug, PartialEq)]
354
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
355
#[cfg_attr(
356
feature = "bevy_reflect",
357
derive(Reflect),
358
reflect(Debug, PartialEq, Clone)
359
)]
360
#[cfg_attr(
361
all(feature = "serialize", feature = "bevy_reflect"),
362
reflect(Serialize, Deserialize)
363
)]
364
pub struct Line3d {
365
/// The direction of the line
366
pub direction: Dir3,
367
}
368
369
impl Primitive3d for Line3d {}
370
371
/// A line segment defined by two endpoints in 3D space.
372
#[derive(Clone, Copy, Debug, PartialEq)]
373
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
374
#[cfg_attr(
375
feature = "bevy_reflect",
376
derive(Reflect),
377
reflect(Debug, PartialEq, Clone)
378
)]
379
#[cfg_attr(
380
all(feature = "serialize", feature = "bevy_reflect"),
381
reflect(Serialize, Deserialize)
382
)]
383
#[doc(alias = "LineSegment3d")]
384
pub struct Segment3d {
385
/// The endpoints of the line segment.
386
pub vertices: [Vec3; 2],
387
}
388
389
impl Primitive3d for Segment3d {}
390
391
impl Default for Segment3d {
392
fn default() -> Self {
393
Self {
394
vertices: [Vec3::new(-0.5, 0.0, 0.0), Vec3::new(0.5, 0.0, 0.0)],
395
}
396
}
397
}
398
399
impl Segment3d {
400
/// Create a new `Segment3d` from its endpoints.
401
#[inline]
402
pub const fn new(point1: Vec3, point2: Vec3) -> Self {
403
Self {
404
vertices: [point1, point2],
405
}
406
}
407
408
/// Create a new `Segment3d` centered at the origin with the given direction and length.
409
///
410
/// The endpoints will be at `-direction * length / 2.0` and `direction * length / 2.0`.
411
#[inline]
412
pub fn from_direction_and_length(direction: Dir3, length: f32) -> Self {
413
let endpoint = 0.5 * length * direction;
414
Self {
415
vertices: [-endpoint, endpoint],
416
}
417
}
418
419
/// Create a new `Segment3d` centered at the origin from a vector representing
420
/// the direction and length of the line segment.
421
///
422
/// The endpoints will be at `-scaled_direction / 2.0` and `scaled_direction / 2.0`.
423
#[inline]
424
pub fn from_scaled_direction(scaled_direction: Vec3) -> Self {
425
let endpoint = 0.5 * scaled_direction;
426
Self {
427
vertices: [-endpoint, endpoint],
428
}
429
}
430
431
/// Create a new `Segment3d` starting from the origin of the given `ray`,
432
/// going in the direction of the ray for the given `length`.
433
///
434
/// The endpoints will be at `ray.origin` and `ray.origin + length * ray.direction`.
435
#[inline]
436
pub fn from_ray_and_length(ray: Ray3d, length: f32) -> Self {
437
Self {
438
vertices: [ray.origin, ray.get_point(length)],
439
}
440
}
441
442
/// Get the position of the first endpoint of the line segment.
443
#[inline]
444
pub const fn point1(&self) -> Vec3 {
445
self.vertices[0]
446
}
447
448
/// Get the position of the second endpoint of the line segment.
449
#[inline]
450
pub const fn point2(&self) -> Vec3 {
451
self.vertices[1]
452
}
453
454
/// Compute the midpoint between the two endpoints of the line segment.
455
#[inline]
456
#[doc(alias = "midpoint")]
457
pub fn center(&self) -> Vec3 {
458
self.point1().midpoint(self.point2())
459
}
460
461
/// Compute the length of the line segment.
462
#[inline]
463
pub fn length(&self) -> f32 {
464
self.point1().distance(self.point2())
465
}
466
467
/// Compute the squared length of the line segment.
468
#[inline]
469
pub fn length_squared(&self) -> f32 {
470
self.point1().distance_squared(self.point2())
471
}
472
473
/// Compute the normalized direction pointing from the first endpoint to the second endpoint.
474
///
475
/// For the non-panicking version, see [`Segment3d::try_direction`].
476
///
477
/// # Panics
478
///
479
/// Panics if a valid direction could not be computed, for example when the endpoints are coincident, NaN, or infinite.
480
#[inline]
481
pub fn direction(&self) -> Dir3 {
482
self.try_direction().unwrap_or_else(|err| {
483
panic!("Failed to compute the direction of a line segment: {err}")
484
})
485
}
486
487
/// Try to compute the normalized direction pointing from the first endpoint to the second endpoint.
488
///
489
/// Returns [`Err(InvalidDirectionError)`](InvalidDirectionError) if a valid direction could not be computed,
490
/// for example when the endpoints are coincident, NaN, or infinite.
491
#[inline]
492
pub fn try_direction(&self) -> Result<Dir3, InvalidDirectionError> {
493
Dir3::new(self.scaled_direction())
494
}
495
496
/// Compute the vector from the first endpoint to the second endpoint.
497
#[inline]
498
pub fn scaled_direction(&self) -> Vec3 {
499
self.point2() - self.point1()
500
}
501
502
/// Compute the segment transformed by the given [`Isometry3d`].
503
#[inline]
504
pub fn transformed(&self, isometry: impl Into<Isometry3d>) -> Self {
505
let isometry: Isometry3d = isometry.into();
506
Self::new(
507
isometry.transform_point(self.point1()).into(),
508
isometry.transform_point(self.point2()).into(),
509
)
510
}
511
512
/// Compute the segment translated by the given vector.
513
#[inline]
514
pub fn translated(&self, translation: Vec3) -> Segment3d {
515
Self::new(self.point1() + translation, self.point2() + translation)
516
}
517
518
/// Compute the segment rotated around the origin by the given rotation.
519
#[inline]
520
pub fn rotated(&self, rotation: Quat) -> Segment3d {
521
Segment3d::new(rotation * self.point1(), rotation * self.point2())
522
}
523
524
/// Compute the segment rotated around the given point by the given rotation.
525
#[inline]
526
pub fn rotated_around(&self, rotation: Quat, point: Vec3) -> Segment3d {
527
// We offset our segment so that our segment is rotated as if from the origin, then we can apply the offset back
528
let offset = self.translated(-point);
529
let rotated = offset.rotated(rotation);
530
rotated.translated(point)
531
}
532
533
/// Compute the segment rotated around its own center.
534
#[inline]
535
pub fn rotated_around_center(&self, rotation: Quat) -> Segment3d {
536
self.rotated_around(rotation, self.center())
537
}
538
539
/// Compute the segment with its center at the origin, keeping the same direction and length.
540
#[inline]
541
pub fn centered(&self) -> Segment3d {
542
let center = self.center();
543
self.translated(-center)
544
}
545
546
/// Compute the segment with a new length, keeping the same direction and center.
547
#[inline]
548
pub fn resized(&self, length: f32) -> Segment3d {
549
let offset_from_origin = self.center();
550
let centered = self.translated(-offset_from_origin);
551
let ratio = length / self.length();
552
let segment = Segment3d::new(centered.point1() * ratio, centered.point2() * ratio);
553
segment.translated(offset_from_origin)
554
}
555
556
/// Reverses the direction of the line segment by swapping the endpoints.
557
#[inline]
558
pub fn reverse(&mut self) {
559
let [point1, point2] = &mut self.vertices;
560
core::mem::swap(point1, point2);
561
}
562
563
/// Returns the line segment with its direction reversed by swapping the endpoints.
564
#[inline]
565
#[must_use]
566
pub fn reversed(mut self) -> Self {
567
self.reverse();
568
self
569
}
570
571
/// Returns the point on the [`Segment3d`] that is closest to the specified `point`.
572
#[inline]
573
pub fn closest_point(&self, point: Vec3) -> Vec3 {
574
// `point`
575
// x
576
// ^|
577
// / |
578
//`offset`/ |
579
// / | `segment_vector`
580
// x----.-------------->x
581
// 0 t 1
582
let segment_vector = self.vertices[1] - self.vertices[0];
583
let offset = point - self.vertices[0];
584
// The signed projection of `offset` onto `segment_vector`, scaled by the length of the segment.
585
let projection_scaled = segment_vector.dot(offset);
586
587
// `point` is too far "left" in the picture
588
if projection_scaled <= 0.0 {
589
return self.vertices[0];
590
}
591
592
let length_squared = segment_vector.length_squared();
593
// `point` is too far "right" in the picture
594
if projection_scaled >= length_squared {
595
return self.vertices[1];
596
}
597
598
// Point lies somewhere in the middle, we compute the closest point by finding the parameter along the line.
599
let t = projection_scaled / length_squared;
600
self.vertices[0] + t * segment_vector
601
}
602
}
603
604
impl From<[Vec3; 2]> for Segment3d {
605
#[inline]
606
fn from(vertices: [Vec3; 2]) -> Self {
607
Self { vertices }
608
}
609
}
610
611
impl From<(Vec3, Vec3)> for Segment3d {
612
#[inline]
613
fn from((point1, point2): (Vec3, Vec3)) -> Self {
614
Self::new(point1, point2)
615
}
616
}
617
618
/// A series of connected line segments in 3D space.
619
#[cfg(feature = "alloc")]
620
#[derive(Clone, Debug, PartialEq)]
621
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
622
#[cfg_attr(
623
feature = "bevy_reflect",
624
derive(Reflect),
625
reflect(Debug, PartialEq, Clone)
626
)]
627
#[cfg_attr(
628
all(feature = "serialize", feature = "bevy_reflect"),
629
reflect(Serialize, Deserialize)
630
)]
631
pub struct Polyline3d {
632
/// The vertices of the polyline
633
pub vertices: Vec<Vec3>,
634
}
635
636
#[cfg(feature = "alloc")]
637
impl Primitive3d for Polyline3d {}
638
639
#[cfg(feature = "alloc")]
640
impl FromIterator<Vec3> for Polyline3d {
641
fn from_iter<I: IntoIterator<Item = Vec3>>(iter: I) -> Self {
642
Self {
643
vertices: iter.into_iter().collect(),
644
}
645
}
646
}
647
648
#[cfg(feature = "alloc")]
649
impl Default for Polyline3d {
650
fn default() -> Self {
651
Self::new([Vec3::new(-0.5, 0.0, 0.0), Vec3::new(0.5, 0.0, 0.0)])
652
}
653
}
654
655
#[cfg(feature = "alloc")]
656
impl Polyline3d {
657
/// Create a new `Polyline3d` from its vertices
658
pub fn new(vertices: impl IntoIterator<Item = Vec3>) -> Self {
659
Self::from_iter(vertices)
660
}
661
662
/// Create a new `Polyline3d` from two endpoints with subdivision points.
663
/// `subdivisions = 0` creates a simple line with just start and end points.
664
/// `subdivisions = 1` adds one point in the middle, creating 2 segments, etc.
665
pub fn with_subdivisions(start: Vec3, end: Vec3, subdivisions: usize) -> Self {
666
let total_vertices = subdivisions + 2;
667
let mut vertices = Vec::with_capacity(total_vertices);
668
669
let step = (end - start) / (subdivisions + 1) as f32;
670
for i in 0..total_vertices {
671
vertices.push(start + step * i as f32);
672
}
673
674
Self { vertices }
675
}
676
}
677
678
/// A cuboid primitive, which is like a cube, except that the x, y, and z dimensions are not
679
/// required to be the same.
680
#[derive(Clone, Copy, Debug, PartialEq)]
681
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
682
#[cfg_attr(
683
feature = "bevy_reflect",
684
derive(Reflect),
685
reflect(Debug, PartialEq, Default, Clone)
686
)]
687
#[cfg_attr(
688
all(feature = "serialize", feature = "bevy_reflect"),
689
reflect(Serialize, Deserialize)
690
)]
691
pub struct Cuboid {
692
/// Half of the width, height and depth of the cuboid
693
pub half_size: Vec3,
694
}
695
696
impl Primitive3d for Cuboid {}
697
698
impl Default for Cuboid {
699
/// Returns the default [`Cuboid`] with a width, height, and depth of `1.0`.
700
fn default() -> Self {
701
Self {
702
half_size: Vec3::splat(0.5),
703
}
704
}
705
}
706
707
impl Cuboid {
708
/// Create a new `Cuboid` from a full x, y, and z length
709
#[inline]
710
pub const fn new(x_length: f32, y_length: f32, z_length: f32) -> Self {
711
Self::from_size(Vec3::new(x_length, y_length, z_length))
712
}
713
714
/// Create a new `Cuboid` from a given full size
715
#[inline]
716
pub const fn from_size(size: Vec3) -> Self {
717
Self {
718
half_size: Vec3::new(size.x / 2.0, size.y / 2.0, size.z / 2.0),
719
}
720
}
721
722
/// Create a new `Cuboid` from two corner points
723
#[inline]
724
pub fn from_corners(point1: Vec3, point2: Vec3) -> Self {
725
Self {
726
half_size: (point2 - point1).abs() / 2.0,
727
}
728
}
729
730
/// Create a `Cuboid` from a single length.
731
/// The resulting `Cuboid` will be the same size in every direction.
732
#[inline]
733
pub const fn from_length(length: f32) -> Self {
734
Self {
735
half_size: Vec3::splat(length / 2.0),
736
}
737
}
738
739
/// Get the size of the cuboid
740
#[inline]
741
pub fn size(&self) -> Vec3 {
742
2.0 * self.half_size
743
}
744
745
/// Finds the point on the cuboid that is closest to the given `point`.
746
///
747
/// If the point is outside the cuboid, the returned point will be on the surface of the cuboid.
748
/// Otherwise, it will be inside the cuboid and returned as is.
749
#[inline]
750
pub fn closest_point(&self, point: Vec3) -> Vec3 {
751
// Clamp point coordinates to the cuboid
752
point.clamp(-self.half_size, self.half_size)
753
}
754
}
755
756
impl Measured3d for Cuboid {
757
/// Get the surface area of the cuboid
758
#[inline]
759
fn area(&self) -> f32 {
760
8.0 * (self.half_size.x * self.half_size.y
761
+ self.half_size.y * self.half_size.z
762
+ self.half_size.x * self.half_size.z)
763
}
764
765
/// Get the volume of the cuboid
766
#[inline]
767
fn volume(&self) -> f32 {
768
8.0 * self.half_size.x * self.half_size.y * self.half_size.z
769
}
770
}
771
772
/// A cylinder primitive centered on the origin
773
#[derive(Clone, Copy, Debug, PartialEq)]
774
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
775
#[cfg_attr(
776
feature = "bevy_reflect",
777
derive(Reflect),
778
reflect(Debug, PartialEq, Default, Clone)
779
)]
780
#[cfg_attr(
781
all(feature = "serialize", feature = "bevy_reflect"),
782
reflect(Serialize, Deserialize)
783
)]
784
pub struct Cylinder {
785
/// The radius of the cylinder
786
pub radius: f32,
787
/// The half height of the cylinder
788
pub half_height: f32,
789
}
790
791
impl Primitive3d for Cylinder {}
792
793
impl Default for Cylinder {
794
/// Returns the default [`Cylinder`] with a radius of `0.5` and a height of `1.0`.
795
fn default() -> Self {
796
Self {
797
radius: 0.5,
798
half_height: 0.5,
799
}
800
}
801
}
802
803
impl Cylinder {
804
/// Create a new `Cylinder` from a radius and full height
805
#[inline]
806
pub const fn new(radius: f32, height: f32) -> Self {
807
Self {
808
radius,
809
half_height: height / 2.0,
810
}
811
}
812
813
/// Get the base of the cylinder as a [`Circle`]
814
#[inline]
815
pub const fn base(&self) -> Circle {
816
Circle {
817
radius: self.radius,
818
}
819
}
820
821
/// Get the surface area of the side of the cylinder,
822
/// also known as the lateral area
823
#[inline]
824
#[doc(alias = "side_area")]
825
pub const fn lateral_area(&self) -> f32 {
826
4.0 * PI * self.radius * self.half_height
827
}
828
829
/// Get the surface area of one base of the cylinder
830
#[inline]
831
pub fn base_area(&self) -> f32 {
832
PI * self.radius.squared()
833
}
834
}
835
836
impl Measured3d for Cylinder {
837
/// Get the total surface area of the cylinder
838
#[inline]
839
fn area(&self) -> f32 {
840
2.0 * PI * self.radius * (self.radius + 2.0 * self.half_height)
841
}
842
843
/// Get the volume of the cylinder
844
#[inline]
845
fn volume(&self) -> f32 {
846
self.base_area() * 2.0 * self.half_height
847
}
848
}
849
850
/// A 3D capsule primitive centered on the origin
851
/// A three-dimensional capsule is defined as a surface at a distance (radius) from a line
852
#[derive(Clone, Copy, Debug, PartialEq)]
853
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
854
#[cfg_attr(
855
feature = "bevy_reflect",
856
derive(Reflect),
857
reflect(Debug, PartialEq, Default, Clone)
858
)]
859
#[cfg_attr(
860
all(feature = "serialize", feature = "bevy_reflect"),
861
reflect(Serialize, Deserialize)
862
)]
863
pub struct Capsule3d {
864
/// The radius of the capsule
865
pub radius: f32,
866
/// Half the height of the capsule, excluding the hemispheres
867
pub half_length: f32,
868
}
869
870
impl Primitive3d for Capsule3d {}
871
872
impl Default for Capsule3d {
873
/// Returns the default [`Capsule3d`] with a radius of `0.5` and a segment length of `1.0`.
874
/// The total height is `2.0`.
875
fn default() -> Self {
876
Self {
877
radius: 0.5,
878
half_length: 0.5,
879
}
880
}
881
}
882
883
impl Capsule3d {
884
/// Create a new `Capsule3d` from a radius and length
885
pub const fn new(radius: f32, length: f32) -> Self {
886
Self {
887
radius,
888
half_length: length / 2.0,
889
}
890
}
891
892
/// Get the part connecting the hemispherical ends
893
/// of the capsule as a [`Cylinder`]
894
#[inline]
895
pub const fn to_cylinder(&self) -> Cylinder {
896
Cylinder {
897
radius: self.radius,
898
half_height: self.half_length,
899
}
900
}
901
}
902
903
impl Measured3d for Capsule3d {
904
/// Get the surface area of the capsule
905
#[inline]
906
fn area(&self) -> f32 {
907
// Modified version of 2pi * r * (2r + h)
908
4.0 * PI * self.radius * (self.radius + self.half_length)
909
}
910
911
/// Get the volume of the capsule
912
#[inline]
913
fn volume(&self) -> f32 {
914
// Modified version of pi * r^2 * (4/3 * r + a)
915
let diameter = self.radius * 2.0;
916
PI * self.radius * diameter * (diameter / 3.0 + self.half_length)
917
}
918
}
919
920
/// A cone primitive centered on the midpoint between the tip of the cone and the center of its base.
921
///
922
/// The cone is oriented with its tip pointing towards the Y axis.
923
#[derive(Clone, Copy, Debug, PartialEq)]
924
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
925
#[cfg_attr(
926
feature = "bevy_reflect",
927
derive(Reflect),
928
reflect(Debug, PartialEq, Default, Clone)
929
)]
930
#[cfg_attr(
931
all(feature = "serialize", feature = "bevy_reflect"),
932
reflect(Serialize, Deserialize)
933
)]
934
pub struct Cone {
935
/// The radius of the base
936
pub radius: f32,
937
/// The height of the cone
938
pub height: f32,
939
}
940
941
impl Primitive3d for Cone {}
942
943
impl Default for Cone {
944
/// Returns the default [`Cone`] with a base radius of `0.5` and a height of `1.0`.
945
fn default() -> Self {
946
Self {
947
radius: 0.5,
948
height: 1.0,
949
}
950
}
951
}
952
953
impl Cone {
954
/// Create a new [`Cone`] from a radius and height.
955
pub const fn new(radius: f32, height: f32) -> Self {
956
Self { radius, height }
957
}
958
/// Get the base of the cone as a [`Circle`]
959
#[inline]
960
pub const fn base(&self) -> Circle {
961
Circle {
962
radius: self.radius,
963
}
964
}
965
966
/// Get the slant height of the cone, the length of the line segment
967
/// connecting a point on the base to the apex
968
#[inline]
969
#[doc(alias = "side_length")]
970
pub fn slant_height(&self) -> f32 {
971
ops::hypot(self.radius, self.height)
972
}
973
974
/// Get the surface area of the side of the cone,
975
/// also known as the lateral area
976
#[inline]
977
#[doc(alias = "side_area")]
978
pub fn lateral_area(&self) -> f32 {
979
PI * self.radius * self.slant_height()
980
}
981
982
/// Get the surface area of the base of the cone
983
#[inline]
984
pub fn base_area(&self) -> f32 {
985
PI * self.radius.squared()
986
}
987
}
988
989
impl Measured3d for Cone {
990
/// Get the total surface area of the cone
991
#[inline]
992
fn area(&self) -> f32 {
993
self.base_area() + self.lateral_area()
994
}
995
996
/// Get the volume of the cone
997
#[inline]
998
fn volume(&self) -> f32 {
999
(self.base_area() * self.height) / 3.0
1000
}
1001
}
1002
1003
/// A conical frustum primitive.
1004
/// A conical frustum can be created
1005
/// by slicing off a section of a cone.
1006
#[derive(Clone, Copy, Debug, PartialEq)]
1007
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
1008
#[cfg_attr(
1009
feature = "bevy_reflect",
1010
derive(Reflect),
1011
reflect(Debug, PartialEq, Default, Clone)
1012
)]
1013
#[cfg_attr(
1014
all(feature = "serialize", feature = "bevy_reflect"),
1015
reflect(Serialize, Deserialize)
1016
)]
1017
pub struct ConicalFrustum {
1018
/// The radius of the top of the frustum
1019
pub radius_top: f32,
1020
/// The radius of the base of the frustum
1021
pub radius_bottom: f32,
1022
/// The height of the frustum
1023
pub height: f32,
1024
}
1025
1026
impl Primitive3d for ConicalFrustum {}
1027
1028
impl Default for ConicalFrustum {
1029
/// Returns the default [`ConicalFrustum`] with a top radius of `0.25`, bottom radius of `0.5`, and a height of `0.5`.
1030
fn default() -> Self {
1031
Self {
1032
radius_top: 0.25,
1033
radius_bottom: 0.5,
1034
height: 0.5,
1035
}
1036
}
1037
}
1038
1039
impl ConicalFrustum {
1040
/// Get the bottom base of the conical frustum as a [`Circle`]
1041
#[inline]
1042
pub const fn bottom_base(&self) -> Circle {
1043
Circle {
1044
radius: self.radius_bottom,
1045
}
1046
}
1047
1048
/// Get the top base of the conical frustum as a [`Circle`]
1049
#[inline]
1050
pub const fn top_base(&self) -> Circle {
1051
Circle {
1052
radius: self.radius_top,
1053
}
1054
}
1055
1056
/// Get the slant height of the conical frustum, the length of the line segment
1057
/// connecting a point on the base to the closest point on the top
1058
#[inline]
1059
#[doc(alias = "side_length")]
1060
pub fn slant_height(&self) -> f32 {
1061
ops::hypot(self.radius_bottom - self.radius_top, self.height)
1062
}
1063
1064
/// Get the surface area of the side of the conical frustum,
1065
/// also known as the lateral area
1066
#[inline]
1067
#[doc(alias = "side_area")]
1068
pub fn lateral_area(&self) -> f32 {
1069
PI * (self.radius_bottom + self.radius_top) * self.slant_height()
1070
}
1071
1072
/// Get the surface area of the bottom base of the conical frustum
1073
#[inline]
1074
pub fn bottom_base_area(&self) -> f32 {
1075
PI * self.radius_bottom.squared()
1076
}
1077
1078
/// Get the surface area of the top base of the conical frustum
1079
#[inline]
1080
pub fn top_base_area(&self) -> f32 {
1081
PI * self.radius_top.squared()
1082
}
1083
}
1084
1085
impl Measured3d for ConicalFrustum {
1086
#[inline]
1087
fn volume(&self) -> f32 {
1088
FRAC_PI_3
1089
* self.height
1090
* (self.radius_bottom * self.radius_bottom
1091
+ self.radius_top * self.radius_top
1092
+ self.radius_top * self.radius_bottom)
1093
}
1094
#[inline]
1095
fn area(&self) -> f32 {
1096
self.bottom_base_area() + self.top_base_area() + self.lateral_area()
1097
}
1098
}
1099
1100
/// The type of torus determined by the minor and major radii
1101
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1102
pub enum TorusKind {
1103
/// A torus that has a ring.
1104
/// The major radius is greater than the minor radius
1105
Ring,
1106
/// A torus that has no hole but also doesn't intersect itself.
1107
/// The major radius is equal to the minor radius
1108
Horn,
1109
/// A self-intersecting torus.
1110
/// The major radius is less than the minor radius
1111
Spindle,
1112
/// A torus with non-geometric properties like
1113
/// a minor or major radius that is non-positive,
1114
/// infinite, or `NaN`
1115
Invalid,
1116
}
1117
1118
/// A torus primitive, often representing a ring or donut shape
1119
/// The set of points some distance from a circle centered at the origin
1120
#[derive(Clone, Copy, Debug, PartialEq)]
1121
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
1122
#[cfg_attr(
1123
feature = "bevy_reflect",
1124
derive(Reflect),
1125
reflect(Debug, PartialEq, Default, Clone)
1126
)]
1127
#[cfg_attr(
1128
all(feature = "serialize", feature = "bevy_reflect"),
1129
reflect(Serialize, Deserialize)
1130
)]
1131
pub struct Torus {
1132
/// The radius of the tube of the torus
1133
#[doc(
1134
alias = "ring_radius",
1135
alias = "tube_radius",
1136
alias = "cross_section_radius"
1137
)]
1138
pub minor_radius: f32,
1139
/// The distance from the center of the torus to the center of the tube
1140
#[doc(alias = "radius_of_revolution")]
1141
pub major_radius: f32,
1142
}
1143
1144
impl Primitive3d for Torus {}
1145
1146
impl Default for Torus {
1147
/// Returns the default [`Torus`] with a minor radius of `0.25` and a major radius of `0.75`.
1148
fn default() -> Self {
1149
Self {
1150
minor_radius: 0.25,
1151
major_radius: 0.75,
1152
}
1153
}
1154
}
1155
1156
impl Torus {
1157
/// Create a new `Torus` from an inner and outer radius.
1158
///
1159
/// The inner radius is the radius of the hole, and the outer radius
1160
/// is the radius of the entire object
1161
#[inline]
1162
pub const fn new(inner_radius: f32, outer_radius: f32) -> Self {
1163
let minor_radius = (outer_radius - inner_radius) / 2.0;
1164
let major_radius = outer_radius - minor_radius;
1165
1166
Self {
1167
minor_radius,
1168
major_radius,
1169
}
1170
}
1171
1172
/// Get the inner radius of the torus.
1173
/// For a ring torus, this corresponds to the radius of the hole,
1174
/// or `major_radius - minor_radius`
1175
#[inline]
1176
pub const fn inner_radius(&self) -> f32 {
1177
self.major_radius - self.minor_radius
1178
}
1179
1180
/// Get the outer radius of the torus.
1181
/// This corresponds to the overall radius of the entire object,
1182
/// or `major_radius + minor_radius`
1183
#[inline]
1184
pub const fn outer_radius(&self) -> f32 {
1185
self.major_radius + self.minor_radius
1186
}
1187
1188
/// Get the [`TorusKind`] determined by the minor and major radii.
1189
///
1190
/// The torus can either be a *ring torus* that has a hole,
1191
/// a *horn torus* that doesn't have a hole but also isn't self-intersecting,
1192
/// or a *spindle torus* that is self-intersecting.
1193
///
1194
/// If the minor or major radius is non-positive, infinite, or `NaN`,
1195
/// [`TorusKind::Invalid`] is returned
1196
#[inline]
1197
pub fn kind(&self) -> TorusKind {
1198
// Invalid if minor or major radius is non-positive, infinite, or NaN
1199
if self.minor_radius <= 0.0
1200
|| !self.minor_radius.is_finite()
1201
|| self.major_radius <= 0.0
1202
|| !self.major_radius.is_finite()
1203
{
1204
return TorusKind::Invalid;
1205
}
1206
1207
match self.major_radius.partial_cmp(&self.minor_radius).unwrap() {
1208
core::cmp::Ordering::Greater => TorusKind::Ring,
1209
core::cmp::Ordering::Equal => TorusKind::Horn,
1210
core::cmp::Ordering::Less => TorusKind::Spindle,
1211
}
1212
}
1213
}
1214
1215
impl Measured3d for Torus {
1216
/// Get the surface area of the torus. Note that this only produces
1217
/// the expected result when the torus has a ring and isn't self-intersecting
1218
#[inline]
1219
fn area(&self) -> f32 {
1220
4.0 * PI.squared() * self.major_radius * self.minor_radius
1221
}
1222
1223
/// Get the volume of the torus. Note that this only produces
1224
/// the expected result when the torus has a ring and isn't self-intersecting
1225
#[inline]
1226
fn volume(&self) -> f32 {
1227
2.0 * PI.squared() * self.major_radius * self.minor_radius.squared()
1228
}
1229
}
1230
1231
/// A 3D triangle primitive.
1232
#[derive(Clone, Copy, Debug, PartialEq)]
1233
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
1234
#[cfg_attr(
1235
feature = "bevy_reflect",
1236
derive(Reflect),
1237
reflect(Debug, PartialEq, Default, Clone)
1238
)]
1239
#[cfg_attr(
1240
all(feature = "serialize", feature = "bevy_reflect"),
1241
reflect(Serialize, Deserialize)
1242
)]
1243
pub struct Triangle3d {
1244
/// The vertices of the triangle.
1245
pub vertices: [Vec3; 3],
1246
}
1247
1248
impl Primitive3d for Triangle3d {}
1249
1250
impl Default for Triangle3d {
1251
/// Returns the default [`Triangle3d`] with the vertices `[0.0, 0.5, 0.0]`, `[-0.5, -0.5, 0.0]`, and `[0.5, -0.5, 0.0]`.
1252
fn default() -> Self {
1253
Self {
1254
vertices: [
1255
Vec3::new(0.0, 0.5, 0.0),
1256
Vec3::new(-0.5, -0.5, 0.0),
1257
Vec3::new(0.5, -0.5, 0.0),
1258
],
1259
}
1260
}
1261
}
1262
1263
impl Triangle3d {
1264
/// Create a new [`Triangle3d`] from points `a`, `b`, and `c`.
1265
#[inline]
1266
pub const fn new(a: Vec3, b: Vec3, c: Vec3) -> Self {
1267
Self {
1268
vertices: [a, b, c],
1269
}
1270
}
1271
1272
/// Get the normal of the triangle in the direction of the right-hand rule, assuming
1273
/// the vertices are ordered in a counter-clockwise direction.
1274
///
1275
/// The normal is computed as the cross product of the vectors `ab` and `ac`.
1276
///
1277
/// # Errors
1278
///
1279
/// Returns [`Err(InvalidDirectionError)`](InvalidDirectionError) if the length
1280
/// of the given vector is zero (or very close to zero), infinite, or `NaN`.
1281
#[inline]
1282
pub fn normal(&self) -> Result<Dir3, InvalidDirectionError> {
1283
let [a, b, c] = self.vertices;
1284
let ab = b - a;
1285
let ac = c - a;
1286
Dir3::new(ab.cross(ac))
1287
}
1288
1289
/// Checks if the triangle is degenerate, meaning it has zero area.
1290
///
1291
/// A triangle is degenerate if the cross product of the vectors `ab` and `ac` has a length less than `10e-7`.
1292
/// This indicates that the three vertices are collinear or nearly collinear.
1293
#[inline]
1294
pub fn is_degenerate(&self) -> bool {
1295
let [a, b, c] = self.vertices;
1296
let ab = b - a;
1297
let ac = c - a;
1298
ab.cross(ac).length() < 10e-7
1299
}
1300
1301
/// Checks if the triangle is acute, meaning all angles are less than 90 degrees
1302
#[inline]
1303
pub fn is_acute(&self) -> bool {
1304
let [a, b, c] = self.vertices;
1305
let ab = b - a;
1306
let bc = c - b;
1307
let ca = a - c;
1308
1309
// a^2 + b^2 < c^2 for an acute triangle
1310
let side_lengths = [
1311
ab.length_squared(),
1312
bc.length_squared(),
1313
ca.length_squared(),
1314
];
1315
let sum = side_lengths[0] + side_lengths[1] + side_lengths[2];
1316
let max = side_lengths[0].max(side_lengths[1]).max(side_lengths[2]);
1317
sum - max > max
1318
}
1319
1320
/// Checks if the triangle is obtuse, meaning one angle is greater than 90 degrees
1321
#[inline]
1322
pub fn is_obtuse(&self) -> bool {
1323
let [a, b, c] = self.vertices;
1324
let ab = b - a;
1325
let bc = c - b;
1326
let ca = a - c;
1327
1328
// a^2 + b^2 > c^2 for an obtuse triangle
1329
let side_lengths = [
1330
ab.length_squared(),
1331
bc.length_squared(),
1332
ca.length_squared(),
1333
];
1334
let sum = side_lengths[0] + side_lengths[1] + side_lengths[2];
1335
let max = side_lengths[0].max(side_lengths[1]).max(side_lengths[2]);
1336
sum - max < max
1337
}
1338
1339
/// Reverse the triangle by swapping the first and last vertices.
1340
#[inline]
1341
pub fn reverse(&mut self) {
1342
self.vertices.swap(0, 2);
1343
}
1344
1345
/// This triangle but reversed.
1346
#[inline]
1347
#[must_use]
1348
pub fn reversed(mut self) -> Triangle3d {
1349
self.reverse();
1350
self
1351
}
1352
1353
/// Get the centroid of the triangle.
1354
///
1355
/// This function finds the geometric center of the triangle by averaging the vertices:
1356
/// `centroid = (a + b + c) / 3`.
1357
#[doc(alias("center", "barycenter", "baricenter"))]
1358
#[inline]
1359
pub fn centroid(&self) -> Vec3 {
1360
(self.vertices[0] + self.vertices[1] + self.vertices[2]) / 3.0
1361
}
1362
1363
/// Get the largest side of the triangle.
1364
///
1365
/// Returns the two points that form the largest side of the triangle.
1366
#[inline]
1367
pub fn largest_side(&self) -> (Vec3, Vec3) {
1368
let [a, b, c] = self.vertices;
1369
let ab = b - a;
1370
let bc = c - b;
1371
let ca = a - c;
1372
1373
let mut largest_side_points = (a, b);
1374
let mut largest_side_length = ab.length_squared();
1375
1376
let bc_length = bc.length_squared();
1377
if bc_length > largest_side_length {
1378
largest_side_points = (b, c);
1379
largest_side_length = bc_length;
1380
}
1381
1382
let ca_length = ca.length_squared();
1383
if ca_length > largest_side_length {
1384
largest_side_points = (a, c);
1385
}
1386
1387
largest_side_points
1388
}
1389
1390
/// Get the circumcenter of the triangle.
1391
#[inline]
1392
pub fn circumcenter(&self) -> Vec3 {
1393
if self.is_degenerate() {
1394
// If the triangle is degenerate, the circumcenter is the midpoint of the largest side.
1395
let (p1, p2) = self.largest_side();
1396
return (p1 + p2) / 2.0;
1397
}
1398
1399
let [a, b, c] = self.vertices;
1400
let ab = b - a;
1401
let ac = c - a;
1402
let n = ab.cross(ac);
1403
1404
// Reference: https://gamedev.stackexchange.com/questions/60630/how-do-i-find-the-circumcenter-of-a-triangle-in-3d
1405
a + ((ac.length_squared() * n.cross(ab) + ab.length_squared() * ac.cross(ab).cross(ac))
1406
/ (2.0 * n.length_squared()))
1407
}
1408
}
1409
1410
impl Measured2d for Triangle3d {
1411
/// Get the area of the triangle.
1412
#[inline]
1413
fn area(&self) -> f32 {
1414
let [a, b, c] = self.vertices;
1415
let ab = b - a;
1416
let ac = c - a;
1417
ab.cross(ac).length() / 2.0
1418
}
1419
1420
/// Get the perimeter of the triangle.
1421
#[inline]
1422
fn perimeter(&self) -> f32 {
1423
let [a, b, c] = self.vertices;
1424
a.distance(b) + b.distance(c) + c.distance(a)
1425
}
1426
}
1427
1428
/// A tetrahedron primitive.
1429
#[derive(Clone, Copy, Debug, PartialEq)]
1430
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
1431
#[cfg_attr(
1432
feature = "bevy_reflect",
1433
derive(Reflect),
1434
reflect(Debug, PartialEq, Default, Clone)
1435
)]
1436
#[cfg_attr(
1437
all(feature = "serialize", feature = "bevy_reflect"),
1438
reflect(Serialize, Deserialize)
1439
)]
1440
pub struct Tetrahedron {
1441
/// The vertices of the tetrahedron.
1442
pub vertices: [Vec3; 4],
1443
}
1444
1445
impl Primitive3d for Tetrahedron {}
1446
1447
impl Default for Tetrahedron {
1448
/// Returns the default [`Tetrahedron`] with the vertices
1449
/// `[0.5, 0.5, 0.5]`, `[-0.5, 0.5, -0.5]`, `[-0.5, -0.5, 0.5]` and `[0.5, -0.5, -0.5]`.
1450
fn default() -> Self {
1451
Self {
1452
vertices: [
1453
Vec3::new(0.5, 0.5, 0.5),
1454
Vec3::new(-0.5, 0.5, -0.5),
1455
Vec3::new(-0.5, -0.5, 0.5),
1456
Vec3::new(0.5, -0.5, -0.5),
1457
],
1458
}
1459
}
1460
}
1461
1462
impl Tetrahedron {
1463
/// Create a new [`Tetrahedron`] from points `a`, `b`, `c` and `d`.
1464
#[inline]
1465
pub const fn new(a: Vec3, b: Vec3, c: Vec3, d: Vec3) -> Self {
1466
Self {
1467
vertices: [a, b, c, d],
1468
}
1469
}
1470
1471
/// Get the signed volume of the tetrahedron.
1472
///
1473
/// If it's negative, the normal vector of the face defined by
1474
/// the first three points using the right-hand rule points
1475
/// away from the fourth vertex.
1476
#[inline]
1477
pub fn signed_volume(&self) -> f32 {
1478
let [a, b, c, d] = self.vertices;
1479
let ab = b - a;
1480
let ac = c - a;
1481
let ad = d - a;
1482
Mat3::from_cols(ab, ac, ad).determinant() / 6.0
1483
}
1484
1485
/// Get the centroid of the tetrahedron.
1486
///
1487
/// This function finds the geometric center of the tetrahedron
1488
/// by averaging the vertices: `centroid = (a + b + c + d) / 4`.
1489
#[doc(alias("center", "barycenter", "baricenter"))]
1490
#[inline]
1491
pub fn centroid(&self) -> Vec3 {
1492
(self.vertices[0] + self.vertices[1] + self.vertices[2] + self.vertices[3]) / 4.0
1493
}
1494
1495
/// Get the triangles that form the faces of this tetrahedron.
1496
///
1497
/// Note that the orientations of the faces are determined by that of the tetrahedron; if the
1498
/// signed volume of this tetrahedron is positive, then the triangles' normals will point
1499
/// outward, and if the signed volume is negative they will point inward.
1500
#[inline]
1501
pub fn faces(&self) -> [Triangle3d; 4] {
1502
let [a, b, c, d] = self.vertices;
1503
[
1504
Triangle3d::new(b, c, d),
1505
Triangle3d::new(a, c, d).reversed(),
1506
Triangle3d::new(a, b, d),
1507
Triangle3d::new(a, b, c).reversed(),
1508
]
1509
}
1510
}
1511
1512
impl Measured3d for Tetrahedron {
1513
/// Get the surface area of the tetrahedron.
1514
#[inline]
1515
fn area(&self) -> f32 {
1516
let [a, b, c, d] = self.vertices;
1517
let ab = b - a;
1518
let ac = c - a;
1519
let ad = d - a;
1520
let bc = c - b;
1521
let bd = d - b;
1522
(ab.cross(ac).length()
1523
+ ab.cross(ad).length()
1524
+ ac.cross(ad).length()
1525
+ bc.cross(bd).length())
1526
/ 2.0
1527
}
1528
1529
/// Get the volume of the tetrahedron.
1530
#[inline]
1531
fn volume(&self) -> f32 {
1532
ops::abs(self.signed_volume())
1533
}
1534
}
1535
1536
/// A 3D shape representing an extruded 2D `base_shape`.
1537
///
1538
/// Extruding a shape effectively "thickens" a 2D shapes,
1539
/// creating a shape with the same cross-section over the entire depth.
1540
///
1541
/// The resulting volumes are prisms.
1542
/// For example, a triangle becomes a triangular prism, while a circle becomes a cylinder.
1543
#[doc(alias = "Prism")]
1544
#[derive(Clone, Copy, Debug, PartialEq)]
1545
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
1546
pub struct Extrusion<T: Primitive2d> {
1547
/// The base shape of the extrusion
1548
pub base_shape: T,
1549
/// Half of the depth of the extrusion
1550
pub half_depth: f32,
1551
}
1552
1553
impl<T: Primitive2d> Primitive3d for Extrusion<T> {}
1554
1555
impl<T: Primitive2d> Extrusion<T> {
1556
/// Create a new `Extrusion<T>` from a given `base_shape` and `depth`
1557
pub fn new(base_shape: T, depth: f32) -> Self {
1558
Self {
1559
base_shape,
1560
half_depth: depth / 2.,
1561
}
1562
}
1563
}
1564
1565
impl<T: Primitive2d + Measured2d> Measured3d for Extrusion<T> {
1566
/// Get the surface area of the extrusion
1567
fn area(&self) -> f32 {
1568
2. * (self.base_shape.area() + self.half_depth * self.base_shape.perimeter())
1569
}
1570
1571
/// Get the volume of the extrusion
1572
fn volume(&self) -> f32 {
1573
2. * self.base_shape.area() * self.half_depth
1574
}
1575
}
1576
1577
#[cfg(test)]
1578
mod tests {
1579
// Reference values were computed by hand and/or with external tools
1580
1581
use super::*;
1582
use crate::{InvalidDirectionError, Quat};
1583
use approx::assert_relative_eq;
1584
1585
#[test]
1586
fn direction_creation() {
1587
assert_eq!(Dir3::new(Vec3::X * 12.5), Ok(Dir3::X));
1588
assert_eq!(
1589
Dir3::new(Vec3::new(0.0, 0.0, 0.0)),
1590
Err(InvalidDirectionError::Zero)
1591
);
1592
assert_eq!(
1593
Dir3::new(Vec3::new(f32::INFINITY, 0.0, 0.0)),
1594
Err(InvalidDirectionError::Infinite)
1595
);
1596
assert_eq!(
1597
Dir3::new(Vec3::new(f32::NEG_INFINITY, 0.0, 0.0)),
1598
Err(InvalidDirectionError::Infinite)
1599
);
1600
assert_eq!(
1601
Dir3::new(Vec3::new(f32::NAN, 0.0, 0.0)),
1602
Err(InvalidDirectionError::NaN)
1603
);
1604
assert_eq!(Dir3::new_and_length(Vec3::X * 6.5), Ok((Dir3::X, 6.5)));
1605
1606
// Test rotation
1607
assert!(
1608
(Quat::from_rotation_z(core::f32::consts::FRAC_PI_2) * Dir3::X)
1609
.abs_diff_eq(Vec3::Y, 10e-6)
1610
);
1611
}
1612
1613
#[test]
1614
fn cuboid_closest_point() {
1615
let cuboid = Cuboid::new(2.0, 2.0, 2.0);
1616
assert_eq!(cuboid.closest_point(Vec3::X * 10.0), Vec3::X);
1617
assert_eq!(cuboid.closest_point(Vec3::NEG_ONE * 10.0), Vec3::NEG_ONE);
1618
assert_eq!(
1619
cuboid.closest_point(Vec3::new(0.25, 0.1, 0.3)),
1620
Vec3::new(0.25, 0.1, 0.3)
1621
);
1622
}
1623
1624
#[test]
1625
fn sphere_closest_point() {
1626
let sphere = Sphere { radius: 1.0 };
1627
assert_eq!(sphere.closest_point(Vec3::X * 10.0), Vec3::X);
1628
assert_eq!(
1629
sphere.closest_point(Vec3::NEG_ONE * 10.0),
1630
Vec3::NEG_ONE.normalize()
1631
);
1632
assert_eq!(
1633
sphere.closest_point(Vec3::new(0.25, 0.1, 0.3)),
1634
Vec3::new(0.25, 0.1, 0.3)
1635
);
1636
}
1637
1638
#[test]
1639
fn segment_closest_point() {
1640
assert_eq!(
1641
Segment3d::new(Vec3::new(0.0, 0.0, 0.0), Vec3::new(3.0, 0.0, 0.0))
1642
.closest_point(Vec3::new(1.0, 6.0, -2.0)),
1643
Vec3::new(1.0, 0.0, 0.0)
1644
);
1645
1646
let segments = [
1647
Segment3d::new(Vec3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 0.0)),
1648
Segment3d::new(Vec3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0)),
1649
Segment3d::new(Vec3::new(1.0, 0.0, 2.0), Vec3::new(0.0, 1.0, -2.0)),
1650
Segment3d::new(
1651
Vec3::new(1.0, 0.0, 0.0),
1652
Vec3::new(1.0, 5.0 * f32::EPSILON, 0.0),
1653
),
1654
];
1655
let points = [
1656
Vec3::new(0.0, 0.0, 0.0),
1657
Vec3::new(1.0, 0.0, 0.0),
1658
Vec3::new(-1.0, 1.0, 2.0),
1659
Vec3::new(1.0, 1.0, 1.0),
1660
Vec3::new(-1.0, 0.0, 0.0),
1661
Vec3::new(5.0, -1.0, 0.5),
1662
Vec3::new(1.0, f32::EPSILON, 0.0),
1663
];
1664
1665
for point in points.iter() {
1666
for segment in segments.iter() {
1667
let closest = segment.closest_point(*point);
1668
assert!(
1669
point.distance_squared(closest) <= point.distance_squared(segment.point1()),
1670
"Closest point must always be at least as close as either vertex."
1671
);
1672
assert!(
1673
point.distance_squared(closest) <= point.distance_squared(segment.point2()),
1674
"Closest point must always be at least as close as either vertex."
1675
);
1676
assert!(
1677
point.distance_squared(closest) <= point.distance_squared(segment.center()),
1678
"Closest point must always be at least as close as the center."
1679
);
1680
let closest_to_closest = segment.closest_point(closest);
1681
// Closest point must already be on the segment
1682
assert_relative_eq!(closest_to_closest, closest);
1683
}
1684
}
1685
}
1686
1687
#[test]
1688
fn sphere_math() {
1689
let sphere = Sphere { radius: 4.0 };
1690
assert_eq!(sphere.diameter(), 8.0, "incorrect diameter");
1691
assert_eq!(sphere.area(), 201.06193, "incorrect area");
1692
assert_eq!(sphere.volume(), 268.08257, "incorrect volume");
1693
}
1694
1695
#[test]
1696
fn plane_from_points() {
1697
let (plane, translation) = Plane3d::from_points(Vec3::X, Vec3::Z, Vec3::NEG_X);
1698
assert_eq!(*plane.normal, Vec3::NEG_Y, "incorrect normal");
1699
assert_eq!(plane.half_size, Vec2::new(0.5, 0.5), "incorrect half size");
1700
assert_eq!(translation, Vec3::Z * 0.33333334, "incorrect translation");
1701
}
1702
1703
#[test]
1704
fn infinite_plane_math() {
1705
let (plane, origin) = InfinitePlane3d::from_points(Vec3::X, Vec3::Z, Vec3::NEG_X);
1706
assert_eq!(*plane.normal, Vec3::NEG_Y, "incorrect normal");
1707
assert_eq!(origin, Vec3::Z * 0.33333334, "incorrect translation");
1708
1709
let point_in_plane = Vec3::X + Vec3::Z;
1710
assert_eq!(
1711
plane.signed_distance(origin, point_in_plane),
1712
0.0,
1713
"incorrect distance"
1714
);
1715
assert_eq!(
1716
plane.project_point(origin, point_in_plane),
1717
point_in_plane,
1718
"incorrect point"
1719
);
1720
1721
let point_outside = Vec3::Y;
1722
assert_eq!(
1723
plane.signed_distance(origin, point_outside),
1724
-1.0,
1725
"incorrect distance"
1726
);
1727
assert_eq!(
1728
plane.project_point(origin, point_outside),
1729
Vec3::ZERO,
1730
"incorrect point"
1731
);
1732
1733
let point_outside = Vec3::NEG_Y;
1734
assert_eq!(
1735
plane.signed_distance(origin, point_outside),
1736
1.0,
1737
"incorrect distance"
1738
);
1739
assert_eq!(
1740
plane.project_point(origin, point_outside),
1741
Vec3::ZERO,
1742
"incorrect point"
1743
);
1744
1745
let area_f = |[a, b, c]: [Vec3; 3]| (a - b).cross(a - c).length() * 0.5;
1746
let (proj, inj) = plane.isometries_xy(origin);
1747
1748
let triangle = [Vec3::X, Vec3::Y, Vec3::ZERO];
1749
assert_eq!(area_f(triangle), 0.5, "incorrect area");
1750
1751
let triangle_proj = triangle.map(|vec3| proj * vec3);
1752
assert_relative_eq!(area_f(triangle_proj), 0.5);
1753
1754
let triangle_proj_inj = triangle_proj.map(|vec3| inj * vec3);
1755
assert_relative_eq!(area_f(triangle_proj_inj), 0.5);
1756
}
1757
1758
#[test]
1759
fn cuboid_math() {
1760
let cuboid = Cuboid::new(3.0, 7.0, 2.0);
1761
assert_eq!(
1762
cuboid,
1763
Cuboid::from_corners(Vec3::new(-1.5, -3.5, -1.0), Vec3::new(1.5, 3.5, 1.0)),
1764
"incorrect dimensions when created from corners"
1765
);
1766
assert_eq!(cuboid.area(), 82.0, "incorrect area");
1767
assert_eq!(cuboid.volume(), 42.0, "incorrect volume");
1768
}
1769
1770
#[test]
1771
fn cylinder_math() {
1772
let cylinder = Cylinder::new(2.0, 9.0);
1773
assert_eq!(
1774
cylinder.base(),
1775
Circle { radius: 2.0 },
1776
"base produces incorrect circle"
1777
);
1778
assert_eq!(
1779
cylinder.lateral_area(),
1780
113.097336,
1781
"incorrect lateral area"
1782
);
1783
assert_eq!(cylinder.base_area(), 12.566371, "incorrect base area");
1784
assert_relative_eq!(cylinder.area(), 138.23007);
1785
assert_eq!(cylinder.volume(), 113.097336, "incorrect volume");
1786
}
1787
1788
#[test]
1789
fn capsule_math() {
1790
let capsule = Capsule3d::new(2.0, 9.0);
1791
assert_eq!(
1792
capsule.to_cylinder(),
1793
Cylinder::new(2.0, 9.0),
1794
"cylinder wasn't created correctly from a capsule"
1795
);
1796
assert_eq!(capsule.area(), 163.36282, "incorrect area");
1797
assert_relative_eq!(capsule.volume(), 146.60765);
1798
}
1799
1800
#[test]
1801
fn cone_math() {
1802
let cone = Cone {
1803
radius: 2.0,
1804
height: 9.0,
1805
};
1806
assert_eq!(
1807
cone.base(),
1808
Circle { radius: 2.0 },
1809
"base produces incorrect circle"
1810
);
1811
assert_eq!(cone.slant_height(), 9.219544, "incorrect slant height");
1812
assert_eq!(cone.lateral_area(), 57.92811, "incorrect lateral area");
1813
assert_eq!(cone.base_area(), 12.566371, "incorrect base area");
1814
assert_relative_eq!(cone.area(), 70.49447);
1815
assert_eq!(cone.volume(), 37.699111, "incorrect volume");
1816
}
1817
1818
#[test]
1819
fn conical_frustum_math() {
1820
let frustum = ConicalFrustum {
1821
height: 9.0,
1822
radius_top: 1.0,
1823
radius_bottom: 2.0,
1824
};
1825
assert_eq!(
1826
frustum.bottom_base(),
1827
Circle { radius: 2.0 },
1828
"bottom base produces incorrect circle"
1829
);
1830
assert_eq!(
1831
frustum.top_base(),
1832
Circle { radius: 1.0 },
1833
"top base produces incorrect circle"
1834
);
1835
assert_eq!(frustum.slant_height(), 9.055386, "incorrect slant height");
1836
assert_eq!(frustum.lateral_area(), 85.345, "incorrect lateral area");
1837
assert_eq!(
1838
frustum.bottom_base_area(),
1839
12.566371,
1840
"incorrect bottom base area"
1841
);
1842
assert_eq!(frustum.top_base_area(), PI, "incorrect top base area");
1843
assert_eq!(frustum.area(), 101.05296, "incorrect surface area");
1844
assert_eq!(frustum.volume(), 65.97345, "incorrect volume");
1845
}
1846
1847
#[test]
1848
fn torus_math() {
1849
let torus = Torus {
1850
minor_radius: 0.3,
1851
major_radius: 2.8,
1852
};
1853
assert_eq!(torus.inner_radius(), 2.5, "incorrect inner radius");
1854
assert_eq!(torus.outer_radius(), 3.1, "incorrect outer radius");
1855
assert_eq!(torus.kind(), TorusKind::Ring, "incorrect torus kind");
1856
assert_eq!(
1857
Torus::new(0.0, 1.0).kind(),
1858
TorusKind::Horn,
1859
"incorrect torus kind"
1860
);
1861
assert_eq!(
1862
Torus::new(-0.5, 1.0).kind(),
1863
TorusKind::Spindle,
1864
"incorrect torus kind"
1865
);
1866
assert_eq!(
1867
Torus::new(1.5, 1.0).kind(),
1868
TorusKind::Invalid,
1869
"torus should be invalid"
1870
);
1871
assert_relative_eq!(torus.area(), 33.16187);
1872
assert_relative_eq!(torus.volume(), 4.97428, epsilon = 0.00001);
1873
}
1874
1875
#[test]
1876
fn tetrahedron_math() {
1877
let tetrahedron = Tetrahedron {
1878
vertices: [
1879
Vec3::new(0.3, 1.0, 1.7),
1880
Vec3::new(-2.0, -1.0, 0.0),
1881
Vec3::new(1.8, 0.5, 1.0),
1882
Vec3::new(-1.0, -2.0, 3.5),
1883
],
1884
};
1885
assert_eq!(tetrahedron.area(), 19.251068, "incorrect area");
1886
assert_eq!(tetrahedron.volume(), 3.2058334, "incorrect volume");
1887
assert_eq!(
1888
tetrahedron.signed_volume(),
1889
3.2058334,
1890
"incorrect signed volume"
1891
);
1892
assert_relative_eq!(tetrahedron.centroid(), Vec3::new(-0.225, -0.375, 1.55));
1893
1894
assert_eq!(Tetrahedron::default().area(), 3.4641016, "incorrect area");
1895
assert_eq!(
1896
Tetrahedron::default().volume(),
1897
0.33333334,
1898
"incorrect volume"
1899
);
1900
assert_eq!(
1901
Tetrahedron::default().signed_volume(),
1902
-0.33333334,
1903
"incorrect signed volume"
1904
);
1905
assert_relative_eq!(Tetrahedron::default().centroid(), Vec3::ZERO);
1906
}
1907
1908
#[test]
1909
fn extrusion_math() {
1910
let circle = Circle::new(0.75);
1911
let cylinder = Extrusion::new(circle, 2.5);
1912
assert_eq!(cylinder.area(), 15.315264, "incorrect surface area");
1913
assert_eq!(cylinder.volume(), 4.417865, "incorrect volume");
1914
1915
let annulus = crate::primitives::Annulus::new(0.25, 1.375);
1916
let tube = Extrusion::new(annulus, 0.333);
1917
assert_eq!(tube.area(), 14.886437, "incorrect surface area");
1918
assert_eq!(tube.volume(), 1.9124937, "incorrect volume");
1919
1920
let polygon = crate::primitives::RegularPolygon::new(3.8, 7);
1921
let regular_prism = Extrusion::new(polygon, 1.25);
1922
assert_eq!(regular_prism.area(), 107.8808, "incorrect surface area");
1923
assert_eq!(regular_prism.volume(), 49.392204, "incorrect volume");
1924
}
1925
1926
#[test]
1927
fn triangle_math() {
1928
// Default triangle tests
1929
let mut default_triangle = Triangle3d::default();
1930
let reverse_default_triangle = Triangle3d::new(
1931
Vec3::new(0.5, -0.5, 0.0),
1932
Vec3::new(-0.5, -0.5, 0.0),
1933
Vec3::new(0.0, 0.5, 0.0),
1934
);
1935
assert_eq!(default_triangle.area(), 0.5, "incorrect area");
1936
assert_relative_eq!(
1937
default_triangle.perimeter(),
1938
1.0 + 2.0 * ops::sqrt(1.25_f32),
1939
epsilon = 10e-9
1940
);
1941
assert_eq!(default_triangle.normal(), Ok(Dir3::Z), "incorrect normal");
1942
assert!(
1943
!default_triangle.is_degenerate(),
1944
"incorrect degenerate check"
1945
);
1946
assert_eq!(
1947
default_triangle.centroid(),
1948
Vec3::new(0.0, -0.16666667, 0.0),
1949
"incorrect centroid"
1950
);
1951
assert_eq!(
1952
default_triangle.largest_side(),
1953
(Vec3::new(0.0, 0.5, 0.0), Vec3::new(-0.5, -0.5, 0.0))
1954
);
1955
default_triangle.reverse();
1956
assert_eq!(
1957
default_triangle, reverse_default_triangle,
1958
"incorrect reverse"
1959
);
1960
assert_eq!(
1961
default_triangle.circumcenter(),
1962
Vec3::new(0.0, -0.125, 0.0),
1963
"incorrect circumcenter"
1964
);
1965
1966
// Custom triangle tests
1967
let right_triangle = Triangle3d::new(Vec3::ZERO, Vec3::X, Vec3::Y);
1968
let obtuse_triangle = Triangle3d::new(Vec3::NEG_X, Vec3::X, Vec3::new(0.0, 0.1, 0.0));
1969
let acute_triangle = Triangle3d::new(Vec3::ZERO, Vec3::X, Vec3::new(0.5, 5.0, 0.0));
1970
1971
assert_eq!(
1972
right_triangle.circumcenter(),
1973
Vec3::new(0.5, 0.5, 0.0),
1974
"incorrect circumcenter"
1975
);
1976
assert_eq!(
1977
obtuse_triangle.circumcenter(),
1978
Vec3::new(0.0, -4.95, 0.0),
1979
"incorrect circumcenter"
1980
);
1981
assert_eq!(
1982
acute_triangle.circumcenter(),
1983
Vec3::new(0.5, 2.475, 0.0),
1984
"incorrect circumcenter"
1985
);
1986
1987
assert!(acute_triangle.is_acute());
1988
assert!(!acute_triangle.is_obtuse());
1989
assert!(!obtuse_triangle.is_acute());
1990
assert!(obtuse_triangle.is_obtuse());
1991
1992
// Arbitrary triangle tests
1993
let [a, b, c] = [Vec3::ZERO, Vec3::new(1., 1., 0.5), Vec3::new(-3., 2.5, 1.)];
1994
let triangle = Triangle3d::new(a, b, c);
1995
1996
assert!(!triangle.is_degenerate(), "incorrectly found degenerate");
1997
assert_eq!(triangle.area(), 3.0233467, "incorrect area");
1998
assert_eq!(triangle.perimeter(), 9.832292, "incorrect perimeter");
1999
assert_eq!(
2000
triangle.circumcenter(),
2001
Vec3::new(-1., 1.75, 0.75),
2002
"incorrect circumcenter"
2003
);
2004
assert_eq!(
2005
triangle.normal(),
2006
Ok(Dir3::new_unchecked(Vec3::new(
2007
-0.04134491,
2008
-0.4134491,
2009
0.90958804
2010
))),
2011
"incorrect normal"
2012
);
2013
2014
// Degenerate triangle tests
2015
let zero_degenerate_triangle = Triangle3d::new(Vec3::ZERO, Vec3::ZERO, Vec3::ZERO);
2016
assert!(
2017
zero_degenerate_triangle.is_degenerate(),
2018
"incorrect degenerate check"
2019
);
2020
assert_eq!(
2021
zero_degenerate_triangle.normal(),
2022
Err(InvalidDirectionError::Zero),
2023
"incorrect normal"
2024
);
2025
assert_eq!(
2026
zero_degenerate_triangle.largest_side(),
2027
(Vec3::ZERO, Vec3::ZERO),
2028
"incorrect largest side"
2029
);
2030
2031
let dup_degenerate_triangle = Triangle3d::new(Vec3::ZERO, Vec3::X, Vec3::X);
2032
assert!(
2033
dup_degenerate_triangle.is_degenerate(),
2034
"incorrect degenerate check"
2035
);
2036
assert_eq!(
2037
dup_degenerate_triangle.normal(),
2038
Err(InvalidDirectionError::Zero),
2039
"incorrect normal"
2040
);
2041
assert_eq!(
2042
dup_degenerate_triangle.largest_side(),
2043
(Vec3::ZERO, Vec3::X),
2044
"incorrect largest side"
2045
);
2046
2047
let collinear_degenerate_triangle = Triangle3d::new(Vec3::NEG_X, Vec3::ZERO, Vec3::X);
2048
assert!(
2049
collinear_degenerate_triangle.is_degenerate(),
2050
"incorrect degenerate check"
2051
);
2052
assert_eq!(
2053
collinear_degenerate_triangle.normal(),
2054
Err(InvalidDirectionError::Zero),
2055
"incorrect normal"
2056
);
2057
assert_eq!(
2058
collinear_degenerate_triangle.largest_side(),
2059
(Vec3::NEG_X, Vec3::X),
2060
"incorrect largest side"
2061
);
2062
}
2063
}
2064
2065