1#![expect(
4 clippy::module_name_repetitions,
5 reason = "module is private; https://github.com/rust-lang/rust-clippy/issues/8524"
6)]
7
8use alloc::boxed::Box;
9use core::iter;
10use eg::mono_font::MonoFont;
11use embedded_graphics::mono_font::mapping::GlyphMapping;
12
13use arcstr::ArcStr;
14use embedded_graphics as eg;
15use embedded_graphics::{Drawable as _, prelude::Dimensions as _};
16use euclid::vec3;
17
18use crate::block::{self, Block, BlockAttributes, Evoxel, MinEval, Resolution};
19use crate::content::palette;
20use crate::drawing::{DrawingPlane, rectangle_to_aab};
21use crate::math::{
22 FaceMap, GridAab, GridCoordinate, GridVector, Gridgid, Rgb, Rgba, Vol, rgba_const,
23};
24use crate::space::{self, SpaceTransaction};
25use crate::universe;
26
27#[cfg(doc)]
28use crate::block::{Modifier, Primitive};
29
30use super::Evoxels;
31
32#[derive(Clone, Debug, Eq, Hash, PartialEq)]
50pub struct Text {
51 string: ArcStr,
52
53 font: Font,
54
55 foreground: Block,
56
57 outline: Option<Block>,
58
59 resolution: Resolution,
60
61 layout_bounds: GridAab,
63
64 positioning: Positioning,
65
66 debug: bool,
67}
68
69#[derive(Debug, Clone)]
71#[must_use]
72pub struct TextBuilder {
73 string: ArcStr,
74
75 font: Font,
76
77 foreground: Block,
78
79 outline: Option<Block>,
80
81 resolution: Resolution,
82
83 layout_bounds: Option<GridAab>,
84
85 positioning: Positioning,
86
87 debug: bool,
88}
89
90impl Text {
91 pub fn builder() -> TextBuilder {
94 TextBuilder::default()
95 }
96
97 pub fn into_builder(self) -> TextBuilder {
99 let Self {
100 string,
101 font,
102 foreground,
103 outline,
104 resolution,
105 layout_bounds,
106 positioning,
107 debug,
108 } = self;
109 TextBuilder {
110 string,
111 font,
112 foreground,
113 outline,
114 resolution,
115 layout_bounds: Some(layout_bounds),
116 positioning,
117 debug,
118 }
119 }
120
121 pub fn string(&self) -> &ArcStr {
123 &self.string
124 }
125 pub fn font(&self) -> &Font {
127 &self.font
128 }
129
130 pub fn resolution(&self) -> &Font {
132 &self.font
133 }
134
135 pub fn layout_bounds(&self) -> GridAab {
142 self.layout_bounds
143 }
144
145 pub fn positioning(&self) -> Positioning {
147 self.positioning
148 }
149
150 pub fn debug(&self) -> bool {
152 self.debug
153 }
154
155 pub fn bounding_voxels(&self) -> GridAab {
161 let dummy_voxel = Evoxel::from_color(Rgba::BLACK); self.with_transform_and_drawable(
165 match self.outline {
166 Some(_) => Brush::Outline {
167 foreground: dummy_voxel,
168 outline: dummy_voxel,
169 },
170 None => Brush::Plain(dummy_voxel),
171 },
172 GridVector::zero(),
173 |_text_obj, text_aab, _drawing_transform| text_aab,
174 )
175 }
176 pub fn bounding_blocks(&self) -> GridAab {
181 self.bounding_voxels().divide(self.resolution.into())
182 }
183
184 pub fn installation(
194 &self,
195 transform: Gridgid,
196 block_fn: impl Fn(Block) -> Block,
197 ) -> SpaceTransaction {
198 let dst_to_src_transform = transform.inverse();
199 let block_rotation = transform.rotation;
200 SpaceTransaction::filling(
201 self.bounding_blocks().transform(transform).unwrap(),
202 |cube| {
203 space::CubeTransaction::replacing(
204 None,
205 Some(block_fn(
206 Block::from_primitive(block::Primitive::Text {
207 text: self.clone(),
208 offset: dst_to_src_transform
209 .transform_cube(cube)
210 .lower_bounds()
211 .to_vector(),
212 })
213 .rotate(block_rotation),
214 )),
215 )
216 },
217 )
218 }
219
220 pub fn single_block(self) -> Block {
223 Block::from_primitive(block::Primitive::Text {
224 text: self,
225 offset: GridVector::zero(),
226 })
227 }
228
229 pub(in crate::block) fn evaluate(
232 &self,
233 block_offset: GridVector,
234 filter: &super::EvalFilter<'_>,
235 ) -> Result<MinEval, block::InEvalError> {
236 if filter.skip_eval {
237 return Ok(block::AIR_EVALUATED_MIN); }
241
242 let brush = {
244 let _recursion_scope = block::Budget::recurse(&filter.budget)?;
245
246 let ev_foreground = self.foreground.evaluate_to_evoxel_internal(filter)?;
249 match self.outline {
250 Some(ref block) => Brush::Outline {
251 foreground: ev_foreground,
252 outline: block.evaluate_to_evoxel_internal(filter)?,
253 },
254 None => Brush::Plain(ev_foreground),
255 }
256 };
257
258 self.with_transform_and_drawable(
259 brush,
260 block_offset,
261 |text_obj, text_aab, drawing_transform| {
262 let voxels: Evoxels =
263 match text_aab.intersection_cubes(GridAab::for_block(self.resolution)) {
264 Some(bounds_in_this_block) => {
265 let fill = if self.debug {
266 DEBUG_TEXT_BOUNDS_VOXEL
267 } else {
268 Evoxel::AIR
269 };
270 let mut voxels: Vol<Box<[Evoxel]>> =
271 Vol::from_fn(bounds_in_this_block, |_| fill);
272
273 text_obj
274 .draw(&mut DrawingPlane::new(&mut voxels, drawing_transform))
275 .unwrap();
276
277 Evoxels::from_many(self.resolution, voxels.map_container(Into::into))
278 }
279
280 None => Evoxels::from_one(if self.debug {
281 DEBUG_NO_INTERSECTION_VOXEL
282 } else {
283 Evoxel::AIR
284 }),
285 };
286
287 Ok(MinEval::new(
288 BlockAttributes {
289 display_name: self.string.clone(),
290 ..BlockAttributes::default()
291 },
292 voxels,
293 ))
294 },
295 )
296 }
297
298 fn thickness(&self) -> GridCoordinate {
299 match self.outline {
300 Some(_) => 2,
301 None => 1,
302 }
303 }
304
305 fn with_transform_and_drawable<R>(
306 &self,
307 brush: Brush,
308 block_offset: GridVector,
309 f: impl FnOnce(
310 &'_ eg::text::Text<'_, eg::mono_font::MonoTextStyle<'_, Brush>>,
311 GridAab,
312 Gridgid,
313 ) -> R,
314 ) -> R {
315 let resolution_g = GridCoordinate::from(self.resolution);
316 let Positioning {
317 x: positioning_x,
318 line_y,
319 z: positioning_z,
320 } = self.positioning;
321
322 let lb = self.layout_bounds;
323 let layout_offset = vec3(
328 match positioning_x {
329 PositioningX::Left => lb.lower_bounds().x,
330 PositioningX::Center => libm::round(lb.center().x - 0.75) as GridCoordinate,
333 PositioningX::Right => lb.upper_bounds().x - 1,
334 },
335 match line_y {
336 PositioningY::BodyBottom | PositioningY::Baseline => lb.lower_bounds().y,
337 PositioningY::BodyMiddle => libm::round(lb.center().y - 0.75) as GridCoordinate,
338 PositioningY::BodyTop => lb.upper_bounds().y - 1,
339 },
340 match positioning_z {
341 PositioningZ::Back => lb.lower_bounds().z,
342 PositioningZ::Front => lb.upper_bounds().z.saturating_sub(self.thickness()),
343 },
344 );
345
346 let drawing_transform =
347 Gridgid::from_translation(layout_offset - (block_offset * resolution_g))
348 * Gridgid::FLIP_Y;
349
350 let character_style = eg::mono_font::MonoTextStyle::new(self.font.eg_font(), brush);
351 let text_style = eg::text::TextStyleBuilder::new()
352 .alignment(match positioning_x {
353 PositioningX::Left => eg::text::Alignment::Left,
354 PositioningX::Center => eg::text::Alignment::Center,
355 PositioningX::Right => eg::text::Alignment::Right,
356 })
357 .baseline(match line_y {
358 PositioningY::BodyTop => eg::text::Baseline::Top,
359 PositioningY::BodyMiddle => eg::text::Baseline::Middle,
360 PositioningY::Baseline => eg::text::Baseline::Alphabetic,
361 PositioningY::BodyBottom => eg::text::Baseline::Bottom,
362 })
363 .build();
364 let text_obj = &eg::text::Text::with_text_style(
365 self.string.as_str(),
366 eg::prelude::Point::new(0, 0),
367 character_style,
368 text_style,
369 );
370 let text_aab = brush.expand(rectangle_to_aab(
371 text_obj.bounding_box(),
372 drawing_transform,
373 GridAab::ORIGIN_CUBE,
374 ));
375
376 f(text_obj, text_aab, drawing_transform)
377 }
378}
379
380impl universe::VisitHandles for Text {
381 fn visit_handles(&self, visitor: &mut dyn universe::HandleVisitor) {
382 let Self {
383 string: _,
384 font,
385 foreground,
386 outline,
387 resolution: _,
388 layout_bounds: _,
389 positioning: _,
390 debug: _,
391 } = self;
392 font.visit_handles(visitor);
393 foreground.visit_handles(visitor);
394 outline.visit_handles(visitor);
395 }
396}
397
398#[cfg(feature = "arbitrary")]
399impl<'a> arbitrary::Arbitrary<'a> for Text {
400 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
401 let layout_bounds = GridAab::checked_from_lower_upper(
408 <[i16; 3]>::arbitrary(u)?.map(i32::from),
409 <[u16; 3]>::arbitrary(u)?.map(i32::from),
410 )
411 .map_err(|_volume_error| arbitrary::Error::IncorrectFormat)?;
412
413 Ok(Self {
414 string: alloc::string::String::arbitrary(u)?.into(),
416 font: Font::arbitrary(u)?,
417 foreground: Block::arbitrary(u)?,
418 outline: Option::<Block>::arbitrary(u)?,
419 resolution: Resolution::arbitrary(u)?,
420 layout_bounds,
421 positioning: Positioning::arbitrary(u)?,
422 debug: bool::arbitrary(u)?,
423 })
424 }
425
426 fn size_hint(depth: usize) -> (usize, Option<usize>) {
427 Self::try_size_hint(depth).unwrap_or_default()
429 }
430
431 fn try_size_hint(
432 depth: usize,
433 ) -> Result<(usize, Option<usize>), arbitrary::MaxRecursionReached> {
434 arbitrary::size_hint::try_recursion_guard(depth, |depth| {
439 Ok(arbitrary::size_hint::and_all(&[
440 alloc::string::String::size_hint(depth),
441 Font::size_hint(depth),
442 Block::size_hint(depth),
443 Option::<Block>::size_hint(depth),
444 Resolution::size_hint(depth),
445 GridAab::size_hint(depth),
446 bool::size_hint(depth),
447 ]))
448 })
449 }
450}
451
452impl TextBuilder {
453 pub fn build(self) -> Text {
455 let Self {
456 string,
457 font,
458 foreground,
459 outline,
460 resolution,
461 layout_bounds,
462 positioning,
463 debug,
464 } = self;
465 Text {
466 string,
467 font,
468 foreground,
469 outline,
470 resolution,
471 layout_bounds: layout_bounds.unwrap_or(GridAab::for_block(resolution)),
472 positioning,
473 debug,
474 }
475 }
476
477 pub fn string(mut self, string: ArcStr) -> Self {
483 self.string = string;
484 self
485 }
486
487 pub fn font(mut self, font: Font) -> Self {
491 self.font = font;
492 self
493 }
494
495 pub fn foreground(mut self, foreground: Block) -> Self {
504 self.foreground = foreground;
505 self
506 }
507
508 pub fn outline(mut self, outline: Option<Block>) -> Self {
517 self.outline = outline;
518 self
519 }
520
521 pub fn resolution(mut self, resolution: Resolution) -> Self {
528 self.resolution = resolution;
529 self
530 }
531
532 pub fn positioning(mut self, positioning: Positioning) -> Self {
549 self.positioning = positioning;
550 self
551 }
552
553 pub fn layout_bounds(mut self, resolution: Resolution, bounds: GridAab) -> Self {
559 self.layout_bounds = Some(bounds);
560 self.resolution = resolution;
561 self
562 }
563 pub fn debug(mut self, debug: bool) -> Self {
574 self.debug = debug;
575 self
576 }
577}
578
579impl Default for TextBuilder {
580 fn default() -> Self {
581 Self {
582 string: ArcStr::new(),
583 font: Font::System16,
584 foreground: block::from_color!(palette::ALMOST_BLACK),
585 outline: None,
586 resolution: Resolution::R16,
587 layout_bounds: None,
588 positioning: Positioning {
589 x: PositioningX::Center,
590 line_y: PositioningY::BodyMiddle,
591 z: PositioningZ::Back,
592 },
593 debug: false,
594 }
595 }
596}
597
598#[derive(Clone, Debug, Eq, Hash, PartialEq)]
600#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
601#[non_exhaustive]
602pub enum Font {
603 System16,
609
610 #[doc(hidden)]
611 Logo,
613
614 #[doc(hidden)]
615 SmallerBodyText,
617}
618impl Font {
619 #[doc(hidden)]
621 pub fn eg_font(&self) -> &MonoFont<'static> {
622 use eg::mono_font::iso_8859_1 as f;
623 match self {
624 Self::System16 => &MonoFont {
625 glyph_mapping: &RemapTo8859_1(f::FONT_8X13_BOLD.glyph_mapping),
626 ..f::FONT_8X13_BOLD
627 },
628 Self::Logo => &MonoFont {
629 glyph_mapping: &RemapTo8859_1(f::FONT_9X15_BOLD.glyph_mapping),
630 ..f::FONT_9X15_BOLD
631 },
632 Self::SmallerBodyText => &MonoFont {
633 glyph_mapping: &RemapTo8859_1(f::FONT_6X10.glyph_mapping),
634 ..f::FONT_6X10
635 },
636 }
637 }
638}
639
640impl universe::VisitHandles for Font {
641 fn visit_handles(&self, _: &mut dyn universe::HandleVisitor) {
642 match self {
643 Self::System16 | Self::SmallerBodyText | Self::Logo => {}
644 }
645 }
646}
647
648#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
650#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
651#[expect(
652 clippy::exhaustive_structs,
653 reason = "TODO: probably want to do something else"
654)]
655pub struct Positioning {
656 pub x: PositioningX,
658
659 pub line_y: PositioningY,
664
665 pub z: PositioningZ,
674}
675
676#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
680#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
681#[non_exhaustive]
682pub enum PositioningX {
683 Left,
689
690 Center,
692
693 Right,
698}
699
700#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
704#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
705#[non_exhaustive]
706pub enum PositioningY {
707 BodyTop,
710
711 BodyMiddle,
716
717 Baseline,
720
721 BodyBottom,
724}
725
726#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
730#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
731#[non_exhaustive]
732pub enum PositioningZ {
733 Back,
735
736 Front,
738}
739
740#[cfg(feature = "save")]
741mod serialization {
742 use crate::block::text;
743 use crate::save::schema;
744
745 impl From<&text::Text> for schema::TextSer {
746 fn from(value: &text::Text) -> Self {
747 let &text::Text {
748 ref string,
749 ref font,
750 ref foreground,
751 ref outline,
752 resolution,
753 layout_bounds,
754 positioning,
755 debug,
756 } = value;
757 schema::TextSer::TextV1 {
758 string: string.clone(),
759 font: font.into(),
760 foreground: foreground.clone(),
761 outline: outline.clone(),
762 resolution,
763 layout_bounds,
764 positioning: positioning.into(),
765 debug,
766 }
767 }
768 }
769
770 impl From<schema::TextSer> for text::Text {
771 fn from(value: schema::TextSer) -> Self {
772 match value {
773 schema::TextSer::TextV1 {
774 string,
775 font,
776 foreground,
777 outline,
778 resolution,
779 layout_bounds,
780 positioning,
781 debug,
782 } => text::Text::builder()
783 .string(string)
784 .font(font.into())
785 .foreground(foreground)
786 .outline(outline)
787 .layout_bounds(resolution, layout_bounds)
788 .positioning(positioning.into())
789 .debug(debug)
790 .build(),
791 }
792 }
793 }
794}
795
796impl Positioning {
797 #[doc(hidden)] pub const LOW: Self = Positioning {
799 x: PositioningX::Left,
800 line_y: PositioningY::BodyBottom,
801 z: PositioningZ::Back,
802 };
803}
804
805const DEBUG_NO_INTERSECTION_VOXEL: Evoxel = Evoxel {
806 color: rgba_const!(1.0, 0.0, 0.0, 0.5),
807 emission: Rgb::ZERO,
808 selectable: true,
809 collision: block::BlockCollision::None,
810};
811const DEBUG_TEXT_BOUNDS_VOXEL: Evoxel = Evoxel {
812 color: rgba_const!(0.0, 1.0, 0.0, 0.5),
813 emission: Rgb::ZERO,
814 selectable: true,
815 collision: block::BlockCollision::None,
816};
817
818#[derive(Clone, Copy, Debug, Eq, PartialEq)]
820pub(crate) enum Brush {
821 Plain(Evoxel),
822 Outline { foreground: Evoxel, outline: Evoxel },
823}
824
825impl Brush {
826 fn expand(&self, aab: GridAab) -> GridAab {
827 match self {
828 Brush::Plain(_) => aab,
829 Brush::Outline { .. } => aab.expand(FaceMap {
830 nx: 0, ny: 0, nz: 0,
836 px: 0, py: 0, pz: 1,
839 }),
840 }
841 }
842
843 pub(crate) fn iter(&self) -> impl Iterator<Item = (GridVector, Evoxel)> + '_ {
844 use itertools::Either::{Left, Right};
845 match *self {
846 Brush::Plain(foreground) => Left(iter::once((GridVector::zero(), foreground))),
847 Brush::Outline {
848 foreground,
849 outline,
850 } => Right(
851 [
852 (vec3(0, 0, 1), foreground),
853 (vec3(0, 0, 0), outline),
854 (vec3(-1, 0, 0), outline),
855 (vec3(1, 0, 0), outline),
856 (vec3(0, -1, 0), outline),
857 (vec3(0, 1, 0), outline),
858 ]
859 .into_iter(),
860 ),
861 }
862 }
863}
864
865struct RemapTo8859_1<'a>(&'a dyn GlyphMapping);
868
869impl GlyphMapping for RemapTo8859_1<'_> {
870 fn index(&self, c: char) -> usize {
871 self.0.index(match c {
872 '‘' | '’' => '\'',
873 '“' | '”' => '"',
874 c => c,
875 })
876 }
877}
878
879#[cfg(test)]
880mod tests {
881 use super::*;
882 use crate::block::Primitive;
883 use crate::math::Cube;
884 use crate::raytracer::print_space;
885 use crate::space::Space;
886 use crate::universe::Universe;
887 use alloc::string::String;
888 use alloc::vec::Vec;
889 use arcstr::literal;
890 use pretty_assertions::assert_eq;
891
892 fn plane_to_text(voxels: Vol<&[Evoxel]>) -> Vec<String> {
894 fn convert_voxel(v: &Evoxel) -> char {
895 if v.color.fully_transparent() {
896 '.'
897 } else {
898 '#'
899 }
900 }
901
902 let z = voxels.bounds().lower_bounds().z;
903 assert_eq!(voxels.bounds().z_range().len(), 1);
904 voxels
905 .bounds()
906 .y_range()
907 .rev() .map(|y| {
909 voxels
910 .bounds()
911 .x_range()
912 .map(|x| convert_voxel(&voxels[Cube::new(x, y, z)]))
913 .collect::<String>()
914 })
915 .collect()
916 }
917
918 fn single_block_test_case(text: Text) -> (Box<Universe>, Block) {
919 let universe = Universe::new();
923
924 let block = Block::from_primitive(Primitive::Text {
927 text,
928 offset: GridVector::zero(),
929 });
930
931 {
933 let space = Space::builder(GridAab::ORIGIN_CUBE)
934 .read_ticket(universe.read_ticket())
935 .filled_with(block.clone())
936 .build();
937 print_space(&space.read(), [0., 0., 1.]);
938 }
939
940 (universe, block)
941 }
942
943 #[test]
944 fn single_line_text_smoke_test() {
945 let text = Text::builder()
946 .string(literal!("ab"))
947 .font(Font::System16)
948 .resolution(Resolution::R16)
949 .positioning(Positioning {
950 x: PositioningX::Left,
951 line_y: PositioningY::BodyBottom,
952 z: PositioningZ::Back,
953 })
954 .build();
955 let (universe, block) = single_block_test_case(text.clone());
956
957 let ev = block.evaluate(universe.read_ticket()).unwrap();
958 assert_eq!(
959 ev.attributes,
960 BlockAttributes {
961 display_name: arcstr::literal!("ab"),
962 ..BlockAttributes::default()
963 }
964 );
965 assert_eq!(
966 ev.voxels.bounds(),
967 GridAab::from_lower_size([0, 0, 0], [16, 13, 1])
968 );
969 assert_eq!(ev.voxels.bounds(), text.bounding_voxels());
970
971 assert_eq!(
972 plane_to_text(ev.voxels.as_vol_ref()),
973 vec![
974 "................",
975 "........##......",
976 "........##......",
977 "........##......",
978 ".#####..##.###..",
979 ".....##.###..##.",
980 ".######.##...##.",
981 "##...##.##...##.",
982 "##...##.##...##.",
983 "##..###.###..##.",
984 ".###.##.##.###..",
985 "................",
986 "................",
987 ]
988 )
989 }
990
991 #[test]
992 fn multiple_line() {
993 let (universe, block) = single_block_test_case(
994 Text::builder()
995 .resolution(Resolution::R32)
996 .string(literal!("abcd\nabcd"))
997 .font(Font::System16)
998 .positioning(Positioning {
999 x: PositioningX::Left,
1000 line_y: PositioningY::BodyTop, z: PositioningZ::Back,
1002 })
1003 .build(),
1004 );
1005
1006 assert_eq!(
1007 plane_to_text(block.evaluate(universe.read_ticket()).unwrap().voxels.as_vol_ref()),
1008 vec![
1009 "................................",
1010 "........##...................##.",
1011 "........##...................##.",
1012 "........##...................##.",
1013 ".#####..##.###...#####...###.##.",
1014 ".....##.###..##.###..##.##..###.",
1015 ".######.##...##.##......##...##.",
1016 "##...##.##...##.##......##...##.",
1017 "##...##.##...##.##......##...##.",
1018 "##..###.###..##.###..##.##..###.",
1019 ".###.##.##.###...#####...###.##.",
1020 "................................",
1021 "................................",
1022 "................................",
1023 "........##...................##.",
1024 "........##...................##.",
1025 "........##...................##.",
1026 ".#####..##.###...#####...###.##.",
1027 ".....##.###..##.###..##.##..###.",
1028 ".######.##...##.##......##...##.",
1029 "##...##.##...##.##......##...##.",
1030 "##...##.##...##.##......##...##.",
1031 "##..###.###..##.###..##.##..###.",
1032 ".###.##.##.###...#####...###.##.",
1033 "................................",
1034 "................................",
1035 ]
1036 )
1037 }
1038
1039 #[test]
1042 fn bounding_voxels_of_positioning_high() {
1043 let text = Text::builder()
1044 .resolution(Resolution::R32)
1045 .string(literal!("abc"))
1046 .font(Font::System16)
1047 .positioning(Positioning {
1048 x: PositioningX::Right,
1049 line_y: PositioningY::BodyTop,
1050 z: PositioningZ::Front,
1051 })
1052 .build();
1053
1054 assert_eq!(
1057 text.bounding_voxels(),
1058 GridAab::from_lower_upper([8, 19, 31], [32, 32, 32])
1059 );
1060 }
1061
1062 #[rstest::rstest]
1072 #[case(PositioningX::Left, false, 0..16, 0..48)]
1073 #[case(PositioningX::Right, false, 0..16, -32..16)]
1074 #[case(PositioningX::Center, false, 0..16, -16..32)]
1075 #[case(PositioningX::Center, true, 0..16, -6..21)]
1076 #[case(PositioningX::Center, false, 0..15, -16..32)]
1077 #[case(PositioningX::Center, true, 0..15, -6..21)]
1078 #[case(PositioningX::Center, false, 1..16, -15..33)]
1079 #[case(PositioningX::Center, true, 1..16, -5..22)]
1080 fn positioning_x(
1081 #[case] pos: PositioningX,
1082 #[case] odd_font: bool,
1083 #[case] bounds_range: core::ops::Range<i32>,
1084 #[case] expected: core::ops::Range<i32>,
1085 ) {
1086 let text = Text::builder()
1087 .string(if odd_font {
1088 literal!("abc")
1090 } else {
1091 literal!("abcdef")
1092 })
1093 .font(if odd_font { Font::Logo } else { Font::System16 })
1096 .layout_bounds(
1097 Resolution::R16,
1098 GridAab::from_ranges([bounds_range, 0..16, 0..16]),
1099 )
1100 .positioning(Positioning {
1101 x: pos,
1102 line_y: PositioningY::BodyMiddle,
1103 z: PositioningZ::Back,
1104 })
1105 .build();
1106
1107 assert_eq!(text.bounding_voxels().x_range(), expected);
1108 }
1109
1110 #[test]
1111 fn no_intersection_with_block() {
1112 let (universe, block) = single_block_test_case({
1113 Text::builder()
1114 .string(literal!("ab"))
1115 .font(Font::System16)
1116 .layout_bounds(
1117 Resolution::R16,
1118 GridAab::from_lower_size([100000, 0, 0], [16, 16, 16]),
1119 )
1120 .build()
1121 });
1122
1123 let ev = block.evaluate(universe.read_ticket()).unwrap();
1124 assert_eq!(
1125 ev.attributes,
1126 BlockAttributes {
1127 display_name: arcstr::literal!("ab"),
1128 ..BlockAttributes::default()
1129 }
1130 );
1131 assert_eq!(ev.resolution(), Resolution::R1);
1132 assert!(!ev.visible());
1133 }
1134
1135 }