Repository: ManimCommunity/manim
Pull request: Created a more accessible way to create Angles with line.py angle function - :meth:`.Angle.from_three_points`
<!-- Thank you for contributing to Manim! Learn more about the process in our contributing guidelines: https://docs.manim.community/en/latest/contributing.html -->

## Overview: What does this pull request change?
<!-- If there is more information than the PR title that should be added to our release changelog, add it in the following changelog section. This is optional, but recommended for larger pull requests. -->
<!--changelog-start-->

<!--changelog-end-->
Added a function to create an angle object with three points.
## Motivation and Explanation: Why and how do your changes improve the library?
<!-- Optional for bugfixes, small enhancements, and documentation-related PRs. Otherwise, please give a short reasoning for your changes. -->
Simplifies creating an angle and provides easy and accessible usage.
## Links to added or changed documentation pages
<!-- Please add links to the affected documentation pages (edit the description after opening the PR). The link to the documentation for your PR is https://manimce--####.org.readthedocs.build/en/####/, where #### represents the PR number. -->

https://manimce--2684.org.readthedocs.build/en/2684/reference/manim.mobject.geometry.line.Angle.html?highlight=angle#anglefromthreepointsbasicexample

## Further Information and Comments
<!-- If applicable, put further comments for the reviewers here. -->
Documentation has not been created yet. We are looking to add it, but do not quite know how.


<!-- Thank you again for contributing! Do not modify the lines below, they are for reviewers. -->
## Reviewer Checklist
- [X] The PR title is descriptive enough for the changelog, and the PR is labeled correctly
- [X] If applicable: newly added non-private functions and classes have a docstring including a short summary and a PARAMETERS section
- [X] If applicable: newly added functions and classes are tested


Problem statement:
More accessible way to create Angles
## Description of proposed feature
<!-- Add a clear and concise description of the new feature, including a motivation: why do you think this will be useful? -->

The current way of creating `Angle` mobjects is somewhat clunky and could be improved. One way of doing so would be the introduction of a new static method `Angle.from_three_points` which takes in three points and creates one of the two possible angles.

## How can the new feature be used?
<!-- If possible, illustrate how this new feature could be used. -->

```py
A = np.array(...)
B = np.array(...)
C = np.array(...)
ang = Angle.from_three_points(A, B, C)
```

## Additional comments
<!-- Add further context that you think might be relevant. -->

The implementation might look something like this:
```py
@staticmethod
def from_three_points(A, B, C, **kwargs):
            return Angle(Line(B, A), Line(B, C), **kwargs)
```
The choice of end points (A and C) *should* make sure that as long as the `quadrant` is not modified, the correct angle (or its complement) are constructed. Documentation and tests have to be written though.

Hints from issue discussion:
I can tackle this with @jbalpert !!!
Where would this function actually get defined? In the line.py file? @behackl 
> Where would this function actually get defined? In the line.py file? @behackl

Go ahead! Yes, in `line.py`, as a member of the `Angle` class.
We have implemented the member function and have wrote a pytest for it. Everything looks good! We are not totally sure where to write documentation for this function though, where are these documentation files located? @behackl

Commit message: added additional example

Patch to review:
diff --git a/manim/mobject/geometry/line.py b/manim/mobject/geometry/line.py
index 76e17cc288..fead180c1a 100644
--- a/manim/mobject/geometry/line.py
+++ b/manim/mobject/geometry/line.py
@@ -1015,6 +1015,53 @@ def construct(self):
             return self.angle_value / DEGREES
         return self.angle_value
 
+    @staticmethod
+    def from_three_points(
+        A: np.ndarray, B: np.ndarray, C: np.ndarray, **kwargs
+    ) -> Angle:
+        """The angle between the lines AB and BC.
+
+        This constructs the angle :math:`\angle ABC`.
+
+        Parameters
+        ----------
+        A
+            The endpoint of the first angle leg
+        B
+            The vertex of the angle
+        C
+            The endpoint of the second angle leg
+
+        **kwargs
+            Further keyword arguments are passed to :class:`.Angle`
+
+        Returns
+        -------
+        The Angle calculated from the three points
+
+                    Angle(line1, line2, radius=0.5, quadrant=(-1,1), stroke_width=8),
+                    Angle(line1, line2, radius=0.7, quadrant=(-1,-1), color=RED, other_angle=True),
+
+        Examples
+        --------
+        .. manim:: AngleFromThreePointsBasicExample
+            :save_last_frame:
+
+            class AngleFromThreePointsBasicExample(Scene):
+                def construct(self):
+                    right_angle = Angle.from_three_points(UP, ORIGIN, RIGHT)
+                    self.add(right_angle)
+
+        .. manim:: AngleFromThreePointsRedExample
+            :save_last_frame:
+
+            class AngleFromThreePointsRedExample(Scene):
+                def construct(self):
+                    right_angle = Angle.from_three_points(LEFT + UP, ORIGIN, RIGHT, stroke_width=8, color=RED)
+                    self.add(right_angle)
+        """
+        return Angle(Line(B, A), Line(B, C), **kwargs)
+
 
 class RightAngle(Angle):
     """An elbow-type mobject representing a right angle between two lines.
diff --git a/tests/test_graphical_units/control_data/geometry/three_points_Angle.npz b/tests/test_graphical_units/control_data/geometry/three_points_Angle.npz
new file mode 100644
index 0000000000..a084129e8f
Binary files /dev/null and b/tests/test_graphical_units/control_data/geometry/three_points_Angle.npz differ
diff --git a/tests/test_graphical_units/test_geometry.py b/tests/test_graphical_units/test_geometry.py
index 51e6817d74..6f6ab72e13 100644
--- a/tests/test_graphical_units/test_geometry.py
+++ b/tests/test_graphical_units/test_geometry.py
@@ -168,6 +168,35 @@ def test_Angle(scene):
     scene.add(a)
 
 
+@frames_comparison
+def test_three_points_Angle(scene):
+    # acute angle
+    acute = Angle.from_three_points(
+        np.array([10, 0, 0]), np.array([0, 0, 0]), np.array([10, 10, 0])
+    )
+    # obtuse angle
+    obtuse = Angle.from_three_points(
+        np.array([-10, 1, 0]), np.array([0, 0, 0]), np.array([10, 1, 0])
+    )
+    # quadrant 1 angle
+    q1 = Angle.from_three_points(
+        np.array([10, 10, 0]), np.array([0, 0, 0]), np.array([10, 1, 0])
+    )
+    # quadrant 2 angle
+    q2 = Angle.from_three_points(
+        np.array([-10, 1, 0]), np.array([0, 0, 0]), np.array([-1, 10, 0])
+    )
+    # quadrant 3 angle
+    q3 = Angle.from_three_points(
+        np.array([-10, -1, 0]), np.array([0, 0, 0]), np.array([-1, -10, 0])
+    )
+    # quadrant 4 angle
+    q4 = Angle.from_three_points(
+        np.array([10, -1, 0]), np.array([0, 0, 0]), np.array([1, -10, 0])
+    )
+    scene.add(VGroup(acute, obtuse, q1, q2, q3, q4).arrange(RIGHT))
+
+
 @frames_comparison
 def test_RightAngle(scene):
     l1 = Line(ORIGIN, RIGHT)


Code review comments:
diff_hunk: @@ -1015,6 +1015,53 @@ def construct(self):
             return self.angle_value / DEGREES
         return self.angle_value
 
+    @staticmethod
+    def from_three_points(
+        A: np.ndarray, B: np.ndarray, C: np.ndarray, **kwargs
+    ) -> Angle:
+        """The angle between the lines AB and BC.
+
+        This constructs the angle :math:`\angle ABC`.
+
+        Parameters
+        ----------
+        A
+            The endpoint of the first angle leg
+        B
+            The vertex of the angle
+        C
+            The endpoint of the second angle leg
+
+        **kwargs
+            Further keyword arguments are passed to :class:`.Angle`
+
+        Returns
+        -------
+        The Angle calculated from the three points
+
+                    Angle(line1, line2, radius=0.5, quadrant=(-1,1), stroke_width=8),
+                    Angle(line1, line2, radius=0.7, quadrant=(-1,-1), color=RED, other_angle=True),
+
+        Examples
+        --------
+        .. manim:: AngleFromThreePointsBasicExample
+            :save_last_frame:
+
+            class AngleFromThreePointsBasicExample(Scene):
+                def construct(self):
+                    right_angle = Angle.from_three_points(UP, ORIGIN, RIGHT)
+                    self.add(right_angle)
+
+        .. manim:: AngleFromThreePointsRedExample
+            :save_last_frame:
+
+            class AngleFromThreePointsRedExample(Scene):
+                def construct(self):
+                    right_angle = Angle.from_three_points(LEFT + UP, ORIGIN, RIGHT, stroke_width=8, color=RED)
+                    self.add(right_angle)
path: manim/mobject/geometry/line.py
text: @user1:
I don't think we need to separate examples, one example with two (or more) angles added is sufficient in my opinion. The color is a good way of holding them apart; the angle is again not a right angle though, so I'd suggest renaming it to `red_angle`, for example.

@author:
Would these be two separate classes, or within the same class?

@user1:
Both within the same class. Just create two different mobjects and add them both to the scene; almost like in the test.

@author:
Also, would I use Vgroup like
```self.add(VGroup(sample_angle, red_angle).arrange(RIGHT)) ```


@author:
or alternatively,

```
                    sample_angle = Angle.from_three_points(UP, ORIGIN, RIGHT, other_angle=True)
                    red_angle = Angle.from_three_points(LEFT + UP, ORIGIN, RIGHT, stroke_width=8, color=RED)
                    self.add(sample_angle)
                    self.add(red_angle)
```

@user1:
Personally, I would for the sake of simplicity go for
```py
sample_angle = Angle.from_three_points(...)
red_angle = Angle.from_three_points(...)
self.add(sample_angle, red_angle)
```
where the two angles are constructed in a way such that the vertex of the angle is not the same point for both.

@author:
I just updated the angles and rendered them and they are easily distinguishable from each other!

diff_hunk: @@ -1015,6 +1015,53 @@ def construct(self):
             return self.angle_value / DEGREES
         return self.angle_value
 
+    @staticmethod
+    def from_three_points(
+        A: np.ndarray, B: np.ndarray, C: np.ndarray, **kwargs
+    ) -> Angle:
+        """The angle between the lines AB and BC.
+
+        This constructs the angle :math:`\angle ABC`.
+
+        Parameters
+        ----------
+        A
+            The endpoint of the first angle leg
+        B
+            The vertex of the angle
+        C
+            The endpoint of the second angle leg
+
+        **kwargs
+            Further keyword arguments are passed to :class:`.Angle`
+
+        Returns
+        -------
+        The Angle calculated from the three points
+
+                    Angle(line1, line2, radius=0.5, quadrant=(-1,1), stroke_width=8),
+                    Angle(line1, line2, radius=0.7, quadrant=(-1,-1), color=RED, other_angle=True),
+
+        Examples
+        --------
+        .. manim:: AngleFromThreePointsBasicExample
+            :save_last_frame:
+
+            class AngleFromThreePointsBasicExample(Scene):
+                def construct(self):
+                    right_angle = Angle.from_three_points(UP, ORIGIN, RIGHT)
path: manim/mobject/geometry/line.py
text: @user1:
Looking at the rendered output, https://manimce--2684.org.readthedocs.build/en/2684/reference/manim.mobject.geometry.line.Angle.html?highlight=angle#anglefromthreepointsbasicexample, the angle only is a "right angle" in *some* sense. Either try changing the order of the legs (`RIGHT, ORIGIN, UP`), or additionally pass `other_angle=True`

@author:
Sounds good! Changed the name!