Repository: ManimCommunity/manim

Issue:
TransformMatchingTex fails if fade_transform_mismatches set but there are no mismatches
## Description of bug / unexpected behavior
When rearranging the terms in a Latex equation, I ran into a confounding error that ended up being because I set `fade_transform_matches` when it wasn't strictly necessary. That is, all strings in the first equation exist in the second equation, so there is nothing to fade out or in.

## Expected behavior
Setting `face_transform_matches` should not cause a crash, but just be a no-op if there are no arriving or exiting Latex parts


## How to reproduce the issue

<details><summary>Code for reproducing the problem</summary>

```py
class ReproBug(Scene):
  def construct(self):
    start = MathTex("A", "->", "B")
    end = MathTex("B", "->", "A")

    self.add(start)
    self.play(TransformMatchingTex(start, end, fade_transform_mismatches=True))
```
</details>


## Logs
<details><summary>Terminal output</summary>

```
│   815 │   end = MathTex("B", "->", "A")                                                          │
│   816 │                                                                                          │
│   817 │   self.add(start)                                                                        │
│ ❱ 818 │   self.play(TransformMatchingTex(start, end, fade_transform_mismatches=True))            │
│   819                                                                                            │
│   820                                                                                            │
│                                                                                                  │
│ /usr/local/lib/python3.9/site-packages/manim/scene/scene.py:1012 in play                         │
│                                                                                                  │
│   1009 │   │                                                                                     │
│   1010 │   │   """                                                                               │
│   1011 │   │   start_time = self.renderer.time                                                   │
│ ❱ 1012 │   │   self.renderer.play(self, *args, **kwargs)                                         │
│   1013 │   │   run_time = self.renderer.time - start_time                                        │
│   1014 │   │   if subcaption:                                                                    │
│   1015 │   │   │   if subcaption_duration is None:                                               │
│                                                                                                  │
│ /usr/local/lib/python3.9/site-packages/manim/renderer/cairo_renderer.py:90 in play               │
│                                                                                                  │
│    87 │   │   )                                                                                  │
│    88 │   │                                                                                      │
│    89 │   │   self.file_writer.begin_animation(not self.skip_animations)                         │
│ ❱  90 │   │   scene.begin_animations()                                                           │
│    91 │   │                                                                                      │
│    92 │   │   # Save a static image, to avoid rendering non moving objects.                      │
│    93 │   │   self.static_image = self.save_static_frame_data(scene, scene.static_mobjects)      │
│                                                                                                  │
│ /usr/local/lib/python3.9/site-packages/manim/scene/scene.py:1139 in begin_animations             │
│                                                                                                  │
│   1136 │   │   """Start the animations of the scene."""                                          │
│   1137 │   │   for animation in self.animations:                                                 │
│   1138 │   │   │   animation._setup_scene(self)                                                  │
│ ❱ 1139 │   │   │   animation.begin()                                                             │
│   1140 │   │                                                                                     │
│   1141 │   │   if config.renderer != "opengl":                                                   │
│   1142 │   │   │   # Paint all non-moving objects onto the screen, so they don't                 │
│                                                                                                  │
│ /usr/local/lib/python3.9/site-packages/manim/animation/composition.py:60 in begin                │
│                                                                                                  │
│    57 │   │   if self.suspend_mobject_updating:                                                  │
│    58 │   │   │   self.group.suspend_updating()                                                  │
│    59 │   │   for anim in self.animations:                                                       │
│ ❱  60 │   │   │   anim.begin()                                                                   │
│    61 │                                                                                          │
│    62 │   def _setup_scene(self, scene) -> None:                                                 │
│    63 │   │   for anim in self.animations:                                                       │
│                                                                                                  │
│ /usr/local/lib/python3.9/site-packages/manim/animation/transform.py:805 in begin                 │
│                                                                                                  │
│   802 │                                                                                          │
│   803 │   def begin(self):                                                                       │
│   804 │   │   self.mobject[0].align_submobjects(self.mobject[1])                                 │
│ ❱ 805 │   │   super().begin()                                                                    │
│   806 │                                                                                          │
│   807 │   def ghost_to(self, source, target):                                                    │
│   808 │   │   """Replaces the source submobjects by the target submobjects and sets              │
│                                                                                                  │
│ /usr/local/lib/python3.9/site-packages/manim/animation/transform.py:749 in begin                 │
│                                                                                                  │
│   746 │   │   # and at the end it should be the other way around.                                │
│   747 │   │   start, end = self.starting_mobject, self.ending_mobject                            │
│   748 │   │   for m0, m1 in ((start[1], start[0]), (end[0], end[1])):                            │
│ ❱ 749 │   │   │   self.ghost_to(m0, m1)                                                          │
│   750 │                                                                                          │
│   751 │   def ghost_to(self, source, target):                                                    │
│   752 │   │   """Replaces the source by the target and sets the opacity to 0."""                 │
│                                                                                                  │
│ /usr/local/lib/python3.9/site-packages/manim/animation/transform.py:812 in ghost_to              │
│                                                                                                  │
│   809 │   │   the opacity to 0.                                                                  │
│   810 │   │   """                                                                                │
│   811 │   │   for sm0, sm1 in zip(source.get_family(), target.get_family()):                     │
│ ❱ 812 │   │   │   super().ghost_to(sm0, sm1)                                                     │
│   813                                                                                            │
│                                                                                                  │
│ /usr/local/lib/python3.9/site-packages/manim/animation/transform.py:753 in ghost_to              │
│                                                                                                  │
│   750 │                                                                                          │
│   751 │   def ghost_to(self, source, target):                                                    │
│   752 │   │   """Replaces the source by the target and sets the opacity to 0."""                 │
│ ❱ 753 │   │   source.replace(target, stretch=self.stretch, dim_to_match=self.dim_to_match)       │
│   754 │   │   source.set_opacity(0)                                                              │
│   755 │                                                                                          │
│   756 │   def get_all_mobjects(self) -> Sequence[Mobject]:                                       │
│                                                                                                  │
│ /usr/local/lib/python3.9/site-packages/manim/mobject/mobject.py:1599 in replace                  │
│                                                                                                  │
│   1596 │                                                                                         │
│   1597 │   def replace(self, mobject, dim_to_match=0, stretch=False):                            │
│   1598 │   │   if not mobject.get_num_points() and not mobject.submobjects:                      │
│ ❱ 1599 │   │   │   raise Warning("Attempting to replace mobject with no points")                 │
│   1600 │   │   if stretch:                                                                       │
│   1601 │   │   │   self.stretch_to_fit_width(mobject.width)                                      │
│   1602 │   │   │   self.stretch_to_fit_height(mobject.height)                                    │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
Warning: Attempting to replace mobject with no points
```
</details>


## System specifications

<details><summary>System Details</summary>

- OS (with version, e.g., Windows 10 v2004 or macOS 10.15 (Catalina)): MacOS 12.4
- RAM: 8 GB
- Python version (`python/py/python3 --version`): 3.9.13
- Installed modules (provide output from `pip list`):  Manim community 0.15.2 is installed. Others available upon request.
</details>

<details><summary>LaTeX details</summary>

+ LaTeX distribution (e.g. TeX Live 2020): [MacTeX](http://www.tug.org/mactex/)
+ Installed LaTeX packages: Available upon request
</details>

<details><summary>FFMPEG</summary>

Output of `ffmpeg -version`:

```
ffmpeg version 5.0.1 Copyright (c) 2000-2022 the FFmpeg developers
built with Apple clang version 13.1.6 (clang-1316.0.21.2.5)
configuration: --prefix=/usr/local/Cellar/ffmpeg/5.0.1_2 --enable-shared --enable-pthreads --enable-version3 --cc=clang --host-cflags= --host-ldflags= --enable-ffplay --enable-gnutls --enable-gpl --enable-libaom --enable-libbluray --enable-libdav1d --enable-libmp3lame --enable-libopus --enable-librav1e --enable-librist --enable-librubberband --enable-libsnappy --enable-libsrt --enable-libtesseract --enable-libtheora --enable-libvidstab --enable-libvmaf --enable-libvorbis --enable-libvpx --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxml2 --enable-libxvid --enable-lzma --enable-libfontconfig --enable-libfreetype --enable-frei0r --enable-libass --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenjpeg --enable-libspeex --enable-libsoxr --enable-libzmq --enable-libzimg --disable-libjack --disable-indev=jack --enable-videotoolbox
libavutil      57. 17.100 / 57. 17.100
libavcodec     59. 18.100 / 59. 18.100
libavformat    59. 16.100 / 59. 16.100
libavdevice    59.  4.100 / 59.  4.100
libavfilter     8. 24.100 /  8. 24.100
libswscale      6.  4.100 /  6.  4.100
libswresample   4.  3.100 /  4.  3.100
libpostproc    56.  3.100 / 56.  3.100
```
</details>

## Additional comments
This could probably be fixed by making [FadeTransformPieces](https://github.com/ManimCommunity/manim/blob/bcb5233259c34e8d6f8cfe23a6bd2aa71db4f2ec/manim/animation/transform.py#L836) be a no-op if the input and output are empty or skip adding fades in [TransformMatchingAbstractBase](https://github.com/ManimCommunity/manim/blob/bcb5233259c34e8d6f8cfe23a6bd2aa71db4f2ec/manim/animation/transform_matching_parts.py#L126)



Discussion:

Resolving patch:
diff --git a/manim/animation/transform.py b/manim/animation/transform.py
--- a/manim/animation/transform.py
+++ b/manim/animation/transform.py
@@ -823,8 +823,14 @@ def begin(self):
             self.ghost_to(m0, m1)
 
     def ghost_to(self, source, target):
-        """Replaces the source by the target and sets the opacity to 0."""
-        source.replace(target, stretch=self.stretch, dim_to_match=self.dim_to_match)
+        """Replaces the source by the target and sets the opacity to 0.
+
+        If the provided target has no points, and thus a location of [0, 0, 0]
+        the source will simply fade out where it currently is.
+        """
+        # mobject.replace() does not work if the target has no points.
+        if target.get_num_points() or target.submobjects:
+            source.replace(target, stretch=self.stretch, dim_to_match=self.dim_to_match)
         source.set_opacity(0)
 
     def get_all_mobjects(self) -> Sequence[Mobject]:


Test patch:
diff --git a/tests/test_graphical_units/control_data/transform/FadeTransform.npz b/tests/test_graphical_units/control_data/transform/FadeTransform.npz
new file mode 100644
Binary files /dev/null and b/tests/test_graphical_units/control_data/transform/FadeTransform.npz differ
diff --git a/tests/test_graphical_units/control_data/transform/FadeTransformPieces.npz b/tests/test_graphical_units/control_data/transform/FadeTransformPieces.npz
new file mode 100644
Binary files /dev/null and b/tests/test_graphical_units/control_data/transform/FadeTransformPieces.npz differ
diff --git a/tests/test_graphical_units/control_data/transform/FadeTransform_TargetIsEmpty_FadesOutInPlace.npz b/tests/test_graphical_units/control_data/transform/FadeTransform_TargetIsEmpty_FadesOutInPlace.npz
new file mode 100644
Binary files /dev/null and b/tests/test_graphical_units/control_data/transform/FadeTransform_TargetIsEmpty_FadesOutInPlace.npz differ
diff --git a/tests/test_graphical_units/control_data/transform_matching_parts/TransformMatchingTex.npz b/tests/test_graphical_units/control_data/transform_matching_parts/TransformMatchingTex.npz
new file mode 100644
Binary files /dev/null and b/tests/test_graphical_units/control_data/transform_matching_parts/TransformMatchingTex.npz differ
diff --git a/tests/test_graphical_units/control_data/transform_matching_parts/TransformMatchingTex_FadeTransformMismatches.npz b/tests/test_graphical_units/control_data/transform_matching_parts/TransformMatchingTex_FadeTransformMismatches.npz
new file mode 100644
Binary files /dev/null and b/tests/test_graphical_units/control_data/transform_matching_parts/TransformMatchingTex_FadeTransformMismatches.npz differ
diff --git a/tests/test_graphical_units/control_data/transform_matching_parts/TransformMatchingTex_FadeTransformMismatches_NothingToFade.npz b/tests/test_graphical_units/control_data/transform_matching_parts/TransformMatchingTex_FadeTransformMismatches_NothingToFade.npz
new file mode 100644
Binary files /dev/null and b/tests/test_graphical_units/control_data/transform_matching_parts/TransformMatchingTex_FadeTransformMismatches_NothingToFade.npz differ
diff --git a/tests/test_graphical_units/control_data/transform_matching_parts/TransformMatchingTex_TransformMismatches.npz b/tests/test_graphical_units/control_data/transform_matching_parts/TransformMatchingTex_TransformMismatches.npz
new file mode 100644
Binary files /dev/null and b/tests/test_graphical_units/control_data/transform_matching_parts/TransformMatchingTex_TransformMismatches.npz differ
diff --git a/tests/test_graphical_units/test_transform.py b/tests/test_graphical_units/test_transform.py
--- a/tests/test_graphical_units/test_transform.py
+++ b/tests/test_graphical_units/test_transform.py
@@ -199,3 +199,40 @@ def test_TransformWithConflictingPaths(scene):
             path_arc_centers=ORIGIN,
         )
     )
+
+
+@frames_comparison(last_frame=False)
+def test_FadeTransformPieces(scene):
+    src = VGroup(Square(), Circle().shift(LEFT + UP))
+    src.shift(3 * LEFT)
+
+    target = VGroup(Circle(), Triangle().shift(RIGHT + DOWN))
+    target.shift(3 * RIGHT)
+
+    scene.add(src)
+    scene.play(FadeTransformPieces(src, target))
+
+
+@frames_comparison(last_frame=False)
+def test_FadeTransform(scene):
+    src = Square(fill_opacity=1.0)
+    src.shift(3 * LEFT)
+
+    target = Circle(fill_opacity=1.0, color=ORANGE)
+    target.shift(3 * RIGHT)
+
+    scene.add(src)
+    scene.play(FadeTransform(src, target))
+
+
+@frames_comparison(last_frame=False)
+def test_FadeTransform_TargetIsEmpty_FadesOutInPlace(scene):
+    # https://github.com/ManimCommunity/manim/issues/2845
+    src = Square(fill_opacity=1.0)
+    src.shift(3 * LEFT)
+
+    target = VGroup()
+    target.shift(3 * RIGHT)
+
+    scene.add(src)
+    scene.play(FadeTransform(src, target))
diff --git a/tests/test_graphical_units/test_transform_matching_parts.py b/tests/test_graphical_units/test_transform_matching_parts.py
--- a/tests/test_graphical_units/test_transform_matching_parts.py
+++ b/tests/test_graphical_units/test_transform_matching_parts.py
@@ -25,3 +25,40 @@ def test_TransformMatchingDisplaysCorrect(scene):
     scene.wait(0.5)
     # Shift to make sure object isn't duplicated if moved
     scene.play(circle.animate.shift(DOWN))
+
+
+@frames_comparison(last_frame=False)
+def test_TransformMatchingTex(scene):
+    start = MathTex("A", "+", "B", "=", "C")
+    end = MathTex("C", "=", "B", "-", "A")
+
+    scene.add(start)
+    scene.play(TransformMatchingTex(start, end))
+
+
+@frames_comparison(last_frame=False)
+def test_TransformMatchingTex_FadeTransformMismatches(scene):
+    start = MathTex("A", "+", "B", "=", "C")
+    end = MathTex("C", "=", "B", "-", "A")
+
+    scene.add(start)
+    scene.play(TransformMatchingTex(start, end, fade_transform_mismatches=True))
+
+
+@frames_comparison(last_frame=False)
+def test_TransformMatchingTex_TransformMismatches(scene):
+    start = MathTex("A", "+", "B", "=", "C")
+    end = MathTex("C", "=", "B", "-", "A")
+
+    scene.add(start)
+    scene.play(TransformMatchingTex(start, end, transform_mismatches=True))
+
+
+@frames_comparison(last_frame=False)
+def test_TransformMatchingTex_FadeTransformMismatches_NothingToFade(scene):
+    # https://github.com/ManimCommunity/manim/issues/2845
+    start = MathTex("A", r"\to", "B")
+    end = MathTex("B", r"\to", "A")
+
+    scene.add(start)
+    scene.play(TransformMatchingTex(start, end, fade_transform_mismatches=True))