1#![allow(
2 clippy::module_name_repetitions,
3 reason = "false positive; TODO: remove after Rust 1.84 is released"
4)]
5
6use alloc::vec;
7use core::mem;
8
9use crate::block::{
10 self, Block, BlockCollision, Evoxel, Evoxels, MinEval, Modifier, Resolution::R1, AIR,
11};
12use crate::math::{
13 Cube, GridAab, GridCoordinate, GridRotation, GridSize, PositiveSign, Rgb, Vol, ZeroOne,
14};
15use crate::op::Operation;
16use crate::universe;
17
18#[derive(Clone, Debug, Eq, Hash, PartialEq)]
25#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
26#[non_exhaustive]
27pub struct Composite {
28 pub source: Block,
31
32 pub operator: CompositeOperator,
34
35 pub reverse: bool,
37
38 pub disassemblable: bool,
40 }
44
45impl Composite {
46 pub fn new(source: Block, operator: CompositeOperator) -> Self {
49 Self {
50 source,
51 operator,
52 reverse: false,
53 disassemblable: false,
54 }
55 }
56
57 #[must_use]
59 pub fn reversed(mut self) -> Self {
60 self.reverse = !self.reverse;
61 self
62 }
63
64 #[must_use]
69 pub fn with_disassemblable(mut self) -> Self {
70 self.disassemblable = true;
71 self
72 }
73
74 pub fn compose_or_replace(mut self, mut destination: Block) -> Block {
93 let dest_rot = if let Some(&Modifier::Rotate(dest_rot)) = destination.modifiers().last() {
95 destination.modifiers_mut().pop();
96 dest_rot
97 } else {
98 GridRotation::IDENTITY
99 };
100
101 if destination == AIR {
102 self.source
107 } else if self.source == AIR {
108 destination.rotate(dest_rot)
110 } else {
111 self.source = self.source.rotate(dest_rot.inverse());
112 destination.with_modifier(self).rotate(dest_rot)
113 }
114 }
115
116 pub fn stack(destination: Block, parts: impl IntoIterator<Item = Composite>) -> Block {
118 parts
119 .into_iter()
120 .fold(destination, |block, part| part.compose_or_replace(block))
121 }
122
123 pub(super) fn evaluate(
125 &self,
126 block: &Block,
127 this_modifier_index: usize,
128 mut dst_evaluated: MinEval,
129 filter: &block::EvalFilter,
130 ) -> Result<MinEval, block::InEvalError> {
131 let Composite {
132 ref source,
133 operator,
134 reverse,
135 disassemblable,
136 } = *self;
137
138 let mut src_evaluated = {
141 let _recursion_scope = block::Budget::recurse(&filter.budget)?;
142 source.evaluate_impl(filter)?
143 };
144
145 if filter.skip_eval {
146 return Ok(dst_evaluated);
147 }
148
149 if reverse {
151 mem::swap(&mut src_evaluated, &mut dst_evaluated);
152 }
153
154 evaluate_composition(
155 src_evaluated,
156 dst_evaluated,
157 operator,
158 filter,
159 &CompEvalCtx {
160 block,
161 this_modifier_index: Some(this_modifier_index),
162 was_reversed: reverse,
163 disassemblable,
164 },
165 )
166 }
167
168 pub(super) fn unspecialize(&self, entire_block: &Block) -> block::ModifierUnspecialize {
170 if self.disassemblable {
171 let mut destination = entire_block.clone();
172 destination
173 .modifiers_mut()
174 .pop()
175 .expect("Missing Composite modifier");
176 block::ModifierUnspecialize::Replace(vec![self.source.clone(), destination])
177 } else {
178 block::ModifierUnspecialize::Keep
179 }
180 }
181
182 pub(crate) fn rotationally_symmetric(&self) -> bool {
183 let Self {
184 source,
185 operator,
186 reverse: _,
187 disassemblable: _,
188 } = self;
189 source.rotationally_symmetric() && operator.rotationally_symmetric()
190 }
191
192 #[must_use]
193 pub(crate) fn rotate(self, rotation: GridRotation) -> Self {
194 let Self {
195 source,
196 operator,
197 reverse,
198 disassemblable,
199 } = self;
200 Self {
201 source: source.rotate(rotation),
202 operator,
203 reverse,
204 disassemblable,
205 }
206 }
207}
208
209struct CompEvalCtx<'a> {
211 block: &'a Block,
212 this_modifier_index: Option<usize>,
214 was_reversed: bool,
215 disassemblable: bool,
216}
217
218fn evaluate_composition(
224 src_evaluated: MinEval,
225 dst_evaluated: MinEval,
226 operator: CompositeOperator,
227 filter: &block::EvalFilter,
228 ctx: &CompEvalCtx<'_>,
229) -> Result<MinEval, block::InEvalError> {
230 if operator == CompositeOperator::Over && src_evaluated == block::AIR_EVALUATED_MIN {
235 return Ok(dst_evaluated);
236 }
237
238 let (dst_att, dst_voxels) = dst_evaluated.into_parts();
240 let (src_att, src_voxels) = src_evaluated.into_parts();
241
242 let src_resolution = src_voxels.resolution();
243 let dst_resolution = dst_voxels.resolution();
244 let effective_resolution = src_resolution.max(dst_resolution);
245 let src_scale =
246 GridCoordinate::from(effective_resolution) / GridCoordinate::from(src_resolution);
247 let dst_scale =
248 GridCoordinate::from(effective_resolution) / GridCoordinate::from(dst_resolution);
249
250 let src_bounds_scaled = bounds_excluding_air(&src_voxels, src_scale);
251 let dst_bounds_scaled = bounds_excluding_air(&dst_voxels, dst_scale);
252
253 let output_bounds = operator.bounds(src_bounds_scaled, dst_bounds_scaled);
254
255 block::Budget::decrement_voxels(&filter.budget, output_bounds.volume().unwrap())?;
256
257 let attributes = block::BlockAttributes {
258 display_name: dst_att.display_name, selectable: src_att.selectable | dst_att.selectable,
260 inventory: src_att.inventory.concatenate(dst_att.inventory),
261 rotation_rule: dst_att.rotation_rule, placement_action: operator
263 .blend_operations(
264 ctx,
265 src_att.placement_action.as_ref().map(|a| &a.operation),
266 dst_att.placement_action.as_ref().map(|a| &a.operation),
267 )
268 .map(|operation| block::PlacementAction {
269 operation,
270 in_front: src_att.placement_action.is_some_and(|a| a.in_front)
272 || dst_att.placement_action.is_some_and(|a| a.in_front),
273 }),
274 tick_action: operator
275 .blend_operations(
276 ctx,
277 src_att.tick_action.as_ref().map(|a| &a.operation),
278 dst_att.tick_action.as_ref().map(|a| &a.operation),
279 )
280 .map(|operation| block::TickAction {
281 operation,
282 schedule: src_att
286 .tick_action
287 .as_ref()
288 .map(|a| a.schedule)
289 .or_else(|| dst_att.tick_action.as_ref().map(|a| a.schedule))
290 .expect("unreachable: no schedule"),
291 }),
292 activation_action: operator.blend_operations(
293 ctx,
294 src_att.activation_action.as_ref(),
295 dst_att.activation_action.as_ref(),
296 ),
297 animation_hint: src_att.animation_hint | dst_att.animation_hint, };
299
300 let voxels = if let (true, Some(src_voxel), Some(dst_voxel)) = (
301 output_bounds == GridAab::ORIGIN_CUBE,
302 src_voxels.single_voxel(),
303 dst_voxels.single_voxel(),
304 ) {
305 Evoxels::from_one(operator.blend_evoxel(src_voxel, dst_voxel))
309 } else {
310 Evoxels::from_many(
311 effective_resolution,
312 Vol::from_fn(output_bounds, |cube| {
313 let p = cube.lower_bounds();
314 operator.blend_evoxel(
315 src_voxels
316 .get(Cube::from(p / src_scale))
317 .unwrap_or(Evoxel::AIR),
318 dst_voxels
319 .get(Cube::from(p / dst_scale))
320 .unwrap_or(Evoxel::AIR),
321 )
322 }),
323 )
324 };
325
326 Ok(MinEval::new(attributes, voxels))
327}
328
329fn bounds_excluding_air(voxels: &Evoxels, src_scale: i32) -> GridAab {
333 if voxels.single_voxel() == Some(Evoxel::AIR) {
334 GridAab::ORIGIN_EMPTY
335 } else {
336 voxels.bounds().multiply(src_scale)
337 }
338}
339
340impl From<Composite> for Modifier {
341 fn from(value: Composite) -> Self {
342 Modifier::Composite(value)
343 }
344}
345
346impl universe::VisitHandles for Composite {
347 fn visit_handles(&self, visitor: &mut dyn universe::HandleVisitor) {
348 let Self {
349 source,
350 operator: _,
351 reverse: _,
352 disassemblable: _,
353 } = self;
354 source.visit_handles(visitor);
355 }
356}
357
358#[doc = include_str!("../../save/serde-warning.md")]
366#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
367#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
368#[cfg_attr(feature = "save", derive(serde::Serialize, serde::Deserialize))]
369#[non_exhaustive]
370pub enum CompositeOperator {
371 Over,
374
375 In,
380
381 Out,
386
387 Atop,
391 }
397
398impl CompositeOperator {
399 fn blend_evoxel(self, src_ev: Evoxel, dst_ev: Evoxel) -> Evoxel {
401 use BlockCollision as Coll;
402 Evoxel {
403 color: {
404 let source = src_ev.color.clamp();
406 let destination = dst_ev.color.clamp();
407 let (rgb, a) = self.alpha_blend(
408 source.to_rgb(),
409 source.alpha(),
410 destination.to_rgb(),
411 destination.alpha(),
412 );
413 rgb.with_alpha(a)
414 },
415
416 emission: {
419 let (color_blend, alpha) = self.alpha_blend(
420 src_ev.emission,
421 src_ev.color.clamp().alpha(),
422 dst_ev.emission,
423 dst_ev.color.clamp().alpha(),
424 );
425 color_blend * alpha
428 },
429
430 selectable: self.blend_binary(src_ev.selectable, dst_ev.selectable),
431
432 collision: {
433 let src_is_something = !matches!(src_ev.collision, Coll::None);
434 let dst_is_something = !matches!(dst_ev.collision, Coll::None);
435 if self.blend_binary(src_is_something, dst_is_something) {
436 if src_is_something {
441 src_ev.collision
442 } else {
443 dst_ev.collision
444 }
445 } else {
446 Coll::None
447 }
448 },
449 }
450 }
451
452 fn alpha_blend(
457 self,
458 source: Rgb,
459 sa: ZeroOne<f32>,
460 destination: Rgb,
461 da: ZeroOne<f32>,
462 ) -> (Rgb, ZeroOne<f32>) {
463 match self {
464 Self::Over => {
465 let sa_complement = sa.complement();
468 let rgb = source * sa + destination * sa_complement;
469 (
470 rgb,
471 ZeroOne::<f32>::new_clamped(
473 sa.into_inner() + (sa_complement * da).into_inner(),
474 ),
475 )
476 }
477
478 Self::In => (source, sa * da),
479 Self::Out => (source, sa * da.complement()),
480
481 Self::Atop => {
482 let sa_complement = PositiveSign::<f32>::new_clamped(1. - sa.into_inner());
483 let rgb = source * sa + destination * sa_complement;
484
485 let out_alpha = da;
486 if out_alpha.is_zero() {
487 (Rgb::ZERO, out_alpha)
489 } else {
490 (rgb, out_alpha)
491 }
492 }
493 }
494 }
495
496 #[expect(clippy::needless_bitwise_bool)] fn blend_binary(self, source: bool, destination: bool) -> bool {
500 match self {
501 Self::Over => source | destination,
502 Self::In => source & destination,
503 Self::Out => source & !destination,
504 Self::Atop => destination,
505 }
506 }
507
508 fn blend_operations<'op>(
509 self,
510 ctx: &CompEvalCtx<'_>,
511 mut source: Option<&'op Operation>,
512 mut destination: Option<&'op Operation>,
513 ) -> Option<Operation> {
514 let Some(this_modifier_index) = ctx.this_modifier_index else {
515 return None;
518 };
519
520 if ctx.was_reversed {
522 mem::swap(&mut source, &mut destination);
523 }
524
525 fn require_become(op: Option<&Operation>) -> Option<&Block> {
528 match op {
529 Some(Operation::Become(block)) => Some(block),
530 _ => None,
531 }
532 }
533 let source = require_become(source);
534 let destination = require_become(destination);
535
536 if source.is_none() && destination.is_none() {
537 return None;
539 }
540
541 let mut new_block = match destination {
544 Some(block) => block.clone(),
546 None => {
548 let mut new_block = Block::from_primitive(ctx.block.primitive().clone());
549 new_block
550 .modifiers_mut()
551 .extend(ctx.block.modifiers()[..this_modifier_index].iter().cloned());
552 new_block
553 }
554 };
555 new_block
556 .modifiers_mut()
557 .push(Modifier::Composite(Composite {
558 source: match source {
559 Some(source) => source.clone(),
560 None => {
561 let Modifier::Composite(Composite { source, .. }) =
562 &ctx.block.modifiers()[this_modifier_index]
563 else {
564 panic!("modifier mismatch");
565 };
566 source.clone()
567 }
568 },
569 operator: self,
570 reverse: ctx.was_reversed,
571 disassemblable: ctx.disassemblable,
572 }));
573
574 Some(Operation::Become(new_block))
575 }
576
577 fn bounds(self, source: GridAab, destination: GridAab) -> GridAab {
579 match self {
580 Self::Over => source.union_cubes(destination),
581 Self::In => source
584 .intersection_box(destination)
585 .unwrap_or(GridAab::ORIGIN_EMPTY),
586 Self::Out => source,
587 Self::Atop => destination,
588 }
589 }
590
591 #[expect(clippy::unused_self)]
594 fn rotationally_symmetric(self) -> bool {
595 true
596 }
597}
598
599pub(in crate::block) fn render_inventory(
601 mut input: MinEval,
602 inventory: &crate::inv::Inventory,
603 filter: &block::EvalFilter,
604) -> Result<MinEval, block::InEvalError> {
605 if filter.skip_eval {
606 return Ok(input);
607 }
608
609 let config = input.attributes().inventory.clone();
611
612 for (slot_index, icon_position) in config.icon_positions(inventory.slots.len()) {
613 let Some(placed_icon_bounds) = GridAab::from_lower_size(
614 icon_position,
615 GridSize::splat(
616 (config.icon_resolution / config.icon_scale)
617 .unwrap_or(R1)
618 .into(),
619 ),
620 )
621 .intersection_cubes(GridAab::for_block(config.icon_resolution)) else {
622 continue;
624 };
625
626 let Some(icon): Option<&Block> = inventory
628 .slots
629 .get(slot_index)
630 .and_then(|slot| slot.icon_only_if_intrinsic())
631 else {
632 continue;
634 };
635
636 let mut icon_evaluated = {
637 let _recursion_scope = block::Budget::recurse(&filter.budget)?;
638 icon.evaluate_impl(filter)?
640 .finish(icon.clone(), filter.budget.get().to_cost())
641 };
642
643 let icon_voxel = Evoxel::from_block(&icon_evaluated);
647 icon_evaluated.voxels = Evoxels::from_many(
648 config.icon_resolution,
649 Vol::repeat(placed_icon_bounds, icon_voxel),
650 );
651
652 input = evaluate_composition(
653 icon_evaluated.into(),
654 input,
655 CompositeOperator::Over,
656 filter,
657 &CompEvalCtx {
658 block: const { &AIR }, this_modifier_index: None, was_reversed: false,
661 disassemblable: false,
662 },
663 )?;
664 }
665
666 Ok(input)
667}
668
669#[cfg(test)]
670mod tests {
671 use super::*;
672 use crate::block::{EvKey, EvaluatedBlock, Resolution::*};
673 use crate::content::{make_slab, make_some_blocks};
674 use crate::math::{zo32, Rgba};
675 use crate::space::Space;
676 use crate::time;
677 use crate::universe::Universe;
678 use pretty_assertions::assert_eq;
679 use BlockCollision::{Hard, None as CNone};
680 use CompositeOperator::{Atop, In, Over};
681
682 #[track_caller]
686 fn assert_blend(src: Evoxel, operator: CompositeOperator, dst: Evoxel, outcome: Evoxel) {
687 assert_eq!(
690 operator.blend_evoxel(src, dst),
691 outcome,
692 "\nexpecting {operator:?}.blend(\n {src:?},\n {dst:?}\n) == {outcome:?}"
693 );
694 }
695
696 fn evcolor(color: Rgba) -> Evoxel {
697 Evoxel {
698 color,
699 emission: Rgb::ZERO,
700 selectable: true,
701 collision: Hard,
702 }
703 }
704
705 fn evemit(emission: Rgb, alpha: f32) -> Evoxel {
708 Evoxel {
709 color: Rgb::ZERO.with_alpha(zo32(alpha)),
712 emission,
713 selectable: true,
714 collision: Hard,
715 }
716 }
717
718 fn evcoll(collision: BlockCollision) -> Evoxel {
719 Evoxel {
720 color: Rgba::WHITE, emission: Rgb::ZERO,
722 selectable: false, collision,
724 }
725 }
726
727 #[track_caller]
728 fn eval_compose(src: &Block, operator: CompositeOperator, dst: &Block) -> EvaluatedBlock {
729 dst.clone()
730 .with_modifier(Composite::new(src.clone(), operator))
731 .evaluate()
732 .expect("failed to evaluate in eval_compose()")
733 }
734
735 #[test]
738 fn bounding_volume_combination() {
739 let universe = &mut Universe::new();
740 let bounds1 = GridAab::from_lower_size([0, 0, 0], [2, 1, 1]);
742 let bounds2 = GridAab::from_lower_size([1, 0, 0], [2, 1, 1]);
743 let space1 = universe.insert_anonymous(Space::builder(bounds1).build());
744 let space2 = universe.insert_anonymous(Space::builder(bounds2).build());
745 let block1 = Block::builder().voxels_handle(R4, space1).build();
746 let block2 = Block::builder().voxels_handle(R4, space2).build();
747
748 let union = GridAab::from_lower_size([0, 0, 0], [3, 1, 1]);
749 let intersection = GridAab::from_lower_size([1, 0, 0], [1, 1, 1]);
750
751 assert_eq!(
752 eval_compose(&block1, Over, &block2).voxels_bounds(),
753 union,
754 "Over"
755 );
756 assert_eq!(
757 eval_compose(&block1, In, &block2).voxels_bounds(),
758 intersection,
759 "In"
760 );
761 assert_eq!(
762 eval_compose(&block1, Atop, &block2).voxels_bounds(),
763 bounds2,
764 "Atop"
765 );
766 }
767
768 #[test]
769 fn bounding_volume_when_one_is_air() {
770 let universe = &mut Universe::new();
771 let slab = make_slab(universe, 1, R2);
772 let slab_bounds = slab.evaluate().unwrap().voxels_bounds();
773
774 assert_eq!(
775 eval_compose(&slab, Over, &AIR).voxels_bounds(),
776 slab_bounds,
777 "Over",
778 );
779 assert_eq!(
780 eval_compose(&slab, In, &AIR).voxels_bounds(),
781 GridAab::ORIGIN_EMPTY,
782 "In",
783 );
784 assert_eq!(
785 eval_compose(&slab, Atop, &AIR).voxels_bounds(),
786 GridAab::ORIGIN_EMPTY,
787 "Atop AIR",
788 );
789 assert_eq!(
790 eval_compose(&AIR, Atop, &slab).voxels_bounds(),
791 slab_bounds,
792 "AIR Atop",
793 );
794 }
795
796 #[test]
800 fn composite_with_air_is_short_circuit_noop() {
801 let universe = &mut Universe::new();
802
803 let base_block = Block::builder()
808 .voxels_fn(R4, |_| color_block!(1.0, 0.0, 0.0, 1.0))
809 .unwrap()
810 .build_into(universe);
811 let base_block = Block::from(universe.insert_anonymous(block::BlockDef::new(base_block)));
812 let base_key = EvKey::new(&base_block.evaluate().unwrap());
813 assert_eq!(
814 base_key,
815 EvKey::new(&base_block.evaluate().unwrap()),
816 "test assumption failed"
817 );
818
819 let air_src_block = base_block.clone().with_modifier(Composite::new(AIR, Over));
820 assert_eq!(
821 base_key,
822 EvKey::new(&air_src_block.evaluate().unwrap()),
823 "src"
824 );
825
826 let air_dst_block = base_block.with_modifier(Composite::new(AIR, Over).reversed());
831 assert_ne!(
832 base_key,
833 EvKey::new(&air_dst_block.evaluate().unwrap()),
834 "dst"
835 );
836 }
837
838 mod voxel {
840 use super::*;
841
842 #[test]
843 fn over_silly_floats() {
844 Over.blend_evoxel(
847 evcolor(Rgba::new(2e25, 2e25, 2e25, 1.0)),
848 evcolor(Rgba::new(2e25, 2e25, 2e25, 1.0)),
849 );
850 }
851
852 #[test]
853 fn over_emission() {
854 let red_1 = evemit(Rgb::new(1., 0., 0.), 1.0);
855 let green_0 = evemit(Rgb::new(0., 1., 0.), 0.0);
856 let green_05 = evemit(Rgb::new(0., 1., 0.), 0.5);
857 let none_1 = evemit(Rgb::ZERO, 1.0);
858 let none_0 = evemit(Rgb::ZERO, 0.0);
859
860 assert_blend(red_1, Over, none_1, red_1);
862 assert_blend(none_1, Over, red_1, none_1);
863 assert_blend(none_1, Over, none_1, none_1);
864 assert_blend(red_1, Over, red_1, red_1);
865 assert_blend(red_1, Over, none_0, red_1);
866 assert_blend(none_0, Over, red_1, red_1);
867
868 assert_blend(red_1, Over, green_05, red_1);
870 assert_blend(green_05, Over, red_1, evemit(Rgb::new(0.5, 0.5, 0.0), 1.0));
871 assert_blend(
872 green_05,
873 Over,
874 green_05,
875 evemit(Rgb::new(0.0, 0.75, 0.0), 0.75),
876 );
877 assert_blend(green_0, Over, none_1, none_1);
881 assert_blend(none_1, Over, green_0, none_1);
883 }
885
886 #[test]
887 fn over_collision() {
888 assert_blend(evcoll(Hard), Over, evcoll(Hard), evcoll(Hard));
889 assert_blend(evcoll(CNone), Over, evcoll(CNone), evcoll(CNone));
890 assert_blend(evcoll(Hard), Over, evcoll(CNone), evcoll(Hard));
891 assert_blend(evcoll(CNone), Over, evcoll(Hard), evcoll(Hard));
892 }
893
894 #[test]
895 fn in_emission() {
896 let red_1 = evemit(Rgb::new(1., 0., 0.), 1.0);
897 let green_1 = evemit(Rgb::new(0., 1., 0.), 1.0);
898 let green_0 = evemit(Rgb::new(0., 1., 0.), 0.0);
899 let green_05 = evemit(Rgb::new(0., 1., 0.), 0.5);
900 let none_1 = evemit(Rgb::ZERO, 1.0);
901 let none_0 = evemit(Rgb::ZERO, 0.0);
902
903 assert_blend(red_1, In, none_1, red_1);
905 assert_blend(red_1, In, red_1, red_1);
906 assert_blend(red_1, In, green_1, red_1);
907 assert_blend(red_1, In, none_0, none_0);
908 assert_blend(none_1, In, red_1, none_1);
909 assert_blend(none_0, In, red_1, none_0);
910 assert_blend(none_1, In, none_1, none_1);
911 assert_blend(none_0, In, none_1, none_0);
912
913 assert_blend(red_1, In, green_05, evemit(Rgb::new(0.5, 0.0, 0.0), 0.5));
915 assert_blend(green_05, In, red_1, evemit(Rgb::new(0.0, 0.5, 0.0), 0.5));
916 assert_blend(
917 green_05,
918 In,
919 green_05,
920 evemit(Rgb::new(0.0, 0.25, 0.0), 0.25),
921 );
922 assert_blend(green_05, In, none_0, none_0); assert_blend(green_0, In, none_1, none_0);
926 assert_blend(green_0, In, none_0, none_0);
927 assert_blend(none_1, In, green_0, none_0);
928 assert_blend(green_0, In, green_0, none_0); }
930
931 #[test]
932 fn in_collision() {
933 assert_blend(evcoll(Hard), In, evcoll(Hard), evcoll(Hard));
934 assert_blend(evcoll(CNone), In, evcoll(CNone), evcoll(CNone));
935 assert_blend(evcoll(Hard), In, evcoll(CNone), evcoll(CNone));
936 assert_blend(evcoll(CNone), In, evcoll(Hard), evcoll(CNone));
937 }
938
939 #[test]
940 fn atop_color() {
941 let opaque1 = evcolor(Rgba::new(1.0, 0.0, 0.0, 1.0));
942 let opaque2 = evcolor(Rgba::new(0.0, 1.0, 0.0, 1.0));
943 let half_red = evcolor(Rgba::new(1.0, 0.0, 0.0, 0.5));
944 let clear = evcolor(Rgba::TRANSPARENT);
945
946 assert_blend(opaque1, Atop, opaque2, opaque1);
947 assert_blend(
948 half_red,
949 Atop,
950 opaque2,
951 evcolor(Rgba::new(0.5, 0.5, 0.0, 1.0)),
952 );
953 assert_blend(opaque1, Atop, clear, clear);
954 assert_blend(clear, Atop, opaque2, opaque2);
955 assert_blend(clear, Atop, clear, clear);
956 }
957
958 #[test]
959 fn atop_emission() {
960 let red_1 = evemit(Rgb::new(1., 0., 0.), 1.0);
961 let green_1 = evemit(Rgb::new(0., 1., 0.), 1.0);
962 let green_0 = evemit(Rgb::new(0., 1., 0.), 0.0);
963 let green_05 = evemit(Rgb::new(0., 1., 0.), 0.5);
964 let none_1 = evemit(Rgb::ZERO, 1.0);
965 let none_0 = evemit(Rgb::ZERO, 0.0);
966
967 assert_blend(red_1, Atop, none_1, red_1);
969 assert_blend(red_1, Atop, red_1, red_1);
970 assert_blend(red_1, Atop, green_1, red_1);
971 assert_blend(red_1, Atop, none_0, none_0);
972 assert_blend(none_1, Atop, red_1, none_1);
973 assert_blend(none_0, Atop, red_1, red_1);
974 assert_blend(none_1, Atop, none_1, none_1);
975 assert_blend(none_0, Atop, none_1, none_1);
976
977 assert_blend(red_1, Atop, green_05, evemit(Rgb::new(0.5, 0.0, 0.0), 0.5));
979 assert_blend(green_05, Atop, red_1, evemit(Rgb::new(0.5, 0.5, 0.0), 1.0));
980 assert_blend(
981 green_05,
982 Atop,
983 green_05,
984 evemit(Rgb::new(0.0, 0.5, 0.0), 0.5),
985 );
986 assert_blend(green_05, Atop, none_0, none_0);
987
988 assert_blend(green_0, Atop, none_1, none_1);
990 assert_blend(green_0, Atop, none_0, none_0);
991 assert_blend(none_1, Atop, green_0, none_0);
992 assert_blend(green_0, Atop, green_0, none_0); }
994
995 #[test]
996 fn blend_atop_collision() {
997 assert_blend(evcoll(Hard), Atop, evcoll(Hard), evcoll(Hard));
998 assert_blend(evcoll(CNone), Atop, evcoll(CNone), evcoll(CNone));
999 assert_blend(evcoll(Hard), Atop, evcoll(CNone), evcoll(CNone));
1000 assert_blend(evcoll(CNone), Atop, evcoll(Hard), evcoll(Hard));
1001 }
1002 }
1003
1004 mod attributes {
1006 use super::{assert_eq, *};
1007
1008 #[test]
1009 fn selectable_if_either_is_selectable() {
1010 let is_sel = &Block::builder().color(Rgba::WHITE).selectable(true).build();
1013 let not_sel = &Block::builder()
1014 .color(Rgba::WHITE)
1015 .selectable(false)
1016 .build();
1017
1018 assert!(eval_compose(is_sel, Over, is_sel).attributes().selectable);
1019 assert!(eval_compose(is_sel, Over, not_sel).attributes().selectable);
1020 assert!(eval_compose(not_sel, Over, is_sel).attributes().selectable);
1021 assert!(!eval_compose(not_sel, Over, not_sel).attributes().selectable);
1022
1023 assert!(eval_compose(is_sel, In, is_sel).attributes().selectable);
1024 assert!(eval_compose(is_sel, In, not_sel).attributes().selectable);
1025 assert!(eval_compose(not_sel, In, is_sel).attributes().selectable);
1026 assert!(!eval_compose(not_sel, In, not_sel).attributes().selectable);
1027 }
1028
1029 #[test]
1030 fn activation_action_is_composed() {
1031 let [result1, result2] = make_some_blocks();
1032 let b1 = &Block::builder()
1033 .color(Rgba::WHITE)
1034 .activation_action(Operation::Become(result1.clone()))
1035 .build();
1036 let b2 = &Block::builder()
1037 .color(Rgba::WHITE)
1038 .activation_action(Operation::Become(result2.clone()))
1039 .build();
1040
1041 assert_eq!(
1042 eval_compose(b1, Over, b2).attributes().activation_action,
1043 Some(Operation::Become(
1044 result2.with_modifier(Composite::new(result1, Over))
1045 ))
1046 );
1047
1048 }
1050
1051 #[test]
1052 fn tick_action_is_composed() {
1053 let [result1, result2] = make_some_blocks();
1054 let b1 = &Block::builder()
1055 .color(Rgba::WHITE)
1056 .tick_action(block::TickAction {
1057 schedule: time::Schedule::EVERY_TICK,
1058 operation: Operation::Become(result1.clone()),
1059 })
1060 .build();
1061 let b2 = &Block::builder()
1062 .color(Rgba::WHITE)
1063 .tick_action(block::TickAction {
1064 schedule: time::Schedule::EVERY_TICK,
1065 operation: Operation::Become(result2.clone()),
1066 })
1067 .build();
1068
1069 assert_eq!(
1070 eval_compose(b1, Over, b2).attributes().tick_action,
1071 Some(block::TickAction {
1072 schedule: time::Schedule::EVERY_TICK,
1073 operation: Operation::Become(
1074 result2.with_modifier(Composite::new(result1, Over))
1075 )
1076 })
1077 );
1078
1079 }
1082 }
1083
1084 mod ops {
1086 use super::{assert_eq, *};
1087
1088 #[test]
1089 fn compose_or_replace_source_is_air() {
1090 let [block] = make_some_blocks();
1091 assert_eq!(
1092 Composite::new(AIR, Over).compose_or_replace(block.clone()),
1093 block
1094 );
1095 }
1096
1097 #[test]
1098 fn compose_or_replace_destination_is_air() {
1099 let [block] = make_some_blocks();
1100 assert_eq!(
1101 Composite::new(block.clone(), Over).compose_or_replace(AIR),
1102 block
1103 );
1104 }
1105
1106 #[test]
1107 fn unspecialize_no() {
1108 let [b1, b2] = make_some_blocks();
1109 let composed = b1.clone().with_modifier(Composite::new(b2.clone(), Over));
1110 assert_eq!(composed.unspecialize(), vec![composed]);
1111 }
1112
1113 #[test]
1114 fn unspecialize_yes() {
1115 let [b1, b2] = make_some_blocks();
1116 let composed = b1
1117 .clone()
1118 .with_modifier(Composite::new(b2.clone(), Over).with_disassemblable());
1119 assert_eq!(composed.unspecialize(), vec![b2, b1]);
1120 }
1121 }
1122}