all_is_cubes/block/modifier/
zoom.rs1use 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#[derive(Clone, Debug, Eq, Hash, PartialEq)]
16pub struct Zoom {
17 scale: Resolution,
19
20 offset: Point3D<u8, Cube>,
24 }
30
31impl Zoom {
32 #[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 #[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 let original_resolution = input.resolution();
76
77 let zoom_resolution = (original_resolution / scale).unwrap_or(R1);
80
81 Ok(match input.voxels().single_voxel() {
82 Some(_) => {
83 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 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 pub fn scale(&self) -> Resolution {
119 self.scale
120 }
121
122 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; let zoom_resolution = ev_original.resolution().halve().unwrap();
210 let original_voxels = &ev_original.voxels;
211
212 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 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 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}