all_is_cubes/
drawing.rs

1//! Draw 2D graphics and text into [`Space`]s, using a general adapter for
2//! [`embedded_graphics`]'s drawing algorithms.
3//!
4//! The [`VoxelBrush`] type can also be useful in direct 3D drawing.
5//!
6//! ## Coordinate system differences
7//!
8//! [`embedded_graphics`] uses coordinates which are different from ours in
9//! two ways that should be kept in mind when trying to align 2D and 3D shapes:
10//!
11//! *   Text drawing presumes that +X is rightward and +Y is downward. Hence,
12//!     text will be upside-down unless the chosen transformation inverts Y (or
13//!     otherwise transforms to suit the orientation the text is being viewed from).
14//! *   Coordinates are considered to refer to pixel centers rather than low corners,
15//!     and rectangles have inclusive upper bounds (whereas our [`GridAab`]s have
16//!     exclusive upper bounds).
17
18use alloc::borrow::{Borrow, Cow};
19use alloc::vec::Vec;
20use core::marker::PhantomData;
21use core::ops::Range;
22
23use embedded_graphics::geometry::{Dimensions, Point, Size};
24use embedded_graphics::pixelcolor::{PixelColor, Rgb888};
25use embedded_graphics::prelude::{DrawTarget, Pixel};
26use embedded_graphics::primitives::Rectangle;
27
28/// Re-export the version of the [`embedded_graphics`] crate we're using.
29pub use embedded_graphics;
30
31use crate::block::{Block, Evoxel, text};
32use crate::math::{
33    Cube, FaceMap, GridAab, GridCoordinate, GridPoint, GridRotation, GridVector, Gridgid, Rgb01,
34    Rgba, Vol,
35};
36use crate::space::{Mutation, SetCubeError, SpaceTransaction};
37
38#[cfg(doc)]
39use crate::space::{CubeTransaction, Space};
40#[cfg(doc)]
41use embedded_graphics::Drawable;
42
43/// Convert a bounding-box rectangle, as from [`embedded_graphics::geometry::Dimensions`],
44/// to a [`GridAab`] which encloses the voxels that would be affected by drawing a
45/// [`Drawable`] with those bounds on a [`DrawingPlane`] with the given `transform`.
46///
47/// `max_brush` should be the union of bounds of [`VoxelBrush`]es used by the drawable.
48/// If using plain colors, `GridAab::ORIGIN_CUBE` is the appropriate
49/// input.
50///
51/// Please note that coordinate behavior may be surprising. [`embedded_graphics`]
52/// considers coordinates to refer to pixel centers, which is similar but not identical
53/// to our identifying [`Cube`]s by their low corner. The `transform` is
54/// then applied to those coordinates. So, for example, applying [`Gridgid::FLIP_Y`]
55/// to a [`Rectangle`] whose top-left corner is `[0, 0]` will result in a [`GridAab`]
56/// which *includes* the <var>y</var> = 0 row — not one which abuts it and is strictly in
57/// the negative y range.
58///
59/// TODO: The above text is either wrong or describes a bad idea. Fix.
60///
61/// TODO: This function still has some bugs to work out
62///
63/// TODO: This function needs a better name
64///
65/// TODO: Handling zero-area rectangles is not implemented
66pub fn rectangle_to_aab(rectangle: Rectangle, transform: Gridgid, max_brush: GridAab) -> GridAab {
67    // Note that embedded_graphics uses the convention that coordinates *identify pixels*,
68    // not the boundaries between pixels. Thus, a rectangle whose bottom_right corner is
69    // 1, 1 includes the pixel with coordinates 1, 1. This is consistent with our “cube”
70    // coordinate convention, but not with `GridAab`'s meaning of upper bounds. However,
71    // accounting for `max_brush` will conveniently fix that for us in exactly the right
72    // way, since it is precisely about identifying the volume occupied by drawing a
73    // 2D-pixel.
74
75    #![allow(clippy::too_long_first_doc_paragraph)] // TODO: find better phrasing
76
77    if rectangle.size.width == 0 || rectangle.size.height == 0 {
78        // Handle zero-sized rectangles — they don't draw any pixels, so don't enlarge them
79
80        let type_converted = GridAab::from_lower_size(
81            [rectangle.top_left.x, rectangle.top_left.y, 0],
82            [rectangle.size.width, rectangle.size.height, 0],
83        );
84
85        // Transform into the target 3D coordinate system.
86        type_converted.transform(transform).unwrap()
87    } else {
88        // Construct rectangle whose edges *exclude* the direction in which the
89        // drawn pixels overhang, because that's going to change.
90        let type_converted_excluding_size = GridAab::from_lower_size(
91            [rectangle.top_left.x, rectangle.top_left.y, 0],
92            [(rectangle.size.width - 1), (rectangle.size.height - 1), 0],
93        );
94
95        // Transform into the target 3D coordinate system.
96        let transformed = type_converted_excluding_size.transform(transform).unwrap();
97
98        // Account for the brush size -- assuming the brush is *not* rotated by the
99        // transform, so we must cancel it out.
100        // TODO: We want to change this to rotate the brush, but must do it globally
101        // consistently in both drawing and size-computation.
102        transformed.minkowski_sum(max_brush).unwrap()
103    }
104}
105
106/// Adapter to use a [`Mutation`] or [`SpaceTransaction`] as a [`DrawTarget`].
107///
108/// Use [`Mutation::draw_target()`] or [`SpaceTransaction::draw_target()`] to construct this.
109///
110/// `'s` is the lifetime of the borrowed target.
111/// `C` is the “color” type to use, which should implement [`VoxelColor`].
112#[derive(Debug)]
113#[expect(clippy::module_name_repetitions)]
114pub struct DrawingPlane<'s, T, C> {
115    space: &'s mut T,
116    /// Defines the coordinate transformation from 2D graphics to the [`Space`].
117    transform: Gridgid,
118    _color: PhantomData<fn(C)>,
119}
120
121impl<'s, T, C> DrawingPlane<'s, T, C> {
122    pub(crate) fn new(space: &'s mut T, transform: Gridgid) -> Self {
123        Self {
124            space,
125            transform,
126            _color: PhantomData,
127        }
128    }
129
130    // TODO: We should probably have ways to stack more transforms
131
132    /// Converts 2D e-g [`Point`] to 3D [`Cube`]. Helper for multiple `impl DrawTarget`s.
133    fn convert_point(&self, point: Point) -> Cube {
134        // TODO: This should, now obviously, be `transform_cube` but changing that will
135        // break other things.
136        Cube::from(self.transform.transform_point(GridPoint::new(point.x, point.y, 0)))
137    }
138}
139
140/// A [`DrawingPlane`] accepts any color type that implements [`VoxelColor`].
141impl<'c, C> DrawTarget for DrawingPlane<'_, Mutation<'_, '_>, C>
142where
143    C: VoxelColor<'c>,
144{
145    type Color = C;
146    type Error = SetCubeError;
147
148    fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
149    where
150        I: IntoIterator<Item = Pixel<Self::Color>>,
151    {
152        for Pixel(point, color) in pixels {
153            // TODO: Add a cache so we're not reconstructing the block for every single pixel.
154            // (This is possible because `PixelColor: PartialEq`.)
155            // TODO: Need to rotate the brush to match our transform
156            let cube = self.convert_point(point);
157            // TODO(read_ticket): migrate DrawingPlane as a whole to operate on Mutation
158            color.into_blocks().paint(self.space, cube)?;
159        }
160        Ok(())
161    }
162}
163
164/// A [`DrawingPlane`] accepts any color type that implements [`VoxelColor`].
165impl<'c, C> DrawTarget for DrawingPlane<'_, SpaceTransaction, C>
166where
167    C: VoxelColor<'c>,
168{
169    type Color = C;
170    type Error = SetCubeError;
171
172    fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
173    where
174        I: IntoIterator<Item = Pixel<Self::Color>>,
175    {
176        for Pixel(point, color) in pixels {
177            // TODO: Add a cache so we're not reconstructing the block for every single pixel.
178            // (This is possible because `PixelColor: PartialEq`.)
179            // TODO: Need to rotate the brush to match our transform
180            color.into_blocks().paint_transaction_mut(self.space, self.convert_point(point));
181        }
182        Ok(())
183    }
184}
185
186impl<Container> DrawTarget for DrawingPlane<'_, Vol<Container>, text::Brush>
187where
188    Container: core::ops::DerefMut<Target = [Evoxel]>,
189{
190    type Color = text::Brush;
191    type Error = core::convert::Infallible;
192
193    fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
194    where
195        I: IntoIterator<Item = Pixel<Self::Color>>,
196    {
197        for Pixel(point, brush) in pixels {
198            let point3d = self.convert_point(point);
199            for (offset, ev) in brush.iter() {
200                let offset = self.transform.rotation.transform_vector(offset);
201                if let Some(vox) = self.space.get_mut(point3d + offset) {
202                    *vox = ev;
203                }
204            }
205        }
206        Ok(())
207    }
208}
209
210impl<C> Dimensions for DrawingPlane<'_, Mutation<'_, '_>, C> {
211    fn bounding_box(&self) -> Rectangle {
212        rectangle_from_bounds(self.transform, self.space.bounds())
213    }
214}
215impl<Container, Color> Dimensions for DrawingPlane<'_, Vol<Container>, Color> {
216    fn bounding_box(&self) -> Rectangle {
217        rectangle_from_bounds(self.transform, self.space.bounds())
218    }
219}
220fn rectangle_from_bounds(transform: Gridgid, bounds: GridAab) -> Rectangle {
221    // Invert our coordinate transform to bring the bounds into the drawing
222    // coordinate system.
223    let bounds = bounds
224        .shrink(FaceMap::from_fn(|f| f.is_positive().into()))
225        .unwrap()
226        .transform(transform.inverse())
227        .unwrap_or(GridAab::ORIGIN_CUBE);
228
229    let size = bounds.size();
230    Rectangle {
231        top_left: Point {
232            x: bounds.lower_bounds().x,
233            y: bounds.lower_bounds().y,
234        },
235        size: Size {
236            width: size.width + 1,
237            height: size.height + 1,
238        },
239    }
240}
241
242impl<C> Dimensions for DrawingPlane<'_, SpaceTransaction, C> {
243    fn bounding_box(&self) -> Rectangle {
244        Rectangle {
245            top_left: Point {
246                x: i32::MIN,
247                y: i32::MIN,
248            },
249            size: Size {
250                width: u32::MAX,
251                height: u32::MAX,
252            },
253        }
254    }
255}
256
257/// Allows “drawing” blocks onto a [`DrawingPlane`], a two-dimensional coordinate system
258/// established within a [`Space`].
259///
260/// Builds on [`PixelColor`] by defining a conversion to [`Block`]s and tracking depth.
261/// [`PixelColor::Raw`] is ignored; the supertrait is present only because
262/// [`embedded_graphics`] requires it.
263pub trait VoxelColor<'a>: PixelColor {
264    /// Returns a corresponding [`VoxelBrush`], the most general form of blocky drawing.
265    fn into_blocks(self) -> VoxelBrush<'a>;
266}
267
268impl PixelColor for &Block {
269    type Raw = ();
270}
271impl<'a> VoxelColor<'a> for &'a Block {
272    fn into_blocks(self) -> VoxelBrush<'a> {
273        VoxelBrush::new([([0, 0, 0], self)])
274    }
275}
276
277impl<'a> VoxelColor<'a> for Rgb01 {
278    fn into_blocks(self) -> VoxelBrush<'a> {
279        VoxelBrush::single(Block::from(self))
280    }
281}
282
283impl<'a> VoxelColor<'a> for Rgba {
284    fn into_blocks(self) -> VoxelBrush<'a> {
285        VoxelBrush::single(Block::from(self))
286    }
287}
288
289impl PixelColor for text::Brush {
290    type Raw = ();
291}
292
293/// Adapt [`embedded_graphics`]'s most general color type to ours.
294impl<'a> VoxelColor<'a> for Rgb888 {
295    fn into_blocks(self) -> VoxelBrush<'a> {
296        VoxelBrush::single(Block::from(Rgb01::from(self)))
297    }
298}
299
300/// A shape of multiple blocks to “paint” with. This may be used to make copies of a
301/// simple shape, or to make multi-layered "2.5D" drawings using [`DrawingPlane`].
302///
303/// Note that only `&VoxelBrush` implements [`PixelColor`]; this is because `PixelColor`
304/// requires a value implementing [`Copy`].
305#[derive(Clone, Debug, Eq, Hash, PartialEq)]
306#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
307pub struct VoxelBrush<'a>(Vec<(GridVector, Cow<'a, Block>)>);
308
309impl VoxelBrush<'static> {
310    /// A reference to `VoxelBrush::new([])`.
311    pub const EMPTY_REF: &'static Self = &VoxelBrush(Vec::new());
312}
313
314impl<'a> VoxelBrush<'a> {
315    /// Makes a [`VoxelBrush`] which paints the specified blocks at the specified offsets
316    /// from each pixel position. (`Cube::ORIGIN` is zero offset.)
317    // TODO: revisit what generics the parameter types have.
318    pub fn new<V, B>(blocks: impl IntoIterator<Item = (V, B)>) -> Self
319    where
320        V: Into<GridVector>,
321        B: Into<Cow<'a, Block>>,
322    {
323        Self(
324            blocks
325                .into_iter()
326                .map(|(offset, block)| (offset.into(), block.into()))
327                .collect(),
328        )
329    }
330
331    /// Makes a [`VoxelBrush`] which paints the specified block with no offset.
332    pub fn single<B>(block: B) -> Self
333    where
334        B: Into<Cow<'a, Block>>,
335    {
336        Self::new([([0, 0, 0], block)])
337    }
338
339    /// Makes a [`VoxelBrush`] which paints the specified block within the specified Z-axis range.
340    pub fn with_thickness<B>(block: B, range: Range<GridCoordinate>) -> Self
341    where
342        B: Into<Cow<'a, Block>>,
343    {
344        let block = block.into();
345        Self::new(range.map(|z| (GridVector::new(0, 0, z), block.clone())))
346    }
347
348    /// Copies each of the brush's blocks into `m` relative to the given origin
349    /// point.
350    ///
351    /// Unlike [`Mutation::set()`], it is not considered an error if any of the affected cubes
352    /// fall outside of the `Space`'s bounds.
353    pub fn paint(&self, m: &mut Mutation<'_, '_>, origin: Cube) -> Result<(), SetCubeError> {
354        for &(offset, ref block) in &self.0 {
355            ignore_out_of_bounds(m.set(origin + offset, Cow::borrow(block)))?;
356        }
357        Ok(())
358    }
359
360    /// Creates a transaction equivalent to [`VoxelBrush::paint`].
361    ///
362    /// Note that [`VoxelBrush::paint`] or using it in a [`DrawTarget`] ignores
363    /// out-of-bounds drawing, but transactions do not support this and will fail instead.
364    pub fn paint_transaction(&self, origin: Cube) -> SpaceTransaction {
365        let mut txn = SpaceTransaction::default();
366        self.paint_transaction_mut(&mut txn, origin);
367        txn
368    }
369
370    /// Like [`Self::paint_transaction()`] but modifies an existing transaction (as per
371    /// [`CubeTransaction::overwrite()`]).
372    ///
373    /// Note that [`VoxelBrush::paint`] or using it in a [`DrawTarget`] ignores
374    /// out-of-bounds drawing, but transactions do not support this and will fail instead.
375    pub fn paint_transaction_mut(&self, transaction: &mut SpaceTransaction, origin: Cube) {
376        for &(offset, ref block) in &self.0 {
377            transaction.at(origin + offset).overwrite(Block::clone(block));
378        }
379    }
380
381    /// Converts a `&VoxelBrush` into a `VoxelBrush` that borrows it.
382    pub fn as_ref(&self) -> VoxelBrush<'_> {
383        VoxelBrush(self.0.iter().map(|(v, b)| (*v, Cow::Borrowed(b.as_ref()))).collect())
384    }
385
386    /// Converts a `VoxelBrush` with borrowed blocks to one with owned blocks.
387    pub fn into_owned(self) -> VoxelBrush<'static> {
388        VoxelBrush(self.0.into_iter().map(|(v, b)| (v, Cow::Owned(b.into_owned()))).collect())
389    }
390
391    /// Add the given offset to the offset of each block, offsetting everything drawn.
392    #[must_use]
393    pub fn translate<V: Into<GridVector>>(mut self, offset: V) -> Self {
394        let offset = offset.into();
395        for (block_offset, _) in self.0.iter_mut() {
396            // TODO: use explicitly checked add for a good error?
397            *block_offset += offset;
398        }
399        self
400    }
401
402    /// Apply the given rotation (about the no-offset block) to the position of each block
403    /// and to the blocks themselves.
404    #[must_use]
405    pub fn rotate(self, rotation: GridRotation) -> Self {
406        if rotation == GridRotation::IDENTITY {
407            self
408        } else {
409            VoxelBrush::new(self.0.into_iter().map(|(block_offset, block)| {
410                (
411                    rotation.transform_vector(block_offset),
412                    block.into_owned().rotate(rotation),
413                )
414            }))
415        }
416    }
417
418    /// Computes the region affected by this brush, as if it were painted at the origin.
419    ///
420    /// Returns [`None`] if the brush is empty.
421    pub fn bounds(&self) -> Option<GridAab> {
422        let mut bounds: Option<GridAab> = None;
423        for &(offset, _) in self.0.iter() {
424            let cube = Cube::from(offset.to_point());
425            if let Some(bounds) = &mut bounds {
426                *bounds = (*bounds).union_cube(cube);
427            } else {
428                bounds = Some(GridAab::single_cube(cube));
429            }
430        }
431        bounds
432    }
433
434    /// Returns the block at the origin if there is one.
435    ///
436    /// This is the inverse of [`VoxelBrush::single()`].
437    pub fn origin_block(&self) -> Option<&Block> {
438        self.0
439            .iter()
440            .find(|&&(p, _)| p == GridVector::zero())
441            .map(|(_, block)| &**block)
442    }
443}
444
445impl<'a> PixelColor for &'a VoxelBrush<'a> {
446    type Raw = ();
447}
448impl<'a> VoxelColor<'a> for &'a VoxelBrush<'a> {
449    fn into_blocks(self) -> VoxelBrush<'a> {
450        self.as_ref()
451    }
452}
453
454impl<'a> From<&'a VoxelBrush<'a>> for SpaceTransaction {
455    /// Converts the brush into an equivalent transaction, as by
456    /// [`VoxelBrush::paint_transaction`] at the origin.
457    #[mutants::skip]
458    fn from(brush: &'a VoxelBrush<'a>) -> Self {
459        brush.paint_transaction(Cube::ORIGIN)
460    }
461}
462impl<'a> From<VoxelBrush<'a>> for SpaceTransaction {
463    /// Converts the brush into an equivalent transaction, as by
464    /// [`VoxelBrush::paint_transaction`] at the origin.
465    #[mutants::skip]
466    fn from(brush: VoxelBrush<'a>) -> Self {
467        SpaceTransaction::from(&brush)
468    }
469}
470
471impl crate::universe::VisitHandles for VoxelBrush<'_> {
472    fn visit_handles(&self, visitor: &mut dyn crate::universe::HandleVisitor) {
473        for (_, block) in self.0.iter() {
474            block.visit_handles(visitor);
475        }
476    }
477}
478
479/// Converts the return value of [`Mutation::set`] to the return value of
480/// [`DrawTarget::draw_pixel`], by making out-of-bounds not an error.
481fn ignore_out_of_bounds(result: Result<bool, SetCubeError>) -> Result<(), SetCubeError> {
482    match result {
483        Ok(_) => Ok(()),
484        // Drawing out of bounds is not an error.
485        Err(SetCubeError::OutOfBounds { .. }) => Ok(()),
486        Err(e) => Err(e),
487    }
488}
489
490#[cfg(test)]
491mod tests {
492    use super::*;
493    use crate::block::{self, AIR};
494    use crate::content::make_some_blocks;
495    use crate::space::Space;
496    use crate::universe::ReadTicket;
497    use embedded_graphics::Drawable as _;
498    use embedded_graphics::primitives::{Primitive, PrimitiveStyle};
499
500    /// With identity transform, `rectangle_to_aab`'s output matches exactly as one might
501    /// expect.
502    #[test]
503    fn rectangle_to_aab_simple() {
504        assert_eq!(
505            rectangle_to_aab(
506                Rectangle::new(Point::new(3, 4), Size::new(10, 20)),
507                Gridgid::IDENTITY,
508                GridAab::ORIGIN_CUBE
509            ),
510            GridAab::from_lower_size([3, 4, 0], [10, 20, 1])
511        );
512    }
513
514    #[test]
515    fn rectangle_to_aab_y_flipped() {
516        assert_eq!(
517            rectangle_to_aab(
518                Rectangle::new(Point::new(3, 4), Size::new(10, 20)),
519                Gridgid::FLIP_Y,
520                GridAab::ORIGIN_CUBE
521            ),
522            GridAab::from_lower_size([3, -4 - 20 + 1, 0], [10, 20, 1])
523        );
524    }
525
526    #[test]
527    fn rectangle_to_aab_with_brush() {
528        assert_eq!(
529            rectangle_to_aab(
530                Rectangle::new(Point::new(10, 10), Size::new(10, 10)),
531                Gridgid::IDENTITY,
532                GridAab::from_lower_size([0, 0, 0], [2, 1, 2])
533            ),
534            GridAab::from_lower_upper([10, 10, 0], [21, 20, 2])
535        );
536    }
537
538    #[test]
539    fn rectangle_to_aab_empty_rects_no_transform() {
540        assert_eq!(
541            rectangle_to_aab(
542                Rectangle::new(Point::new(3, 4), Size::new(0, 10)),
543                Gridgid::IDENTITY,
544                GridAab::ORIGIN_CUBE
545            ),
546            GridAab::from_lower_size([3, 4, 0], [0, 10, 0]),
547            "empty width",
548        );
549        assert_eq!(
550            rectangle_to_aab(
551                Rectangle::new(Point::new(3, 4), Size::new(10, 0)),
552                Gridgid::IDENTITY,
553                GridAab::ORIGIN_CUBE
554            ),
555            GridAab::from_lower_size([3, 4, 0], [10, 0, 0]),
556            "empty height",
557        );
558    }
559
560    /// Test consistency between [`rectangle_to_aab`], the cubes affected by actual drawing,
561    /// and `<DrawingPlane as Dimensions>::bounding_box()`.
562    #[test]
563    fn rectangle_to_aab_consistent_with_drawing_and_bounding_box() {
564        // The bounds of this space will be used as the test case, by constructing various
565        // transformed DrawingPlanes and seeing what they think their bounding box is.
566        let space_bounds = GridAab::from_lower_upper([-11, -20, -100], [30, 10, 100]);
567        let mut space = Space::builder(space_bounds).build();
568
569        // Brush to nominally draw with.
570        // TODO: also test bigger or offset brushes
571        let brush = VoxelBrush::single(block::from_color!(Rgba::WHITE));
572        let style = PrimitiveStyle::with_fill(&brush);
573        let brush_box = brush.bounds().unwrap();
574
575        println!(
576            "Space bounds: {space_bounds:?} size {:?}\n\n",
577            space_bounds.size()
578        );
579
580        let mut all_good = true;
581        space.mutate(ReadTicket::stub(), |m| {
582            for rotation in GridRotation::ALL {
583                // Pick a translation to test.
584                // Note: these translations must not cause the depth axis to exit the space_bounds.
585                for translation in [
586                    GridVector::zero(),
587                    GridVector::new(10, 5, 0),
588                    GridVector::new(-10, -5, 0),
589                ] {
590                    // The transform we're testing with.
591                    let transform = Gridgid {
592                        rotation,
593                        translation,
594                    };
595
596                    // Fetch what DrawingPlane thinks the nominal bounding box is.
597                    let plane: DrawingPlane<'_, _, VoxelBrush<'static>> = m.draw_target(transform);
598                    let plane_bbox = plane.bounding_box();
599                    // Convert that back to a GridAab in the space's coordinate system.
600                    let bounds_converted = rectangle_to_aab(plane_bbox, transform, brush_box);
601                    // We can't do an equality test, because the bounds_converted will be flat
602                    // on some axis (which axis depending on the rotation), but it should
603                    // always be contained within the space bounds (given that the space bounds
604                    // contain the transformed origin).
605                    let bounding_box_fits = space_bounds.contains_box(bounds_converted);
606
607                    // Try actually drawing (to transaction, since that has an easy bounds check),
608                    // and see what the bounds of the drawing are.
609                    let mut txn = SpaceTransaction::default();
610                    plane_bbox.into_styled(style).draw(&mut txn.draw_target(transform)).unwrap();
611                    let txn_bounds = txn.bounds().unwrap();
612                    let txn_matches_bounding_box = txn_bounds == bounds_converted;
613
614                    println!("{transform:?} → rect {plane_bbox:?}");
615                    println!("  rectangle_to_aab() = {bounds_converted:?} ({bounding_box_fits:?})");
616                    println!("  drawn = {txn_bounds:?} ({txn_matches_bounding_box:?})");
617                    println!();
618                    all_good &= bounding_box_fits && txn_matches_bounding_box;
619                }
620            }
621        });
622        assert!(all_good);
623    }
624
625    /// Test using a particular color type with [`DrawingPlane`].
626    fn test_color_drawing<'c, C>(color_value: C, expected_block: &Block)
627    where
628        C: VoxelColor<'c>,
629    {
630        let mut space = Space::empty_positive(100, 100, 100);
631        space.mutate(ReadTicket::stub(), |m| {
632            let mut display = m.draw_target(Gridgid::from_translation([1, 2, 4]));
633            Pixel(Point::new(2, 3), color_value).draw(&mut display).unwrap();
634        });
635        assert_eq!(space[[3, 5, 4]], *expected_block);
636    }
637
638    #[test]
639    fn draw_with_block_ref() {
640        let [block] = make_some_blocks();
641        test_color_drawing(&block, &block);
642    }
643
644    #[test]
645    fn draw_with_eg_rgb888() {
646        // Note that there is a conversion from sRGB to linear.
647        test_color_drawing(
648            Rgb888::new(0, 127, 255),
649            &Rgba::new(0.0, 0.21223073, 1.0, 1.0).into(),
650        );
651    }
652
653    #[test]
654    fn draw_with_our_rgb() {
655        let color = Rgb01::new(0.73, 0.27, 0.11);
656        test_color_drawing(color, &color.into());
657    }
658
659    #[test]
660    fn draw_with_our_rgba() {
661        let color = Rgba::new(0.73, 0.27, 0.11, 0.9);
662        test_color_drawing(color, &color.into());
663    }
664
665    #[test]
666    fn draw_with_brush() -> Result<(), SetCubeError> {
667        let [block_0, block_1] = make_some_blocks();
668        let mut space = Space::empty_positive(100, 100, 100);
669
670        let brush = VoxelBrush::new([([0, 0, 0], &block_0), ([0, 1, 1], &block_1)]);
671        space.mutate(ReadTicket::stub(), |m| {
672            Pixel(Point::new(2, 3), &brush)
673                .draw(&mut m.draw_target(Gridgid::from_translation([0, 0, 4])))
674        })?;
675
676        assert_eq!(&space[[2, 3, 4]], &block_0);
677        assert_eq!(&space[[2, 4, 5]], &block_1);
678        Ok(())
679    }
680
681    #[test]
682    fn draw_out_of_bounds_is_ok() -> Result<(), SetCubeError> {
683        let mut space = Space::empty_positive(100, 100, 100);
684
685        // This should not fail with SetCubeError::OutOfBounds
686        space.mutate(ReadTicket::stub(), |m| {
687            Pixel(Point::new(-10, 0), Rgb888::new(0, 127, 255))
688                .draw(&mut m.draw_target(Gridgid::from_translation([0, 0, 4])))
689        })?;
690        Ok(())
691    }
692
693    /// TODO: We no longer have an easy way to trigger a set() failure
694    #[test]
695    #[cfg(false)]
696    fn draw_set_failure() {
697        let name = Name::from("foo");
698        let dead_block = Block::builder().voxels_handle(R1, Handle::new_gone(name.clone())).build();
699        let mut space = Space::empty_positive(100, 100, 100);
700
701        // This should fail with SetCubeError::EvalBlock since the block has no valid definition
702        assert_eq!(
703            Pixel(Point::new(0, 0), &dead_block)
704                .draw(&mut space.draw_target(Gridgid::IDENTITY))
705                .unwrap_err(),
706            SetCubeError::EvalBlock(EvalBlockError::Handle(HandleError::Gone(name)))
707        );
708    }
709
710    #[test]
711    fn voxel_brush_single() {
712        let [block] = make_some_blocks();
713        assert_eq!(
714            VoxelBrush::single(&block),
715            VoxelBrush::new([([0, 0, 0], &block)]),
716        );
717    }
718
719    #[test]
720    fn voxel_brush_translate() {
721        let [block] = make_some_blocks();
722        assert_eq!(
723            VoxelBrush::new([([1, 2, 3], &block)]).translate([10, 20, 30]),
724            VoxelBrush::new([([11, 22, 33], &block)]),
725        );
726    }
727
728    /// Test that `VoxelBrush::bounds()` gives the same result as `SpaceTransaction::bounds()`.
729    #[test]
730    fn voxel_brush_bounds() {
731        for brush_vec in [
732            vec![],
733            vec![([0, 0, 0], AIR)],
734            vec![([100, 0, 0], AIR)],
735            vec![([0, 0, 5], AIR), ([0, 5, 0], AIR)],
736        ] {
737            let brush: VoxelBrush<'static> = VoxelBrush::new(brush_vec);
738            assert_eq!(
739                brush.bounds(),
740                brush.paint_transaction(Cube::ORIGIN).bounds()
741            );
742        }
743    }
744}