Repository: ManimCommunity/manim

Issue:
Transform: missing documentation of path_arc_centers keyword argument
## Description of bug / unexpected behavior
<!-- Add a clear and concise description of the problem you encountered. -->
The `Transform` constructor accepts a keyword argument, `path_arc_centers`, which is only being used once and this use is not working as intended.

Inspecting `Transform`'s `__init__` code, at line 64, there's the following code:

```python
if self.path_arc_centers is not None:
    self.path_func = path_along_circles(
        path_arc,
        self.path_arc_centers,
        self.path_arc_axis,
    )

self.path_func: Callable | None = path_func
```

As it can be seen, if `self.path_arc_centers` is not None, it is used to construct a path function which is assigned to `self.path_func`. But inmediately after this, `self.path_func` is assigned the `path_func` keyword argument in all cases, overwriting any possibly stored value in `self.path_func`.

Thus, this `path_arc_centers` keyword argument is not working as intended, and as mentioned before, it's not being used anywhere else inside `Transform`'s definition.

## Expected behavior
<!-- Add a clear and concise description of what you expected to happen. -->

## How to reproduce the issue
<!-- Provide a piece of code illustrating the undesired behavior. -->

## Additional media files

## Logs

## System specifications

## Additional comments
<!-- Add further context that you think might be relevant for this issue here. -->



Discussion:
Contrary to what I thought before (and also contrary to any sort of reasonable coding practice), this is actually wrong. When we discussed this earlier, I missed that `path_func` is actually a property whose setter checks whether the value that is being assigned is `None` (and if so, the actual function is not changed).

So actually, if `path_arc_centers` is passed, a "proper" `path_func` is constructed and assigned (which the setter also does properly, as not-`None` is passed), then `self.path_func = None` is run and the setter actually *does not* change the value of the internal attribute.

Which means that this is not a bug, but just something that needs to be documented better (and I actually also think that changing the two blocks would not hurt either). I actually played around with the `path_arc_centers` argument briefly, but I'm still not entirely sure how exactly it should be used.

Resolving patch:
diff --git a/manim/animation/transform.py b/manim/animation/transform.py
--- a/manim/animation/transform.py
+++ b/manim/animation/transform.py
@@ -46,6 +46,36 @@
 
 
 class Transform(Animation):
+    """A Transform transforms a Mobject into a target Mobject.
+
+    Parameters
+    ----------
+    mobject
+        The :class:`.Mobject` to be transformed. It will be mutated to become the ``target_mobject``.
+    target_mobject
+        The target of the transformation.
+    path_func
+        A function defining the path that the points of the ``mobject`` are being moved
+        along until they match the points of the ``target_mobject``, see :mod:`.utils.paths`.
+    path_arc
+        The arc angle (in radians) that the points of ``mobject`` will follow to reach
+        the points of the target if using a circular path arc, see ``path_arc_centers``.
+    path_arc_axis
+        The axis to rotate along if using a circular path arc, see ``path_arc_centers``.
+    path_arc_centers
+        The center of the circular arcs along which the points of ``mobject`` are
+        moved by the transformation.
+
+        If this is set and ``path_func`` is not set, then a ``path_along_circles`` path will be generated
+        using the ``path_arc`` parameters and stored in ``path_func``. If ``path_func`` is set, this and the
+        other ``path_arc`` fields are set as attributes, but a ``path_func`` is not generated from it.
+    replace_mobject_with_target_in_scene
+        Controls which mobject is replaced when the transformation is complete.
+
+        If set to True, ``mobject`` will be removed from the scene and ``target_mobject`` will
+        replace it. Otherwise, ``target_mobject`` is never added and ``mobject`` just takes its shape.
+    """
+
     def __init__(
         self,
         mobject: Mobject | None,
@@ -61,14 +91,16 @@ def __init__(
         self.path_arc_centers: np.ndarray = path_arc_centers
         self.path_arc: float = path_arc
 
-        if self.path_arc_centers is not None:
+        # path_func is a property a few lines below so it doesn't need to be set in any case
+        if path_func is not None:
+            self.path_func: Callable = path_func
+        elif self.path_arc_centers is not None:
             self.path_func = path_along_circles(
                 path_arc,
                 self.path_arc_centers,
                 self.path_arc_axis,
             )
 
-        self.path_func: Callable | None = path_func
         self.replace_mobject_with_target_in_scene: bool = (
             replace_mobject_with_target_in_scene
         )


Test patch:
diff --git a/tests/test_graphical_units/control_data/transform/TransformWithConflictingPaths.npz b/tests/test_graphical_units/control_data/transform/TransformWithConflictingPaths.npz
new file mode 100644
Binary files /dev/null and b/tests/test_graphical_units/control_data/transform/TransformWithConflictingPaths.npz differ
diff --git a/tests/test_graphical_units/control_data/transform/TransformWithPathArcCenters.npz b/tests/test_graphical_units/control_data/transform/TransformWithPathArcCenters.npz
new file mode 100644
Binary files /dev/null and b/tests/test_graphical_units/control_data/transform/TransformWithPathArcCenters.npz differ
diff --git a/tests/test_graphical_units/control_data/transform/TransformWithPathFunc.npz b/tests/test_graphical_units/control_data/transform/TransformWithPathFunc.npz
new file mode 100644
Binary files /dev/null and b/tests/test_graphical_units/control_data/transform/TransformWithPathFunc.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
@@ -163,3 +163,39 @@ def test_ReplacementTransform(scene):
     v2 = Vector()
     v3 = Line()
     scene.play(ReplacementTransform(VGroup(v1, v2), v3))
+
+
+@frames_comparison(last_frame=False)
+def test_TransformWithPathFunc(scene):
+    dots_start = VGroup(*[Dot(LEFT, color=BLUE), Dot(3 * RIGHT, color=RED)])
+    dots_end = VGroup(*[Dot(LEFT + 2 * DOWN, color=BLUE), Dot(2 * UP, color=RED)])
+    scene.play(Transform(dots_start, dots_end, path_func=clockwise_path()))
+
+
+@frames_comparison(last_frame=False)
+def test_TransformWithPathArcCenters(scene):
+    dots_start = VGroup(*[Dot(LEFT, color=BLUE), Dot(3 * RIGHT, color=RED)])
+    dots_end = VGroup(*[Dot(LEFT + 2 * DOWN, color=BLUE), Dot(2 * UP, color=RED)])
+    scene.play(
+        Transform(
+            dots_start,
+            dots_end,
+            path_arc=2 * PI,
+            path_arc_centers=ORIGIN,
+        )
+    )
+
+
+@frames_comparison(last_frame=False)
+def test_TransformWithConflictingPaths(scene):
+    dots_start = VGroup(*[Dot(LEFT, color=BLUE), Dot(3 * RIGHT, color=RED)])
+    dots_end = VGroup(*[Dot(LEFT + 2 * DOWN, color=BLUE), Dot(2 * UP, color=RED)])
+    scene.play(
+        Transform(
+            dots_start,
+            dots_end,
+            path_func=clockwise_path(),
+            path_arc=2 * PI,
+            path_arc_centers=ORIGIN,
+        )
+    )