all_is_cubes/block/
builder.rs

1//! Support for [`Builder`].
2
3use alloc::borrow::Cow;
4use alloc::sync::Arc;
5use alloc::vec::Vec;
6
7use crate::block::{
8    self, AIR, Block, BlockAttributes, BlockCollision, BlockParts, BlockPtr, Modifier, Primitive,
9    Resolution,
10};
11use crate::math::{Cube, GridAab, GridPoint, Rgb, Rgb01, Rgba};
12use crate::space::{SetCubeError, Space};
13use crate::transaction::{self, Merge, Transaction};
14use crate::universe::{Handle, Name, ReadTicket, Universe, UniverseTransaction};
15
16#[cfg(doc)]
17use crate::space;
18
19/// Tool for constructing [`Block`] values conveniently.
20///
21/// It can also be used to construct [`BlockAttributes`] values.
22///
23/// To create one, call [`Block::builder()`].
24/// ([`Builder::default()`] is also available.)
25///
26/// # Example
27///
28/// ```
29/// use all_is_cubes::block::{Block, EvaluatedBlock};
30/// use all_is_cubes::math::Rgba;
31/// use all_is_cubes::universe::ReadTicket;
32///
33/// let block = Block::builder()
34///    .display_name("BROWN")
35///    .color(Rgba::new(0.5, 0.5, 0., 1.))
36///    .build();
37///
38/// let evaluated: EvaluatedBlock = block.evaluate(ReadTicket::stub()).unwrap();
39/// assert_eq!(evaluated.color(), Rgba::new(0.5, 0.5, 0., 1.));
40/// assert_eq!(evaluated.attributes().display_name.as_str(), "BROWN");
41/// ```
42///
43/// # Type parameters
44///
45/// * `P` is a type corresponding to the type of [`Primitive`] that is being built.
46/// * `Txn` is [`UniverseTransaction`] if the block builder is also building a transaction
47///   that must be executed, and [`()`][primitive@unit] otherwise.
48#[derive(Clone, Debug, Eq, PartialEq)]
49#[must_use]
50pub struct Builder<'u, P, Txn> {
51    read_ticket: ReadTicket<'u>,
52
53    /// public so that `BlockAttributes`'s macros can define methods for us
54    pub(in crate::block) attributes: BlockAttributes,
55
56    primitive_builder: P,
57
58    modifiers: Vec<Modifier>,
59
60    /// If this is a [`UniverseTransaction`], then it must be produced for the caller to execute.
61    /// If this is `()`, then it may be disregarded.
62    transaction: Txn,
63}
64
65impl Default for Builder<'_, NeedsPrimitive, ()> {
66    fn default() -> Self {
67        Self::new()
68    }
69}
70
71impl Builder<'_, NeedsPrimitive, ()> {
72    /// Common implementation of [`Block::builder`] and [`Default::default`]; use one of those to call this.
73    pub(super) const fn new() -> Self {
74        Builder {
75            read_ticket: ReadTicket::stub(),
76            attributes: BlockAttributes::default(),
77            primitive_builder: NeedsPrimitive,
78            modifiers: Vec::new(),
79            transaction: (),
80        }
81    }
82
83    /// Returns a [`BlockAttributes`] instead of building a block with those attributes.
84    ///
85    /// Panics if any modifiers were added to the builder.
86    #[track_caller]
87    pub fn build_attributes(self) -> BlockAttributes {
88        let Self {
89            read_ticket: _,
90            attributes,
91            primitive_builder: NeedsPrimitive,
92            modifiers,
93            transaction: (),
94        } = self;
95        assert_eq!(modifiers, []);
96        attributes
97    }
98}
99
100impl<'u, P, Txn> Builder<'u, P, Txn> {
101    /// Sets the [`BlockAttributes`] the block will have.
102    /// This replaces individual attribute values set using other builder methods.
103    pub fn attributes(mut self, value: BlockAttributes) -> Self {
104        self.attributes = value;
105        self
106    }
107
108    /// Adds a modifier to the end of the list of modifiers for the block.
109    /// It will be applied after all previously specified modifiers.
110    pub fn modifier(mut self, modifier: Modifier) -> Self {
111        // TODO: implement a modifier canonicalization procedure here
112        self.modifiers.push(modifier);
113        self
114    }
115
116    /// Sets the color value for building a [`Primitive::Atom`].
117    ///
118    /// This will replace any previous color **or voxels.**
119    pub fn color(self, color: impl Into<Rgba>) -> Builder<'u, Atom, ()> {
120        let Self {
121            read_ticket,
122            attributes,
123            primitive_builder: _,
124            modifiers,
125            transaction: _,
126        } = self;
127        Builder {
128            read_ticket,
129            attributes,
130            primitive_builder: Atom {
131                color: color.into(),
132                emission: Rgb::ZERO,
133                collision: BlockCollision::Hard,
134            },
135            modifiers,
136            // TODO: This might not be the right thing in more general transaction usage.
137            // For now, it's OK that we discard the transaction because it can only ever be
138            // inserting a `Space` we are not going to use any more.
139            transaction: (),
140        }
141    }
142
143    /// Sets the space for building a [`Primitive::Recur`].
144    ///
145    /// This will replace any previous voxels **or color.**
146    pub fn voxels_handle(
147        self,
148        resolution: Resolution,
149        space: Handle<Space>,
150    ) -> Builder<'u, Voxels, ()> {
151        let Self {
152            read_ticket,
153            attributes,
154            primitive_builder: _,
155            modifiers,
156            transaction: _,
157        } = self;
158        Builder {
159            read_ticket,
160            attributes,
161            primitive_builder: Voxels {
162                space,
163                resolution,
164                offset: GridPoint::origin(),
165            },
166            modifiers,
167            // TODO: This might not be the right thing in more general transaction usage.
168            // For now, it's OK that we discard the transaction because it can only ever be
169            // inserting a `Space` we are not going to use any more.
170            transaction: (),
171        }
172    }
173
174    /// As [`Self::voxels_handle()`], but for inserting the [`Space`] too.
175    ///
176    /// TODO: good public API?
177    pub(crate) fn voxels_space(
178        self,
179        resolution: Resolution,
180        space: Space,
181    ) -> Builder<'u, Voxels, UniverseTransaction> {
182        let (space_handle, transaction) = UniverseTransaction::insert(Name::Pending, space);
183
184        let Self {
185            read_ticket,
186            attributes,
187            primitive_builder: _,
188            modifiers,
189            transaction: _,
190        } = self;
191        Builder {
192            read_ticket,
193            attributes,
194            primitive_builder: Voxels {
195                space: space_handle,
196                resolution,
197                offset: GridPoint::origin(),
198            },
199            modifiers,
200            // TODO: This might not be the right thing in more general transaction usage.
201            // For now, it's OK that we discard the transaction because it can only ever be
202            // inserting a `Space` we are not going to use any more.
203            transaction,
204        }
205    }
206
207    /// Constructs a `Space` for building a [`Primitive::Recur`], and calls
208    /// the given function to fill it with blocks, in the manner of [`space::Mutation::fill()`].
209    ///
210    /// You must provide a [`ReadTicket`] using [`Builder::read_ticket()`]
211    /// if any of the provided voxel blocks contain relevant handles.
212    /// (Blocks constructed purely from colors do not.)
213    ///
214    /// If the voxels do not fill the entire volume of the block being built — that is, there is
215    /// some smaller region outside of which they are all [`AIR`] — then the [`Space`] will be
216    /// shrunk to tightly enclose that region, to improve performance. However, this still requires
217    /// the function to be called on all positions within the full block bounds, so for very high
218    /// ratios of resolution to actual content, it may be wise to use
219    /// [`voxels_handle()`](Self::voxels_handle) instead.
220    ///
221    /// Note that if the resulting builder is cloned, all clones will share the same
222    /// space.
223    // TODO: (doc) test for this
224    pub fn voxels_fn<'a, F, B>(
225        self,
226        // TODO: Maybe resolution should be a separate method? Check usage patterns later.
227        resolution: Resolution,
228        mut function: F,
229    ) -> Result<Builder<'u, Voxels, UniverseTransaction>, SetCubeError>
230    where
231        F: FnMut(Cube) -> B,
232        B: Into<Cow<'a, Block>>,
233    {
234        // This is a worldgen convenience, not the most efficient possible path (which would be
235        // `Builder::palette_and_contents()`), so save quite a lot of code generation
236        // by keeping it monomorphic and not inlined.
237        #[inline(never)]
238        fn voxels_fn_impl<'a, 'u>(
239            read_ticket: ReadTicket<'u>,
240            attributes: BlockAttributes,
241            modifiers: Vec<Modifier>,
242            resolution: Resolution,
243            function: &mut dyn FnMut(Cube) -> Cow<'a, Block>,
244        ) -> Result<Builder<'u, Voxels, UniverseTransaction>, SetCubeError> {
245            let mut not_air_bounds: Option<GridAab> = None;
246
247            let mut space = Space::for_block(resolution).build();
248            // TODO: Teach the Space Builder to accept a function in the same way?
249            space.mutate(read_ticket, |m| {
250                m.fill_all(|cube| {
251                    let block = function(cube);
252
253                    // Track which of the blocks are not equal to AIR, for later use.
254                    if block.as_ref() != &AIR {
255                        let cube_bb = cube.grid_aab();
256                        not_air_bounds = Some(if let Some(bounds) = not_air_bounds {
257                            bounds.union_box(cube_bb)
258                        } else {
259                            cube_bb
260                        });
261                    }
262
263                    Some(block)
264                })
265            })?;
266
267            // If the block bounding box is not full of non-AIR blocks, then construct a replacement
268            // Space that is smaller. This is equivalent, but improves the performance of all future
269            // uses of this block.
270            let not_air_bounds = not_air_bounds.unwrap_or(GridAab::ORIGIN_EMPTY);
271            if space.bounds() != not_air_bounds {
272                // TODO: Eventually we should be able to ask the Space to resize itself,
273                // but that is not yet an available operation.
274                let mut shrunk =
275                    Space::builder(not_air_bounds).physics(space.physics().clone()).build();
276                shrunk.mutate(read_ticket, |m| {
277                    m.fill(not_air_bounds, |cube| Some(&space[cube]))
278                })?;
279                space = shrunk;
280            }
281
282            let (space_handle, transaction) = UniverseTransaction::insert(Name::Pending, space);
283
284            Ok(Builder {
285                read_ticket,
286                attributes,
287                primitive_builder: Voxels {
288                    space: space_handle,
289                    resolution,
290                    offset: GridPoint::origin(),
291                },
292                modifiers,
293                transaction,
294            })
295        }
296
297        let Self {
298            read_ticket,
299            attributes,
300            primitive_builder: _,
301            modifiers,
302            transaction: _,
303        } = self;
304        voxels_fn_impl(
305            read_ticket,
306            attributes,
307            modifiers,
308            resolution,
309            &mut |cube| function(cube).into(),
310        )
311    }
312
313    /// Set the [`ReadTicket`] used by this builder.
314    ///
315    /// This is currently only necessary when using [`Builder::voxels_fn()`] with blocks that
316    /// contain handles. It is not necessary when simple solid-color blocks are used.
317    ///
318    /// The default is [`ReadTicket::stub()`].
319    //---
320    #[expect(clippy::elidable_lifetime_names, reason = "names for clarity")]
321    pub fn read_ticket<'u2>(self, read_ticket: ReadTicket<'u2>) -> Builder<'u2, P, Txn> {
322        Builder {
323            read_ticket,
324            attributes: self.attributes,
325            primitive_builder: self.primitive_builder,
326            modifiers: self.modifiers,
327            transaction: self.transaction,
328        }
329    }
330
331    fn build_block_and_txn_internal(self) -> (Block, Txn)
332    where
333        P: BuildPrimitive,
334    {
335        let Self {
336            read_ticket: _,
337            attributes,
338            primitive_builder,
339            mut modifiers,
340            transaction,
341        } = self;
342        let primitive = primitive_builder.build_primitive();
343
344        if attributes != BlockAttributes::default() {
345            modifiers.insert(0, Modifier::Attributes(Arc::new(attributes)));
346        }
347
348        let block = if matches!(primitive, Primitive::Air) && modifiers.is_empty() {
349            // Avoid allocating an Arc.
350            AIR
351        } else {
352            Block(BlockPtr::Owned(Arc::new(BlockParts {
353                primitive,
354                modifiers,
355            })))
356        };
357
358        (block, transaction)
359    }
360}
361
362impl<P: BuildPrimitive> Builder<'_, P, ()> {
363    /// Converts this builder into a block value.
364    ///
365    /// This method may only be used when the builder has *not* been used with `voxels_fn()`,
366    /// since in that case a universe transaction must be executed.
367    pub fn build(self) -> Block {
368        let (block, ()) = self.build_block_and_txn_internal();
369        block
370    }
371}
372
373impl<P: BuildPrimitive> Builder<'_, P, UniverseTransaction> {
374    // TODO: Also allow extracting the transaction for later use
375
376    /// Converts this builder into a block value, and inserts its associated [`Space`] into the
377    /// given universe.
378    pub fn build_into(self, universe: &mut Universe) -> Block {
379        let (block, transaction) = self.build_block_and_txn_internal();
380
381        // The transaction is always an insert_anonymous, which cannot fail.
382        transaction.execute(universe, (), &mut transaction::no_outputs).unwrap();
383
384        block
385    }
386
387    /// Converts this builder into a [`Block`] value, and modifies the given transaction to include
388    /// inserting the associated space into the universe the block is to be used in.
389    pub fn build_txn(self, transaction: &mut UniverseTransaction) -> Block {
390        let (block, txn) = self.build_block_and_txn_internal();
391        transaction.merge_from(txn).unwrap();
392        block
393    }
394}
395
396/// Atom-specific builder methods.
397impl<Txn> Builder<'_, Atom, Txn> {
398    /// Sets the collision behavior of a [`Primitive::Atom`] block.
399    pub const fn collision(mut self, collision: BlockCollision) -> Self {
400        self.primitive_builder.collision = collision;
401        self
402    }
403
404    /// Sets the light emission of a [`Primitive::Atom`] block.
405    ///
406    /// See [`Atom::emission`](block::Atom::emission) for details on the meaning of this value.
407    pub fn light_emission(mut self, value: impl Into<Rgb>) -> Self {
408        self.primitive_builder.emission = value.into();
409        self
410    }
411}
412
413/// Voxel-specific builder methods.
414impl<Txn> Builder<'_, Voxels, Txn> {
415    /// Sets the coordinate offset for building a [`Primitive::Recur`]:
416    /// the lower-bound corner of the region of the [`Space`]
417    /// which will be used for block voxels. The default is zero.
418    pub fn offset(mut self, offset: GridPoint) -> Self {
419        self.primitive_builder.offset = offset;
420        self
421    }
422
423    // TODO: It might be useful to have "offset equal to resolution"
424    // and "add offset", but don't add those until use cases are seen.
425}
426
427/// Allows implicitly converting [`Builder`] to the block it would build.
428impl<C: BuildPrimitive> From<Builder<'_, C, ()>> for Block {
429    fn from(builder: Builder<'_, C, ()>) -> Self {
430        builder.build()
431    }
432}
433/// Equivalent to `Block::builder().color(color)`.
434impl From<Rgba> for Builder<'_, Atom, ()> {
435    fn from(color: Rgba) -> Self {
436        Block::builder().color(color)
437    }
438}
439/// Equivalent to `Block::builder().color(color.with_alpha_one())`.
440impl From<Rgb01> for Builder<'_, Atom, ()> {
441    fn from(color: Rgb01) -> Self {
442        Block::builder().color(color.with_alpha_one())
443    }
444}
445
446/// Placeholder type for an incomplete [`Builder`]'s content. The builder
447/// cannot create an actual block until this is replaced.
448#[expect(clippy::exhaustive_structs)]
449#[derive(Copy, Clone, Debug, Default, Eq, Hash, PartialEq)]
450pub struct NeedsPrimitive;
451
452/// Something that a parameterized [`Builder`] can use to construct a block's primitive.
453///
454/// TODO: This is not currently necessary; we can replace the BuildPrimitive types with the
455/// primitive itself. (But will that remain true?)
456#[doc(hidden)]
457pub trait BuildPrimitive {
458    fn build_primitive(self) -> Primitive;
459}
460
461/// Parameter type for a [`Builder`] that is building a block with a [`Primitive::Atom`].
462///
463/// This is not the same as the [`block::Atom`] type.
464#[derive(Clone, Debug, Eq, Hash, PartialEq)]
465pub struct Atom {
466    color: Rgba,
467    emission: Rgb,
468    collision: BlockCollision,
469}
470impl BuildPrimitive for Atom {
471    fn build_primitive(self) -> Primitive {
472        Primitive::Atom(block::Atom {
473            color: self.color,
474            emission: self.emission,
475            collision: self.collision,
476        })
477    }
478}
479
480/// Parameter type for a [`Builder`] that is building a block with voxels ([`Primitive::Recur`]).
481#[derive(Clone, Debug, Eq, Hash, PartialEq)]
482pub struct Voxels {
483    space: Handle<Space>,
484    resolution: Resolution,
485    offset: GridPoint,
486}
487impl BuildPrimitive for Voxels {
488    fn build_primitive(self) -> Primitive {
489        Primitive::Recur {
490            offset: self.offset,
491            resolution: self.resolution,
492            space: self.space,
493        }
494    }
495}
496
497#[cfg(test)]
498mod tests {
499    use alloc::boxed::Box;
500    use euclid::{point3, vec3};
501
502    use crate::block::{self, Resolution::*, TickAction};
503    use crate::content::palette;
504    use crate::inv;
505    use crate::math::{Face6, GridRotation, Vol, ps32};
506    use crate::op::Operation;
507    use crate::sound;
508    use crate::space::SpacePhysics;
509    use crate::transaction::Transactional as _;
510
511    use super::*;
512
513    #[test]
514    fn defaults() {
515        let color = Rgba::new(0.1, 0.2, 0.3, 0.4);
516        assert_eq!(
517            Block::builder().color(color).build(),
518            Block::from(block::Atom {
519                color,
520                emission: Rgb::ZERO,
521                collision: BlockCollision::Hard,
522            }),
523        );
524    }
525
526    #[test]
527    fn default_equivalent() {
528        assert_eq!(
529            Builder::new(),
530            <Builder<'_, NeedsPrimitive, ()> as Default>::default()
531        );
532    }
533
534    #[test]
535    fn every_field_nondefault() {
536        let color = Rgba::new(0.1, 0.2, 0.3, 0.4);
537        let emission = Rgb::new(0.1, 3.0, 0.1);
538        let inventory = inv::InvInBlock::new(
539            9,
540            R4,
541            R16,
542            vec![
543                inv::IconRow::new(0..3, point3(1, 1, 1), vec3(5, 0, 0)),
544                inv::IconRow::new(3..6, point3(1, 1, 6), vec3(5, 0, 0)),
545                inv::IconRow::new(6..9, point3(1, 1, 11), vec3(5, 0, 0)),
546            ],
547        );
548        let ambient_sound = {
549            let mut s = sound::Ambient::SILENT;
550            s.noise_bands[sound::Band::MIN] = ps32(0.5);
551            s
552        };
553        let rotation_rule = block::RotationPlacementRule::Attach { by: Face6::NZ };
554        let placement_action = Some(block::PlacementAction {
555            operation: Operation::Become(block::from_color!(1.0, 0.0, 1.0)),
556            in_front: false,
557        });
558        let tick_action = Some(TickAction::from(Operation::Become(AIR)));
559        let activation_action = Some(Operation::Become(block::from_color!(1.0, 1.0, 1.0)));
560        assert_eq!(
561            Block::builder()
562                .color(color)
563                .display_name("hello world")
564                .inventory_config(inventory.clone())
565                .collision(BlockCollision::None)
566                .rotation_rule(rotation_rule)
567                .selectable(false)
568                .light_emission(emission)
569                .ambient_sound(ambient_sound.clone())
570                .placement_action(placement_action.clone())
571                .tick_action(tick_action.clone())
572                .activation_action(activation_action.clone())
573                .animation_hint(block::AnimationHint::replacement(
574                    block::AnimationChange::Shape
575                ))
576                .modifier(Modifier::Rotate(Face6::PY.clockwise()))
577                .build(),
578            Block::from(block::Atom {
579                color,
580                emission,
581                collision: BlockCollision::None,
582            })
583            .with_modifier(BlockAttributes {
584                display_name: "hello world".into(),
585                selectable: false,
586                placement_action,
587                inventory,
588                ambient_sound,
589                rotation_rule,
590                tick_action,
591                activation_action,
592                animation_hint: block::AnimationHint::replacement(block::AnimationChange::Shape),
593            })
594            .with_modifier(Modifier::Rotate(Face6::PY.clockwise()))
595        );
596    }
597
598    #[test]
599    fn voxels_from_space() {
600        let mut universe = Universe::new();
601        let space_handle = universe.insert_anonymous(Space::empty_positive(1, 1, 1));
602
603        assert_eq!(
604            Block::builder()
605                .display_name("hello world")
606                .voxels_handle(R2, space_handle.clone())
607                .build(),
608            Block::from_primitive(Primitive::Recur {
609                offset: GridPoint::origin(),
610                resolution: R2, // not same as space size
611                space: space_handle
612            })
613            .with_modifier(Modifier::Attributes(Arc::new(BlockAttributes {
614                display_name: "hello world".into(),
615                ..BlockAttributes::default()
616            }))),
617        );
618    }
619
620    #[test]
621    fn voxels_from_fn_basic() {
622        let mut universe = Universe::new();
623
624        let resolution = R4;
625        let expected_bounds = GridAab::for_block(resolution);
626        let atom = block::from_color!(palette::DIRT);
627        let block = Block::builder()
628            .display_name("hello world")
629            .voxels_fn(resolution, |_cube| &atom)
630            .unwrap()
631            .build_into(&mut universe);
632
633        // Extract the implicitly constructed space handle
634        let space_handle = if let Primitive::Recur { space, .. } = block.primitive() {
635            space.clone()
636        } else {
637            panic!("expected Recur, found {block:?}");
638        };
639
640        assert_eq!(
641            block,
642            Block::from_primitive(Primitive::Recur {
643                offset: GridPoint::origin(),
644                resolution,
645                space: space_handle.clone()
646            })
647            .with_modifier(BlockAttributes {
648                display_name: "hello world".into(),
649                ..BlockAttributes::default()
650            }),
651        );
652
653        // Check the space's characteristics
654        let space = space_handle.read(universe.read_ticket()).unwrap();
655        assert_eq!(space.bounds(), expected_bounds);
656        assert_eq!(space.physics(), &SpacePhysics::DEFAULT_FOR_BLOCK);
657        assert_eq!(
658            space.extract(expected_bounds, |e| e.block_data().block()),
659            Vol::<Box<[&Block]>>::from_fn(expected_bounds, |_| &atom)
660        );
661    }
662
663    /// `voxels_fn()` automatically shrinks the space bounds to fit only the nonair blocks.
664    #[test]
665    fn voxels_from_fn_shrinkwrap() {
666        let mut universe = Universe::new();
667
668        let resolution = R4;
669        let expected_bounds = GridAab::from_lower_upper([0, 0, 0], [2, 4, 4]);
670        let atom = block::from_color!(palette::DIRT);
671        let block = Block::builder()
672            .display_name("hello world")
673            .voxels_fn(resolution, |cube| {
674                if expected_bounds.contains_cube(cube) {
675                    &atom
676                } else {
677                    &AIR
678                }
679            })
680            .unwrap()
681            .build_into(&mut universe);
682
683        // Extract the implicitly constructed space handle
684        let space_handle = if let Primitive::Recur { space, .. } = block.primitive() {
685            space.clone()
686        } else {
687            panic!("expected Recur, found {block:?}");
688        };
689
690        // Check the space's characteristics; not just that it has the smaller bounds, but that
691        // it has the expected physics and contents.
692        let space = space_handle.read(universe.read_ticket()).unwrap();
693        assert_eq!(space.bounds(), expected_bounds);
694        assert_eq!(space.physics(), &SpacePhysics::DEFAULT_FOR_BLOCK);
695        assert_eq!(
696            space.extract(expected_bounds, |e| e.block_data().block()),
697            Vol::<Box<[&Block]>>::from_fn(expected_bounds, |_| &atom)
698        );
699    }
700
701    #[test]
702    fn explicit_txn() {
703        let resolution = R8;
704        let mut universe = Universe::new();
705        let _block = universe
706            .transact(|txn, _| {
707                Ok(Block::builder()
708                    .display_name("hello world")
709                    .voxels_fn(resolution, |_cube| &AIR)
710                    .unwrap()
711                    .build_txn(txn))
712            })
713            .unwrap();
714
715        assert_eq!(universe.iter_by_type::<Space>().count(), 1);
716    }
717
718    #[test]
719    fn modifier_before_color_is_equivalent() {
720        assert_eq!(
721            Block::builder()
722                .modifier(Modifier::Rotate(GridRotation::RXYz))
723                .color(Rgba::WHITE),
724            Block::builder()
725                .color(Rgba::WHITE)
726                .modifier(Modifier::Rotate(GridRotation::RXYz))
727        );
728    }
729
730    #[test]
731    fn modifier_before_voxels_is_equivalent() {
732        let h = Handle::new_gone(Name::Pending);
733        assert_eq!(
734            Block::builder()
735                .modifier(Modifier::Rotate(GridRotation::RXYz))
736                .voxels_handle(R8, h.clone()),
737            Block::builder()
738                .voxels_handle(R8, h)
739                .modifier(Modifier::Rotate(GridRotation::RXYz))
740        );
741    }
742}