1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
// Copyright 2020 Kevin Reid under the terms of the MIT License as detailed
// in the accompanying file README.md or <http://opensource.org/licenses/MIT>.

//! Draw 2D graphics into spaces and blocks, including text, using an adapter for the
//! `embedded_graphics` crate.

use embedded_graphics::drawable::{Drawable, Pixel};
use embedded_graphics::fonts::{Font, Text};
use embedded_graphics::geometry::{Dimensions, Point, Size};
use embedded_graphics::pixelcolor::{Rgb888, RgbColor};
use embedded_graphics::style::TextStyleBuilder;
use embedded_graphics::transform::Transform;
use embedded_graphics::DrawTarget;
use std::convert::{Infallible, TryInto};

use crate::block::{Block, BlockAttributes};
use crate::blockgen::BlockGen;
use crate::math::{GridCoordinate, GridPoint, RGB, RGBA};
use crate::space::{Grid, Space};

/// Draw text into a `Space`, extending in the +X and -Y directions from `origin`.
pub fn draw_text<F>(
    space: &mut Space,
    color: Rgb888,
    origin: GridPoint,
    font: F,
    text: impl AsRef<str>,
) where
    F: Font + Copy,
{
    let style = TextStyleBuilder::new(font).text_color(color).build();
    Text::new(text.as_ref(), Point::new(origin.x as i32, origin.y as i32))
        .into_styled(style)
        .draw(&mut VoxelDisplayAdapter::new(space, origin.z))
        .unwrap(); // cannot fail
}

/// Generate a set of blocks which together display the given `Drawable` which may be
/// larger than one block. The Z position is always the middle of the block.
pub fn draw_to_blocks<D>(ctx: &mut BlockGen, object: D) -> Space
where
    for<'a> &'a D: Drawable<Rgb888>,
    D: Dimensions + Transform,
{
    let block_size: i32 = ctx.size;
    let top_left_2d = object.top_left();
    let bottom_right_2d = object.bottom_right();
    // Compute corners as Grid knows them. Note that the Y coordinate is flipped because
    // for text drawing, embedded_graphics assumes a Y-down coordinate system.
    let low_block = GridPoint::new(
        floor_divide(top_left_2d.x, block_size),
        floor_divide(-bottom_right_2d.y, block_size),
        0,
    );
    let high_block = GridPoint::new(
        ceil_divide(bottom_right_2d.x, block_size),
        ceil_divide(-top_left_2d.y, block_size),
        1,
    );
    let block_grid = Grid::new(low_block, high_block - low_block);
    let mut output_space = Space::empty(block_grid);

    for cube in block_grid.interior_iter() {
        let mut block_space = ctx.new_block_space();

        if false {
            // For debugging block bounds chosen for the graphic. TODO: Keep this around
            // as an option but draw a full bounding box instead.
            block_space.set(
                (0, 0, 0),
                &Block::Atom(BlockAttributes::default(), RGBA::new(1.0, 0.0, 0.0, 1.0)),
            );
        }

        let offset = Point::new(-cube.x as i32 * block_size, cube.y as i32 * block_size);
        object
            .translate(offset)
            .draw(&mut VoxelDisplayAdapter::new(
                &mut block_space,
                ctx.size / 2,
            ))
            .unwrap(); // cannot fail
        output_space.set(
            cube,
            // TODO: Allow attribute alteration.
            &Block::Recur(
                BlockAttributes::default(),
                ctx.universe.insert_anonymous(block_space),
            ),
        );
    }
    output_space
}

/// Adapter to use a `Space` as a `embedded_graphics::DrawTarget`.
///
/// The coordinate system is currently fixed to map X to X, Y to -Y, and a constant to Z.
/// The vertical flip is because embedded_graphics assumes Y-down coordinates for text.
struct VoxelDisplayAdapter<'a> {
    space: &'a mut Space,
    // TODO: allow input pixel color to control z, or even thickness/patterning,
    // by providing a custom type instead of Rgb888. (Unfortunately, pixel color types
    // are required to be Copy so we cannot just use Block.)
    z: GridCoordinate,
}

impl<'a> VoxelDisplayAdapter<'a> {
    fn new(space: &'a mut Space, z: GridCoordinate) -> Self {
        Self { space, z }
    }
}

impl DrawTarget<Rgb888> for VoxelDisplayAdapter<'_> {
    type Error = Infallible;

    fn draw_pixel(&mut self, pixel: Pixel<Rgb888>) -> Result<(), Self::Error> {
        let Pixel(Point { x, y }, color) = pixel;
        self.space.set(
            (x as GridCoordinate, -y as GridCoordinate, self.z),
            // TODO: Allow attribute alteration.
            &Block::Atom(BlockAttributes::default(), RGB::from(color).with_alpha(1.0)),
        );
        Ok(())
    }

    fn size(&self) -> Size {
        let size = self.space.grid().size();
        Size {
            // TODO: Surely there's a better way to write a saturating cast?
            width: size.x.try_into().unwrap_or(u32::MAX),
            height: size.y.try_into().unwrap_or(u32::MAX),
        }
    }
}

/// Adapt embedded_graphics's color type to ours.
impl From<Rgb888> for RGB {
    fn from(color: Rgb888) -> RGB {
        RGB::new(
            f32::from(color.r()) / 255.0,
            f32::from(color.g()) / 255.0,
            f32::from(color.b()) / 255.0,
        )
    }
}

// TODO: dig up a crate that does this?
fn ceil_divide(a: i32, b: i32) -> i32 {
    assert!(b > 0);
    if a < 0 {
        a / b
    } else {
        (a + b - 1) / b
    }
}
fn floor_divide(a: i32, b: i32) -> i32 {
    assert!(b > 0);
    if a > 0 {
        a / b
    } else {
        (a - (b - 1)) / b
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::math::RGBA;
    use crate::universe::Universe;
    use embedded_graphics::primitives::{Primitive, Rectangle};
    use embedded_graphics::style::{PrimitiveStyle, PrimitiveStyleBuilder};

    #[test]
    fn drawing_adapter_works() -> Result<(), Infallible> {
        let mut space = Space::empty_positive(100, 100, 100);

        Pixel(Point::new(2, -3), Rgb888::new(0, 127, 255))
            .draw(&mut VoxelDisplayAdapter::new(&mut space, 4))?;
        assert_eq!(
            space[(2, 3, 4)].color(),
            RGBA::new(0.0, 127.0 / 255.0, 1.0, 1.0)
        );
        Ok(())
    }

    fn a_primitive_style() -> PrimitiveStyle<Rgb888> {
        PrimitiveStyleBuilder::new()
            .fill_color(Rgb888::new(0, 127, 255))
            .build()
    }
    /// Cube color corresponding to a_primitive_style().
    fn a_primitive_color() -> RGBA {
        RGBA::new(0.0, 127.0 / 255.0, 1.0, 1.0)
    }

    #[test]
    fn draw_to_blocks_bounds_one_block() {
        let mut universe = Universe::new();
        let mut ctx: BlockGen = BlockGen::new(&mut universe, 16);
        let drawable =
            Rectangle::new(Point::new(0, 0), Point::new(2, 3)).into_styled(a_primitive_style());
        let space = draw_to_blocks(&mut ctx, drawable);
        // Output is at negative Y because coordinate system is flipped.
        assert_eq!(*space.grid(), Grid::new((0, -1, 0), (1, 1, 1)));
        let ref block_space_ref = space[(0, -1, 0)].space().expect("not a recursive block");
        assert_eq!(
            block_space_ref.borrow()[(0, 15, 8)].color(),
            a_primitive_color()
        );
    }

    #[test]
    fn draw_to_blocks_bounds_negative_coords_one_block() {
        let mut universe = Universe::new();
        let mut ctx: BlockGen = BlockGen::new(&mut universe, 16);
        let drawable =
            Rectangle::new(Point::new(-3, -2), Point::new(0, 0)).into_styled(a_primitive_style());
        let space = draw_to_blocks(&mut ctx, drawable);
        assert_eq!(*space.grid(), Grid::new((-1, 0, 0), (1, 1, 1)));
        let ref block_space_ref = space[(-1, 0, 0)].space().expect("not a recursive block");
        assert_eq!(
            block_space_ref.borrow()[(15, 0, 8)].color(),
            a_primitive_color()
        );
    }
}