Repository: ManimCommunity/manim

Issue:
Mobject Ordering Regression in 0.15
Consider the following scene:

```python
class Test(Scene):
    def construct(self):
        a = Square(color = RED)
        b = Square(color = BLUE)
        b.shift(RIGHT)
        self.add(a)
        self.wait()
        self.bring_to_back(b)
        self.play(FadeIn(b))
        self.wait()
```

In 0.14, this is the output:

https://user-images.githubusercontent.com/20936595/156047966-0ba1256e-bfef-41f0-99ed-8a8a5278be6e.mp4

In 0.15, this is the output:

https://user-images.githubusercontent.com/20936595/156047888-01c82af9-5c90-41bb-9944-72eb8197a9c8.mp4

As you can see, the blue square is correctly drawn behind the red one in 0.14, but in 0.15, the blue square is drawn on top.


Discussion:
Bisecting shows that this was introduced via #2396. This should be fixed by checking whether the mobject belonging to an animation with `introducer=True` set (like `FadeIn`) has already been added to the scene.

There is one workaround I can offer: in your example, either put
```py
self.play(FadeIn(b), introducer=False)
```
or, more verbose,
```py
anim = FadeIn(b)
anim.introducer = False
self.play(anim)
```
(the obvious `FadeIn(b, introducer=False)` unfortunately won't work due to the keyword argument not being propagated correctly.)

Resolving patch:
diff --git a/manim/animation/animation.py b/manim/animation/animation.py
--- a/manim/animation/animation.py
+++ b/manim/animation/animation.py
@@ -233,7 +233,10 @@ def _setup_scene(self, scene: Scene) -> None:
         """
         if scene is None:
             return
-        if self.is_introducer():
+        if (
+            self.is_introducer()
+            and self.mobject not in scene.get_mobject_family_members()
+        ):
             scene.add(self.mobject)
 
     def create_starting_mobject(self) -> Mobject:
diff --git a/manim/renderer/cairo_renderer.py b/manim/renderer/cairo_renderer.py
--- a/manim/renderer/cairo_renderer.py
+++ b/manim/renderer/cairo_renderer.py
@@ -86,13 +86,14 @@ def play(self, scene, *args, **kwargs):
             {"h": str(self.animations_hashes[:5])},
         )
 
+        self.file_writer.begin_animation(not self.skip_animations)
+        scene.begin_animations()
+
         # Save a static image, to avoid rendering non moving objects.
         self.static_image = self.save_static_frame_data(scene, scene.static_mobjects)
 
-        self.file_writer.begin_animation(not self.skip_animations)
-        scene.begin_animations()
         if scene.is_current_animation_frozen_frame():
-            self.update_frame(scene)
+            self.update_frame(scene, mobjects=scene.moving_mobjects)
             # self.duration stands for the total run time of all the animations.
             # In this case, as there is only a wait, it will be the length of the wait.
             self.freeze_current_frame(scene.duration)
@@ -218,8 +219,8 @@ def save_static_frame_data(
         typing.Iterable[Mobject]
             the static image computed.
         """
+        self.static_image = None
         if not static_mobjects:
-            self.static_image = None
             return None
         self.update_frame(scene, mobjects=static_mobjects)
         self.static_image = self.get_frame()
@@ -258,8 +259,10 @@ def scene_finished(self, scene):
             config.save_last_frame = True
             config.write_to_movie = False
         else:
+            self.static_image = None
             self.update_frame(scene)
 
         if config["save_last_frame"]:
+            self.static_image = None
             self.update_frame(scene)
             self.file_writer.save_final_image(self.camera.get_image())
diff --git a/manim/scene/scene.py b/manim/scene/scene.py
--- a/manim/scene/scene.py
+++ b/manim/scene/scene.py
@@ -451,7 +451,7 @@ def add(self, *mobjects):
             mobjects = [*mobjects, *self.foreground_mobjects]
             self.restructure_mobjects(to_remove=mobjects)
             self.mobjects += mobjects
-            if self.moving_mobjects is not None:
+            if self.moving_mobjects:
                 self.restructure_mobjects(
                     to_remove=mobjects,
                     mobject_list_name="moving_mobjects",
@@ -1074,13 +1074,6 @@ def compile_animation_data(self, *animations: Animation, **play_kwargs):
                 # Static image logic when the wait is static is done by the renderer, not here.
                 self.animations[0].is_static_wait = True
                 return None
-        elif config.renderer != "opengl":
-            # Paint all non-moving objects onto the screen, so they don't
-            # have to be rendered every frame
-            (
-                self.moving_mobjects,
-                self.static_mobjects,
-            ) = self.get_moving_and_static_mobjects(self.animations)
         self.duration = self.get_run_time(self.animations)
         return self
 
@@ -1090,6 +1083,14 @@ def begin_animations(self) -> None:
             animation._setup_scene(self)
             animation.begin()
 
+        if config.renderer != "opengl":
+            # Paint all non-moving objects onto the screen, so they don't
+            # have to be rendered every frame
+            (
+                self.moving_mobjects,
+                self.static_mobjects,
+            ) = self.get_moving_and_static_mobjects(self.animations)
+
     def is_current_animation_frozen_frame(self) -> bool:
         """Returns whether the current animation produces a static frame (generally a Wait)."""
         return (


Test patch:
diff --git a/tests/opengl/test_composition_opengl.py b/tests/opengl/test_composition_opengl.py
--- a/tests/opengl/test_composition_opengl.py
+++ b/tests/opengl/test_composition_opengl.py
@@ -1,6 +1,6 @@
 from __future__ import annotations
 
-from unittest.mock import Mock
+from unittest.mock import MagicMock
 
 from manim.animation.animation import Animation, Wait
 from manim.animation.composition import AnimationGroup, Succession
@@ -18,7 +18,7 @@ def test_succession_timing(using_opengl_renderer):
     animation_4s = FadeOut(line, shift=DOWN, run_time=4.0)
     succession = Succession(animation_1s, animation_4s)
     assert succession.get_run_time() == 5.0
-    succession._setup_scene(Mock())
+    succession._setup_scene(MagicMock())
     succession.begin()
     assert succession.active_index == 0
     # The first animation takes 20% of the total run time.
@@ -50,7 +50,7 @@ def test_succession_in_succession_timing(using_opengl_renderer):
     )
     assert nested_succession.get_run_time() == 5.0
     assert succession.get_run_time() == 10.0
-    succession._setup_scene(Mock())
+    succession._setup_scene(MagicMock())
     succession.begin()
     succession.interpolate(0.1)
     assert succession.active_index == 0
diff --git a/tests/test_composition.py b/tests/test_composition.py
--- a/tests/test_composition.py
+++ b/tests/test_composition.py
@@ -1,6 +1,6 @@
 from __future__ import annotations
 
-from unittest.mock import Mock
+from unittest.mock import MagicMock
 
 import pytest
 
@@ -22,7 +22,7 @@ def test_succession_timing():
     animation_4s = FadeOut(line, shift=DOWN, run_time=4.0)
     succession = Succession(animation_1s, animation_4s)
     assert succession.get_run_time() == 5.0
-    succession._setup_scene(Mock())
+    succession._setup_scene(MagicMock())
     succession.begin()
     assert succession.active_index == 0
     # The first animation takes 20% of the total run time.
@@ -54,7 +54,7 @@ def test_succession_in_succession_timing():
     )
     assert nested_succession.get_run_time() == 5.0
     assert succession.get_run_time() == 10.0
-    succession._setup_scene(Mock())
+    succession._setup_scene(MagicMock())
     succession.begin()
     succession.interpolate(0.1)
     assert succession.active_index == 0
diff --git a/tests/test_graphical_units/control_data/creation/bring_to_back_introducer.npz b/tests/test_graphical_units/control_data/creation/bring_to_back_introducer.npz
new file mode 100644
Binary files /dev/null and b/tests/test_graphical_units/control_data/creation/bring_to_back_introducer.npz differ
diff --git a/tests/test_graphical_units/control_data/creation/z_index_introducer.npz b/tests/test_graphical_units/control_data/creation/z_index_introducer.npz
new file mode 100644
Binary files /dev/null and b/tests/test_graphical_units/control_data/creation/z_index_introducer.npz differ
diff --git a/tests/test_graphical_units/control_data/vector_scene/vector_to_coords.npz b/tests/test_graphical_units/control_data/vector_scene/vector_to_coords.npz
Binary files a/tests/test_graphical_units/control_data/vector_scene/vector_to_coords.npz and b/tests/test_graphical_units/control_data/vector_scene/vector_to_coords.npz differ
diff --git a/tests/test_graphical_units/test_creation.py b/tests/test_graphical_units/test_creation.py
--- a/tests/test_graphical_units/test_creation.py
+++ b/tests/test_graphical_units/test_creation.py
@@ -67,3 +67,23 @@ def test_SpinInFromNothing(scene):
 def test_ShrinkToCenter(scene):
     square = Square()
     scene.play(ShrinkToCenter(square))
+
+
+@frames_comparison(last_frame=False)
+def test_bring_to_back_introducer(scene):
+    a = Square(color=RED, fill_opacity=1)
+    b = Square(color=BLUE, fill_opacity=1).shift(RIGHT)
+    scene.add(a)
+    scene.bring_to_back(b)
+    scene.play(FadeIn(b))
+    scene.wait()
+
+
+@frames_comparison(last_frame=False)
+def test_z_index_introducer(scene):
+    a = Circle().set_fill(color=RED, opacity=1.0)
+    scene.add(a)
+    b = Circle(arc_center=(0.5, 0.5, 0.0), color=GREEN, fill_opacity=1)
+    b.set_z_index(-1)
+    scene.play(Create(b))
+    scene.wait()
diff --git a/tests/test_graphical_units/test_vector_scene.py b/tests/test_graphical_units/test_vector_scene.py
--- a/tests/test_graphical_units/test_vector_scene.py
+++ b/tests/test_graphical_units/test_vector_scene.py
@@ -13,3 +13,4 @@ def test_vector_to_coords(scene):
     basis = scene.get_basis_vectors()
     scene.add(basis)
     scene.vector_to_coords(vector=vector)
+    scene.wait()