From 112855343bb15ba98661d96899399601e887ac04 Mon Sep 17 00:00:00 2001 From: Kevin Reid Date: Fri, 8 Sep 2023 13:40:53 -0700 Subject: [PATCH] Generalize `GridArray` into `Vol` which allows borrowed data. Future work: allow more orderings (only the useful ones) and migrate away from the `GridArray` type (now a type alias). --- CHANGELOG.md | 2 + all-is-cubes/src/block/evaluated.rs | 2 +- all-is-cubes/src/math/vol.rs | 265 ++++++++++++++++++++-------- 3 files changed, 193 insertions(+), 76 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5f70e0a4..f7d3eb1c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,8 @@ - `space::Space::draw_target()` - `space::SpaceTransaction::draw_target()` + - `math::GridArray` is now `math::Vol` and allows choice of the data container type. + - `camera::HeadlessRenderer` now returns a custom image container type `Rendering` instead of using `image::RgbaImage`. (This way, no dependency on `image` is needed.) diff --git a/all-is-cubes/src/block/evaluated.rs b/all-is-cubes/src/block/evaluated.rs index 3ee65a76f..191450942 100644 --- a/all-is-cubes/src/block/evaluated.rs +++ b/all-is-cubes/src/block/evaluated.rs @@ -526,7 +526,7 @@ impl Evoxels { pub(crate) fn iter_mut(&mut self) -> impl Iterator { match self { Evoxels::One(v) => core::slice::from_mut(v).iter_mut(), - Evoxels::Many(_, voxels) => voxels.elements_mut().iter_mut(), + Evoxels::Many(_, voxels) => voxels.as_linear_mut().iter_mut(), } } diff --git a/all-is-cubes/src/math/vol.rs b/all-is-cubes/src/math/vol.rs index 7b8b3c657..6dfd6b6e8 100644 --- a/all-is-cubes/src/math/vol.rs +++ b/all-is-cubes/src/math/vol.rs @@ -1,32 +1,70 @@ use alloc::boxed::Box; use core::fmt; +use core::ops::{Deref, DerefMut}; -use crate::math::{Cube, GridAab, GridCoordinate, GridVector}; +#[cfg(doc)] +use alloc::vec::Vec; + +use crate::math::{Cube, GridAab, GridCoordinate, GridIter, GridVector}; + +// #[derive(Clone, Copy, Debug)] +// pub struct XMaj; + +/// Z-major ordering: linearly adjacent elements have adjacent Z coordinates. +/// +/// `[0, 0, 0], [0, 0, 1], [0, 0, 2], ..., [0, 1, 0], [0, 1, 1], ...` +/// +/// Use this type with [`Vol`] to store volume data in this order. +#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] +#[allow(clippy::exhaustive_structs)] +pub struct ZMaj; /// A 3-dimensional array with arbitrary element type instead of [`Space`](crate::space::Space)'s /// fixed types. +// --- +// TOOD: deprecate/replace this +pub type GridArray = Vol, ZMaj>; + +/// Type for volume data stored in a slice, or for generating linear indexing. +/// +/// * `C` is some slice container type, e.g. `&[T]` or `Box<[T]>`. +/// It may also be `()` to describe a linearization without actually storing data. +/// * `O` specifies the choice of linearization. +/// Currently, only one choice exists, [`ZMaj`]. +/// +/// In addition to the data, each [`Vol`] stores the [`GridAab`] defining its size; +/// the container's length must be equal to the volume of that AAB. Whether that can be +/// relied upon entirely depends on whether the specific container value produces +/// the same length of slice every time it is [`Deref`]erenced without mutating it directly. +/// For example, `Vec` and `Box<[T]>` satisfy this criterion; the fact that [`Vec`] has +/// length-mutating operations is irrelevant because no `&mut Vec` is exposed. /// -/// TODO: Should we rebuild Space on top of this? +/// A [`Vol`] whose volume exceeds [`usize::MAX`] cannot exist. #[derive(Clone, Debug, Eq, Hash, PartialEq)] // TODO: nondefault Debug -pub struct GridArray { +pub struct Vol { + /// Invariant: `bounds` has a volume that is at most [`usize::MAX`]. bounds: GridAab, - contents: Box<[V]>, + ordering: O, + /// Invariant: `contents.deref().len()`, if it exists, equals `bounds.volume()`. + contents: C, } -impl GridArray { - /// Constructs a [`GridArray`] by using the provided function to compute a value +/// Constructors from elements. +impl Vol, O> { + /// Constructs a `Vol>` by using the provided function to compute a value /// for each point. pub fn from_fn(bounds: GridAab, f: F) -> Self where F: FnMut(Cube) -> V, { - GridArray { + Vol { bounds, + ordering: O::default(), contents: bounds.interior_iter().map(f).collect(), } } - /// Constructs a [`GridArray`] by cloning the provided value for each point. + /// Constructs a `Vol>` by cloning the provided value for each point. /// /// TODO: This feels like it should be called 'filled' or 'cloned', but if so, /// maybe [`FaceMap::repeat`](crate::math::FaceMap::repeat) should also change? @@ -37,45 +75,46 @@ impl GridArray { Self::from_fn(bounds, |_| value.clone()) } - /// Constructs a [`GridArray`] with a single value, in bounds `ORIGIN_CUBE`. + /// Constructs a `Vol>` with a single value, in bounds `ORIGIN_CUBE`. /// /// If the single element should be at a different location, you can call - /// [`.translate(offset)`](Self::translate), or use [`GridArray::from_elements()`] + /// [`.translate(offset)`](Self::translate), or use [`Vol::from_elements()`] /// instead. pub fn from_element(value: V) -> Self { Self::from_elements(GridAab::ORIGIN_CUBE, [value]).unwrap() } - /// Constructs a [`GridArray`] containing the provided elements, which must be in the - /// ordering used by [`GridAab::interior_iter()`]. + /// Constructs a `Vol>` containing the provided elements, which must be in the + /// ordering specified by `O`. /// - /// Returns an [`ArrayLengthError`] if the number of elements does not match + /// Returns a [`VolLengthError`] if the number of elements does not match /// [`bounds.volume()`](GridAab::volume). pub fn from_elements( bounds: GridAab, elements: impl Into>, - ) -> Result { + ) -> Result { let elements = elements.into(); if elements.len() == bounds.volume() { - Ok(GridArray { + Ok(Vol { bounds, + ordering: O::default(), contents: elements, }) } else { - Err(ArrayLengthError { + Err(VolLengthError { input_length: elements.len(), bounds, }) } } - /// Constructs a [`GridArray`] from nested Rust arrays in [Z][Y[X] order with the Y axis + /// Constructs a [`Vol>`] from nested Rust arrays in [Z][Y][X] order with the Y axis /// mirrored. The result's bounds's lower bounds are zero. /// /// Note: The current implementation requires that `V` implement [`Clone`], and will /// clone each element once, but this may be improved in the future. // TODO: Decide if this is a good public interface. - // TODO: Reimplement this in terms of adopting the elements as a linear array, then performing an axis swap. + // TODO: Reimplement this in terms of adopting the elements as a linear array. // TODO: Test. #[doc(hidden)] // used by all-is-cubes-content pub fn from_y_flipped_array( @@ -96,32 +135,21 @@ impl GridArray { |p| array[p.z as usize][(DY - 1) - (p.y as usize)][p.x as usize].clone(), ) } +} - /// Returns the [`GridAab`] specifying the bounds of this array. +impl Vol { + /// Returns the [`GridAab`] specifying the bounds of this volume data. #[inline] pub fn bounds(&self) -> GridAab { self.bounds } - /// Returns the element at `position` of this array, or [`None`] if `position` is out - /// of bounds. - #[inline] - pub fn get(&self, position: impl Into) -> Option<&V> { - self.bounds - .index(position.into()) - .map(|index| &self.contents[index]) - } - - /// Returns a mutable reference to the element at `position` of this array, - /// or [`None`] if `position` is out of bounds. - #[inline] - pub fn get_mut(&mut self, position: impl Into) -> Option<&mut V> { - self.bounds - .index(position.into()) - .map(|index| &mut self.contents[index]) + /// Returns the linear contents without copying. + pub(crate) fn into_elements(self) -> C { + self.contents } - /// Adds to the origin of the array without affecting the contents. + /// Translates the volume without affecting its contents. /// /// Panics if this would cause numeric overflow. /// @@ -133,57 +161,138 @@ impl GridArray { if new_bounds.size() != self.bounds.size() { // We can't just continue like `GridAab::translate` does, because that would // break the invariant that self.bounds.volume() == self.contents.len(). - panic!("GridArray::translate() offset caused numeric overflow"); + panic!("Vol::translate() offset caused numeric overflow"); } self.bounds = new_bounds; self } +} + +impl Vol { + /// Iterate over all cubes that this contains, in the order of the linearization, + /// without including the stored data (if there is any). + pub fn iter_cubes(&self) -> GridIter { + self.bounds.interior_iter() + } +} + +/// Linear data access. +impl Vol +where + C: Deref, + O: Copy, +{ + /// Return a [`Vol`] that borrows the contents of this one. + pub fn as_ref(&self) -> Vol<&[V], O> { + Vol { + bounds: self.bounds, + ordering: self.ordering, + contents: self.as_linear(), + } + } + + /// Return a [`Vol`] that mutably borrows the contents of this one. + pub fn as_mut(&mut self) -> Vol<&mut [V], O> + where + C: DerefMut, + { + Vol { + bounds: self.bounds, + ordering: self.ordering, + contents: self.as_linear_mut(), + } + } - /// Iterates over all the cubes and values in this array, in the ordering used by - /// [`GridAab::interior_iter()`]. - pub fn iter(&self) -> impl Iterator { - self.bounds.interior_iter().zip(self.contents.iter()) + /// Returns the linear contents viewed as a slice. + pub fn as_linear(&self) -> &[V] { + let s = &*self.contents; + debug_assert_eq!(s.len(), self.bounds.volume()); + s } - /// Iterates over all the cubes and values in this array, in the ordering used by - /// [`GridAab::interior_iter()`]. - pub fn iter_mut(&mut self) -> impl Iterator { - self.bounds.interior_iter().zip(self.contents.iter_mut()) + /// Returns the linear contents viewed as a mutable slice. + pub fn as_linear_mut(&mut self) -> &mut [V] + where + C: DerefMut, + { + let s = &mut *self.contents; + debug_assert_eq!(s.len(), self.bounds.volume()); + s } +} - /// Returns mutable access to the contents. They are ordered in the same order that - /// [`GridArray::from_elements()`] expects. - pub(crate) fn elements_mut(&mut self) -> &mut [V] { - // Note that since we only return a reference to the _slice_, providing this access - // cannot break the length invariant. - &mut self.contents +/// Element lookup operations by 3D coordinates. +impl Vol +where + C: Deref, +{ + /// Returns the element at `position` of this volume data, or [`None`] if `position` is out + /// of bounds. + #[inline] + pub fn get(&self, position: impl Into) -> Option<&V> { + let index = self.bounds.index(position.into())?; + Some(&self.as_linear()[index]) } - /// Apply `f` to each element of the array, producing a new array of the results. - pub fn map(self, f: F) -> GridArray + /// Returns a mutable reference to the element at `position` of this volume data, + /// or [`None`] if `position` is out of bounds. + #[inline] + pub fn get_mut(&mut self, position: impl Into) -> Option<&mut V> + where + C: DerefMut, + { + let index = self.bounds.index(position.into())?; + Some(&mut self.as_linear_mut()[index]) + } + + /// Iterates over all the cubes and values in this volume data, in the ordering specified + /// by the `O` type parameter. + pub fn iter<'s>(&'s self) -> impl Iterator + where + V: 's, + { + self.bounds.interior_iter().zip(self.as_linear().iter()) + } + + /// Iterates by mutable reference over all the cubes and values in this volume data, + /// in the ordering specified by the `O` type parameter. + pub fn iter_mut<'s>(&'s mut self) -> impl Iterator + where + C: DerefMut, + V: 's, + { + self.bounds + .interior_iter() + .zip(self.as_linear_mut().iter_mut()) + } +} + +impl Vol, O> { + /// Apply `f` to each element and collect the results into the same shape and ordering. + pub fn map(self, f: F) -> Vol, O> where F: FnMut(V) -> T, { - GridArray { + Vol { bounds: self.bounds, + ordering: self.ordering, contents: self.contents.into_vec().into_iter().map(f).collect(), } } - - /// Returns the contents without copying. They are ordered in the same order that - /// [`GridArray::from_elements()`] expects. - pub(crate) fn into_elements(self) -> Box<[V]> { - self.contents - } } -impl, V> core::ops::Index

for GridArray { +impl core::ops::Index

for Vol +where + P: Into, + C: Deref, + O: Copy, +{ type Output = V; - /// Returns the element at `position` of this array, or panics if `position` is out of - /// bounds. + /// Returns the element at `position` of this volume data, + /// or panics if `position` is out of bounds. /// - /// Use [`GridArray::get`] for a non-panicing alternative. + /// Use [`Vol::get()`] for a non-panicing alternative. #[inline(always)] // measured faster on wasm32 in worldgen fn index(&self, position: P) -> &Self::Output { let position: Cube = position.into(); @@ -197,9 +306,14 @@ impl, V> core::ops::Index

for GridArray { } } } -impl, V> core::ops::IndexMut

for GridArray { - /// Returns the element at `position` of this array, or panics if `position` is out of - /// bounds. +impl core::ops::IndexMut

for Vol +where + P: Into, + C: DerefMut, + O: Copy, +{ + /// Returns the element at `position` of this volume data, + /// or panics if `position` is out of bounds. #[inline(always)] fn index_mut(&mut self, position: P) -> &mut Self::Output { let position: Cube = position.into(); @@ -215,14 +329,15 @@ impl, V> core::ops::IndexMut

for GridArray { } #[cfg(feature = "arbitrary")] -mod grid_array_arb { +mod vol_arb { use super::*; use arbitrary::Arbitrary; - /// Let's not spend too much memory on generating arbitrary length arrays. + /// Let's not spend too much memory on generating arbitrary length vectors. /// This does reduce coverage... const MAX_VOLUME: usize = 2_usize.pow(16); + // TODO: Generalize this implementation to `Vol` impl<'a, V: Arbitrary<'a>> Arbitrary<'a> for GridArray { fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { let bounds = GridAab::arbitrary_with_max_volume(u, MAX_VOLUME)?; @@ -245,17 +360,17 @@ mod grid_array_arb { } } -/// Error from [`GridArray::from_elements`] being given the wrong length. +/// Error from [`Vol::from_elements()`] being given the wrong length. #[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct ArrayLengthError { +pub struct VolLengthError { input_length: usize, bounds: GridAab, } #[cfg(feature = "std")] -impl std::error::Error for ArrayLengthError {} +impl std::error::Error for VolLengthError {} -impl fmt::Display for ArrayLengthError { +impl fmt::Display for VolLengthError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let Self { input_length, @@ -263,7 +378,7 @@ impl fmt::Display for ArrayLengthError { } = self; write!( f, - "array of length {input_length} cannot fill volume {v} of {bounds:?}", + "data of length {input_length} cannot fill volume {v} of {bounds:?}", v = self.bounds.volume() ) } @@ -289,7 +404,7 @@ mod tests { let bounds = GridAab::from_lower_size([10, 0, 0], [4, 1, 1]); assert_eq!( GridArray::from_elements(bounds, vec![10i32, 11, 12]), - Err(ArrayLengthError { + Err(VolLengthError { input_length: 3, bounds })