phys_geom/shape.rs
1// Copyright (C) 2020-2025 phys-geom authors. All Rights Reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Geometric shape implementations for 3D physics simulations.
16//!
17//! This module provides a collection of primitive geometric shapes commonly used in physics
18//! engines, along with their basic properties and operations. All shapes are designed to be
19//! FFI-safe through the `#[repr(C)]` attribute and support optional serialization via the `serde`
20//! feature.
21//!
22//! ## Available Shapes
23//!
24//! - [`Sphere`] - A perfect sphere defined by its radius
25//! - [`Capsule`] - A capsule with cylindrical body and hemispherical caps
26//! - [`Cuboid`] - A rectangular box (also known as a box or rectangular prism)
27//! - [`Cylinder`] - A cylinder with circular cross-section
28//! - [`InfinitePlane`] - An infinite plane (primarily for geometric computations)
29//! - [`Tetrahedron`] - A tetrahedron defined by four vertices
30//!
31//! ## Common Properties
32//!
33//! All shapes share these characteristics:
34//! - **FFI-safe**: All shapes use `#[repr(C)]` for C compatibility
35//! - **Serializable**: Support `serde` serialization when the feature is enabled
36//! - **Validated**: Construction validates parameters (e.g., positive dimensions)
37//! - **Copy-friendly**: All shapes implement `Copy` and `Clone`
38//!
39//! ## Usage Examples
40//!
41//! ```rust
42//! use phys_geom::shape::{Sphere, Capsule, Cuboid, Cylinder};
43//!
44//! // Create a sphere
45//! let sphere = Sphere::new(1.0);
46//! assert_eq!(sphere.radius(), 1.0);
47//!
48//! // Create a capsule with cylindrical half-height 2.0 and radius 0.5
49//! let capsule = Capsule::new(2.0, 0.5);
50//! assert_eq!(capsule.height(), 4.0); // cylinder height (excluding caps)
51//!
52//! // Create a cuboid (box) with dimensions 2x3x1
53//! let cuboid = Cuboid::new([2.0, 3.0, 1.0]);
54//! assert_eq!(cuboid.length(), [2.0, 3.0, 1.0]);
55//!
56//! // Create a cylinder
57//! let cylinder = Cylinder::new(1.5, 0.8); // half-height=1.5, radius=0.8
58//! assert_eq!(cylinder.height(), 3.0);
59//! ```
60
61use crate::math::*;
62
63/// A sphere shape centered at the origin.
64///
65/// The sphere is defined by its radius and is commonly used for simple collision detection
66/// and physics calculations. The sphere is centered at the origin (0, 0, 0).
67///
68/// # Examples
69///
70/// ```rust
71/// use phys_geom::shape::Sphere;
72///
73/// // Create a sphere with radius 1.0
74/// let sphere = Sphere::new(1.0);
75/// assert_eq!(sphere.radius(), 1.0);
76///
77/// // Use the predefined unit sphere
78/// let unit_sphere = Sphere::UNIT;
79/// assert_eq!(unit_sphere.radius(), 0.5);
80/// ```
81///
82/// # Panics
83///
84/// - [`Sphere::new()`] panics if the radius is not positive
85/// - [`Sphere::set_radius()`] panics if the new radius is not positive
86#[repr(C)]
87#[derive(Debug, Copy, Clone, Default, PartialEq)]
88#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
89pub struct Sphere {
90 radius: Real,
91}
92
93impl Sphere {
94 /// A unit sphere with radius 0.5.
95 ///
96 /// This is useful as a reference size for normalized shapes.
97 pub const UNIT: Sphere = Sphere { radius: 0.5 };
98
99 /// Creates a new sphere with the given radius.
100 ///
101 /// # Arguments
102 ///
103 /// * `radius` - The radius of the sphere (must be positive)
104 ///
105 /// # Returns
106 ///
107 /// A new `Sphere` instance with the specified radius.
108 ///
109 /// # Panics
110 ///
111 /// Panics if `radius` is not positive.
112 ///
113 /// # Examples
114 ///
115 /// ```rust
116 /// use phys_geom::shape::Sphere;
117 ///
118 /// let sphere = Sphere::new(2.5);
119 /// assert_eq!(sphere.radius(), 2.5);
120 /// ```
121 #[inline]
122 #[must_use]
123 pub fn new(radius: Real) -> Sphere {
124 assert!(radius > 0.0);
125 Sphere { radius }
126 }
127
128 /// Returns the radius of the sphere.
129 ///
130 /// # Returns
131 ///
132 /// The radius as a `Real` (f32 or f64 depending on features).
133 ///
134 /// # Examples
135 ///
136 /// ```rust
137 /// use phys_geom::shape::Sphere;
138 ///
139 /// let sphere = Sphere::new(3.0);
140 /// assert_eq!(sphere.radius(), 3.0);
141 /// ```
142 #[inline]
143 pub const fn radius(&self) -> Real {
144 self.radius
145 }
146
147 /// Sets the radius of the sphere.
148 ///
149 /// # Arguments
150 ///
151 /// * `value` - The new radius (must be positive)
152 ///
153 /// # Panics
154 ///
155 /// Panics if `value` is not positive.
156 ///
157 /// # Examples
158 ///
159 /// ```rust
160 /// use phys_geom::shape::Sphere;
161 ///
162 /// let mut sphere = Sphere::new(1.0);
163 /// sphere.set_radius(2.0);
164 /// assert_eq!(sphere.radius(), 2.0);
165 /// ```
166 #[inline]
167 pub fn set_radius(&mut self, value: Real) {
168 *self = Self::new(value);
169 }
170}
171
172/// A capsule shape centered at the origin, aligned along the Y-axis.
173///
174/// A capsule consists of a cylinder with hemispherical caps at both ends.
175/// It's commonly used in physics engines for character controllers because
176/// it provides good collision detection while being easy to rotate.
177///
178/// The capsule is centered at the origin (0, 0, 0) and extends along the Y-axis.
179///
180/// # Examples
181///
182/// ```rust
183/// use phys_geom::shape::Capsule;
184///
185/// // Create a capsule with half-height 2.0 and radius 0.5
186/// let capsule = Capsule::new(2.0, 0.5);
187/// assert_eq!(capsule.half_height(), 2.0);
188/// assert_eq!(capsule.radius(), 0.5);
189/// assert_eq!(capsule.height(), 4.0); // total height
190/// ```
191///
192/// # Dimensions
193///
194/// - `half_height`: Height of the cylindrical portion (excluding caps)
195/// - `radius`: Radius of both the cylinder and the hemispherical caps
196/// - Total height = `2 * half_height + 2 * radius`
197#[repr(C)]
198#[derive(Debug, Copy, Clone, Default, PartialEq)]
199#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
200pub struct Capsule {
201 half_height: Real,
202 radius: Real,
203}
204
205impl Capsule {
206 /// Creates a new capsule with the given dimensions.
207 ///
208 /// # Arguments
209 ///
210 /// * `half_height` - Half the height of the cylindrical portion (must be non-negative)
211 /// * `radius` - The radius of the capsule (must be positive)
212 ///
213 /// # Returns
214 ///
215 /// A new `Capsule` instance.
216 ///
217 /// # Panics
218 ///
219 /// Panics if:
220 /// - `half_height` is negative
221 /// - `radius` is not positive
222 ///
223 /// # Examples
224 ///
225 /// ```rust
226 /// use phys_geom::shape::Capsule;
227 ///
228 /// // Create a capsule with cylindrical portion 3 units tall and 0.5 unit radius
229 /// let capsule = Capsule::new(1.5, 0.5);
230 /// assert_eq!(capsule.height(), 3.0);
231 /// ```
232 #[must_use]
233 pub fn new(half_height: Real, radius: Real) -> Self {
234 assert!(half_height >= 0.0);
235 assert!(radius > 0.0);
236 Self {
237 half_height,
238 radius,
239 }
240 }
241
242 /// Returns the height of the cylindrical portion of the capsule.
243 ///
244 /// This is the height of just the cylindrical part, excluding the hemispherical caps.
245 /// Height = 2 * half_height
246 ///
247 /// # Returns
248 ///
249 /// The height of the cylindrical portion as a `Real`.
250 ///
251 /// # Examples
252 ///
253 /// ```rust
254 /// use phys_geom::shape::Capsule;
255 ///
256 /// let capsule = Capsule::new(2.0, 0.5);
257 /// assert_eq!(capsule.height(), 4.0); // cylinder height = 2 * half_height
258 /// ```
259 #[inline]
260 pub fn height(&self) -> Real {
261 self.half_height + self.half_height
262 }
263
264 /// Returns the half height of the cylindrical portion.
265 ///
266 /// This is the height of the cylindrical part excluding the hemispherical caps.
267 ///
268 /// # Returns
269 ///
270 /// The half height as a `Real`.
271 ///
272 /// # Examples
273 ///
274 /// ```rust
275 /// use phys_geom::shape::Capsule;
276 ///
277 /// let capsule = Capsule::new(2.0, 0.5);
278 /// assert_eq!(capsule.half_height(), 2.0);
279 /// ```
280 #[inline]
281 pub fn half_height(&self) -> Real {
282 self.half_height
283 }
284
285 /// Returns the radius of the capsule.
286 ///
287 /// This is the radius of both the cylindrical body and the hemispherical caps.
288 ///
289 /// # Returns
290 ///
291 /// The radius as a `Real`.
292 ///
293 /// # Examples
294 ///
295 /// ```rust
296 /// use phys_geom::shape::Capsule;
297 ///
298 /// let capsule = Capsule::new(2.0, 0.5);
299 /// assert_eq!(capsule.radius(), 0.5);
300 /// ```
301 #[inline]
302 pub fn radius(&self) -> Real {
303 self.radius
304 }
305}
306
307/// A cuboid (rectangular box) shape centered at the origin.
308///
309/// A cuboid is a three-dimensional rectangular box with six rectangular faces.
310/// It's one of the most commonly used shapes in physics engines for representing
311/// boxes, containers, and general rectangular objects.
312///
313/// The cuboid is centered at the origin (0, 0, 0) with its edges aligned to the coordinate axes.
314///
315/// # Examples
316///
317/// ```rust
318/// use phys_geom::shape::Cuboid;
319///
320/// // Create a cuboid with dimensions 2x3x1
321/// let cuboid = Cuboid::new([2.0, 3.0, 1.0]);
322/// assert_eq!(cuboid.length(), [2.0, 3.0, 1.0]);
323/// assert_eq!(cuboid.half_length(), [1.0, 1.5, 0.5]);
324///
325/// // Create a unit cube
326/// let unit_cube = Cuboid::UNIT;
327/// assert_eq!(unit_cube.length(), [1.0, 1.0, 1.0]);
328/// ```
329///
330/// # Dimensions
331///
332/// - `half_length`: Half-lengths along each axis (x, y, z)
333/// - Total dimensions: `[x, y, z] = [2 * half_length[0], 2 * half_length[1], 2 * half_length[2]]`
334///
335/// # Construction
336///
337/// The cuboid can be constructed from:
338/// - An array of three lengths: `Cuboid::new([x, y, z])`
339/// - Individual components: `Cuboid::new_xyz(x, y, z)`
340#[repr(C)]
341#[derive(Debug, Copy, Clone, Default, PartialEq)]
342#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
343pub struct Cuboid {
344 half_length: [Real; 3],
345}
346
347impl Cuboid {
348 /// A unit cube with side length 1.0.
349 ///
350 /// This is useful as a reference size for normalized shapes.
351 /// The cube extends from -0.5 to +0.5 along each axis.
352 pub const UNIT: Cuboid = Cuboid {
353 half_length: [0.5, 0.5, 0.5],
354 };
355
356 /// Creates a new cuboid with the given dimensions.
357 ///
358 /// # Arguments
359 ///
360 /// * `length` - The dimensions of the cuboid as a 3-element array [x, y, z]
361 ///
362 /// # Returns
363 ///
364 /// A new `Cuboid` instance.
365 ///
366 /// # Panics
367 ///
368 /// Panics if any component of length is not positive.
369 ///
370 /// # Examples
371 ///
372 /// ```rust
373 /// use phys_geom::shape::Cuboid;
374 ///
375 /// // Create from array
376 /// let cuboid1 = Cuboid::new([2.0, 3.0, 1.0]);
377 ///
378 /// // Create from tuple (via Into trait)
379 /// let cuboid2 = Cuboid::new((2.0, 3.0, 1.0));
380 ///
381 /// assert_eq!(cuboid1.length(), cuboid2.length());
382 /// ```
383 #[inline]
384 #[must_use]
385 pub fn new(length: impl Into<[Real; 3]>) -> Cuboid {
386 let length = length.into();
387 assert!(length[0] > 0.0);
388 assert!(length[1] > 0.0);
389 assert!(length[2] > 0.0);
390
391 const HALF: Real = 0.5;
392 Cuboid {
393 half_length: [length[0] * HALF, length[1] * HALF, length[2] * HALF],
394 }
395 }
396
397 /// Creates a new cuboid with individual x, y, z dimensions.
398 ///
399 /// This is a convenience method equivalent to `Cuboid::new([x, y, z])`.
400 ///
401 /// # Arguments
402 ///
403 /// * `x` - Length along the X-axis
404 /// * `y` - Length along the Y-axis
405 /// * `z` - Length along the Z-axis
406 ///
407 /// # Returns
408 ///
409 /// A new `Cuboid` instance.
410 ///
411 /// # Examples
412 ///
413 /// ```rust
414 /// use phys_geom::shape::Cuboid;
415 ///
416 /// let cuboid = Cuboid::new_xyz(2.0, 3.0, 1.0);
417 /// assert_eq!(cuboid.length(), [2.0, 3.0, 1.0]);
418 /// ```
419 #[inline]
420 pub fn new_xyz(x: Real, y: Real, z: Real) -> Cuboid {
421 Self::new([x, y, z])
422 }
423
424 /// Returns the full dimensions of the cuboid.
425 ///
426 /// Returns an array [x, y, z] representing the total length along each axis.
427 ///
428 /// # Returns
429 ///
430 /// An array of three `Real` values [x, y, z].
431 ///
432 /// # Examples
433 ///
434 /// ```rust
435 /// use phys_geom::shape::Cuboid;
436 ///
437 /// let cuboid = Cuboid::new([2.0, 3.0, 1.0]);
438 /// assert_eq!(cuboid.length(), [2.0, 3.0, 1.0]);
439 /// ```
440 #[inline]
441 pub fn length(&self) -> [Real; 3] {
442 const DOUBLE: Real = 2.0;
443 [
444 self.half_length[0] * DOUBLE,
445 self.half_length[1] * DOUBLE,
446 self.half_length[2] * DOUBLE,
447 ]
448 }
449
450 /// Returns the half-lengths of the cuboid.
451 ///
452 /// Returns an array [x/2, y/2, z/2] representing the distance from the center
453 /// to each face along the respective axes.
454 ///
455 /// # Returns
456 ///
457 /// An array of three `Real` values [x/2, y/2, z/2].
458 ///
459 /// # Examples
460 ///
461 /// ```rust
462 /// use phys_geom::shape::Cuboid;
463 ///
464 /// let cuboid = Cuboid::new([2.0, 3.0, 1.0]);
465 /// assert_eq!(cuboid.half_length(), [1.0, 1.5, 0.5]);
466 /// ```
467 #[inline]
468 pub fn half_length(&self) -> [Real; 3] {
469 self.half_length
470 }
471
472 /// Sets the dimensions of the cuboid.
473 ///
474 /// # Arguments
475 ///
476 /// * `length` - The new dimensions as a 3-element array [x, y, z]
477 ///
478 /// # Panics
479 ///
480 /// Panics if any component of length is not positive.
481 ///
482 /// # Examples
483 ///
484 /// ```rust
485 /// use phys_geom::shape::Cuboid;
486 ///
487 /// let mut cuboid = Cuboid::new([1.0, 1.0, 1.0]);
488 /// cuboid.set_length([2.0, 3.0, 1.0]);
489 /// assert_eq!(cuboid.length(), [2.0, 3.0, 1.0]);
490 /// ```
491 #[inline]
492 pub fn set_length(&mut self, length: impl Into<[Real; 3]>) {
493 *self = Self::new(length);
494 }
495}
496
497/// A cylinder shape centered at the origin, aligned along the Y-axis.
498///
499/// A cylinder is a three-dimensional shape with a circular cross-section.
500/// It's commonly used in physics engines for representing pillars, pipes,
501/// wheels, and other cylindrical objects.
502///
503/// The cylinder is centered at the origin (0, 0, 0) with its axis
504/// aligned to the Y-axis. It extends from -half_height to +half_height
505/// along the Y-axis.
506///
507/// # Examples
508///
509/// ```rust
510/// use phys_geom::shape::Cylinder;
511///
512/// // Create a cylinder with height 3.0 and radius 0.8
513/// let cylinder = Cylinder::new(1.5, 0.8);
514/// assert_eq!(cylinder.height(), 3.0);
515/// assert_eq!(cylinder.radius(), 0.8);
516/// ```
517///
518/// # Dimensions
519///
520/// - `half_height`: Half the height of the cylinder
521/// - `radius`: Radius of the circular cross-section
522/// - Total height = `2 * half_height`
523#[repr(C)]
524#[derive(Debug, Clone, Default, Copy, PartialEq)]
525#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
526pub struct Cylinder {
527 half_height: Real,
528 radius: Real,
529}
530
531impl Cylinder {
532 /// Creates a new cylinder with the given dimensions.
533 ///
534 /// # Arguments
535 ///
536 /// * `half_height` - Half the height of the cylinder (must be positive)
537 /// * `radius` - The radius of the cylinder (must be positive)
538 ///
539 /// # Returns
540 ///
541 /// A new `Cylinder` instance.
542 ///
543 /// # Panics
544 ///
545 /// Panics if `half_height` or `radius` is not positive.
546 ///
547 /// # Examples
548 ///
549 /// ```rust
550 /// use phys_geom::shape::Cylinder;
551 ///
552 /// // Create a cylinder with total height 4.0 and radius 1.0
553 /// let cylinder = Cylinder::new(2.0, 1.0);
554 /// assert_eq!(cylinder.height(), 4.0);
555 /// assert_eq!(cylinder.radius(), 1.0);
556 /// ```
557 #[inline]
558 #[must_use]
559 pub fn new(half_height: Real, radius: Real) -> Self {
560 assert!(radius > 0.0);
561 assert!(half_height > 0.0);
562 Self {
563 half_height,
564 radius,
565 }
566 }
567
568 /// Returns the total height of the cylinder.
569 ///
570 /// # Returns
571 ///
572 /// The total height as a `Real`.
573 ///
574 /// # Examples
575 ///
576 /// ```rust
577 /// use phys_geom::shape::Cylinder;
578 ///
579 /// let cylinder = Cylinder::new(1.5, 0.8);
580 /// assert_eq!(cylinder.height(), 3.0);
581 /// ```
582 #[inline]
583 pub fn height(&self) -> Real {
584 self.half_height * 2.0
585 }
586
587 /// Returns the half height of the cylinder.
588 ///
589 /// This is the distance from the center to either end of the cylinder.
590 ///
591 /// # Returns
592 ///
593 /// The half height as a `Real`.
594 ///
595 /// # Examples
596 ///
597 /// ```rust
598 /// use phys_geom::shape::Cylinder;
599 ///
600 /// let cylinder = Cylinder::new(1.5, 0.8);
601 /// assert_eq!(cylinder.half_height(), 1.5);
602 /// ```
603 #[inline]
604 pub fn half_height(&self) -> Real {
605 self.half_height
606 }
607
608 /// Returns the radius of the cylinder.
609 ///
610 /// # Returns
611 ///
612 /// The radius as a `Real`.
613 ///
614 /// # Examples
615 ///
616 /// ```rust
617 /// use phys_geom::shape::Cylinder;
618 ///
619 /// let cylinder = Cylinder::new(1.5, 0.8);
620 /// assert_eq!(cylinder.radius(), 0.8);
621 /// ```
622 #[inline]
623 pub fn radius(&self) -> Real {
624 self.radius
625 }
626}
627
628/// An infinite plane in 3D space.
629///
630/// An infinite plane represents a flat surface that extends infinitely in all directions.
631/// This is primarily used for geometric computations and as a conceptual shape in
632/// physics simulations (e.g., ground planes, collision surfaces).
633///
634/// Note: This is a marker struct for type system purposes. The actual plane definition
635/// would typically be handled separately with a normal vector and distance from origin.
636///
637/// # FFI Safety
638///
639/// The struct includes padding to ensure FFI safety, as empty structs are not
640/// FFI-safe in Rust (see [Rust issue #17679]).
641///
642/// # Examples
643///
644/// ```rust
645/// use phys_geom::shape::InfinitePlane;
646///
647/// // Create an infinite plane
648/// let plane = InfinitePlane::default();
649/// ```
650#[repr(C)]
651#[derive(Default, Debug, Clone, Copy, PartialEq)]
652#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
653pub struct InfinitePlane {
654 // empty struct is not FFI-safe, https://github.com/rust-lang/rust/issues/17679
655 _padding: i32,
656}
657
658/// A triangle defined by three vertices in 3D space.
659///
660/// A triangle is the simplest polygon in 3D space, consisting of three vertices
661/// connected by three edges. It's a fundamental building block in computer graphics,
662/// computational geometry, and physics simulations.
663///
664/// The triangle is defined by three vertices A, B, and C. The orientation and
665/// properties are computed based on the relative positions of these vertices.
666///
667/// # Examples
668///
669/// ```rust
670/// use phys_geom::shape::Triangle;
671/// use phys_geom::math::Point3;
672///
673/// // Create a triangle
674/// let triangle = Triangle::new(
675/// Point3::new(0.0, 0.0, 0.0),
676/// Point3::new(1.0, 0.0, 0.0),
677/// Point3::new(0.0, 1.0, 0.0),
678/// );
679/// ```
680///
681/// # Mathematical Properties
682///
683/// - Area = 0.5 * |(B-A) × (C-A)|
684/// - Normal = normalize((B-A) × (C-A))
685/// - Center = (A + B + C) / 3
686#[repr(C)]
687#[derive(Debug, Clone, Copy, Default, PartialEq)]
688#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
689pub struct Triangle {
690 /// First vertex of the triangle
691 pub a: Point3,
692 /// Second vertex of the triangle
693 pub b: Point3,
694 /// Third vertex of the triangle
695 pub c: Point3,
696}
697
698impl Triangle {
699 /// Creates a new triangle from three vertices.
700 ///
701 /// # Arguments
702 ///
703 /// * `a` - First vertex
704 /// * `b` - Second vertex
705 /// * `c` - Third vertex
706 ///
707 /// # Returns
708 ///
709 /// A new `Triangle` instance.
710 ///
711 /// # Examples
712 ///
713 /// ```rust
714 /// use phys_geom::shape::Triangle;
715 /// use phys_geom::math::Point3;
716 ///
717 /// let triangle = Triangle::new(
718 /// Point3::new(0.0, 0.0, 0.0),
719 /// Point3::new(1.0, 0.0, 0.0),
720 /// Point3::new(0.0, 1.0, 0.0),
721 /// );
722 /// ```
723 #[inline]
724 #[must_use]
725 pub fn new(a: impl Into<[Real; 3]>, b: impl Into<[Real; 3]>, c: impl Into<[Real; 3]>) -> Self {
726 Self {
727 a: Point3::from(a.into()),
728 b: Point3::from(b.into()),
729 c: Point3::from(c.into()),
730 }
731 }
732
733 /// Computes the geometric center (centroid) of the triangle.
734 ///
735 /// The centroid is calculated as the average of all three vertices:
736 /// C = (A + B + C) / 3
737 ///
738 /// # Returns
739 ///
740 /// The centroid coordinates as a `Point3`.
741 ///
742 /// # Examples
743 ///
744 /// ```rust
745 /// use phys_geom::shape::Triangle;
746 /// use phys_geom::math::Point3;
747 ///
748 /// let triangle = Triangle::new(
749 /// Point3::new(0.0, 0.0, 0.0),
750 /// Point3::new(2.0, 0.0, 0.0),
751 /// Point3::new(0.0, 2.0, 0.0),
752 /// );
753 ///
754 /// let center = triangle.center();
755 /// assert_eq!(center, Point3::new(2.0/3.0, 2.0/3.0, 0.0));
756 /// ```
757 #[inline]
758 pub fn center(&self) -> Point3 {
759 let a: Vec3 = self.a.coords;
760 let b: Vec3 = self.b.coords;
761 let c: Vec3 = self.c.coords;
762 Point3::from((a + b + c) / 3.0)
763 }
764
765 /// Computes the normal vector of the triangle.
766 ///
767 /// The normal is calculated using the cross product of two edges:
768 /// N = normalize((B-A) × (C-A))
769 ///
770 /// The direction follows the right-hand rule based on the vertex order (A, B, C).
771 ///
772 /// # Returns
773 ///
774 /// The unit normal vector as a `UnitVec3`.
775 ///
776 /// # Examples
777 ///
778 /// ```rust
779 /// use phys_geom::shape::Triangle;
780 /// use phys_geom::math::{Point3, UnitVec3};
781 ///
782 /// let triangle = Triangle::new(
783 /// Point3::new(0.0, 0.0, 0.0),
784 /// Point3::new(1.0, 0.0, 0.0),
785 /// Point3::new(0.0, 1.0, 0.0),
786 /// );
787 ///
788 /// let normal = triangle.normal();
789 /// // Triangle lies in the XY plane, normal points in +Z direction
790 /// assert!(normal.z > 0.9); // Close to 1.0 for unit vector
791 /// ```
792 #[inline]
793 pub fn normal(&self) -> UnitVec3 {
794 let edge1 = self.b - self.a;
795 let edge2 = self.c - self.a;
796 UnitVec3::new_normalize(edge1.cross(&edge2))
797 }
798
799 /// Computes the area of the triangle.
800 ///
801 /// The area is calculated using half the magnitude of the cross product:
802 /// Area = 0.5 * |(B-A) × (C-A)|
803 ///
804 /// # Returns
805 ///
806 /// The area as a `Real` value.
807 ///
808 /// # Examples
809 ///
810 /// ```rust
811 /// use phys_geom::shape::Triangle;
812 /// use phys_geom::math::Point3;
813 ///
814 /// let triangle = Triangle::new(
815 /// Point3::new(0.0, 0.0, 0.0),
816 /// Point3::new(2.0, 0.0, 0.0),
817 /// Point3::new(0.0, 2.0, 0.0),
818 /// );
819 ///
820 /// let area = triangle.area();
821 /// assert_eq!(area, 2.0); // Right triangle with legs of length 2
822 /// ```
823 #[inline]
824 pub fn area(&self) -> Real {
825 let edge1 = self.b - self.a;
826 let edge2 = self.c - self.a;
827 edge1.cross(&edge2).magnitude() / 2.0
828 }
829
830 /// Checks if the triangle is degenerate (has zero area).
831 ///
832 /// A triangle is degenerate if all three vertices are collinear, which means
833 /// the cross product of two edges has zero (or near-zero) magnitude.
834 ///
835 /// # Returns
836 ///
837 /// `true` if the triangle is degenerate, `false` otherwise.
838 ///
839 /// # Examples
840 ///
841 /// ```rust
842 /// use phys_geom::shape::Triangle;
843 /// use phys_geom::math::Point3;
844 ///
845 /// // Non-degenerate triangle
846 /// let triangle1 = Triangle::new(
847 /// Point3::new(0.0, 0.0, 0.0),
848 /// Point3::new(1.0, 0.0, 0.0),
849 /// Point3::new(0.0, 1.0, 0.0),
850 /// );
851 /// assert!(!triangle1.is_degenerate());
852 ///
853 /// // Degenerate triangle (all points on a line)
854 /// let triangle2 = Triangle::new(
855 /// Point3::new(0.0, 0.0, 0.0),
856 /// Point3::new(1.0, 0.0, 0.0),
857 /// Point3::new(2.0, 0.0, 0.0),
858 /// );
859 /// assert!(triangle2.is_degenerate());
860 /// ```
861 #[inline]
862 pub fn is_degenerate(&self) -> bool {
863 self.area() <= Real::EPSILON
864 }
865}
866
867use crate::{Aabb3, ComputeAabb3};
868
869impl ComputeAabb3 for Triangle {
870 #[inline]
871 fn compute_aabb(&self) -> Aabb3 {
872 let p1 = self.a.coords.inf(&self.b.coords).inf(&self.c.coords);
873 let p2 = self.a.coords.sup(&self.b.coords).sup(&self.c.coords);
874 Aabb3::new(Point3::from(p1), Point3::from(p2))
875 }
876}
877
878/// A tetrahedron defined by four vertices in 3D space.
879///
880/// A tetrahedron is a polyhedron with four triangular faces, six edges, and four vertices.
881/// It's the simplest type of polyhedron and is commonly used in computational geometry,
882/// finite element analysis, and physics simulations.
883///
884/// The tetrahedron is defined by four vertices A, B, C, and D. The volume and other
885/// properties are computed based on the relative positions of these vertices.
886///
887/// # Examples
888///
889/// ```rust
890/// use phys_geom::shape::Tetrahedron;
891///
892/// // Create a regular tetrahedron
893/// let tetrahedron = Tetrahedron {
894/// a: [1.0, 1.0, 1.0],
895/// b: [1.0, -1.0, -1.0],
896/// c: [-1.0, 1.0, -1.0],
897/// d: [-1.0, -1.0, 1.0],
898/// };
899///
900/// // Compute its signed volume
901/// let volume = tetrahedron.signed_volume();
902/// let center = tetrahedron.center();
903/// ```
904///
905/// # Volume Sign Convention
906///
907/// The signed volume follows the right-hand rule:
908/// - Positive volume: Vertex D is on the side pointed by the normal of triangle ABC
909/// - Negative volume: Vertex D is on the opposite side
910/// - Zero volume: All vertices are coplanar
911///
912/// # Mathematical Properties
913///
914/// - Volume = (1/6) * |(B-A) · ((C-A) × (D-A))|
915/// - Center = (A + B + C + D) / 4
916pub struct Tetrahedron {
917 /// First vertex of the tetrahedron
918 pub a: [Real; 3],
919 /// Second vertex of the tetrahedron
920 pub b: [Real; 3],
921 /// Third vertex of the tetrahedron
922 pub c: [Real; 3],
923 /// Fourth vertex of the tetrahedron
924 pub d: [Real; 3],
925}
926
927impl Tetrahedron {
928 /// Computes the signed volume of this tetrahedron.
929 ///
930 /// The signed volume is calculated using the scalar triple product:
931 /// V = (1/6) * ((B-A) · ((C-A) × (D-A)))
932 ///
933 /// # Returns
934 ///
935 /// The signed volume as a `Real` value.
936 ///
937 /// # Volume Sign Convention
938 ///
939 /// - **Positive**: Vertex D is on the half-space pointed by the normal of the oriented triangle
940 /// (A, B, C)
941 /// - **Negative**: Vertex D is on the opposite side
942 /// - **Zero**: All four vertices are coplanar
943 ///
944 /// # Examples
945 ///
946 /// ```rust
947 /// use phys_geom::shape::Tetrahedron;
948 ///
949 /// let tetrahedron = Tetrahedron {
950 /// a: [0.0, 0.0, 0.0],
951 /// b: [1.0, 0.0, 0.0],
952 /// c: [0.0, 1.0, 0.0],
953 /// d: [0.0, 0.0, 1.0],
954 /// };
955 ///
956 /// let volume = tetrahedron.signed_volume();
957 /// assert!(volume > 0.0);
958 /// ```
959 #[inline]
960 pub fn signed_volume(&self) -> Real {
961 let a: Vec3 = self.a.into();
962 let b: Vec3 = self.b.into();
963 let c: Vec3 = self.c.into();
964 let d: Vec3 = self.d.into();
965 let v1 = b - a;
966 let v2 = c - a;
967 let v3 = d - a;
968 v1.dot(&v2.cross(&v3)) / 6.0
969 }
970
971 /// Computes the centroid (geometric center) of the tetrahedron.
972 ///
973 /// The centroid is calculated as the average of all four vertices:
974 /// C = (A + B + C + D) / 4
975 ///
976 /// # Returns
977 ///
978 /// The centroid coordinates as a 3-element array [x, y, z].
979 ///
980 /// # Examples
981 ///
982 /// ```rust
983 /// use phys_geom::shape::Tetrahedron;
984 ///
985 /// let tetrahedron = Tetrahedron {
986 /// a: [0.0, 0.0, 0.0],
987 /// b: [2.0, 0.0, 0.0],
988 /// c: [0.0, 2.0, 0.0],
989 /// d: [0.0, 0.0, 2.0],
990 /// };
991 ///
992 /// let center = tetrahedron.center();
993 /// assert_eq!(center, [0.5, 0.5, 0.5]);
994 /// ```
995 #[inline]
996 pub fn center(&self) -> [Real; 3] {
997 let a: Vec3 = self.a.into();
998 let b: Vec3 = self.b.into();
999 let c: Vec3 = self.c.into();
1000 let d: Vec3 = self.d.into();
1001 let center = (a + b + c + d) / 4.0;
1002 center.into()
1003 }
1004}
1005
1006#[cfg(test)]
1007mod tests {
1008
1009 use super::*;
1010 use crate::ComputeAabb3;
1011
1012 #[test]
1013 fn test_sphere() {
1014 let mut sphere = Sphere::new(1.0);
1015 assert_eq!(sphere.radius(), 1.0);
1016
1017 sphere.set_radius(2.0);
1018 assert_eq!(sphere.radius(), 2.0);
1019 }
1020
1021 #[test]
1022 fn test_capsule() {
1023 let capsule = Capsule::new(1.0, 1.0);
1024 assert_eq!(capsule.half_height(), 1.0);
1025 assert_eq!(capsule.radius(), 1.0);
1026 }
1027
1028 #[test]
1029 fn test_cuboid() {
1030 let cuboid = Cuboid::new([1.0, 1.0, 1.0]);
1031 assert_eq!(cuboid.length(), [1.0, 1.0, 1.0]);
1032 assert_eq!(cuboid.half_length(), [0.5, 0.5, 0.5]);
1033 }
1034
1035 #[test]
1036 fn test_cylinder() {
1037 let cylinder = Cylinder::new(1.0, 1.0);
1038 assert_eq!(cylinder.height(), 2.0);
1039 assert_eq!(cylinder.half_height(), 1.0);
1040 assert_eq!(cylinder.radius(), 1.0);
1041 }
1042
1043 #[test]
1044 fn test_triangle() {
1045 let triangle = Triangle::new([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
1046
1047 assert_eq!(triangle.a, Point3::new(0.0, 0.0, 0.0));
1048 assert_eq!(triangle.b, Point3::new(1.0, 0.0, 0.0));
1049 assert_eq!(triangle.c, Point3::new(0.0, 1.0, 0.0));
1050 }
1051
1052 #[test]
1053 fn test_triangle_center() {
1054 let triangle = Triangle::new(
1055 Point3::new(0.0, 0.0, 0.0),
1056 Point3::new(2.0, 0.0, 0.0),
1057 Point3::new(0.0, 2.0, 0.0),
1058 );
1059
1060 let center = triangle.center();
1061 assert_eq!(center, Point3::new(2.0 / 3.0, 2.0 / 3.0, 0.0));
1062 }
1063
1064 #[test]
1065 fn test_triangle_normal() {
1066 let triangle = Triangle::new(
1067 Point3::new(0.0, 0.0, 0.0),
1068 Point3::new(1.0, 0.0, 0.0),
1069 Point3::new(0.0, 1.0, 0.0),
1070 );
1071
1072 let normal = triangle.normal();
1073 // Triangle lies in the XY plane, normal should point in +Z direction
1074 assert!(normal.z > 0.9);
1075 assert!(normal.x.abs() < 0.1);
1076 assert!(normal.y.abs() < 0.1);
1077 }
1078
1079 #[test]
1080 fn test_triangle_area() {
1081 let triangle = Triangle::new(
1082 Point3::new(0.0, 0.0, 0.0),
1083 Point3::new(2.0, 0.0, 0.0),
1084 Point3::new(0.0, 2.0, 0.0),
1085 );
1086
1087 let area = triangle.area();
1088 assert_eq!(area, 2.0); // Right triangle with legs of length 2
1089 }
1090
1091 #[test]
1092 fn test_triangle_degenerate() {
1093 // Non-degenerate triangle
1094 let triangle1 = Triangle::new(
1095 Point3::new(0.0, 0.0, 0.0),
1096 Point3::new(1.0, 0.0, 0.0),
1097 Point3::new(0.0, 1.0, 0.0),
1098 );
1099 assert!(!triangle1.is_degenerate());
1100
1101 // Degenerate triangle (all points on a line)
1102 let triangle2 = Triangle::new(
1103 Point3::new(0.0, 0.0, 0.0),
1104 Point3::new(1.0, 0.0, 0.0),
1105 Point3::new(2.0, 0.0, 0.0),
1106 );
1107 assert!(triangle2.is_degenerate());
1108 }
1109
1110 #[test]
1111 fn test_triangle_equality() {
1112 let triangle1 = Triangle::new(
1113 Point3::new(0.0, 0.0, 0.0),
1114 Point3::new(1.0, 0.0, 0.0),
1115 Point3::new(0.0, 1.0, 0.0),
1116 );
1117
1118 let triangle2 = Triangle::new(
1119 Point3::new(0.0, 0.0, 0.0),
1120 Point3::new(1.0, 0.0, 0.0),
1121 Point3::new(0.0, 1.0, 0.0),
1122 );
1123
1124 let triangle3 = Triangle::new(
1125 Point3::new(0.0, 0.0, 0.0),
1126 Point3::new(1.0, 0.0, 0.0),
1127 Point3::new(0.0, 2.0, 0.0),
1128 );
1129
1130 assert_eq!(triangle1, triangle2);
1131 assert_ne!(triangle1, triangle3);
1132 }
1133
1134 #[test]
1135 fn test_triangle_aabb() {
1136 let triangle = Triangle::new(
1137 Point3::new(0.0, 0.0, 0.0),
1138 Point3::new(1.0, 2.0, 1.0),
1139 Point3::new(0.0, 1.0, 0.0),
1140 );
1141
1142 let aabb = triangle.compute_aabb();
1143 assert_eq!(aabb.min(), Point3::new(0.0, 0.0, 0.0));
1144 assert_eq!(aabb.max(), Point3::new(1.0, 2.0, 1.0));
1145 }
1146}