all_is_cubes/block/modifier/
zoom.rs

1use euclid::Point3D;
2
3use crate::block::{
4    self, Evoxel, Evoxels, MinEval, Modifier,
5    Resolution::{self, R1},
6};
7use crate::math::{Cube, GridAab, GridCoordinate, GridPoint, GridRotation, Vol};
8use crate::universe;
9
10/// Data for [`Modifier::Zoom`], describing a portion of the original block that is scaled
11/// up to become the whole block.
12///
13/// Design note: This is a struct separate from [`Modifier`] so that it can have a
14/// constructor accepting only valid bounds.
15#[derive(Clone, Debug, Eq, Hash, PartialEq)]
16pub struct Zoom {
17    /// Scale factor to zoom in by.
18    scale: Resolution,
19
20    /// Which portion of the block/space will be used, specified in terms of an offset
21    /// in the grid of zoomed blocks (that is, this should have coordinates between `0`
22    /// and `scale - 1`).
23    offset: Point3D<u8, Cube>,
24    // /// If present, a space to extract voxels from _instead of_ the underlying
25    // /// [`Primitive`]. This may be used so that the before-zooming block can be a
26    // /// custom preview rather than an exact miniature of the multi-block
27    // /// structure.
28    // space: Option<Handle<Space>>,
29}
30
31impl Zoom {
32    /// Construct a [`Zoom`] which enlarges the original block's voxels by `scale` and
33    /// selects the region of them whose lower corner is `offset * scale`.
34    ///
35    /// Panics if any of `offset`'s components are out of bounds, i.e. less than 0 or
36    /// greater than `scale - 1`.
37    #[track_caller]
38    pub fn new(scale: Resolution, offset: GridPoint) -> Self {
39        if !GridAab::for_block(scale).contains_cube(Cube::from(offset)) {
40            panic!("Zoom offset {offset:?} out of bounds for {scale}");
41        }
42
43        Self {
44            scale,
45            offset: offset.map(|c| c as u8),
46        }
47    }
48
49    /// Decompose into parts, for serialization.
50    #[cfg(feature = "save")]
51    pub(crate) fn to_serial_schema(&self) -> crate::save::schema::ModifierSer<'static> {
52        let Zoom { scale, offset } = *self;
53        crate::save::schema::ModifierSer::ZoomV1 {
54            scale,
55            offset: offset.into(),
56        }
57    }
58
59    pub(super) fn evaluate(
60        &self,
61        input: MinEval,
62        filter: &block::EvalFilter<'_>,
63    ) -> Result<MinEval, block::InEvalError> {
64        let Zoom {
65            offset: offset_in_zoomed_blocks,
66            scale,
67        } = *self;
68
69        // TODO: respect filter.skip_eval
70
71        // TODO: To efficiently implement this, we should be able to run in a phase
72        // *before* the `Primitive` evaluation, which allows us to reduce how many
73        // of the primitive voxels are evaluated. (Modifier::Move will also benefit.)
74
75        let original_resolution = input.resolution();
76
77        // TODO: write test cases for what happens if the division fails
78        // (this is probably wrong in that we need to duplicate voxels if it happens)
79        let zoom_resolution = (original_resolution / scale).unwrap_or(R1);
80
81        Ok(match input.voxels().single_voxel() {
82            Some(_) => {
83                // Block has resolution 1.
84                // Zoom::new() checks that the region is not outside the block's unit cube,
85                // so we can just unconditionally return the original color.
86                input
87            }
88            None => {
89                let (attributes, voxels) = input.into_parts();
90                let voxels = voxels.as_vol_ref();
91                let voxel_offset = offset_in_zoomed_blocks.map(GridCoordinate::from).to_vector()
92                    * GridCoordinate::from(zoom_resolution);
93                match GridAab::for_block(zoom_resolution)
94                    .intersection_cubes(voxels.bounds().translate(-voxel_offset))
95                {
96                    // This case occurs when the voxels' actual bounds (which may be smaller
97                    // than the block bounding box) don't intersect the zoom region.
98                    None => MinEval::new(attributes, Evoxels::from_one(Evoxel::AIR)),
99                    Some(intersected_bounds) => {
100                        block::Budget::decrement_voxels(
101                            &filter.budget,
102                            intersected_bounds.volume().unwrap(),
103                        )?;
104                        MinEval::new(
105                            attributes,
106                            Evoxels::from_many(
107                                zoom_resolution,
108                                Vol::from_fn(intersected_bounds, |p| voxels[p + voxel_offset]),
109                            ),
110                        )
111                    }
112                }
113            }
114        })
115    }
116
117    /// Scale factor to zoom in by.
118    pub fn scale(&self) -> Resolution {
119        self.scale
120    }
121
122    /// Which portion of the block/space will be used, specified in terms of an offset
123    /// in the grid of zoomed blocks (that is, this will have coordinates between `0`
124    /// and `scale - 1`).
125    pub fn offset(&self) -> GridPoint {
126        self.offset.map(i32::from)
127    }
128
129    pub(crate) fn rotate(self, rotation: GridRotation) -> Self {
130        Self {
131            scale: self.scale,
132            offset: rotation
133                .to_positive_octant_transform(self.scale.into())
134                .transform_point(self.offset())
135                .cast(),
136        }
137    }
138}
139
140impl From<Zoom> for Modifier {
141    fn from(value: Zoom) -> Self {
142        Modifier::Zoom(value)
143    }
144}
145
146impl universe::VisitHandles for Zoom {
147    fn visit_handles(&self, _visitor: &mut dyn universe::HandleVisitor) {
148        let Zoom {
149            scale: _,
150            offset: _,
151        } = self;
152    }
153}
154
155#[cfg(feature = "arbitrary")]
156impl<'a> arbitrary::Arbitrary<'a> for Zoom {
157    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
158        let scale = u.arbitrary()?;
159        let max_offset = GridCoordinate::from(scale) - 1;
160        Ok(Self::new(
161            scale,
162            GridPoint::new(
163                u.int_in_range(0..=max_offset)?,
164                u.int_in_range(0..=max_offset)?,
165                u.int_in_range(0..=max_offset)?,
166            ),
167        ))
168    }
169
170    fn size_hint(depth: usize) -> (usize, Option<usize>) {
171        use arbitrary::{Arbitrary, size_hint::and_all};
172        and_all(&[
173            <Resolution as Arbitrary>::size_hint(depth),
174            <[GridCoordinate; 3] as Arbitrary>::size_hint(depth),
175        ])
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182    use crate::block::{EvaluatedBlock, Resolution::R2};
183    use crate::content::{make_some_blocks, make_some_voxel_blocks};
184    use crate::math::{GridVector, Rgba};
185    use crate::universe::Universe;
186    use euclid::point3;
187    use pretty_assertions::assert_eq;
188
189    #[test]
190    #[should_panic(expected = "Zoom offset (2, 1, 1) out of bounds for 2")]
191    fn construction_out_of_range_high() {
192        Zoom::new(R2, point3(2, 1, 1));
193    }
194
195    #[test]
196    #[should_panic(expected = "Zoom offset (-1, 1, 1) out of bounds for 2")]
197    fn construction_out_of_range_low() {
198        Zoom::new(R2, point3(-1, 1, 1));
199    }
200
201    #[test]
202    fn evaluation() {
203        let mut universe = Universe::new();
204        let [original_block] = make_some_voxel_blocks(&mut universe);
205
206        let ev_original = original_block.evaluate(universe.read_ticket()).unwrap();
207        assert_eq!(ev_original.resolution(), Resolution::R16);
208        let scale = R2; // scale up by two = divide resolution by two
209        let zoom_resolution = ev_original.resolution().halve().unwrap();
210        let original_voxels = &ev_original.voxels;
211
212        // Try zoom at multiple offset steps.
213        for x in 0i32..2 {
214            dbg!(x);
215            let zoomed = original_block.clone().with_modifier(Zoom::new(scale, point3(x, 0, 0)));
216            let ev_zoomed = zoomed.evaluate(universe.read_ticket()).unwrap();
217            assert_eq!(
218                ev_zoomed,
219                if x >= 2 {
220                    // out of range
221                    EvaluatedBlock::from_voxels(
222                        zoomed,
223                        ev_original.attributes.clone(),
224                        Evoxels::from_one(Evoxel::from_color(Rgba::TRANSPARENT)),
225                        block::Cost {
226                            components: ev_original.cost.components + 1,
227                            ..ev_original.cost
228                        },
229                    )
230                } else {
231                    EvaluatedBlock::from_voxels(
232                        zoomed,
233                        ev_original.attributes.clone(),
234                        Evoxels::from_many(
235                            zoom_resolution,
236                            Vol::from_fn(GridAab::for_block(zoom_resolution), |p| {
237                                original_voxels[p + GridVector::new(
238                                    GridCoordinate::from(zoom_resolution) * x,
239                                    0,
240                                    0,
241                                )]
242                            }),
243                        ),
244                        block::Cost {
245                            components: ev_original.cost.components + 1,
246                            // 8 = 16 (original) / 2 (zoom level)
247                            voxels: ev_original.cost.voxels
248                                + u32::from((ev_original.resolution() / scale).unwrap()).pow(3),
249                            ..ev_original.cost
250                        },
251                    )
252                }
253            );
254        }
255    }
256
257    #[test]
258    fn atom_in_bounds() {
259        let universe = Universe::new();
260        let [original] = make_some_blocks();
261        let mut zoomed = original.clone();
262        zoomed.modifiers_mut().push(Modifier::Zoom(Zoom {
263            scale: R2,
264            offset: point3(1, 0, 0),
265        }));
266        assert_eq!(
267            zoomed.evaluate(universe.read_ticket()).unwrap().color(),
268            original.color()
269        );
270    }
271}