Repository: ManimCommunity/manim
Pull request: Add Cutout to geometry - fix VMobject().force_direction()
## List of Changes
Added a class that allows for making shapes with an arbitrary number of cutouts. See the example in the documentation.
Also fixed a really sneaky bug in VMobject().force_direction().

## Motivation
The code is trivial yet the resulting class is very useful for a whole number of applications.

## Other comments
~Also once the docs are built properly~ Docs are looking good. I'd like some feedback on naming the class. I'm not sure `Cutout` is the best choice. Also I'll have to add this and my previous PRs to the changenotes.

Problem statement:
Shapes with cutouts
Another feature request I'll write down here to take care of eventually myself, probably after `ArcPolygon`. While I was working on the Hadwiger-Nelson problem I created the highly complex class `ExclusionZone`, and while I was worrying that their appearance of a roughly annulus shaped zone with a central cutout wouldn't render nicely, I did in fact find manim to be able to render them perfectly. Using `Write` on these zones causes first the outer boundary to be drawn, then the inner boundary, then the area in between is filled out. A class to basically cut one area out from another bigger would be handy to have, and should only require a miniscule amount of code.

Hints from issue discussion:

Commit message: Add Cutout to geometry - fix VMobject().force_direction()

Patch to review:
diff --git a/manim/mobject/geometry.py b/manim/mobject/geometry.py
index 3d43adc472..3441593043 100644
--- a/manim/mobject/geometry.py
+++ b/manim/mobject/geometry.py
@@ -53,6 +53,7 @@ def construct(self):
     "Rectangle",
     "Square",
     "RoundedRectangle",
+    "Cutout",
 ]
 
 import warnings
@@ -1390,3 +1391,42 @@ def __init__(self, **kwargs):
 class ArrowSquareFilledTip(ArrowFilledTip, ArrowSquareTip):
     r"""Square arrow tip with filled tip."""
     pass
+
+
+class Cutout(VMobject):
+    """A shape with smaller cutouts.
+
+    Parameters
+    ----------
+    main_shape : :class:`~.VMobject`
+        The primary shape from which cutouts are made.
+    mobjects : :class:`~.VMobject`
+        The smaller shapes which are to be cut out of the main_shape.
+    kwargs
+        Further keyword arguments that are passed to the constructor of
+        :class:`~.VMobject`.
+
+    Examples
+    --------
+    .. manim:: CutoutExample
+
+        class CutoutExample(Scene):
+            def construct(self):
+                s1=Square().scale(2.5)
+                s2=Triangle().shift(DOWN+RIGHT).scale(0.5)
+                s3=Square().shift(UP+RIGHT).scale(0.5)
+                s4=RegularPolygon(5).shift(DOWN+LEFT).scale(0.5)
+                s5=RegularPolygon(6).shift(UP+LEFT).scale(0.5)
+                c=Cutout(s1,s2,s3,s4,s5,fill_opacity=1,color=BLUE,stroke_color=RED)
+                self.play(Write(c),run_time=4)
+                self.wait()
+    """
+    def __init__(self, main_shape, *mobjects, **kwargs):
+        VMobject.__init__(self, **kwargs)
+        self.append_points(main_shape.get_points())
+        if main_shape.get_direction()=="CW":
+            sub_direction="CCW"
+        else:
+            sub_direction="CW"
+        for mobject in mobjects:
+            self.append_points(mobject.force_direction(sub_direction).get_points())
diff --git a/manim/mobject/types/vectorized_mobject.py b/manim/mobject/types/vectorized_mobject.py
index 74ae2cbd11..fb51c541bf 100644
--- a/manim/mobject/types/vectorized_mobject.py
+++ b/manim/mobject/types/vectorized_mobject.py
@@ -940,7 +940,7 @@ def force_direction(self, target_direction):
             # Since we already assured the input is CW or CCW,
             # and the directions don't match, we just reverse
             self.reverse_direction()
-            return self
+        return self
 
 
 class VGroup(VMobject):


Code review comments:
diff_hunk: @@ -1390,3 +1391,42 @@ def __init__(self, **kwargs):
 class ArrowSquareFilledTip(ArrowFilledTip, ArrowSquareTip):
     r"""Square arrow tip with filled tip."""
     pass
+
+
+class Cutout(VMobject):
+    """A shape with smaller cutouts.
+
+    Parameters
+    ----------
+    main_shape : :class:`~.VMobject`
+        The primary shape from which cutouts are made.
+    mobjects : :class:`~.VMobject`
+        The smaller shapes which are to be cut out of the main_shape.
+    kwargs
+        Further keyword arguments that are passed to the constructor of
+        :class:`~.VMobject`.
+
+    Examples
+    --------
+    .. manim:: CutoutExample
+
+        class CutoutExample(Scene):
+            def construct(self):
+                s1=Square().scale(2.5)
+                s2=Triangle().shift(DOWN+RIGHT).scale(0.5)
+                s3=Square().shift(UP+RIGHT).scale(0.5)
+                s4=RegularPolygon(5).shift(DOWN+LEFT).scale(0.5)
+                s5=RegularPolygon(6).shift(UP+LEFT).scale(0.5)
+                c=Cutout(s1,s2,s3,s4,s5,fill_opacity=1,color=BLUE,stroke_color=RED)
+                self.play(Write(c),run_time=4)
+                self.wait()
+    """
path: manim/mobject/geometry.py
text: @user1:
```suggestion
        class CutoutExample(Scene):
            def construct(self):
                s1 = Square().scale(2.5)
                s2 = Triangle().shift(DOWN + RIGHT).scale(0.5)
                s3 = Square().shift(UP + RIGHT).scale(0.5)
                s4 = RegularPolygon(5).shift(DOWN + LEFT).scale(0.5)
                s5 = RegularPolygon(6).shift(UP + LEFT).scale(0.5)
                c = Cutout(s1, s2, s3, s4, s5, fill_opacity=1, color=BLUE, stroke_color=RED)
                self.play(Write(c), run_time=4)
                self.wait()
    """
```