1use alloc::vec;
2use core::mem;
3
4use crate::block::{
5 self, AIR, Block, BlockCollision, Evoxel, Evoxels, MinEval, Modifier, Resolution::R1,
6};
7use crate::math::{
8 Cube, GridAab, GridCoordinate, GridPoint, GridRotation, GridSize, GridVector, PositiveSign,
9 Rgb, Vol, ZeroOne,
10};
11use crate::op::Operation;
12use crate::universe;
13
14#[derive(Clone, Debug, Eq, Hash, PartialEq)]
21#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
22#[non_exhaustive]
23pub struct Composite {
24 pub source: Block,
27
28 pub operator: CompositeOperator,
30
31 pub reverse: bool,
33
34 pub disassemblable: bool,
36 }
40
41impl Composite {
42 pub fn new(source: Block, operator: CompositeOperator) -> Self {
45 Self {
46 source,
47 operator,
48 reverse: false,
49 disassemblable: false,
50 }
51 }
52
53 #[must_use]
55 pub fn reversed(mut self) -> Self {
56 self.reverse = !self.reverse;
57 self
58 }
59
60 #[must_use]
65 pub fn with_disassemblable(mut self) -> Self {
66 self.disassemblable = true;
67 self
68 }
69
70 pub fn compose_or_replace(mut self, mut destination: Block) -> Block {
89 let dest_rot = if let Some(&Modifier::Rotate(dest_rot)) = destination.modifiers().last() {
91 destination.modifiers_mut().pop();
92 dest_rot
93 } else {
94 GridRotation::IDENTITY
95 };
96
97 if destination == AIR {
98 self.source
103 } else if self.source == AIR {
104 destination.rotate(dest_rot)
106 } else {
107 self.source = self.source.rotate(dest_rot.inverse());
108 destination.with_modifier(self).rotate(dest_rot)
109 }
110 }
111
112 pub fn stack(destination: Block, parts: impl IntoIterator<Item = Composite>) -> Block {
114 parts
115 .into_iter()
116 .fold(destination, |block, part| part.compose_or_replace(block))
117 }
118
119 pub(super) fn evaluate(
121 &self,
122 block: &Block,
123 this_modifier_index: usize,
124 mut dst_evaluated: MinEval,
125 filter: &block::EvalFilter<'_>,
126 ) -> Result<MinEval, block::InEvalError> {
127 let Composite {
128 ref source,
129 operator,
130 reverse,
131 disassemblable,
132 } = *self;
133
134 let mut src_evaluated = {
137 let _recursion_scope = block::Budget::recurse(&filter.budget)?;
138 source.evaluate_impl(filter)?
139 };
140
141 if filter.skip_eval {
142 return Ok(dst_evaluated);
143 }
144
145 if reverse {
147 mem::swap(&mut src_evaluated, &mut dst_evaluated);
148 }
149
150 evaluate_composition(
151 src_evaluated,
152 dst_evaluated,
153 operator,
154 filter,
155 &CompEvalCtx {
156 block,
157 this_modifier_index: Some(this_modifier_index),
158 was_reversed: reverse,
159 disassemblable,
160 },
161 )
162 }
163
164 pub(super) fn unspecialize(&self, entire_block: &Block) -> block::ModifierUnspecialize {
166 if self.disassemblable {
167 let mut destination = entire_block.clone();
168 destination.modifiers_mut().pop().expect("Missing Composite modifier");
169 block::ModifierUnspecialize::Replace(vec![self.source.clone(), destination])
170 } else {
171 block::ModifierUnspecialize::Keep
172 }
173 }
174
175 pub(crate) fn rotationally_symmetric(&self) -> bool {
176 let Self {
177 source,
178 operator,
179 reverse: _,
180 disassemblable: _,
181 } = self;
182 source.rotationally_symmetric() && operator.rotationally_symmetric()
183 }
184
185 #[must_use]
186 pub(crate) fn rotate(self, rotation: GridRotation) -> Self {
187 let Self {
188 source,
189 operator,
190 reverse,
191 disassemblable,
192 } = self;
193 Self {
194 source: source.rotate(rotation),
195 operator,
196 reverse,
197 disassemblable,
198 }
199 }
200}
201
202struct CompEvalCtx<'a> {
204 block: &'a Block,
205 this_modifier_index: Option<usize>,
207 was_reversed: bool,
208 disassemblable: bool,
209}
210
211fn evaluate_composition(
217 src_evaluated: MinEval,
218 dst_evaluated: MinEval,
219 operator: CompositeOperator,
220 filter: &block::EvalFilter<'_>,
221 ctx: &CompEvalCtx<'_>,
222) -> Result<MinEval, block::InEvalError> {
223 if operator == CompositeOperator::Over && src_evaluated == block::AIR_EVALUATED_MIN {
228 return Ok(dst_evaluated);
229 }
230
231 let (dst_att, mut dst_voxels) = dst_evaluated.into_parts();
233 let (src_att, mut src_voxels) = src_evaluated.into_parts();
234
235 let src_resolution = src_voxels.resolution();
236 let dst_resolution = dst_voxels.resolution();
237 let effective_resolution = src_resolution.max(dst_resolution);
238 let src_scale =
239 GridCoordinate::from(effective_resolution) / GridCoordinate::from(src_resolution);
240 let dst_scale =
241 GridCoordinate::from(effective_resolution) / GridCoordinate::from(dst_resolution);
242
243 let src_bounds_scaled = bounds_excluding_air(&src_voxels, src_scale);
244 let dst_bounds_scaled = bounds_excluding_air(&dst_voxels, dst_scale);
245
246 let output_bounds = operator.bounds(src_bounds_scaled, dst_bounds_scaled);
247
248 let intersection_for_blend = src_voxels
250 .bounds()
251 .intersection_cubes(dst_voxels.bounds())
252 .unwrap_or(GridAab::ORIGIN_EMPTY);
253
254 let attributes = block::BlockAttributes {
255 display_name: if dst_att.display_name.is_empty() {
260 src_att.display_name
261 } else {
262 dst_att.display_name
263 },
264 selectable: src_att.selectable | dst_att.selectable,
265 inventory: src_att.inventory.concatenate(dst_att.inventory),
266 ambient_sound: dst_att.ambient_sound, rotation_rule: dst_att.rotation_rule, placement_action: operator
269 .blend_operations(
270 ctx,
271 src_att.placement_action.as_ref().map(|a| &a.operation),
272 dst_att.placement_action.as_ref().map(|a| &a.operation),
273 )
274 .map(|operation| block::PlacementAction {
275 operation,
276 in_front: src_att.placement_action.is_some_and(|a| a.in_front)
278 || dst_att.placement_action.is_some_and(|a| a.in_front),
279 }),
280 tick_action: operator
281 .blend_operations(
282 ctx,
283 src_att.tick_action.as_ref().map(|a| &a.operation),
284 dst_att.tick_action.as_ref().map(|a| &a.operation),
285 )
286 .map(|operation| block::TickAction {
287 operation,
288 schedule: src_att
292 .tick_action
293 .as_ref()
294 .map(|a| a.schedule)
295 .or_else(|| dst_att.tick_action.as_ref().map(|a| a.schedule))
296 .expect("unreachable: no schedule"),
297 }),
298 activation_action: operator.blend_operations(
299 ctx,
300 src_att.activation_action.as_ref(),
301 dst_att.activation_action.as_ref(),
302 ),
303 animation_hint: src_att.animation_hint | dst_att.animation_hint, };
305
306 let voxels = if let (true, Some(src_voxel), Some(dst_voxel)) = (
310 output_bounds == GridAab::ORIGIN_CUBE,
311 src_voxels.single_voxel(),
312 dst_voxels.single_voxel(),
313 ) {
314 block::Budget::decrement_voxels(&filter.budget, 1)?;
316 Evoxels::from_one(operator.blend_evoxel(src_voxel, dst_voxel))
319 } else if operator.air_src_leaves_dst_unchanged()
320 && output_bounds == dst_voxels.bounds()
321 && dst_scale == src_scale
322 {
323 block::Budget::decrement_voxels(&filter.budget, src_voxels.count())?;
327 let mut dst_voxels_mut = dst_voxels.as_vol_mut();
328 intersection_for_blend.interior_iter().for_each(|cube| {
329 let dst_voxel = &mut dst_voxels_mut[cube];
330 *dst_voxel = operator.blend_evoxel(src_voxels[cube], *dst_voxel);
331 });
332 dst_voxels
333 } else if operator.air_dst_leaves_src_unchanged()
334 && output_bounds == src_voxels.bounds()
335 && dst_scale == src_scale
336 {
337 block::Budget::decrement_voxels(&filter.budget, dst_voxels.count())?;
341 let mut src_voxels_mut = src_voxels.as_vol_mut();
342 intersection_for_blend.interior_iter().for_each(|cube| {
343 let src_voxel = &mut src_voxels_mut[cube];
344 *src_voxel = operator.blend_evoxel(*src_voxel, dst_voxels[cube]);
345 });
346 src_voxels
347 } else {
348 block::Budget::decrement_voxels(&filter.budget, output_bounds.volume().unwrap())?;
350 Evoxels::from_many(
351 effective_resolution,
352 Vol::from_fn(output_bounds, |cube| {
353 let p = cube.lower_bounds();
354 operator.blend_evoxel(
355 src_voxels.get(Cube::from(p / src_scale)).unwrap_or(Evoxel::AIR),
356 dst_voxels.get(Cube::from(p / dst_scale)).unwrap_or(Evoxel::AIR),
357 )
358 }),
359 )
360 };
361
362 Ok(MinEval::new(attributes, voxels))
363}
364
365fn bounds_excluding_air(voxels: &Evoxels, src_scale: i32) -> GridAab {
369 if voxels.single_voxel() == Some(Evoxel::AIR) {
370 GridAab::ORIGIN_EMPTY
371 } else {
372 voxels.bounds().multiply(src_scale)
373 }
374}
375
376impl From<Composite> for Modifier {
377 fn from(value: Composite) -> Self {
378 Modifier::Composite(value)
379 }
380}
381
382impl universe::VisitHandles for Composite {
383 fn visit_handles(&self, visitor: &mut dyn universe::HandleVisitor) {
384 let Self {
385 source,
386 operator: _,
387 reverse: _,
388 disassemblable: _,
389 } = self;
390 source.visit_handles(visitor);
391 }
392}
393
394#[doc = include_str!("../../save/serde-warning.md")]
402#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
403#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
404#[cfg_attr(feature = "save", derive(serde::Serialize, serde::Deserialize))]
405#[non_exhaustive]
406pub enum CompositeOperator {
407 Over,
410
411 In,
416
417 Out,
422
423 Atop,
427 }
433
434impl CompositeOperator {
435 fn blend_evoxel(self, src_ev: Evoxel, dst_ev: Evoxel) -> Evoxel {
437 use BlockCollision as Coll;
438 Evoxel {
439 color: {
440 let source = src_ev.color.clamp();
442 let destination = dst_ev.color.clamp();
443 let (rgb, a) = self.alpha_blend(
444 source.to_rgb(),
445 source.alpha(),
446 destination.to_rgb(),
447 destination.alpha(),
448 );
449 rgb.with_alpha(a)
450 },
451
452 emission: {
455 let (color_blend, alpha) = self.alpha_blend(
456 src_ev.emission,
457 src_ev.color.clamp().alpha(),
458 dst_ev.emission,
459 dst_ev.color.clamp().alpha(),
460 );
461 color_blend * alpha
464 },
465
466 selectable: self.blend_binary(src_ev.selectable, dst_ev.selectable),
467
468 collision: {
469 let src_is_something = !matches!(src_ev.collision, Coll::None);
470 let dst_is_something = !matches!(dst_ev.collision, Coll::None);
471 if self.blend_binary(src_is_something, dst_is_something) {
472 if src_is_something {
477 src_ev.collision
478 } else {
479 dst_ev.collision
480 }
481 } else {
482 Coll::None
483 }
484 },
485 }
486 }
487
488 fn alpha_blend(
493 self,
494 source: Rgb,
495 sa: ZeroOne<f32>,
496 destination: Rgb,
497 da: ZeroOne<f32>,
498 ) -> (Rgb, ZeroOne<f32>) {
499 match self {
500 Self::Over => {
501 let sa_complement = sa.complement();
504 let rgb = source * sa + destination * sa_complement;
505 (
506 rgb,
507 ZeroOne::<f32>::new_clamped(
509 sa.into_inner() + (sa_complement * da).into_inner(),
510 ),
511 )
512 }
513
514 Self::In => (source, sa * da),
515 Self::Out => (source, sa * da.complement()),
516
517 Self::Atop => {
518 let sa_complement = PositiveSign::<f32>::new_clamped(1. - sa.into_inner());
519 let rgb = source * sa + destination * sa_complement;
520
521 let out_alpha = da;
522 if out_alpha.is_zero() {
523 (Rgb::ZERO, out_alpha)
525 } else {
526 (rgb, out_alpha)
527 }
528 }
529 }
530 }
531
532 #[expect(clippy::needless_bitwise_bool)] fn blend_binary(self, source: bool, destination: bool) -> bool {
536 match self {
537 Self::Over => source | destination,
538 Self::In => source & destination,
539 Self::Out => source & !destination,
540 Self::Atop => destination,
541 }
542 }
543
544 fn blend_operations<'op>(
545 self,
546 ctx: &CompEvalCtx<'_>,
547 mut source_op: Option<&'op Operation>,
548 mut destination_op: Option<&'op Operation>,
549 ) -> Option<Operation> {
550 let Some(this_modifier_index) = ctx.this_modifier_index else {
551 return None;
554 };
555
556 if ctx.was_reversed {
558 mem::swap(&mut source_op, &mut destination_op);
559 }
560
561 fn require_become(op: Option<&Operation>) -> Option<&Block> {
564 match op {
565 Some(Operation::Become(block)) => Some(block),
566 _ => None,
567 }
568 }
569 let source_becoming = require_become(source_op);
570 let destination_becoming = require_become(destination_op);
571
572 if source_becoming.is_none() && destination_becoming.is_none() {
573 return None;
575 }
576
577 let mut new_block = match destination_becoming {
580 Some(block) => block.clone(),
582 None => {
584 let mut new_block = Block::from_primitive(ctx.block.primitive().clone());
585 new_block
586 .modifiers_mut()
587 .extend(ctx.block.modifiers()[..this_modifier_index].iter().cloned());
588 new_block
589 }
590 };
591 new_block.modifiers_mut().push(Modifier::Composite(Composite {
592 source: match source_becoming {
593 Some(source_becoming) => source_becoming.clone(),
594 None => {
595 let Modifier::Composite(Composite { source, .. }) =
598 &ctx.block.modifiers()[this_modifier_index]
599 else {
600 panic!("modifier mismatch");
601 };
602 source.clone()
603 }
604 },
605 operator: self,
606 reverse: ctx.was_reversed,
607 disassemblable: ctx.disassemblable,
608 }));
609
610 Some(Operation::Become(new_block))
611 }
612
613 fn bounds(self, source: GridAab, destination: GridAab) -> GridAab {
615 match self {
616 Self::Over => source.union_cubes(destination),
617 Self::In => source.intersection_box(destination).unwrap_or(GridAab::ORIGIN_EMPTY),
620 Self::Out => source,
621 Self::Atop => destination,
622 }
623 }
624
625 fn air_src_leaves_dst_unchanged(self) -> bool {
627 match self {
628 CompositeOperator::Over => true,
629 CompositeOperator::In => true,
630 CompositeOperator::Out => false,
631 CompositeOperator::Atop => true,
632 }
633 }
634
635 fn air_dst_leaves_src_unchanged(self) -> bool {
637 match self {
638 CompositeOperator::Over => true,
639 CompositeOperator::In => false,
640 CompositeOperator::Out => true,
641 CompositeOperator::Atop => false,
642 }
643 }
644
645 #[expect(clippy::unused_self)]
648 fn rotationally_symmetric(self) -> bool {
649 true
650 }
651}
652
653pub(in crate::block) fn render_inventory(
655 mut input: MinEval,
656 inventory: &crate::inv::Inventory,
657 filter: &block::EvalFilter<'_>,
658) -> Result<MinEval, block::InEvalError> {
659 if filter.skip_eval {
660 return Ok(input);
661 }
662
663 let original_attributes = input.attributes().clone();
664
665 let config = input.attributes().inventory.clone();
667
668 for (slot_index, icon_position) in config.icon_positions(inventory.size()) {
669 let Some(placed_icon_bounds) = GridAab::checked_from_lower_size(
670 icon_position,
671 GridSize::splat((config.icon_resolution / config.icon_scale).unwrap_or(R1).into()),
672 )
673 .ok()
674 .and_then(|b| b.intersection_cubes(GridAab::for_block(config.icon_resolution))) else {
675 continue;
677 };
678
679 let Some(icon): Option<&Block> =
681 inventory.get(slot_index).and_then(|slot| slot.icon_only_if_intrinsic())
682 else {
683 continue;
685 };
686
687 let mut icon_evaluated = {
688 let _recursion_scope = block::Budget::recurse(&filter.budget)?;
689 icon.evaluate_impl(filter)?.finish(icon.clone(), filter.budget.get().to_cost())
691 };
692
693 let resample_scale = GridCoordinate::from(icon_evaluated.voxels.resolution())
696 * GridCoordinate::from(config.icon_scale)
697 / GridCoordinate::from(config.icon_resolution);
698 let resample_point_offset = GridVector::splat(resample_scale / 2);
699 icon_evaluated.voxels = Evoxels::from_many(
700 config.icon_resolution,
701 Vol::from_fn(placed_icon_bounds, |render_cube| {
702 let translated: Cube = render_cube - placed_icon_bounds.lower_bounds().to_vector();
704 let translated_and_scaled: Cube =
706 Cube::from(GridPoint::from(translated) * resample_scale);
707 icon_evaluated
708 .voxels
709 .get(translated_and_scaled + resample_point_offset)
710 .unwrap_or(Evoxel::AIR)
711 }),
712 );
713
714 input = evaluate_composition(
715 icon_evaluated.into(),
716 input,
717 CompositeOperator::Over,
718 filter,
719 &CompEvalCtx {
720 block: const { &AIR }, this_modifier_index: None, was_reversed: false,
723 disassemblable: false,
724 },
725 )?;
726 }
727
728 input.set_attributes(block::BlockAttributes {
732 animation_hint: input.attributes().animation_hint,
733 ..original_attributes
734 });
735
736 Ok(input)
737}
738
739#[cfg(test)]
740mod tests {
741 use super::*;
742 use crate::block::{EvKey, EvaluatedBlock, Resolution::*};
743 use crate::content::{make_slab, make_some_blocks};
744 use crate::math::{Rgba, zo32};
745 use crate::space::Space;
746 use crate::time;
747 use crate::universe::Universe;
748 use BlockCollision::{Hard, None as CNone};
749 use CompositeOperator::{Atop, In, Out, Over};
750 use arcstr::literal;
751 use pretty_assertions::assert_eq;
752
753 #[track_caller]
757 fn assert_blend(src: Evoxel, operator: CompositeOperator, dst: Evoxel, outcome: Evoxel) {
758 assert_eq!(
761 operator.blend_evoxel(src, dst),
762 outcome,
763 "\nexpecting {operator:?}.blend(\n {src:?},\n {dst:?}\n) == {outcome:?}"
764 );
765 }
766
767 fn evcolor(color: Rgba) -> Evoxel {
768 Evoxel {
769 color,
770 emission: Rgb::ZERO,
771 selectable: true,
772 collision: Hard,
773 }
774 }
775
776 fn evemit(emission: Rgb, alpha: f32) -> Evoxel {
779 Evoxel {
780 color: Rgb::ZERO.with_alpha(zo32(alpha)),
783 emission,
784 selectable: true,
785 collision: Hard,
786 }
787 }
788
789 fn evcoll(collision: BlockCollision) -> Evoxel {
790 Evoxel {
791 color: Rgba::WHITE, emission: Rgb::ZERO,
793 selectable: false, collision,
795 }
796 }
797
798 #[track_caller]
799 fn eval_compose(
800 universe: &Universe,
801 src: &Block,
802 operator: CompositeOperator,
803 dst: &Block,
804 ) -> EvaluatedBlock {
805 dst.clone()
806 .with_modifier(Composite::new(src.clone(), operator))
807 .evaluate(universe.read_ticket())
808 .expect("failed to evaluate in eval_compose()")
809 }
810
811 #[test]
814 fn bounding_volume_combination() {
815 let universe = &mut Universe::new();
816 let bounds1 = GridAab::from_lower_size([0, 0, 0], [2, 1, 1]);
818 let bounds2 = GridAab::from_lower_size([1, 0, 0], [2, 1, 1]);
819 let space1 = universe.insert_anonymous(Space::builder(bounds1).build());
820 let space2 = universe.insert_anonymous(Space::builder(bounds2).build());
821 let block1 = Block::builder().voxels_handle(R4, space1).build();
822 let block2 = Block::builder().voxels_handle(R4, space2).build();
823
824 let union = GridAab::from_lower_size([0, 0, 0], [3, 1, 1]);
825 let intersection = GridAab::from_lower_size([1, 0, 0], [1, 1, 1]);
826
827 assert_eq!(
828 eval_compose(universe, &block1, Over, &block2).voxels_bounds(),
829 union,
830 "Over"
831 );
832 assert_eq!(
833 eval_compose(universe, &block1, In, &block2).voxels_bounds(),
834 intersection,
835 "In"
836 );
837 assert_eq!(
838 eval_compose(universe, &block1, Atop, &block2).voxels_bounds(),
839 bounds2,
840 "Atop"
841 );
842 }
843
844 #[test]
845 fn bounding_volume_when_one_is_air() {
846 let universe = &mut Universe::new();
847 let slab = make_slab(universe, 1, R2);
848 let slab_bounds = slab.evaluate(universe.read_ticket()).unwrap().voxels_bounds();
849
850 assert_eq!(
851 eval_compose(universe, &slab, Over, &AIR).voxels_bounds(),
852 slab_bounds,
853 "Over",
854 );
855 assert_eq!(
856 eval_compose(universe, &slab, In, &AIR).voxels_bounds(),
857 GridAab::ORIGIN_EMPTY,
858 "In",
859 );
860 assert_eq!(
861 eval_compose(universe, &slab, Atop, &AIR).voxels_bounds(),
862 GridAab::ORIGIN_EMPTY,
863 "Atop AIR",
864 );
865 assert_eq!(
866 eval_compose(universe, &AIR, Atop, &slab).voxels_bounds(),
867 slab_bounds,
868 "AIR Atop",
869 );
870 }
871
872 mod voxel {
874 use super::*;
875
876 #[test]
877 fn over_silly_floats() {
878 Over.blend_evoxel(
881 evcolor(Rgba::new(2e25, 2e25, 2e25, 1.0)),
882 evcolor(Rgba::new(2e25, 2e25, 2e25, 1.0)),
883 );
884 }
885
886 #[test]
887 fn over_emission() {
888 let red_1 = evemit(Rgb::new(1., 0., 0.), 1.0);
889 let green_0 = evemit(Rgb::new(0., 1., 0.), 0.0);
890 let green_05 = evemit(Rgb::new(0., 1., 0.), 0.5);
891 let none_1 = evemit(Rgb::ZERO, 1.0);
892 let none_0 = evemit(Rgb::ZERO, 0.0);
893
894 assert_blend(red_1, Over, none_1, red_1);
896 assert_blend(none_1, Over, red_1, none_1);
897 assert_blend(none_1, Over, none_1, none_1);
898 assert_blend(red_1, Over, red_1, red_1);
899 assert_blend(red_1, Over, none_0, red_1);
900 assert_blend(none_0, Over, red_1, red_1);
901
902 assert_blend(red_1, Over, green_05, red_1);
904 assert_blend(green_05, Over, red_1, evemit(Rgb::new(0.5, 0.5, 0.0), 1.0));
905 assert_blend(
906 green_05,
907 Over,
908 green_05,
909 evemit(Rgb::new(0.0, 0.75, 0.0), 0.75),
910 );
911 assert_blend(green_0, Over, none_1, none_1);
915 assert_blend(none_1, Over, green_0, none_1);
917 }
919
920 #[test]
921 fn over_collision() {
922 assert_blend(evcoll(Hard), Over, evcoll(Hard), evcoll(Hard));
923 assert_blend(evcoll(CNone), Over, evcoll(CNone), evcoll(CNone));
924 assert_blend(evcoll(Hard), Over, evcoll(CNone), evcoll(Hard));
925 assert_blend(evcoll(CNone), Over, evcoll(Hard), evcoll(Hard));
926 }
927
928 #[test]
929 fn in_emission() {
930 let red_1 = evemit(Rgb::new(1., 0., 0.), 1.0);
931 let green_1 = evemit(Rgb::new(0., 1., 0.), 1.0);
932 let green_0 = evemit(Rgb::new(0., 1., 0.), 0.0);
933 let green_05 = evemit(Rgb::new(0., 1., 0.), 0.5);
934 let none_1 = evemit(Rgb::ZERO, 1.0);
935 let none_0 = evemit(Rgb::ZERO, 0.0);
936
937 assert_blend(red_1, In, none_1, red_1);
939 assert_blend(red_1, In, red_1, red_1);
940 assert_blend(red_1, In, green_1, red_1);
941 assert_blend(red_1, In, none_0, none_0);
942 assert_blend(none_1, In, red_1, none_1);
943 assert_blend(none_0, In, red_1, none_0);
944 assert_blend(none_1, In, none_1, none_1);
945 assert_blend(none_0, In, none_1, none_0);
946
947 assert_blend(red_1, In, green_05, evemit(Rgb::new(0.5, 0.0, 0.0), 0.5));
949 assert_blend(green_05, In, red_1, evemit(Rgb::new(0.0, 0.5, 0.0), 0.5));
950 assert_blend(
951 green_05,
952 In,
953 green_05,
954 evemit(Rgb::new(0.0, 0.25, 0.0), 0.25),
955 );
956 assert_blend(green_05, In, none_0, none_0); assert_blend(green_0, In, none_1, none_0);
960 assert_blend(green_0, In, none_0, none_0);
961 assert_blend(none_1, In, green_0, none_0);
962 assert_blend(green_0, In, green_0, none_0); }
964
965 #[test]
966 fn in_collision() {
967 assert_blend(evcoll(Hard), In, evcoll(Hard), evcoll(Hard));
968 assert_blend(evcoll(CNone), In, evcoll(CNone), evcoll(CNone));
969 assert_blend(evcoll(Hard), In, evcoll(CNone), evcoll(CNone));
970 assert_blend(evcoll(CNone), In, evcoll(Hard), evcoll(CNone));
971 }
972
973 #[test]
974 fn atop_color() {
975 let opaque1 = evcolor(Rgba::new(1.0, 0.0, 0.0, 1.0));
976 let opaque2 = evcolor(Rgba::new(0.0, 1.0, 0.0, 1.0));
977 let half_red = evcolor(Rgba::new(1.0, 0.0, 0.0, 0.5));
978 let clear = evcolor(Rgba::TRANSPARENT);
979
980 assert_blend(opaque1, Atop, opaque2, opaque1);
981 assert_blend(
982 half_red,
983 Atop,
984 opaque2,
985 evcolor(Rgba::new(0.5, 0.5, 0.0, 1.0)),
986 );
987 assert_blend(opaque1, Atop, clear, clear);
988 assert_blend(clear, Atop, opaque2, opaque2);
989 assert_blend(clear, Atop, clear, clear);
990 }
991
992 #[test]
993 fn atop_emission() {
994 let red_1 = evemit(Rgb::new(1., 0., 0.), 1.0);
995 let green_1 = evemit(Rgb::new(0., 1., 0.), 1.0);
996 let green_0 = evemit(Rgb::new(0., 1., 0.), 0.0);
997 let green_05 = evemit(Rgb::new(0., 1., 0.), 0.5);
998 let none_1 = evemit(Rgb::ZERO, 1.0);
999 let none_0 = evemit(Rgb::ZERO, 0.0);
1000
1001 assert_blend(red_1, Atop, none_1, red_1);
1003 assert_blend(red_1, Atop, red_1, red_1);
1004 assert_blend(red_1, Atop, green_1, red_1);
1005 assert_blend(red_1, Atop, none_0, none_0);
1006 assert_blend(none_1, Atop, red_1, none_1);
1007 assert_blend(none_0, Atop, red_1, red_1);
1008 assert_blend(none_1, Atop, none_1, none_1);
1009 assert_blend(none_0, Atop, none_1, none_1);
1010
1011 assert_blend(red_1, Atop, green_05, evemit(Rgb::new(0.5, 0.0, 0.0), 0.5));
1013 assert_blend(green_05, Atop, red_1, evemit(Rgb::new(0.5, 0.5, 0.0), 1.0));
1014 assert_blend(
1015 green_05,
1016 Atop,
1017 green_05,
1018 evemit(Rgb::new(0.0, 0.5, 0.0), 0.5),
1019 );
1020 assert_blend(green_05, Atop, none_0, none_0);
1021
1022 assert_blend(green_0, Atop, none_1, none_1);
1024 assert_blend(green_0, Atop, none_0, none_0);
1025 assert_blend(none_1, Atop, green_0, none_0);
1026 assert_blend(green_0, Atop, green_0, none_0); }
1028
1029 #[test]
1030 fn blend_atop_collision() {
1031 assert_blend(evcoll(Hard), Atop, evcoll(Hard), evcoll(Hard));
1032 assert_blend(evcoll(CNone), Atop, evcoll(CNone), evcoll(CNone));
1033 assert_blend(evcoll(Hard), Atop, evcoll(CNone), evcoll(CNone));
1034 assert_blend(evcoll(CNone), Atop, evcoll(Hard), evcoll(Hard));
1035 }
1036 }
1037
1038 mod attributes {
1040 use super::{assert_eq, *};
1041
1042 #[test]
1043 fn display_name() {
1044 let u = Universe::new();
1045 let no_name = &Block::builder().color(Rgba::WHITE).build();
1046 let has_name_1 =
1047 &Block::builder().color(Rgba::WHITE).display_name(literal!("has_name_1")).build();
1048 let has_name_2 =
1049 &Block::builder().color(Rgba::WHITE).display_name(literal!("has_name_2")).build();
1050
1051 assert_eq!(
1053 eval_compose(&u, has_name_1, Over, no_name).attributes().display_name,
1054 literal!("has_name_1")
1055 );
1056
1057 assert_eq!(
1059 eval_compose(&u, no_name, Over, has_name_1).attributes().display_name,
1060 literal!("has_name_1")
1061 );
1062
1063 assert_eq!(
1065 eval_compose(&u, has_name_1, Over, has_name_2).attributes().display_name,
1066 literal!("has_name_2")
1068 );
1069
1070 assert_eq!(
1077 has_name_2
1078 .clone()
1079 .with_modifier(Composite::new(has_name_1.clone(), Over).reversed())
1080 .evaluate(u.read_ticket())
1081 .unwrap()
1082 .attributes()
1083 .display_name,
1084 literal!("has_name_1")
1085 );
1086 }
1087
1088 #[test]
1089 fn selectable_if_either_is_selectable() {
1090 let u = Universe::new();
1091 let is_s = &Block::builder().color(Rgba::WHITE).selectable(true).build();
1094 let not_s = &Block::builder().color(Rgba::WHITE).selectable(false).build();
1095
1096 assert!(eval_compose(&u, is_s, Over, is_s).attributes().selectable);
1097 assert!(eval_compose(&u, is_s, Over, not_s).attributes().selectable);
1098 assert!(eval_compose(&u, not_s, Over, is_s).attributes().selectable);
1099 assert!(!eval_compose(&u, not_s, Over, not_s).attributes().selectable);
1100
1101 assert!(eval_compose(&u, is_s, In, is_s).attributes().selectable);
1102 assert!(eval_compose(&u, is_s, In, not_s).attributes().selectable);
1103 assert!(eval_compose(&u, not_s, In, is_s).attributes().selectable);
1104 assert!(!eval_compose(&u, not_s, In, not_s).attributes().selectable);
1105 }
1106
1107 #[test]
1108 fn activation_action_is_composed() {
1109 let universe = Universe::new();
1110 let [result1, result2] = make_some_blocks();
1111 let b1 = &Block::builder()
1112 .color(Rgba::WHITE)
1113 .activation_action(Operation::Become(result1.clone()))
1114 .build();
1115 let b2 = &Block::builder()
1116 .color(Rgba::WHITE)
1117 .activation_action(Operation::Become(result2.clone()))
1118 .build();
1119
1120 assert_eq!(
1121 eval_compose(&universe, b1, Over, b2).attributes().activation_action,
1122 Some(Operation::Become(
1123 result2.with_modifier(Composite::new(result1, Over))
1124 ))
1125 );
1126
1127 }
1129
1130 #[test]
1131 fn tick_action_is_composed() {
1132 let universe = Universe::new();
1133 let [result1, result2] = make_some_blocks();
1134 let b1 = &Block::builder()
1135 .color(Rgba::WHITE)
1136 .tick_action(block::TickAction {
1137 schedule: time::Schedule::EVERY_TICK,
1138 operation: Operation::Become(result1.clone()),
1139 })
1140 .build();
1141 let b2 = &Block::builder()
1142 .color(Rgba::WHITE)
1143 .tick_action(block::TickAction {
1144 schedule: time::Schedule::EVERY_TICK,
1145 operation: Operation::Become(result2.clone()),
1146 })
1147 .build();
1148
1149 assert_eq!(
1150 eval_compose(&universe, b1, Over, b2).attributes().tick_action,
1151 Some(block::TickAction {
1152 schedule: time::Schedule::EVERY_TICK,
1153 operation: Operation::Become(
1154 result2.with_modifier(Composite::new(result1, Over))
1155 )
1156 })
1157 );
1158
1159 }
1162 }
1163
1164 mod ops {
1166 use super::{assert_eq, *};
1167
1168 #[test]
1169 fn compose_or_replace_source_is_air() {
1170 let [block] = make_some_blocks();
1171 assert_eq!(
1172 Composite::new(AIR, Over).compose_or_replace(block.clone()),
1173 block
1174 );
1175 }
1176
1177 #[test]
1178 fn compose_or_replace_destination_is_air() {
1179 let [block] = make_some_blocks();
1180 assert_eq!(
1181 Composite::new(block.clone(), Over).compose_or_replace(AIR),
1182 block
1183 );
1184 }
1185
1186 #[test]
1187 fn unspecialize_no() {
1188 let [b1, b2] = make_some_blocks();
1189 let composed = b1.with_modifier(Composite::new(b2, Over));
1190 assert_eq!(composed.unspecialize(), vec![composed]);
1191 }
1192
1193 #[test]
1194 fn unspecialize_yes() {
1195 let [b1, b2] = make_some_blocks();
1196 let composed =
1197 b1.clone().with_modifier(Composite::new(b2.clone(), Over).with_disassemblable());
1198 assert_eq!(composed.unspecialize(), vec![b2, b1]);
1199 }
1200 }
1201
1202 mod optimization {
1205 use super::{assert_eq, *};
1206
1207 #[test]
1211 fn composite_with_air_is_short_circuit_noop() {
1212 let universe = &mut Universe::new();
1213
1214 let base_block = Block::builder()
1219 .voxels_fn(R4, |_| block::from_color!(1.0, 0.0, 0.0, 1.0))
1220 .unwrap()
1221 .build_into(universe);
1222 let base_block = Block::from(
1223 universe.insert_anonymous(block::BlockDef::new(universe.read_ticket(), base_block)),
1224 );
1225 let base_block_voxel_count = 64;
1226 let base_ev = base_block.evaluate(universe.read_ticket()).unwrap();
1227 let base_key = EvKey::new(&base_ev);
1228 assert_eq!(base_key, EvKey::new(&base_ev), "test assumption failed");
1229 assert_eq!(
1230 base_ev.cost,
1231 block::Cost {
1232 components: 1,
1233 voxels: 0, recursion: 0
1235 },
1236 "test assumption failed"
1237 );
1238
1239 let air_src_block = base_block.clone().with_modifier(Composite::new(AIR, Over));
1240 let air_src_ev = air_src_block.evaluate(universe.read_ticket()).unwrap();
1241 assert_eq!(base_key, EvKey::new(&air_src_ev), "src");
1242 assert_eq!(
1243 air_src_ev.cost,
1244 block::Cost {
1245 components: 3,
1246 voxels: 0,
1247 recursion: 1
1248 }
1249 );
1250
1251 let air_dst_block = base_block.with_modifier(Composite::new(AIR, Over).reversed());
1256 let air_dst_ev = air_dst_block.evaluate(universe.read_ticket()).unwrap();
1257 assert_ne!(base_key, EvKey::new(&air_dst_ev), "dst");
1258 assert_eq!(
1259 air_dst_ev.cost,
1260 block::Cost {
1261 components: 3,
1262 voxels: base_block_voxel_count,
1263 recursion: 1
1264 }
1265 );
1266 }
1267
1268 #[test]
1274 fn unaltered_volume_is_untouched() {
1275 let universe = &mut Universe::new();
1276
1277 let base_block = Block::builder()
1279 .voxels_fn(R4, |_| block::from_color!(1.0, 0.0, 0.0, 1.0))
1280 .unwrap()
1281 .build_into(universe);
1282 let smaller_block = Block::builder()
1284 .voxels_fn(R4, |cube| {
1285 if cube.x == 0 {
1286 block::from_color!(0.0, 1.0, 0.0, 1.0)
1287 } else {
1288 AIR
1289 }
1290 })
1291 .unwrap()
1292 .build_into(universe);
1293 assert_eq!(
1294 smaller_block.evaluate(universe.read_ticket()).unwrap().voxels().bounds(),
1295 GridAab::from_lower_size([0, 0, 0], [1, 4, 4]),
1296 "test assumption failed"
1297 );
1298 let expected_lower_cost = block::Cost {
1299 components: 3,
1300 voxels: 64 + 16 + 16,
1303 recursion: 1,
1304 };
1305
1306 {
1308 let composition_evaluation = base_block
1309 .clone()
1310 .with_modifier(Composite::new(smaller_block.clone(), Over))
1311 .evaluate(universe.read_ticket())
1312 .unwrap();
1313 assert_eq!(
1314 composition_evaluation.cost, expected_lower_cost,
1315 "small Over large"
1316 );
1317 }
1318
1319 {
1321 let composition_evaluation = smaller_block
1322 .clone()
1323 .with_modifier(Composite::new(base_block, Out))
1324 .evaluate(universe.read_ticket())
1325 .unwrap();
1326 assert_eq!(
1327 composition_evaluation.cost, expected_lower_cost,
1328 "large Out small"
1329 );
1330 }
1331 }
1332 }
1333}