1use 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
28pub 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
43pub fn rectangle_to_aab(rectangle: Rectangle, transform: Gridgid, max_brush: GridAab) -> GridAab {
67 #![allow(clippy::too_long_first_doc_paragraph)] if rectangle.size.width == 0 || rectangle.size.height == 0 {
78 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 type_converted.transform(transform).unwrap()
87 } else {
88 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 let transformed = type_converted_excluding_size.transform(transform).unwrap();
97
98 transformed.minkowski_sum(max_brush).unwrap()
103 }
104}
105
106#[derive(Debug)]
113#[expect(clippy::module_name_repetitions)]
114pub struct DrawingPlane<'s, T, C> {
115 space: &'s mut T,
116 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 fn convert_point(&self, point: Point) -> Cube {
134 Cube::from(self.transform.transform_point(GridPoint::new(point.x, point.y, 0)))
137 }
138}
139
140impl<'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 let cube = self.convert_point(point);
157 color.into_blocks().paint(self.space, cube)?;
159 }
160 Ok(())
161 }
162}
163
164impl<'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 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 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
257pub trait VoxelColor<'a>: PixelColor {
264 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
293impl<'a> VoxelColor<'a> for Rgb888 {
295 fn into_blocks(self) -> VoxelBrush<'a> {
296 VoxelBrush::single(Block::from(Rgb01::from(self)))
297 }
298}
299
300#[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 pub const EMPTY_REF: &'static Self = &VoxelBrush(Vec::new());
312}
313
314impl<'a> VoxelBrush<'a> {
315 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 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 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 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 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 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 pub fn as_ref(&self) -> VoxelBrush<'_> {
383 VoxelBrush(self.0.iter().map(|(v, b)| (*v, Cow::Borrowed(b.as_ref()))).collect())
384 }
385
386 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 #[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 *block_offset += offset;
398 }
399 self
400 }
401
402 #[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 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 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 #[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 #[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
479fn ignore_out_of_bounds(result: Result<bool, SetCubeError>) -> Result<(), SetCubeError> {
482 match result {
483 Ok(_) => Ok(()),
484 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 #[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]
563 fn rectangle_to_aab_consistent_with_drawing_and_bounding_box() {
564 let space_bounds = GridAab::from_lower_upper([-11, -20, -100], [30, 10, 100]);
567 let mut space = Space::builder(space_bounds).build();
568
569 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 for translation in [
586 GridVector::zero(),
587 GridVector::new(10, 5, 0),
588 GridVector::new(-10, -5, 0),
589 ] {
590 let transform = Gridgid {
592 rotation,
593 translation,
594 };
595
596 let plane: DrawingPlane<'_, _, VoxelBrush<'static>> = m.draw_target(transform);
598 let plane_bbox = plane.bounding_box();
599 let bounds_converted = rectangle_to_aab(plane_bbox, transform, brush_box);
601 let bounding_box_fits = space_bounds.contains_box(bounds_converted);
606
607 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 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 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 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 #[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 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]
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}