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