Skip to main content

vector_traits/
plane.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2// Copyright (c) 2023, 2025 lacklustr@protonmail.com https://github.com/eadf
3
4// This file is part of vector-traits.
5
6use crate::prelude::*;
7use approx::{AbsDiffEq, UlpsEq};
8use num_traits::Float;
9use std::borrow::Borrow;
10
11/// Axis aligned planes, used to describe how imported 'flat' data is arranged in space
12#[allow(clippy::upper_case_acronyms)]
13#[derive(Debug, Copy, Clone, Eq, PartialEq)]
14pub enum Plane {
15    XY,
16    XZ,
17    YZ,
18}
19
20impl Plane {
21    #[allow(dead_code)] // this code is used by all the x_impls
22    #[inline(always)]
23    /// Try to figure out what axes defines the plane.
24    /// If the AABB delta of one axis (a) is virtually nothing compared to
25    /// the widest axis (b) while the third axis (c) is comparable to (b)
26    /// by some fraction, we assume that (a) isn't part of the plane.
27    ///
28    /// It's not possible to compare to zero exactly because blender
29    /// leaves some decimal in coordinates that's supposed to be zero.
30    pub(crate) fn get_plane<T>(aabb: &T) -> Option<Self>
31    where
32        T: Aabb3,
33        T::Vector: GenericVector3<Aabb = T> + HasXY,
34    {
35        Plane::get_plane_relaxed::<T::Vector>(
36            aabb,
37            <<T as Aabb3>::Vector as HasXY>::Scalar::default_epsilon(),
38            <<T as Aabb3>::Vector as HasXY>::Scalar::default_max_ulps(),
39        )
40    }
41
42    #[allow(dead_code)] // this code is used by all the x_impls
43    /// Try to figure out what axes defines the plane.
44    /// If the AABB delta of one axis (a) is virtually nothing compared to
45    /// the widest axis (b) while the third axis (c) is comparable to (b)
46    /// by some fraction, we assume that (a) isn't part of the plane.
47    ///
48    /// It's not possible to compare to zero exactly because blender
49    /// leaves some decimal in coordinates that's supposed to be zero.
50    pub(crate) fn get_plane_relaxed<T>(
51        aabb: &T::Aabb,
52        epsilon: T::Scalar,
53        max_ulps: u32,
54    ) -> Option<Plane>
55    where
56        T: GenericVector3,
57        T::Aabb: Aabb3<Vector = T>,
58    {
59        if aabb.is_empty() {
60            return None;
61        }
62
63        let (_, _, delta) = aabb.extents();
64        let max_delta = T::Scalar::max(T::Scalar::max(delta.x(), delta.y()), delta.z());
65
66        // Early exit if the AABB is degenerate (all dimensions near zero)
67        if T::Scalar::ZERO.ulps_eq(&max_delta, epsilon, max_ulps) {
68            return None;
69        }
70
71        // Normalize deltas by the largest dimension
72        let norm_dx = delta.x() / max_delta;
73        let norm_dy = delta.y() / max_delta;
74        let norm_dz = delta.z() / max_delta;
75
76        // Check if any dimension is negligible compared to the largest
77        let is_x_negligible = T::Scalar::ZERO.ulps_eq(&norm_dx, epsilon, max_ulps);
78        let is_y_negligible = T::Scalar::ZERO.ulps_eq(&norm_dy, epsilon, max_ulps);
79        let is_z_negligible = T::Scalar::ZERO.ulps_eq(&norm_dz, epsilon, max_ulps);
80
81        match (is_x_negligible, is_y_negligible, is_z_negligible) {
82            (true, false, false) => Some(Plane::YZ), // X is negligible => plane is YZ
83            (false, true, false) => Some(Plane::XZ), // Y is negligible => plane is XZ
84            (false, false, true) => Some(Plane::XY), // Z is negligible => plane is XY
85            _ => None, // No single negligible dimension (could be 3D or ambiguous)
86        }
87    }
88
89    /// Copy this Point2 into a Point3, populating the axes defined by 'plane'
90    /// `Plane::XY`: `Point2(AB)` -> `Point3(AB0)`
91    /// `Plane::XZ`: `Point2(AB)` -> `Point3(A0B)`
92    /// `Plane::YZ`: `Point2(AB)` -> `Point3(0BA)`
93    #[inline(always)]
94    pub fn point_to_3d<T: GenericVector3>(&self, point: T::Vector2) -> T {
95        match self {
96            Plane::XY => T::new_3d(point.x(), point.y(), T::Scalar::ZERO),
97            Plane::XZ => T::new_3d(point.x(), T::Scalar::ZERO, point.y()),
98            Plane::YZ => T::new_3d(T::Scalar::ZERO, point.y(), point.x()),
99        }
100    }
101
102    /// Copy this Point3 into a Point2, populating the axes defined by 'plane'
103    /// `Plane::XY`: `Point3(ABC)` -> `Point2(AB)`
104    /// `Plane::XZ`: `Point3(ABC)` -> `Point2(AC)`
105    /// `Plane::YZ`: `Point3(ABC)` -> `Point2(CB)`
106    #[inline(always)]
107    pub fn point_to_2d<T: GenericVector3>(&self, point: T) -> T::Vector2 {
108        match self {
109            Plane::XY => T::Vector2::new_2d(point.x(), point.y()),
110            Plane::XZ => T::Vector2::new_2d(point.x(), point.z()),
111            Plane::YZ => T::Vector2::new_2d(point.z(), point.y()),
112        }
113    }
114
115    /// Convert an iterator of 2D points into 3D points based on the plane.
116    /// This works like `.map()`, but optimized for the plane.
117    /// `Plane::XY`: `Point2(AB)` -> `Point3(AB0)`
118    /// `Plane::XZ`: `Point2(AB)` -> `Point3(A0B)`
119    /// `Plane::YZ`: `Point2(AB)` -> `Point3(0BA)`
120    #[inline]
121    pub fn points_to_3d<'a, T: GenericVector3 + 'a, I>(
122        &self,
123        iter: I,
124    ) -> impl Iterator<Item = T> + 'a
125    where
126        I: IntoIterator + 'a,
127        I::Item: Borrow<T::Vector2>,
128        <T as GenericVector3>::Vector2: 'a + ToOwned<Owned = T::Vector2>,
129    {
130        let mapper = match self {
131            Plane::XY => |p: T::Vector2| T::new_3d(p.x(), p.y(), T::Scalar::ZERO),
132            Plane::XZ => |p: T::Vector2| T::new_3d(p.x(), T::Scalar::ZERO, p.y()),
133            Plane::YZ => |p: T::Vector2| T::new_3d(T::Scalar::ZERO, p.y(), p.x()),
134        };
135
136        iter.into_iter().map(move |v| mapper(v.borrow().to_owned()))
137    }
138
139    /// Convert an iterator of 3D points into 2D points based on the plane.
140    /// This works like `.map()`, but optimized for the plane.
141    /// `Plane::XY`: `Point3(ABC)` -> `Point2(AB)`
142    /// `Plane::XZ`: `Point3(ABC)` -> `Point2(AC)`
143    /// `Plane::YZ`: `Point3(ABC)` -> `Point2(CB)`
144    #[inline]
145    pub fn points_to_2d<'a, T: GenericVector2 + 'a, I>(
146        &self,
147        iter: I,
148    ) -> impl Iterator<Item = T> + 'a
149    where
150        I: IntoIterator + 'a,
151        I::Item: Borrow<T::Vector3>,
152        <T as GenericVector2>::Vector3: 'a + ToOwned<Owned = T::Vector3>,
153    {
154        let mapper = match self {
155            Plane::XY => |p: T::Vector3| T::new_2d(p.x(), p.y()),
156            Plane::XZ => |p: T::Vector3| T::new_2d(p.x(), p.z()),
157            Plane::YZ => |p: T::Vector3| T::new_2d(p.z(), p.y()),
158        };
159        iter.into_iter().map(move |v| mapper(v.borrow().to_owned()))
160    }
161}