vector-traits 0.6.2

Rust traits for 2D and 3D vector types.
Documentation
// SPDX-License-Identifier: MIT OR Apache-2.0
// Copyright (c) 2023, 2025 lacklustr@protonmail.com https://github.com/eadf

// This file is part of vector-traits.

use crate::prelude::*;
use approx::{AbsDiffEq, UlpsEq};
use num_traits::Float;
use std::borrow::Borrow;

/// Axis aligned planes, used to describe how imported 'flat' data is arranged in space
#[allow(clippy::upper_case_acronyms)]
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum Plane {
    XY,
    XZ,
    YZ,
}

impl Plane {
    #[allow(dead_code)] // this code is used by all the x_impls
    #[inline(always)]
    /// Try to figure out what axes defines the plane.
    /// If the AABB delta of one axis (a) is virtually nothing compared to
    /// the widest axis (b) while the third axis (c) is comparable to (b)
    /// by some fraction, we assume that (a) isn't part of the plane.
    ///
    /// It's not possible to compare to zero exactly because blender
    /// leaves some decimal in coordinates that's supposed to be zero.
    pub(crate) fn get_plane<T>(aabb: &T) -> Option<Self>
    where
        T: Aabb3,
        T::Vector: GenericVector3<Aabb = T> + HasXY,
    {
        Plane::get_plane_relaxed::<T::Vector>(
            aabb,
            <<T as Aabb3>::Vector as HasXY>::Scalar::default_epsilon(),
            <<T as Aabb3>::Vector as HasXY>::Scalar::default_max_ulps(),
        )
    }

    #[allow(dead_code)] // this code is used by all the x_impls
    /// Try to figure out what axes defines the plane.
    /// If the AABB delta of one axis (a) is virtually nothing compared to
    /// the widest axis (b) while the third axis (c) is comparable to (b)
    /// by some fraction, we assume that (a) isn't part of the plane.
    ///
    /// It's not possible to compare to zero exactly because blender
    /// leaves some decimal in coordinates that's supposed to be zero.
    pub(crate) fn get_plane_relaxed<T>(
        aabb: &T::Aabb,
        epsilon: T::Scalar,
        max_ulps: u32,
    ) -> Option<Plane>
    where
        T: GenericVector3,
        T::Aabb: Aabb3<Vector = T>,
    {
        if aabb.is_empty() {
            return None;
        }

        let (_, _, delta) = aabb.extents();
        let max_delta = T::Scalar::max(T::Scalar::max(delta.x(), delta.y()), delta.z());

        // Early exit if the AABB is degenerate (all dimensions near zero)
        if T::Scalar::ZERO.ulps_eq(&max_delta, epsilon, max_ulps) {
            return None;
        }

        // Normalize deltas by the largest dimension
        let norm_dx = delta.x() / max_delta;
        let norm_dy = delta.y() / max_delta;
        let norm_dz = delta.z() / max_delta;

        // Check if any dimension is negligible compared to the largest
        let is_x_negligible = T::Scalar::ZERO.ulps_eq(&norm_dx, epsilon, max_ulps);
        let is_y_negligible = T::Scalar::ZERO.ulps_eq(&norm_dy, epsilon, max_ulps);
        let is_z_negligible = T::Scalar::ZERO.ulps_eq(&norm_dz, epsilon, max_ulps);

        match (is_x_negligible, is_y_negligible, is_z_negligible) {
            (true, false, false) => Some(Plane::YZ), // X is negligible => plane is YZ
            (false, true, false) => Some(Plane::XZ), // Y is negligible => plane is XZ
            (false, false, true) => Some(Plane::XY), // Z is negligible => plane is XY
            _ => None, // No single negligible dimension (could be 3D or ambiguous)
        }
    }

    /// Copy this Point2 into a Point3, populating the axes defined by 'plane'
    /// `Plane::XY`: `Point2(AB)` -> `Point3(AB0)`
    /// `Plane::XZ`: `Point2(AB)` -> `Point3(A0B)`
    /// `Plane::YZ`: `Point2(AB)` -> `Point3(0BA)`
    #[inline(always)]
    pub fn point_to_3d<T: GenericVector3>(&self, point: T::Vector2) -> T {
        match self {
            Plane::XY => T::new_3d(point.x(), point.y(), T::Scalar::ZERO),
            Plane::XZ => T::new_3d(point.x(), T::Scalar::ZERO, point.y()),
            Plane::YZ => T::new_3d(T::Scalar::ZERO, point.y(), point.x()),
        }
    }

    /// Copy this Point3 into a Point2, populating the axes defined by 'plane'
    /// `Plane::XY`: `Point3(ABC)` -> `Point2(AB)`
    /// `Plane::XZ`: `Point3(ABC)` -> `Point2(AC)`
    /// `Plane::YZ`: `Point3(ABC)` -> `Point2(CB)`
    #[inline(always)]
    pub fn point_to_2d<T: GenericVector3>(&self, point: T) -> T::Vector2 {
        match self {
            Plane::XY => T::Vector2::new_2d(point.x(), point.y()),
            Plane::XZ => T::Vector2::new_2d(point.x(), point.z()),
            Plane::YZ => T::Vector2::new_2d(point.z(), point.y()),
        }
    }

    /// Convert an iterator of 2D points into 3D points based on the plane.
    /// This works like `.map()`, but optimized for the plane.
    /// `Plane::XY`: `Point2(AB)` -> `Point3(AB0)`
    /// `Plane::XZ`: `Point2(AB)` -> `Point3(A0B)`
    /// `Plane::YZ`: `Point2(AB)` -> `Point3(0BA)`
    #[inline]
    pub fn points_to_3d<'a, T: GenericVector3 + 'a, I>(
        &self,
        iter: I,
    ) -> impl Iterator<Item = T> + 'a
    where
        I: IntoIterator + 'a,
        I::Item: Borrow<T::Vector2>,
        <T as GenericVector3>::Vector2: 'a + ToOwned<Owned = T::Vector2>,
    {
        let mapper = match self {
            Plane::XY => |p: T::Vector2| T::new_3d(p.x(), p.y(), T::Scalar::ZERO),
            Plane::XZ => |p: T::Vector2| T::new_3d(p.x(), T::Scalar::ZERO, p.y()),
            Plane::YZ => |p: T::Vector2| T::new_3d(T::Scalar::ZERO, p.y(), p.x()),
        };

        iter.into_iter().map(move |v| mapper(v.borrow().to_owned()))
    }

    /// Convert an iterator of 3D points into 2D points based on the plane.
    /// This works like `.map()`, but optimized for the plane.
    /// `Plane::XY`: `Point3(ABC)` -> `Point2(AB)`
    /// `Plane::XZ`: `Point3(ABC)` -> `Point2(AC)`
    /// `Plane::YZ`: `Point3(ABC)` -> `Point2(CB)`
    #[inline]
    pub fn points_to_2d<'a, T: GenericVector2 + 'a, I>(
        &self,
        iter: I,
    ) -> impl Iterator<Item = T> + 'a
    where
        I: IntoIterator + 'a,
        I::Item: Borrow<T::Vector3>,
        <T as GenericVector2>::Vector3: 'a + ToOwned<Owned = T::Vector3>,
    {
        let mapper = match self {
            Plane::XY => |p: T::Vector3| T::new_2d(p.x(), p.y()),
            Plane::XZ => |p: T::Vector3| T::new_2d(p.x(), p.z()),
            Plane::YZ => |p: T::Vector3| T::new_2d(p.z(), p.y()),
        };
        iter.into_iter().map(move |v| mapper(v.borrow().to_owned()))
    }
}