all-is-cubes 0.5.0

Recursive voxel game engine. Can be used for voxel raytracing.
Documentation
//! Axis-aligned unit vectors: the [`Face6`] and [`Face7`] types.
//! This module is private but reexported by its parent.

use std::ops::{Index, IndexMut};

use cgmath::{BaseNum, Transform, Vector3};
pub use ordered_float::{FloatIsNan, NotNan};

use crate::math::*;

/// Identifies a face of a cube or an orthogonal unit vector.
///
/// See also the similar type [`Face7`], which adds a “zero” or “within the cube”
/// variant. The two enums use the same discriminant numbering.
#[allow(clippy::upper_case_acronyms)]
#[allow(clippy::exhaustive_enums)]
#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, exhaust::Exhaust)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[repr(u8)]
pub enum Face6 {
    /// Negative X; the face whose normal vector is `(-1, 0, 0)`.
    NX = 1,
    /// Negative Y; the face whose normal vector is `(0, -1, 0)`; downward.
    NY = 2,
    /// Negative Z; the face whose normal vector is `(0, 0, -1)`.
    NZ = 3,
    /// Positive X; the face whose normal vector is `(1, 0, 0)`.
    PX = 4,
    /// Positive Y; the face whose normal vector is `(0, 1, 0)`; upward.
    PY = 5,
    /// Positive Z; the face whose normal vector is `(0, 0, 1)`.
    PZ = 6,
}

/// Identifies a face of a cube or an orthogonal unit vector, except for
/// [`Within`](Face7::Within) meaning “zero distance and undefined direction”.
///
/// This is essentially `Option<`[`Face6`]`>`, except with `Face`-specific methods
/// provided. The two enums use the same discriminant numbering.
#[allow(clippy::upper_case_acronyms)]
#[allow(clippy::exhaustive_enums)]
#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, exhaust::Exhaust)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[repr(u8)]
pub enum Face7 {
    /// The interior volume of a cube, or an undefined direction. Corresponds to the vector `(0, 0, 0)`.
    Within = 0,
    /// Negative X; the face whose normal vector is `(-1, 0, 0)`.
    NX,
    /// Negative Y; the face whose normal vector is `(0, -1, 0)`; downward.
    NY,
    /// Negative Z; the face whose normal vector is `(0, 0, -1)`.
    NZ,
    /// Positive X; the face whose normal vector is `(1, 0, 0)`.
    PX,
    /// Positive Y; the face whose normal vector is `(0, 1, 0)`; upward.
    PY,
    /// Positive Z; the face whose normal vector is `(0, 0, 1)`.
    PZ,
}

impl Face6 {
    /// All the values of [`Face6`].
    pub const ALL: [Face6; 6] = [
        Face6::NX,
        Face6::NY,
        Face6::NZ,
        Face6::PX,
        Face6::PY,
        Face6::PZ,
    ];

    /// Inverse function of `face as u8`, converting the number to [`Face6`].
    #[inline]
    pub const fn from_discriminant(d: u8) -> Option<Self> {
        match d {
            1 => Some(Self::NX),
            2 => Some(Self::NY),
            3 => Some(Self::NZ),
            4 => Some(Self::PX),
            5 => Some(Self::PY),
            6 => Some(Self::PZ),
            _ => None,
        }
    }

    /// Returns which axis this face's normal vector is parallel to, with the numbering
    /// X = 0, Y = 1, Z = 2, which matches the indexes used by most arrays.
    ///
    /// The numeric type is [`usize`] for convenient use in array indexing.
    #[inline]
    #[must_use]
    pub const fn axis_number(self) -> usize {
        match self {
            Self::NX | Self::PX => 0,
            Self::NY | Self::PY => 1,
            Self::NZ | Self::PZ => 2,
        }
    }

    /// Returns whether this face is a “positive” face: one whose unit vector's nonzero
    /// coordinate is positive.
    ///
    /// ```
    /// use all_is_cubes::math::Face6;
    ///
    /// assert_eq!(Face6::PX.is_positive(), true);
    /// assert_eq!(Face6::NX.is_positive(), false);
    /// ```
    #[inline]
    pub fn is_positive(self) -> bool {
        matches!(self, Self::PX | Self::PY | Self::PZ)
    }

    /// Returns whether this face is a negative face: one whose unit vector's nonzero
    /// coordinate is negative.
    ///
    /// ```
    /// use all_is_cubes::math::Face6;
    ///
    /// assert_eq!(Face6::PX.is_negative(), false);
    /// assert_eq!(Face6::NX.is_negative(), true);
    /// ```
    #[inline]
    pub fn is_negative(self) -> bool {
        matches!(self, Self::NX | Self::NY | Self::NZ)
    }

    /// Returns the opposite face (maps [`PX`](Self::PX) to [`NX`](Self::NX) and so on).
    #[inline]
    #[must_use]
    pub const fn opposite(self) -> Face6 {
        match self {
            Face6::NX => Face6::PX,
            Face6::NY => Face6::PY,
            Face6::NZ => Face6::PZ,
            Face6::PX => Face6::NX,
            Face6::PY => Face6::NY,
            Face6::PZ => Face6::NZ,
        }
    }

    /// Returns the face whose normal is the cross product of these faces' normals.
    /// Since cross products may be zero, the result is a [`Face7`].
    ///
    /// ```
    /// use all_is_cubes::math::Face6;
    ///
    /// for face1 in Face6::ALL {
    ///     for face2 in Face6::ALL {
    ///         // Cross product of faces is identical to cross product of vectors.
    ///         assert_eq!(
    ///             face1.cross(face2).normal_vector::<f64>(),
    ///             face1.normal_vector().cross(face2.normal_vector()),
    ///             "{:?} cross {:?}", face1, face2,
    ///         );
    ///     }
    /// }
    /// ```
    #[inline]
    #[must_use]
    pub const fn cross(self, other: Self) -> Face7 {
        self.into7().cross(other.into7())
    }
    /// Returns the axis-aligned unit vector normal to this face.
    #[inline]
    #[must_use]
    pub fn normal_vector<S>(self) -> Vector3<S>
    where
        S: BaseNum + std::ops::Neg<Output = S>,
    {
        self.into7().normal_vector()
    }

    /// Dot product of this face as a unit vector and the given vector,
    /// implemented by selecting the relevant component.
    ///
    /// ```
    /// use cgmath::{Vector3, InnerSpace};
    /// use all_is_cubes::math::Face6;
    ///
    /// let sample_vector = Vector3::new(1.0, 2.0, 5.0_f64);
    /// for face in Face6::ALL {
    ///     assert_eq!(face.dot(sample_vector), face.normal_vector().dot(sample_vector));
    /// }
    /// ```
    #[inline]
    #[must_use]
    pub fn dot<S>(self, vector: Vector3<S>) -> S
    where
        S: Zero + std::ops::Neg<Output = S>,
    {
        self.into7().dot(vector)
    }

    /// Returns a homogeneous transformation matrix which, if given points on the square
    /// with x ∈ [0, scale], y ∈ [0, scale] and z = 0, converts them to points that lie
    /// on the faces of the cube with x ∈ [0, scale], y ∈ [0, scale], and z ∈ [0, scale].
    ///
    /// Specifically, `Face6::NZ.gmatrix()` is the identity matrix and all others are
    /// consistent with that. Note that there are arbitrary choices in the rotation
    /// of all other faces. (TODO: Document those choices and test them.)
    ///
    /// To work with floating-point coordinates, use `.matrix(1).to_free()`.
    #[must_use]
    pub const fn matrix(self, scale: GridCoordinate) -> GridMatrix {
        self.into7().matrix(scale)
    }

    /// Helper to convert in const context; equivalent to `.into()`.
    #[inline]
    const fn into7(self) -> Face7 {
        match self {
            Face6::NX => Face7::NX,
            Face6::NY => Face7::NY,
            Face6::NZ => Face7::NZ,
            Face6::PX => Face7::PX,
            Face6::PY => Face7::PY,
            Face6::PZ => Face7::PZ,
        }
    }
}

impl Face7 {
    /// All the values of [`Face7`], with [`Face7::Within`] listed first.
    pub const ALL: [Face7; 7] = [
        Face7::Within,
        Face7::NX,
        Face7::NY,
        Face7::NZ,
        Face7::PX,
        Face7::PY,
        Face7::PZ,
    ];

    /// Inverse function of `face as u8`, converting the number to [`Face7`].
    #[inline]
    pub const fn from_discriminant(d: u8) -> Option<Self> {
        match d {
            0 => Some(Self::Within),
            1 => Some(Self::NX),
            2 => Some(Self::NY),
            3 => Some(Self::NZ),
            4 => Some(Self::PX),
            5 => Some(Self::PY),
            6 => Some(Self::PZ),
            _ => None,
        }
    }

    /// Returns which axis this face's normal vector is parallel to, with the numbering
    /// X = 0, Y = 1, Z = 2, or [`None`] if the face is [`Face7::Within`].
    ///
    /// The numeric type is [`usize`] for convenient use in array indexing.
    #[inline]
    #[must_use]
    pub const fn axis_number(self) -> Option<usize> {
        match self {
            Face7::Within => None,
            Face7::NX | Face7::PX => Some(0),
            Face7::NY | Face7::PY => Some(1),
            Face7::NZ | Face7::PZ => Some(2),
        }
    }

    /// Returns whether this face is a “positive” face: one whose unit vector's nonzero
    /// coordinate is positive.
    ///
    /// ```
    /// use all_is_cubes::math::Face7;
    ///
    /// assert_eq!(Face7::PX.is_positive(), true);
    /// assert_eq!(Face7::NX.is_positive(), false);
    /// assert_eq!(Face7::Within.is_positive(), false);
    /// ```
    #[inline]
    pub fn is_positive(self) -> bool {
        matches!(self, Face7::PX | Face7::PY | Face7::PZ)
    }

    /// Returns whether this face is a negative face: one whose unit vector's nonzero
    /// coordinate is negative.
    ///
    /// ```
    /// use all_is_cubes::math::Face7;
    ///
    /// assert_eq!(Face7::PX.is_negative(), false);
    /// assert_eq!(Face7::NX.is_negative(), true);
    /// assert_eq!(Face7::Within.is_negative(), false);
    /// ```
    #[inline]
    pub fn is_negative(self) -> bool {
        matches!(self, Face7::NX | Face7::NY | Face7::NZ)
    }

    /// Returns the opposite face (maps [`PX`](Self::PX) to [`NX`](Self::NX) and so on).
    #[inline]
    #[must_use]
    pub const fn opposite(self) -> Face7 {
        match self {
            Face7::Within => Face7::Within,
            Face7::NX => Face7::PX,
            Face7::NY => Face7::PY,
            Face7::NZ => Face7::PZ,
            Face7::PX => Face7::NX,
            Face7::PY => Face7::NY,
            Face7::PZ => Face7::NZ,
        }
    }

    /// Returns the face whose normal is the cross product of these faces' normals.
    ///
    /// ```
    /// use all_is_cubes::math::Face7;
    ///
    /// for face1 in Face7::ALL {
    ///     for face2 in Face7::ALL {
    ///         // Cross product of faces is identical to cross product of vectors.
    ///         assert_eq!(
    ///             face1.cross(face2).normal_vector::<f64>(),
    ///             face1.normal_vector().cross(face2.normal_vector()),
    ///             "{:?} cross {:?}", face1, face2,
    ///         );
    ///     }
    /// }
    /// ```
    #[inline]
    #[must_use]
    pub const fn cross(self, other: Self) -> Self {
        use Face7::*;
        match (self, other) {
            // Zero input
            (Within, _) => Within,
            (_, Within) => Within,

            // Equal vectors
            (Face7::NX, Face7::NX) => Within,
            (Face7::NY, Face7::NY) => Within,
            (Face7::NZ, Face7::NZ) => Within,
            (Face7::PX, Face7::PX) => Within,
            (Face7::PY, Face7::PY) => Within,
            (Face7::PZ, Face7::PZ) => Within,

            // Opposite vectors
            (Face7::NX, Face7::PX) => Within,
            (Face7::NY, Face7::PY) => Within,
            (Face7::NZ, Face7::PZ) => Within,
            (Face7::PX, Face7::NX) => Within,
            (Face7::PY, Face7::NY) => Within,
            (Face7::PZ, Face7::NZ) => Within,

            (Face7::NX, Face7::NY) => PZ,
            (Face7::NX, Face7::NZ) => NY,
            (Face7::NX, Face7::PY) => NZ,
            (Face7::NX, Face7::PZ) => PY,

            (Face7::NY, Face7::NX) => NZ,
            (Face7::NY, Face7::NZ) => PX,
            (Face7::NY, Face7::PX) => PZ,
            (Face7::NY, Face7::PZ) => NX,

            (Face7::NZ, Face7::NX) => PY,
            (Face7::NZ, Face7::NY) => NX,
            (Face7::NZ, Face7::PX) => NY,
            (Face7::NZ, Face7::PY) => PX,

            (Face7::PX, Face7::NY) => NZ,
            (Face7::PX, Face7::NZ) => PY,
            (Face7::PX, Face7::PY) => PZ,
            (Face7::PX, Face7::PZ) => NY,

            (Face7::PY, Face7::NX) => PZ,
            (Face7::PY, Face7::NZ) => NX,
            (Face7::PY, Face7::PX) => NZ,
            (Face7::PY, Face7::PZ) => PX,

            (Face7::PZ, Face7::NX) => NY,
            (Face7::PZ, Face7::NY) => PX,
            (Face7::PZ, Face7::PX) => PY,
            (Face7::PZ, Face7::PY) => NX,
        }
    }

    /// Returns the vector normal to this face. [`Within`](Self::Within) is assigned the
    /// zero vector.
    #[inline]
    #[must_use]
    pub fn normal_vector<S>(self) -> Vector3<S>
    where
        S: BaseNum + std::ops::Neg<Output = S>,
    {
        match self {
            Face7::Within => Vector3::new(S::zero(), S::zero(), S::zero()),
            Face7::NX => Vector3::new(-S::one(), S::zero(), S::zero()),
            Face7::NY => Vector3::new(S::zero(), -S::one(), S::zero()),
            Face7::NZ => Vector3::new(S::zero(), S::zero(), -S::one()),
            Face7::PX => Vector3::new(S::one(), S::zero(), S::zero()),
            Face7::PY => Vector3::new(S::zero(), S::one(), S::zero()),
            Face7::PZ => Vector3::new(S::zero(), S::zero(), S::one()),
        }
    }

    /// Dot product of this face as a unit vector and the given vector,
    /// implemented by selecting the relevant component.
    ///
    /// ```
    /// use cgmath::{Vector3, InnerSpace};
    /// use all_is_cubes::math::Face7;
    ///
    /// let sample_vector = Vector3::new(1.0, 2.0, 5.0_f64);
    /// for face in Face7::ALL {
    ///     assert_eq!(face.dot(sample_vector), face.normal_vector().dot(sample_vector));
    /// }
    /// ```
    #[inline]
    #[must_use]
    pub fn dot<S>(self, vector: Vector3<S>) -> S
    where
        S: Zero + std::ops::Neg<Output = S>,
    {
        match self {
            Face7::Within => S::zero(),
            Face7::NX => -vector.x,
            Face7::NY => -vector.y,
            Face7::NZ => -vector.z,
            Face7::PX => vector.x,
            Face7::PY => vector.y,
            Face7::PZ => vector.z,
        }
    }

    /// Returns a homogeneous transformation matrix which, if given points on the square
    /// with x ∈ [0, scale], y ∈ [0, scale] and z = 0, converts them to points that lie
    /// on the faces of the cube with x ∈ [0, scale], y ∈ [0, scale], and z ∈ [0, scale].
    ///
    /// Specifically, `Face7::NZ.gmatrix()` is the identity matrix and all others are
    /// consistent with that. Note that there are arbitrary choices in the rotation
    /// of all other faces. (TODO: Document those choices and test them.)
    ///
    /// To work with floating-point coordinates, use `.matrix(1).to_free()`.
    #[rustfmt::skip]
    #[must_use]
    pub const fn matrix(self, scale: GridCoordinate) -> GridMatrix {
        match self {
            Face7::Within => GridMatrix::ZERO,
            Face7::NX => GridMatrix::new(
                0, 1, 0,
                0, 0, 1,
                1, 0, 0,
                0, 0, 0,
            ),
            Face7::NY => GridMatrix::new(
                0, 0, 1,
                1, 0, 0,
                0, 1, 0,
                0, 0, 0,
            ),
            Face7::NZ => GridMatrix::new(
                // Z face leaves X and Y unchanged!
                1, 0, 0,
                0, 1, 0,
                0, 0, 1,
                0, 0, 0,
            ),
            // Positives are same as negatives but with translation and an arbitrary choice of rotation.
            // PX rotates about Y.
            Face7::PX => GridMatrix::new(
                0, -1, 0,
                0, 0, 1,
                -1, 0, 0,
                scale, scale, 0,
            ),
            // PY rotates about X.
            Face7::PY => GridMatrix::new(
                0, 0, 1,
                -1, 0, 0,
                0, -1, 0,
                scale, scale, 0,
            ),
            // PZ rotates about Y.
            Face7::PZ => GridMatrix::new(
                1, 0, 0,
                0, -1, 0,
                0, 0, -1,
                0, scale, scale,
            ),
        }
    }
}

impl From<Face6> for Face7 {
    #[inline]
    fn from(value: Face6) -> Self {
        value.into7()
    }
}
impl TryFrom<Face7> for Face6 {
    type Error = Faceless;
    #[inline]
    fn try_from(value: Face7) -> Result<Face6, Self::Error> {
        match value {
            Face7::Within => Err(Faceless),
            Face7::NX => Ok(Face6::NX),
            Face7::NY => Ok(Face6::NY),
            Face7::NZ => Ok(Face6::NZ),
            Face7::PX => Ok(Face6::PX),
            Face7::PY => Ok(Face6::PY),
            Face7::PZ => Ok(Face6::PZ),
        }
    }
}

impl TryFrom<GridVector> for Face6 {
    /// Returns the original vector on failure.
    /// (An error message would probably be too lacking context to be helpful.)
    type Error = GridVector;

    /// Recovers a `Face6` from its corresponding unit normal vector. All other vectors
    /// are rejected.
    ///
    /// ```
    /// use all_is_cubes::math::{Face6, GridVector};
    ///
    /// // A Face6 may be converted from its normal vector.
    /// for face in Face6::ALL {
    ///     assert_eq!(Face6::try_from(face.normal_vector()), Ok(face));
    /// }
    ///
    /// // If the vector does not correspond to any Face6, it is returned.
    /// let v = GridVector::new(1, 2, 3);
    /// assert_eq!(Face6::try_from(v), Err(v));
    /// ```
    #[inline]
    fn try_from(value: GridVector) -> Result<Self, Self::Error> {
        let f7 = Face7::try_from(value)?;
        Face6::try_from(f7).map_err(|_| value)
    }
}
impl TryFrom<GridVector> for Face7 {
    /// Returns the original vector on failure.
    /// (An error message would probably be too lacking context to be helpful.)
    type Error = GridVector;

    /// Recovers a [`Face7`] from its corresponding unit normal vector. All other vectors
    /// are rejected.
    ///
    /// ```
    /// use all_is_cubes::math::{Face7, GridVector};
    ///
    /// // A Face7 may be converted from its normal vector.
    /// for face in Face7::ALL {
    ///     assert_eq!(Face7::try_from(face.normal_vector()), Ok(face));
    /// }
    ///
    /// // If the vector does not correspond to any Face7, it is returned.
    /// let v = GridVector::new(1, 2, 3);
    /// assert_eq!(Face7::try_from(v), Err(v));
    /// ```
    fn try_from(value: GridVector) -> Result<Self, Self::Error> {
        use Face7::*;
        match value {
            GridVector { x: 0, y: 0, z: 0 } => Ok(Within),
            GridVector { x: 1, y: 0, z: 0 } => Ok(PX),
            GridVector { x: 0, y: 1, z: 0 } => Ok(PY),
            GridVector { x: 0, y: 0, z: 1 } => Ok(PZ),
            GridVector { x: -1, y: 0, z: 0 } => Ok(NX),
            GridVector { x: 0, y: -1, z: 0 } => Ok(NY),
            GridVector { x: 0, y: 0, z: -1 } => Ok(NZ),
            not_unit_vector => Err(not_unit_vector),
        }
    }
}

/// Error resulting from providing [`Face7::Within`] where a definite nonzero direction
/// is needed, such as converting to a [`Face6`].
#[derive(Copy, Clone, Debug, Eq, PartialEq, thiserror::Error)]
#[error("Face7::Within does not have a direction or axis")]
#[allow(clippy::exhaustive_structs)]
pub struct Faceless;

/// Container for values keyed by [`Face6`]s. Always holds exactly six elements.
#[allow(clippy::exhaustive_structs)]
#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub struct FaceMap<V> {
    /// The value whose key is [`Face6::NX`].
    pub nx: V,
    /// The value whose key is [`Face6::NY`].
    pub ny: V,
    /// The value whose key is [`Face6::NZ`].
    pub nz: V,
    /// The value whose key is [`Face6::PX`].
    pub px: V,
    /// The value whose key is [`Face6::PY`].
    pub py: V,
    /// The value whose key is [`Face6::PZ`].
    pub pz: V,
}

impl<V> FaceMap<V> {
    /// Constructs a [`FaceMap`] by using the provided function to compute
    /// a value for each [`Face6`] enum variant.
    #[inline]
    pub fn from_fn(mut f: impl FnMut(Face6) -> V) -> Self {
        Self {
            nx: f(Face6::NX),
            ny: f(Face6::NY),
            nz: f(Face6::NZ),
            px: f(Face6::PX),
            py: f(Face6::PY),
            pz: f(Face6::PZ),
        }
    }

    /// Constructs a [`FaceMap`] whose negative and positive directions are equal.
    // TODO: Evaluate whether this is a good API.
    #[inline]
    #[doc(hidden)] // used by all-is-cubes-content
    pub fn symmetric(values: impl Into<Vector3<V>>) -> Self
    where
        V: Default + Clone,
    {
        let values = values.into();
        Self {
            nx: values.x.clone(),
            px: values.x,
            ny: values.y.clone(),
            py: values.y,
            nz: values.z.clone(),
            pz: values.z,
        }
    }

    /// Returns a vector containing the values for each negative face.
    pub fn negatives(self) -> Vector3<V>
    where
        V: Copy,
    {
        Vector3::new(self.nx, self.ny, self.nz)
    }

    /// Returns a vector containing the values for each positive face.
    pub fn positives(self) -> Vector3<V>
    where
        V: Copy,
    {
        Vector3::new(self.px, self.py, self.pz)
    }

    /// Iterate over the map's key-value pairs by reference, in the same order as [`Face6::ALL`].
    pub fn iter(&self) -> impl Iterator<Item = (Face6, &V)> {
        Face6::ALL.iter().copied().map(move |f| (f, &self[f]))
    }

    /// Iterate over the map values by reference, in the same order as [`Face6::ALL`].
    pub fn values(&self) -> impl Iterator<Item = &V> {
        Face6::ALL.iter().copied().map(move |f| &self[f])
    }

    pub fn into_values(self) -> [V; 6] {
        [self.nx, self.ny, self.nz, self.px, self.py, self.pz]
    }

    pub fn into_values_iter(self) -> impl Iterator<Item = V> {
        // TODO: eliminate this as not really useful in Rust 2021
        self.into_values().into_iter()
    }

    /// Transform values.
    pub fn map<U>(self, mut f: impl FnMut(Face6, V) -> U) -> FaceMap<U> {
        FaceMap {
            nx: f(Face6::NX, self.nx),
            ny: f(Face6::NY, self.ny),
            nz: f(Face6::NZ, self.nz),
            px: f(Face6::PX, self.px),
            py: f(Face6::PY, self.py),
            pz: f(Face6::PZ, self.pz),
        }
    }

    /// Combine two [`FaceMap`]s using a function applied to each pair of corresponding values.
    pub fn zip<U, R>(self, other: FaceMap<U>, mut f: impl FnMut(Face6, V, U) -> R) -> FaceMap<R> {
        FaceMap {
            nx: f(Face6::NX, self.nx, other.nx),
            ny: f(Face6::NY, self.ny, other.ny),
            nz: f(Face6::NZ, self.nz, other.nz),
            px: f(Face6::PX, self.px, other.px),
            py: f(Face6::PY, self.py, other.py),
            pz: f(Face6::PZ, self.pz, other.pz),
        }
    }

    /// Returns this map with one entry's value replaced.
    ///
    /// This may be used for constructing a map with only one interesting entry:
    ///
    /// ```
    /// use all_is_cubes::math::{Face6, FaceMap};
    ///
    /// assert_eq!(
    ///     FaceMap::default().with(Face6::PY, 10),
    ///     {
    ///         let mut m = FaceMap::default();
    ///         m[Face6::PY] = 10;
    ///         m
    ///     },
    /// );
    /// ```
    #[must_use]
    pub fn with(mut self, face: Face6, value: V) -> Self {
        self[face] = value;
        self
    }

    /// Shuffle the values in this map according to the given rotation.
    #[must_use]
    pub fn rotate(self, rotation: GridRotation) -> Self {
        // TODO: Can we make this cleaner? (If GridRotation had a way to ask it what swaps
        // it corresponds to, that might also be useful for GridArray rotations.)
        let to_source = rotation.inverse();
        let mut source = self.map(|_, value| Some(value));
        Self::from_fn(|face| source[to_source.transform(face)].take().unwrap())
    }
}

impl<V: Clone> FaceMap<V> {
    /// Constructs a [`FaceMap`] containing clones of the provided value.
    #[inline]
    pub fn repeat(value: V) -> Self {
        Self {
            nx: value.clone(),
            ny: value.clone(),
            nz: value.clone(),
            px: value.clone(),
            py: value.clone(),
            pz: value,
        }
    }
}

impl<V: Copy> FaceMap<V> {
    /// Constructs a [`FaceMap`] containing copies of the provided value.
    ///
    /// This is practically identical to [`FaceMap::repeat()`] except that it is a
    /// `const fn`. It may be removed from future major versions once Rust supports const
    /// trait function calls.
    #[inline]
    pub const fn repeat_copy(value: V) -> Self {
        Self {
            nx: value,
            ny: value,
            nz: value,
            px: value,
            py: value,
            pz: value,
        }
    }
}

impl<V> Index<Face6> for FaceMap<V> {
    type Output = V;
    fn index(&self, face: Face6) -> &V {
        match face {
            Face6::NX => &self.nx,
            Face6::NY => &self.ny,
            Face6::NZ => &self.nz,
            Face6::PX => &self.px,
            Face6::PY => &self.py,
            Face6::PZ => &self.pz,
        }
    }
}

impl<V> IndexMut<Face6> for FaceMap<V> {
    fn index_mut(&mut self, face: Face6) -> &mut V {
        match face {
            Face6::NX => &mut self.nx,
            Face6::NY => &mut self.ny,
            Face6::NZ => &mut self.nz,
            Face6::PX => &mut self.px,
            Face6::PY => &mut self.py,
            Face6::PZ => &mut self.pz,
        }
    }
}

/// The combination of a [`GridPoint`] identifying a unit cube and a [`Face7`] identifying
/// one face of it. This pattern recurs in selection and collision detection.
#[derive(Clone, Copy, Hash, Eq, PartialEq)]
#[allow(clippy::exhaustive_structs)]
pub struct CubeFace {
    pub cube: GridPoint,
    pub face: Face7,
}

impl CubeFace {
    #[inline]
    pub fn new(cube: impl Into<GridPoint>, face: Face7) -> Self {
        Self {
            cube: cube.into(),
            face,
        }
    }

    /// Computes the cube that is adjacent in the direction of [`self.face`](Self::face).
    /// Equal to [`self.cube`](Self::cube) if the face is [`Face7::Within`].
    #[inline]
    pub fn adjacent(self) -> GridPoint {
        self.cube + self.face.normal_vector()
    }
}

impl fmt::Debug for CubeFace {
    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            fmt,
            "CubeFace({:?}, {:?})",
            self.cube.custom_format(ConciseDebug),
            self.face,
        )
    }
}

// TODO: This is a quick kludge to get some debug rendering going. We should offer more controls, probably
impl Geometry for CubeFace {
    type Coord = GridCoordinate;

    fn translate(mut self, offset: Vector3<Self::Coord>) -> Self {
        self.cube += offset;
        self
    }

    fn wireframe_points<E>(&self, output: &mut E)
    where
        E: Extend<(Point3<FreeCoordinate>, Option<Rgba>)>,
    {
        // TODO: How much to offset the lines should be a parameter of the wireframe_points process.
        let expansion = 0.005;
        let aab = Aab::from_cube(self.cube).expand(expansion);
        aab.wireframe_points(output);

        // Draw an X on the face.
        let face_matrix = self.face.matrix(1);
        const X_POINTS: [GridPoint; 4] = [
            Point3::new(0, 0, 0),
            Point3::new(1, 1, 0),
            Point3::new(1, 0, 0),
            Point3::new(0, 1, 0),
        ];
        // TODO: this is a messy kludge and really we should be stealing corner points
        // from the AAB instead, but there isn't yet a good way to do that.
        output.extend(X_POINTS.into_iter().map(|p| {
            (
                (face_matrix.transform_point(p))
                    .map(|c| (FreeCoordinate::from(c) - 0.5) * (1. + expansion * 2.) + 0.5)
                    + self.cube.to_vec().map(FreeCoordinate::from),
                None,
            )
        }));
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use cgmath::SquareMatrix as _;

    #[test]
    fn face_matrix_does_not_scale_or_reflect() {
        for face in Face6::ALL {
            assert_eq!(1.0, face.matrix(7).to_free().determinant());
            assert_eq!(1.0, Face7::from(face).matrix(7).to_free().determinant());
        }
    }

    // TODO: More tests of face.matrix()

    #[test]
    fn face_map_iter_in_enum_order() {
        // TODO: Maybe generalize this to _all_ the Face/FaceMap methods that have an ordering?
        let map = FaceMap::from_fn(|f| f);
        assert_eq!(
            Face6::ALL.to_vec(),
            map.iter().map(|(_, &v)| v).collect::<Vec<_>>(),
        );
        assert_eq!(
            Face6::ALL.to_vec(),
            map.values().copied().collect::<Vec<_>>(),
        );
    }

    #[test]
    fn face_map_rotate() {
        assert_eq!(
            FaceMap {
                nx: 10,
                px: 20,
                ny: 11,
                py: 21,
                nz: 12,
                pz: 22,
            }.rotate(GridRotation::RyXZ),
            FaceMap {
                nx: 11,
                px: 21,
                ny: 20,
                py: 10,
                nz: 12,
                pz: 22,
            }
        )
    }

    // TODO: More tests of FaceMap

    #[test]
    fn cubeface_format() {
        let cube_face = CubeFace {
            cube: GridPoint::new(1, 2, 3),
            face: Face7::NY,
        };
        assert_eq!(&format!("{cube_face:#?}"), "CubeFace((+1, +2, +3), NY)");
    }
}