diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000000..d4f347b2dc --- /dev/null +++ b/.clang-format @@ -0,0 +1 @@ +BasedOnStyle: Microsoft diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index beb076dfba..cd4bd4dbae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-22.04, macos-13, windows-latest] - python: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python: ["3.10", "3.11", "3.12", "3.13"] steps: - name: Checkout the repository diff --git a/README.md b/README.md index a909e5a1df..bfc4ff38e0 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Manim is an animation engine for explanatory math videos. It's used to create pr ## Installation -> [!CAUTION] +> [!WARNING] > These instructions are for the community version _only_. Trying to use these instructions to install [3b1b/manim](https://github.com/3b1b/manim) or instructions there to install this version will cause problems. Read [this](https://docs.manim.community/en/stable/faq/installation.html#why-are-there-different-versions-of-manim) and decide which version you wish to install, then only follow the instructions for your desired version. Manim requires a few dependencies that must be installed prior to using it. If you @@ -73,7 +73,7 @@ In order to view the output of this scene, save the code in a file called `examp manim -p -ql example.py SquareToCircle ``` -You should see your native video player program pop up and play a simple scene in which a square is transformed into a circle. You may find some more simple examples within this +You should see a window pop up and play a simple scene in which a square is transformed into a circle. You may find some more simple examples within this [GitHub repository](example_scenes). You can also visit the [official gallery](https://docs.manim.community/en/stable/examples.html) for more advanced examples. Manim also ships with a `%%manim` IPython magic which allows to use it conveniently in JupyterLab (as well as classic Jupyter) notebooks. See the @@ -86,13 +86,15 @@ The general usage of Manim is as follows: ![manim-illustration](https://raw.githubusercontent.com/ManimCommunity/manim/main/docs/source/_static/command.png) -The `-p` flag in the command above is for previewing, meaning the video file will automatically open when it is done rendering. The `-ql` flag is for a faster rendering at a lower quality. +The `-p` flag in the command above is for previewing, meaning a window will show up to render it in real time. +The `-ql` flag is for a faster rendering at a lower quality. Some other useful flags include: - `-s` to skip to the end and just show the final frame. - `-n ` to skip ahead to the `n`'th animation of a scene. - `-f` show the file in the file browser. +- `-w` to actually write the result into a video file. For a thorough list of command line arguments, visit the [documentation](https://docs.manim.community/en/stable/guides/configuration.html). diff --git a/docs/i18n/gettext/reference/manim.mobject.three_d.three_dimensions.Line3D.pot b/docs/i18n/gettext/reference/manim.mobject.three_d.three_dimensions.Line3D.pot index 45444fe0a6..7ba794a3d0 100644 --- a/docs/i18n/gettext/reference/manim.mobject.three_d.three_dimensions.Line3D.pot +++ b/docs/i18n/gettext/reference/manim.mobject.three_d.three_dimensions.Line3D.pot @@ -19,7 +19,7 @@ msgid "Bases: :py:class:`manim.mobject.three_d.three_dimensions.Cylinder`" msgstr "" #: ../../../manim/mobject/three_d/three_dimensions.py:docstring of manim.mobject.three_d.three_dimensions.Line3D:1 -msgid "A cylindrical line, for use in ThreeDScene." +msgid "A cylindrical line." msgstr "" #: ../../../manim/mobject/three_d/three_dimensions.py:docstring of manim.mobject.three_d.three_dimensions.Line3D:4 diff --git a/docs/i18n/gettext/reference/manim.mobject.three_d.three_dimensions.pot b/docs/i18n/gettext/reference/manim.mobject.three_d.three_dimensions.pot index 66e4bc3e93..3ee6e0a815 100644 --- a/docs/i18n/gettext/reference/manim.mobject.three_d.three_dimensions.pot +++ b/docs/i18n/gettext/reference/manim.mobject.three_d.three_dimensions.pot @@ -35,7 +35,7 @@ msgid "A spherical dot." msgstr "" #: ../../source/reference/manim.mobject.three_d.three_dimensions.rst:40::1 -msgid "A cylindrical line, for use in ThreeDScene." +msgid "A cylindrical line." msgstr "" #: ../../source/reference/manim.mobject.three_d.three_dimensions.rst:40::1 diff --git a/docs/i18n/gettext/reference/manim.scene.three_d_scene.SpecialThreeDScene.pot b/docs/i18n/gettext/reference/manim.scene.three_d_scene.SpecialThreeDScene.pot deleted file mode 100644 index 3c9400b6c9..0000000000 --- a/docs/i18n/gettext/reference/manim.scene.three_d_scene.SpecialThreeDScene.pot +++ /dev/null @@ -1,97 +0,0 @@ - -msgid "" -msgstr "" -"Project-Id-Version: Manim \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" - -#: ../../source/reference/manim.scene.three_d_scene.SpecialThreeDScene.rst:2 -msgid "SpecialThreeDScene" -msgstr "" - -#: ../../source/reference/manim.scene.three_d_scene.SpecialThreeDScene.rst:4 -msgid "Qualified name: ``manim.scene.three\\_d\\_scene.SpecialThreeDScene``" -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene:1 -msgid "Bases: :py:class:`manim.scene.three_d_scene.ThreeDScene`" -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene:1 -msgid "An extension of :class:`ThreeDScene` with more settings." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene:3 -msgid "It has some extra configuration for axes, spheres, and an override for low quality rendering. Further key differences are:" -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene:7 -msgid "The camera shades applicable 3DMobjects by default, except if rendering in low quality." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene:9 -msgid "Some default params for Spheres and Axes have been added." -msgstr "" - -#: ../../source/reference/manim.scene.three_d_scene.SpecialThreeDScene.rst:14 -msgid "Methods" -msgstr "" - -#: ../../source/reference/manim.scene.three_d_scene.SpecialThreeDScene.rst:23::1 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_axes:1 -msgid "Return a set of 3D axes." -msgstr "" - -#: ../../source/reference/manim.scene.three_d_scene.SpecialThreeDScene.rst:23::1 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_default_camera_position:1 -msgid "Returns the default_angled_camera position." -msgstr "" - -#: ../../source/reference/manim.scene.three_d_scene.SpecialThreeDScene.rst:23::1 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_sphere:1 -msgid "Returns a sphere with the passed keyword arguments as properties." -msgstr "" - -#: ../../source/reference/manim.scene.three_d_scene.SpecialThreeDScene.rst:23::1 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.set_camera_to_default_position:1 -msgid "Sets the camera to its default position." -msgstr "" - -#: ../../source/reference/manim.scene.three_d_scene.SpecialThreeDScene.rst:25 -msgid "Attributes" -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_axes:0 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_default_camera_position:0 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_sphere:0 -msgid "Returns" -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_axes:3 -msgid "A set of 3D axes." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_axes:0 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_default_camera_position:0 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_sphere:0 -msgid "Return type" -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_default_camera_position:3 -msgid "Dictionary of phi, theta, focal_distance, and gamma." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_sphere:0 -msgid "Parameters" -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_sphere:3 -msgid "Any valid parameter of :class:`~.Sphere` or :class:`~.Surface`." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_sphere:5 -msgid "The sphere object." -msgstr "" - - diff --git a/docs/i18n/gettext/reference/manim.scene.three_d_scene.ThreeDScene.pot b/docs/i18n/gettext/reference/manim.scene.three_d_scene.ThreeDScene.pot deleted file mode 100644 index 2a7d73eb70..0000000000 --- a/docs/i18n/gettext/reference/manim.scene.three_d_scene.ThreeDScene.pot +++ /dev/null @@ -1,210 +0,0 @@ - -msgid "" -msgstr "" -"Project-Id-Version: Manim \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" - -#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:2 -msgid "ThreeDScene" -msgstr "" - -#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:4 -msgid "Qualified name: ``manim.scene.three\\_d\\_scene.ThreeDScene``" -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene:1 -msgid "Bases: :py:class:`manim.scene.scene.Scene`" -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene:1 -msgid "This is a Scene, with special configurations and properties that make it suitable for Three Dimensional Scenes." -msgstr "" - -#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:14 -msgid "Methods" -msgstr "" - -#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:31::1 -msgid "This method is used to prevent the rotation and movement of mobjects as the camera moves around." -msgstr "" - -#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:31::1 -msgid "This method is used to prevent the rotation and tilting of mobjects as the camera moves around." -msgstr "" - -#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:31::1 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.begin_3dillusion_camera_rotation:1 -msgid "This method creates a 3D camera rotation illusion around the current camera orientation." -msgstr "" - -#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:31::1 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.begin_ambient_camera_rotation:1 -msgid "This method begins an ambient rotation of the camera about the Z_AXIS, in the anticlockwise direction" -msgstr "" - -#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:31::1 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.get_moving_mobjects:1 -msgid "This method returns a list of all of the Mobjects in the Scene that are moving, that are also in the animations passed." -msgstr "" - -#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:31::1 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.move_camera:1 -msgid "This method animates the movement of the camera to the given spherical coordinates." -msgstr "" - -#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:31::1 -msgid "This method undoes what add_fixed_in_frame_mobjects does." -msgstr "" - -#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:31::1 -msgid "This method \"unfixes\" the orientation of the mobjects passed, meaning they will no longer be at the same angle relative to the camera." -msgstr "" - -#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:31::1 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.set_camera_orientation:1 -msgid "This method sets the orientation of the camera in the scene." -msgstr "" - -#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:31::1 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.set_to_default_angled_camera_orientation:1 -msgid "This method sets the default_angled_camera_orientation to the keyword arguments passed, and sets the camera to that orientation." -msgstr "" - -#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:31::1 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.stop_3dillusion_camera_rotation:1 -msgid "This method stops all illusion camera rotations." -msgstr "" - -#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:31::1 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.stop_ambient_camera_rotation:1 -msgid "This method stops all ambient camera rotation." -msgstr "" - -#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:33 -msgid "Attributes" -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.add_fixed_in_frame_mobjects:1 -msgid "This method is used to prevent the rotation and movement of mobjects as the camera moves around. The mobject is essentially overlaid, and is not impacted by the camera's movement in any way." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.add_fixed_in_frame_mobjects:0 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.add_fixed_orientation_mobjects:0 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.begin_3dillusion_camera_rotation:0 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.begin_ambient_camera_rotation:0 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.get_moving_mobjects:0 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.move_camera:0 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.remove_fixed_in_frame_mobjects:0 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.remove_fixed_orientation_mobjects:0 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.set_camera_orientation:0 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.set_to_default_angled_camera_orientation:0 -msgid "Parameters" -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.add_fixed_in_frame_mobjects:6 -msgid "The Mobjects whose orientation must be fixed." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.add_fixed_orientation_mobjects:1 -msgid "This method is used to prevent the rotation and tilting of mobjects as the camera moves around. The mobject can still move in the x,y,z directions, but will always be at the angle (relative to the camera) that it was at when it was passed through this method.)" -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.add_fixed_orientation_mobjects:7 -msgid "The Mobject(s) whose orientation must be fixed." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.add_fixed_orientation_mobjects:9 -msgid "Some valid kwargs are use_static_center_func : bool center_func : function" -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.add_fixed_orientation_mobjects:11 -msgid "Some valid kwargs are" -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.add_fixed_orientation_mobjects:11 -msgid "use_static_center_func : bool center_func : function" -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.begin_3dillusion_camera_rotation:4 -msgid "The rate at which the camera rotation illusion should operate." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.begin_3dillusion_camera_rotation:5 -msgid "The polar angle the camera should move around. Defaults to the current phi angle." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.begin_3dillusion_camera_rotation:7 -msgid "The azimutal angle the camera should move around. Defaults to the current theta angle." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.begin_ambient_camera_rotation:4 -msgid "The rate at which the camera should rotate about the Z_AXIS. Negative rate means clockwise rotation." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.begin_ambient_camera_rotation:7 -msgid "one of 3 options: [\"theta\", \"phi\", \"gamma\"]. defaults to theta." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.get_moving_mobjects:4 -msgid "The animations whose mobjects will be checked." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.move_camera:4 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.set_camera_orientation:3 -msgid "The polar angle i.e the angle between Z_AXIS and Camera through ORIGIN in radians." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.move_camera:6 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.set_camera_orientation:5 -msgid "The azimuthal angle i.e the angle that spins the camera around the Z_AXIS." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.move_camera:8 -msgid "The radial focal_distance between ORIGIN and Camera." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.move_camera:10 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.set_camera_orientation:9 -msgid "The rotation of the camera about the vector from the ORIGIN to the Camera." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.move_camera:12 -msgid "The zoom factor of the camera." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.move_camera:14 -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.set_camera_orientation:13 -msgid "The new center of the camera frame in cartesian coordinates." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.move_camera:16 -msgid "Any other animations to be played at the same time." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.remove_fixed_in_frame_mobjects:1 -msgid "This method undoes what add_fixed_in_frame_mobjects does. It allows the mobject to be affected by the movement of the camera." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.remove_fixed_in_frame_mobjects:5 -msgid "The Mobjects whose position and orientation must be unfixed." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.remove_fixed_orientation_mobjects:1 -msgid "This method \"unfixes\" the orientation of the mobjects passed, meaning they will no longer be at the same angle relative to the camera. This only makes sense if the mobject was passed through add_fixed_orientation_mobjects first." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.remove_fixed_orientation_mobjects:6 -msgid "The Mobjects whose orientation must be unfixed." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.set_camera_orientation:7 -msgid "The focal_distance of the Camera." -msgstr "" - -#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.set_camera_orientation:11 -msgid "The zoom factor of the scene." -msgstr "" - - diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index f2afb6cfd8..06d126b080 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -9,6 +9,7 @@ releases since v0.18.0) are documented on our .. toctree:: + changelog/experimental changelog/0.18.0-changelog changelog/0.17.3-changelog changelog/0.17.2-changelog diff --git a/docs/source/changelog/experimental.md b/docs/source/changelog/experimental.md new file mode 100644 index 0000000000..13859e4e43 --- /dev/null +++ b/docs/source/changelog/experimental.md @@ -0,0 +1,97 @@ +# Migrating from v0.19.0 to v0.20.0 + +This constitutes a list of all the changes needed to migrate your code +to work with the latest version of Manim + +## Manager +If you ever used `Scene.render`, you must replace it with {class}`.Manager`. + +Original code: +```py +scene = SceneClass() +scene.render() +``` +should be changed to: +```py +manager = Manager(SceneClass) +manager.render() +``` + +If you are a plugin author that subclasses `Scene` and changed `Scene.render`, you should migrate +your code to use the specific public methods on {class}`.Manager` instead. + +## ThreeDScene and Camera +`ThreeDScene` has been completely removed, and all of its functionality has been replaced +with methods on {class}`.Camera`, which can be accessed via {attr}`.Scene.camera`. + +For example, the following code +```py +class MyScene(ThreeDScene): + def construct(self): + t = Text("Hello") + self.add_fixed_in_frame_mobjects(t) + self.begin_ambient_camera_rotation() + self.wait(3) +``` +should be changed to +```py +# change ThreeDScene -> Scene +class MyScene(Scene): + def construct(self): + t = Text("Hello") + # add_fixed_in_frame_mobjects() no longer exists. + # Now you must use Mobject.fix_in_frame() manually for each Mobject. + t.fix_in_frame() + self.add(t) + + # access the method on the camera + self.camera.begin_ambient_rotation() + self.add(self.camera) + self.wait(3) +``` + +## Animation +`Animation.interpolate_mobject` has been combined into `Animation.interpolate`. + +Methods `Animation._setup_scene` and `Animation.clean_up_from_scene` have been removed +in favor of `Animation.begin` and `Animation.finish`. If you need to access the scene, +you can use a simple buffer to communicate. Note that this buffer cannot access +methods on the {class}`.Scene`, but can only do basic actions like {meth}`.Scene.add`, +{meth}`.Scene.remove`, and {meth}`.Scene.replace`. + +For example, the following code: +```py +class MyAnimation(Animation): + def begin(self) -> None: + self._sqrs = VGroup(Square()) + + def _setup_scene(self, scene: Scene) -> None: + scene.add(self._sqr) + self.scene = scene + + def interpolate_mobject(self, alpha: float) -> None: + sqr = Square().move_to((alpha, 0, 0)) + self._sqrs.add(sqr) + self.scene.add(sqr) + + def clean_up_from_scene(self, scene: Scene) -> None: + scene.remove(self._sqrs) +``` + +should be changed to +```py +class MyAnimation(Animation): + def begin(self) -> None: + self._sqrs = VGroup(Square()) + self.buffer.add(self._sqrs) + + def interpolate(self, alpha: float) -> None: + sqr = Square().move_to((alpha, 0, 0)) + self._sqrs.add(sqr) + self.buffer.add(sqr) + # tell the scene to empty the buffer + self.apply_buffer = True + + def finish(self) -> None: + self.buffer.remove(self._sqrs) +``` diff --git a/docs/source/contributing/performance.rst b/docs/source/contributing/performance.rst index f16121c40c..09b3fa11e0 100644 --- a/docs/source/contributing/performance.rst +++ b/docs/source/contributing/performance.rst @@ -24,8 +24,8 @@ to the bottom of the file: .. code-block:: python with tempconfig({"quality": "medium_quality", "disable_caching": True}): - scene = SceneName() - scene.render() + manager = Manager(SceneName) + manager.render() Where ``SceneName`` is the name of the scene you want to run. You can then run the file directly, and can thus follow the instructions for most profilers. @@ -58,8 +58,8 @@ to ``square_to_circle.py``: with tempconfig({"quality": "medium_quality", "disable_caching": True}): - scene = SquareToCircle() - scene.render() + manager = Manager(SquareToCircle) + manager.render() Now run the following in the terminal: diff --git a/docs/source/contributing/testing.rst b/docs/source/contributing/testing.rst index dfc6a225f8..caddc5c463 100644 --- a/docs/source/contributing/testing.rst +++ b/docs/source/contributing/testing.rst @@ -213,11 +213,11 @@ The decorator can be used with or without parentheses. **By default, the test on circle = Circle() scene.play(Animation(circle)) -You can also specify, when needed, which base scene you need (ThreeDScene, for example) : +You can also specify, when needed, which base scene you need (VectorScene, for example) : .. code:: python - @frames_comparison(last_frame=False, base_scene=ThreeDScene) + @frames_comparison(last_frame=False, base_scene=VectorScene) def test_circle(scene): circle = Circle() scene.play(Animation(circle)) diff --git a/docs/source/examples.rst b/docs/source/examples.rst index 00b4299a52..7b3bef95f4 100644 --- a/docs/source/examples.rst +++ b/docs/source/examples.rst @@ -597,25 +597,25 @@ Special Camera Settings .. manim:: FixedInFrameMObjectTest :save_last_frame: - :ref_classes: ThreeDScene - :ref_methods: ThreeDScene.set_camera_orientation ThreeDScene.add_fixed_in_frame_mobjects + :ref_classes: Scene + :ref_methods: Camera.set_orientation OpenGLMobject.fix_in_frame - class FixedInFrameMObjectTest(ThreeDScene): + class FixedInFrameMObjectTest(Scene): def construct(self): axes = ThreeDAxes() - self.set_camera_orientation(phi=75 * DEGREES, theta=-45 * DEGREES) + self.camera.set_orientation(theta=-45 * DEGREES, phi=75 * DEGREES) text3d = Text("This is a 3D text") - self.add_fixed_in_frame_mobjects(text3d) + text3d.fix_in_frame() text3d.to_corner(UL) self.add(axes) self.wait() .. manim:: ThreeDLightSourcePosition :save_last_frame: - :ref_classes: ThreeDScene ThreeDAxes Surface - :ref_methods: ThreeDScene.set_camera_orientation + :ref_classes: Scene ThreeDAxes Surface + :ref_methods: Camera.set_orientation - class ThreeDLightSourcePosition(ThreeDScene): + class ThreeDLightSourcePosition(Scene): def construct(self): axes = ThreeDAxes() sphere = Surface( @@ -626,49 +626,57 @@ Special Camera Settings ]), v_range=[0, TAU], u_range=[-PI / 2, PI / 2], checkerboard_colors=[RED_D, RED_E], resolution=(15, 32) ) - self.renderer.camera.light_source.move_to(3*IN) # changes the source of the light - self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES) + # TODO: implement light source + self.camera.light_source.move_to(3*IN) # changes the source of the light + self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES) self.add(axes, sphere) .. manim:: ThreeDCameraRotation - :ref_classes: ThreeDScene ThreeDAxes - :ref_methods: ThreeDScene.begin_ambient_camera_rotation ThreeDScene.stop_ambient_camera_rotation + :ref_classes: Circle Scene ThreeDAxes + :ref_methods: Camera.begin_ambient_rotation Camera.stop_ambient_rotation - class ThreeDCameraRotation(ThreeDScene): + class ThreeDCameraRotation(Scene): def construct(self): axes = ThreeDAxes() - circle=Circle() - self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES) + circle = Circle() + self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES) self.add(circle,axes) - self.begin_ambient_camera_rotation(rate=0.1) + self.camera.begin_ambient_rotation(rate=0.1) + self.add(self.camera) self.wait() - self.stop_ambient_camera_rotation() - self.move_camera(phi=75 * DEGREES, theta=30 * DEGREES) + self.camera.stop_ambient_rotation() + self.play( + self.camera.animate.set_orientation( + theta=30 * DEGREES, phi=75 * DEGREES + ), + ) self.wait() -.. manim:: ThreeDCameraIllusionRotation - :ref_classes: ThreeDScene ThreeDAxes - :ref_methods: ThreeDScene.begin_3dillusion_camera_rotation ThreeDScene.stop_3dillusion_camera_rotation +.. manim:: ThreeDCameraPrecession + :ref_classes: Circle Scene ThreeDAxes + :ref_methods: Camera.begin_precession Camera.stop_precession - class ThreeDCameraIllusionRotation(ThreeDScene): + class ThreeDCameraPrecession(Scene): def construct(self): axes = ThreeDAxes() - circle=Circle() - self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES) + circle = Circle() + self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES) self.add(circle,axes) - self.begin_3dillusion_camera_rotation(rate=2) + self.camera.begin_precession(rate=2) + self.add(self.camera) self.wait(PI/2) - self.stop_3dillusion_camera_rotation() + self.camera.stop_precession() .. manim:: ThreeDSurfacePlot :save_last_frame: - :ref_classes: ThreeDScene Surface + :ref_classes: Scene Surface + :ref_methods: Camera.set_orientation - class ThreeDSurfacePlot(ThreeDScene): + class ThreeDSurfacePlot(Scene): def construct(self): resolution_fa = 24 - self.set_camera_orientation(phi=75 * DEGREES, theta=-30 * DEGREES) + self.camera.set_orientation(theta=-30 * DEGREES, phi=75 * DEGREES) def param_gauss(u, v): x = u diff --git a/docs/source/guides/configuration.rst b/docs/source/guides/configuration.rst index 17cdfecc90..8af5d230d4 100644 --- a/docs/source/guides/configuration.rst +++ b/docs/source/guides/configuration.rst @@ -357,10 +357,10 @@ A list of all config options 'log_dir', 'log_to_file', 'max_files_cached', 'media_dir', 'media_width', 'movie_file_extension', 'notify_outdated_version', 'output_file', 'partial_movie_dir', 'pixel_height', 'pixel_width', 'plugins', 'preview', - 'progress_bar', 'quality', 'right_side', 'save_as_gif', 'save_last_frame', - 'save_pngs', 'scene_names', 'show_in_file_browser', 'sound', 'tex_dir', + 'progress_bar', 'quality', 'right_side', 'save_last_frame', + 'scene_names', 'show_in_file_browser', 'sound', 'tex_dir', 'tex_template', 'tex_template_file', 'text_dir', 'top', 'transparent', - 'upto_animation_number', 'use_opengl_renderer', 'verbosity', 'video_dir', + 'upto_animation_number', 'verbosity', 'video_dir', 'window_position', 'window_monitor', 'window_size', 'write_all', 'write_to_movie', 'enable_wireframe', 'force_window'] diff --git a/docs/source/guides/deep_dive.rst b/docs/source/guides/deep_dive.rst index 3ea950230f..b8c0bbf111 100644 --- a/docs/source/guides/deep_dive.rst +++ b/docs/source/guides/deep_dive.rst @@ -1,11 +1,11 @@ A deep dive into Manim's internals ================================== -**Author:** `Benjamin Hackl `__ +**Authors:** `Benjamin Hackl `__ and `Aarush Deshpande `__ .. admonition:: Disclaimer - This guide reflects the state of the library as of version ``v0.16.0`` + This guide reflects the state of the library as of version ``v0.20.0`` and primarily treats the Cairo renderer. The situation in the latest version of Manim might be different; in case of substantial deviations we will add a note below. @@ -84,7 +84,7 @@ discussing the contents of the following chapters on a very high level. to prepare a scene for rendering; right until the point where the user-overridden ``construct`` method is ran. This includes a brief discussion on using Manim's CLI versus other means of rendering (e.g., via Jupyter notebooks, or in your Python - script by calling the :meth:`.Scene.render` method yourself). + script by calling the :meth:`.Manager.render` method yourself). - `Mobject Initialization`_: For the second chapter we dive into creating and handling Mobjects, the basic elements that should be displayed in our scene. We discuss the :class:`.Mobject` base class, how there are essentially @@ -107,6 +107,25 @@ discussing the contents of the following chapters on a very high level. :meth:`.Scene.construct` has been run, the library combines the partial movie files to one video. +.. hint:: + + As we move forward, try to keep in mind the responsibilities of every + class we introduce. We'll talk more about them in detail, but here's a brief + overview + + * :class:`.Scene` is responsible for managing the classes :class:`Mobject`, :class:`.Animation`, + and :class:`.Camera`. + + * :class:`.Manager` is responsible for coordinating the :class:`.Scene`, :class:`.Renderer`, + and :class:`.FileWriter`. + + * :class:`.FileWriter` is responsible for writing frames and partial movie files, as well + as combining them all into a final movie file. + + * :class:`.Renderer` is an abstract class which has to be subclassed. + It's job is to take information related to the :class:`.Camera`, and the mobjects + on the :class:`.Scene` at a certain frame, and to return the pixels in a frame. + And with that, let us get *in medias res*. Preliminaries @@ -123,8 +142,8 @@ like :: with tempconfig({"quality": "medium_quality", "preview": True}): - scene = ToyExample() - scene.render() + manager = Manager(ToyExample) + manager.render() or whether you are rendering the code in a Jupyter notebook, you are still telling your python interpreter to import the library. The usual pattern used to do this is @@ -202,8 +221,8 @@ have created a file ``toy_example.py`` which looks like this:: self.play(FadeOut(blue_circle, small_dot)) with tempconfig({"quality": "medium_quality", "preview": True}): - scene = ToyExample() - scene.render() + manager = Manager(ToyExample) + manager.render() With such a file, the desired scene is rendered by simply running this Python script via ``python toy_example.py``. Then, as described above, the library @@ -218,10 +237,10 @@ dictionary, and upon leaving the context the original version of the configuration is restored. TL;DR: it provides a fancy way of temporarily setting configuration options. -Inside the context manager, two things happen: an actual ``ToyExample``-scene -object is instantiated, and the ``render`` method is called. Every way of using +Inside the context manager, two things happen: a :class:`.Manager` is created for +the ``ToyExample``-scene, and the ``render`` method is called. Every way of using Manim ultimately does something along of these lines, the library always instantiates -the scene object and then calls its ``render`` method. To illustrate that this +the manager of the scene object and then calls its ``render`` method. To illustrate that this really is the case, let us briefly look at the two most common ways of rendering scenes: @@ -243,54 +262,75 @@ and the code creating the scene class and calling its render method is located `here `__. -Now that we know that either way, a :class:`.Scene` object is created, let us investigate -what Manim does when that happens. When instantiating our scene object +Now that we know that either way, a :class:`.Manager` for a :class:`.Scene` object is created, let us investigate +what Manim does when that happens. When instantiating our manager :: - scene = ToyExample() + manager = Manager(ToyExample) -the ``Scene.__init__`` method is called, given that we did not implement our own initialization -method. Inspecting the corresponding code (see -`here `__) -reveals that ``Scene.__init__`` first sets several attributes of the scene objects that do not -depend on any configuration options set in ``config``. Then the scene inspects the value of -``config.renderer``, and based on its value, either instantiates a ``CairoRenderer`` or an -``OpenGLRenderer`` object and assigns it to its ``renderer`` attribute. +The :meth:`.Manager.__init__` method is called. Looking at the source code (`here `__), +we see that the :meth:`.Scene.__init__` method is called, +given that we did not implement our own initialization +method. Inspecting the corresponding code (see `here `__) +reveals that :class:`Scene.__init__` first sets several attributes of the scene objects that do not +depend on any configuration options set in ``config``. It then initializes it's :class:`.Camera`. +The purpose of a :class:`.Camera` is to keep track of what you can see in the scene. Think of it +as a pair of eyes, that limit how far you can look sideways and vertically. -The scene then asks its renderer to initialize the scene by calling +The :class:`.Scene` also sets up :attr:`.Scene.mobjects`. This attribute keeps track of all the :class:`.Mobject` +that have been added to the scene. -:: +The :class:`.Manager` then continues on to create a :class:`.Window`, which is the popopen interactive window, +and creates the renderer:: - self.renderer.init_scene(self) + self.renderer = self.create_renderer() + self.renderer.use_window() -Inspecting both the default Cairo renderer and the OpenGL renderer shows that the ``init_scene`` -method effectively makes the renderer instantiate a :class:`.SceneFileWriter` object, which -basically is Manim's interface to ``libav`` (FFMPEG) and actually writes the movie file. The Cairo -renderer (see the implementation `here `__) does not require any further initialization. The OpenGL renderer -does some additional setup to enable the realtime rendering preview window, which we do not go -into detail further here. +If you hover over :attr:`.Manager.renderer`, you might see that the type is a :class:`.RendererProtocol`. +A :class:`~typing.Protocol` is a contract for a class. It says that whatever the class is, it will implement +the methods defined inside the protocol. In this case, it means that the renderer will have all the methods +defined in :class:`.RendererProtocol`. -.. warning:: +.. note:: - Currently, there is a lot of interplay between a scene and its renderer. This is a flaw - in Manim's current architecture, and we are working on reducing this interdependency to - achieve a less convoluted code flow. + The point of using :class:`~typing.Protocol` is so that in the future, plugins + can swap out the renderer with their own version - either for speed, or for a different + behavior. -After the renderer has been instantiated and initialized its file writer, the scene populates -further initial attributes (notable mention: the ``mobjects`` attribute which keeps track -of the mobjects that have been added to the scene). It is then done with its instantiation -and ready to be rendered. + +For the rest of this article to take a concrete example, we'll use :class:`.OpenGLRenderer`. + +Finally, the :class:`.Manager` creates a :class:`.FileWriter`. This is the object that actually +writes the partial movie files. The rest of this article is concerned with the last line in our toy example script:: - scene.render() + manager.render() This is where the actual magic happens. +.. note:: + + TODO TO REVIEWERS - Replace this link with the proper permanent link + Inspecting the `implementation of the render method `__ -reveals that there are several hooks that can be used for pre- or postprocessing -a scene. Unsurprisingly, :meth:`.Scene.render` describes the full *render cycle* +we see that there are two passes of rendering. + +.. note:: + + As of the experimental branch at June 30th, 2024, two pass rendering + does not exist. This will proceed to explain the single pass rendering system. + +Looking around, we find that there are several hooks that can be used for pre- or postprocessing +a scene (check out :meth:`.Manager._setup`, and :meth:`.Manager._tear_down`). + +.. note:: + + You might notice :attr:`.Manager.virtual_animation_start_time` and :attr:`.Manager.real_animation_start_time` + when looking through :meth:`.Manager._setup`. These will be explained later. + +Unsurprisingly, :meth:`.Manager.render` describes the full *render cycle* of a scene. During this life cycle, there are three custom methods whose base implementation is empty and that can be overwritten to suit your purposes. In the order they are called, these customizable methods are: @@ -308,14 +348,14 @@ the order they are called, these customizable methods are: Python scripts). After these three methods are run, the animations have been fully rendered, -and Manim calls :meth:`.CairoRenderer.scene_finished` to gracefully +and Manim calls :meth:`.Manager.tear_down` to gracefully complete the rendering process. This checks whether any animations have been played -- and if so, it tells the :class:`.SceneFileWriter` to close the output file. If not, Manim assumes that a static image should be output which it then renders using the same strategy by calling the render loop (see below) once. -**Back in our toy example,** the call to :meth:`.Scene.render` first +**Back in our toy example,** the call to :meth:`.Manager.render` first triggers :meth:`.Scene.setup` (which only consists of ``pass``), followed by a call of :meth:`.Scene.construct`. At this point, our *animation script* is run, starting with the initialization of ``orange_square``. @@ -348,16 +388,12 @@ of :class:`.Mobject`, you will find that not too much happens in there: - and finally, ``init_colors`` is called. Digging deeper, you will find that :meth:`.Mobject.reset_points` simply -sets the ``points`` attribute of the mobject to an empty NumPy vector, +sets the ``points`` attribute of the mobject to an empty NumPy array, while the other two methods, :meth:`.Mobject.generate_points` and :meth:`.Mobject.init_colors` are just implemented as ``pass``. This makes sense: :class:`.Mobject` is not supposed to be used as -an *actual* object that is displayed on screen; in fact the camera -(which we will discuss later in more detail; it is the class that is, -for the Cairo renderer, responsible for "taking a picture" of the -current scene) does not process "pure" :class:`Mobjects <.Mobject>` -in any way, they *cannot* even appear in the rendered output. +an *actual* object that is displayed on screen. This is where different types of mobjects come into play. Roughly speaking, the Cairo renderer setup knows three different types of @@ -376,24 +412,24 @@ mobjects that can be rendered: As just mentioned, :class:`VMobjects <.VMobject>` represent vectorized mobjects. To render a :class:`.VMobject`, the camera looks at the -``points`` attribute of a :class:`.VMobject` and divides it into sets -of four points each. Each of these sets is then used to construct a -cubic Bézier curve with the first and last entry describing the -end points of the curve ("anchors"), and the second and third entry -describing the control points in between ("handles"). +:attr:`~.VMobject.points` attribute of a :class:`.VMobject` and divides it into sets +of three points each. Each of these sets is then used to construct a +quadratic Bézier curve with the first and last entry describing the +end points of the curve ("anchors"), and the second entry +describing the control points in between ("handle"). .. hint:: To learn more about Bézier curves, take a look at the excellent online textbook `A Primer on Bézier curves `__ by `Pomax `__ -- there is a playground representing - cubic Bézier curves `in §1 `__, + quadratic Bézier curves `in §1 `__, the red and yellow points are "anchors", and the green and blue points are "handles". In contrast to :class:`.Mobject`, :class:`.VMobject` can be displayed on screen (even though, technically, it is still considered a base class). To illustrate how points are processed, consider the following short example -of a :class:`.VMobject` with 8 points (and thus made out of 8/4 = 2 cubic +of a :class:`.VMobject` with 6 points (and thus made out of 6/3 = 2 cubic Bézier curves). The resulting :class:`.VMobject` is drawn in green. The handles are drawn as red dots with a line to their closest anchor. @@ -430,6 +466,7 @@ The handles are drawn as red dots with a line to their closest anchor. .. warning:: + Manually setting the points of your :class:`.VMobject` is usually discouraged; there are specialized methods that can take care of that for you -- but it might be relevant when implementing your own, @@ -561,59 +598,12 @@ is not a "flat" list of mobjects, but a list of mobjects which might contain mobjects themselves, and so on. Stepping through the code in :meth:`.Scene.add`, we see that first -it is checked whether we are currently using the OpenGL renderer -(which we are not) -- adding mobjects to the scene works slightly -different (and actually easier!) for the OpenGL renderer. Then, the -code branch for the Cairo renderer is entered and the list of so-called -foreground mobjects (which are rendered on top of all other mobjects) -is added to the list of passed mobjects. This is to ensure that the -foreground mobjects will stay above of the other mobjects, even after -adding the new ones. In our case, the list of foreground mobjects -is actually empty, and nothing changes. - -Next, :meth:`.Scene.restructure_mobjects` is called with the list -of mobjects to be added as the ``to_remove`` argument, which might -sound odd at first. Practically, this ensures that mobjects are not -added twice, as mentioned above: if they were present in the scene -``Scene.mobjects`` list before (even if they were contained as a -child of some other mobject), they are first removed from the list. -The way :meth:`.Scene.restrucutre_mobjects` works is rather aggressive: -It always operates on a given list of mobjects; in the ``add`` method -two different lists occur: the default one, ``Scene.mobjects`` (no extra -keyword argument is passed), and ``Scene.moving_mobjects`` (which we will -discuss later in more detail). It iterates through all of the members of -the list, and checks whether any of the mobjects passed in ``to_remove`` -are contained as children (in any nesting level). If so, **their parent -mobject is deconstructed** and their siblings are inserted directly -one level higher. Consider the following example:: - - >>> from manim import Scene, Square, Circle, Group - >>> test_scene = Scene() - >>> mob1 = Square() - >>> mob2 = Circle() - >>> mob_group = Group(mob1, mob2) - >>> test_scene.add(mob_group) - - >>> test_scene.mobjects - [Group] - >>> test_scene.restructure_mobjects(to_remove=[mob1]) - - >>> test_scene.mobjects - [Circle] - -Note that the group is disbanded and the circle moves into the -root layer of mobjects in ``test_scene.mobjects``. - -After the mobject list is "restructured", the mobject to be added -are simply appended to ``Scene.mobjects``. In our toy example, -the ``Scene.mobjects`` list is actually empty, so the -``restructure_mobjects`` method does not actually do anything. The -``orange_square`` is simply added to ``Scene.mobjects``, and as -the aforementioned ``Scene.moving_mobjects`` list is, at this point, -also still empty, nothing happens and :meth:`.Scene.add` returns. - -We will hear more about the ``moving_mobject`` list when we discuss -the render loop. Before we do that, let us look at the next line +we remove all the mobjects that are being added -- this is to make +sure we don't add a :class:`.Mobject` twice! After that, we can safely +add it to :attr:`.Scene.mobjects`. + +We will hear more from :class:`.Scene` soon. +Before we do that, let us look at the next line of code in our toy example, which includes the initialization of an animation class, :: @@ -642,11 +632,11 @@ the run time of animations is also fixed and known beforehand. The initialization of animations actually is not very exciting, :meth:`.Animation.__init__` merely sets some attributes derived from the passed keyword arguments and additionally ensures that -the ``Animation.starting_mobject`` and ``Animation.mobject`` +the :attr:`~Animation.starting_mobject` and :attr:`~.Animation.mobject` attributes are populated. Once the animation is played, the -``starting_mobject`` attribute holds an unmodified copy of the +:attr:`~.Animation.starting_mobject` attribute holds an unmodified copy of the mobject the animation is attached to; during the initialization -it is set to a placeholder mobject. The ``mobject`` attribute +it is set to a placeholder mobject. The :attr:`~.Animation.mobject` attribute is set to the mobject the animation is attached to. Animations have a few special methods which are called during the @@ -681,77 +671,80 @@ animation (like its ``run_time``, the ``rate_func``, etc.) are processed there -- and then the animation object is fully initialized and ready to be played. +The Animation Buffer +^^^^^^^^^^^^^^^^^^^^ +There's an attribute of animations that we have glossed +over, and that is :attr:`.Animation.buffer`, of type :class:`.SceneBuffer`. +The :attr:`~.Animation.buffer` is the animations way of communicating +with what happens on the scene. If you want to modify +the scene during the interpolation stage (outside of :meth:`~.Animation.begin` or :meth:`~.Animation.finish`), +the attribute :attr:`.Animation.apply_buffer` is what tells the scene that the buffer +should be processed. + +For example, an animation that adds a circle to the scene every frame might look like this + +.. code-block:: python + + class CircleAnimation(Animation): + def begin(self) -> None: + self.circles = VGroup() + + def interpolate(self, alpha: float) -> None: + # create and arrange the circles + self.circles.add(Circle()) + self.circles().arrange() + # add the new circle to the scene + self.buffer.add(self.circles[-1]) + # make sure the scene actually realizes something changed + self.apply_buffer = True + +Every time the :class:`.Scene` applies the buffer, it gets emptied out +for use the next time. + The ``play`` call: preparing to enter Manim's render loop ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ We are finally there, the render loop is in our reach. Let us walk through the code that is run when :meth:`.Scene.play` is called. -.. hint:: +.. note:: - Recall that this article is specifically about the Cairo renderer. - Up to here, things were more or less the same for the OpenGL renderer - as well; while some base mobjects might be different, the control flow - and lifecycle of mobjects is still more or less the same. There are more - substantial differences when it comes to the rendering loop. + In the future, control will not be passed to the Manager. + Instead, the Scene will keep track of every animation and + at the very end, the Manager will render everything. As you will see when inspecting the method, :meth:`.Scene.play` almost -immediately passes over to the ``play`` method of the renderer, -in our case :class:`.CairoRenderer.play`. The one thing :meth:`.Scene.play` -takes care of is the management of subcaptions that you might have -passed to it (see the the documentation of :meth:`.Scene.play` and -:meth:`.Scene.add_subcaption` for more information). +immediately passes over to the :class:`~.Manager._play` method of the :class:`.Manager`. +The one thing :meth:`.Scene.play` does before that is preparing the animations. +Whenever :attr:`.Mobject.animate` is called, it creates a new object called a +:class:`._AnimationBuilder`. We have to make sure to convert that into an actual +animation by calling it's :meth:`._AnimationBuilder.build` method. +We also have to update the animations with the correct rate functions, lag ratios, +and run time. + +.. note:: + + Methods in :class:`.Manager` starting with an underscore ``_`` are intended to be + private, and are not guaranteed to be stable across versions of Manim. The :class:`.Manager` + class provides some "public" methods (methods not prefixed with ``_``) that can be overridden to + change the behavior of the program. .. warning:: - As has been said before, the communication between scene and renderer - is not in a very clean state at this point, so the following paragraphs - might be confusing if you don't run a debugger and step through the - code yourself a bit. - -Inside :meth:`.CairoRenderer.play`, the renderer first checks whether -it may skip rendering of the current play call. This might happen, for example, -when ``-s`` is passed to the CLI (i.e., only the last frame should be rendered), -or when the ``-n`` flag is passed and the current play call is outside of the -specified render bounds. The "skipping status" is updated in form of the -call to :meth:`.CairoRenderer.update_skipping_status`. - -Next, the renderer asks the scene to process the animations in the play -call so that renderer obtains all of the information it needs. To -be more concrete, :meth:`.Scene.compile_animation_data` is called, -which then takes care of several things: - -- The method processes all animations and the keyword arguments passed - to the initial :meth:`.Scene.play` call. In particular, this means - that it makes sure all arguments passed to the play call are actually - animations (or ``.animate`` syntax calls, which are also assembled to - be actual :class:`.Animation`-objects at that point). It also propagates - any animation-related keyword arguments (like ``run_time``, - or ``rate_func``) passed to :class:`.Scene.play` to each individual - animation. The processed animations are then stored in the ``animations`` - attribute of the scene (which the renderer later reads...). -- It adds all mobjects to which the animations that are played are - bound to to the scene (provided the animation is not an mobject-introducing - animation -- for these, the addition to the scene happens later). -- In case the played animation is a :class:`.Wait` animation (this is the - case in a :meth:`.Scene.wait` call), the method checks whether a static - image should be rendered, or whether the render loop should be processed - as usual (see :meth:`.Scene.should_update_mobjects` for the exact conditions, - basically it checks whether there are any time-dependent updater functions - and so on). -- Finally, the method determines the total run time of the play call (which - at this point is computed as the maximum of the run times of the passed - animations). This is stored in the ``duration`` attribute of the scene. - - -After the animation data has been compiled by the scene, the renderer -continues to prepare for entering the render loop. It now checks the -skipping status which has been determined before. If the renderer can -skip this play call, it does so: it sets the current play call hash (which -we will get back to in a moment) to ``None`` and increases the time of the -renderer by the determined animation run time. - -Otherwise, the renderer checks whether or not Manim's caching system should + Subcaptions and audio is still in progress + + +After the :class:`.Scene` has done all the processing of animations, +it hands out control to the :class:`.Manager`. The :class:`.Manager` +then updates the skipping status of the :class:`.Scene`. This makes sure +that if ``-s`` or ``-n`` is used for sections, the scene does the correct +thing. + +The next important line is:: + + self._write_hashed_movie_file() + +Here, the :class:`.Manager` checks whether or not Manim's caching system should be used. The idea of the caching system is simple: for every play call, a hash value is computed, which is then stored and upon re-rendering the scene, the hash is generated again and checked against the stored value. If it is the @@ -761,8 +754,8 @@ to learn more, the :func:`.get_hash_from_play_call` function in the :mod:`.utils.hashing` module is essentially the entry point to the caching mechanism. -In the event that the animation has to be rendered, the renderer asks -its :class:`.SceneFileWriter` to open an output container. The process +In the event that the animation has to be rendered, the manager asks +its :class:`.FileWriter` to open an output container. The process is started by a call to ``libav`` and opens a container to which rendered raw frames can be written. As long as the output is open, the container can be accessed via the ``output_container`` attribute of the file writer. @@ -770,31 +763,18 @@ With the writing process in place, the renderer then asks the scene to "begin" the animations. First, it literally *begins* all of the animations by calling their -setup methods (:meth:`.Animation._setup_scene`, :meth:`.Animation.begin`). +setup methods (:meth:`.Animation.begin`). In doing so, the mobjects that are newly introduced by an animation (like via :class:`.Create` etc.) are added to the scene. Furthermore, the animation suspends updater functions being called on its mobject, and it sets its mobject to the state that corresponds to the first frame of the animation. -After this has happened for all animations in the current ``play`` call, -the Cairo renderer determines which of the scene's mobjects can be -painted statically to the background, and which ones have to be -redrawn every frame. It does so by calling -:meth:`.Scene.get_moving_and_static_mobjects`, and the resulting -partition of mobjects is stored in the corresponding ``moving_mobjects`` -and ``static_mobjects`` attributes. +.. note:: -.. NOTE:: + Implementation of figuring out which mobjects have to be redrawn + is still in progress. - The mechanism that determines static and moving mobjects is - specific for the Cairo renderer, the OpenGL renderer works differently. - Basically, moving mobjects are determined by checking whether they, - any of their children, or any of the mobjects "below" them (in the - sense of the order in which mobjects are processed in the scene) - either have an update function attached, or whether they appear - in one of the current animations. See the implementation of - :meth:`.Scene.get_moving_mobjects` for more details. Up to this very point, we did not actually render any (partial) image or movie files from the scene yet. This is, however, about to change. @@ -835,68 +815,28 @@ Time to render some frames. The render loop (for real this time) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Now we get to the meat of rendering, which happens in :meth:`.Manager._progress_through_animations`. -As mentioned above, due to the mechanism that determines static and moving -mobjects in the scene, the renderer knows which mobjects it can paint -statically to the background of the scene. Practically, this means that -it partially renders a scene (to produce a background image), and then -when iterating through the time progression of the animation only the -"moving mobjects" are re-painted on top of the static background. - -The renderer calls :meth:`.CairoRenderer.save_static_frame_data`, which -first checks whether there are currently any static mobjects, and if there -are, it updates the frame (only with the static mobjects; more about how -exactly this works in a moment) and then saves a NumPy array representing -the rendered frame in the ``static_image`` attribute. In our toy example, -there are no static mobjects, and so the ``static_image`` attribute is -simply set to ``None``. - -Next, the renderer asks the scene whether the current animation is -a "frozen frame" animation, which would mean that the renderer actually -does not have to repaint the moving mobjects in every frame of the time -progression. It can then just take the latest static frame, and display it -throughout the animation. - -.. NOTE:: - - An animation is considered a "frozen frame" animation if only a - static :class:`.Wait` animation is played. See the description - of :meth:`.Scene.compile_animation_data` above, or the - implementation of :meth:`.Scene.should_update_mobjects` for - more details. - -If this is not the case (just as in our toy example), the renderer -then calls the :meth:`.Scene.play_internal` method, which is the -integral part of the render loop (in which the library steps through -the time progression of the animation and renders the corresponding -frames). - -Within :meth:`.Scene.play_internal`, the following steps are performed: - -- The scene determines the run time of the animations by calling - :meth:`.Scene.get_run_time`. This method basically takes the maximum +- The manager determines the run time of the animations by calling + :meth:`.Manager._calc_run_time`. This method basically takes the maximum ``run_time`` attribute of all of the animations passed to the :meth:`.Scene.play` call. -- Then the *time progression* is constructed via the (internal) - :meth:`.Scene._get_animation_time_progression` method, which wraps - the actual :meth:`.Scene.get_time_progression` method. The time - progression is a ``tqdm`` `progress bar object `__ - for an iterator over ``np.arange(0, run_time, 1 / config.frame_rate)``. In +- Then, the progressbar is created by :meth:`.Manager._create_progressbar`, + which returns a ``tqdm`` `progress bar object `__ + object (from the ``tqdm`` library), or a fake progressbar if + :attr:`.ManimConfig.write_to_movie` is ``False``. +- Then the *time progression* is constructed via + :meth:`.Manager._calc_time_progression` method, which returns + ``np.arange(0, run_time, 1 / config.frame_rate)``. In other words, the time progression holds the time stamps (relative to the current animations, so starting at 0 and ending at the total animation run time, with the step size determined by the render frame rate) of the timeline where a new animation frame should be rendered. - Then the scene iterates over the time progression: for each time stamp ``t``, - :meth:`.Scene.update_to_time` is called, which ... - - - ... first computes the time passed since the last update (which might be 0, - especially for the initial call) and references it as ``dt``, - - then (in the order in which the animations are passed to :meth:`.Scene.play`) - calls :meth:`.Animation.update_mobjects` to trigger all updater functions that - are attached to the respective animation except for the "main mobject" of - the animation (that is, for example, for :class:`.Transform` the unmodified - copies of start and target mobject -- see :meth:`.Animation.get_all_mobjects_to_update` - for more details), + we find the time difference between the current and previous frame (AKA ``dt``). + We then update the animations in the scene using ``dt`` by + - iterating over each animation + - next, we update the animations mobjects - then the relative time progression with respect to the current animation is computed (``alpha = t / animation.run_time``), which is then used to update the state of the animation with a call to :meth:`.Animation.interpolate`. @@ -904,62 +844,29 @@ Within :meth:`.Scene.play_internal`, the following steps are performed: of all mobjects in the scene, all meshes, and finally those attached to the scene itself are run. -At this point, the internal (Python) state of all mobjects has been updated -to match the currently processed timestamp. If rendering should not be skipped, -then it is now time to *take a picture*! + After updating the animations, we pass ``dt`` to :meth:`.Manager._update_frame` which... + + - ... updates the total time passed + - Updates all the mobjects by calling :meth:`.Scene._update_mobjects`. This in turn + iterates over all the mobjects on the screen and updates them. + - After that, the current state of the scene is computed by :meth:`.Scene.get_state`, + which returns a :class:`.SceneState`. + - The state is then passed into :meth:`.Manager._render_frame`, which gets + the renderer to create the pixels. With :class:`.OpenGLRenderer`, this + also updates the window. :meth:`~.Manager._render_frame` also checks if it should write a frame, + and if so, writes a frame via the :class:`.FileWriter`. + - Finally, it uses a concept of virtual time vs real time to see + if the right amount of time has passed in the window. The virtual + time is the amount of time that is supposed to have passed (that is, ``t``). + The real time is how much time has actually passed in the window + (current time - start time of play). If the animations are progressing + faster than they would in real life, it will slow down the window by calling + :meth:`~.Manager._update_frame` with ``dt=0`` until that's no longer the case. + This is to make sure that animations never go too fast: it doesn't do anything if + animations are too slow! -.. NOTE:: - - The update of the internal state (iteration over the time progression) happens - *always* once :meth:`.Scene.play_internal` is entered. This ensures that even - if frames do not need to be rendered (because, e.g., the ``-n`` CLI flag has - been passed, something has been cached, or because we might be in a *Section* - with skipped rendering), updater functions still run correctly, and the state - of the first frame that *is* rendered is kept consistent. - -To render an image, the scene calls the corresponding method of its renderer, -:meth:`.CairoRenderer.render` and passes just the list of *moving mobjects* (remember, -the *static mobjects* are assumed to have already been painted statically to -the background of the scene). All of the hard work then happens when the renderer -updates its current frame via a call to :meth:`.CairoRenderer.update_frame`: - -First, the renderer prepares its :class:`.Camera` by checking whether the renderer -has a ``static_image`` different from ``None`` stored already. If so, it sets the -image as the *background image* of the camera via :meth:`.Camera.set_frame_to_background`, -and otherwise it just resets the camera via :meth:`.Camera.reset`. The camera is then -asked to capture the scene with a call to :meth:`.Camera.capture_mobjects`. - -Things get a bit technical here, and at some point it is more efficient to -delve into the implementation -- but here is a summary of what happens once the -camera is asked to capture the scene: - -- First, a flat list of mobjects is created (so submobjects get extracted from - their parents). This list is then processed in groups of the same type of - mobjects (e.g., a batch of vectorized mobjects, followed by a batch of image mobjects, - followed by more vectorized mobjects, etc. -- in many cases there will just be - one batch of vectorized mobjects). -- Depending on the type of the currently processed batch, the camera uses dedicated - *display functions* to convert the :class:`.Mobject` Python object to - a NumPy array stored in the camera's ``pixel_array`` attribute. - The most important example in that context is the display function for - vectorized mobjects, :meth:`.Camera.display_multiple_vectorized_mobjects`, - or the more particular (in case you did not add a background image to your - :class:`.VMobject`), :meth:`.Camera.display_multiple_non_background_colored_vmobjects`. - This method first gets the current Cairo context, and then, for every (vectorized) - mobject in the batch, calls :meth:`.Camera.display_vectorized`. There, - the actual background stroke, fill, and then stroke of the mobject is - drawn onto the context. See :meth:`.Camera.apply_stroke` and - :meth:`.Camera.set_cairo_context_color` for more details -- but it does not get - much deeper than that, in the latter method the actual Bézier curves - determined by the points of the mobject are drawn; this is where the low-level - interaction with Cairo happens. - -After all batches have been processed, the camera has an image representation -of the Scene at the current time stamp in form of a NumPy array stored in its -``pixel_array`` attribute. The renderer then takes this array and passes it to -its :class:`.SceneFileWriter`. This concludes one iteration of the render loop, -and once the time progression has been processed completely, a final bit -of cleanup is performed before the :meth:`.Scene.play_internal` call is completed. +At this point, the internal (Python) state of all mobjects has been updated +to match the currently processed timestamp. A TL;DR for the render loop, in the context of our toy example, reads as follows: @@ -968,23 +875,20 @@ A TL;DR for the render loop, in the context of our toy example, reads as follows medium render quality, the frame rate is 30 frames per second, and so the time progression with steps ``[0, 1/30, 2/30, ..., 89/30]`` is created. - In the internal render loop, each of these time stamps is processed: - there are no updater functions, so effectively the scene updates the + there are no updater functions, so effectively the manager updates the state of the transformation animation to the desired time stamp (for example, at time stamp ``t = 45/30``, the animation is completed to a rate of ``alpha = 0.5``). -- Then the scene asks the renderer to do its job. The renderer asks its camera - to capture the scene, the only mobject that needs to be processed at this point - is the main mobject attached to the transformation; the camera converts the - current state of the mobject to entries in a NumPy array. The renderer passes - this array to the file writer. +- Then the manager asks the renderer to do its job. The renderer then produces + the pixels, which are then fed into the :class:`.FileWriter`. - At the end of the loop, 90 frames have been passed to the file writer. Completing the render loop ^^^^^^^^^^^^^^^^^^^^^^^^^^ -The last few steps in the :meth:`.Scene.play_internal` call are not too +The last few steps in the :meth:`.Manager._play` call are not too exciting: for every animation, the corresponding :meth:`.Animation.finish` -and :meth:`.Animation.clean_up_from_scene` methods are called. +method is called. .. NOTE:: @@ -999,10 +903,6 @@ and :meth:`.Animation.clean_up_from_scene` methods are called. would be slightly longer than 1 second. We decided against this at some point. In the end, the time progression is closed (which completes the displayed progress bar) -in the terminal. With the closing of the time progression, the -:meth:`.Scene.play_internal` call is completed, and we return to the renderer, -which now orders the :class:`.SceneFileWriter` to close the output container that has -been opened for this animation: a partial movie file is written. This pretty much concludes the walkthrough of a :class:`.Scene.play` call, and actually there is not too much more to say for our toy example either: at @@ -1025,5 +925,4 @@ which triggers the combination of the partial movie files into the final product And there you go! This is a more or less detailed description of how Manim works under the hood. While we did not discuss every single line of code in detail in this walkthrough, it should still give you a fairly good idea of how the general -structural design of the library and at least the Cairo rendering flow in particular -looks like. +structural design of the library looks like. diff --git a/docs/source/reference.rst b/docs/source/reference.rst index 5352f83223..01844a5779 100644 --- a/docs/source/reference.rst +++ b/docs/source/reference.rst @@ -39,12 +39,8 @@ Cameras .. inheritance-diagram:: manim.camera.camera - manim.camera.mapping_camera - manim.camera.moving_camera - manim.camera.multi_camera - manim.camera.three_d_camera :parts: 1 - :top-classes: manim.camera.camera.Camera, manim.mobject.mobject.Mobject + :top-classes: manim.camera.camera.Camera, manim.mobject.opengl.opengl_mobject.OpenGLMobject Mobjects ******** diff --git a/docs/source/tutorials/building_blocks.rst b/docs/source/tutorials/building_blocks.rst index a01874535e..d507d1c5fc 100644 --- a/docs/source/tutorials/building_blocks.rst +++ b/docs/source/tutorials/building_blocks.rst @@ -297,9 +297,9 @@ Creating a custom animation Even though Manim has many built-in animations, you will find times when you need to smoothly animate from one state of a :class:`~.Mobject` to another. If you find yourself in that situation, then you can define your own custom animation. -You start by extending the :class:`~.Animation` class and overriding its :meth:`~.Animation.interpolate_mobject`. -The :meth:`~.Animation.interpolate_mobject` method receives alpha as a parameter that starts at 0 and changes throughout the animation. -So, you just have to manipulate self.mobject inside Animation according to the alpha value in its interpolate_mobject method. +You start by extending the :class:`~.Animation` class and overriding its :meth:`~.Animation.interpolate`. +The :meth:`~.Animation.interpolate` method receives alpha as a parameter that starts at 0 and changes throughout the animation. +So, you just have to manipulate self.mobject inside Animation according to the alpha value in its interpolate method. Then you get all the benefits of :class:`~.Animation` such as playing it for different run times or using different rate functions. Let's say you start with a number and want to create a :class:`~.Transform` animation that transforms it to a target number. @@ -312,11 +312,11 @@ The class can have a constructor with three arguments, a :class:`~.DecimalNumber The constructor will pass the :class:`~.DecimalNumber` Mobject to the super constructor (in this case, the :class:`~.Animation` constructor) and will set start and end. The only thing that you need to do is to define how you want it to look at every step of the animation. -Manim provides you with the alpha value in the :meth:`~.Animation.interpolate_mobject` method based on frame rate of video, rate function, and run time of animation played. +Manim provides you with the alpha value in the :meth:`~.Animation.interpolate` method based on frame rate of video, rate function, and run time of animation played. The alpha parameter holds a value between 0 and 1 representing the step of the currently playing animation. For example, 0 means the beginning of the animation, 0.5 means halfway through the animation, and 1 means the end of the animation. -In the case of the ``Count`` animation, you just have to figure out a way to determine the number to display at the given alpha value and then set that value in the :meth:`~.Animation.interpolate_mobject` method of the ``Count`` animation. +In the case of the ``Count`` animation, you just have to figure out a way to determine the number to display at the given alpha value and then set that value in the :meth:`~.Animation.interpolate` method of the ``Count`` animation. Suppose you are starting at 50 and incrementing until the :class:`~.DecimalNumber` reaches 100 at the end of the animation. * If alpha is 0, you want the value to be 50. @@ -331,7 +331,7 @@ Once you have defined your ``Count`` animation, you can play it in your :class:` .. manim:: CountingScene :ref_classes: Animation DecimalNumber - :ref_methods: Animation.interpolate_mobject Scene.play + :ref_methods: Animation.interpolate Scene.play class Count(Animation): def __init__(self, number: DecimalNumber, start: float, end: float, **kwargs) -> None: @@ -341,7 +341,7 @@ Once you have defined your ``Count`` animation, you can play it in your :class:` self.start = start self.end = end - def interpolate_mobject(self, alpha: float) -> None: + def interpolate(self, alpha: float) -> None: # Set value of DecimalNumber according to alpha value = self.start + (alpha * (self.end - self.start)) self.mobject.set_value(value) diff --git a/docs/source/tutorials/output_and_config.rst b/docs/source/tutorials/output_and_config.rst index af7961d873..5b47fb64f2 100644 --- a/docs/source/tutorials/output_and_config.rst +++ b/docs/source/tutorials/output_and_config.rst @@ -266,6 +266,44 @@ You can also skip rendering all animations belonging to a section like this: +Groups +****** +Sections are a powerful tool to organize your animations into different parts. However, sometimes it's +more useful to look at bigger parts of your animations. *Groups* are effectively sections of sections. + +The syntax is fairly simple:: + + class MyScene(Scene): + # enable groups + groups_api = True + + @group + def introduction(self) -> None: + self.play(Write(Text("Hello World!"))) + self.next_section(...) + self.play(Write(Text("This is a group!"))) + self.next_section(...) + + @group + def main_part(self) -> None: + self.play(Write(Text("This is the main part!"))) + self.next_section(...) + self.play(Write(Text("This is a group as well!"))) + self.next_section(...) + + @group + def conclusion(self) -> None: + self.play(FadeOut(*self.mobjects)) + +You can then play specific groups by using the ``--groups`` flag:: + + manim --groups introduction,conclusion scene.py + +Note that they must be separated by commas and without spaces. +Alternatively, you can set it on Manim's global ``config`` variable:: + + config.groups = ["introduction", "conclusion"] + Some command line flags *********************** diff --git a/example_scenes/bench.py b/example_scenes/bench.py new file mode 100644 index 0000000000..f659602fdd --- /dev/null +++ b/example_scenes/bench.py @@ -0,0 +1,9 @@ +from manim import * + + +class Test(Scene): + def construct(scene): + scene.camera.set_euler_angles(phi=75 * DEGREES, theta=-45 * DEGREES) + text = Tex("This is a 3D tex").fix_in_frame() + scene.add(text) + scene.wait() diff --git a/example_scenes/new_test_new.py b/example_scenes/new_test_new.py new file mode 100644 index 0000000000..ff68c63aec --- /dev/null +++ b/example_scenes/new_test_new.py @@ -0,0 +1,148 @@ +import time + +import numpy as np + +# import pyglet +from pyglet.gl import Config +from pyglet.window import Window + +import manim.utils.color.manim_colors as col +from manim._config import tempconfig +from manim.animation.creation import DrawBorderThenFill +from manim.camera.camera import Camera +from manim.constants import LEFT, OUT, RIGHT, UP +from manim.mobject.geometry.arc import Circle +from manim.mobject.geometry.polygram import Square +from manim.mobject.logo import ManimBanner +from manim.mobject.text.numbers import DecimalNumber +from manim.renderer.opengl_renderer import OpenGLRenderer + +if __name__ == "__main__": + with tempconfig({"renderer": "opengl"}): + win = Window( + width=1920, + height=1080, + fullscreen=True, + vsync=True, + config=Config(double_buffer=True, samples=0), + ) + renderer = OpenGLRenderer(1920, 1080, background_color=col.GRAY) + # vm = OpenGLVMobject([col.RED, col.GREEN]) + vm = ( + Circle( + radius=1, + stroke_color=col.YELLOW, + ) + .shift(3 * RIGHT + OUT) + .set_opacity(0.6) + ) + vm2 = Square(stroke_color=col.GREEN, fill_opacity=0, stroke_opacity=1).move_to( + (0, 0, -0.5) + ) + vm3 = ManimBanner().set_opacity(0.6) + vm4 = ( + Circle(0.5, col.GREEN) + .set_opacity(0.6) + .shift(OUT) + .set_fill(col.BLUE, opacity=0.2) + ) + # vm.set_points_as_corners([[-1920/2, 0, 0], [1920/2, 0, 0], [0, 1080/2, 0]]) + # print(vm.color) + # print(vm.fill_color) + # print(vm.stroke_color) + + clock_mobject = DecimalNumber(0.0).shift(4 * LEFT + 2.5 * UP) + clock_mobject.fix_in_frame() + + camera = Camera() + camera.save_state() + # renderer.init_camera(camera) + + # renderer.render(camera, [vm, vm2]) + # image = renderer.get_pixels() + # print(image.shape) + # Image.fromarray(image, "RGBA").show() + # exit(0) + renderer.use_window() + + # clock = pyglet.clock.get_default() + + def update_circle(dt): + vm.move_to((np.sin(dt) * 4, np.cos(dt) * 4, -1)) + + def p2m(x, y, z): + from manim._config import config + + return ( + config.frame_width * (x / config.pixel_width - 0.5), + config.frame_height * (y / config.pixel_height - 0.5), + z, + ) + + @win.event + def on_close(): + win.close() + + @win.event + def on_mouse_motion(x, y, dx, dy): + # vm.move_to((14.2222 * (x / 1920 - 0.5), 8 * (y / 1080 - 0.5), 0)) + # camera.move_to(p2m(x,y,camera.get_center()[2])) + from scipy.spatial.transform import Rotation + + camera.set_orientation( + Rotation.from_rotvec( + (-UP * (x / 1920 - 0.5) + RIGHT * (y / 1080 - 0.5)) * 2 * 3.1415 + ) + ) + # vm.set_color(col.RED.interpolate(col.GREEN,x/1920)) + # print(x,y) + + @win.event + def on_draw(): + # dt = clock.update_time() + renderer.render(camera, [vm2, vm3, vm4, clock_mobject, vm]) + # update_circle(counter) + + @win.event + def on_resize(width, height): + super(Window, win).on_resize(width, height) + + # pyglet.app.run() + has_started = False + is_finished = False + + run_time = 5 + new_vm = Square(fill_color=col.GREEN, stroke_color=col.BLUE).shift( + 2.5 * RIGHT - UP + 2 * OUT + ) + animation = DrawBorderThenFill(vm3, run_time=run_time) + + real_time = 0 + virtual_time = 0 + start_timestamp = time.time() + dt = 1 / 30 + + while True: + # pyglet.app.platform_event_loop.step() + win.switch_to() + if not has_started: + animation.begin() + has_started = True + + real_time = time.time() - start_timestamp + while virtual_time < real_time: + virtual_time += dt + if not is_finished: + if virtual_time >= run_time: + animation.finish() + buffer = str(animation.buffer) + print(f"buffer = {buffer}") + has_finished = True + else: + animation.update_mobjects(dt) + animation.interpolate(virtual_time / run_time) + # update_circle(virtual_time) + clock_mobject.set_value(virtual_time) + win.dispatch_event("on_draw") + win.dispatch_events() + win.flip() diff --git a/example_scenes/test_new.py b/example_scenes/test_new.py new file mode 100644 index 0000000000..ee45d4359b --- /dev/null +++ b/example_scenes/test_new.py @@ -0,0 +1,114 @@ +import numpy as np +import pyglet +from pyglet.gl import Config +from pyglet.window import Window + +import manim.utils.color.manim_colors as col +from manim._config import tempconfig +from manim.camera.camera import Camera +from manim.constants import OUT, RIGHT, UP +from manim.mobject.geometry.arc import Circle +from manim.mobject.geometry.polygram import Square +from manim.mobject.logo import ManimBanner +from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject +from manim.mobject.text.numbers import DecimalNumber +from manim.renderer.opengl_renderer import OpenGLRenderer + +if __name__ == "__main__": + with tempconfig({"renderer": "opengl"}): + win = Window( + width=1920, + height=1080, + vsync=True, + config=Config(double_buffer=True, samples=0), + ) + renderer = OpenGLRenderer(1920, 1080, background_color=col.GRAY) + # vm = OpenGLVMobject([col.RED, col.GREEN]) + vm = ( + Circle( + radius=1, + stroke_color=col.YELLOW, + ) + .shift(RIGHT) + .set_opacity(0.5) + ) + vm2 = Square(stroke_color=col.GREEN, fill_opacity=0, stroke_opacity=1).move_to( + (0, 0, -0.5) + ) + vm3 = ManimBanner().set_opacity(1.0) + vm4 = ( + Circle(0.5, col.GREEN) + .set_opacity(0.6) + .shift(OUT) + .set_fill(col.BLUE, opacity=0.2) + ) + # vm.set_points_as_corners([[-1920/2, 0, 0], [1920/2, 0, 0], [0, 1080/2, 0]]) + # print(vm.color) + # print(vm.fill_color) + # print(vm.stroke_color) + + camera = Camera() + camera.save_state() + renderer.init_camera(camera) + + # renderer.render(camera, [vm, vm2]) + # image = renderer.get_pixels() + # print(image.shape) + # Image.fromarray(image, "RGBA").show() + # exit(0) + renderer.use_window() + + clock = pyglet.clock.get_default() + + def update_circle(dt): + vm.move_to((np.sin(dt) * 4, np.cos(dt) * 4, -1)) + + def p2m(x, y, z): + from manim._config import config + + return ( + config.frame_width * (x / config.pixel_width - 0.5), + config.frame_height * (y / config.pixel_height - 0.5), + z, + ) + + @win.event + def on_close(): + win.close() + + @win.event + def on_mouse_motion(x, y, dx, dy): + # vm.move_to((14.2222 * (x / 1920 - 0.5), 8 * (y / 1080 - 0.5), 0)) + # camera.move_to(p2m(x,y,camera.get_center()[2])) + from scipy.spatial.transform import Rotation + + camera.set_orientation( + Rotation.from_rotvec( + (-UP * (x / 1920 - 0.5) + RIGHT * (y / 1080 - 0.5)) * 2 * 3.1415 + ) + ) + # vm.set_color(col.RED.interpolate(col.GREEN,x/1920)) + # print(x,y) + + @win.event + def on_draw(): + dt = clock.update_time() + fps: OpenGLVMobject = DecimalNumber(dt) + fps.fix_in_frame() + renderer.render(camera, [vm, vm2, vm3, vm4, fps]) + # update_circle(counter) + + @win.event + def on_resize(width, height): + super(Window, win).on_resize(width, height) + + pyglet.app.run() + # while True: + # pyglet.clock.tick() + # pyglet.app.platform_event_loop.step() + # win.switch_to() + # counter += 0.01 + # update_circle(counter) + # win.dispatch_event("on_draw") + # win.dispatch_events() + # win.flip() diff --git a/example_scenes/test_new_rendering.py b/example_scenes/test_new_rendering.py new file mode 100644 index 0000000000..758a32439f --- /dev/null +++ b/example_scenes/test_new_rendering.py @@ -0,0 +1,53 @@ +from manim import * + + +class Test(Scene): + groups_api = True + + @group + def first_section(self) -> None: + line = Line() + line.add_updater(lambda m, dt: m.rotate(PI * dt)) + t = Tex(r"Math! $\sum e^{i\theta}$").add_updater(lambda m: m.next_to(line, UP)) + line.to_edge(LEFT) + self.add(line, t) + s = Square() + t = Tex( + "Hello, world!", stroke_color=RED, fill_color=BLUE, stroke_width=2 + ).to_edge(RIGHT) + self.add(t) + self.play(Create(t), Rotate(s, PI / 2)) + self.wait(1) + self.play(FadeOut(s)) + + @group + def three_mobjects(self) -> None: + sq = RegularPolygon(6) + c = Circle() + st = Star() + VGroup(sq, c, st).arrange() + self.play( + Succession( + Create(sq), + DrawBorderThenFill(c), + Create(st), + ) + ) + self.play(FadeOut(VGroup(sq, c, st))) + + @group + def never_run(self) -> None: + self.play(Write(Text("This should never be run"))) + + +if __name__ == "__main__": + with tempconfig( + { + "preview": True, + "write_to_movie": False, + "disable_caching": True, + "frame_rate": 60, + "disable_caching_warning": True, + } + ): + Manager(Test).render() diff --git a/manim/__init__.py b/manim/__init__.py index a4034ed134..64d315c370 100644 --- a/manim/__init__.py +++ b/manim/__init__.py @@ -10,98 +10,91 @@ # Importing the config module should be the first thing we do, since other # modules depend on the global config dict for initialization. -from ._config import * +from manim._config import * # many scripts depend on this -> has to be loaded first -from .utils.commands import * +from manim.utils.commands import * # isort: on import numpy as np -from .animation.animation import * -from .animation.changing import * -from .animation.composition import * -from .animation.creation import * -from .animation.fading import * -from .animation.growing import * -from .animation.indication import * -from .animation.movement import * -from .animation.numbers import * -from .animation.rotation import * -from .animation.specialized import * -from .animation.speedmodifier import * -from .animation.transform import * -from .animation.transform_matching_parts import * -from .animation.updaters.mobject_update_utils import * -from .animation.updaters.update import * -from .camera.camera import * -from .camera.mapping_camera import * -from .camera.moving_camera import * -from .camera.multi_camera import * -from .camera.three_d_camera import * -from .constants import * -from .mobject.frame import * -from .mobject.geometry.arc import * -from .mobject.geometry.boolean_ops import * -from .mobject.geometry.labeled import * -from .mobject.geometry.line import * -from .mobject.geometry.polygram import * -from .mobject.geometry.shape_matchers import * -from .mobject.geometry.tips import * -from .mobject.graph import * -from .mobject.graphing.coordinate_systems import * -from .mobject.graphing.functions import * -from .mobject.graphing.number_line import * -from .mobject.graphing.probability import * -from .mobject.graphing.scale import * -from .mobject.logo import * -from .mobject.matrix import * -from .mobject.mobject import * -from .mobject.opengl.dot_cloud import * -from .mobject.opengl.opengl_point_cloud_mobject import * -from .mobject.svg.brace import * -from .mobject.svg.svg_mobject import * -from .mobject.table import * -from .mobject.text.code_mobject import * -from .mobject.text.numbers import * -from .mobject.text.tex_mobject import * -from .mobject.text.text_mobject import * -from .mobject.three_d.polyhedra import * -from .mobject.three_d.three_d_utils import * -from .mobject.three_d.three_dimensions import * -from .mobject.types.image_mobject import * -from .mobject.types.point_cloud_mobject import * -from .mobject.types.vectorized_mobject import * -from .mobject.value_tracker import * -from .mobject.vector_field import * -from .renderer.cairo_renderer import * -from .scene.moving_camera_scene import * -from .scene.scene import * -from .scene.scene_file_writer import * -from .scene.section import * -from .scene.three_d_scene import * -from .scene.vector_space_scene import * -from .scene.zoomed_scene import * -from .utils import color, rate_functions, unit -from .utils.bezier import * -from .utils.color import * -from .utils.config_ops import * -from .utils.debug import * -from .utils.file_ops import * -from .utils.images import * -from .utils.iterables import * -from .utils.paths import * -from .utils.rate_functions import * -from .utils.simple_functions import * -from .utils.sounds import * -from .utils.space_ops import * -from .utils.tex import * -from .utils.tex_templates import * +from manim.animation.animation import * +from manim.animation.changing import * +from manim.animation.composition import * +from manim.animation.creation import * +from manim.animation.fading import * +from manim.animation.growing import * +from manim.animation.indication import * +from manim.animation.movement import * +from manim.animation.numbers import * +from manim.animation.rotation import * +from manim.animation.specialized import * +from manim.animation.speedmodifier import * +from manim.animation.transform import * +from manim.animation.transform_matching_parts import * +from manim.animation.updaters.mobject_update_utils import * +from manim.animation.updaters.update import * +from manim.constants import * +from manim.file_writer import * +from manim.manager import * +from manim.mobject.frame import * +from manim.mobject.geometry.arc import * +from manim.mobject.geometry.boolean_ops import * +from manim.mobject.geometry.labeled import * +from manim.mobject.geometry.line import * +from manim.mobject.geometry.polygram import * +from manim.mobject.geometry.shape_matchers import * +from manim.mobject.geometry.tips import * +from manim.mobject.graph import * +from manim.mobject.graphing.coordinate_systems import * +from manim.mobject.graphing.functions import * +from manim.mobject.graphing.number_line import * +from manim.mobject.graphing.probability import * +from manim.mobject.graphing.scale import * +from manim.mobject.logo import * +from manim.mobject.matrix import * +from manim.mobject.mobject import * +from manim.mobject.opengl.dot_cloud import * +from manim.mobject.opengl.opengl_point_cloud_mobject import * +from manim.mobject.opengl.opengl_vectorized_mobject import * +from manim.mobject.svg.brace import * +from manim.mobject.svg.svg_mobject import * +from manim.mobject.table import * +from manim.mobject.text.code_mobject import * +from manim.mobject.text.numbers import * +from manim.mobject.text.tex_mobject import * +from manim.mobject.text.text_mobject import * +from manim.mobject.three_d.polyhedra import * +from manim.mobject.three_d.three_d_utils import * +from manim.mobject.three_d.three_dimensions import * +from manim.mobject.types.image_mobject import * +from manim.mobject.types.point_cloud_mobject import * +from manim.mobject.types.vectorized_mobject import * +from manim.mobject.value_tracker import * +from manim.mobject.vector_field import * +from manim.scene.scene import * +from manim.scene.sections import * +from manim.scene.vector_space_scene import * +from manim.utils import color, rate_functions, unit +from manim.utils.bezier import * +from manim.utils.color import * +from manim.utils.config_ops import * +from manim.utils.debug import * +from manim.utils.file_ops import * +from manim.utils.images import * +from manim.utils.iterables import * +from manim.utils.paths import * +from manim.utils.rate_functions import * +from manim.utils.simple_functions import * +from manim.utils.sounds import * +from manim.utils.space_ops import * +from manim.utils.tex import * +from manim.utils.tex_templates import * try: from IPython import get_ipython - from .utils.ipython_magic import ManimMagic + from manim.utils.ipython_magic import ManimMagic except ImportError: pass else: @@ -109,4 +102,4 @@ if ipy is not None: ipy.register_magics(ManimMagic) -from .plugins import * +from manim.plugins import * diff --git a/manim/_config/default.cfg b/manim/_config/default.cfg index 9155d28dbd..7e60dd7476 100644 --- a/manim/_config/default.cfg +++ b/manim/_config/default.cfg @@ -7,18 +7,18 @@ # Each of the following will be set to True if the corresponding CLI flag # is present when executing manim. If the flag is not present, they will -# be set to the value found here. For example, running manim with the -w -# flag will set WRITE_TO_MOVIE to True. However, since the default value -# of WRITE_TO_MOVIE defined in this file is also True, running manim -# without the -w value will also output a movie file. To change that, set -# WRITE_TO_MOVIE = False so that running manim without the -w flag will not -# generate a movie file. Note all of the following accept boolean values. +# be set to the value found here. For example, running manim with the --format=mp4 +# flag will set FORMAT to mp4. However, since the default value +# of FORMAT defined in this file is also mp4, running manim +# without the --format=mp4 value will also output an mp4 movie file. To change that, set +# FORMAT = webm so that running manim without the --format=mp4 flag will not +# generate an mp4 movie file. # --notify_outdated_version notify_outdated_version = True # -w, --write_to_movie -write_to_movie = True +write_to_movie = False format = mp4 @@ -29,15 +29,9 @@ save_last_frame = False # -a, --write_all write_all = False -# -g, --save_pngs -save_pngs = False - # -0, --zero_pad zero_pad = 4 -# -i, --save_as_gif -save_as_gif = False - # --save_sections save_sections = False @@ -94,7 +88,7 @@ text_dir = {media_dir}/texts partial_movie_dir = {video_dir}/partial_movie_files/{scene_name} # --renderer [cairo|opengl] -renderer = cairo +renderer = opengl # --enable_gui enable_gui = False @@ -121,12 +115,6 @@ window_monitor = 0 # --force_window force_window = False -# --use_projection_fill_shaders -use_projection_fill_shaders = False - -# --use_projection_stroke_shaders -use_projection_stroke_shaders = False - movie_file_extension = .mp4 # These now override the --quality option. diff --git a/manim/_config/utils.py b/manim/_config/utils.py index b453b290e2..7b8479dd2a 100644 --- a/manim/_config/utils.py +++ b/manim/_config/utils.py @@ -20,7 +20,7 @@ import os import re import sys -from collections.abc import Iterable, Iterator, Mapping, MutableMapping +from collections.abc import Iterable, Iterator, Mapping, MutableMapping, Sequence from pathlib import Path from typing import TYPE_CHECKING, Any, ClassVar, NoReturn @@ -273,6 +273,7 @@ class MyScene(Scene): ... "frame_x_radius", "frame_y_radius", "from_animation_number", + "groups", "images_dir", "input_file", "media_embed", @@ -291,10 +292,8 @@ class MyScene(Scene): ... "preview", "progress_bar", "quality", - "save_as_gif", "save_sections", "save_last_frame", - "save_pngs", "scene_names", "show_in_file_browser", "tex_dir", @@ -305,8 +304,6 @@ class MyScene(Scene): ... "renderer", "enable_gui", "gui_location", - "use_projection_fill_shaders", - "use_projection_stroke_shaders", "verbosity", "video_dir", "sections_dir", @@ -325,6 +322,22 @@ class MyScene(Scene): ... def __init__(self) -> None: self._d: dict[str, Any | None] = dict.fromkeys(self._OPTS) + def _warn_about_config_options(self) -> None: + """Warns about incorrect config options, or permutations of config options.""" + logger = logging.getLogger("manim") + if self.format == "webm": + logger.warning( + "Output format set as webm, this can be slower than other formats", + ) + if not self.preview and not self.write_to_movie: + logger.warning( + "preview and write_to_movie disabled, this is a dry run. Try passing -p or -w." + ) + elif self.preview and self.write_to_movie: + logger.warning( + "Both preview and write_to_movie enabled, this can be slower than just previewing." + ) + # behave like a dict def __iter__(self) -> Iterator[str]: return iter(self._d) @@ -574,8 +587,6 @@ def digest_parser(self, parser: configparser.ConfigParser) -> Self: "write_to_movie", "save_last_frame", "write_all", - "save_pngs", - "save_as_gif", "save_sections", "preview", "show_in_file_browser", @@ -586,8 +597,6 @@ def digest_parser(self, parser: configparser.ConfigParser) -> Self: "custom_folders", "enable_gui", "fullscreen", - "use_projection_fill_shaders", - "use_projection_stroke_shaders", "enable_wireframe", "force_window", "no_latex_cleanup", @@ -690,6 +699,8 @@ def digest_parser(self, parser: configparser.ConfigParser) -> Self: if val: self.quality = _determine_quality(val) + self.groups = parser["CLI"].get("groups", fallback="", raw=True) or [] + return self def digest_args(self, args: argparse.Namespace) -> Self: @@ -744,8 +755,6 @@ def digest_args(self, args: argparse.Namespace) -> Self: "show_in_file_browser", "write_to_movie", "save_last_frame", - "save_pngs", - "save_as_gif", "save_sections", "write_all", "disable_caching", @@ -759,8 +768,6 @@ def digest_args(self, args: argparse.Namespace) -> Self: "background_color", "enable_gui", "fullscreen", - "use_projection_fill_shaders", - "use_projection_stroke_shaders", "zero_pad", "enable_wireframe", "force_window", @@ -932,6 +939,7 @@ def notify_outdated_version(self) -> bool: def notify_outdated_version(self, value: bool) -> None: self._set_boolean("notify_outdated_version", value) + # TODO: Rename to write_to_file @property def write_to_movie(self) -> bool: """Whether to render the scene to a movie file (-w).""" @@ -959,24 +967,6 @@ def write_all(self) -> bool: def write_all(self, value: bool) -> None: self._set_boolean("write_all", value) - @property - def save_pngs(self) -> bool: - """Whether to save all frames in the scene as images files (-g).""" - return self._d["save_pngs"] - - @save_pngs.setter - def save_pngs(self, value: bool) -> None: - self._set_boolean("save_pngs", value) - - @property - def save_as_gif(self) -> bool: - """Whether to save the rendered scene in .gif format (-i).""" - return self._d["save_as_gif"] - - @save_as_gif.setter - def save_as_gif(self, value: bool) -> None: - self._set_boolean("save_as_gif", value) - @property def save_sections(self) -> bool: """Whether to save single videos for each section in addition to the movie file.""" @@ -1048,10 +1038,6 @@ def format(self, val: str) -> None: [None, "png", "gif", "mp4", "mov", "webm"], ) self.resolve_movie_file_extension(self.transparent) - if self.format == "webm": - logger.warning( - "Output format set as webm, this can be slower than other formats", - ) @property def ffmpeg_loglevel(self) -> str: @@ -1205,6 +1191,24 @@ def upto_animation_number(self) -> int: def upto_animation_number(self, value: int) -> None: self._set_pos_number("upto_animation_number", value, True) + @property + def groups(self) -> tuple[str, ...]: + """The name of the groups to play. + + If not passed, it will play all groups. Otherwise, + it will play only the groups passed in. + """ + return self._d["groups"] # type: ignore[misc] + + @groups.setter + def groups(self, value: str | Sequence[str]) -> None: + if isinstance(value, str): + self._set_str("groups", value.replace(" ", "").split(",")) + else: + if not all(isinstance(v, str) for v in value): + raise ValueError("groups must be a string or a sequence of strings") + self._d["groups"] = tuple(value) + @property def max_files_cached(self) -> int: """Maximum number of files cached. Use -1 for infinity (no flag).""" @@ -1464,24 +1468,6 @@ def fullscreen(self) -> bool: def fullscreen(self, value: bool) -> None: self._set_boolean("fullscreen", value) - @property - def use_projection_fill_shaders(self) -> bool: - """Use shaders for OpenGLVMobject fill which are compatible with transformation matrices.""" - return self._d["use_projection_fill_shaders"] - - @use_projection_fill_shaders.setter - def use_projection_fill_shaders(self, value: bool) -> None: - self._set_boolean("use_projection_fill_shaders", value) - - @property - def use_projection_stroke_shaders(self) -> bool: - """Use shaders for OpenGLVMobject stroke which are compatible with transformation matrices.""" - return self._d["use_projection_stroke_shaders"] - - @use_projection_stroke_shaders.setter - def use_projection_stroke_shaders(self, value: bool) -> None: - self._set_boolean("use_projection_stroke_shaders", value) - @property def zero_pad(self) -> int: """PNG zero padding. A number between 0 (no zero padding) and 9 (9 columns minimum).""" diff --git a/manim/animation/animation.py b/manim/animation/animation.py index 491d573740..74abd5d61c 100644 --- a/manim/animation/animation.py +++ b/manim/animation/animation.py @@ -2,34 +2,40 @@ from __future__ import annotations +from collections.abc import Iterable, Sequence +from copy import deepcopy +from functools import partialmethod +from typing import TYPE_CHECKING, Any, Callable, cast, overload + +import numpy as np +from typing_extensions import Self, TypeVar, assert_never + from manim.mobject.opengl.opengl_mobject import OpenGLMobject -from .. import config, logger -from ..constants import RendererType +from .. import logger from ..mobject import mobject from ..mobject.mobject import Group, Mobject from ..mobject.opengl import opengl_mobject from ..utils.rate_functions import linear, smooth +from .protocol import AnimationProtocol, MobjectAnimation +from .scene_buffer import SceneBuffer, SceneOperation -__all__ = ["Animation", "Wait", "Add", "override_animation"] +if TYPE_CHECKING: + from typing_extensions import Self + from manim.scene.scene import Scene -from collections.abc import Iterable, Sequence -from copy import deepcopy -from functools import partialmethod -from typing import TYPE_CHECKING, Any, Callable +M = TypeVar("M", bound=OpenGLMobject) -from typing_extensions import Self -if TYPE_CHECKING: - from manim.scene.scene import Scene +__all__ = ["Animation", "Wait", "override_animation"] DEFAULT_ANIMATION_RUN_TIME: float = 1.0 DEFAULT_ANIMATION_LAG_RATIO: float = 0.0 -class Animation: +class Animation(AnimationProtocol): """An animation. Animations have a fixed time span. @@ -72,9 +78,9 @@ class Animation: .. NOTE:: In the current implementation of this class, the specified rate function is applied - within :meth:`.Animation.interpolate_mobject` call as part of the call to + within :meth:`.Animation.interpolate` call as part of the call to :meth:`.Animation.interpolate_submobject`. For subclasses of :class:`.Animation` - that are implemented by overriding :meth:`interpolate_mobject`, the rate function + that are implemented by overriding :meth:`interpolate`, the rate function has to be applied manually (e.g., by passing ``self.rate_func(alpha)`` instead of just ``alpha``). @@ -130,37 +136,37 @@ def __new__( def __init__( self, - mobject: Mobject | None, + mobject: OpenGLMobject | None, lag_ratio: float = DEFAULT_ANIMATION_LAG_RATIO, run_time: float = DEFAULT_ANIMATION_RUN_TIME, rate_func: Callable[[float], float] = smooth, reverse_rate_function: bool = False, - name: str = None, - remover: bool = False, # remove a mobject from the screen? + name: str = "", + remover: bool = False, # remove a mobject from the screen at end of animation suspend_mobject_updating: bool = True, introducer: bool = False, *, - _on_finish: Callable[[], None] = lambda _: None, + _on_finish: Callable[[SceneBuffer], object] = lambda _: None, **kwargs, ) -> None: self._typecheck_input(mobject) self.run_time: float = run_time self.rate_func: Callable[[float], float] = rate_func self.reverse_rate_function: bool = reverse_rate_function - self.name: str | None = name + self.name: str = name self.remover: bool = remover self.introducer: bool = introducer self.suspend_mobject_updating: bool = suspend_mobject_updating self.lag_ratio: float = lag_ratio - self._on_finish: Callable[[Scene], None] = _on_finish - if config["renderer"] == RendererType.OPENGL: - self.starting_mobject: OpenGLMobject = OpenGLMobject() - self.mobject: OpenGLMobject = ( - mobject if mobject is not None else OpenGLMobject() - ) - else: - self.starting_mobject: Mobject = Mobject() - self.mobject: Mobject = mobject if mobject is not None else Mobject() + self._on_finish = _on_finish + + self.buffer = SceneBuffer() + self.apply_buffer = False # ask scene to apply buffer + + self.starting_mobject: OpenGLMobject = OpenGLMobject() + self.mobject: OpenGLMobject = ( + mobject if mobject is not None else OpenGLMobject() + ) if kwargs: logger.debug("Animation received extra kwargs: %s", kwargs) @@ -199,6 +205,17 @@ def __str__(self) -> str: def __repr__(self) -> str: return str(self) + def update_rate_info( + self, + run_time: float | None = None, + rate_func: Callable[[float], float] | None = None, + lag_ratio: float | None = None, + ): + self.run_time = run_time or self.run_time + self.rate_func = rate_func or self.rate_func + self.lag_ratio = lag_ratio or self.lag_ratio + return self + def begin(self) -> None: """Begin the animation. @@ -218,10 +235,12 @@ def begin(self) -> None: self.mobject.suspend_updating() self.interpolate(0) + # TODO: Figure out a way to check + # if self.mobject in scene.get_mobject_family + if self.introducer: + self.buffer.add(self.mobject) + def finish(self) -> None: - # TODO: begin and finish should require a scene as parameter. - # That way Animation.clean_up_from_screen and Scene.add_mobjects_from_animations - # could be removed as they fulfill basically the same purpose. """Finish the animation. This method gets called when the animation is over. @@ -231,45 +250,16 @@ def finish(self) -> None: if self.suspend_mobject_updating and self.mobject is not None: self.mobject.resume_updating() - def clean_up_from_scene(self, scene: Scene) -> None: - """Clean up the :class:`~.Scene` after finishing the animation. - - This includes to :meth:`~.Scene.remove` the Animation's - :class:`~.Mobject` if the animation is a remover. + # TODO: remove on_finish + self._on_finish(self.buffer) + if self.remover: + self.buffer.remove(self.mobject) - Parameters - ---------- - scene - The scene the animation should be cleaned up from. - """ - self._on_finish(scene) - if self.is_remover(): - scene.remove(self.mobject) - - def _setup_scene(self, scene: Scene) -> None: - """Setup up the :class:`~.Scene` before starting the animation. - - This includes to :meth:`~.Scene.add` the Animation's - :class:`~.Mobject` if the animation is an introducer. - - Parameters - ---------- - scene - The scene the animation should be cleaned up from. - """ - if scene is None: - return - if ( - self.is_introducer() - and self.mobject not in scene.get_mobject_family_members() - ): - scene.add(self.mobject) - - def create_starting_mobject(self) -> Mobject: + def create_starting_mobject(self) -> OpenGLMobject: # Keep track of where the mobject starts return self.mobject.copy() - def get_all_mobjects(self) -> Sequence[Mobject]: + def get_all_mobjects(self) -> Sequence[OpenGLMobject]: """Get all mobjects involved in the animation. Ordering must match the ordering of arguments to interpolate_submobject @@ -282,11 +272,7 @@ def get_all_mobjects(self) -> Sequence[Mobject]: return self.mobject, self.starting_mobject def get_all_families_zipped(self) -> Iterable[tuple]: - if config["renderer"] == RendererType.OPENGL: - return zip(*(mob.get_family() for mob in self.get_all_mobjects())) - return zip( - *(mob.family_members_with_points() for mob in self.get_all_mobjects()) - ) + return zip(*(mob.get_family() for mob in self.get_all_mobjects())) def update_mobjects(self, dt: float) -> None: """ @@ -299,7 +285,24 @@ def update_mobjects(self, dt: float) -> None: for mob in self.get_all_mobjects_to_update(): mob.update(dt) - def get_all_mobjects_to_update(self) -> list[Mobject]: + def process_subanimation_buffer(self, buffer: SceneBuffer): + """ + This is used in animations that are proxies around + other animations, like :class:`.AnimationGroup` + """ + for op, args, kwargs in buffer: + match op: + case SceneOperation.ADD: + self.buffer.add(*args, **kwargs) + case SceneOperation.REMOVE: + self.buffer.remove(*args, **kwargs) + case SceneOperation.REPLACE: + self.buffer.replace(*args, **kwargs) + case _: + assert_never(op) + buffer.clear() + + def get_all_mobjects_to_update(self) -> Sequence[OpenGLMobject]: """Get all mobjects to be updated during the animation. Returns @@ -310,9 +313,9 @@ def get_all_mobjects_to_update(self) -> list[Mobject]: # The surrounding scene typically handles # updating of self.mobject. Besides, in # most cases its updating is suspended anyway - return list(filter(lambda m: m is not self.mobject, self.get_all_mobjects())) + return [m for m in self.get_all_mobjects() if m is not self.mobject] - def copy(self) -> Animation: + def copy(self) -> Self: """Create a copy of the animation. Returns @@ -326,19 +329,6 @@ def copy(self) -> Animation: # TODO: stop using alpha as parameter name in different meanings. def interpolate(self, alpha: float) -> None: - """Set the animation progress. - - This method gets called for every frame during an animation. - - Parameters - ---------- - alpha - The relative time to set the animation to, 0 meaning the start, 1 meaning - the end. - """ - self.interpolate_mobject(alpha) - - def interpolate_mobject(self, alpha: float) -> None: """Interpolates the mobject of the :class:`Animation` based on alpha value. Parameters @@ -348,20 +338,19 @@ def interpolate_mobject(self, alpha: float) -> None: is completed. For example, alpha-values of 0, 0.5, and 1 correspond to the animation being completed 0%, 50%, and 100%, respectively. """ - families = list(self.get_all_families_zipped()) + families = tuple(self.get_all_families_zipped()) for i, mobs in enumerate(families): sub_alpha = self.get_sub_alpha(alpha, i, len(families)) - self.interpolate_submobject(*mobs, sub_alpha) + self.interpolate_submobject(*mobs, sub_alpha) # type: ignore def interpolate_submobject( self, - submobject: Mobject, - starting_submobject: Mobject, + submobject: OpenGLMobject, + starting_submobject: OpenGLMobject, # target_copy: Mobject, #Todo: fix - signature of interpolate_submobject differs in Transform(). alpha: float, ) -> Animation: - # Typically implemented by subclass - pass + raise NotImplementedError("Implement in subclass") def get_sub_alpha(self, alpha: float, index: int, num_submobjects: int) -> float: """Get the animation progress of any submobjects subanimation. @@ -387,13 +376,14 @@ def get_sub_alpha(self, alpha: float, index: int, num_submobjects: int) -> float full_length = (num_submobjects - 1) * lag_ratio + 1 value = alpha * full_length lower = index * lag_ratio + raw_sub_alpha = np.clip((value - lower), 0, 1) if self.reverse_rate_function: - return self.rate_func(1 - (value - lower)) + return self.rate_func(1 - raw_sub_alpha) else: - return self.rate_func(value - lower) + return self.rate_func(raw_sub_alpha) # Getters and setters - def set_run_time(self, run_time: float) -> Animation: + def set_run_time(self, run_time: float) -> Self: """Set the run time of the animation. Parameters @@ -428,7 +418,7 @@ def get_run_time(self) -> float: def set_rate_func( self, rate_func: Callable[[float], float], - ) -> Animation: + ) -> Self: """Set the rate function of the animation. Parameters @@ -457,7 +447,7 @@ def get_rate_func( """ return self.rate_func - def set_name(self, name: str) -> Animation: + def set_name(self, name: str) -> Self: """Set the name of the animation. Parameters @@ -473,26 +463,6 @@ def set_name(self, name: str) -> Animation: self.name = name return self - def is_remover(self) -> bool: - """Test if the animation is a remover. - - Returns - ------- - bool - ``True`` if the animation is a remover, ``False`` otherwise. - """ - return self.remover - - def is_introducer(self) -> bool: - """Test if the animation is an introducer. - - Returns - ------- - bool - ``True`` if the animation is an introducer, ``False`` otherwise. - """ - return self.introducer - @classmethod def __init_subclass__(cls, **kwargs) -> None: super().__init_subclass__(**kwargs) @@ -540,9 +510,23 @@ def construct(self): cls.__init__ = cls._original__init__ +@overload +def prepare_animation(anim: MobjectAnimation[M]) -> MobjectAnimation[M]: ... + + +@overload +def prepare_animation( + anim: AnimationProtocol + | opengl_mobject._AnimationBuilder + | opengl_mobject.OpenGLMobject, +) -> AnimationProtocol: ... + + def prepare_animation( - anim: Animation | mobject._AnimationBuilder, -) -> Animation: + anim: AnimationProtocol + | opengl_mobject._AnimationBuilder + | opengl_mobject.OpenGLMobject, +) -> AnimationProtocol: r"""Returns either an unchanged animation, or the animation built from a passed animation factory. @@ -569,16 +553,17 @@ def prepare_animation( TypeError: Object 42 cannot be converted to an animation """ - if isinstance(anim, mobject._AnimationBuilder): - return anim.build() - - if isinstance(anim, opengl_mobject._AnimationBuilder): + if isinstance(anim, (mobject._AnimationBuilder, opengl_mobject._AnimationBuilder)): return anim.build() - if isinstance(anim, Animation): - return anim - - raise TypeError(f"Object {anim} cannot be converted to an animation") + # if it has these three methods it probably is an AnimationProtocol + # but we don't use isinstance because it's slow + try: + for method in ("begin", "finish", "update_mobjects"): + getattr(anim, method) + return cast(AnimationProtocol, anim) + except AttributeError: + raise TypeError(f"Object {anim} cannot be converted to an animation") from None class Wait(Animation): @@ -615,12 +600,9 @@ def __init__( if stop_condition and frozen_frame: raise ValueError("A static Wait animation cannot have a stop condition.") - self.duration: float = run_time self.stop_condition = stop_condition - self.is_static_wait: bool = frozen_frame + self.is_static_wait: bool = bool(frozen_frame) super().__init__(None, run_time=run_time, rate_func=rate_func, **kwargs) - # quick fix to work in opengl setting: - self.mobject.shader_wrapper_list = [] def begin(self) -> None: pass @@ -628,9 +610,6 @@ def begin(self) -> None: def finish(self) -> None: pass - def clean_up_from_scene(self, scene: Scene) -> None: - pass - def update_mobjects(self, dt: float) -> None: pass @@ -760,9 +739,10 @@ def construct(self): self.play(FadeIn(MySquare())) """ + _F = TypeVar("_F", bound=Callable) - def decorator(func): - func._override_animation = animation_class + def decorator(func: _F) -> _F: + func._override_animation = animation_class # type: ignore return func return decorator diff --git a/manim/animation/changing.py b/manim/animation/changing.py index bb11cfc0a4..f28d0c8ff9 100644 --- a/manim/animation/changing.py +++ b/manim/animation/changing.py @@ -4,10 +4,16 @@ __all__ = ["AnimatedBoundary", "TracedPath"] -from typing import Callable +from typing import TYPE_CHECKING, Callable + +if TYPE_CHECKING: + import numpy.typing as npt + +import numpy as np from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL -from manim.mobject.types.vectorized_mobject import VGroup, VMobject +from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVGroup as VGroup +from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject as VMobject from manim.utils.color import ( BLUE_B, BLUE_D, @@ -60,7 +66,7 @@ def __init__( ] self.add(*self.boundary_copies) self.total_time = 0 - self.add_updater(lambda m, dt: self.update_boundary_copies(dt)) + self.add_updater(lambda _, dt: self.update_boundary_copies(dt)) def update_boundary_copies(self, dt): # Not actual time, but something which passes at @@ -142,23 +148,32 @@ def construct(self): def __init__( self, - traced_point_func: Callable, + traced_point_func: Callable[ + [], npt.NDArray[npt.float] + ], # TODO: Replace with Callable[[], Point3D] stroke_width: float = 2, stroke_color: ParsableManimColor | None = WHITE, dissipating_time: float | None = None, + fill_opacity: float = 0, **kwargs, ): - super().__init__(stroke_color=stroke_color, stroke_width=stroke_width, **kwargs) + super().__init__( + stroke_color=stroke_color, + stroke_width=stroke_width, + fill_opacity=fill_opacity, + **kwargs, + ) self.traced_point_func = traced_point_func self.dissipating_time = dissipating_time self.time = 1 if self.dissipating_time else None self.add_updater(self.update_path) - def update_path(self, mob, dt): + def update_path(self, _mob, dt): new_point = self.traced_point_func() if not self.has_points(): self.start_new_path(new_point) - self.add_line_to(new_point) + if not np.allclose(self.get_end(), new_point): + self.add_line_to(new_point) if self.dissipating_time: self.time += dt if self.time - 1 > self.dissipating_time: diff --git a/manim/animation/composition.py b/manim/animation/composition.py index 128066ba80..9dbda10767 100644 --- a/manim/animation/composition.py +++ b/manim/animation/composition.py @@ -2,7 +2,6 @@ from __future__ import annotations -import types from collections.abc import Iterable, Sequence from typing import TYPE_CHECKING, Callable @@ -13,7 +12,6 @@ from manim.constants import RendererType from manim.mobject.mobject import Group, Mobject from manim.mobject.opengl.opengl_mobject import OpenGLGroup -from manim.scene.scene import Scene from manim.utils.iterables import remove_list_redundancies from manim.utils.parameter_parsing import flatten_iterable_parameters from manim.utils.rate_functions import linear @@ -54,20 +52,21 @@ class AnimationGroup(Animation): def __init__( self, - *animations: Animation | Iterable[Animation] | types.GeneratorType[Animation], - group: Group | VGroup | OpenGLGroup | OpenGLVGroup = None, + *animations: Animation | Iterable[Animation], + group: Group | VGroup | OpenGLGroup | OpenGLVGroup | None = None, run_time: float | None = None, rate_func: Callable[[float], float] = linear, lag_ratio: float = 0, **kwargs, ) -> None: arg_anim = flatten_iterable_parameters(animations) + self.animations = [prepare_animation(anim) for anim in arg_anim] self.rate_func = rate_func self.group = group if self.group is None: mobjects = remove_list_redundancies( - [anim.mobject for anim in self.animations if not anim.is_introducer()], + [anim.mobject for anim in self.animations if not anim.introducer], ) if config["renderer"] == RendererType.OPENGL: self.group = OpenGLGroup(*mobjects) @@ -87,30 +86,31 @@ def begin(self) -> None: f"Trying to play {self} without animations, this is not supported. " "Please add at least one subanimation." ) - self.anim_group_time = 0.0 - if self.suspend_mobject_updating: - self.group.suspend_updating() + for anim in self.animations: + if self.introducer: + anim.introducer = True anim.begin() + self.process_subanimation_buffer(anim.buffer) - def _setup_scene(self, scene) -> None: - for anim in self.animations: - anim._setup_scene(scene) + self.anim_group_time = 0.0 + if self.suspend_mobject_updating: + self.group.suspend_updating() def finish(self) -> None: for anim in self.animations: anim.finish() self.anims_begun[:] = True self.anims_finished[:] = True - if self.suspend_mobject_updating: - self.group.resume_updating() - - def clean_up_from_scene(self, scene: Scene) -> None: - self._on_finish(scene) for anim in self.animations: if self.remover: - anim.remover = self.remover - anim.clean_up_from_scene(scene) + anim.remover = True + anim.finish() + self.process_subanimation_buffer(anim.buffer) + + if self.suspend_mobject_updating: + self.group.resume_updating() + self._on_finish(self.buffer) def update_mobjects(self, dt: float) -> None: for anim in self.anims_with_timings["anim"][ @@ -247,16 +247,6 @@ def update_mobjects(self, dt: float) -> None: if self.active_animation: self.active_animation.update_mobjects(dt) - def _setup_scene(self, scene) -> None: - if scene is None: - return - if self.is_introducer(): - for anim in self.animations: - if not anim.is_introducer() and anim.mobject is not None: - scene.add(anim.mobject) - - self.scene = scene - def update_active_animation(self, index: int) -> None: self.active_index = index if index >= len(self.animations): @@ -265,8 +255,9 @@ def update_active_animation(self, index: int) -> None: self.active_end_time: float | None = None else: self.active_animation = self.animations[index] - self.active_animation._setup_scene(self.scene) self.active_animation.begin() + self.process_subanimation_buffer(self.active_animation.buffer) + self.apply_buffer = True self.active_start_time = self.anims_with_timings[index]["start"] self.active_end_time = self.anims_with_timings[index]["end"] @@ -277,6 +268,7 @@ def next_animation(self) -> None: """ if self.active_animation is not None: self.active_animation.finish() + self.process_subanimation_buffer(self.active_animation.buffer) self.update_active_animation(self.active_index + 1) def interpolate(self, alpha: float) -> None: diff --git a/manim/animation/creation.py b/manim/animation/creation.py index dc3ec69527..8a225dc6fe 100644 --- a/manim/animation/creation.py +++ b/manim/animation/creation.py @@ -82,19 +82,22 @@ def construct(self): import numpy as np if TYPE_CHECKING: + from typing_extensions import Self + from manim.mobject.text.text_mobject import Text from manim.scene.scene import Scene from manim.constants import RIGHT, TAU from manim.mobject.opengl.opengl_surface import OpenGLSurface from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject +from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject as VMobject from manim.utils.color import ManimColor from .. import config from ..animation.animation import Animation from ..animation.composition import Succession -from ..mobject.mobject import Group, Mobject -from ..mobject.types.vectorized_mobject import VMobject +from ..mobject.mobject import Group +from ..mobject.opengl.opengl_mobject import OpenGLMobject from ..utils.bezier import integer_interpolate from ..utils.rate_functions import double_smooth, linear @@ -125,15 +128,16 @@ def __init__( def interpolate_submobject( self, - submobject: Mobject, - starting_submobject: Mobject, + submobject: OpenGLMobject, + starting_submobject: OpenGLMobject, alpha: float, - ) -> None: + ) -> Self: submobject.pointwise_become_partial( starting_submobject, *self._get_bounds(alpha) ) + return self - def _get_bounds(self, alpha: float) -> None: + def _get_bounds(self, alpha: float) -> tuple[float, float]: raise NotImplementedError("Please use Create or ShowPassingFlash") @@ -228,7 +232,7 @@ def __init__( run_time: float = 2, rate_func: Callable[[float], float] = double_smooth, stroke_width: float = 2, - stroke_color: str = None, + stroke_color: ManimColor | None = None, draw_border_animation_config: dict = {}, # what does this dict accept? fill_animation_config: dict = {}, introducer: bool = True, @@ -248,17 +252,19 @@ def __init__( self.fill_animation_config = fill_animation_config self.outline = self.get_outline() - def _typecheck_input(self, vmobject: VMobject | OpenGLVMobject) -> None: - if not isinstance(vmobject, (VMobject, OpenGLVMobject)): + def _typecheck_input(self, vmobject: OpenGLVMobject) -> None: + if not isinstance(vmobject, OpenGLVMobject): raise TypeError( f"{self.__class__.__name__} only works for vectorized Mobjects" ) def begin(self) -> None: + # this self.get_outline() has to be called + # before super().begin(), for whatever reason self.outline = self.get_outline() super().begin() - def get_outline(self) -> Mobject: + def get_outline(self) -> OpenGLMobject: outline = self.mobject.copy() outline.set_fill(opacity=0) for sm in outline.family_members_with_points(): @@ -272,16 +278,16 @@ def get_stroke_color(self, vmobject: VMobject | OpenGLVMobject) -> ManimColor: return vmobject.get_stroke_color() return vmobject.get_color() - def get_all_mobjects(self) -> Sequence[Mobject]: + def get_all_mobjects(self) -> Sequence[OpenGLMobject]: return [*super().get_all_mobjects(), self.outline] def interpolate_submobject( self, - submobject: Mobject, - starting_submobject: Mobject, - outline, + submobject: OpenGLMobject, + starting_submobject: OpenGLMobject, + outline: OpenGLMobject, alpha: float, - ) -> None: # Fixme: not matching the parent class? What is outline doing here? + ) -> None: index: int subalpha: float index, subalpha = integer_interpolate(0, 2, alpha) @@ -324,10 +330,10 @@ def __init__( vmobject: VMobject | OpenGLVMobject, rate_func: Callable[[float], float] = linear, reverse: bool = False, + run_time: float | None = None, + lag_ratio: float | None = None, **kwargs, ) -> None: - run_time: float | None = kwargs.pop("run_time", None) - lag_ratio: float | None = kwargs.pop("lag_ratio", None) run_time, lag_ratio = self._set_default_config_from_length( vmobject, run_time, @@ -358,18 +364,15 @@ def _set_default_config_from_length( lag_ratio = min(4.0 / max(1.0, length), 0.2) return run_time, lag_ratio - def reverse_submobjects(self) -> None: - self.mobject.invert(recursive=True) - def begin(self) -> None: if self.reverse: - self.reverse_submobjects() + self.mobject.reverse_submobjects(recursive=True) super().begin() def finish(self) -> None: super().finish() if self.reverse: - self.reverse_submobjects() + self.mobject.reverse_submobjects(recursive=True) class Unwrite(Write): @@ -454,7 +457,7 @@ def construct(self): def __init__( self, - shapes: Mobject, + shapes: OpenGLMobject, scale_factor: float = 8, fade_in_fraction=0.3, **kwargs, @@ -474,7 +477,7 @@ def __init__( super().__init__(shapes, introducer=True, **kwargs) - def interpolate_mobject(self, alpha: float) -> None: + def interpolate(self, alpha: float) -> None: alpha = self.rate_func(alpha) for original_shape, shape in zip(self.shapes, self.mobject): shape.restore() @@ -511,7 +514,7 @@ def construct(self): def __init__( self, - group: Mobject, + group: OpenGLMobject, suspend_mobject_updating: bool = False, int_func: Callable[[np.ndarray], np.ndarray] = np.floor, reverse_rate_function=False, @@ -528,7 +531,7 @@ def __init__( **kwargs, ) - def interpolate_mobject(self, alpha: float) -> None: + def interpolate(self, alpha: float) -> None: n_submobs = len(self.all_submobs) value = ( 1 - self.rate_func(alpha) @@ -639,7 +642,7 @@ class ShowSubmobjectsOneByOne(ShowIncreasingSubsets): def __init__( self, - group: Iterable[Mobject], + group: Iterable[OpenGLMobject], int_func: Callable[[np.ndarray], np.ndarray] = np.ceil, **kwargs, ) -> None: @@ -724,7 +727,7 @@ def construct(self): def __init__( self, text: Text, - cursor: Mobject, + cursor: OpenGLMobject, buff: float = 0.1, keep_cursor_y: bool = True, leave_cursor_on: bool = True, diff --git a/manim/animation/fading.py b/manim/animation/fading.py index 79cd41a516..d63677d265 100644 --- a/manim/animation/fading.py +++ b/manim/animation/fading.py @@ -19,18 +19,22 @@ def construct(self): "FadeIn", ] +from typing import TYPE_CHECKING + import numpy as np from manim.mobject.opengl.opengl_mobject import OpenGLMobject from ..animation.transform import Transform from ..constants import ORIGIN -from ..mobject.mobject import Group, Mobject -from ..scene.scene import Scene +from ..mobject.mobject import Group + +if TYPE_CHECKING: + pass class _Fade(Transform): - """Fade :class:`~.Mobject` s in or out. + """Fade :class:`~.OpenGLMobject` s in or out. Parameters ---------- @@ -49,9 +53,9 @@ class _Fade(Transform): def __init__( self, - *mobjects: Mobject, + *mobjects: OpenGLMobject, shift: np.ndarray | None = None, - target_position: np.ndarray | Mobject | None = None, + target_position: np.ndarray | OpenGLMobject | None = None, scale: float = 1, **kwargs, ) -> None: @@ -62,7 +66,7 @@ def __init__( self.point_target = False if shift is None: if target_position is not None: - if isinstance(target_position, (Mobject, OpenGLMobject)): + if isinstance(target_position, OpenGLMobject): target_position = target_position.get_center() shift = target_position - mobject.get_center() self.point_target = True @@ -72,29 +76,29 @@ def __init__( self.scale_factor = scale super().__init__(mobject, **kwargs) - def _create_faded_mobject(self, fadeIn: bool) -> Mobject: + def _create_faded_mobject(self, fade_in: bool) -> OpenGLMobject: """Create a faded, shifted and scaled copy of the mobject. Parameters ---------- - fadeIn + fade_in Whether the faded mobject is used to fade in. Returns ------- - Mobject + OpenGLMobject The faded, shifted and scaled copy of the mobject. """ faded_mobject = self.mobject.copy() faded_mobject.fade(1) - direction_modifier = -1 if fadeIn and not self.point_target else 1 + direction_modifier = -1 if fade_in and not self.point_target else 1 faded_mobject.shift(self.shift_vector * direction_modifier) faded_mobject.scale(self.scale_factor) return faded_mobject class FadeIn(_Fade): - r"""Fade in :class:`~.Mobject` s. + r"""Fade in :class:`~.OpenGLMobject` s. Parameters ---------- @@ -131,18 +135,18 @@ def construct(self): """ - def __init__(self, *mobjects: Mobject, **kwargs) -> None: + def __init__(self, *mobjects: OpenGLMobject, **kwargs) -> None: super().__init__(*mobjects, introducer=True, **kwargs) def create_target(self): return self.mobject def create_starting_mobject(self): - return self._create_faded_mobject(fadeIn=True) + return self._create_faded_mobject(fade_in=True) class FadeOut(_Fade): - r"""Fade out :class:`~.Mobject` s. + r"""Fade out :class:`~.OpenGLMobject` s. Parameters ---------- @@ -179,12 +183,12 @@ def construct(self): """ - def __init__(self, *mobjects: Mobject, **kwargs) -> None: + def __init__(self, *mobjects: OpenGLMobject, **kwargs) -> None: super().__init__(*mobjects, remover=True, **kwargs) def create_target(self): - return self._create_faded_mobject(fadeIn=False) + return self._create_faded_mobject(fade_in=False) - def clean_up_from_scene(self, scene: Scene = None) -> None: - super().clean_up_from_scene(scene) + def begin(self) -> None: + super().begin() self.interpolate(0) diff --git a/manim/animation/indication.py b/manim/animation/indication.py index f931491b37..ab3141ef0d 100644 --- a/manim/animation/indication.py +++ b/manim/animation/indication.py @@ -48,7 +48,6 @@ def construct(self): from manim.mobject.geometry.line import Line from manim.mobject.geometry.polygram import Rectangle from manim.mobject.geometry.shape_matchers import SurroundingRectangle -from manim.scene.scene import Scene from .. import config from ..animation.animation import Animation @@ -307,7 +306,7 @@ def __init__(self, mobject: VMobject, time_width: float = 0.1, **kwargs) -> None self.time_width = time_width super().__init__(mobject, remover=True, introducer=True, **kwargs) - def _get_bounds(self, alpha: float) -> tuple[float]: + def _get_bounds(self, alpha: float) -> tuple[float, float]: tw = self.time_width upper = interpolate(0, 1 + tw, alpha) lower = upper - tw @@ -315,8 +314,8 @@ def _get_bounds(self, alpha: float) -> tuple[float]: lower = max(lower, 0) return (lower, upper) - def clean_up_from_scene(self, scene: Scene) -> None: - super().clean_up_from_scene(scene) + def finish(self) -> None: + super().finish() for submob, start in self.get_all_families_zipped(): submob.pointwise_become_partial(start, 0, 1) @@ -395,6 +394,7 @@ def __init__( time_width: float = 1, ripples: int = 1, run_time: float = 2, + introducer: bool = True, **kwargs, ) -> None: x_min = mobject.get_left()[0] @@ -469,7 +469,9 @@ def homotopy( nudge = wave(wave_phase) * vect return np.array([x, y, z]) + nudge - super().__init__(homotopy, mobject, run_time=run_time, **kwargs) + super().__init__( + homotopy, mobject, run_time=run_time, introducer=introducer, **kwargs + ) class Wiggle(Animation): @@ -550,6 +552,7 @@ def interpolate_submobject( ) +# TODO: get rid of this if condition madness class Circumscribe(Succession): r"""Draw a temporary line surrounding the mobject. diff --git a/manim/animation/movement.py b/manim/animation/movement.py index 0be7e01c15..2ebc6a0d26 100644 --- a/manim/animation/movement.py +++ b/manim/animation/movement.py @@ -18,7 +18,8 @@ from ..utils.rate_functions import linear if TYPE_CHECKING: - from ..mobject.mobject import Mobject, VMobject + from ..mobject.mobject import Mobject + from ..mobject.types.vectorized_mobject import VMobject class Homotopy(Animation): @@ -55,12 +56,12 @@ def __init__( **kwargs, ) -> None: self.homotopy = homotopy - self.apply_function_kwargs = ( - apply_function_kwargs if apply_function_kwargs is not None else {} - ) + self.apply_function_kwargs = apply_function_kwargs or {} super().__init__(mobject, run_time=run_time, **kwargs) - def function_at_time_t(self, t: float) -> tuple[float, float, float]: + def function_at_time_t( + self, t: float + ) -> Callable[[tuple[float, float, float]], tuple[float, float, float]]: return lambda p: self.homotopy(*p, t) def interpolate_submobject( @@ -69,7 +70,7 @@ def interpolate_submobject( starting_submobject: Mobject, alpha: float, ) -> None: - submobject.points = starting_submobject.points + submobject.match_points(starting_submobject) submobject.apply_function( self.function_at_time_t(alpha), **self.apply_function_kwargs ) @@ -123,7 +124,7 @@ def __init__( **kwargs, ) - def interpolate_mobject(self, alpha: float) -> None: + def interpolate(self, alpha: float) -> None: if hasattr(self, "last_alpha"): dt = self.virtual_time * ( self.rate_func(alpha) - self.rate_func(self.last_alpha) @@ -159,6 +160,6 @@ def __init__( mobject, suspend_mobject_updating=suspend_mobject_updating, **kwargs ) - def interpolate_mobject(self, alpha: float) -> None: + def interpolate(self, alpha: float) -> None: point = self.path.point_from_proportion(self.rate_func(alpha)) self.mobject.move_to(point) diff --git a/manim/animation/numbers.py b/manim/animation/numbers.py index 86bfe7154b..4f257170c1 100644 --- a/manim/animation/numbers.py +++ b/manim/animation/numbers.py @@ -31,7 +31,7 @@ def check_validity_of_input(self, decimal_mob: DecimalNumber) -> None: if not isinstance(decimal_mob, DecimalNumber): raise TypeError("ChangingDecimal can only take in a DecimalNumber") - def interpolate_mobject(self, alpha: float) -> None: + def interpolate(self, alpha: float) -> None: self.mobject.set_value(self.number_update_func(self.rate_func(alpha))) diff --git a/manim/animation/protocol.py b/manim/animation/protocol.py new file mode 100644 index 0000000000..2ad77fcde1 --- /dev/null +++ b/manim/animation/protocol.py @@ -0,0 +1,82 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Protocol + +from typing_extensions import TypeVar + +if TYPE_CHECKING: + from manim.mobject.opengl.opengl_mobject import OpenGLMobject + from manim.typing import RateFunc + + from .scene_buffer import SceneBuffer + +M = TypeVar("M", bound="OpenGLMobject", default="OpenGLMobject") + + +__all__ = ("AnimationProtocol",) + + +class AnimationProtocol(Protocol): + """A protocol that all animations must implement.""" + + buffer: SceneBuffer + """The interface to the scene. This can be used to add, remove, or replace mobjects on the scene.""" + + apply_buffer: bool + """Normally, the buffer is only applied at the beginning and end of an animation. + + To apply it mid animation, set :attr:`apply_buffer` to ``True``.""" + + def begin(self) -> object: + """Called before the animation starts. + + This is where all setup for the animation should be done, such + as creating copies/targets of the mobject to animate, etc. + """ + + def finish(self) -> object: + """Called after the animation finishes. + + This is where all cleanup should happen, such as removing + mobjects from the scene, etc. + """ + + def interpolate(self, alpha: float) -> object: + """This is called every frame of the animation. + + This method should update the animation to the given ``alpha`` value. + + Parameters + ---------- + alpha : a value in the interval :math:`[0, 1]` representing the proportion of the animation that has passed. + """ + + def get_run_time(self) -> float: + """Compute and return the run time of the animation.""" + raise NotImplementedError + + def update_rate_info( + self, + run_time: float | None, + rate_func: RateFunc | None, + lag_ratio: float | None, + ) -> object: + """Update the rate information for the animation. + + If any value is ``None``, it should not update + the animation's corresponding attribute. + """ + + def update_mobjects(self, dt: float) -> object: + """Update the mobjects during the animation. + + This method is called every frame of the animation + """ + + +class MobjectAnimation(AnimationProtocol, Protocol[M]): + mobject: M + """The mobject that is being animated.""" + + suspend_mobject_updating: bool + """Whether to suspend updating the mobject during the animation.""" diff --git a/manim/animation/rotation.py b/manim/animation/rotation.py index 7bdd42238a..e4be408005 100644 --- a/manim/animation/rotation.py +++ b/manim/animation/rotation.py @@ -4,49 +4,57 @@ __all__ = ["Rotating", "Rotate"] -from collections.abc import Sequence -from typing import TYPE_CHECKING, Callable +from typing import TYPE_CHECKING -import numpy as np - -from ..animation.animation import Animation -from ..animation.transform import Transform -from ..constants import OUT, PI, TAU -from ..utils.rate_functions import linear +from manim.animation.animation import Animation +from manim.constants import ORIGIN, OUT, PI, TAU +from manim.utils.rate_functions import linear if TYPE_CHECKING: - from ..mobject.mobject import Mobject + from manim.mobject.opengl.opengl_mobject import OpenGLMobject + from manim.typing import Point3D, RateFunc, Vector3D class Rotating(Animation): def __init__( self, - mobject: Mobject, - axis: np.ndarray = OUT, - radians: np.ndarray = TAU, - about_point: np.ndarray | None = None, - about_edge: np.ndarray | None = None, - run_time: float = 5, - rate_func: Callable[[float], float] = linear, + mobject: OpenGLMobject, + angle: float = TAU, + axis: Vector3D = OUT, + about_point: Point3D | None = None, + about_edge: Vector3D | None = None, + rate_func: RateFunc = linear, + suspend_mobject_updating: bool = False, **kwargs, - ) -> None: + ): + super().__init__( + mobject, + rate_func=rate_func, + suspend_mobject_updating=suspend_mobject_updating, + **kwargs, + ) + self.angle = angle self.axis = axis - self.radians = radians self.about_point = about_point self.about_edge = about_edge - super().__init__(mobject, run_time=run_time, rate_func=rate_func, **kwargs) - def interpolate_mobject(self, alpha: float) -> None: - self.mobject.become(self.starting_mobject) + def interpolate(self, alpha: float) -> None: + pairs = zip( + self.mobject.family_members_with_points(), + self.starting_mobject.family_members_with_points(), + ) + for sm1, sm2 in pairs: + sm1.points[:] = sm2.points + self.mobject.rotate( - self.rate_func(alpha) * self.radians, + self.rate_func(alpha) * self.angle, axis=self.axis, about_point=self.about_point, about_edge=self.about_edge, ) -class Rotate(Transform): +class Rotate(Rotating): """Animation that rotates a Mobject. Parameters @@ -78,37 +86,24 @@ def construct(self): rate_func=linear, ), Rotate(Square(side_length=0.5), angle=2*PI, rate_func=linear), - ) - + ) """ def __init__( self, - mobject: Mobject, + mobject: OpenGLMobject, angle: float = PI, - axis: np.ndarray = OUT, - about_point: Sequence[float] | None = None, - about_edge: Sequence[float] | None = None, + axis: Vector3D = OUT, + run_time: float = 1, + about_edge: Vector3D = ORIGIN, **kwargs, - ) -> None: - if "path_arc" not in kwargs: - kwargs["path_arc"] = angle - if "path_arc_axis" not in kwargs: - kwargs["path_arc_axis"] = axis - self.angle = angle - self.axis = axis - self.about_edge = about_edge - self.about_point = about_point - if self.about_point is None: - self.about_point = mobject.get_center() - super().__init__(mobject, path_arc_centers=self.about_point, **kwargs) - - def create_target(self) -> Mobject: - target = self.mobject.copy() - target.rotate( - self.angle, - axis=self.axis, - about_point=self.about_point, - about_edge=self.about_edge, + ): + super().__init__( + mobject, + angle, + axis, + run_time=run_time, + about_edge=about_edge, + introducer=True, + **kwargs, ) - return target diff --git a/manim/animation/scene_buffer.py b/manim/animation/scene_buffer.py new file mode 100644 index 0000000000..58ff851fe4 --- /dev/null +++ b/manim/animation/scene_buffer.py @@ -0,0 +1,81 @@ +from __future__ import annotations + +from collections.abc import Iterator, Sequence +from enum import Enum +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from manim.mobject.opengl.opengl_mobject import OpenGLMobject + +__all__ = ["SceneBuffer", "SceneOperation"] + + +class SceneOperation(Enum): + ADD = "add" + REMOVE = "remove" + REPLACE = "replace" + + +class SceneBuffer: + """ + A "buffer" between :class:`.Scene` and :class:`.Animation` + + Operations an animation wants to do on :class:`.Scene` should be + done here (eg. :meth:`.Scene.add`, :meth:`.Scene.remove`). The + scene will then apply these changes at specific points (namely + at the beginning and end of animations) + + It is the scenes job to clear the buffer in between the beginning + and end of animations. + + To iterate over the operations, simply iterate over the buffer. + + Example + ------- + + .. code-block:: pycon + + >>> buffer = SceneBuffer() + >>> buffer.add(Square()) + >>> buffer.remove(Circle()) + >>> buffer.replace(Square(), Circle(), flag=True) + >>> for operation in buffer: + ... print(operation) + (SceneOperation.ADD, (Square(),), {}) + (SceneOperation.REMOVE, (Circle(),), {}) + (SceneOperation.REPLACE, (Square(), Circle()), {"flag": True}) + """ + + def __init__(self) -> None: + self.operations: list[ + tuple[SceneOperation, Sequence[OpenGLMobject], dict[str, Any]] + ] = [] + + def add(self, *mobs: OpenGLMobject, **kwargs: Any) -> None: + """Add mobjects to the scene.""" + self.operations.append((SceneOperation.ADD, mobs, kwargs)) + + def remove(self, *mobs: OpenGLMobject, **kwargs: Any) -> None: + """Remove mobjects from the scene.""" + self.operations.append((SceneOperation.REMOVE, mobs, kwargs)) + + def replace( + self, mob: OpenGLMobject, *replacements: OpenGLMobject, **kwargs: Any + ) -> None: + """Replace a ``mob`` with ``replacements`` on the scene.""" + self.operations.append((SceneOperation.REPLACE, (mob, *replacements), kwargs)) + + def clear(self) -> None: + """Clear the buffer.""" + self.operations.clear() + + def __str__(self) -> str: + operations = self.operations + return f"{type(self).__name__}({operations=})" + + __repr__ = __str__ + + def __iter__( + self, + ) -> Iterator[tuple[SceneOperation, Sequence[OpenGLMobject], dict[str, Any]]]: + return iter(self.operations) diff --git a/manim/animation/speedmodifier.py b/manim/animation/speedmodifier.py index 63b9b2e5b3..88a00671d1 100644 --- a/manim/animation/speedmodifier.py +++ b/manim/animation/speedmodifier.py @@ -11,10 +11,10 @@ from ..animation.animation import Animation, Wait, prepare_animation from ..animation.composition import AnimationGroup from ..mobject.mobject import Mobject, _AnimationBuilder -from ..scene.scene import Scene if TYPE_CHECKING: from ..mobject.mobject import Updater + from .protocol import MobjectAnimation __all__ = ["ChangeSpeed"] @@ -101,7 +101,7 @@ def __init__( affects_speed_updaters: bool = True, **kwargs, ) -> None: - if issubclass(type(anim), AnimationGroup): + if isinstance(anim, AnimationGroup): self.anim = type(anim)( *map(self.setup, anim.animations), group=anim.group, @@ -208,11 +208,11 @@ def func(t): super().__init__( self.anim.mobject, rate_func=self.rate_func, - run_time=scaled_total_time * self.anim.run_time, + run_time=scaled_total_time * self.anim.get_run_time(), **kwargs, ) - def setup(self, anim): + def setup(self, anim: MobjectAnimation): if type(anim) is Wait: anim.interpolate = types.MethodType( lambda self, alpha: self.rate_func(alpha), anim @@ -281,15 +281,11 @@ def interpolate(self, alpha: float) -> None: def update_mobjects(self, dt: float) -> None: self.anim.update_mobjects(dt) - def finish(self) -> None: - ChangeSpeed.is_changing_dt = False - self.anim.finish() - def begin(self) -> None: self.anim.begin() + self.process_subanimation_buffer(self.anim.buffer) - def clean_up_from_scene(self, scene: Scene) -> None: - self.anim.clean_up_from_scene(scene) - - def _setup_scene(self, scene) -> None: - self.anim._setup_scene(scene) + def finish(self) -> None: + ChangeSpeed.is_changing_dt = False + self.anim.finish() + self.process_subanimation_buffer(self.anim.buffer) diff --git a/manim/animation/transform.py b/manim/animation/transform.py index a7c62a9e6a..a6ae671bba 100644 --- a/manim/animation/transform.py +++ b/manim/animation/transform.py @@ -2,6 +2,8 @@ from __future__ import annotations +from manim.typing import PathFuncType + __all__ = [ "Transform", "ReplacementTransform", @@ -28,28 +30,26 @@ import inspect import types -from collections.abc import Iterable, Sequence +from collections.abc import Sequence from typing import TYPE_CHECKING, Any, Callable import numpy as np -from manim.mobject.opengl.opengl_mobject import OpenGLGroup, OpenGLMobject +from manim.mobject.opengl.opengl_mobject import OpenGLMobject -from .. import config from ..animation.animation import Animation from ..constants import ( DEFAULT_POINTWISE_FUNCTION_RUN_TIME, DEGREES, ORIGIN, OUT, - RendererType, ) from ..mobject.mobject import Group, Mobject from ..utils.paths import path_along_arc, path_along_circles from ..utils.rate_functions import smooth, squish_rate_func if TYPE_CHECKING: - from ..scene.scene import Scene + pass class Transform(Animation): @@ -127,12 +127,12 @@ def make_arc_path(start, end, arc_angle): def __init__( self, - mobject: Mobject | None, - target_mobject: Mobject | None = None, + mobject: OpenGLMobject | None, + target_mobject: OpenGLMobject | None = None, path_func: Callable | None = None, path_arc: float = 0, path_arc_axis: np.ndarray = OUT, - path_arc_centers: np.ndarray = None, + path_arc_centers: np.ndarray | None = None, replace_mobject_with_target_in_scene: bool = False, **kwargs, ) -> None: @@ -153,8 +153,8 @@ def __init__( self.replace_mobject_with_target_in_scene: bool = ( replace_mobject_with_target_in_scene ) - self.target_mobject: Mobject = ( - target_mobject if target_mobject is not None else Mobject() + self.target_mobject: OpenGLMobject = ( + target_mobject if target_mobject is not None else OpenGLMobject() ) super().__init__(mobject, **kwargs) @@ -173,48 +173,42 @@ def path_arc(self, path_arc: float) -> None: @property def path_func( self, - ) -> Callable[ - [Iterable[np.ndarray], Iterable[np.ndarray], float], - Iterable[np.ndarray], - ]: + ) -> PathFuncType: return self._path_func @path_func.setter def path_func( self, - path_func: Callable[ - [Iterable[np.ndarray], Iterable[np.ndarray], float], - Iterable[np.ndarray], - ], + path_func: PathFuncType, ) -> None: if path_func is not None: self._path_func = path_func def begin(self) -> None: - # Use a copy of target_mobject for the align_data - # call so that the actual target_mobject stays - # preserved. self.target_mobject = self.create_target() - self.target_copy = self.target_mobject.copy() - # Note, this potentially changes the structure - # of both mobject and target_mobject - if config.renderer == RendererType.OPENGL: - self.mobject.align_data_and_family(self.target_copy) + if self.mobject.is_aligned_with(self.target_mobject): + self.target_copy = self.target_mobject else: - self.mobject.align_data(self.target_copy) + # Use a copy of target_mobject for the align_data_and_family + # call so that the actual target_mobject stays + # preserved, since calling align_data will potentially + # change the structure of both arguments + self.target_copy = self.target_mobject.copy() + self.mobject.align_data_and_family(self.target_copy) + super().begin() - def create_target(self) -> Mobject: + def create_target(self) -> OpenGLMobject: # Has no meaningful effect here, but may be useful # in subclasses return self.target_mobject - def clean_up_from_scene(self, scene: Scene) -> None: - super().clean_up_from_scene(scene) + def finish(self) -> None: + super().finish() if self.replace_mobject_with_target_in_scene: - scene.replace(self.mobject, self.target_mobject) + self.buffer.replace(self.mobject, self.target_mobject) - def get_all_mobjects(self) -> Sequence[Mobject]: + def get_all_mobjects(self) -> Sequence[OpenGLMobject]: return [ self.mobject, self.starting_mobject, @@ -222,21 +216,21 @@ def get_all_mobjects(self) -> Sequence[Mobject]: self.target_copy, ] - def get_all_families_zipped(self) -> Iterable[tuple]: # more precise typing? + def get_all_families_zipped( + self, + ) -> zip[tuple[OpenGLMobject, OpenGLMobject, OpenGLMobject]]: mobs = [ self.mobject, self.starting_mobject, self.target_copy, ] - if config.renderer == RendererType.OPENGL: - return zip(*(mob.get_family() for mob in mobs)) - return zip(*(mob.family_members_with_points() for mob in mobs)) + return zip(*(mob.get_family() for mob in mobs)) def interpolate_submobject( self, - submobject: Mobject, - starting_submobject: Mobject, - target_copy: Mobject, + submobject: OpenGLMobject, + starting_submobject: OpenGLMobject, + target_copy: OpenGLMobject, alpha: float, ) -> Transform: submobject.interpolate(starting_submobject, target_copy, alpha, self.path_func) @@ -291,7 +285,9 @@ def construct(self): """ - def __init__(self, mobject: Mobject, target_mobject: Mobject, **kwargs) -> None: + def __init__( + self, mobject: OpenGLMobject, target_mobject: OpenGLMobject, **kwargs + ) -> None: super().__init__( mobject, target_mobject, replace_mobject_with_target_in_scene=True, **kwargs ) @@ -300,7 +296,9 @@ def __init__(self, mobject: Mobject, target_mobject: Mobject, **kwargs) -> None: class TransformFromCopy(Transform): """Performs a reversed Transform""" - def __init__(self, mobject: Mobject, target_mobject: Mobject, **kwargs) -> None: + def __init__( + self, mobject: OpenGLMobject, target_mobject: OpenGLMobject, **kwargs + ) -> None: super().__init__(target_mobject, mobject, **kwargs) def interpolate(self, alpha: float) -> None: @@ -339,8 +337,8 @@ def construct(self): def __init__( self, - mobject: Mobject, - target_mobject: Mobject, + mobject: OpenGLMobject, + target_mobject: OpenGLMobject, path_arc: float = -np.pi, **kwargs, ) -> None: @@ -388,8 +386,8 @@ def construct(self): def __init__( self, - mobject: Mobject, - target_mobject: Mobject, + mobject: OpenGLMobject, + target_mobject: OpenGLMobject, path_arc: float = np.pi, **kwargs, ) -> None: @@ -422,11 +420,11 @@ def construct(self): """ - def __init__(self, mobject: Mobject, **kwargs) -> None: + def __init__(self, mobject: OpenGLMobject, **kwargs) -> None: self.check_validity_of_input(mobject) super().__init__(mobject, mobject.target, **kwargs) - def check_validity_of_input(self, mobject: Mobject) -> None: + def check_validity_of_input(self, mobject: OpenGLMobject) -> None: if not hasattr(mobject, "target"): raise ValueError( "MoveToTarget called on mobject" "without attribute 'target'", @@ -479,7 +477,7 @@ def check_validity_of_input(self, method: Callable) -> None: ) assert isinstance(method.__self__, (Mobject, OpenGLMobject)) - def create_target(self) -> Mobject: + def create_target(self) -> OpenGLMobject: method = self.method # Make sure it's a list so that args.pop() works args = list(self.method_args) @@ -525,7 +523,9 @@ def __init__( class ApplyPointwiseFunctionToCenter(ApplyPointwiseFunction): - def __init__(self, function: types.MethodType, mobject: Mobject, **kwargs) -> None: + def __init__( + self, function: types.MethodType, mobject: OpenGLMobject, **kwargs + ) -> None: self.function = function super().__init__(mobject.move_to, **kwargs) @@ -615,7 +615,9 @@ def __init__(self, mobject: Mobject, **kwargs) -> None: class ApplyFunction(Transform): - def __init__(self, function: types.MethodType, mobject: Mobject, **kwargs) -> None: + def __init__( + self, function: types.MethodType, mobject: OpenGLMobject, **kwargs + ) -> None: self.function = function super().__init__(mobject, **kwargs) @@ -686,6 +688,7 @@ def __init__(self, function: types.MethodType, mobject: Mobject, **kwargs) -> No super().__init__(method, function, **kwargs) def _init_path_func(self) -> None: + # TODO: this seems broken? func1 = self.function(complex(1)) self.path_arc = np.log(func1).imag super()._init_path_func() @@ -834,10 +837,7 @@ def __init__(self, mobject, target_mobject, stretch=True, dim_to_match=1, **kwar self.stretch = stretch self.dim_to_match = dim_to_match mobject.save_state() - if config.renderer == RendererType.OPENGL: - group = OpenGLGroup(mobject, target_mobject.copy()) - else: - group = Group(mobject, target_mobject.copy()) + group = Group(mobject, target_mobject.copy()) super().__init__(group, **kwargs) def begin(self): @@ -878,11 +878,11 @@ def get_all_mobjects(self) -> Sequence[Mobject]: def get_all_families_zipped(self): return Animation.get_all_families_zipped(self) - def clean_up_from_scene(self, scene): - Animation.clean_up_from_scene(self, scene) - scene.remove(self.mobject) + def finish(self): + Animation.finish(self) # TODO: is this really needed over super()? + self.buffer.remove(self.mobject) self.mobject[0].restore() - scene.add(self.to_add_on_completion) + self.buffer.add(self.to_add_on_completion) class FadeTransformPieces(FadeTransform): diff --git a/manim/animation/transform_matching_parts.py b/manim/animation/transform_matching_parts.py index dbf5dd294e..9758982fd1 100644 --- a/manim/animation/transform_matching_parts.py +++ b/manim/animation/transform_matching_parts.py @@ -4,7 +4,6 @@ __all__ = ["TransformMatchingShapes", "TransformMatchingTex"] -from typing import TYPE_CHECKING import numpy as np @@ -19,9 +18,6 @@ from .fading import FadeIn, FadeOut from .transform import FadeTransformPieces, Transform -if TYPE_CHECKING: - from ..scene.scene import Scene - class TransformMatchingAbstractBase(AnimationGroup): """Abstract base class for transformations that keep track of matching parts. @@ -147,19 +143,20 @@ def get_shape_map(self, mobject: Mobject) -> dict: key = self.get_mobject_key(sm) if key not in shape_map: if config["renderer"] == RendererType.OPENGL: - shape_map[key] = OpenGLVGroup() + shape_map[key] = VGroup() else: shape_map[key] = VGroup() shape_map[key].add(sm) return shape_map - def clean_up_from_scene(self, scene: Scene) -> None: + def finish(self) -> None: + super().finish() # Interpolate all animations back to 0 to ensure source mobjects remain unchanged. for anim in self.animations: anim.interpolate(0) - scene.remove(self.mobject) - scene.remove(*self.to_remove) - scene.add(self.to_add) + self.buffer.remove(self.mobject) + self.buffer.remove(*self.to_remove) + self.buffer.add(self.to_add) @staticmethod def get_mobject_parts(mobject: Mobject): diff --git a/manim/animation/updaters/mobject_update_utils.py b/manim/animation/updaters/mobject_update_utils.py index 213180f3bd..1d9584a363 100644 --- a/manim/animation/updaters/mobject_update_utils.py +++ b/manim/animation/updaters/mobject_update_utils.py @@ -3,53 +3,64 @@ from __future__ import annotations __all__ = [ - "assert_is_mobject_method", "always", "f_always", "always_redraw", - "always_shift", - "always_rotate", "turn_animation_into_updater", "cycle_animation", ] import inspect -from typing import TYPE_CHECKING, Callable +from typing import TYPE_CHECKING, Any, Callable, TypeVar, cast import numpy as np -from manim.constants import DEGREES, RIGHT -from manim.mobject.mobject import Mobject -from manim.opengl import OpenGLMobject -from manim.utils.space_ops import normalize +from manim.mobject.opengl.opengl_mobject import OpenGLMobject if TYPE_CHECKING: - from manim.animation.animation import Animation + import types + from typing_extensions import Concatenate, ParamSpec, TypeIs -def assert_is_mobject_method(method: Callable) -> None: - assert inspect.ismethod(method) - mobject = method.__self__ - assert isinstance(mobject, (Mobject, OpenGLMobject)) + from manim.animation.protocol import MobjectAnimation + P = ParamSpec("P") -def always(method: Callable, *args, **kwargs) -> Mobject: - assert_is_mobject_method(method) - mobject = method.__self__ + +M = TypeVar("M", bound=OpenGLMobject) + + +# TODO: figure out how to typehint as MethodType[OpenGLMobject] to avoid the cast +# madness in always/f_always +def is_mobject_method(method: Callable[..., Any]) -> TypeIs[types.MethodType]: + return inspect.ismethod(method) and isinstance(method.__self__, OpenGLMobject) + + +def always( + method: Callable[Concatenate[M, P], object], *args: P.args, **kwargs: P.kwargs +) -> M: + if not is_mobject_method(method): + raise ValueError("always must take a method of a Mobject") + mobject = cast(M, method.__self__) func = method.__func__ mobject.add_updater(lambda m: func(m, *args, **kwargs)) return mobject -def f_always(method: Callable[[Mobject], None], *arg_generators, **kwargs) -> Mobject: +def f_always( + method: Callable[Concatenate[M, ...], None], + *arg_generators: Callable[[], object], + **kwargs, +) -> M: """ More functional version of always, where instead of taking in args, it takes in functions which output the relevant arguments. """ - assert_is_mobject_method(method) - mobject = method.__self__ + if not is_mobject_method(method): + raise ValueError("f_always must take a method of a Mobject") + mobject = cast(M, method.__self__) func = method.__func__ def updater(mob): @@ -60,7 +71,7 @@ def updater(mob): return mobject -def always_redraw(func: Callable[[], Mobject]) -> Mobject: +def always_redraw(func: Callable[[], M]) -> M: """Redraw the mobject constructed by a function every frame. This function returns a mobject with an attached updater that @@ -75,7 +86,6 @@ def always_redraw(func: Callable[[], Mobject]) -> Mobject: Examples -------- - .. manim:: TangentAnimation class TangentAnimation(Scene): @@ -105,81 +115,11 @@ def construct(self): return mob -def always_shift( - mobject: Mobject, direction: np.ndarray[np.float64] = RIGHT, rate: float = 0.1 -) -> Mobject: - """A mobject which is continuously shifted along some direction - at a certain rate. - - Parameters - ---------- - mobject - The mobject to shift. - direction - The direction to shift. The vector is normalized, the specified magnitude - is not relevant. - rate - Length in Manim units which the mobject travels in one - second along the specified direction. - - Examples - -------- - - .. manim:: ShiftingSquare - - class ShiftingSquare(Scene): - def construct(self): - sq = Square().set_fill(opacity=1) - tri = Triangle() - VGroup(sq, tri).arrange(LEFT) - - # construct a square which is continuously - # shifted to the right - always_shift(sq, RIGHT, rate=5) - - self.add(sq) - self.play(tri.animate.set_fill(opacity=1)) - """ - mobject.add_updater(lambda m, dt: m.shift(dt * rate * normalize(direction))) - return mobject - - -def always_rotate(mobject: Mobject, rate: float = 20 * DEGREES, **kwargs) -> Mobject: - """A mobject which is continuously rotated at a certain rate. - - Parameters - ---------- - mobject - The mobject to be rotated. - rate - The angle which the mobject is rotated by - over one second. - kwags - Further arguments to be passed to :meth:`.Mobject.rotate`. - - Examples - -------- - - .. manim:: SpinningTriangle - - class SpinningTriangle(Scene): - def construct(self): - tri = Triangle().set_fill(opacity=1).set_z_index(2) - sq = Square().to_edge(LEFT) - - # will keep spinning while there is an animation going on - always_rotate(tri, rate=2*PI, about_point=ORIGIN) - - self.add(tri, sq) - self.play(sq.animate.to_edge(RIGHT), rate_func=linear, run_time=1) - """ - mobject.add_updater(lambda m, dt: m.rotate(dt * rate, **kwargs)) - return mobject - - def turn_animation_into_updater( - animation: Animation, cycle: bool = False, delay: float = 0, **kwargs -) -> Mobject: + animation: MobjectAnimation[M], + cycle: bool = False, + delay: float = 0, +) -> M: """ Add an updater to the animation's mobject which applies the interpolation and update functions of the animation @@ -208,27 +148,29 @@ def construct(self): mobject = animation.mobject animation.suspend_mobject_updating = False animation.begin() - animation.total_time = -delay - def update(m: Mobject, dt: float): - if animation.total_time >= 0: + total_time = -delay + + def update(m: M, dt: float): + nonlocal total_time + if total_time >= 0: run_time = animation.get_run_time() - time_ratio = animation.total_time / run_time + time_ratio = total_time / run_time if cycle: alpha = time_ratio % 1 else: alpha = np.clip(time_ratio, 0, 1) if alpha >= 1: animation.finish() - m.remove_updater(update) + m.remove_updater(update) # type: ignore return animation.interpolate(alpha) animation.update_mobjects(dt) - animation.total_time += dt + total_time += dt - mobject.add_updater(update) + mobject.add_updater(update) # type: ignore return mobject -def cycle_animation(animation: Animation, **kwargs) -> Mobject: +def cycle_animation(animation: MobjectAnimation[M], **kwargs) -> M: return turn_animation_into_updater(animation, cycle=True, **kwargs) diff --git a/manim/animation/updaters/update.py b/manim/animation/updaters/update.py index ded160cff7..4393172d1c 100644 --- a/manim/animation/updaters/update.py +++ b/manim/animation/updaters/update.py @@ -11,7 +11,7 @@ from manim.animation.animation import Animation if typing.TYPE_CHECKING: - from manim.mobject.mobject import Mobject + from manim.mobject.opengl.opengl_mobject import OpenGLMobject class UpdateFromFunc(Animation): @@ -23,27 +23,29 @@ class UpdateFromFunc(Animation): def __init__( self, - mobject: Mobject, - update_function: typing.Callable[[Mobject], typing.Any], + mobject: OpenGLMobject, + update_function: typing.Callable[[OpenGLMobject], object], suspend_mobject_updating: bool = False, **kwargs, ) -> None: - self.update_function = update_function super().__init__( mobject, suspend_mobject_updating=suspend_mobject_updating, **kwargs ) + self.update_function = update_function - def interpolate_mobject(self, alpha: float) -> None: + def interpolate(self, alpha: float) -> None: self.update_function(self.mobject) class UpdateFromAlphaFunc(UpdateFromFunc): - def interpolate_mobject(self, alpha: float) -> None: + def interpolate(self, alpha: float) -> None: self.update_function(self.mobject, self.rate_func(alpha)) class MaintainPositionRelativeTo(Animation): - def __init__(self, mobject: Mobject, tracked_mobject: Mobject, **kwargs) -> None: + def __init__( + self, mobject: OpenGLMobject, tracked_mobject: OpenGLMobject, **kwargs + ) -> None: self.tracked_mobject = tracked_mobject self.diff = op.sub( mobject.get_center(), @@ -51,7 +53,7 @@ def __init__(self, mobject: Mobject, tracked_mobject: Mobject, **kwargs) -> None ) super().__init__(mobject, **kwargs) - def interpolate_mobject(self, alpha: float) -> None: + def interpolate(self, alpha: float) -> None: target = self.tracked_mobject.get_center() location = self.mobject.get_center() self.mobject.shift(target - location + self.diff) diff --git a/manim/camera/camera.py b/manim/camera/camera.py index af5899c5c5..1bfd3e1527 100644 --- a/manim/camera/camera.py +++ b/manim/camera/camera.py @@ -1,1367 +1,658 @@ -"""A camera converts the mobjects contained in a Scene into an array of pixels.""" +"""A camera that controls the FOV, orientation, and position of the scene.""" from __future__ import annotations -__all__ = ["Camera", "BackgroundColoredVMobjectDisplayer"] +import math +from typing import TYPE_CHECKING, Any, TypedDict -import copy -import itertools as it -import operator as op -import pathlib -from collections.abc import Iterable -from functools import reduce -from typing import Any, Callable - -import cairo import numpy as np -from PIL import Image -from scipy.spatial.distance import pdist - -from .. import config, logger -from ..constants import * -from ..mobject.mobject import Mobject -from ..mobject.types.image_mobject import AbstractImageMobject -from ..mobject.types.point_cloud_mobject import PMobject -from ..mobject.types.vectorized_mobject import VMobject -from ..utils.color import ManimColor, ParsableManimColor, color_to_int_rgba -from ..utils.family import extract_mobject_family_members -from ..utils.images import get_full_raster_image_path -from ..utils.iterables import list_difference_update -from ..utils.space_ops import angle_of_vector - -LINE_JOIN_MAP = { - LineJointType.AUTO: None, # TODO: this could be improved - LineJointType.ROUND: cairo.LineJoin.ROUND, - LineJointType.BEVEL: cairo.LineJoin.BEVEL, - LineJointType.MITER: cairo.LineJoin.MITER, -} - - -CAP_STYLE_MAP = { - CapStyleType.AUTO: None, # TODO: this could be improved - CapStyleType.ROUND: cairo.LineCap.ROUND, - CapStyleType.BUTT: cairo.LineCap.BUTT, - CapStyleType.SQUARE: cairo.LineCap.SQUARE, -} - - -class Camera: - """Base camera class. - - This is the object which takes care of what exactly is displayed - on screen at any given moment. - - Parameters - ---------- - background_image - The path to an image that should be the background image. - If not set, the background is filled with :attr:`self.background_color` - background - What :attr:`background` is set to. By default, ``None``. - pixel_height - The height of the scene in pixels. - pixel_width - The width of the scene in pixels. - kwargs - Additional arguments (``background_color``, ``background_opacity``) - to be set. - """ - - def __init__( - self, - background_image: str | None = None, - frame_center: np.ndarray = ORIGIN, - image_mode: str = "RGBA", - n_channels: int = 4, - pixel_array_dtype: str = "uint8", - cairo_line_width_multiple: float = 0.01, - use_z_index: bool = True, - background: np.ndarray | None = None, - pixel_height: int | None = None, - pixel_width: int | None = None, - frame_height: float | None = None, - frame_width: float | None = None, - frame_rate: float | None = None, - background_color: ParsableManimColor | None = None, - background_opacity: float | None = None, - **kwargs, - ): - self.background_image = background_image - self.frame_center = frame_center - self.image_mode = image_mode - self.n_channels = n_channels - self.pixel_array_dtype = pixel_array_dtype - self.cairo_line_width_multiple = cairo_line_width_multiple - self.use_z_index = use_z_index - self.background = background - - if pixel_height is None: - pixel_height = config["pixel_height"] - self.pixel_height = pixel_height - - if pixel_width is None: - pixel_width = config["pixel_width"] - self.pixel_width = pixel_width - - if frame_height is None: - frame_height = config["frame_height"] - self.frame_height = frame_height - - if frame_width is None: - frame_width = config["frame_width"] - self.frame_width = frame_width - - if frame_rate is None: - frame_rate = config["frame_rate"] - self.frame_rate = frame_rate - - if background_color is None: - self._background_color = ManimColor.parse(config["background_color"]) - else: - self._background_color = ManimColor.parse(background_color) - if background_opacity is None: - self._background_opacity = config["background_opacity"] - else: - self._background_opacity = background_opacity - - # This one is in the same boat as the above, but it doesn't have the - # same name as the corresponding key so it has to be handled on its own - self.max_allowable_norm = config["frame_width"] - - self.rgb_max_val = np.iinfo(self.pixel_array_dtype).max - self.pixel_array_to_cairo_context = {} - - # Contains the correct method to process a list of Mobjects of the - # corresponding class. If a Mobject is not an instance of a class in - # this dict (or an instance of a class that inherits from a class in - # this dict), then it cannot be rendered. - - self.init_background() - self.resize_frame_shape() - self.reset() - - def __deepcopy__(self, memo): - # This is to address a strange bug where deepcopying - # will result in a segfault, which is somehow related - # to the aggdraw library - self.canvas = None - return copy.copy(self) - - @property - def background_color(self): - return self._background_color - - @background_color.setter - def background_color(self, color): - self._background_color = color - self.init_background() - - @property - def background_opacity(self): - return self._background_opacity - - @background_opacity.setter - def background_opacity(self, alpha): - self._background_opacity = alpha - self.init_background() - - def type_or_raise(self, mobject: Mobject): - """Return the type of mobject, if it is a type that can be rendered. - - If `mobject` is an instance of a class that inherits from a class that - can be rendered, return the super class. For example, an instance of a - Square is also an instance of VMobject, and these can be rendered. - Therefore, `type_or_raise(Square())` returns True. - - Parameters - ---------- - mobject - The object to take the type of. - - Notes - ----- - For a list of classes that can currently be rendered, see :meth:`display_funcs`. - - Returns - ------- - Type[:class:`~.Mobject`] - The type of mobjects, if it can be rendered. - - Raises - ------ - :exc:`TypeError` - When mobject is not an instance of a class that can be rendered. - """ - self.display_funcs = { - VMobject: self.display_multiple_vectorized_mobjects, - PMobject: self.display_multiple_point_cloud_mobjects, - AbstractImageMobject: self.display_multiple_image_mobjects, - Mobject: lambda batch, pa: batch, # Do nothing - } - # We have to check each type in turn because we are dealing with - # super classes. For example, if square = Square(), then - # type(square) != VMobject, but isinstance(square, VMobject) == True. - for _type in self.display_funcs: - if isinstance(mobject, _type): - return _type - raise TypeError(f"Displaying an object of class {_type} is not supported") - - def reset_pixel_shape(self, new_height: float, new_width: float): - """This method resets the height and width - of a single pixel to the passed new_height and new_width. - - Parameters - ---------- - new_height - The new height of the entire scene in pixels - new_width - The new width of the entire scene in pixels - """ - self.pixel_width = new_width - self.pixel_height = new_height - self.init_background() - self.resize_frame_shape() - self.reset() - - def resize_frame_shape(self, fixed_dimension: int = 0): - """ - Changes frame_shape to match the aspect ratio - of the pixels, where fixed_dimension determines - whether frame_height or frame_width - remains fixed while the other changes accordingly. - - Parameters - ---------- - fixed_dimension - If 0, height is scaled with respect to width - else, width is scaled with respect to height. - """ - pixel_height = self.pixel_height - pixel_width = self.pixel_width - frame_height = self.frame_height - frame_width = self.frame_width - aspect_ratio = pixel_width / pixel_height - if fixed_dimension == 0: - frame_height = frame_width / aspect_ratio - else: - frame_width = aspect_ratio * frame_height - self.frame_height = frame_height - self.frame_width = frame_width - - def init_background(self): - """Initialize the background. - If self.background_image is the path of an image - the image is set as background; else, the default - background color fills the background. - """ - height = self.pixel_height - width = self.pixel_width - if self.background_image is not None: - path = get_full_raster_image_path(self.background_image) - image = Image.open(path).convert(self.image_mode) - # TODO, how to gracefully handle backgrounds - # with different sizes? - self.background = np.array(image)[:height, :width] - self.background = self.background.astype(self.pixel_array_dtype) - else: - background_rgba = color_to_int_rgba( - self.background_color, - self.background_opacity, - ) - self.background = np.zeros( - (height, width, self.n_channels), - dtype=self.pixel_array_dtype, - ) - self.background[:, :] = background_rgba - - def get_image(self, pixel_array: np.ndarray | list | tuple | None = None): - """Returns an image from the passed - pixel array, or from the current frame - if the passed pixel array is none. - - Parameters - ---------- - pixel_array - The pixel array from which to get an image, by default None +import numpy.typing as npt - Returns - ------- - PIL.Image - The PIL image of the array. - """ - if pixel_array is None: - pixel_array = self.pixel_array - return Image.fromarray(pixel_array, mode=self.image_mode) - - def convert_pixel_array( - self, pixel_array: np.ndarray | list | tuple, convert_from_floats: bool = False - ): - """Converts a pixel array from values that have floats in then - to proper RGB values. - - Parameters - ---------- - pixel_array - Pixel array to convert. - convert_from_floats - Whether or not to convert float values to ints, by default False +from manim._config import config, logger +from manim.constants import * +from manim.mobject.opengl.opengl_mobject import InvisibleMobject, OpenGLMobject +from manim.utils.paths import straight_path +from manim.utils.space_ops import rotation_matrix - Returns - ------- - np.array - The new, converted pixel array. - """ - retval = np.array(pixel_array) - if convert_from_floats: - retval = np.apply_along_axis( - lambda f: (f * self.rgb_max_val).astype(self.pixel_array_dtype), - 2, - retval, - ) - return retval +if TYPE_CHECKING: + from typing_extensions import Self - def set_pixel_array( - self, pixel_array: np.ndarray | list | tuple, convert_from_floats: bool = False - ): - """Sets the pixel array of the camera to the passed pixel array. + from manim.typing import ManimFloat, MatrixMN, Point3D, Vector3D - Parameters - ---------- - pixel_array - The pixel array to convert and then set as the camera's pixel array. - convert_from_floats - Whether or not to convert float values to proper RGB values, by default False - """ - converted_array = self.convert_pixel_array(pixel_array, convert_from_floats) - if not ( - hasattr(self, "pixel_array") - and self.pixel_array.shape == converted_array.shape - ): - self.pixel_array = converted_array - else: - # Set in place - self.pixel_array[:, :, :] = converted_array[:, :, :] - def set_background( - self, pixel_array: np.ndarray | list | tuple, convert_from_floats: bool = False - ): - """Sets the background to the passed pixel_array after converting - to valid RGB values. +class CameraOrientationConfig(TypedDict, total=False): + theta: float | None + phi: float | None + gamma: float | None + zoom: float | None + focal_distance: float | None + frame_center: OpenGLMobject | Sequence[float] | None - Parameters - ---------- - pixel_array - The pixel array to set the background to. - convert_from_floats - Whether or not to convert floats values to proper RGB valid ones, by default False - """ - self.background = self.convert_pixel_array(pixel_array, convert_from_floats) - # TODO, this should live in utils, not as a method of Camera - def make_background_from_func( - self, coords_to_colors_func: Callable[[np.ndarray], np.ndarray] +class Camera(OpenGLMobject, InvisibleMobject): + def __init__( + self, + frame_shape: tuple[float, float] = (config.frame_width, config.frame_height), + center_point: Point3D = ORIGIN, # TODO: use Point3DLike + focal_distance: float = 16.0, + **kwargs, ): - """ - Makes a pixel array for the background by using coords_to_colors_func to determine each pixel's color. Each input - pixel's color. Each input to coords_to_colors_func is an (x, y) pair in space (in ordinary space coordinates; not - pixel coordinates), and each output is expected to be an RGBA array of 4 floats. - - Parameters - ---------- - coords_to_colors_func - The function whose input is an (x,y) pair of coordinates and - whose return values must be the colors for that point - - Returns - ------- - np.array - The pixel array which can then be passed to set_background. - """ - logger.info("Starting set_background") - coords = self.get_coords_of_all_pixels() - new_background = np.apply_along_axis(coords_to_colors_func, 2, coords) - logger.info("Ending set_background") - - return self.convert_pixel_array(new_background, convert_from_floats=True) + self.initial_frame_shape = frame_shape + self.center_point = center_point + self.focal_distance = focal_distance + self.set_euler_angles(theta=-TAU / 4, phi=0.0, gamma=0.0) + self.ambient_rotation_updaters_dict: dict[Updater | None] = { + "theta": None, + "gamma": None, + "phi": None, + } + self.precession_updater: Updater | None = None + super().__init__(**kwargs) - def set_background_from_func( - self, coords_to_colors_func: Callable[[np.ndarray], np.ndarray] - ): - """ - Sets the background to a pixel array using coords_to_colors_func to determine each pixel's color. Each input - pixel's color. Each input to coords_to_colors_func is an (x, y) pair in space (in ordinary space coordinates; not - pixel coordinates), and each output is expected to be an RGBA array of 4 floats. + def init_points(self) -> None: + self.set_points([ORIGIN, LEFT, RIGHT, DOWN, UP]) + self.set_width(self.initial_frame_shape[0], stretch=True) + self.set_height(self.initial_frame_shape[1], stretch=True) + self.move_to(self.center_point) - Parameters - ---------- - coords_to_colors_func - The function whose input is an (x,y) pair of coordinates and - whose return values must be the colors for that point - """ - self.set_background(self.make_background_from_func(coords_to_colors_func)) + def interpolate( + self, + mobject1: OpenGLMobject, + mobject2: OpenGLMobject, + alpha: float, + path_func: PathFuncType = straight_path(), + ) -> Self: + """Interpolate the orientation of two cameras.""" + cam1: Camera = mobject1 + cam2: Camera = mobject2 + + orientation1 = cam1.get_orientation() + orientation2 = cam2.get_orientation() + new_orientation = { + key: path_func(orientation1[key], orientation2[key], alpha) + for key in orientation1 + } + return self.set_orientation(**new_orientation) + + def get_orientation(self) -> CameraOrientationConfig: + return { + "theta": self.get_theta(), + "phi": self.get_phi(), + "gamma": self.get_gamma(), + "zoom": self.get_zoom(), + "focal_distance": self.focal_distance, + "frame_center": self.get_center(), + } - def reset(self): - """Resets the camera's pixel array - to that of the background + def set_orientation( + self, + theta: float | None = None, + phi: float | None = None, + gamma: float | None = None, + zoom: float | None = None, + focal_distance: float | None = None, + frame_center: OpenGLMobject | Point3D | None = None, # TODO: use Point3DLike + ) -> Self: + """This method sets the orientation of the camera in the scene. + + Parameters + ---------- + theta + The azimuthal angle i.e the angle that spins the camera around the Z_AXIS. + phi + The polar angle i.e the angle between Z_AXIS and Camera through ORIGIN in radians. + gamma + The rotation of the camera about the vector from the ORIGIN to the Camera. + zoom + The zoom factor of the scene. + focal_distance + The focal_distance of the Camera. + frame_center + The new center of the camera frame in cartesian coordinates. Returns ------- - Camera - The camera object after setting the pixel array. - """ - self.set_pixel_array(self.background) + :class:`Camera` + The camera after applying all changes. + """ + self.set_euler_angles(theta=theta, phi=phi, gamma=gamma) + if focal_distance is not None: + self.focal_distance = focal_distance + if zoom is not None: + self.set_zoom(zoom) + if frame_center is not None: + self.move_to(frame_center) return self - def set_frame_to_background(self, background): - self.set_pixel_array(background) + def get_euler_angles(self) -> npt.NDArray[ManimFloat]: + return np.array([self._theta, self._phi, self._gamma]) - #### - - def get_mobjects_to_display( + def set_euler_angles( self, - mobjects: Iterable[Mobject], - include_submobjects: bool = True, - excluded_mobjects: list | None = None, - ): - """Used to get the list of mobjects to display - with the camera. + theta: float | None = None, + phi: float | None = None, + gamma: float | None = None, + ) -> Self: + if theta is not None: + self.set_theta(theta) + if phi is not None: + self.set_phi(phi) + if gamma is not None: + self.set_gamma(gamma) + return self - Parameters - ---------- - mobjects - The Mobjects - include_submobjects - Whether or not to include the submobjects of mobjects, by default True - excluded_mobjects - Any mobjects to exclude, by default None + def get_theta(self) -> float: + """Get the angle theta along which the camera is rotated about the Z + axis. Returns ------- - list - list of mobjects + float + The theta angle. """ - if include_submobjects: - mobjects = extract_mobject_family_members( - mobjects, - use_z_index=self.use_z_index, - only_those_with_points=True, - ) - if excluded_mobjects: - all_excluded = extract_mobject_family_members( - excluded_mobjects, - use_z_index=self.use_z_index, - ) - mobjects = list_difference_update(mobjects, all_excluded) - return list(mobjects) - - def is_in_frame(self, mobject: Mobject): - """Checks whether the passed mobject is in - frame or not. + return self._theta + + def set_theta(self, theta: float) -> Self: + """Set the angle theta by which the camera is rotated about the Z + axis. Parameters ---------- - mobject - The mobject for which the checking needs to be done. + theta + The new theta angle. Returns ------- - bool - True if in frame, False otherwise. - """ - fc = self.frame_center - fh = self.frame_height - fw = self.frame_width - return not reduce( - op.or_, + :class:`Camera` + The camera after setting its theta angle. + """ + self._theta = theta + self._rotation_matrix = None + # If we don't add TAU/4 (90°) to theta, the camera will be positioned + # over the negative Y axis instead of the positive X axis. + cos = np.cos(theta + TAU / 4) + sin = np.sin(theta + TAU / 4) + self._theta_z_matrix = np.array( [ - mobject.get_right()[0] < fc[0] - fw / 2, - mobject.get_bottom()[1] > fc[1] + fh / 2, - mobject.get_left()[0] > fc[0] + fw / 2, - mobject.get_top()[1] < fc[1] - fh / 2, - ], + [cos, -sin, 0], + [sin, cos, 0], + [0, 0, 1], + ] ) + return self - def capture_mobject(self, mobject: Mobject, **kwargs: Any): - """Capture mobjects by storing it in :attr:`pixel_array`. - - This is a single-mobject version of :meth:`capture_mobjects`. - - Parameters - ---------- - mobject - Mobject to capture. - - kwargs - Keyword arguments to be passed to :meth:`get_mobjects_to_display`. - - """ - return self.capture_mobjects([mobject], **kwargs) - - def capture_mobjects(self, mobjects: Iterable[Mobject], **kwargs): - """Capture mobjects by printing them on :attr:`pixel_array`. - - This is the essential function that converts the contents of a Scene - into an array, which is then converted to an image or video. - - Parameters - ---------- - mobjects - Mobjects to capture. - - kwargs - Keyword arguments to be passed to :meth:`get_mobjects_to_display`. - - Notes - ----- - For a list of classes that can currently be rendered, see :meth:`display_funcs`. - - """ - # The mobjects will be processed in batches (or runs) of mobjects of - # the same type. That is, if the list mobjects contains objects of - # types [VMobject, VMobject, VMobject, PMobject, PMobject, VMobject], - # then they will be captured in three batches: [VMobject, VMobject, - # VMobject], [PMobject, PMobject], and [VMobject]. This must be done - # without altering their order. it.groupby computes exactly this - # partition while at the same time preserving order. - mobjects = self.get_mobjects_to_display(mobjects, **kwargs) - for group_type, group in it.groupby(mobjects, self.type_or_raise): - self.display_funcs[group_type](list(group), self.pixel_array) - - # Methods associated with svg rendering - - # NOTE: None of the methods below have been mentioned outside of their definitions. Their DocStrings are not as - # detailed as possible. - - def get_cached_cairo_context(self, pixel_array: np.ndarray): - """Returns the cached cairo context of the passed - pixel array if it exists, and None if it doesn't. - - Parameters - ---------- - pixel_array - The pixel array to check. - - Returns - ------- - cairo.Context - The cached cairo context. - """ - return self.pixel_array_to_cairo_context.get(id(pixel_array), None) - - def cache_cairo_context(self, pixel_array: np.ndarray, ctx: cairo.Context): - """Caches the passed Pixel array into a Cairo Context - - Parameters - ---------- - pixel_array - The pixel array to cache - ctx - The context to cache it into. - """ - self.pixel_array_to_cairo_context[id(pixel_array)] = ctx - - def get_cairo_context(self, pixel_array: np.ndarray): - """Returns the cairo context for a pixel array after - caching it to self.pixel_array_to_cairo_context - If that array has already been cached, it returns the - cached version instead. + def increment_theta(self, dtheta: float) -> Self: + """Incremeet the angle theta by which the camera is rotated about the Z + axis, by a given ``dtheta``. Parameters ---------- - pixel_array - The Pixel array to get the cairo context of. + dtheta + The increment in the angle theta. Returns ------- - cairo.Context - The cairo context of the pixel array. - """ - cached_ctx = self.get_cached_cairo_context(pixel_array) - if cached_ctx: - return cached_ctx - pw = self.pixel_width - ph = self.pixel_height - fw = self.frame_width - fh = self.frame_height - fc = self.frame_center - surface = cairo.ImageSurface.create_for_data( - pixel_array, - cairo.FORMAT_ARGB32, - pw, - ph, - ) - ctx = cairo.Context(surface) - ctx.scale(pw, ph) - ctx.set_matrix( - cairo.Matrix( - (pw / fw), - 0, - 0, - -(ph / fh), - (pw / 2) - fc[0] * (pw / fw), - (ph / 2) + fc[1] * (ph / fh), - ), - ) - self.cache_cairo_context(pixel_array, ctx) - return ctx - - def display_multiple_vectorized_mobjects( - self, vmobjects: list, pixel_array: np.ndarray - ): - """Displays multiple VMobjects in the pixel_array - - Parameters - ---------- - vmobjects - list of VMobjects to display - pixel_array - The pixel array - """ - if len(vmobjects) == 0: - return - batch_image_pairs = it.groupby(vmobjects, lambda vm: vm.get_background_image()) - for image, batch in batch_image_pairs: - if image: - self.display_multiple_background_colored_vmobjects(batch, pixel_array) - else: - self.display_multiple_non_background_colored_vmobjects( - batch, - pixel_array, - ) - - def display_multiple_non_background_colored_vmobjects( - self, vmobjects: list, pixel_array: np.ndarray - ): - """Displays multiple VMobjects in the cairo context, as long as they don't have - background colors. - - Parameters - ---------- - vmobjects - list of the VMobjects - pixel_array - The Pixel array to add the VMobjects to. + :class:`Camera` + The camera after incrementing its theta angle. """ - ctx = self.get_cairo_context(pixel_array) - for vmobject in vmobjects: - self.display_vectorized(vmobject, ctx) + return self.set_theta(self._theta + dtheta) - def display_vectorized(self, vmobject: VMobject, ctx: cairo.Context): - """Displays a VMobject in the cairo context - - Parameters - ---------- - vmobject - The Vectorized Mobject to display - ctx - The cairo context to use. + def get_phi(self) -> float: + """Get the angle phi between the camera and the Z axis. Returns ------- - Camera - The camera object + float + The phi angle. """ - self.set_cairo_context_path(ctx, vmobject) - self.apply_stroke(ctx, vmobject, background=True) - self.apply_fill(ctx, vmobject) - self.apply_stroke(ctx, vmobject) - return self + return self._phi - def set_cairo_context_path(self, ctx: cairo.Context, vmobject: VMobject): - """Sets a path for the cairo context with the vmobject passed + def set_phi(self, phi: float) -> Self: + """Set the angle phi between the camera and the Z axis. Parameters ---------- - ctx - The cairo context - vmobject - The VMobject + phi + The new phi angle. Returns ------- - Camera - Camera object after setting cairo_context_path - """ - points = self.transform_points_pre_display(vmobject, vmobject.points) - # TODO, shouldn't this be handled in transform_points_pre_display? - # points = points - self.get_frame_center() - if len(points) == 0: - return - - ctx.new_path() - subpaths = vmobject.gen_subpaths_from_points_2d(points) - for subpath in subpaths: - quads = vmobject.gen_cubic_bezier_tuples_from_points(subpath) - ctx.new_sub_path() - start = subpath[0] - ctx.move_to(*start[:2]) - for _p0, p1, p2, p3 in quads: - ctx.curve_to(*p1[:2], *p2[:2], *p3[:2]) - if vmobject.consider_points_equals_2d(subpath[0], subpath[-1]): - ctx.close_path() + :class:`Camera` + The camera after setting its phi angle. + """ + self._phi = phi + self._rotation_matrix = None + cos = np.cos(phi) + sin = np.sin(phi) + self._phi_x_matrix = np.array( + [ + [1, 0, 0], + [0, cos, -sin], + [0, sin, cos], + ] + ) return self - def set_cairo_context_color( - self, ctx: cairo.Context, rgbas: np.ndarray, vmobject: VMobject - ): - """Sets the color of the cairo context + def increment_phi(self, dphi: float) -> Self: + """Increment the angle phi between the camera and the Z axis by a given + ``dphi``. Parameters ---------- - ctx - The cairo context - rgbas - The RGBA array with which to color the context. - vmobject - The VMobject with which to set the color. + dphi + The increment in the angle phi. Returns ------- - Camera - The camera object + :class:`Camera` + The camera after incrementing its phi angle. """ - if len(rgbas) == 1: - # Use reversed rgb because cairo surface is - # encodes it in reverse order - ctx.set_source_rgba(*rgbas[0][2::-1], rgbas[0][3]) - else: - points = vmobject.get_gradient_start_and_end_points() - points = self.transform_points_pre_display(vmobject, points) - pat = cairo.LinearGradient(*it.chain(*(point[:2] for point in points))) - step = 1.0 / (len(rgbas) - 1) - offsets = np.arange(0, 1 + step, step) - for rgba, offset in zip(rgbas, offsets): - pat.add_color_stop_rgba(offset, *rgba[2::-1], rgba[3]) - ctx.set_source(pat) - return self - - def apply_fill(self, ctx: cairo.Context, vmobject: VMobject): - """Fills the cairo context + return self.set_phi(self._phi + dgamma) - Parameters - ---------- - ctx - The cairo context - vmobject - The VMobject + def get_gamma(self) -> float: + """Get the angle gamma by which the camera is rotated while standing on + its current position. Returns ------- - Camera - The camera object. + float + The gamma angle. """ - self.set_cairo_context_color(ctx, self.get_fill_rgbas(vmobject), vmobject) - ctx.fill_preserve() - return self + return self._gamma - def apply_stroke( - self, ctx: cairo.Context, vmobject: VMobject, background: bool = False - ): - """Applies a stroke to the VMobject in the cairo context. + def set_gamma(self, gamma: float) -> Self: + """Set the angle gamma by which the camera is rotated while standing on + its current position. Parameters ---------- - ctx - The cairo context - vmobject - The VMobject - background - Whether or not to consider the background when applying this - stroke width, by default False + gamma + The new gamma angle. Returns ------- - Camera - The camera object with the stroke applied. - """ - width = vmobject.get_stroke_width(background) - if width == 0: - return self - self.set_cairo_context_color( - ctx, - self.get_stroke_rgbas(vmobject, background=background), - vmobject, - ) - ctx.set_line_width( - width - * self.cairo_line_width_multiple - * (self.frame_width / self.frame_width), - # This ensures lines have constant width as you zoom in on them. + :class:`Camera` + The camera after setting its gamma angle. + """ + self._gamma = gamma + self._rotation_matrix = None + cos = np.cos(gamma) + sin = np.sin(gamma) + self._gamma_z_matrix = np.array( + [ + [cos, -sin, 0], + [sin, cos, 0], + [0, 0, 1], + ] ) - if vmobject.joint_type != LineJointType.AUTO: - ctx.set_line_join(LINE_JOIN_MAP[vmobject.joint_type]) - if vmobject.cap_style != CapStyleType.AUTO: - ctx.set_line_cap(CAP_STYLE_MAP[vmobject.cap_style]) - ctx.stroke_preserve() return self - def get_stroke_rgbas(self, vmobject: VMobject, background: bool = False): - """Gets the RGBA array for the stroke of the passed - VMobject. + def increment_gamma(self, dgamma: float) -> Self: + """Increment the angle gamma by which the camera is rotated while + standing on its current position, by an angle ``dgamma``. Parameters ---------- - vmobject - The VMobject - background - Whether or not to consider the background when getting the stroke - RGBAs, by default False + dgamma + The increment in the angle gamma. Returns ------- - np.ndarray - The RGBA array of the stroke. + :class:`Camera` + The camera after incrementing its gamma angle. """ - return vmobject.get_stroke_rgbas(background) + return self.set_gamma(self._gamma + dgamma) - def get_fill_rgbas(self, vmobject: VMobject): - """Returns the RGBA array of the fill of the passed VMobject + def get_rotation_matrix(self) -> MatrixMN: + r"""Get the current rotation matrix. - Parameters - ---------- - vmobject - The VMobject + In order to get the current rotation using the Euler angles: - Returns - ------- - np.array - The RGBA Array of the fill of the VMobject - """ - return vmobject.get_fill_rgbas() + 1. Rotate :math:`\gamma` along the Z axis (XY plane). + 2. Rotate :math:`\varphi` along the X axis (YZ plane). + 3. Rotate :math:`\theta` along the Z axis again (XY plane). - def get_background_colored_vmobject_displayer(self): - """Returns the background_colored_vmobject_displayer - if it exists or makes one and returns it if not. + See :meth:`Camera.rotate()` for more information. Returns ------- - BackGroundColoredVMobjectDisplayer - Object that displays VMobjects that have the same color - as the background. + MatrixMN + The current 3x3 rotation matrix. """ - # Quite wordy to type out a bunch - bcvd = "background_colored_vmobject_displayer" - if not hasattr(self, bcvd): - setattr(self, bcvd, BackgroundColoredVMobjectDisplayer(self)) - return getattr(self, bcvd) - - def display_multiple_background_colored_vmobjects( - self, cvmobjects: list, pixel_array: np.ndarray - ): - """Displays multiple vmobjects that have the same color as the background. + if self._rotation_matrix is None: + self._rotation_matrix = ( + self._theta_z_matrix @ self._phi_x_matrix @ self._gamma_z_matrix + ) + return self._rotation_matrix - Parameters - ---------- - cvmobjects - List of Colored VMobjects - pixel_array - The pixel array. + def get_inverse_rotation_matrix(self) -> MatrixMN: + return self.get_rotation_matrix().T - Returns - ------- - Camera - The camera object. - """ - displayer = self.get_background_colored_vmobject_displayer() - cvmobject_pixel_array = displayer.display(*cvmobjects) - self.overlay_rgba_array(pixel_array, cvmobject_pixel_array) + def set_focal_distance(self, focal_distance: float) -> Self: + self.focal_distance = focal_distance return self - # Methods for other rendering + # TODO: rotate is still unreliable. The Euler angles are automatically + # standardized to (-TAU/2, TAU/2), leading to potentially unwanted behavior + # when animating. Plus, if the camera is on the Z axis, which occurs when + # phi is a multiple of TAU/2, the current implementation can only determine + # theta + gamma, but not exactly theta or gamma yet. + def rotate(self, angle: float, axis: Vector3D = OUT, **kwargs: Any) -> Self: + r"""Rotate the camera in a given ``angle`` along the given ``axis``. - # NOTE: Out of the following methods, only `transform_points_pre_display` and `points_to_pixel_coords` have been mentioned outside of their definitions. - # As a result, the other methods do not have as detailed docstrings as would be preferred. + After rotating the camera, the Euler angles must be recalculated, Given + the Euler angles :math:`\theta`, :math:`\varphi` and :math:`\gamma`, + the current rotation matrix is obtained in this way: - def display_multiple_point_cloud_mobjects( - self, pmobjects: list, pixel_array: np.ndarray - ): - """Displays multiple PMobjects by modifying the passed pixel array. + 1. Rotate :math:`\gamma` along the Z axis (XY plane). This + corresponds to multiplying by the following matrix: - Parameters - ---------- - pmobjects - List of PMobjects - pixel_array - The pixel array to modify. - """ - for pmobject in pmobjects: - self.display_point_cloud( - pmobject, - pmobject.points, - pmobject.rgbas, - self.adjusted_thickness(pmobject.stroke_width), - pixel_array, - ) + .. math:: - def display_point_cloud( - self, - pmobject: PMobject, - points: list, - rgbas: np.ndarray, - thickness: float, - pixel_array: np.ndarray, - ): - """Displays a PMobject by modifying the pixel array suitably. + R_z(\gamma) = \begin{pmatrix} + \cos(\gamma) & -\sin(\gamma) & 0 \\ + \sin(\gamma) & \cos(\gamma) & 0 \\ + 0 & 0 & 1 + \end{pmatrix} - TODO: Write a description for the rgbas argument. + 2. Rotate :math:`\varphi` along the X axis (YZ plane). This + corresponds to multiplying by the following matrix: - Parameters - ---------- - pmobject - Point Cloud Mobject - points - The points to display in the point cloud mobject - rgbas + .. math:: - thickness - The thickness of each point of the PMobject - pixel_array - The pixel array to modify. + R_x(\varphi) = \begin{pmatrix} + 1 & 0 & 0 \\ + 0 & \cos(\varphi) & -\sin(\gamma) \\ + 0 & \sin(\varphi) & \cos(\varphi) + \end{pmatrix} - """ - if len(points) == 0: - return - pixel_coords = self.points_to_pixel_coords(pmobject, points) - pixel_coords = self.thickened_coordinates(pixel_coords, thickness) - rgba_len = pixel_array.shape[2] - - rgbas = (self.rgb_max_val * rgbas).astype(self.pixel_array_dtype) - target_len = len(pixel_coords) - factor = target_len // len(rgbas) - rgbas = np.array([rgbas] * factor).reshape((target_len, rgba_len)) - - on_screen_indices = self.on_screen_pixels(pixel_coords) - pixel_coords = pixel_coords[on_screen_indices] - rgbas = rgbas[on_screen_indices] - - ph = self.pixel_height - pw = self.pixel_width - - flattener = np.array([1, pw], dtype="int") - flattener = flattener.reshape((2, 1)) - indices = np.dot(pixel_coords, flattener)[:, 0] - indices = indices.astype("int") - - new_pa = pixel_array.reshape((ph * pw, rgba_len)) - new_pa[indices] = rgbas - pixel_array[:, :] = new_pa.reshape((ph, pw, rgba_len)) - - def display_multiple_image_mobjects( - self, image_mobjects: list, pixel_array: np.ndarray - ): - """Displays multiple image mobjects by modifying the passed pixel_array. + 3. Rotate :math:`\theta` along the Z axis again (XY plane). This + corresponds to multiplying by the following matrix: - Parameters - ---------- - image_mobjects - list of ImageMobjects - pixel_array - The pixel array to modify. - """ - for image_mobject in image_mobjects: - self.display_image_mobject(image_mobject, pixel_array) + .. math:: - def display_image_mobject( - self, image_mobject: AbstractImageMobject, pixel_array: np.ndarray - ): - """Displays an ImageMobject by changing the pixel_array suitably. + R_z(\theta) = \begin{pmatrix} + \cos(\theta) & -\sin(\theta) & 0 \\ + \sin(\theta) & \cos(\theta) & 0 \\ + 0 & 0 & 1 + \end{pmatrix} - Parameters - ---------- - image_mobject - The imageMobject to display - pixel_array - The Pixel array to put the imagemobject in. - """ - corner_coords = self.points_to_pixel_coords(image_mobject, image_mobject.points) - ul_coords, ur_coords, dl_coords, _ = corner_coords - right_vect = ur_coords - ul_coords - down_vect = dl_coords - ul_coords - center_coords = ul_coords + (right_vect + down_vect) / 2 - - sub_image = Image.fromarray(image_mobject.get_pixel_array(), mode="RGBA") - - # Reshape - pixel_width = max(int(pdist([ul_coords, ur_coords]).item()), 1) - pixel_height = max(int(pdist([ul_coords, dl_coords]).item()), 1) - sub_image = sub_image.resize( - (pixel_width, pixel_height), - resample=image_mobject.resampling_algorithm, - ) + Applying these matrices in order, the final rotation matrix is: - # Rotate - angle = angle_of_vector(right_vect) - adjusted_angle = -int(360 * angle / TAU) - if adjusted_angle != 0: - sub_image = sub_image.rotate( - adjusted_angle, - resample=image_mobject.resampling_algorithm, - expand=1, - ) + .. math:: - # TODO, there is no accounting for a shear... + R = R_z(\theta) R_x(\varphi) R_z(\gamma) = \begin{pmatrix} + \cos(\theta)\cos(\gamma) - \sin(\theta)\cos(\varphi)\cos(\gamma) & -\sin(\theta)\cos(varphi)\cos(\gamma) - \cos(\theta)\sin(\gamma) & \sin(\theta)\sin(\varphi) \\ + \cos(\theta)\cos(\varphi)\sin(\gamma) + \sin(\theta)\cos(\gamma) & \cos(\theta)\cos(varphi)\cos(\gamma) - \sin(\theta)\sin(\gamma) & -\cos(\theta)\sin(\varphi) \\ + \sin(\varphi)\sin(\gamma) & \sin(\varphi)\cos(\gamma) & \cos(\varphi) + \end{pmatrix} - # Paste into an image as large as the camera's pixel array - full_image = Image.fromarray( - np.zeros((self.pixel_height, self.pixel_width)), - mode="RGBA", - ) - new_ul_coords = center_coords - np.array(sub_image.size) / 2 - new_ul_coords = new_ul_coords.astype(int) - full_image.paste( - sub_image, - box=( - new_ul_coords[0], - new_ul_coords[1], - new_ul_coords[0] + sub_image.size[0], - new_ul_coords[1] + sub_image.size[1], - ), - ) - # Paint on top of existing pixel array - self.overlay_PIL_image(pixel_array, full_image) + From this matrix, if :math:`\sin(\varphi) \neq 0`, then it is possible + to retrieve the Euler angles in the following way: - def overlay_rgba_array(self, pixel_array: np.ndarray, new_array: np.ndarray): - """Overlays an RGBA array on top of the given Pixel array. + .. math:: - Parameters - ---------- - pixel_array - The original pixel array to modify. - new_array - The new pixel array to overlay. - """ - self.overlay_PIL_image(pixel_array, self.get_image(new_array)) + \frac{R_{1,3}}{-R_{2,3}} = \frac{\sin(\theta)\sin(\varphi)}{\cos(\theta)\sin(\varphi)} = \tan(\theta) \quad &\Longrightarrow \quad \theta = \text{atan2}(-R_{2,3}, R_{1,3}) \\ + \frac{\sqrt{R_{1,3}^2 + R_{2,3}^2}}{R_{3,3}} = \frac{\sqrt{\sin^2(\theta)\sin^2(\varphi) + \cos^2(\theta)\sin^2(\varphi)}}{\cos(\varphi)} = \tan(\varphi) \quad &\Longrightarrow \quad \theta = \text{atan2}(\sqrt{R_{1,3}^2 + R_{2,3}^2}, R_{3,3}) \\ + \frac{R_{3,1}}{R_{3,2}} = \frac{\sin(\varphi)\sin(\gamma)}{\sin(\varphi)\cos(\gamma)} = \tan(\gamma) \quad &\Longrightarrow \quad \theta = \text{atan2}(R_{3,2}, R_{3,1}) \\ - def overlay_PIL_image(self, pixel_array: np.ndarray, image: Image): - """Overlays a PIL image on the passed pixel array. + However, if :math:`\sin(\varphi) = 0`, then: - Parameters - ---------- - pixel_array - The Pixel array - image - The Image to overlay. - """ - pixel_array[:, :] = np.array( - Image.alpha_composite(self.get_image(pixel_array), image), - dtype="uint8", - ) + .. math:: + \frac{R_{2,2}}{R_{1,1}} = \frac{\sin(\theta \pm \gamma)}{\cos(\theta \pm \gamma)} = \tan(\theta \pm \gamma) \quad \Longrightarrow \quad \theta \pm \gamma = \text{atan2}(R_{1,1}, R_{2,2}) + + and currently there's no implemented way to exactly find :math:`\theta` + and :math:`\gamma`, so this function sets :math:`\gamma = 0`. - def adjust_out_of_range_points(self, points: np.ndarray): - """If any of the points in the passed array are out of - the viable range, they are adjusted suitably. + .. warning:: + + This method is still unreliable. The Euler angles are automatically + standardized to (-TAU/2, TAU/2), leading to potentially unwanted behavior + when using :attr:`OpenGLMobject.animate`. Plus, if the camera is on + the Z axis, which occurs when phi is a multiple of TAU/2, the current + implementation can only determine theta +- gamma, but not exactly + theta or gamma yet. Parameters ---------- - points - The points to adjust + angle + Angle of rotation. + axis + Axis of rotation. + **kwargs + Additional parameters which are required by + :meth:`OpenGLMobject.rotate`. Returns ------- - np.array - The adjusted points. - """ - if not np.any(points > self.max_allowable_norm): - return points - norms = np.apply_along_axis(np.linalg.norm, 1, points) - violator_indices = norms > self.max_allowable_norm - violators = points[violator_indices, :] - violator_norms = norms[violator_indices] - reshaped_norms = np.repeat( - violator_norms.reshape((len(violator_norms), 1)), - points.shape[1], - 1, + :class:`Camera` + The camera after the rotation. + """ + logger.warning( + "Using this method automatically standardizes the Euler angles " + "theta, phi and gamma, which might result in unexpected behavior " + "when animating the camera. If phi is 0° or 180°, this method " + "is not able to determine exactly theta and gamma, because their " + "axes are aligned. Therefore, in that case, gamma will be set to " + "0°." ) - rescaled = self.max_allowable_norm * violators / reshaped_norms - points[violator_indices] = rescaled - return points - def transform_points_pre_display( - self, - mobject, - points, - ): # TODO: Write more detailed docstrings for this method. - # NOTE: There seems to be an unused argument `mobject`. - - # Subclasses (like ThreeDCamera) may want to - # adjust points further before they're shown - if not np.all(np.isfinite(points)): - # TODO, print some kind of warning about - # mobject having invalid points? - points = np.zeros((1, 3)) - return points - - def points_to_pixel_coords( - self, - mobject, - points, - ): # TODO: Write more detailed docstrings for this method. - points = self.transform_points_pre_display(mobject, points) - shifted_points = points - self.frame_center - - result = np.zeros((len(points), 2)) - pixel_height = self.pixel_height - pixel_width = self.pixel_width - frame_height = self.frame_height - frame_width = self.frame_width - width_mult = pixel_width / frame_width - width_add = pixel_width / 2 - height_mult = pixel_height / frame_height - height_add = pixel_height / 2 - # Flip on y-axis as you go - height_mult *= -1 - - result[:, 0] = shifted_points[:, 0] * width_mult + width_add - result[:, 1] = shifted_points[:, 1] * height_mult + height_add - return result.astype("int") - - def on_screen_pixels(self, pixel_coords: np.ndarray): - """Returns array of pixels that are on the screen from a given - array of pixel_coordinates + new_rot = rotation_matrix(angle, axis) @ self.get_rotation_matrix() + + # Recalculate theta, phi and gamma. + cos_phi = new_rot[2, 2] + # If phi is 0 or TAU/2, there's a gimbal lock and it's not trivial to + # determine theta and gamma, only theta +- gamma. + if cos_phi in [-1, 1]: + cos_theta_pm_gamma = new_rot[0, 0] + sin_theta_pm_gamma = new_rot[1, 0] + theta_pm_gamma = np.arctan2(cos_theta_pm_gamma, sin_theta_pm_gamma) + + # TODO: based on the axis, maybe there is a way to recover theta and gamma. + theta = theta_pm_gamma + phi = 0.0 if cos_phi == 1 else TAU / 2 + gamma = 0.0 + else: + sin_theta_sin_phi = new_rot[0, 2] + cos_theta_sin_phi = -new_rot[1, 2] + theta = np.arctan2(sin_theta_sin_phi, cos_theta_sin_phi) - Parameters - ---------- - pixel_coords - The pixel coords to check. + sin_phi = np.sqrt(sin_theta_sin_phi**2 + cos_theta_sin_phi**2) + phi = np.arctan2(cos_phi, sin_phi) - Returns - ------- - np.array - The pixel coords on screen. - """ - return reduce( - op.and_, - [ - pixel_coords[:, 0] >= 0, - pixel_coords[:, 0] < self.pixel_width, - pixel_coords[:, 1] >= 0, - pixel_coords[:, 1] < self.pixel_height, - ], - ) + sin_phi_sin_gamma = new_rot[2, 0] + sin_phi_cos_gamma = new_rot[2, 1] + gamma = np.arctan2(sin_phi_cos_gamma, sin_phi_sin_gamma) - def adjusted_thickness(self, thickness: float) -> float: - """Computes the adjusted stroke width for a zoomed camera. + self.set_euler_angles(theta=theta, phi=phi, gamma=gamma) + self._rotation_matrix = new_rot + return self - Parameters - ---------- - thickness - The stroke width of a mobject. + def get_zoom(self) -> float: + return config.frame_height / self.height - Returns - ------- - float - The adjusted stroke width that reflects zooming in with - the camera. - """ - # TODO: This seems...unsystematic - big_sum = op.add(config["pixel_height"], config["pixel_width"]) - this_sum = op.add(self.pixel_height, self.pixel_width) - factor = big_sum / this_sum - return 1 + (thickness - 1) * factor + def set_zoom(self, zoom: float) -> Self: + scale_factor = config.frame_height / (zoom * self.height) + return self.scale(scale_factor) - def get_thickening_nudges(self, thickness: float): - """Determine a list of vectors used to nudge - two-dimensional pixel coordinates. + def get_field_of_view(self) -> float: + return 2 * math.atan(self.focal_distance / (2 * self.height)) - Parameters - ---------- - thickness + def set_field_of_view(self, field_of_view: float) -> Self: + self.focal_distance = 2 * math.tan(field_of_view / 2) * self.height + return self - Returns - ------- - np.array + def get_frame_shape(self) -> tuple[float, float]: + return (self.get_width(), self.get_height()) - """ - thickness = int(thickness) - _range = list(range(-thickness // 2 + 1, thickness // 2 + 1)) - return np.array(list(it.product(_range, _range))) + def get_center(self) -> Point3D: + # Assumes first point is at the center + return self.points[0] - def thickened_coordinates(self, pixel_coords: np.ndarray, thickness: float): - """Returns thickened coordinates for a passed array of pixel coords and - a thickness to thicken by. + def get_width(self) -> float: + points = self.points + return points[2, 0] - points[1, 0] - Parameters - ---------- - pixel_coords - Pixel coordinates - thickness - Thickness + def get_height(self) -> float: + points = self.points + return points[4, 1] - points[3, 1] + + def get_implied_camera_direction(self) -> Vector3D: + """Use the rotation matrix given by the Euler angles theta, phi and + gamma to calculate the direction along which the camera would be + positioned if it had a physical position. Returns ------- - np.array - Array of thickened pixel coords. + :class:`Vector3D` + The direction along which the camera would be positioned if it had + a physical position. """ - nudges = self.get_thickening_nudges(thickness) - pixel_coords = np.array([pixel_coords + nudge for nudge in nudges]) - size = pixel_coords.size - return pixel_coords.reshape((size // 2, 2)) + return self.get_rotation_matrix()[:, 2] - # TODO, reimplement using cairo matrix - def get_coords_of_all_pixels(self): - """Returns the cartesian coordinates of each pixel. + def get_implied_camera_location(self) -> Point3D: + """Use the Euler angles theta, phi and gamma, as well as the frame + center and the focal distance, to calculate the point in which the + camera would be positioned if it had a physical position. Returns ------- - np.ndarray - The array of cartesian coordinates. + :class:`Point3D` + The point in which the camera would be positioned if it had a + physical position. """ - # These are in x, y order, to help me keep things straight - full_space_dims = np.array([self.frame_width, self.frame_height]) - full_pixel_dims = np.array([self.pixel_width, self.pixel_height]) - - # These are addressed in the same y, x order as in pixel_array, but the values in them - # are listed in x, y order - uncentered_pixel_coords = np.indices([self.pixel_height, self.pixel_width])[ - ::-1 - ].transpose(1, 2, 0) - uncentered_space_coords = ( - uncentered_pixel_coords * full_space_dims - ) / full_pixel_dims - # Could structure above line's computation slightly differently, but figured (without much - # thought) multiplying by frame_shape first, THEN dividing by pixel_shape, is probably - # better than the other order, for avoiding underflow quantization in the division (whereas - # overflow is unlikely to be a problem) - - centered_space_coords = uncentered_space_coords - (full_space_dims / 2) - - # Have to also flip the y coordinates to account for pixel array being listed in - # top-to-bottom order, opposite of screen coordinate convention - centered_space_coords = centered_space_coords * (1, -1) - - return centered_space_coords - - -# NOTE: The methods of the following class have not been mentioned outside of their definitions. -# Their DocStrings are not as detailed as preferred. -class BackgroundColoredVMobjectDisplayer: - """Auxiliary class that handles displaying vectorized mobjects with - a set background image. - - Parameters - ---------- - camera - Camera object to use. - """ - - def __init__(self, camera: Camera): - self.camera = camera - self.file_name_to_pixel_array_map = {} - self.pixel_array = np.array(camera.pixel_array) - self.reset_pixel_array() - - def reset_pixel_array(self): - self.pixel_array[:, :] = 0 - - def resize_background_array( - self, - background_array: np.ndarray, - new_width: float, - new_height: float, - mode: str = "RGBA", - ): - """Resizes the pixel array representing the background. + to_camera = self.get_implied_camera_direction() + return self.get_center() + self.focal_distance * to_camera + + # Movement methods + + def begin_ambient_rotation(self, rate: float = 0.02, about: str = "theta") -> Self: + """Apply an updater to rotate the camera on every frame by modifying + one of three Euler angles: "theta" (rotate about the Z axis), "phi" + (modify the angle between the camera and the Z axis) or "gamma" (rotate + the camera in its position while it's looking at the same point). Parameters ---------- - background_array - The pixel - new_width - The new width of the background - new_height - The new height of the background - mode - The PIL image mode, by default "RGBA" + rate + The rate at which the camera should rotate about the specified + angle. A positive rate means counterclockwise rotation, and a + negative rate means clockwise rotation. + about + One of 3 options: ["theta", "phi", "gamma"]. Defaults to "theta". Returns ------- - np.array - The numpy pixel array of the resized background. - """ - image = Image.fromarray(background_array) - image = image.convert(mode) - resized_image = image.resize((new_width, new_height)) - return np.array(resized_image) + :class:`Camera` + The camera after applying the rotation updater. + """ + # TODO, use a ValueTracker for rate, so that it + # can begin and end smoothly + about: str = about.lower() + self.stop_ambient_rotation(about=about) + + methods = { + "theta": self.increment_theta, + "phi": self.increment_phi, + "gamma": self.increment_gamma, + } + if about not in methods: + raise ValueError(f"Invalid ambient rotation angle '{about}'.") - def resize_background_array_to_match( - self, background_array: np.ndarray, pixel_array: np.ndarray - ): - """Resizes the background array to match the passed pixel array. + def ambient_rotation(mob: Camera, dt: float) -> Camera: + methods[about](rate * dt) + return mob + + self.add_updater(ambient_rotation) + self.ambient_rotation_updaters_dict[about] = ambient_rotation + return self + + def stop_ambient_rotation(self, about: str = "theta") -> Self: + """Stop ambient camera rotation on the specified angle. If there's a + corresponding ambient rotation updater applied on the camera, remove + it. Parameters ---------- - background_array - The prospective pixel array. - pixel_array - The pixel array whose width and height should be matched. + about + The Euler angle for which the rotation should stop. This angle can + be "theta", "phi" or "gamma". Defaults to "theta". Returns ------- - np.array - The resized background array. + :class:`Camera` + The camera after applying the rotation updater. """ - height, width = pixel_array.shape[:2] - mode = "RGBA" if pixel_array.shape[2] == 4 else "RGB" - return self.resize_background_array(background_array, width, height, mode) + about: str = about.lower() + if about not in self.ambient_rotation_updaters_dict: + raise ValueError(f"Invalid ambient rotation angle '{about}'.") - def get_background_array(self, image: Image.Image | pathlib.Path | str): - """Gets the background array that has the passed file_name. + updater = self.ambient_rotation_updaters_dict[about] + if updater is not None: + self.remove_updater(updater) + self.ambient_rotation_updaters_dict[about] = None - Parameters - ---------- - image - The background image or its file name. + return self + + def begin_precession( + self, + rate: float = 1.0, + radius: float = 0.2, + origin_theta: float | None = None, + origin_phi: float | None = None, + ) -> Self: + """Begin a camera precession by adding an updater. This precession + consists of moving around the point given by ``origin_phi`` and + ``origin_theta``, keeping the ``gamma`` Euler angle constant. + + Parameters + ---------- + rate + The rate at which the camera precession should operate. + radius + The precession radius. + origin_phi + The polar angle the camera should move around. If ``None``, + defaults to the current ``phi`` angle. + origin_theta + The azimutal angle the camera should move around. If ``None``, + defaults to the current ``theta`` angle. Returns ------- - np.ndarray - The pixel array of the image. + :class:`Camera` + The camera after applying the precession updater. """ - image_key = str(image) + self.stop_precession() - if image_key in self.file_name_to_pixel_array_map: - return self.file_name_to_pixel_array_map[image_key] - if isinstance(image, str): - full_path = get_full_raster_image_path(image) - image = Image.open(full_path) - back_array = np.array(image) + if origin_theta is None: + origin_theta = self.get_theta() + if origin_phi is None: + origin_phi = self.get_phi() - pixel_array = self.pixel_array - if not np.all(pixel_array.shape == back_array.shape): - back_array = self.resize_background_array_to_match(back_array, pixel_array) + precession_angle = 0.0 - self.file_name_to_pixel_array_map[image_key] = back_array - return back_array + def precession(mob: Camera, dt: float) -> Camera: + nonlocal precession_angle + precession_angle += rate * dt + dtheta = radius * np.sin(precession_angle) + dphi = radius * np.cos(precession_angle) + return mob.set_theta(origin_theta + dtheta).set_phi(origin_phi + dphi) - def display(self, *cvmobjects: VMobject): - """Displays the colored VMobjects. + self.add_updater(precession) + self.precession_updater = precession + return self - Parameters - ---------- - *cvmobjects - The VMobjects + def stop_precession(self) -> Self: + """Remove the precession camera updater, if any. Returns ------- - np.array - The pixel array with the `cvmobjects` displayed. + :class:`Camera` + The camera after removing the precession updater. """ - batch_image_pairs = it.groupby(cvmobjects, lambda cv: cv.get_background_image()) - curr_array = None - for image, batch in batch_image_pairs: - background_array = self.get_background_array(image) - pixel_array = self.pixel_array - self.camera.display_multiple_non_background_colored_vmobjects( - batch, - pixel_array, - ) - new_array = np.array( - (background_array * pixel_array.astype("float") / 255), - dtype=self.camera.pixel_array_dtype, - ) - if curr_array is None: - curr_array = new_array - else: - curr_array = np.maximum(curr_array, new_array) - self.reset_pixel_array() - return curr_array + updater = self.precession_updater + if updater is not None: + self.remove_updater(updater) + self.precession_updater = None + return self diff --git a/manim/camera/mapping_camera.py b/manim/camera/mapping_camera.py deleted file mode 100644 index 03f0afc3b4..0000000000 --- a/manim/camera/mapping_camera.py +++ /dev/null @@ -1,140 +0,0 @@ -"""A camera that allows mapping between objects.""" - -from __future__ import annotations - -__all__ = ["MappingCamera", "OldMultiCamera", "SplitScreenCamera"] - -import math - -import numpy as np - -from ..camera.camera import Camera -from ..mobject.types.vectorized_mobject import VMobject -from ..utils.config_ops import DictAsObject - -# TODO: Add an attribute to mobjects under which they can specify that they should just -# map their centers but remain otherwise undistorted (useful for labels, etc.) - - -class MappingCamera(Camera): - """Camera object that allows mapping - between objects. - """ - - def __init__( - self, - mapping_func=lambda p: p, - min_num_curves=50, - allow_object_intrusion=False, - **kwargs, - ): - self.mapping_func = mapping_func - self.min_num_curves = min_num_curves - self.allow_object_intrusion = allow_object_intrusion - super().__init__(**kwargs) - - def points_to_pixel_coords(self, mobject, points): - return super().points_to_pixel_coords( - mobject, - np.apply_along_axis(self.mapping_func, 1, points), - ) - - def capture_mobjects(self, mobjects, **kwargs): - mobjects = self.get_mobjects_to_display(mobjects, **kwargs) - if self.allow_object_intrusion: - mobject_copies = mobjects - else: - mobject_copies = [mobject.copy() for mobject in mobjects] - for mobject in mobject_copies: - if ( - isinstance(mobject, VMobject) - and 0 < mobject.get_num_curves() < self.min_num_curves - ): - mobject.insert_n_curves(self.min_num_curves) - super().capture_mobjects( - mobject_copies, - include_submobjects=False, - excluded_mobjects=None, - ) - - -# Note: This allows layering of multiple cameras onto the same portion of the pixel array, -# the later cameras overwriting the former -# -# TODO: Add optional separator borders between cameras (or perhaps peel this off into a -# CameraPlusOverlay class) - - -# TODO, the classes below should likely be deleted -class OldMultiCamera(Camera): - def __init__(self, *cameras_with_start_positions, **kwargs): - self.shifted_cameras = [ - DictAsObject( - { - "camera": camera_with_start_positions[0], - "start_x": camera_with_start_positions[1][1], - "start_y": camera_with_start_positions[1][0], - "end_x": camera_with_start_positions[1][1] - + camera_with_start_positions[0].pixel_width, - "end_y": camera_with_start_positions[1][0] - + camera_with_start_positions[0].pixel_height, - }, - ) - for camera_with_start_positions in cameras_with_start_positions - ] - super().__init__(**kwargs) - - def capture_mobjects(self, mobjects, **kwargs): - for shifted_camera in self.shifted_cameras: - shifted_camera.camera.capture_mobjects(mobjects, **kwargs) - - self.pixel_array[ - shifted_camera.start_y : shifted_camera.end_y, - shifted_camera.start_x : shifted_camera.end_x, - ] = shifted_camera.camera.pixel_array - - def set_background(self, pixel_array, **kwargs): - for shifted_camera in self.shifted_cameras: - shifted_camera.camera.set_background( - pixel_array[ - shifted_camera.start_y : shifted_camera.end_y, - shifted_camera.start_x : shifted_camera.end_x, - ], - **kwargs, - ) - - def set_pixel_array(self, pixel_array, **kwargs): - super().set_pixel_array(pixel_array, **kwargs) - for shifted_camera in self.shifted_cameras: - shifted_camera.camera.set_pixel_array( - pixel_array[ - shifted_camera.start_y : shifted_camera.end_y, - shifted_camera.start_x : shifted_camera.end_x, - ], - **kwargs, - ) - - def init_background(self): - super().init_background() - for shifted_camera in self.shifted_cameras: - shifted_camera.camera.init_background() - - -# A OldMultiCamera which, when called with two full-size cameras, initializes itself -# as a split screen, also taking care to resize each individual camera within it - - -class SplitScreenCamera(OldMultiCamera): - def __init__(self, left_camera, right_camera, **kwargs): - Camera.__init__(self, **kwargs) # to set attributes such as pixel_width - self.left_camera = left_camera - self.right_camera = right_camera - - half_width = math.ceil(self.pixel_width / 2) - for camera in [self.left_camera, self.right_camera]: - camera.reset_pixel_shape(camera.pixel_height, half_width) - - super().__init__( - (left_camera, (0, 0)), - (right_camera, (0, half_width)), - ) diff --git a/manim/camera/moving_camera.py b/manim/camera/moving_camera.py deleted file mode 100644 index 1d01d01e22..0000000000 --- a/manim/camera/moving_camera.py +++ /dev/null @@ -1,253 +0,0 @@ -"""A camera able to move through a scene. - -.. SEEALSO:: - - :mod:`.moving_camera_scene` - -""" - -from __future__ import annotations - -__all__ = ["MovingCamera"] - -import numpy as np - -from .. import config -from ..camera.camera import Camera -from ..constants import DOWN, LEFT, RIGHT, UP -from ..mobject.frame import ScreenRectangle -from ..mobject.mobject import Mobject -from ..utils.color import WHITE - - -class MovingCamera(Camera): - """ - Stays in line with the height, width and position of it's 'frame', which is a Rectangle - - .. SEEALSO:: - - :class:`.MovingCameraScene` - - """ - - def __init__( - self, - frame=None, - fixed_dimension=0, # width - default_frame_stroke_color=WHITE, - default_frame_stroke_width=0, - **kwargs, - ): - """ - Frame is a Mobject, (should almost certainly be a rectangle) - determining which region of space the camera displays - """ - self.fixed_dimension = fixed_dimension - self.default_frame_stroke_color = default_frame_stroke_color - self.default_frame_stroke_width = default_frame_stroke_width - if frame is None: - frame = ScreenRectangle(height=config["frame_height"]) - frame.set_stroke( - self.default_frame_stroke_color, - self.default_frame_stroke_width, - ) - self.frame = frame - super().__init__(**kwargs) - - # TODO, make these work for a rotated frame - @property - def frame_height(self): - """Returns the height of the frame. - - Returns - ------- - float - The height of the frame. - """ - return self.frame.height - - @property - def frame_width(self): - """Returns the width of the frame - - Returns - ------- - float - The width of the frame. - """ - return self.frame.width - - @property - def frame_center(self): - """Returns the centerpoint of the frame in cartesian coordinates. - - Returns - ------- - np.array - The cartesian coordinates of the center of the frame. - """ - return self.frame.get_center() - - @frame_height.setter - def frame_height(self, frame_height: float): - """Sets the height of the frame in MUnits. - - Parameters - ---------- - frame_height - The new frame_height. - """ - self.frame.stretch_to_fit_height(frame_height) - - @frame_width.setter - def frame_width(self, frame_width: float): - """Sets the width of the frame in MUnits. - - Parameters - ---------- - frame_width - The new frame_width. - """ - self.frame.stretch_to_fit_width(frame_width) - - @frame_center.setter - def frame_center(self, frame_center: np.ndarray | list | tuple | Mobject): - """Sets the centerpoint of the frame. - - Parameters - ---------- - frame_center - The point to which the frame must be moved. - If is of type mobject, the frame will be moved to - the center of that mobject. - """ - self.frame.move_to(frame_center) - - def capture_mobjects(self, mobjects, **kwargs): - # self.reset_frame_center() - # self.realign_frame_shape() - super().capture_mobjects(mobjects, **kwargs) - - # Since the frame can be moving around, the cairo - # context used for updating should be regenerated - # at each frame. So no caching. - def get_cached_cairo_context(self, pixel_array): - """ - Since the frame can be moving around, the cairo - context used for updating should be regenerated - at each frame. So no caching. - """ - return None - - def cache_cairo_context(self, pixel_array, ctx): - """ - Since the frame can be moving around, the cairo - context used for updating should be regenerated - at each frame. So no caching. - """ - pass - - # def reset_frame_center(self): - # self.frame_center = self.frame.get_center() - - # def realign_frame_shape(self): - # height, width = self.frame_shape - # if self.fixed_dimension == 0: - # self.frame_shape = (height, self.frame.width - # else: - # self.frame_shape = (self.frame.height, width) - # self.resize_frame_shape(fixed_dimension=self.fixed_dimension) - - def get_mobjects_indicating_movement(self): - """ - Returns all mobjects whose movement implies that the camera - should think of all other mobjects on the screen as moving - - Returns - ------- - list - """ - return [self.frame] - - def auto_zoom( - self, - mobjects: list[Mobject], - margin: float = 0, - only_mobjects_in_frame: bool = False, - animate: bool = True, - ): - """Zooms on to a given array of mobjects (or a singular mobject) - and automatically resizes to frame all the mobjects. - - .. NOTE:: - - This method only works when 2D-objects in the XY-plane are considered, it - will not work correctly when the camera has been rotated. - - Parameters - ---------- - mobjects - The mobject or array of mobjects that the camera will focus on. - - margin - The width of the margin that is added to the frame (optional, 0 by default). - - only_mobjects_in_frame - If set to ``True``, only allows focusing on mobjects that are already in frame. - - animate - If set to ``False``, applies the changes instead of returning the corresponding animation - - Returns - ------- - Union[_AnimationBuilder, ScreenRectangle] - _AnimationBuilder that zooms the camera view to a given list of mobjects - or ScreenRectangle with position and size updated to zoomed position. - - """ - scene_critical_x_left = None - scene_critical_x_right = None - scene_critical_y_up = None - scene_critical_y_down = None - - for m in mobjects: - if (m == self.frame) or ( - only_mobjects_in_frame and not self.is_in_frame(m) - ): - # detected camera frame, should not be used to calculate final position of camera - continue - - # initialize scene critical points with first mobjects critical points - if scene_critical_x_left is None: - scene_critical_x_left = m.get_critical_point(LEFT)[0] - scene_critical_x_right = m.get_critical_point(RIGHT)[0] - scene_critical_y_up = m.get_critical_point(UP)[1] - scene_critical_y_down = m.get_critical_point(DOWN)[1] - - else: - if m.get_critical_point(LEFT)[0] < scene_critical_x_left: - scene_critical_x_left = m.get_critical_point(LEFT)[0] - - if m.get_critical_point(RIGHT)[0] > scene_critical_x_right: - scene_critical_x_right = m.get_critical_point(RIGHT)[0] - - if m.get_critical_point(UP)[1] > scene_critical_y_up: - scene_critical_y_up = m.get_critical_point(UP)[1] - - if m.get_critical_point(DOWN)[1] < scene_critical_y_down: - scene_critical_y_down = m.get_critical_point(DOWN)[1] - - # calculate center x and y - x = (scene_critical_x_left + scene_critical_x_right) / 2 - y = (scene_critical_y_up + scene_critical_y_down) / 2 - - # calculate proposed width and height of zoomed scene - new_width = abs(scene_critical_x_left - scene_critical_x_right) - new_height = abs(scene_critical_y_up - scene_critical_y_down) - - m_target = self.frame.animate if animate else self.frame - # zoom to fit all mobjects along the side that has the largest size - if new_width / self.frame.width > new_height / self.frame.height: - return m_target.set_x(x).set_y(y).set(width=new_width + margin) - else: - return m_target.set_x(x).set_y(y).set(height=new_height + margin) diff --git a/manim/camera/multi_camera.py b/manim/camera/multi_camera.py deleted file mode 100644 index a5202135e9..0000000000 --- a/manim/camera/multi_camera.py +++ /dev/null @@ -1,101 +0,0 @@ -"""A camera supporting multiple perspectives.""" - -from __future__ import annotations - -__all__ = ["MultiCamera"] - - -from manim.mobject.types.image_mobject import ImageMobject - -from ..camera.moving_camera import MovingCamera -from ..utils.iterables import list_difference_update - - -class MultiCamera(MovingCamera): - """Camera Object that allows for multiple perspectives.""" - - def __init__( - self, - image_mobjects_from_cameras: ImageMobject | None = None, - allow_cameras_to_capture_their_own_display=False, - **kwargs, - ): - """Initialises the MultiCamera - - Parameters - ---------- - image_mobjects_from_cameras - - kwargs - Any valid keyword arguments of MovingCamera. - """ - self.image_mobjects_from_cameras = [] - if image_mobjects_from_cameras is not None: - for imfc in image_mobjects_from_cameras: - self.add_image_mobject_from_camera(imfc) - self.allow_cameras_to_capture_their_own_display = ( - allow_cameras_to_capture_their_own_display - ) - super().__init__(**kwargs) - - def add_image_mobject_from_camera(self, image_mobject_from_camera: ImageMobject): - """Adds an ImageMobject that's been obtained from the camera - into the list ``self.image_mobject_from_cameras`` - - Parameters - ---------- - image_mobject_from_camera - The ImageMobject to add to self.image_mobject_from_cameras - """ - # A silly method to have right now, but maybe there are things - # we want to guarantee about any imfc's added later. - imfc = image_mobject_from_camera - assert isinstance(imfc.camera, MovingCamera) - self.image_mobjects_from_cameras.append(imfc) - - def update_sub_cameras(self): - """Reshape sub_camera pixel_arrays""" - for imfc in self.image_mobjects_from_cameras: - pixel_height, pixel_width = self.pixel_array.shape[:2] - imfc.camera.frame_shape = ( - imfc.camera.frame.height, - imfc.camera.frame.width, - ) - imfc.camera.reset_pixel_shape( - int(pixel_height * imfc.height / self.frame_height), - int(pixel_width * imfc.width / self.frame_width), - ) - - def reset(self): - """Resets the MultiCamera. - - Returns - ------- - MultiCamera - The reset MultiCamera - """ - for imfc in self.image_mobjects_from_cameras: - imfc.camera.reset() - super().reset() - return self - - def capture_mobjects(self, mobjects, **kwargs): - self.update_sub_cameras() - for imfc in self.image_mobjects_from_cameras: - to_add = list(mobjects) - if not self.allow_cameras_to_capture_their_own_display: - to_add = list_difference_update(to_add, imfc.get_family()) - imfc.camera.capture_mobjects(to_add, **kwargs) - super().capture_mobjects(mobjects, **kwargs) - - def get_mobjects_indicating_movement(self): - """Returns all mobjects whose movement implies that the camera - should think of all other mobjects on the screen as moving - - Returns - ------- - list - """ - return [self.frame] + [ - imfc.camera.frame for imfc in self.image_mobjects_from_cameras - ] diff --git a/manim/camera/three_d_camera.py b/manim/camera/three_d_camera.py deleted file mode 100644 index f45854e810..0000000000 --- a/manim/camera/three_d_camera.py +++ /dev/null @@ -1,443 +0,0 @@ -"""A camera that can be positioned and oriented in three-dimensional space.""" - -from __future__ import annotations - -__all__ = ["ThreeDCamera"] - - -from typing import Callable - -import numpy as np - -from manim.mobject.mobject import Mobject -from manim.mobject.three_d.three_d_utils import ( - get_3d_vmob_end_corner, - get_3d_vmob_end_corner_unit_normal, - get_3d_vmob_start_corner, - get_3d_vmob_start_corner_unit_normal, -) -from manim.mobject.value_tracker import ValueTracker - -from .. import config -from ..camera.camera import Camera -from ..constants import * -from ..mobject.types.point_cloud_mobject import Point -from ..utils.color import get_shaded_rgb -from ..utils.family import extract_mobject_family_members -from ..utils.space_ops import rotation_about_z, rotation_matrix - - -class ThreeDCamera(Camera): - def __init__( - self, - focal_distance=20.0, - shading_factor=0.2, - default_distance=5.0, - light_source_start_point=9 * DOWN + 7 * LEFT + 10 * OUT, - should_apply_shading=True, - exponential_projection=False, - phi=0, - theta=-90 * DEGREES, - gamma=0, - zoom=1, - **kwargs, - ): - """Initializes the ThreeDCamera - - Parameters - ---------- - *kwargs - Any keyword argument of Camera. - """ - self._frame_center = Point(kwargs.get("frame_center", ORIGIN), stroke_width=0) - super().__init__(**kwargs) - self.focal_distance = focal_distance - self.phi = phi - self.theta = theta - self.gamma = gamma - self.zoom = zoom - self.shading_factor = shading_factor - self.default_distance = default_distance - self.light_source_start_point = light_source_start_point - self.light_source = Point(self.light_source_start_point) - self.should_apply_shading = should_apply_shading - self.exponential_projection = exponential_projection - self.max_allowable_norm = 3 * config["frame_width"] - self.phi_tracker = ValueTracker(self.phi) - self.theta_tracker = ValueTracker(self.theta) - self.focal_distance_tracker = ValueTracker(self.focal_distance) - self.gamma_tracker = ValueTracker(self.gamma) - self.zoom_tracker = ValueTracker(self.zoom) - self.fixed_orientation_mobjects = {} - self.fixed_in_frame_mobjects = set() - self.reset_rotation_matrix() - - @property - def frame_center(self): - return self._frame_center.points[0] - - @frame_center.setter - def frame_center(self, point): - self._frame_center.move_to(point) - - def capture_mobjects(self, mobjects, **kwargs): - self.reset_rotation_matrix() - super().capture_mobjects(mobjects, **kwargs) - - def get_value_trackers(self): - """A list of :class:`ValueTrackers <.ValueTracker>` of phi, theta, focal_distance, - gamma and zoom. - - Returns - ------- - list - list of ValueTracker objects - """ - return [ - self.phi_tracker, - self.theta_tracker, - self.focal_distance_tracker, - self.gamma_tracker, - self.zoom_tracker, - ] - - def modified_rgbas(self, vmobject, rgbas): - if not self.should_apply_shading: - return rgbas - if vmobject.shade_in_3d and (vmobject.get_num_points() > 0): - light_source_point = self.light_source.points[0] - if len(rgbas) < 2: - shaded_rgbas = rgbas.repeat(2, axis=0) - else: - shaded_rgbas = np.array(rgbas[:2]) - shaded_rgbas[0, :3] = get_shaded_rgb( - shaded_rgbas[0, :3], - get_3d_vmob_start_corner(vmobject), - get_3d_vmob_start_corner_unit_normal(vmobject), - light_source_point, - ) - shaded_rgbas[1, :3] = get_shaded_rgb( - shaded_rgbas[1, :3], - get_3d_vmob_end_corner(vmobject), - get_3d_vmob_end_corner_unit_normal(vmobject), - light_source_point, - ) - return shaded_rgbas - return rgbas - - def get_stroke_rgbas( - self, - vmobject, - background=False, - ): # NOTE : DocStrings From parent - return self.modified_rgbas(vmobject, vmobject.get_stroke_rgbas(background)) - - def get_fill_rgbas(self, vmobject): # NOTE : DocStrings From parent - return self.modified_rgbas(vmobject, vmobject.get_fill_rgbas()) - - def get_mobjects_to_display(self, *args, **kwargs): # NOTE : DocStrings From parent - mobjects = super().get_mobjects_to_display(*args, **kwargs) - rot_matrix = self.get_rotation_matrix() - - def z_key(mob): - if not (hasattr(mob, "shade_in_3d") and mob.shade_in_3d): - return np.inf - # Assign a number to a three dimensional mobjects - # based on how close it is to the camera - return np.dot(mob.get_z_index_reference_point(), rot_matrix.T)[2] - - return sorted(mobjects, key=z_key) - - def get_phi(self): - """Returns the Polar angle (the angle off Z_AXIS) phi. - - Returns - ------- - float - The Polar angle in radians. - """ - return self.phi_tracker.get_value() - - def get_theta(self): - """Returns the Azimuthal i.e the angle that spins the camera around the Z_AXIS. - - Returns - ------- - float - The Azimuthal angle in radians. - """ - return self.theta_tracker.get_value() - - def get_focal_distance(self): - """Returns focal_distance of the Camera. - - Returns - ------- - float - The focal_distance of the Camera in MUnits. - """ - return self.focal_distance_tracker.get_value() - - def get_gamma(self): - """Returns the rotation of the camera about the vector from the ORIGIN to the Camera. - - Returns - ------- - float - The angle of rotation of the camera about the vector - from the ORIGIN to the Camera in radians - """ - return self.gamma_tracker.get_value() - - def get_zoom(self): - """Returns the zoom amount of the camera. - - Returns - ------- - float - The zoom amount of the camera. - """ - return self.zoom_tracker.get_value() - - def set_phi(self, value: float): - """Sets the polar angle i.e the angle between Z_AXIS and Camera through ORIGIN in radians. - - Parameters - ---------- - value - The new value of the polar angle in radians. - """ - self.phi_tracker.set_value(value) - - def set_theta(self, value: float): - """Sets the azimuthal angle i.e the angle that spins the camera around Z_AXIS in radians. - - Parameters - ---------- - value - The new value of the azimuthal angle in radians. - """ - self.theta_tracker.set_value(value) - - def set_focal_distance(self, value: float): - """Sets the focal_distance of the Camera. - - Parameters - ---------- - value - The focal_distance of the Camera. - """ - self.focal_distance_tracker.set_value(value) - - def set_gamma(self, value: float): - """Sets the angle of rotation of the camera about the vector from the ORIGIN to the Camera. - - Parameters - ---------- - value - The new angle of rotation of the camera. - """ - self.gamma_tracker.set_value(value) - - def set_zoom(self, value: float): - """Sets the zoom amount of the camera. - - Parameters - ---------- - value - The zoom amount of the camera. - """ - self.zoom_tracker.set_value(value) - - def reset_rotation_matrix(self): - """Sets the value of self.rotation_matrix to - the matrix corresponding to the current position of the camera - """ - self.rotation_matrix = self.generate_rotation_matrix() - - def get_rotation_matrix(self): - """Returns the matrix corresponding to the current position of the camera. - - Returns - ------- - np.array - The matrix corresponding to the current position of the camera. - """ - return self.rotation_matrix - - def generate_rotation_matrix(self): - """Generates a rotation matrix based off the current position of the camera. - - Returns - ------- - np.array - The matrix corresponding to the current position of the camera. - """ - phi = self.get_phi() - theta = self.get_theta() - gamma = self.get_gamma() - matrices = [ - rotation_about_z(-theta - 90 * DEGREES), - rotation_matrix(-phi, RIGHT), - rotation_about_z(gamma), - ] - result = np.identity(3) - for matrix in matrices: - result = np.dot(matrix, result) - return result - - def project_points(self, points: np.ndarray | list): - """Applies the current rotation_matrix as a projection - matrix to the passed array of points. - - Parameters - ---------- - points - The list of points to project. - - Returns - ------- - np.array - The points after projecting. - """ - frame_center = self.frame_center - focal_distance = self.get_focal_distance() - zoom = self.get_zoom() - rot_matrix = self.get_rotation_matrix() - - points = points - frame_center - points = np.dot(points, rot_matrix.T) - zs = points[:, 2] - for i in 0, 1: - if self.exponential_projection: - # Proper projection would involve multiplying - # x and y by d / (d-z). But for points with high - # z value that causes weird artifacts, and applying - # the exponential helps smooth it out. - factor = np.exp(zs / focal_distance) - lt0 = zs < 0 - factor[lt0] = focal_distance / (focal_distance - zs[lt0]) - else: - factor = focal_distance / (focal_distance - zs) - factor[(focal_distance - zs) < 0] = 10**6 - points[:, i] *= factor * zoom - return points - - def project_point(self, point: list | np.ndarray): - """Applies the current rotation_matrix as a projection - matrix to the passed point. - - Parameters - ---------- - point - The point to project. - - Returns - ------- - np.array - The point after projection. - """ - return self.project_points(point.reshape((1, 3)))[0, :] - - def transform_points_pre_display( - self, - mobject, - points, - ): # TODO: Write Docstrings for this Method. - points = super().transform_points_pre_display(mobject, points) - fixed_orientation = mobject in self.fixed_orientation_mobjects - fixed_in_frame = mobject in self.fixed_in_frame_mobjects - - if fixed_in_frame: - return points - if fixed_orientation: - center_func = self.fixed_orientation_mobjects[mobject] - center = center_func() - new_center = self.project_point(center) - return points + (new_center - center) - else: - return self.project_points(points) - - def add_fixed_orientation_mobjects( - self, - *mobjects: Mobject, - use_static_center_func: bool = False, - center_func: Callable[[], np.ndarray] | None = None, - ): - """This method allows the mobject to have a fixed orientation, - even when the camera moves around. - E.G If it was passed through this method, facing the camera, it - will continue to face the camera even as the camera moves. - Highly useful when adding labels to graphs and the like. - - Parameters - ---------- - *mobjects - The mobject whose orientation must be fixed. - use_static_center_func - Whether or not to use the function that takes the mobject's - center as centerpoint, by default False - center_func - The function which returns the centerpoint - with respect to which the mobject will be oriented, by default None - """ - - # This prevents the computation of mobject.get_center - # every single time a projection happens - def get_static_center_func(mobject): - point = mobject.get_center() - return lambda: point - - for mobject in mobjects: - if center_func: - func = center_func - elif use_static_center_func: - func = get_static_center_func(mobject) - else: - func = mobject.get_center - for submob in mobject.get_family(): - self.fixed_orientation_mobjects[submob] = func - - def add_fixed_in_frame_mobjects(self, *mobjects: Mobject): - """This method allows the mobject to have a fixed position, - even when the camera moves around. - E.G If it was passed through this method, at the top of the frame, it - will continue to be displayed at the top of the frame. - - Highly useful when displaying Titles or formulae or the like. - - Parameters - ---------- - **mobjects - The mobject to fix in frame. - """ - for mobject in extract_mobject_family_members(mobjects): - self.fixed_in_frame_mobjects.add(mobject) - - def remove_fixed_orientation_mobjects(self, *mobjects: Mobject): - """If a mobject was fixed in its orientation by passing it through - :meth:`.add_fixed_orientation_mobjects`, then this undoes that fixing. - The Mobject will no longer have a fixed orientation. - - Parameters - ---------- - mobjects - The mobjects whose orientation need not be fixed any longer. - """ - for mobject in extract_mobject_family_members(mobjects): - if mobject in self.fixed_orientation_mobjects: - del self.fixed_orientation_mobjects[mobject] - - def remove_fixed_in_frame_mobjects(self, *mobjects: Mobject): - """If a mobject was fixed in frame by passing it through - :meth:`.add_fixed_in_frame_mobjects`, then this undoes that fixing. - The Mobject will no longer be fixed in frame. - - Parameters - ---------- - mobjects - The mobjects which need not be fixed in frame any longer. - """ - for mobject in extract_mobject_family_members(mobjects): - if mobject in self.fixed_in_frame_mobjects: - self.fixed_in_frame_mobjects.remove(mobject) diff --git a/manim/cli/checkhealth/commands.py b/manim/cli/checkhealth/commands.py index 3750f63d4f..b1a44d1353 100644 --- a/manim/cli/checkhealth/commands.py +++ b/manim/cli/checkhealth/commands.py @@ -84,7 +84,9 @@ def construct(self) -> None: self.execution_time = timeit.timeit(self._inner_construct, number=1) with mn.tempconfig({"preview": True, "disable_caching": True}): - scene = CheckHealthDemo() - scene.render() + manager = mn.Manager(CheckHealthDemo) + manager.render() - click.echo(f"Scene rendered in {scene.execution_time:.2f} seconds.") + click.echo( + f"Scene rendered in {manager.scene.execution_time:.2f} seconds." + ) diff --git a/manim/cli/render/commands.py b/manim/cli/render/commands.py index fde82f4970..d3ef66265d 100644 --- a/manim/cli/render/commands.py +++ b/manim/cli/render/commands.py @@ -31,7 +31,8 @@ from manim.cli.render.global_options import global_options from manim.cli.render.output_options import output_options from manim.cli.render.render_options import render_options -from manim.constants import EPILOG, RendererType +from manim.constants import EPILOG +from manim.manager import Manager from manim.utils.module_ops import scene_classes_from_file __all__ = ["render"] @@ -75,14 +76,6 @@ def render(**kwargs: Any) -> ClickArgs | dict[str, Any]: SCENES is an optional list of scenes in the file. """ - if kwargs["save_as_gif"]: - logger.warning("--save_as_gif is deprecated, please use --format=gif instead!") - kwargs["format"] = "gif" - - if kwargs["save_pngs"]: - logger.warning("--save_pngs is deprecated, please use --format=png instead!") - kwargs["format"] = "png" - if kwargs["show_in_file_browser"]: logger.warning( "The short form of show_in_file_browser is deprecated and will be moved to support --format.", @@ -94,38 +87,14 @@ def render(**kwargs: Any) -> ClickArgs | dict[str, Any]: config.digest_args(click_args) file = Path(config.input_file) - if config.renderer == RendererType.OPENGL: - from manim.renderer.opengl_renderer import OpenGLRenderer - - try: - renderer = OpenGLRenderer() - keep_running = True - while keep_running: - for SceneClass in scene_classes_from_file(file): - with tempconfig({}): - scene = SceneClass(renderer) - rerun = scene.render() - if rerun or config["write_all"]: - renderer.num_plays = 0 - continue - else: - keep_running = False - break - if config["write_all"]: - keep_running = False - - except Exception: - error_console.print_exception() - sys.exit(1) - else: + try: for SceneClass in scene_classes_from_file(file): - try: - with tempconfig({}): - scene = SceneClass() - scene.render() - except Exception: - error_console.print_exception() - sys.exit(1) + with tempconfig({}): + manager = Manager(SceneClass) + manager.render() + except Exception: + error_console.print_exception() + sys.exit(1) if config.notify_outdated_version: manim_info_url = "https://pypi.org/pypi/manim/json" diff --git a/manim/cli/render/global_options.py b/manim/cli/render/global_options.py index 32f9547b0c..b46da44973 100644 --- a/manim/cli/render/global_options.py +++ b/manim/cli/render/global_options.py @@ -12,6 +12,7 @@ __all__ = ["global_options"] + logger = logging.getLogger("manim") diff --git a/manim/cli/render/output_options.py b/manim/cli/render/output_options.py index a7613f1565..8af7fadabc 100644 --- a/manim/cli/render/output_options.py +++ b/manim/cli/render/output_options.py @@ -21,10 +21,11 @@ help="Zero padding for PNG file names.", ), option( + "-w", "--write_to_movie", is_flag=True, default=None, - help="Write the video rendered with opengl to a file.", + help="Write the video to a file.", ), option( "--media_dir", diff --git a/manim/cli/render/render_options.py b/manim/cli/render/render_options.py index 0f069c04e0..422be0a365 100644 --- a/manim/cli/render/render_options.py +++ b/manim/cli/render/render_options.py @@ -7,7 +7,7 @@ from cloup import Choice, option, option_group -from manim.constants import QUALITIES, RendererType +from manim.constants import QUALITIES if TYPE_CHECKING: from click import Context, Option @@ -16,6 +16,8 @@ logger = logging.getLogger("manim") +__all__ = ["render_options"] + def validate_scene_range( ctx: Context, param: Option, value: str | None @@ -112,6 +114,13 @@ def validate_resolution( "renders all scenes after n_0.", default=None, ), + option( + "-g", + "--groups", + callback=lambda ctx, param, value: value.split(","), + help="Render only the specified groups.", + default=[], + ), option( "-a", "--write_all", @@ -165,29 +174,6 @@ def validate_resolution( default=None, help="Render at this frame rate.", ), - option( - "--renderer", - type=Choice( - [renderer_type.value for renderer_type in RendererType], - case_sensitive=False, - ), - help="Select a renderer for your Scene.", - default="cairo", - ), - option( - "-g", - "--save_pngs", - is_flag=True, - default=None, - help="Save each frame as png (Deprecated).", - ), - option( - "-i", - "--save_as_gif", - default=None, - is_flag=True, - help="Save as a gif (Deprecated).", - ), option( "--save_sections", default=None, @@ -200,16 +186,4 @@ def validate_resolution( is_flag=True, help="Render scenes with alpha channel.", ), - option( - "--use_projection_fill_shaders", - is_flag=True, - help="Use shaders for OpenGLVMobject fill which are compatible with transformation matrices.", - default=None, - ), - option( - "--use_projection_stroke_shaders", - is_flag=True, - help="Use shaders for OpenGLVMobject stroke which are compatible with transformation matrices.", - default=None, - ), ) diff --git a/manim/constants.py b/manim/constants.py index 0a3e00da85..13ab617c09 100644 --- a/manim/constants.py +++ b/manim/constants.py @@ -67,12 +67,11 @@ "PI", "TAU", "DEGREES", + "RADIANS", "QUALITIES", "DEFAULT_QUALITY", "EPILOG", "CONTEXT_SETTINGS", - "SHIFT_VALUE", - "CTRL_VALUE", "RendererType", "LineJointType", "CapStyleType", @@ -198,6 +197,9 @@ DEGREES = TAU / 360 """The exchange rate between radians and degrees.""" +RADIANS: float = 1.0 +"""Just a default to select for camera.""" + class QualityDict(TypedDict): flag: str | None @@ -249,8 +251,6 @@ class QualityDict(TypedDict): DEFAULT_QUALITY = "high_quality" EPILOG = "Made with <3 by Manim Community developers." -SHIFT_VALUE = 65505 -CTRL_VALUE = 65507 CONTEXT_SETTINGS = Context.settings( align_option_groups=True, diff --git a/manim/event_handler/__init__.py b/manim/event_handler/__init__.py new file mode 100644 index 0000000000..4a80e3dfbd --- /dev/null +++ b/manim/event_handler/__init__.py @@ -0,0 +1,7 @@ +from __future__ import annotations + +from manim.event_handler.event_dispatcher import EventDispatcher + +# This is supposed to be a Singleton +# i.e., during runtime there should be only one object of Event Dispatcher +EVENT_DISPATCHER = EventDispatcher() diff --git a/manim/event_handler/event_dispatcher.py b/manim/event_handler/event_dispatcher.py new file mode 100644 index 0000000000..5c8f7f663e --- /dev/null +++ b/manim/event_handler/event_dispatcher.py @@ -0,0 +1,92 @@ +from __future__ import annotations + +import numpy as np +from typing_extensions import Any, Self + +from manim.event_handler.event_listener import EventListener +from manim.event_handler.event_type import EventType + + +class EventDispatcher: + def __init__(self) -> None: + self.event_listeners: dict[EventType, list[EventListener]] = { + event_type: [] for event_type in EventType + } + self.mouse_point = np.array((0.0, 0.0, 0.0)) + self.mouse_drag_point = np.array((0.0, 0.0, 0.0)) + self.pressed_keys: set[int] = set() + self.draggable_object_listeners: list[EventListener] = [] + + def add_listener(self, event_listener: EventListener) -> Self: + assert isinstance(event_listener, EventListener) + self.event_listeners[event_listener.event_type].append(event_listener) + return self + + def remove_listener(self, event_listener: EventListener) -> Self: + assert isinstance(event_listener, EventListener) + try: + while event_listener in self.event_listeners[event_listener.event_type]: + self.event_listeners[event_listener.event_type].remove(event_listener) + except Exception: + # raise ValueError("Handler is not handling this event, so cannot remove it.") + pass + return self + + def dispatch(self, event_type: EventType, **event_data: Any) -> bool | None: + if event_type == EventType.MouseMotionEvent: + self.mouse_point = event_data["point"] + elif event_type == EventType.MouseDragEvent: + self.mouse_drag_point = event_data["point"] + elif event_type == EventType.KeyPressEvent: + self.pressed_keys.add(event_data["symbol"]) # Modifiers? + elif event_type == EventType.KeyReleaseEvent: + self.pressed_keys.difference_update({event_data["symbol"]}) # Modifiers? + elif event_type == EventType.MousePressEvent: + self.draggable_object_listeners = [ + listener + for listener in self.event_listeners[EventType.MouseDragEvent] + if listener.mobject.is_point_touching(self.mouse_point) + ] + elif event_type == EventType.MouseReleaseEvent: + self.draggable_object_listeners = [] + + propagate_event = None + + if event_type == EventType.MouseDragEvent: + for listener in self.draggable_object_listeners: + assert isinstance(listener, EventListener) + propagate_event = listener.callback(listener.mobject, event_data) + if propagate_event is not None and propagate_event is False: + return propagate_event + + elif event_type.value.startswith("mouse"): + for listener in self.event_listeners[event_type]: + if listener.mobject.is_point_touching(self.mouse_point): + propagate_event = listener.callback(listener.mobject, event_data) + if propagate_event is not None and propagate_event is False: + return propagate_event + + elif event_type.value.startswith("key"): + for listener in self.event_listeners[event_type]: + propagate_event = listener.callback(listener.mobject, event_data) + if propagate_event is not None and propagate_event is False: + return propagate_event + + return propagate_event + + def get_listeners_count(self) -> int: + return sum([len(value) for key, value in self.event_listeners.items()]) + + def get_mouse_point(self) -> np.ndarray: + return self.mouse_point + + def get_mouse_drag_point(self) -> np.ndarray: + return self.mouse_drag_point + + def is_key_pressed(self, symbol: int) -> bool: + return symbol in self.pressed_keys + + __iadd__ = add_listener + __isub__ = remove_listener + __call__ = dispatch + __len__ = get_listeners_count diff --git a/manim/event_handler/event_listener.py b/manim/event_handler/event_listener.py new file mode 100644 index 0000000000..33c0349d84 --- /dev/null +++ b/manim/event_handler/event_listener.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +import contextlib +from typing import TYPE_CHECKING, Callable + +if TYPE_CHECKING: + from typing_extensions import Any + + from manim.event_handler.event_type import EventType + from manim.mobject.opengl.opengl_mobject import OpenGLMobject + + +class EventListener: + def __init__( + self, + mobject: OpenGLMobject, + event_type: EventType, + event_callback: Callable[[OpenGLMobject, dict[str, str]], None], + ) -> None: + self.mobject = mobject + self.event_type = event_type + self.callback = event_callback + + def __eq__(self, other: Any) -> bool: + return_val = False + if isinstance(other, EventListener): + with contextlib.suppress(Exception): + return_val = ( + self.callback == other.callback + and self.mobject == other.mobject + and self.event_type == other.event_type + ) + return return_val diff --git a/manim/event_handler/event_type.py b/manim/event_handler/event_type.py new file mode 100644 index 0000000000..a49b583093 --- /dev/null +++ b/manim/event_handler/event_type.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from enum import Enum + + +class EventType(Enum): + MouseMotionEvent = "mouse_motion_event" + MousePressEvent = "mouse_press_event" + MouseReleaseEvent = "mouse_release_event" + MouseDragEvent = "mouse_drag_event" + MouseScrollEvent = "mouse_scroll_event" + KeyPressEvent = "key_press_event" + KeyReleaseEvent = "key_release_event" diff --git a/manim/event_handler/window.py b/manim/event_handler/window.py new file mode 100644 index 0000000000..cde15873fe --- /dev/null +++ b/manim/event_handler/window.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from typing import Protocol + + +class WindowProtocol(Protocol): + @property + def is_closing(self) -> bool: ... + + def swap_buffers(self) -> object: ... + + def close(self) -> object: ... + + def clear(self) -> object: ... diff --git a/manim/file_writer/__init__.py b/manim/file_writer/__init__.py new file mode 100644 index 0000000000..c3dd8a7056 --- /dev/null +++ b/manim/file_writer/__init__.py @@ -0,0 +1,4 @@ +from __future__ import annotations + +from .file_writer import FileWriter +from .sections import * diff --git a/manim/scene/scene_file_writer.py b/manim/file_writer/file_writer.py similarity index 90% rename from manim/scene/scene_file_writer.py rename to manim/file_writer/file_writer.py index 4293de7105..660b795436 100644 --- a/manim/scene/scene_file_writer.py +++ b/manim/file_writer/file_writer.py @@ -2,7 +2,7 @@ from __future__ import annotations -__all__ = ["SceneFileWriter"] +__all__ = ["FileWriter"] import json import shutil @@ -20,12 +20,11 @@ from pydub import AudioSegment from manim import __version__ -from manim.typing import PixelArray - -from .. import config, logger -from .._config.logger_utils import set_file_logger -from ..constants import RendererType -from ..utils.file_ops import ( +from manim._config import config, logger +from manim._config.logger_utils import set_file_logger +from manim.file_writer.protocols import FileWriterProtocol +from manim.file_writer.sections import DefaultSectionType, Section +from manim.utils.file_ops import ( add_extension_if_not_present, add_version_before_extension, guarantee_existence, @@ -34,14 +33,13 @@ modify_atime, write_to_movie, ) -from ..utils.sounds import get_full_sound_file_path -from .section import DefaultSectionType, Section +from manim.utils.sounds import get_full_sound_file_path if TYPE_CHECKING: - from manim.renderer.opengl_renderer import OpenGLRenderer + from manim.typing import PixelArray, StrOrBytesPath -def to_av_frame_rate(fps): +def to_av_frame_rate(fps: float) -> Fraction: epsilon1 = 1e-4 epsilon2 = 0.02 @@ -58,7 +56,11 @@ def to_av_frame_rate(fps): return Fraction(num, denom) -def convert_audio(input_path: Path, output_path: Path, codec_name: str): +def convert_audio( + input_path: StrOrBytesPath, + output_path: StrOrBytesPath, + codec_name: str, +) -> None: with ( av.open(input_path) as input_audio, av.open(output_path, "w") as output_audio, @@ -73,9 +75,9 @@ def convert_audio(input_path: Path, output_path: Path, codec_name: str): output_audio.mux(packet) -class SceneFileWriter: +class FileWriter(FileWriterProtocol): """ - SceneFileWriter is the object that actually writes the animations + FileWriter is the object that actually writes the animations played, into video files, using FFMPEG. This is mostly for Manim's internal use. You will rarely, if ever, have to use the methods for this class, unless tinkering with the very @@ -104,12 +106,12 @@ class SceneFileWriter: force_output_as_scene_name = False - def __init__(self, renderer, scene_name, **kwargs): - self.renderer = renderer + def __init__(self, scene_name: str) -> None: self.init_output_directories(scene_name) self.init_audio() self.frame_count = 0 - self.partial_movie_files: list[str] = [] + self.num_plays = 0 + self.partial_movie_files: list[str | None] = [] self.subcaptions: list[srt.Subtitle] = [] self.sections: list[Section] = [] # first section gets automatically created for convenience @@ -118,7 +120,11 @@ def __init__(self, renderer, scene_name, **kwargs): name="autocreated", type_=DefaultSectionType.NORMAL, skip_animations=False ) - def init_output_directories(self, scene_name): + @classmethod + def use_output_as_scene_name(cls) -> None: + cls.force_output_as_scene_name = True + + def init_output_directories(self, scene_name: str) -> None: """Initialise output directories. Notes @@ -133,7 +139,7 @@ def init_output_directories(self, scene_name): module_name = config.get_dir("input_file").stem if config["input_file"] else "" - if SceneFileWriter.force_output_as_scene_name: + if self.force_output_as_scene_name: self.output_name = Path(scene_name) elif config["output_file"] and not config["write_all"]: self.output_name = config.get_dir("output_file") @@ -225,7 +231,7 @@ def next_section(self, name: str, type_: str, skip_animations: bool) -> None: ), ) - def add_partial_movie_file(self, hash_animation: str): + def add_partial_movie_file(self, hash_animation: str | None) -> None: """Adds a new partial movie file path to `scene.partial_movie_files` and current section from a hash. This method will compute the path from the hash. In addition to that it adds the new animation to the current section. @@ -245,12 +251,12 @@ def add_partial_movie_file(self, hash_animation: str): else: new_partial_movie_file = str( self.partial_movie_directory - / f"{hash_animation}{config['movie_file_extension']}" + / f"{hash_animation}{config.movie_file_extension}" ) self.partial_movie_files.append(new_partial_movie_file) self.sections[-1].partial_movie_files.append(new_partial_movie_file) - def get_resolution_directory(self): + def get_resolution_directory(self) -> str: """Get the name of the resolution directory directly containing the video file. @@ -275,16 +281,16 @@ def get_resolution_directory(self): :class:`str` The name of the directory. """ - pixel_height = config["pixel_height"] - frame_rate = config["frame_rate"] + pixel_height = config.pixel_height + frame_rate = config.frame_rate return f"{pixel_height}p{frame_rate}" # Sound - def init_audio(self): + def init_audio(self) -> None: """Preps the writer for adding audio to the movie.""" self.includes_sound = False - def create_audio_segment(self): + def create_audio_segment(self) -> None: """Creates an empty, silent, Audio Segment.""" self.audio_segment = AudioSegment.silent() @@ -293,7 +299,7 @@ def add_audio_segment( new_segment: AudioSegment, time: float | None = None, gain_to_background: float | None = None, - ): + ) -> None: """ This method adds an audio segment from an AudioSegment type object and suitable parameters. @@ -314,7 +320,7 @@ def add_audio_segment( self.includes_sound = True self.create_audio_segment() segment = self.audio_segment - curr_end = segment.duration_seconds + curr_end: float = segment.duration_seconds if time is None: time = curr_end if time < 0: @@ -338,8 +344,8 @@ def add_sound( sound_file: str, time: float | None = None, gain: float | None = None, - **kwargs, - ): + **kwargs: Any, + ) -> None: """ This method adds an audio segment from a sound file. @@ -378,7 +384,9 @@ def add_sound( self.add_audio_segment(new_segment, time, **kwargs) # Writers - def begin_animation(self, allow_write: bool = False, file_path=None): + def begin_animation( + self, allow_write: bool = False, file_path: str | None = None + ) -> None: """ Used internally by manim to stream the animation to FFMPEG for displaying or writing to a file. @@ -391,7 +399,7 @@ def begin_animation(self, allow_write: bool = False, file_path=None): if write_to_movie() and allow_write: self.open_partial_movie_stream(file_path=file_path) - def end_animation(self, allow_write: bool = False): + def end_animation(self, allow_write: bool = False) -> None: """ Internally used by Manim to stop streaming to FFMPEG gracefully. @@ -403,8 +411,9 @@ def end_animation(self, allow_write: bool = False): """ if write_to_movie() and allow_write: self.close_partial_movie_stream() + self.num_plays += 1 - def listen_and_write(self): + def listen_and_write(self) -> None: """For internal use only: blocks until new frame is available on the queue.""" while True: num_frames, frame_data = self.queue.get() @@ -429,9 +438,7 @@ def encode_and_write_frame(self, frame: PixelArray, num_frames: int) -> None: for packet in self.video_stream.encode(av_frame): self.video_container.mux(packet) - def write_frame( - self, frame_or_renderer: np.ndarray | OpenGLRenderer, num_frames: int = 1 - ): + def write_frame(self, frame: PixelArray, num_frames: int = 1) -> None: """ Used internally by Manim to write a frame to the FFMPEG input buffer. @@ -444,53 +451,43 @@ def write_frame( The number of times to write frame. """ if write_to_movie(): - frame: np.ndarray = ( - frame_or_renderer.get_frame() - if config.renderer == RendererType.OPENGL - else frame_or_renderer - ) - msg = (num_frames, frame) self.queue.put(msg) - if is_png_format() and not config["dry_run"]: - image: Image = ( - frame_or_renderer.get_image() - if config.renderer == RendererType.OPENGL - else Image.fromarray(frame_or_renderer) - ) + if is_png_format() and not config.dry_run: + image = Image.fromarray(frame) target_dir = self.image_file_path.parent / self.image_file_path.stem extension = self.image_file_path.suffix self.output_image( image, target_dir, extension, - config["zero_pad"], + config.zero_pad, ) - def output_image(self, image: Image.Image, target_dir, ext, zero_pad: bool): + def output_image( + self, image: Image.Image, target_dir: str | Path, ext: str, zero_pad: int + ) -> None: if zero_pad: image.save(f"{target_dir}{str(self.frame_count).zfill(zero_pad)}{ext}") else: image.save(f"{target_dir}{self.frame_count}{ext}") self.frame_count += 1 - def save_final_image(self, image: np.ndarray): + def save_image(self, image: PixelArray) -> None: """ - The name is a misnomer. This method saves the image - passed to it as an in the default image directory. + Saves an image in the default image directory. Parameters ---------- image The pixel array of the image to save. """ - if config["dry_run"]: - return if not config["output_file"]: self.image_file_path = add_version_before_extension(self.image_file_path) - image.save(self.image_file_path) + image_processed = Image.fromarray(image) + image_processed.save(self.image_file_path) self.print_file_ready_message(self.image_file_path) def finish(self) -> None: @@ -516,14 +513,14 @@ def finish(self) -> None: if self.subcaptions: self.write_subcaption_file() - def open_partial_movie_stream(self, file_path=None) -> None: + def open_partial_movie_stream(self, file_path: str | None = None) -> None: """Open a container holding a video stream. This is used internally by Manim initialize the container holding the video stream of a partial movie file. """ if file_path is None: - file_path = self.partial_movie_files[self.renderer.num_plays] + file_path = self.partial_movie_files[self.num_plays] self.partial_movie_file_path = file_path fps = to_av_frame_rate(config.frame_rate) @@ -578,11 +575,11 @@ def close_partial_movie_stream(self) -> None: self.video_container.close() logger.info( - f"Animation {self.renderer.num_plays} : Partial movie file written in %(path)s", + f"Animation {self.num_plays} : Partial movie file written in %(path)s", {"path": f"'{self.partial_movie_file_path}'"}, ) - def is_already_cached(self, hash_invocation: str): + def is_already_cached(self, hash_invocation: str) -> bool: """Will check if a file named with `hash_invocation` exists. Parameters @@ -607,9 +604,9 @@ def combine_files( self, input_files: list[str], output_file: Path, - create_gif=False, - includes_sound=False, - ): + create_gif: bool = False, + includes_sound: bool = False, + ) -> None: file_list = self.partial_movie_directory / "partial_movie_file_list.txt" logger.debug( f"Partial movie files to combine ({len(input_files)} files): %(p)s", @@ -708,7 +705,7 @@ def combine_files( partial_movies_input.close() output_container.close() - def combine_to_movie(self): + def combine_to_movie(self) -> None: """Used internally by Manim to combine the separate partial movie files that make up a Scene into a single video file for that Scene. @@ -828,16 +825,16 @@ def combine_to_section_videos(self) -> None: with (self.sections_output_dir / f"{self.output_name}.json").open("w") as file: json.dump(sections_index, file, indent=4) - def clean_cache(self): + def clean_cache(self) -> None: """Will clean the cache by removing the oldest partial_movie_files.""" cached_partial_movies = [ (self.partial_movie_directory / file_name) for file_name in self.partial_movie_directory.iterdir() if file_name != "partial_movie_file_list.txt" ] - if len(cached_partial_movies) > config["max_files_cached"]: + if len(cached_partial_movies) > config.max_files_cached: number_files_to_delete = ( - len(cached_partial_movies) - config["max_files_cached"] + len(cached_partial_movies) - config.max_files_cached ) oldest_files_to_delete = sorted( cached_partial_movies, @@ -846,11 +843,11 @@ def clean_cache(self): for file_to_delete in oldest_files_to_delete: file_to_delete.unlink() logger.info( - f"The partial movie directory is full (> {config['max_files_cached']} files). Therefore, manim has removed the {number_files_to_delete} oldest file(s)." + f"The partial movie directory is full (> {config.max_files_cached} files). Therefore, manim has removed the {number_files_to_delete} oldest file(s)." " You can change this behaviour by changing max_files_cached in config.", ) - def flush_cache_directory(self): + def flush_cache_directory(self) -> None: """Delete all the cached partial movie files""" cached_partial_movies = [ self.partial_movie_directory / file_name @@ -864,7 +861,7 @@ def flush_cache_directory(self): {"par_dir": self.partial_movie_directory}, ) - def write_subcaption_file(self): + def write_subcaption_file(self) -> None: """Writes the subcaption file.""" if config.output_file is None: return @@ -872,7 +869,7 @@ def write_subcaption_file(self): subcaption_file.write_text(srt.compose(self.subcaptions), encoding="utf-8") logger.info(f"Subcaption file has been written as {subcaption_file}") - def print_file_ready_message(self, file_path): + def print_file_ready_message(self, file_path: str | Path) -> None: """Prints the "File Ready" message to STDOUT.""" - config["output_file"] = file_path - logger.info("\nFile ready at %(file_path)s\n", {"file_path": f"'{file_path}'"}) + config.output_file = str(file_path) + logger.info(f"\nFile ready at {str(file_path)!r}\n") diff --git a/manim/file_writer/protocols.py b/manim/file_writer/protocols.py new file mode 100644 index 0000000000..851569352e --- /dev/null +++ b/manim/file_writer/protocols.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +from typing import Protocol + +from manim.typing import PixelArray + + +class FileWriterProtocol(Protocol): + """Protocol for a file writer. + + This is mainly useful for testing purposes, to create + a mock file writer. However, it can be used in plugins. + """ + + num_plays: int + + def __init__(self, scene_name: str) -> None: ... + + def begin_animation(self, allow_write: bool = False) -> object: ... + + def end_animation(self, allow_write: bool = False) -> object: ... + + def is_already_cached(self, hash_invocation: str) -> bool: ... + + def add_partial_movie_file(self, hash_animation: str) -> object: ... + + def write_frame(self, frame: PixelArray) -> object: ... + + def next_section(self, name: str, type_: str, skip_animations: bool) -> object: ... + + def finish(self) -> None: ... + + def save_image(self, image: PixelArray) -> object: ... diff --git a/manim/scene/section.py b/manim/file_writer/sections.py similarity index 99% rename from manim/scene/section.py rename to manim/file_writer/sections.py index af005b52da..728104f32e 100644 --- a/manim/scene/section.py +++ b/manim/file_writer/sections.py @@ -100,5 +100,5 @@ def get_dict(self, sections_dir: Path) -> dict[str, Any]: **video_metadata, ) - def __repr__(self): + def __repr__(self) -> str: return f"
" diff --git a/manim/manager.py b/manim/manager.py new file mode 100644 index 0000000000..2f9cef06ce --- /dev/null +++ b/manim/manager.py @@ -0,0 +1,483 @@ +from __future__ import annotations + +__all__ = ["Manager"] + +import contextlib +import platform +import time +import warnings +from collections.abc import Iterable, Iterator, Sequence +from typing import TYPE_CHECKING, Callable, Generic, TypeVar + +import numpy as np + +from manim import config, logger +from manim.event_handler.window import WindowProtocol +from manim.file_writer import FileWriter +from manim.renderer.opengl_renderer import OpenGLRenderer +from manim.renderer.opengl_renderer_window import Window +from manim.scene.scene import Scene, SceneState +from manim.utils.exceptions import EndSceneEarlyException +from manim.utils.hashing import get_hash_from_play_call +from manim.utils.progressbar import ( + ExperimentalProgressBarWarning, + NullProgressBar, + ProgressBar, + ProgressBarProtocol, +) + +if TYPE_CHECKING: + import numpy.typing as npt + from typing_extensions import Any + + from manim.animation.protocol import AnimationProtocol + from manim.file_writer.protocols import FileWriterProtocol + from manim.renderer.renderer import RendererProtocol + +Scene_co = TypeVar("Scene_co", covariant=True, bound=Scene) + + +class Manager(Generic[Scene_co]): + """ + The Brain of Manim + + .. admonition:: Warning for Developers + + Only methods of this class that are not prefixed with an + underscore (``_``) are stable. If you override any of the + ``_`` methods, consider pinning your version of Manim. + + Usage + ----- + + .. code-block:: python + + class Manimation(Scene): + def construct(self): + self.play(FadeIn(Circle())) + + + Manager(Manimation).render() + """ + + def __init__(self, scene_cls: type[Scene_co]) -> None: + # scene + self.scene: Scene_co = scene_cls(manager=self) + + if not isinstance(self.scene, Scene): + raise ValueError(f"{self.scene!r} is not an instance of Scene") + + self.time = 0.0 + + # Initialize window, if applicable + self.window = self.create_window() + + # this must be done AFTER instantiating a window + self.renderer = self.create_renderer() + + self.file_writer = self.create_file_writer() + self._write_files = config.write_to_movie + + # internal state + self._skipping = False + + # keep these as instance methods so subclasses + # have access to everything + def create_renderer(self) -> RendererProtocol: + """Create and return a renderer instance. + + This can be overridden in subclasses (plugins), if more processing + is needed. + + Returns + ------- + An instance of a renderer + """ + renderer = OpenGLRenderer() + if config.preview: + renderer.use_window() + return renderer + + def create_window(self) -> WindowProtocol | None: + """Create and return a window instance. + + This can be overridden in subclasses (plugins), if more + processing is needed. + + Returns + ------- + A window if previewing, else None + """ + return Window() if config.preview else None # type: ignore[abstract] + + def create_file_writer(self) -> FileWriterProtocol: + """Create and returna file writer instance. + + This can be overridden in subclasses (plugins), if more + processing is needed. + + Returns + ------- + A file writer satisfying :class:`.FileWriterProtocol` + """ + return FileWriter(scene_name=self.scene.get_default_scene_name()) + + def setup(self) -> None: + """Set up processes and manager""" + self.scene.setup() + + # these are used for making sure it feels like the correct + # amount of time has passed in the window instead of rendering + # at full speed + # See the docstring of :meth:`_wait_for_animation_time` + self.virtual_animation_start_time = 0.0 + self.real_animation_start_time = time.perf_counter() + + def render(self) -> None: + """ + Entry point to running a Manim class + + Example + ------- + + .. code-block:: python + + class MyScene(Scene): + def construct(self): + self.play(Create(Circle())) + + + with tempconfig({"preview": True}): + Manager(MyScene).render() + """ + config._warn_about_config_options() + self._render_first_pass() + self._render_second_pass() + self.release() + + def _render_first_pass(self) -> None: + """ + Temporarily use the normal single pass + rendering system + """ + self.setup() + + with contextlib.suppress(EndSceneEarlyException): + self.construct() + self.post_contruct() + self._interact() + + self.tear_down() + + def construct(self) -> None: + if not self.scene.groups_api: + self.scene.construct() + return + + for group in self.scene.find_groups(): + if not config.groups or group.name in config.groups: + group() + elif group.name not in config.groups: + with self.no_render(): + group() + + def _render_second_pass(self) -> None: + """ + In the future, this method could be used + for two pass rendering + """ + ... + + def release(self) -> None: + self.renderer.release() + + def post_contruct(self) -> None: + """Run post-construct hooks, and clean up the file writer.""" + if self.file_writer.num_plays: + self.file_writer.finish() + # otherwise no animations were played + elif config.write_to_movie or config.save_last_frame: + self.render_state(write_frame=False) + # FIXME: for some reason the OpenGLRenderer does not give out the + # correct frame values here + frame = self.renderer.get_pixels() + self.file_writer.save_image(frame) + + self._write_files = False + + def tear_down(self) -> None: + """Tear down the scene and the window.""" + self.scene.tear_down() + + if self.window is not None: + self.window.close() + self.window = None + + def _interact(self) -> None: + """Live interaction with the Window""" + if self.window is None: + return + logger.info( + "\nTips: Using the keys `d`, `f`, or `z` " + "you can interact with the scene. " + "Press `command + q` or `esc` to quit" + ) + # TODO: Replace with actual dt instead + # of hardcoded dt + dt = 1 / config.frame_rate + while not self.window.is_closing: + self._update_frame(dt) + + @contextlib.contextmanager + def no_render(self) -> Iterator[None]: + """Context manager to temporarily disable rendering. + + Usage + ----- + + .. code-block:: python + + with manager.no_render(): + manager._play(FadeIn(Circle())) + """ + self._skipping = True + yield + self._skipping = False + + # ----------------------------------# + # Animation Pipeline # + # ----------------------------------# + + def _update_frame(self, dt: float, *, write_frame: bool | None = None) -> None: + """Update the current frame by ``dt`` + + Parameters + ---------- + dt : the time in between frames + write_frame : Whether to write the result to the output stream (videos ONLY). + Default value checks :attr:`_write_files` to see if it should be written. + """ + self.time += dt + self.scene._update_mobjects(dt) + self.scene.time = self.time + + if self.window is not None: + if not self._skipping: + self.window.clear() + + # if it's closing, then any subsequent methods will + # raise an error because the internal C window pointer is nullptr. + if self.window.is_closing: + raise EndSceneEarlyException() + + if not self._skipping: + self.render_state(write_frame=write_frame) + self._wait_for_animation_time() + + def _wait_for_animation_time(self) -> None: + """Wait for the real time to catch up to the "virtual" animation time. + + Animations can render faster than real time, so we have to + slow the window down for the correct amount of time, such + as during a wait animation. + """ + if self.window is None: + return + + self.window.swap_buffers() + + if self._skipping: + return + + vt = self.time - self.virtual_animation_start_time + rt = time.perf_counter() - self.real_animation_start_time + # we can't sleep because we still need to poll for events, + # e.g. hitting Escape or close + while rt < vt: + if self.window.is_closing: + raise EndSceneEarlyException() + # make sure to poll for events + self.window.swap_buffers() + rt = time.perf_counter() - self.real_animation_start_time + + def _play(self, *animations: AnimationProtocol) -> None: + """Play a bunch of animations""" + self.scene.pre_play() + + if self.window is not None: + self.real_animation_start_time = time.perf_counter() + self.virtual_animation_start_time = self.time + + self._write_hashed_movie_file(animations) + + self.scene.begin_animations(animations) + self._progress_through_animations(animations) + self.scene.finish_animations(animations) + + self.scene.post_play() + + self.file_writer.end_animation(allow_write=self._write_files) + + def _write_hashed_movie_file(self, animations: Sequence[AnimationProtocol]) -> None: + """Compute the hash of a self.play call, and write it to a file + + Essentially, a series of methods that need to be called to successfully + render a frame. + """ + if not config.write_to_movie or self._skipping: + return + + if config.disable_caching: + if not config.disable_caching_warning: + logger.info("Caching disabled...") + hash_current_play = f"uncached_{self.file_writer.num_plays:05}" + else: + hash_current_play = get_hash_from_play_call( + self.scene, + self.scene.camera, + animations, + self.scene.mobjects, + ) + if self.file_writer.is_already_cached(hash_current_play): + logger.info( + f"Animation {self.file_writer.num_plays} : Using cached data (hash : {hash_current_play})" + ) + # TODO: think about how to skip + raise NotImplementedError( + "Skipping cached animations is not implemented yet" + ) + + self.file_writer.add_partial_movie_file(hash_current_play) + self.file_writer.begin_animation(allow_write=self._write_files) + + def _create_progressbar( + self, total: float, description: str, **kwargs: Any + ) -> contextlib.AbstractContextManager[ProgressBarProtocol]: + """Create a progressbar""" + if not config.progress_bar: + return contextlib.nullcontext(NullProgressBar()) + + with warnings.catch_warnings(): + if config.verbosity != "DEBUG": + # Note: update when rich/notebook tqdm is no longer experimental + warnings.simplefilter("ignore", category=ExperimentalProgressBarWarning) + + return ProgressBar( + total=total, + unit="frames", + desc=description % {"num": self.file_writer.num_plays}, + ascii=True if platform.system() == "Windows" else None, + leave=config.progress_bar == "leave", + disable=config.progress_bar == "none", + **kwargs, + ) + + # TODO: change to a single wait animation + def _wait( + self, + duration: float, + *, + stop_condition: Callable[[], bool] | None = None, + ) -> None: + self.scene.pre_play() + + self._write_hashed_movie_file(animations=[]) + + if self.window is not None: + self.real_animation_start_time = time.perf_counter() + self.virtual_animation_start_time = self.time + + update_mobjects = self.scene.should_update_mobjects() + condition = stop_condition or (lambda: False) + + progression = _calc_time_progression(duration) + + state = self.scene.get_state() + + with self._create_progressbar( + progression.shape[0], "Waiting %(num)d: " + ) as progress: + last_t = 0 + for t in progression: + dt, last_t = t - last_t, t + if update_mobjects or stop_condition is not None: + self._update_frame(dt) + if condition(): + break + else: + self.time += dt + self.renderer.render(state) + if self.window is not None and self.window.is_closing: + raise EndSceneEarlyException() + self._wait_for_animation_time() + progress.update(1) + self.scene.post_play() + + self.file_writer.end_animation(allow_write=self._write_files) + + def _progress_through_animations( + self, animations: Sequence[AnimationProtocol] + ) -> None: + last_t = 0.0 + run_time = _calc_runtime(animations) + progression = _calc_time_progression(run_time) + with self._create_progressbar( + progression.shape[0], + f"Animation %(num)d: {animations[0]}{', etc.' if len(animations) > 1 else ''}", + ) as progress: + for t in progression: + dt, last_t = t - last_t, t + self.scene._update_animations(animations, t, dt) + self._update_frame(dt) + progress.update(1) + + # -------------------------# + # Rendering # + # -------------------------# + + def render_state(self, write_frame: bool | None = None) -> None: + """Render the current state of the scene. + + Any extra kwargs are passed to :meth:`_render_frame`. + """ + state = self.scene.get_state() + self._render_frame(state, write_frame=write_frame) + + def _render_frame( + self, state: SceneState, *, write_frame: bool | None = None + ) -> None: + """Renders a frame based on a state, and writes it to the file writers stream. + + This is used for writing a single frame. Any extra kwargs are passed to :meth:`write_frame`. + + .. warning:: + + This method will not work if :meth:`.FileWriter.begin_animation` and + :meth:`.FileWriter.add_partial_movie_file` have not been called. Do NOT + use this to write a single frame! + """ + self.renderer.render(state) + + should_write = write_frame if write_frame is not None else self._write_files + if should_write: + self.write_frame() + + def write_frame(self) -> None: + """Take a frame from the renderer and write it in the file writer.""" + frame = self.renderer.get_pixels() + self.file_writer.write_frame(frame) + + +def _calc_time_progression(run_time: float) -> npt.NDArray[np.float64]: + """Compute the time values at which to evaluate the animation""" + return np.arange(0, run_time, 1 / config.frame_rate) + + +def _calc_runtime(animations: Iterable[AnimationProtocol]) -> float: + """Calculate the runtime of an iterable of animations. + + .. warning:: + + If animations is a generator, this will consume the generator. + """ + return max(animation.get_run_time() for animation in animations) diff --git a/manim/mobject/geometry/arc.py b/manim/mobject/geometry/arc.py index c211deae01..6c30cf3730 100644 --- a/manim/mobject/geometry/arc.py +++ b/manim/mobject/geometry/arc.py @@ -51,7 +51,8 @@ def construct(self): from manim.constants import * from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL -from manim.mobject.types.vectorized_mobject import VGroup, VMobject +from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVGroup as VGroup +from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject as VMobject from manim.utils.color import BLACK, BLUE, RED, WHITE, ParsableManimColor from manim.utils.iterables import adjacent_pairs from manim.utils.space_ops import ( diff --git a/manim/mobject/geometry/boolean_ops.py b/manim/mobject/geometry/boolean_ops.py index a34d6fc7c4..617becfa35 100644 --- a/manim/mobject/geometry/boolean_ops.py +++ b/manim/mobject/geometry/boolean_ops.py @@ -8,7 +8,6 @@ from pathops import Path as SkiaPath from pathops import PathVerb, difference, intersection, union, xor -from manim import config from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL from manim.mobject.types.vectorized_mobject import VMobject @@ -17,7 +16,6 @@ from manim.typing import InternalPoint3D_Array, Point2D_Array -from ...constants import RendererType __all__ = ["Union", "Intersection", "Difference", "Exclusion"] @@ -86,29 +84,15 @@ def _convert_vmobject_to_skia_path(self, vmobject: VMobject) -> SkiaPath: if len(points) == 0: # what? No points so return empty path return path - # In OpenGL it's quadratic beizer curves while on Cairo it's cubic... - if config.renderer == RendererType.OPENGL: - subpaths = vmobject.get_subpaths_from_points(points) - for subpath in subpaths: - quads = vmobject.get_bezier_tuples_from_points(subpath) - start = subpath[0] - path.moveTo(*start[:2]) - for _p0, p1, p2 in quads: - path.quadTo(*p1[:2], *p2[:2]) - if vmobject.consider_points_equals(subpath[0], subpath[-1]): - path.close() - elif config.renderer == RendererType.CAIRO: - subpaths = vmobject.gen_subpaths_from_points_2d(points) # type: ignore[assignment] - for subpath in subpaths: - quads = vmobject.gen_cubic_bezier_tuples_from_points(subpath) - start = subpath[0] - path.moveTo(*start[:2]) - for _p0, p1, p2, p3 in quads: - path.cubicTo(*p1[:2], *p2[:2], *p3[:2]) - - if vmobject.consider_points_equals_2d(subpath[0], subpath[-1]): - path.close() - + subpaths = vmobject.get_subpaths_from_points(points) + for subpath in subpaths: + quads = vmobject.get_bezier_tuples_from_points(subpath) + start = subpath[0] + path.moveTo(*start[:2]) + for _p0, p1, p2 in quads: + path.quadTo(*p1[:2], *p2[:2]) + if vmobject.consider_points_equals(subpath[0], subpath[-1]): + path.close() return path def _convert_skia_path_to_vmobject(self, path: SkiaPath) -> VMobject: diff --git a/manim/mobject/geometry/line.py b/manim/mobject/geometry/line.py index 75a6037ff3..dfd307f6a8 100644 --- a/manim/mobject/geometry/line.py +++ b/manim/mobject/geometry/line.py @@ -18,14 +18,15 @@ import numpy as np -from manim import config from manim.constants import * from manim.mobject.geometry.arc import Arc, ArcBetweenPoints, Dot, TipableVMobject from manim.mobject.geometry.tips import ArrowTriangleFilledTip from manim.mobject.mobject import Mobject from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL from manim.mobject.opengl.opengl_mobject import OpenGLMobject -from manim.mobject.types.vectorized_mobject import DashedVMobject, VGroup, VMobject +from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVGroup as VGroup +from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject as VMobject +from manim.mobject.types.vectorized_mobject import DashedVMobject from manim.utils.color import WHITE from manim.utils.space_ops import angle_of_vector, line_intersection, normalize @@ -641,20 +642,10 @@ def get_default_tip_length(self) -> float: def _set_stroke_width_from_length(self) -> Self: """Sets stroke width based on length.""" max_ratio = self.max_stroke_width_to_length_ratio - if config.renderer == RendererType.OPENGL: - # Mypy does not recognize that the self object in this case - # is a OpenGLVMobject and that the set_stroke method is - # defined here: - # mobject/opengl/opengl_vectorized_mobject.py#L248 - self.set_stroke( # type: ignore[call-arg] - width=min(self.initial_stroke_width, max_ratio * self.get_length()), - recurse=False, - ) - else: - self.set_stroke( - width=min(self.initial_stroke_width, max_ratio * self.get_length()), - family=False, - ) + self.set_stroke( + width=min(self.initial_stroke_width, max_ratio * self.get_length()), + recurse=False, + ) return self diff --git a/manim/mobject/geometry/polygram.py b/manim/mobject/geometry/polygram.py index 482581df12..e5e0f41bcc 100644 --- a/manim/mobject/geometry/polygram.py +++ b/manim/mobject/geometry/polygram.py @@ -24,8 +24,8 @@ from manim.constants import * from manim.mobject.geometry.arc import ArcBetweenPoints -from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL -from manim.mobject.types.vectorized_mobject import VGroup, VMobject +from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject +from manim.mobject.types.vectorized_mobject import VGroup from manim.utils.color import BLUE, WHITE, ParsableManimColor from manim.utils.iterables import adjacent_n_tuples, adjacent_pairs from manim.utils.qhull import QuickHull @@ -46,7 +46,7 @@ from manim.utils.color import ParsableManimColor -class Polygram(VMobject, metaclass=ConvertToOpenGL): +class Polygram(OpenGLVMobject): """A generalized :class:`Polygon`, allowing for disconnected sets of edges. Parameters @@ -260,17 +260,17 @@ def construct(self): if evenly_distribute_anchors: # Determine the average length of each curve - nonZeroLengthArcs = [arc for arc in arcs if len(arc.points) > 4] - if len(nonZeroLengthArcs): + non_zero_length_arcs = [arc for arc in arcs if len(arc.points) > 4] + if len(non_zero_length_arcs): totalArcLength = sum( - [arc.get_arc_length() for arc in nonZeroLengthArcs] + [arc.get_arc_length() for arc in non_zero_length_arcs] ) totalCurveCount = ( - sum([len(arc.points) for arc in nonZeroLengthArcs]) / 4 + sum([len(arc.points) for arc in non_zero_length_arcs]) / 4 ) - averageLengthPerCurve = totalArcLength / totalCurveCount + average_length_per_curve = totalArcLength / totalCurveCount else: - averageLengthPerCurve = 1 + average_length_per_curve = 1 # To ensure that we loop through starting with last arcs = [arcs[-1], *arcs[:-1]] @@ -284,7 +284,7 @@ def construct(self): # Make sure anchors are evenly distributed, if necessary if evenly_distribute_anchors: line.insert_n_curves( - ceil(line.get_length() / averageLengthPerCurve) + ceil(line.get_length() / average_length_per_curve) # type: ignore ) new_points.extend(line.points) @@ -738,7 +738,7 @@ def __init__(self, corner_radius: float | list[float] = 0.5, **kwargs: Any): self.round_corners(self.corner_radius) -class Cutout(VMobject, metaclass=ConvertToOpenGL): +class Cutout(OpenGLVMobject): """A shape with smaller cutouts. Parameters diff --git a/manim/mobject/geometry/tips.py b/manim/mobject/geometry/tips.py index 5479f768e8..f67b61e1f7 100644 --- a/manim/mobject/geometry/tips.py +++ b/manim/mobject/geometry/tips.py @@ -21,7 +21,7 @@ from manim.mobject.geometry.arc import Circle from manim.mobject.geometry.polygram import Square, Triangle from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL -from manim.mobject.types.vectorized_mobject import VMobject +from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject as VMobject from manim.utils.space_ops import angle_of_vector if TYPE_CHECKING: diff --git a/manim/mobject/graph.py b/manim/mobject/graph.py index 9cca91f03e..17f25a3d6a 100644 --- a/manim/mobject/graph.py +++ b/manim/mobject/graph.py @@ -968,7 +968,8 @@ def remove_vertices(self, *vertices): :: >>> G = Graph([1, 2, 3], [(1, 2), (2, 3)]) - >>> removed = G.remove_vertices(2, 3); removed + >>> removed = G.remove_vertices(2, 3) + >>> removed VGroup(Line, Line, Dot, Dot) >>> G Undirected graph on 1 vertices and 0 edges diff --git a/manim/mobject/graphing/coordinate_systems.py b/manim/mobject/graphing/coordinate_systems.py index fa07c7fd53..9d47106e82 100644 --- a/manim/mobject/graphing/coordinate_systems.py +++ b/manim/mobject/graphing/coordinate_systems.py @@ -954,10 +954,10 @@ def plot_surface( .. manim:: PlotSurfaceExample :save_last_frame: - class PlotSurfaceExample(ThreeDScene): + class PlotSurfaceExample(Scene): def construct(self): resolution_fa = 16 - self.set_camera_orientation(phi=75 * DEGREES, theta=-60 * DEGREES) + self.camera.set_orientation(theta=-60 * DEGREES, phi=75 * DEGREES) axes = ThreeDAxes(x_range=(-3, 3, 1), y_range=(-3, 3, 1), z_range=(-5, 5, 1)) def param_trig(u, v): x = u @@ -973,21 +973,8 @@ def param_trig(u, v): ) self.add(axes, trig_plane) """ - if config.renderer == RendererType.CAIRO: + if config.renderer == RendererType.OPENGL: surface = Surface( - lambda u, v: self.c2p(u, v, function(u, v)), - u_range=u_range, - v_range=v_range, - **kwargs, - ) - if colorscale: - surface.set_fill_by_value( - axes=self.copy(), - colorscale=colorscale, - axis=colorscale_axis, - ) - elif config.renderer == RendererType.OPENGL: - surface = OpenGLSurface( lambda u, v: self.c2p(u, v, function(u, v)), u_range=u_range, v_range=v_range, @@ -996,6 +983,9 @@ def param_trig(u, v): colorscale_axis=colorscale_axis, **kwargs, ) + elif config.renderer == RendererType.CAIRO: + # TODO: CairoSurface? + raise NotImplementedError return surface @@ -2467,6 +2457,7 @@ def __init__( self.z_axis = z_axis if config.renderer == RendererType.CAIRO: + # TODO: check in how far these methods are supported by new VMobject class self._add_3d_pieces() self._set_axis_shading() @@ -2528,11 +2519,11 @@ def get_y_axis_label( .. manim:: GetYAxisLabelExample :save_last_frame: - class GetYAxisLabelExample(ThreeDScene): + class GetYAxisLabelExample(Scene): def construct(self): ax = ThreeDAxes() lab = ax.get_y_axis_label(Tex("$y$-label")) - self.set_camera_orientation(phi=2*PI/5, theta=PI/5) + self.camera.set_orientation(theta=PI/5, phi=2*PI/5) self.add(ax, lab) """ positioned_label = self._get_axis_label( @@ -2578,11 +2569,11 @@ def get_z_axis_label( .. manim:: GetZAxisLabelExample :save_last_frame: - class GetZAxisLabelExample(ThreeDScene): + class GetZAxisLabelExample(Scene): def construct(self): ax = ThreeDAxes() lab = ax.get_z_axis_label(Tex("$z$-label")) - self.set_camera_orientation(phi=2*PI/5, theta=PI/5) + self.camera.set_orientation(theta=PI/5, phi=2*PI/5) self.add(ax, lab) """ positioned_label = self._get_axis_label( @@ -2629,9 +2620,9 @@ def get_axis_labels( .. manim:: GetAxisLabelsExample :save_last_frame: - class GetAxisLabelsExample(ThreeDScene): + class GetAxisLabelsExample(Scene): def construct(self): - self.set_camera_orientation(phi=2*PI/5, theta=PI/5) + self.camera.set_orientation(theta=PI/5, phi=2*PI/5) axes = ThreeDAxes() labels = axes.get_axis_labels( Text("x-axis").scale(0.7), Text("y-axis").scale(0.45), Text("z-axis").scale(0.45) diff --git a/manim/mobject/graphing/functions.py b/manim/mobject/graphing/functions.py index 1cd660b894..8eb19fdd3c 100644 --- a/manim/mobject/graphing/functions.py +++ b/manim/mobject/graphing/functions.py @@ -65,7 +65,7 @@ def construct(self): .. manim:: ThreeDParametricSpring :save_last_frame: - class ThreeDParametricSpring(ThreeDScene): + class ThreeDParametricSpring(Scene): def construct(self): curve1 = ParametricFunction( lambda u: ( @@ -76,7 +76,7 @@ def construct(self): ).set_shade_in_3d(True) axes = ThreeDAxes() self.add(axes, curve1) - self.set_camera_orientation(phi=80 * DEGREES, theta=-60 * DEGREES) + self.camera.set_orientation(theta=-60 * DEGREES, phi=80 * DEGREES) self.wait() .. attention:: diff --git a/manim/mobject/logo.py b/manim/mobject/logo.py index 6242a4c645..4f054c7d72 100644 --- a/manim/mobject/logo.py +++ b/manim/mobject/logo.py @@ -149,7 +149,7 @@ def __init__(self, dark_theme: bool = True): self.scale_factor = 1 self.M = VMobjectFromSVGPath(MANIM_SVG_PATHS[0]).flip(cst.RIGHT).center() - self.M.set(stroke_width=0).scale( + self.M.set_stroke(width=0).scale( 7 * cst.DEFAULT_FONT_SIZE * cst.SCALE_FACTOR_PER_FONT_POINT ) self.M.set_fill(color=self.font_color, opacity=1).shift( @@ -166,7 +166,7 @@ def __init__(self, dark_theme: bool = True): anim = VGroup() for ind, path in enumerate(MANIM_SVG_PATHS[1:]): tex = VMobjectFromSVGPath(path).flip(cst.RIGHT).center() - tex.set(stroke_width=0).scale( + tex.set_stroke(width=0).scale( cst.DEFAULT_FONT_SIZE * cst.SCALE_FACTOR_PER_FONT_POINT ) if ind > 0: @@ -261,7 +261,7 @@ def construct(self): ) """ - if direction not in ["left", "right", "center"]: + if direction.lower() not in {"left", "right", "center"}: raise ValueError("direction must be 'left', 'right' or 'center'.") m_shape_offset = 6.25 * self.scale_factor @@ -301,7 +301,7 @@ def slide_and_uncover(mob, alpha): if alpha == 1: self.remove(*[self.anim]) self.add_to_back(self.anim) - mob.shapes.set_z_index(0) + mob.shapes.set_z(0) mob.shapes.save_state() mob.M.save_state() diff --git a/manim/mobject/mobject.py b/manim/mobject/mobject.py index d0b13adfc3..1399845c26 100644 --- a/manim/mobject/mobject.py +++ b/manim/mobject/mobject.py @@ -21,11 +21,11 @@ import numpy as np +from manim import config, logger +from manim.constants import * from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL - -from .. import config, logger -from ..constants import * -from ..utils.color import ( +from manim.mobject.opengl.opengl_mobject import InvisibleMobject +from manim.utils.color import ( BLACK, WHITE, YELLOW_C, @@ -34,14 +34,15 @@ color_gradient, interpolate_color, ) -from ..utils.exceptions import MultiAnimationOverrideException -from ..utils.iterables import list_update, remove_list_redundancies -from ..utils.paths import straight_path -from ..utils.space_ops import angle_between_vectors, normalize, rotation_matrix +from manim.utils.exceptions import MultiAnimationOverrideException +from manim.utils.iterables import list_update, remove_list_redundancies +from manim.utils.paths import straight_path +from manim.utils.space_ops import angle_between_vectors, normalize, rotation_matrix if TYPE_CHECKING: from typing_extensions import Self, TypeAlias + from manim.animation.animation import Animation from manim.typing import ( FunctionOverride, InternalPoint3D, @@ -55,8 +56,6 @@ Vector3D, ) - from ..animation.animation import Animation - TimeBasedUpdater: TypeAlias = Callable[["Mobject", float], object] NonTimeBasedUpdater: TypeAlias = Callable[["Mobject"], object] Updater: TypeAlias = NonTimeBasedUpdater | TimeBasedUpdater @@ -150,7 +149,7 @@ def _assert_valid_submobjects(self, submobjects: Iterable[Mobject]) -> Self: return self._assert_valid_submobjects_internal(submobjects, Mobject) def _assert_valid_submobjects_internal( - self, submobjects: list[Mobject], mob_class: type[Mobject] + self, submobjects: Iterable[Mobject], mob_class: type[Mobject] ) -> Self: for i, submob in enumerate(submobjects): if not isinstance(submob, mob_class): @@ -268,10 +267,12 @@ def set_default(cls, **kwargs) -> None: >>> from manim import Square, GREEN >>> Square.set_default(color=GREEN, fill_opacity=0.25) - >>> s = Square(); s.color, s.fill_opacity + >>> s = Square() + >>> s.color, s.fill_opacity (ManimColor('#83C167'), 0.25) >>> Square.set_default() - >>> s = Square(); s.color, s.fill_opacity + >>> s = Square() + >>> s.color, s.fill_opacity (ManimColor('#FFFFFF'), 0.0) .. manim:: ChangedDefaultTextcolor @@ -676,13 +677,10 @@ def __getattr__(self, attr: str) -> types.MethodType: # Add automatic compatibility layer # between properties and get_* and set_* # methods. - # - # In python 3.9+ we could change this - # logic to use str.remove_prefix instead. if attr.startswith("get_"): # Remove the "get_" prefix - to_get = attr[4:] + to_get = attr.removeprefix("get_") def getter(self): warnings.warn( @@ -824,7 +822,7 @@ def apply_over_attr_arrays(self, func: MappingFunction) -> Self: def get_image(self, camera=None) -> PixelArray: if camera is None: - from ..camera.camera import Camera + from manim.camera.cairo_camera import CairoCamera as Camera camera = Camera() camera.capture_mobject(self) @@ -1315,11 +1313,12 @@ def func(points): def apply_function(self, function: MappingFunction, **kwargs) -> Self: # Default to applying matrix about the origin, not mobjects center - if len(kwargs) == 0: + if not kwargs: kwargs["about_point"] = ORIGIN self.apply_points_function_about_point( lambda points: np.apply_along_axis(function, 1, points), **kwargs ) + self.note_changed_family() return self def apply_function_to_position(self, function: MappingFunction) -> Self: @@ -1335,12 +1334,12 @@ def apply_matrix(self, matrix, **kwargs) -> Self: # Default to applying matrix about the origin, not mobjects center if ("about_point" not in kwargs) and ("about_edge" not in kwargs): kwargs["about_point"] = ORIGIN - full_matrix = np.identity(self.dim) - matrix = np.array(matrix) - full_matrix[: matrix.shape[0], : matrix.shape[1]] = matrix - self.apply_points_function_about_point( - lambda points: np.dot(points, full_matrix.T), **kwargs - ) + # full_matrix = np.identity(self.dim) + # matrix = np.array(matrix) + # full_matrix[: matrix.shape[0], : matrix.shape[1]] = matrix + # self.apply_points_function_about_point( + # lambda points: np.dot(points, full_matrix.T), **kwargs + # ) return self def apply_complex_function( @@ -3014,7 +3013,7 @@ def set_z_index_by_z_Point3D(self) -> Self: return self -class Group(Mobject, metaclass=ConvertToOpenGL): +class Group(Mobject, InvisibleMobject, metaclass=ConvertToOpenGL): """Groups together multiple :class:`Mobjects <.Mobject>`. Notes @@ -3029,6 +3028,45 @@ def __init__(self, *mobjects, **kwargs) -> None: self.add(*mobjects) +class Point(Mobject, InvisibleMobject, metaclass=ConvertToOpenGL): + def __init__( + self, + location: np.ndarray = ORIGIN, + artificial_width: float = 1e-6, + artificial_height: float = 1e-6, + **kwargs, + ): + self.artificial_width = artificial_width + self.artificial_height = artificial_height + super().__init__(**kwargs) + self.set_location(location) + + @property + def width(self): + return self.artificial_width + + @property + def height(self): + return self.artificial_height + + # TODO: properties vs. getter methods? + + def get_width(self): + return self.artificial_width + + def get_height(self): + return self.artificial_height + + def get_location(self): + return self.points[0].copy() + + def get_bounding_box_point(self, *args, **kwargs): + return self.get_location() + + def set_location(self, new_loc): + self.set_points(np.array(new_loc, ndmin=2, dtype=float)) + + class _AnimationBuilder: def __init__(self, mobject) -> None: self.mobject = mobject diff --git a/manim/mobject/opengl/opengl_mobject.py b/manim/mobject/opengl/opengl_mobject.py index 1f7d44d6a3..fae0e89114 100644 --- a/manim/mobject/opengl/opengl_mobject.py +++ b/manim/mobject/opengl/opengl_mobject.py @@ -3,85 +3,128 @@ import copy import inspect import itertools as it +import logging +import numbers +import os +import pickle import random import sys -import types -from collections.abc import Iterable, Iterator, Sequence +from dataclasses import dataclass from functools import partialmethod, wraps from math import ceil -from typing import TYPE_CHECKING, Any, Callable, TypeVar +from typing import TYPE_CHECKING, Any, Generic -import moderngl import numpy as np +from typing_extensions import TypedDict, TypeVar -from manim import config, logger +from manim import config from manim.constants import * -from manim.renderer.shader_wrapper import get_colormap_code +from manim.event_handler import EVENT_DISPATCHER +from manim.event_handler.event_listener import EventListener +from manim.event_handler.event_type import EventType from manim.utils.bezier import integer_interpolate, interpolate -from manim.utils.color import ( - WHITE, - ManimColor, - ParsableManimColor, - color_gradient, - color_to_rgb, - rgb_to_hex, -) -from manim.utils.config_ops import _Data, _Uniforms +from manim.utils.color import * # from ..utils.iterables import batch_by_property from manim.utils.iterables import ( - batch_by_property, list_update, - listify, - make_even, resize_array, resize_preserving_order, - resize_with_interpolation, uniq_chain, ) from manim.utils.paths import straight_path from manim.utils.space_ops import ( angle_between_vectors, + angle_of_vector, + get_norm, normalize, rotation_matrix_transpose, ) +__all__ = ["InvisibleMobject", "OpenGLMobject", "MobjectKwargs"] + if TYPE_CHECKING: + from collections.abc import Iterable, Sequence + from typing import Callable + import numpy.typing as npt - from typing_extensions import Self, TypeAlias - - from manim.renderer.shader_wrapper import ShaderWrapper - from manim.typing import ( - ManimFloat, - MappingFunction, - MatrixMN, - PathFuncType, - Point3D, - Point3D_Array, - Vector3D, - ) - - TimeBasedUpdater: TypeAlias = Callable[["Mobject", float], object] - NonTimeBasedUpdater: TypeAlias = Callable[["Mobject"], object] - Updater: TypeAlias = NonTimeBasedUpdater | TimeBasedUpdater + from typing_extensions import Concatenate, ParamSpec, Self, TypeAlias + + from manim.animation.animation import Animation + from manim.renderer.renderer import RendererData + from manim.typing import ManimFloat, PathFuncType, Point3D, Point3D_Array + TimeBasedUpdater: TypeAlias = Callable[ + ["OpenGLMobject", float], "OpenGLMobject | None" + ] + NonTimeUpdater: TypeAlias = Callable[["OpenGLMobject"], "OpenGLMobject | None"] + Updater: TypeAlias = TimeBasedUpdater | NonTimeUpdater + PointUpdateFunction: TypeAlias = Callable[[np.ndarray], np.ndarray] + + M = TypeVar("M", bound="OpenGLMobject") T = TypeVar("T") + P = ParamSpec("P") + + +R = TypeVar("R", bound="RendererData") +T_co = TypeVar("T_co", covariant=True, bound="OpenGLMobject") + +logger = logging.getLogger("manim") + + +class InvisibleMobject: + """Marker class for rendering a mobject's submobjects, and not the mobject itself. + + By default, if an :class:`OpenGLMobject` can't be rendered, the + :class:`Renderer` raises a warning before attempting to render its + submobjects. However, any subclass of :class:`OpenGLMobject` which is also + marked as a subclass of :class:`InvisibleMobject` raises no warning, + because it's explicitly marked as not intended to be rendered. + """ + pass -def affects_shader_info_id( - func: Callable[[OpenGLMobject], OpenGLMobject], -) -> Callable[[OpenGLMobject], OpenGLMobject]: + +def stash_mobject_pointers( + func: Callable[Concatenate[M, P], T], +) -> Callable[Concatenate[M, P], T]: @wraps(func) - def wrapper(self: OpenGLMobject) -> OpenGLMobject: - for mob in self.get_family(): - func(mob) - mob.refresh_shader_wrapper_id() - return self + def wrapper(self: M, *args: P.args, **kwargs: P.kwargs): + uncopied_attrs = ["parents", "target", "saved_state"] + stash = {} + for attr in uncopied_attrs: + if hasattr(self, attr): + value = getattr(self, attr) + stash[attr] = value + null_value = [] if isinstance(value, list) else None + setattr(self, attr, null_value) + result = func(self, *args, **kwargs) + self.__dict__.update(stash) + return result return wrapper -__all__ = ["OpenGLMobject", "OpenGLGroup", "OpenGLPoint", "_AnimationBuilder"] +@dataclass +class MobjectStatus: + color_changed: bool = False + position_changed: bool = False + rotation_changed: bool = False + scale_changed: bool = False + points_changed: bool = False + + +# TODO: add this to the **kwargs of all mobjects that use OpenGLMobject +class MobjectKwargs(TypedDict, total=False): + color: ParsableManimColor | Sequence[ParsableManimColor] | None + opacity: float + reflectiveness: float + shadow: float + gloss: float + is_fixed_in_frame: bool + is_fixed_orientation: bool + depth_test: bool + name: str | None class OpenGLMobject: @@ -100,179 +143,88 @@ class OpenGLMobject: """ - shader_dtype = [ - ("point", np.float32, (3,)), - ] - shader_folder = "" - - # _Data and _Uniforms are set as class variables to tell manim how to handle setting/getting these attributes later. - points = _Data() - bounding_box = _Data() - rgbas = _Data() - - is_fixed_in_frame = _Uniforms() - is_fixed_orientation = _Uniforms() - fixed_orientation_center = _Uniforms() # for fixed orientation reference - gloss = _Uniforms() - shadow = _Uniforms() + dim: int = 3 + # WARNING: when changing a parameter here, be sure to update the + # TypedDict above so that autocomplete works for users def __init__( self, - color: ParsableManimColor | Iterable[ParsableManimColor] = WHITE, - opacity: float = 1, - dim: int = 3, # TODO, get rid of this - # Lighting parameters - # Positive gloss up to 1 makes it reflect the light. - gloss: float = 0.0, - # Positive shadow up to 1 makes a side opposite the light darker + color: ParsableManimColor | Sequence[ParsableManimColor] | None = WHITE, + opacity: float = 1.0, + reflectiveness: float = 0.0, shadow: float = 0.0, - # For shaders - render_primitive: int = moderngl.TRIANGLES, - texture_paths: dict[str, str] | None = None, - depth_test: bool = False, - # If true, the mobject will not get rotated according to camera position + gloss: float = 0.0, is_fixed_in_frame: bool = False, is_fixed_orientation: bool = False, - # Must match in attributes of vert shader - # Event listener - listen_to_events: bool = False, - model_matrix: MatrixMN | None = None, - should_render: bool = True, + depth_test: bool = True, name: str | None = None, - **kwargs, + **kwargs: Any, # just dump ): - self.name = self.__class__.__name__ if name is None else name - # getattr in case data/uniforms are already defined in parent classes. - self.data = getattr(self, "data", {}) - self.uniforms = getattr(self, "uniforms", {}) - + self.color = color self.opacity = opacity - self.dim = dim # TODO, get rid of this - # Lighting parameters - # Positive gloss up to 1 makes it reflect the light. - self.gloss = gloss - # Positive shadow up to 1 makes a side opposite the light darker + self.reflectiveness = reflectiveness self.shadow = shadow - # For shaders - self.render_primitive = render_primitive - self.texture_paths = texture_paths + self.gloss = gloss + self.is_fixed_in_frame = is_fixed_in_frame + self.is_fixed_orientation = is_fixed_orientation + self.fixed_orientation_center = (0.0, 0.0, 0.0) self.depth_test = depth_test - # If true, the mobject will not get rotated according to camera position - self.is_fixed_in_frame = float(is_fixed_in_frame) - self.is_fixed_orientation = float(is_fixed_orientation) - self.fixed_orientation_center = (0, 0, 0) - # Must match in attributes of vert shader - # Event listener - self.listen_to_events = listen_to_events - - self._submobjects = [] - self.parents = [] - self.parent = None - self.family = [self] - self.locked_data_keys = set() - self.needs_new_bounding_box = True - if model_matrix is None: - self.model_matrix = np.eye(4) - else: - self.model_matrix = model_matrix + self.name = self.__class__.__name__ if name is None else name + + # internal_state + self.points: npt.NDArray[ManimFloat] = np.zeros((0, 3)) + self.submobjects: list[OpenGLMobject] = [] + self.parents: list[OpenGLMobject] = [] + self.family: list[OpenGLMobject] = [self] + self.needs_new_bounding_box: bool = True + self._bounding_box: npt.NDArray[ManimFloat] = np.zeros((3, 3)) + self._is_animating: bool = False + self.saved_state: OpenGLMobject | None = None + self.target: OpenGLMobject | None = None + + # TODO replace with protocol + self.renderer_data: RendererData | None = None + + # currently does nothing + self.status = MobjectStatus() - self.init_data() self.init_updaters() - # self.init_event_listners() + self.init_event_listeners() self.init_points() self.color = ManimColor.parse(color) self.init_colors() - self.shader_indices = None - - if self.depth_test: - self.apply_depth_test() - - self.should_render = should_render - - def _assert_valid_submobjects(self, submobjects: Iterable[OpenGLMobject]) -> Self: - """Check that all submobjects are actually instances of - :class:`OpenGLMobject`, and that none of them is - ``self`` (an :class:`OpenGLMobject` cannot contain itself). - - This is an auxiliary function called when adding OpenGLMobjects to the - :attr:`submobjects` list. - - This function is intended to be overridden by subclasses such as - :class:`OpenGLVMobject`, which should assert that only other - OpenGLVMobjects may be added into it. - - Parameters - ---------- - submobjects - The list containing values to validate. - - Returns - ------- - :class:`OpenGLMobject` - The OpenGLMobject itself. - - Raises - ------ - TypeError - If any of the values in `submobjects` is not an - :class:`OpenGLMobject`. - ValueError - If there was an attempt to add an :class:`OpenGLMobject` as its own - submobject. - """ - return self._assert_valid_submobjects_internal(submobjects, OpenGLMobject) - - def _assert_valid_submobjects_internal( - self, submobjects: Iterable[OpenGLMobject], mob_class: type[OpenGLMobject] - ) -> Self: - for i, submob in enumerate(submobjects): - if not isinstance(submob, mob_class): - error_message = ( - f"Only values of type {mob_class.__name__} can be added " - f"as submobjects of {type(self).__name__}, but the value " - f"{submob} (at index {i}) is of type " - f"{type(submob).__name__}." - ) - # Intended for subclasses such as OpenGLVMobject, which - # cannot have regular OpenGLMobjects as submobjects - if isinstance(submob, OpenGLMobject): - error_message += ( - " You can try adding this value into a Group instead." - ) - raise TypeError(error_message) - if submob is self: - raise ValueError( - f"Cannot add {type(self).__name__} as a submobject of " - f"itself (at index {i})." - ) - return self - @classmethod - def __init_subclass__(cls, **kwargs) -> None: + def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) cls._original__init__ = cls.__init__ - def __str__(self) -> str: + def __str__(self): return self.__class__.__name__ - def __repr__(self) -> str: - return str(self.name) + def __repr__(self): + return str(self) - def __sub__(self, other): - return NotImplemented + def __add__(self, other: OpenGLMobject) -> Self: + if not isinstance(other, OpenGLMobject): + raise TypeError(f"Only Mobjects can be added to Mobjects not {type(other)}") + return self.get_group_class()(self, other) - def __isub__(self, other): - return NotImplemented + def __mul__(self, other: int) -> Self: + if not isinstance(other, int): + raise TypeError(f"Only int can be multiplied to Mobjects not {type(other)}") + return self.replicate(other) - def __add__(self, mobject): - return NotImplemented + @property + def bounding_box(self) -> npt.NDArray[np.float64]: + return self._bounding_box - def __iadd__(self, mobject): - return NotImplemented + @bounding_box.setter + def bounding_box(self, box: npt.NDArray[np.float64]): + self._bounding_box = box @classmethod - def set_default(cls, **kwargs) -> None: + def set_default(cls, **kwargs): """Sets the default values of keyword arguments. If this method is called without any additional keyword @@ -294,11 +246,13 @@ def set_default(cls, **kwargs) -> None: >>> from manim import Square, GREEN >>> Square.set_default(color=GREEN, fill_opacity=0.25) - >>> s = Square(); s.color, s.fill_opacity - (ManimColor('#83C167'), 0.25) + >>> s = Square() + >>> s.get_color().to_hex(with_alpha=True) + '#83C1673F' >>> Square.set_default() - >>> s = Square(); s.color, s.fill_opacity - (ManimColor('#FFFFFF'), 0.0) + >>> s = Square() + >>> s.get_color().to_hex(with_alpha=True) + '#FFFFFFFF' .. manim:: ChangedDefaultTextcolor :save_last_frame: @@ -319,15 +273,7 @@ def construct(self): else: cls.__init__ = cls._original__init__ - def init_data(self) -> None: - """Initializes the ``points``, ``bounding_box`` and ``rgbas`` attributes and groups them into self.data. - Subclasses can inherit and overwrite this method to extend `self.data`. - """ - self.points = np.zeros((0, 3)) - self.bounding_box = np.zeros((3, 3)) - self.rgbas = np.zeros((1, 4)) - - def init_colors(self) -> object: + def init_colors(self): """Initializes the colors. Gets called upon creation @@ -343,39 +289,6 @@ def init_points(self) -> object: # Typically implemented in subclass, unless purposefully left blank pass - def set(self, **kwargs) -> Self: - """Sets attributes. - - Mainly to be used along with :attr:`animate` to - animate setting attributes. - - Examples - -------- - :: - - >>> mob = OpenGLMobject() - >>> mob.set(foo=0) - OpenGLMobject - >>> mob.foo - 0 - - Parameters - ---------- - **kwargs - The attributes and corresponding values to set. - - Returns - ------- - :class:`OpenGLMobject` - ``self`` - - - """ - for attr, value in kwargs.items(): - setattr(self, attr, value) - - return self - def set_data(self, data: dict[str, Any]) -> Self: for key in data: self.data[key] = data[key].copy() @@ -386,8 +299,12 @@ def set_uniforms(self, uniforms: dict[str, Any]) -> Self: self.uniforms[key] = uniforms[key] # Copy? return self + # https://github.com/python/typing/issues/802 + # so we hack around it by doing | Self + # but this causes issues in Scene.play which only + # accepts _AnimationBuilder/Animations, not Mobjects @property - def animate(self) -> _AnimationBuilder | Self: + def animate(self) -> _AnimationBuilder[Self] | Self: """Used to animate the application of a method. .. warning:: @@ -474,8 +391,101 @@ def construct(self): """ return _AnimationBuilder(self) + def resize_points(self, new_length, resize_func=resize_array): + if new_length != len(self.points): + self.points = resize_func(self.points, new_length) + self.refresh_bounding_box() + return self + + def set_points(self, points: npt.NDArray[ManimFloat]) -> Self: + if len(points) == len(self.points): + self.points[:] = points + elif isinstance(points, np.ndarray): + self.points = points.copy() + else: + self.points = np.array(points) + self.refresh_bounding_box() + return self + + def append_points(self, new_points): + self.points = np.vstack([self.points, new_points]) + self.refresh_bounding_box() + return self + + def reverse_points(self, recursive=False): + self.points = self.points[::-1] + return self + + def apply_points_function( + self, + func: PointUpdateFunction, + about_point=None, + about_edge=ORIGIN, + works_on_bounding_box=False, + ) -> Self: + if about_point is None and about_edge is not None: + about_point = self.get_bounding_box_point(about_edge) + + for mob in self.get_family(): + arrs = [] + if mob.has_points(): + arrs.append(mob.points) + if works_on_bounding_box: + arrs.append(mob.get_bounding_box()) + + for arr in arrs: + if about_point is None: + arr[:] = func(arr) + else: + arr[:] = func(arr - about_point) + about_point + + if not works_on_bounding_box: + self.refresh_bounding_box(recurse_down=True) + else: + for parent in self.parents: + parent.refresh_bounding_box() + return self + + # ce only + def get_array_attrs(self): + """This method is used to determine which attributes of the :class:`~.OpenGLMobject` are arrays. + These can be used to apply functions to all of them at once. + """ + return ["points"] + + # ce only + def apply_over_attr_arrays(self, func): + """This method is used to apply a function to all attributes of the :class:`~.OpenGLMobject` that are arrays.""" + for attr in self.get_array_attrs(): + setattr(self, attr, func(getattr(self, attr))) + return self + + # ce only + def get_midpoint(self) -> np.ndarray: + """Get coordinates of the middle of the path that forms the :class:`~.OpenGLMobject`. + + Examples + -------- + + .. manim:: AngleMidPoint + :save_last_frame: + + class AngleMidPoint(Scene): + def construct(self): + line1 = Line(ORIGIN, 2*RIGHT) + line2 = Line(ORIGIN, 2*RIGHT).rotate_about_origin(80*DEGREES) + + a = Angle(line1, line2, radius=1.5, other_angle=False) + d = Dot(a.get_midpoint()).set_color(RED) + + self.add(line1, line2, a, d) + self.wait() + + """ + return self.point_from_proportion(0.5) + @property - def width(self) -> float: + def width(self): """The width of the mobject. Returns @@ -508,11 +518,11 @@ def construct(self): # Only these methods should directly affect points @width.setter - def width(self, value: float) -> None: + def width(self, value): self.rescale_to_fit(value, 0, stretch=False) @property - def height(self) -> float: + def height(self): """The height of the mobject. Returns @@ -544,11 +554,11 @@ def construct(self): return self.length_over_dim(1) @height.setter - def height(self, value: float) -> None: + def height(self, value): self.rescale_to_fit(value, 1, stretch=False) @property - def depth(self) -> float: + def depth(self): """The depth of the mobject. Returns @@ -564,97 +574,9 @@ def depth(self) -> float: return self.length_over_dim(2) @depth.setter - def depth(self, value: float) -> None: + def depth(self, value): self.rescale_to_fit(value, 2, stretch=False) - def resize_points(self, new_length, resize_func=resize_array): - if new_length != len(self.points): - self.points = resize_func(self.points, new_length) - self.refresh_bounding_box() - return self - - def set_points(self, points: Point3D_Array) -> Self: - if len(points) == len(self.points): - self.points[:] = points - elif isinstance(points, np.ndarray): - self.points = points.copy() - else: - self.points = np.array(points) - self.refresh_bounding_box() - return self - - def apply_over_attr_arrays( - self, func: Callable[[npt.NDArray[T]], npt.NDArray[T]] - ) -> Self: - # TODO: OpenGLMobject.get_array_attrs() doesn't even exist! - for attr in self.get_array_attrs(): - setattr(self, attr, func(getattr(self, attr))) - return self - - def append_points(self, new_points: Point3D_Array) -> Self: - self.points = np.vstack([self.points, new_points]) - self.refresh_bounding_box() - return self - - def reverse_points(self) -> Self: - for mob in self.get_family(): - for key in mob.data: - mob.data[key] = mob.data[key][::-1] - return self - - def get_midpoint(self) -> Point3D: - """Get coordinates of the middle of the path that forms the :class:`~.OpenGLMobject`. - - Examples - -------- - - .. manim:: AngleMidPoint - :save_last_frame: - - class AngleMidPoint(Scene): - def construct(self): - line1 = Line(ORIGIN, 2*RIGHT) - line2 = Line(ORIGIN, 2*RIGHT).rotate_about_origin(80*DEGREES) - - a = Angle(line1, line2, radius=1.5, other_angle=False) - d = Dot(a.get_midpoint()).set_color(RED) - - self.add(line1, line2, a, d) - self.wait() - - """ - return self.point_from_proportion(0.5) - - def apply_points_function( - self, - func: MappingFunction, - about_point: Point3D | None = None, - about_edge: Vector3D | None = ORIGIN, - works_on_bounding_box: bool = False, - ) -> Self: - if about_point is None and about_edge is not None: - about_point = self.get_bounding_box_point(about_edge) - - for mob in self.get_family(): - arrs = [] - if mob.has_points(): - arrs.append(mob.points) - if works_on_bounding_box: - arrs.append(mob.get_bounding_box()) - - for arr in arrs: - if about_point is None: - arr[:] = func(arr) - else: - arr[:] = func(arr - about_point) + about_point - - if not works_on_bounding_box: - self.refresh_bounding_box(recurse_down=True) - else: - for parent in self.parents: - parent.refresh_bounding_box() - return self - # Others related to points def match_points(self, mobject: OpenGLMobject) -> Self: @@ -677,29 +599,28 @@ def construct(self): self.set_points(mobject.points) return self - def clear_points(self) -> Self: + def clear_points(self): self.points = np.empty((0, 3)) - return self - def get_num_points(self) -> int: + def get_num_points(self): return len(self.points) - def get_all_points(self) -> Point3D_Array: + def get_all_points(self): if self.submobjects: return np.vstack([sm.points for sm in self.get_family()]) else: return self.points - def has_points(self) -> bool: + def has_points(self): return self.get_num_points() > 0 - def get_bounding_box(self) -> npt.NDArray[float]: + def get_bounding_box(self): if self.needs_new_bounding_box: self.bounding_box = self.compute_bounding_box() self.needs_new_bounding_box = False return self.bounding_box - def compute_bounding_box(self) -> npt.NDArray[float]: + def compute_bounding_box(self): all_points = np.vstack( [ self.points, @@ -719,9 +640,7 @@ def compute_bounding_box(self) -> npt.NDArray[float]: mids = (mins + maxs) / 2 return np.array([mins, mids, maxs]) - def refresh_bounding_box( - self, recurse_down: bool = False, recurse_up: bool = True - ) -> Self: + def refresh_bounding_box(self, recurse_down=False, recurse_up=True): for mob in self.get_family(recurse_down): mob.needs_new_bounding_box = True if recurse_up: @@ -729,40 +648,60 @@ def refresh_bounding_box( parent.refresh_bounding_box() return self - def is_point_touching(self, point: Point3D, buff: float = MED_SMALL_BUFF) -> bool: + def are_points_touching( + self, points: Point3D_Array, buff: float = 0 + ) -> npt.NDArray[bool]: bb = self.get_bounding_box() mins = bb[0] - buff maxs = bb[2] + buff - return (point >= mins).all() and (point <= maxs).all() + return ((points >= mins) * (points <= maxs)).all(1) + + def is_point_touching(self, point: Point3D, buff: float = MED_SMALL_BUFF) -> bool: + return self.are_points_touching(np.array(point, ndmin=2), buff)[0] + + def is_touching(self, mobject: OpenGLMobject, buff: float = 1e-2) -> bool: + bb1 = self.get_bounding_box() + bb2 = mobject.get_bounding_box() + return not any( + ( + ( + bb2[2] < bb1[0] - buff + ).any(), # E.g. Right of mobject is left of self's left + ( + bb2[0] > bb1[2] + buff + ).any(), # E.g. Left of mobject is right of self's right + ) + ) # Family matters - def __getitem__(self, value: int | slice) -> OpenGLMobject: + def __getitem__(self, value): if isinstance(value, slice): GroupClass = self.get_group_class() return GroupClass(*self.split().__getitem__(value)) return self.split().__getitem__(value) - def __iter__(self) -> Iterator[OpenGLMobject]: + def __iter__(self): return iter(self.split()) def __len__(self) -> int: return len(self.split()) - def split(self) -> Sequence[OpenGLMobject]: + def split(self) -> list[OpenGLMobject]: return self.submobjects - def assemble_family(self) -> Self: + def note_changed_family(self) -> Self: + """Updates bounding boxes and updater statuses.""" sub_families = (sm.get_family() for sm in self.submobjects) self.family = [self, *uniq_chain(*sub_families)] self.refresh_has_updater_status() self.refresh_bounding_box() for parent in self.parents: - parent.assemble_family() + parent.note_changed_family() return self def get_family(self, recurse: bool = True) -> Sequence[OpenGLMobject]: - if recurse and hasattr(self, "family"): + if recurse: return self.family else: return [self] @@ -770,7 +709,88 @@ def get_family(self, recurse: bool = True) -> Sequence[OpenGLMobject]: def family_members_with_points(self) -> Sequence[OpenGLMobject]: return [m for m in self.get_family() if m.has_points()] - def add(self, *mobjects: OpenGLMobject, update_parent: bool = False) -> Self: + def get_ancestors(self, extended: bool = False) -> Sequence[OpenGLMobject]: + """ + Returns parents, grandparents, etc. + Order of result should be from higher members of the hierarchy down. + + If extended is set to true, it includes the ancestors of all family members, + e.g. any other parents of a submobject + """ + ancestors = [] + to_process = list(self.get_family(recurse=extended)) + excluded = set(to_process) + while to_process: + for p in to_process.pop().parents: + if p not in excluded: + ancestors.append(p) + to_process.append(p) + # Ensure mobjects highest in the hierarchy show up first + ancestors.reverse() + # Remove list redundancies while preserving order + return list(dict.fromkeys(ancestors)) + + def _assert_valid_submobjects(self, submobjects: Iterable[OpenGLMobject]) -> Self: + """Check that all submobjects are actually instances of + :class:`Mobject`, and that none of them is ``self`` (a + :class:`Mobject` cannot contain itself). + + This is an auxiliary function called when adding Mobjects to the + :attr:`submobjects` list. + + This function is intended to be overridden by subclasses such as + :class:`VMobject`, which should assert that only other VMobjects + may be added into it. + + Parameters + ---------- + submobjects + The list containing values to validate. + + Returns + ------- + :class:`Mobject` + The Mobject itself. + + Raises + ------ + TypeError + If any of the values in `submobjects` is not a :class:`Mobject`. + ValueError + If there was an attempt to add a :class:`Mobject` as its own + submobject. + """ + return self._assert_valid_submobjects_internal(submobjects, OpenGLMobject) + + def _assert_valid_submobjects_internal( + self, submobjects: Iterable[OpenGLMobject], mob_class: type[OpenGLMobject] + ) -> Self: + for i, submob in enumerate(submobjects): + if not isinstance(submob, mob_class): + error_message = ( + f"Only values of type {mob_class.__name__} can be added " + f"as submobjects of {type(self).__name__}, but the value " + f"{submob} (at index {i}) is of type " + f"{type(submob).__name__}." + ) + # Intended for subclasses such as VMobject, which + # cannot have regular Mobjects as submobjects + if isinstance(submob, OpenGLMobject): + error_message += ( + " You can try adding this value into a Group instead." + ) + raise TypeError(error_message) + if submob is self: + raise ValueError( + f"Cannot add {type(self).__name__} as a submobject of " + f"itself (at index {i})." + ) + return self + + def add( + self, + *mobjects: OpenGLMobject, + ) -> Self: """Add mobjects as submobjects. The mobjects are added to :attr:`submobjects`. @@ -821,74 +841,24 @@ def add(self, *mobjects: OpenGLMobject, update_parent: bool = False) -> Self: >>> len(outer.submobjects) 1 - Only OpenGLMobjects can be added:: - - >>> outer.add(3) - Traceback (most recent call last): - ... - TypeError: Only values of type OpenGLMobject can be added as submobjects of OpenGLMobject, but the value 3 (at index 0) is of type int. - Adding an object to itself raises an error:: >>> outer.add(outer) Traceback (most recent call last): ... - ValueError: Cannot add OpenGLMobject as a submobject of itself (at index 0). + ValueError: OpenGLMobject cannot contain self """ - if update_parent: - assert len(mobjects) == 1, "Can't set multiple parents." - mobjects[0].parent = self - self._assert_valid_submobjects(mobjects) - - if any(mobjects.count(elem) > 1 for elem in mobjects): - logger.warning( - "Attempted adding some Mobject as a child more than once, " - "this is not possible. Repetitions are ignored.", - ) for mobject in mobjects: if mobject not in self.submobjects: self.submobjects.append(mobject) if self not in mobject.parents: mobject.parents.append(self) - self.assemble_family() + self.note_changed_family() return self - def insert( - self, index: int, mobject: OpenGLMobject, update_parent: bool = False - ) -> Self: - """Inserts a mobject at a specific position into self.submobjects - - Effectively just calls ``self.submobjects.insert(index, mobject)``, - where ``self.submobjects`` is a list. - - Highly adapted from ``OpenGLMobject.add``. - - Parameters - ---------- - index - The index at which - mobject - The mobject to be inserted. - update_parent - Whether or not to set ``mobject.parent`` to ``self``. - """ - if update_parent: - mobject.parent = self - - self._assert_valid_submobjects([mobject]) - - if mobject not in self.submobjects: - self.submobjects.insert(index, mobject) - - if self not in mobject.parents: - mobject.parents.append(self) - - self.assemble_family() - return self - - def remove(self, *mobjects: OpenGLMobject, update_parent: bool = False) -> Self: + def remove(self, *mobjects: OpenGLMobject, reassemble: bool = True) -> Self: """Remove :attr:`submobjects`. The mobjects are removed from :attr:`submobjects`, if they exist. @@ -910,16 +880,13 @@ def remove(self, *mobjects: OpenGLMobject, update_parent: bool = False) -> Self: :meth:`add` """ - if update_parent: - assert len(mobjects) == 1, "Can't remove multiple parents." - mobjects[0].parent = None - for mobject in mobjects: if mobject in self.submobjects: self.submobjects.remove(mobject) if self in mobject.parents: mobject.parents.remove(self) - self.assemble_family() + if reassemble: + self.note_changed_family() return self def add_to_back(self, *mobjects: OpenGLMobject) -> Self: @@ -967,24 +934,55 @@ def add_to_back(self, *mobjects: OpenGLMobject) -> Self: :meth:`add` """ - self._assert_valid_submobjects(mobjects) - self.submobjects = list_update(mobjects, self.submobjects) + self.set_submobjects(list_update(mobjects, self.submobjects)) return self - def replace_submobject(self, index: int, new_submob: OpenGLMobject) -> Self: - self._assert_valid_submobjects([new_submob]) + def replace_submobject(self, index, new_submob): old_submob = self.submobjects[index] if self in old_submob.parents: old_submob.parents.remove(self) self.submobjects[index] = new_submob - self.assemble_family() + new_submob.parents.append(self) + self.note_changed_family() + return self + + def insert_submobject(self, index: int, mobject: OpenGLMobject): + """Inserts a mobject at a specific position into self.submobjects + + Effectively just calls ``self.submobjects.insert(index, mobject)``, + where ``self.submobjects`` is a list. + + Highly adapted from ``OpenGLMobject.add``. + + Parameters + ---------- + index + The index at which + mobject + The mobject to be inserted. + update_parent + Whether or not to set ``mobject.parent`` to ``self``. + """ + if mobject is self: + raise ValueError("OpenGLMobject cannot contain self") + + if not isinstance(mobject, OpenGLMobject): + raise TypeError("All submobjects must be of type OpenGLMobject") + + if mobject not in self.submobjects: + self.submobjects.insert(index, mobject) + + self.note_changed_family() + return self + + def set_submobjects(self, submobject_list: list[OpenGLMobject]): + self.remove(*self.submobjects, reassemble=False) + self.add(*submobject_list) return self # Submobject organization - def arrange( - self, direction: Vector3D = RIGHT, center: bool = True, **kwargs - ) -> Self: + def arrange(self, direction=RIGHT, center=True, **kwargs): """Sorts :class:`~.OpenGLMobject` next to each other on screen. Examples @@ -1008,16 +1006,17 @@ def construct(self): self.center() return self - def arrange_in_grid( + # !TODO this differs a lot from 3b1b/manim + def arrange_in_grid_legacy( self, rows: int | None = None, cols: int | None = None, buff: float | tuple[float, float] = MED_SMALL_BUFF, - cell_alignment: Vector3D = ORIGIN, + cell_alignment: np.ndarray = ORIGIN, row_alignments: str | None = None, # "ucd" col_alignments: str | None = None, # "lcr" - row_heights: Sequence[float | None] | None = None, - col_widths: Sequence[float | None] | None = None, + row_heights: Iterable[float | None] | None = None, + col_widths: Iterable[float | None] | None = None, flow_order: str = "rd", **kwargs, ) -> Self: @@ -1116,27 +1115,16 @@ def construct(self): start_pos = self.get_center() # get cols / rows values if given (implicitly) - def init_size( - num: int | None, - alignments: str | None, - sizes: Sequence[float | None] | None, - name: str, - ) -> int: + def init_size(num, alignments, sizes): if num is not None: return num if alignments is not None: return len(alignments) if sizes is not None: return len(sizes) - raise ValueError( - f"At least one of the following parameters: '{name}s', " - f"'{name}_alignments' or " - f"'{name}_{'widths' if name == 'col' else 'heights'}', " - "must not be None" - ) - cols = init_size(cols, col_alignments, col_widths, "col") - rows = init_size(rows, row_alignments, row_heights, "row") + cols = init_size(cols, col_alignments, col_widths) + rows = init_size(rows, row_alignments, row_heights) # calculate rows cols if rows is None and cols is None: @@ -1146,7 +1134,7 @@ def init_size( # This is favored over rows>cols since in general # the sceene is wider than high. if rows is None: - rows = ceil(len(mobs) / cols) + rows = ceil(len(mobs) / cols) # type: ignore if cols is None: cols = ceil(len(mobs) / rows) if rows * cols < len(mobs): @@ -1160,19 +1148,16 @@ def init_size( buff_x = buff_y = buff # Initialize alignments correctly - def init_alignments( - str_alignments: str | None, - num: int, - mapping: dict[str, Vector3D], - name: str, - direction: Vector3D, - ) -> Sequence[Vector3D]: - if str_alignments is None: + def init_alignments(alignments, num, mapping, name, dir_): + if alignments is None: # Use cell_alignment as fallback - return [cell_alignment * direction] * num - if len(str_alignments) != num: + return [cell_alignment * dir_] * num + if len(alignments) != num: raise ValueError(f"{name}_alignments has a mismatching size.") - return [mapping[letter] for letter in str_alignments] + alignments = list(alignments) + for i in range(num): + alignments[i] = mapping[alignments[i]] + return alignments row_alignments = init_alignments( row_alignments, @@ -1204,16 +1189,15 @@ def init_alignments( raise ValueError( 'flow_order must be one of the following values: "dr", "rd", "ld" "dl", "ru", "ur", "lu", "ul".', ) - flow_order = mapper[flow_order] + flow_order = mapper[flow_order] # type: ignore # Reverse row_alignments and row_heights. Necessary since the # grid filling is handled bottom up for simplicity reasons. - def reverse(maybe_list: Sequence[Any] | None) -> Sequence[Any] | None: + def reverse(maybe_list): if maybe_list is not None: maybe_list = list(maybe_list) maybe_list.reverse() return maybe_list - return None row_alignments = reverse(row_alignments) row_heights = reverse(row_heights) @@ -1224,7 +1208,11 @@ def reverse(maybe_list: Sequence[Any] | None) -> Sequence[Any] | None: # properties of 0. mobs.extend([placeholder] * (rows * cols - len(mobs))) - grid = [[mobs[flow_order(r, c)] for c in range(cols)] for r in range(rows)] + + grid = [ + [mobs[flow_order(r, c)] for c in range(cols)] # type:ignore + for r in range(rows) + ] measured_heigths = [ max(grid[r][c].height for c in range(cols)) for r in range(rows) @@ -1234,12 +1222,7 @@ def reverse(maybe_list: Sequence[Any] | None) -> Sequence[Any] | None: ] # Initialize row_heights / col_widths correctly using measurements as fallback - def init_sizes( - sizes: Sequence[float | None] | None, - num: int, - measures: Sequence[float], - name: str, - ) -> Sequence[float]: + def init_sizes(sizes, num, measures, name): if sizes is None: sizes = [None] * num if len(sizes) != num: @@ -1256,7 +1239,7 @@ def init_sizes( x = 0 for c in range(cols): if grid[r][c] is not placeholder: - alignment = row_alignments[r] + col_alignments[c] + alignment = row_alignments[r] + col_alignments[c] # type:ignore line = Line( x * RIGHT + y * UP, (x + widths[c]) * RIGHT + (y + heights[r]) * UP, @@ -1269,39 +1252,90 @@ def init_sizes( x += widths[c] + buff_x y += heights[r] + buff_y - self.move_to(start_pos) + self.move_to(start_pos) + return self + + def arrange_in_grid( + self, + n_rows: int | None = None, + n_cols: int | None = None, + buff: float | None = None, + h_buff: float | None = None, + v_buff: float | None = None, + buff_ratio: float | None = None, + h_buff_ratio: float = 0.5, + v_buff_ratio: float = 0.5, + aligned_edge: np.ndarray = ORIGIN, + fill_rows_first: bool = True, + ): + submobs = self.submobjects + if n_rows is None and n_cols is None: + n_rows = int(np.sqrt(len(submobs))) + if n_rows is None and n_cols is not None: + n_rows = len(submobs) // n_cols + if n_cols is None and n_rows is not None: + n_cols = len(submobs) // n_rows + + if buff is not None: + h_buff = buff + v_buff = buff + else: + if buff_ratio is not None: + v_buff_ratio = buff_ratio + h_buff_ratio = buff_ratio + if h_buff is None: + h_buff = h_buff_ratio * self[0].get_width() + if v_buff is None: + v_buff = v_buff_ratio * self[0].get_height() + + x_unit = h_buff + max(sm.get_width() for sm in submobs) + y_unit = v_buff + max(sm.get_height() for sm in submobs) + + for index, sm in enumerate(submobs): + if fill_rows_first: + x, y = index % n_cols, index // n_cols # type: ignore + else: + x, y = index // n_rows, index % n_rows # type: ignore + sm.move_to(ORIGIN, aligned_edge) + sm.shift(x * x_unit * RIGHT + y * y_unit * DOWN) + self.center() + return self + + def arrange_to_fit_dim(self, length: float, dim: int, about_edge=ORIGIN): + ref_point = self.get_bounding_box_point(about_edge) + n_submobs = len(self.submobjects) + if n_submobs <= 1: + return + total_length = sum(sm.length_over_dim(dim) for sm in self.submobjects) + buff = (length - total_length) / (n_submobs - 1) + vect = np.zeros(self.dim) + vect[dim] = 1 + x = 0 + for submob in self.submobjects: + submob.set_coord(x, dim, -vect) + x += submob.length_over_dim(dim) + buff + self.move_to(ref_point, about_edge) return self - def get_grid( - self, n_rows: int, n_cols: int, height: float | None = None, **kwargs - ) -> OpenGLGroup: - """ - Returns a new mobject containing multiple copies of this one - arranged in a grid - """ - grid = self.duplicate(n_rows * n_cols) - grid.arrange_in_grid(n_rows, n_cols, **kwargs) - if height is not None: - grid.set_height(height) - return grid + def arrange_to_fit_width(self, width: float, about_edge=ORIGIN): + return self.arrange_to_fit_dim(width, 0, about_edge) - def duplicate(self, n: int) -> OpenGLGroup: - """Returns an :class:`~.OpenGLGroup` containing ``n`` copies of the mobject.""" - return self.get_group_class()(*[self.copy() for _ in range(n)]) + def arrange_to_fit_height(self, height: float, about_edge=ORIGIN): + return self.arrange_to_fit_dim(height, 1, about_edge) - def sort( - self, - point_to_num_func: Callable[[Point3D], float] = lambda p: p[0], - submob_func: Callable[[OpenGLMobject], Any] | None = None, - ) -> Self: + def arrange_to_fit_depth(self, depth: float, about_edge=ORIGIN): + return self.arrange_to_fit_dim(depth, 2, about_edge) + + def sort(self, point_to_num_func=lambda p: p[0], submob_func=None): """Sorts the list of :attr:`submobjects` by a function defined by ``submob_func``.""" if submob_func is not None: self.submobjects.sort(key=submob_func) else: self.submobjects.sort(key=lambda m: point_to_num_func(m.get_center())) + self.note_changed_family() return self - def shuffle(self, recurse: bool = False) -> Self: + def shuffle(self, recurse=False): """Shuffles the order of :attr:`submobjects` Examples @@ -1321,11 +1355,11 @@ def construct(self): for submob in self.submobjects: submob.shuffle(recurse=True) random.shuffle(self.submobjects) - self.assemble_family() + self.note_changed_family() return self - def invert(self, recursive: bool = False) -> Self: - """Inverts the list of :attr:`submobjects`. + def reverse_submobjects(self, recursive=False): + """Reverses the list of :attr:`submobjects`. Parameters ---------- @@ -1335,26 +1369,42 @@ def invert(self, recursive: bool = False) -> Self: Examples -------- - .. manim:: InvertSumobjectsExample + .. manim:: ReverseSumobjectsExample - class InvertSumobjectsExample(Scene): + class ReverseSumobjectsExample(Scene): def construct(self): s = VGroup(*[Dot().shift(i*0.1*RIGHT) for i in range(-20,20)]) s2 = s.copy() - s2.invert() + s2.reverse_submobjects() s2.shift(DOWN) self.play(Write(s), Write(s2)) """ + self.submobjects.reverse() if recursive: for submob in self.submobjects: - submob.invert(recursive=True) - self.submobjects.reverse() - self.assemble_family() + submob.reverse_submobjects(recursive=True) + self.note_changed_family() return self # Copying - def copy(self, shallow: bool = False) -> OpenGLMobject: + # TODO: don't use self + @stash_mobject_pointers + def serialize(self) -> bytes: + return pickle.dumps(self) + + def deserialize(self, data: bytes) -> Self: + self.become(pickle.loads(data)) + return self + + def deepcopy(self) -> Self: + try: + return pickle.loads(pickle.dumps(self)) + except AttributeError: + return copy.deepcopy(self) + + @stash_mobject_pointers + def copy(self, deep: bool = False) -> Self: """Create and return an identical copy of the :class:`OpenGLMobject` including all :attr:`submobjects`. @@ -1372,109 +1422,164 @@ def copy(self, shallow: bool = False) -> OpenGLMobject: ---- The clone is initially not visible in the Scene, even if the original was. """ - if not shallow: + if deep: return self.deepcopy() - # TODO, either justify reason for shallow copy, or - # remove this redundancy everywhere - # return self.deepcopy() + result = copy.copy(self) - parents = self.parents - self.parents = [] - copy_mobject = copy.copy(self) - self.parents = parents + result.parents = [] + result.target = None + result.saved_state = None - copy_mobject.data = dict(self.data) - for key in self.data: - copy_mobject.data[key] = self.data[key].copy() + result.points = np.array(self.points) + # + # Instead of adding using result.add, which does some checks for updating + # updater statues and bounding box, just directly modify the family-related + # lists + result.submobjects = [sm.copy() for sm in self.submobjects] + for sm in result.submobjects: + sm.parents = [result] - # TODO, are uniforms ever numpy arrays? - copy_mobject.uniforms = dict(self.uniforms) + result.note_changed_family() - copy_mobject.submobjects = [] - copy_mobject.add(*(sm.copy() for sm in self.submobjects)) - copy_mobject.match_updaters(self) + # this seems correct, but is not needed in 3b1b manim - investigate + # for current, copy_ in zip(self.get_family(), result.get_family()): + # copy_.points = np.array(current.points) + # copy_.match_color(current) - copy_mobject.needs_new_bounding_box = self.needs_new_bounding_box + # Similarly, instead of calling match_updaters, since we know the status + # won't have changed, just directly match with shallow copies. + result.non_time_updaters = self.non_time_updaters.copy() + result.time_based_updaters = self.time_based_updaters.copy() - # Make sure any mobject or numpy array attributes are copied family = self.get_family() - for attr, value in list(self.__dict__.items()): + for attr, value in self.__dict__.items(): if ( isinstance(value, OpenGLMobject) - and value in family and value is not self + and value in family ): - setattr(copy_mobject, attr, value.copy()) + setattr(result, attr, result.family[self.family.index(value)]) if isinstance(value, np.ndarray): - setattr(copy_mobject, attr, value.copy()) - # if isinstance(value, ShaderWrapper): - # setattr(copy_mobject, attr, value.copy()) - return copy_mobject - - def deepcopy(self) -> OpenGLMobject: - parents = self.parents - self.parents = [] - result = copy.deepcopy(self) - self.parents = parents + setattr(result, attr, value.copy()) return result - def generate_target(self, use_deepcopy: bool = False) -> OpenGLMobject: - self.target = None # Prevent exponential explosion - if use_deepcopy: - self.target = self.deepcopy() - else: - self.target = self.copy() + def generate_target(self, use_deepcopy: bool = False): + target: OpenGLMobject = self.copy(deep=use_deepcopy) + target.saved_state = self.saved_state + self.target = target return self.target - def save_state(self, use_deepcopy: bool = False) -> Self: + def save_state(self, use_deepcopy: bool = False): """Save the current state (position, color & size). Can be restored with :meth:`~.OpenGLMobject.restore`.""" - if hasattr(self, "saved_state"): - # Prevent exponential growth of data - self.saved_state = None - if use_deepcopy: - self.saved_state = self.deepcopy() - else: - self.saved_state = self.copy() + saved_state: OpenGLMobject = self.copy(deep=use_deepcopy) + saved_state.target = self.target + self.saved_state = saved_state return self - def restore(self) -> Self: + def restore(self): """Restores the state that was previously saved with :meth:`~.OpenGLMobject.save_state`.""" - if not hasattr(self, "saved_state") or self.save_state is None: + if self.saved_state is None: raise Exception("Trying to restore without having saved") self.become(self.saved_state) return self + def save_to_file(self, file_path: str): + with open(file_path, "wb") as fp: + fp.write(self.serialize()) + logger.info(f"Saved mobject to {file_path}") + return self + + @staticmethod + def load(file_path): + if not os.path.exists(file_path): + logger.error(f"No file found at {file_path}") + sys.exit(2) + with open(file_path, "rb") as fp: + mobject = pickle.load(fp) + return mobject + + # Creating new Mobjects from this one + + def replicate(self, n: int) -> OpenGLGroup: + """Returns an :class:`~.OpenGLVGroup` containing ``n`` copies of the mobject.""" + group_class = self.get_group_class() + return group_class(*(self.copy() for _ in range(n))) + + def get_grid_legacy(self, n_rows, n_cols, height=None, **kwargs): + """ + Returns a new mobject containing multiple copies of this one + arranged in a grid + """ + grid = self.duplicate(n_rows * n_cols) + grid.arrange_in_grid(n_rows, n_cols, **kwargs) + if height is not None: + grid.set_height(height) + return grid + + def get_grid( + self, + n_rows: int, + n_cols: int, + height: float | None = None, + width: float | None = None, + group_by_rows: bool = False, + group_by_cols: bool = False, + **kwargs, + ) -> OpenGLGroup: + """ + Returns a new mobject containing multiple copies of this one + arranged in a grid + """ + total = n_rows * n_cols + grid = self.replicate(total) + if group_by_cols: + kwargs["fill_rows_first"] = False + grid.arrange_in_grid(n_rows, n_cols, **kwargs) + if height is not None: + grid.set_height(height) + if width is not None: + grid.set_height(width) + + group_class = self.get_group_class() + if group_by_rows: + return group_class(*(grid[n : n + n_cols] for n in range(0, total, n_cols))) + elif group_by_cols: + return group_class(*(grid[n : n + n_rows] for n in range(0, total, n_rows))) + else: + return grid + # Updating def init_updaters(self) -> None: - self.time_based_updaters = [] - self.non_time_updaters = [] - self.has_updaters = False - self.updating_suspended = False + self.time_based_updaters: list[TimeBasedUpdater] = [] + self.non_time_updaters: list[NonTimeUpdater] = [] + # so that we don't have to refind updaters + self.has_updaters: bool = False + self.updating_suspended: bool = False def update(self, dt: float = 0, recurse: bool = True) -> Self: if not self.has_updaters or self.updating_suspended: return self - for updater in self.time_based_updaters: - updater(self, dt) - for updater in self.non_time_updaters: - updater(self) + for time_updater in self.time_based_updaters: + time_updater(self, dt) + for non_time_updater in self.non_time_updaters: + non_time_updater(self) if recurse: for submob in self.submobjects: submob.update(dt, recurse) return self - def get_time_based_updaters(self) -> Sequence[TimeBasedUpdater]: + def get_time_based_updaters(self) -> list[TimeBasedUpdater]: return self.time_based_updaters def has_time_based_updater(self) -> bool: return len(self.time_based_updaters) > 0 - def get_updaters(self) -> Sequence[Updater]: + def get_updaters(self) -> list[Updater]: return self.time_based_updaters + self.non_time_updaters - def get_family_updaters(self) -> Sequence[Updater]: + def get_family_updaters(self) -> list[Updater]: return list(it.chain(*(sm.get_updaters() for sm in self.get_family()))) def add_updater( @@ -1484,9 +1589,9 @@ def add_updater( call_updater: bool = False, ) -> Self: if "dt" in inspect.signature(update_function).parameters: - updater_list = self.time_based_updaters + updater_list: list[Updater] = self.time_based_updaters # type: ignore else: - updater_list = self.non_time_updaters + updater_list: list[Updater] = self.non_time_updaters # type: ignore if index is None: updater_list.append(update_function) @@ -1499,7 +1604,11 @@ def add_updater( return self def remove_updater(self, update_function: Updater) -> Self: - for updater_list in [self.time_based_updaters, self.non_time_updaters]: + updater_lists: list[list[Updater]] = [ + self.time_based_updaters, # type: ignore + self.non_time_updaters, # type: ignore + ] + for updater_list in updater_lists: while update_function in updater_list: updater_list.remove(update_function) self.refresh_has_updater_status() @@ -1542,9 +1651,19 @@ def refresh_has_updater_status(self) -> Self: self.has_updaters = any(mob.get_updaters() for mob in self.get_family()) return self + # Check if mark as static or not for camera + + def is_changing(self) -> bool: + return self.has_updaters or self._is_animating + + def set_animating_status(self, is_animating: bool, recurse: bool = True) -> Self: + for mob in (*self.get_family(recurse), *self.get_ancestors(extended=True)): + mob._is_animating = is_animating + return self + # Transforming operations - def shift(self, vector: Vector3D) -> Self: + def shift(self, vector) -> Self: self.apply_points_function( lambda points: points + vector, about_edge=None, @@ -1554,9 +1673,10 @@ def shift(self, vector: Vector3D) -> Self: def scale( self, - scale_factor: float, - about_point: Sequence[float] | None = None, - about_edge: Sequence[float] = ORIGIN, + scale_factor: float | np.ndarray, + min_scale_factor: float = 1e-8, + about_point: Sequence[float] | np.ndarray | None = None, + about_edge: Sequence[float] | np.ndarray = ORIGIN, **kwargs, ) -> Self: r"""Scale the size by a factor. @@ -1575,6 +1695,10 @@ def scale( The scaling factor :math:`\alpha`. If :math:`0 < |\alpha| < 1`, the mobject will shrink, and for :math:`|\alpha| > 1` it will grow. Furthermore, if :math:`\alpha < 0`, the mobject is also flipped. + + min_scale_factor + The minimum scaling factor that is used such that the mobject is not scaled to zero. + kwargs Additional keyword arguments passed to :meth:`apply_points_function_about_point`. @@ -1605,15 +1729,32 @@ def construct(self): :meth:`move_to` """ + if isinstance(scale_factor, numbers.Number): + scale_factor = max(scale_factor, min_scale_factor) + else: + scale_factor = np.array(scale_factor).clip(min=min_scale_factor) # type: ignore self.apply_points_function( lambda points: scale_factor * points, about_point=about_point, about_edge=about_edge, works_on_bounding_box=True, - **kwargs, ) + for mob in self.get_family(): + mob._handle_scale_side_effects(scale_factor) return self + def _handle_scale_side_effects(self, scale_factor: float | np.ndarray) -> None: + r"""In case subclasses, such as DecimalNumber, need to make + any other changes when the size gets altered by scaling. + This method can be overridden in subclasses. + + Parameters + ---------- + scale_factor + The scaling factor :math:`\alpha`. If :math:`0 < |\alpha| < 1` + """ + pass + def stretch(self, factor: float, dim: int, **kwargs) -> Self: def func(points): points[:, dim] *= factor @@ -1622,13 +1763,13 @@ def func(points): self.apply_points_function(func, works_on_bounding_box=True, **kwargs) return self - def rotate_about_origin(self, angle: float, axis: Vector3D = OUT) -> Self: - return self.rotate(angle, axis, about_point=ORIGIN) + def rotate_about_origin(self, angle: float, axis=OUT) -> Self: + return self.rotate(angle, axis, about_point=ORIGIN) # type: ignore def rotate( self, angle: float, - axis: Vector3D = OUT, + axis=OUT, about_point: Sequence[float] | None = None, **kwargs, ) -> Self: @@ -1641,7 +1782,7 @@ def rotate( ) return self - def flip(self, axis: Vector3D = UP, **kwargs) -> Self: + def flip(self, axis=UP, **kwargs) -> Self: """Flips/Mirrors an mobject about its center. Examples @@ -1660,25 +1801,27 @@ def construct(self): """ return self.rotate(TAU / 2, axis, **kwargs) - def apply_function(self, function: MappingFunction, **kwargs) -> Self: + def apply_function(self, function: PointUpdateFunction, **kwargs) -> Self: # Default to applying matrix about the origin, not mobjects center - if len(kwargs) == 0: + if not kwargs: kwargs["about_point"] = ORIGIN self.apply_points_function( lambda points: np.array([function(p) for p in points]), **kwargs ) return self - def apply_function_to_position(self, function: MappingFunction) -> Self: + def apply_function_to_position(self, function: PointUpdateFunction) -> Self: self.move_to(function(self.get_center())) return self - def apply_function_to_submobject_positions(self, function: MappingFunction) -> Self: + def apply_function_to_submobject_positions( + self, function: PointUpdateFunction + ) -> Self: for submob in self.submobjects: submob.apply_function_to_position(function) return self - def apply_matrix(self, matrix: MatrixMN, **kwargs) -> Self: + def apply_matrix(self, matrix, **kwargs) -> Self: # Default to applying matrix about the origin, not mobjects center if ("about_point" not in kwargs) and ("about_edge" not in kwargs): kwargs["about_point"] = ORIGIN @@ -1690,9 +1833,7 @@ def apply_matrix(self, matrix: MatrixMN, **kwargs) -> Self: ) return self - def apply_complex_function( - self, function: Callable[[complex], complex], **kwargs - ) -> Self: + def apply_complex_function(self, function, **kwargs) -> Self: """Applies a complex function to a :class:`OpenGLMobject`. The x and y coordinates correspond to the real and imaginary parts respectively. @@ -1726,7 +1867,7 @@ def R3_func(point): return self.apply_function(R3_func) - def hierarchical_model_matrix(self) -> MatrixMN: + def hierarchical_model_matrix(self): if self.parent is None: return self.model_matrix @@ -1737,12 +1878,7 @@ def hierarchical_model_matrix(self) -> MatrixMN: current_object = current_object.parent return np.linalg.multi_dot(list(reversed(model_matrices))) - def wag( - self, - direction: Vector3D = RIGHT, - axis: Vector3D = DOWN, - wag_factor: float = 1.0, - ) -> Self: + def wag(self, direction=RIGHT, axis=DOWN, wag_factor=1.0) -> Self: for mob in self.family_members_with_points(): alphas = np.dot(mob.points, np.transpose(axis)) alphas -= min(alphas) @@ -1764,18 +1900,14 @@ def center(self) -> Self: self.shift(-self.get_center()) return self - def align_on_border( - self, - direction: Vector3D, - buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER, - ) -> Self: + def align_on_border(self, direction, buff=DEFAULT_MOBJECT_TO_EDGE_BUFFER) -> Self: """ Direction just needs to be a vector pointing towards side or corner in the 2d plane. """ target_point = np.sign(direction) * ( - config["frame_x_radius"], - config["frame_y_radius"], + config.frame_x_radius, + config.frame_y_radius, 0, ) point_to_align = self.get_bounding_box_point(direction) @@ -1785,28 +1917,22 @@ def align_on_border( return self def to_corner( - self, - corner: Vector3D = LEFT + DOWN, - buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER, + self, corner=LEFT + DOWN, buff=DEFAULT_MOBJECT_TO_EDGE_BUFFER ) -> Self: return self.align_on_border(corner, buff) - def to_edge( - self, - edge: Vector3D = LEFT, - buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER, - ) -> Self: + def to_edge(self, edge=LEFT, buff=DEFAULT_MOBJECT_TO_EDGE_BUFFER) -> Self: return self.align_on_border(edge, buff) def next_to( self, - mobject_or_point: OpenGLMobject | Point3D, - direction: Vector3D = RIGHT, - buff: float = DEFAULT_MOBJECT_TO_MOBJECT_BUFFER, - aligned_edge: Vector3D = ORIGIN, - submobject_to_align: OpenGLMobject | None = None, - index_of_submobject_to_align: int | None = None, - coor_mask: Point3D = np.array([1, 1, 1]), + mobject_or_point, + direction=RIGHT, + buff=DEFAULT_MOBJECT_TO_MOBJECT_BUFFER, + aligned_edge=ORIGIN, + submobject_to_align=None, + index_of_submobject_to_align=None, + coor_mask=np.array([1, 1, 1]), ) -> Self: """Move this :class:`~.OpenGLMobject` next to another's :class:`~.OpenGLMobject` or coordinate. @@ -1849,8 +1975,8 @@ def construct(self): self.shift((target_point - point_to_align + buff * direction) * coor_mask) return self - def shift_onto_screen(self, **kwargs) -> Self: - space_lengths = [config["frame_x_radius"], config["frame_y_radius"]] + def shift_onto_screen(self, **kwargs): + space_lengths = [config.frame_x_radius, config.frame_y_radius] for vect in UP, DOWN, LEFT, RIGHT: dim = np.argmax(np.abs(vect)) buff = kwargs.get("buff", DEFAULT_MOBJECT_TO_EDGE_BUFFER) @@ -1860,7 +1986,7 @@ def shift_onto_screen(self, **kwargs) -> Self: self.to_edge(vect, **kwargs) return self - def is_off_screen(self) -> bool: + def is_off_screen(self): if self.get_left()[0] > config.frame_x_radius: return True if self.get_right()[0] < config.frame_x_radius: @@ -1869,12 +1995,10 @@ def is_off_screen(self) -> bool: return True return self.get_top()[1] < -config.frame_y_radius - def stretch_about_point(self, factor: float, dim: int, point: Point3D) -> Self: + def stretch_about_point(self, factor, dim, point): return self.stretch(factor, dim, about_point=point) - def rescale_to_fit( - self, length: float, dim: int, stretch: bool = False, **kwargs - ) -> Self: + def rescale_to_fit(self, length, dim, stretch=False, **kwargs): old_length = self.length_over_dim(dim) if old_length == 0: return self @@ -1884,7 +2008,7 @@ def rescale_to_fit( self.scale(length / old_length, **kwargs) return self - def stretch_to_fit_width(self, width: float, **kwargs) -> Self: + def stretch_to_fit_width(self, width, **kwargs): """Stretches the :class:`~.OpenGLMobject` to fit a width, not keeping height/depth proportional. Returns @@ -1909,15 +2033,15 @@ def stretch_to_fit_width(self, width: float, **kwargs) -> Self: """ return self.rescale_to_fit(width, 0, stretch=True, **kwargs) - def stretch_to_fit_height(self, height: float, **kwargs) -> Self: + def stretch_to_fit_height(self, height, **kwargs): """Stretches the :class:`~.OpenGLMobject` to fit a height, not keeping width/height proportional.""" return self.rescale_to_fit(height, 1, stretch=True, **kwargs) - def stretch_to_fit_depth(self, depth: float, **kwargs) -> Self: + def stretch_to_fit_depth(self, depth, **kwargs): """Stretches the :class:`~.OpenGLMobject` to fit a depth, not keeping width/height proportional.""" return self.rescale_to_fit(depth, 1, stretch=True, **kwargs) - def set_width(self, width: float, stretch: bool = False, **kwargs) -> Self: + def set_width(self, width, stretch=False, **kwargs): """Scales the :class:`~.OpenGLMobject` to fit a width while keeping height/depth proportional. Returns @@ -1944,38 +2068,68 @@ def set_width(self, width: float, stretch: bool = False, **kwargs) -> Self: scale_to_fit_width = set_width - def set_height(self, height: float, stretch: bool = False, **kwargs) -> Self: + def set_height(self, height, stretch=False, **kwargs): """Scales the :class:`~.OpenGLMobject` to fit a height while keeping width/depth proportional.""" return self.rescale_to_fit(height, 1, stretch=stretch, **kwargs) scale_to_fit_height = set_height - def set_depth(self, depth: float, stretch: bool = False, **kwargs): + def set_depth(self, depth, stretch=False, **kwargs): """Scales the :class:`~.OpenGLMobject` to fit a depth while keeping width/height proportional.""" return self.rescale_to_fit(depth, 2, stretch=stretch, **kwargs) scale_to_fit_depth = set_depth - def set_coord(self, value: float, dim: int, direction: Vector3D = ORIGIN) -> Self: + def set_max_width(self, max_width: float, **kwargs): + if self.get_width() > max_width: + self.set_width(max_width, **kwargs) + return self + + def set_max_height(self, max_height: float, **kwargs): + if self.get_height() > max_height: + self.set_height(max_height, **kwargs) + return self + + def set_max_depth(self, max_depth: float, **kwargs): + if self.get_depth() > max_depth: + self.set_depth(max_depth, **kwargs) + return self + + def set_min_width(self, min_width: float, **kwargs): + if self.get_width() < min_width: + self.set_width(min_width, **kwargs) + return self + + def set_min_height(self, min_height: float, **kwargs): + if self.get_height() < min_height: + self.set_height(min_height, **kwargs) + return self + + def set_min_depth(self, min_depth: float, **kwargs): + if self.get_depth() < min_depth: + self.set_depth(min_depth, **kwargs) + return self + + def set_coord(self, value, dim, direction=ORIGIN): curr = self.get_coord(dim, direction) shift_vect = np.zeros(self.dim) shift_vect[dim] = value - curr self.shift(shift_vect) return self - def set_x(self, x: float, direction: Vector3D = ORIGIN) -> Self: + def set_x(self, x, direction=ORIGIN): """Set x value of the center of the :class:`~.OpenGLMobject` (``int`` or ``float``)""" return self.set_coord(x, 0, direction) - def set_y(self, y: float, direction: Vector3D = ORIGIN) -> Self: + def set_y(self, y, direction=ORIGIN): """Set y value of the center of the :class:`~.OpenGLMobject` (``int`` or ``float``)""" return self.set_coord(y, 1, direction) - def set_z(self, z: float, direction: Vector3D = ORIGIN) -> Self: + def set_z(self, z, direction=ORIGIN): """Set z value of the center of the :class:`~.OpenGLMobject` (``int`` or ``float``)""" return self.set_coord(z, 2, direction) - def space_out_submobjects(self, factor: float = 1.5, **kwargs) -> Self: + def space_out_submobjects(self, factor=1.5, **kwargs): self.scale(factor, **kwargs) for submob in self.submobjects: submob.scale(1.0 / factor) @@ -1983,10 +2137,10 @@ def space_out_submobjects(self, factor: float = 1.5, **kwargs) -> Self: def move_to( self, - point_or_mobject: Point3D | OpenGLMobject, - aligned_edge: Vector3D = ORIGIN, - coor_mask: Point3D = np.array([1, 1, 1]), - ) -> Self: + point_or_mobject, + aligned_edge=ORIGIN, + coor_mask=np.array((1, 1, 1)), + ): """Move center of the :class:`~.OpenGLMobject` to certain coordinate.""" if isinstance(point_or_mobject, OpenGLMobject): target = point_or_mobject.get_bounding_box_point(aligned_edge) @@ -1996,12 +2150,7 @@ def move_to( self.shift((target - point_to_align) * coor_mask) return self - def replace( - self, - mobject: OpenGLMobject, - dim_to_match: int = 0, - stretch: bool = False, - ) -> Self: + def replace(self, mobject, dim_to_match=0, stretch=False): if not mobject.get_num_points() and not mobject.submobjects: self.scale(0) return self @@ -2023,18 +2172,19 @@ def surround( dim_to_match: int = 0, stretch: bool = False, buff: float = MED_SMALL_BUFF, - ) -> Self: + ): self.replace(mobject, dim_to_match, stretch) length = mobject.length_over_dim(dim_to_match) self.scale((length + buff) / length) return self - def put_start_and_end_on(self, start: Point3D, end: Point3D) -> Self: + # ! TODO: Check implementation of 3b1b for this method + def put_start_and_end_on_legacy(self, start, end): curr_start, curr_end = self.get_start_and_end() curr_vect = curr_end - curr_start if np.all(curr_vect == 0): raise Exception("Cannot position endpoints of closed loop") - target_vect = np.array(end) - np.array(start) + target_vect = np.asarray(end) - np.asarray(start) axis = ( normalize(np.cross(curr_vect, target_vect)) if np.linalg.norm(np.cross(curr_vect, target_vect)) != 0 @@ -2052,103 +2202,59 @@ def put_start_and_end_on(self, start: Point3D, end: Point3D) -> Self: self.shift(start - curr_start) return self - # Color functions - - def set_rgba_array( - self, - color: ParsableManimColor | Iterable[ParsableManimColor] | None = None, - opacity: float | Iterable[float] | None = None, - name: str = "rgbas", - recurse: bool = True, - ) -> Self: - if color is not None: - rgbs = np.array([color_to_rgb(c) for c in listify(color)]) - if opacity is not None: - opacities = listify(opacity) - - # Color only - if color is not None and opacity is None: - for mob in self.get_family(recurse): - mob.data[name] = resize_array( - mob.data[name] if name in mob.data else np.empty((1, 3)), len(rgbs) - ) - mob.data[name][:, :3] = rgbs - - # Opacity only - if color is None and opacity is not None: - for mob in self.get_family(recurse): - mob.data[name] = resize_array( - mob.data[name] if name in mob.data else np.empty((1, 3)), - len(opacities), - ) - mob.data[name][:, 3] = opacities - - # Color and opacity - if color is not None and opacity is not None: - rgbas = np.array([[*rgb, o] for rgb, o in zip(*make_even(rgbs, opacities))]) - for mob in self.get_family(recurse): - mob.data[name] = rgbas.copy() + def put_start_and_end_on(self, start: np.ndarray, end: np.ndarray): + curr_start, curr_end = self.get_start_and_end() + curr_vect = curr_end - curr_start + if np.all(curr_vect == 0): + raise Exception("Cannot position endpoints of closed loop") + target_vect = end - start + self.scale( + get_norm(target_vect) / get_norm(curr_vect), + about_point=curr_start, + ) + self.rotate( + angle_of_vector(target_vect) - angle_of_vector(curr_vect), + ) + self.rotate( + np.arctan2(curr_vect[2], get_norm(curr_vect[:2])) + - np.arctan2(target_vect[2], get_norm(target_vect[:2])), + axis=np.array([-target_vect[1], target_vect[0], 0]), + ) + self.shift(start - self.get_start()) return self - def set_rgba_array_direct( - self, - rgbas: npt.NDArray[RGBA_Array_Float], - name: str = "rgbas", - recurse: bool = True, - ) -> Self: - """Directly set rgba data from `rgbas` and optionally do the same recursively - with submobjects. This can be used if the `rgbas` have already been generated - with the correct shape and simply need to be set. - - Parameters - ---------- - rgbas - the rgba to be set as data - name - the name of the data attribute to be set - recurse - set to true to recursively apply this method to submobjects - """ - for mob in self.get_family(recurse): - mob.data[name] = rgbas.copy() + # Color functions - def set_color( - self, - color: ParsableManimColor | Iterable[ParsableManimColor] | None, - opacity: float | Iterable[float] | None = None, - recurse: bool = True, - ) -> Self: - self.set_rgba_array(color, opacity, recurse=False) + def set_color(self, color: ParsableManimColor | None, opacity=None, recurse=True): # Recurse to submobjects differently from how set_rgba_array # in case they implement set_color differently if color is not None: self.color: ManimColor = ManimColor.parse(color) if opacity is not None: - self.opacity = opacity + self.color.opacity(opacity) if recurse: for submob in self.submobjects: submob.set_color(color, recurse=True) return self - def set_opacity( - self, opacity: float | Iterable[float] | None, recurse: bool = True - ) -> Self: - self.set_rgba_array(color=None, opacity=opacity, recurse=False) + def set_opacity(self, opacity, recurse=True): + # self.set_rgba_array(color=None, opacity=opacity, recurse=False) if recurse: for submob in self.submobjects: submob.set_opacity(opacity, recurse=True) return self - def get_color(self) -> str: - return rgb_to_hex(self.rgbas[0, :3]) + def get_color(self) -> ManimColor: + return self.color - def get_opacity(self) -> float: - return self.rgbas[0, 3] + def get_opacity(self): + return self.color.opacity() - def set_color_by_gradient(self, *colors: ParsableManimColor) -> Self: - return self.set_submobject_colors_by_gradient(*colors) + def set_color_by_gradient(self, *colors: ParsableManimColor): + self.set_submobject_colors_by_gradient(*colors) + return self - def set_submobject_colors_by_gradient(self, *colors: ParsableManimColor) -> Self: + def set_submobject_colors_by_gradient(self, *colors): if len(colors) == 0: raise Exception("Need at least one color") elif len(colors) == 1: @@ -2162,33 +2268,14 @@ def set_submobject_colors_by_gradient(self, *colors: ParsableManimColor) -> Self mob.set_color(color) return self - def fade(self, darkness: float = 0.5, recurse: bool = True) -> Self: - return self.set_opacity(1.0 - darkness, recurse=recurse) - - def get_gloss(self) -> float: - return self.gloss - - def set_gloss(self, gloss: float, recurse: bool = True) -> Self: - for mob in self.get_family(recurse): - mob.gloss = gloss - return self - - def get_shadow(self) -> float: - return self.shadow - - def set_shadow(self, shadow: float, recurse: bool = True) -> Self: - for mob in self.get_family(recurse): - mob.shadow = shadow - return self + def fade(self, darkness=0.5, recurse=True): + self.set_opacity(1.0 - darkness, recurse=recurse) # Background rectangle def add_background_rectangle( - self, - color: ParsableManimColor | None = None, - opacity: float = 0.75, - **kwargs, - ) -> Self: + self, color: ParsableManimColor | None = None, opacity: float = 0.75, **kwargs + ): # TODO, this does not behave well when the mobject has points, # since it gets displayed on top """Add a BackgroundRectangle as submobject. @@ -2223,42 +2310,51 @@ def add_background_rectangle( self.background_rectangle = BackgroundRectangle( self, color=color, fill_opacity=opacity, **kwargs ) - self.add_to_back(self.background_rectangle) + self.add_to_back(self.background_rectangle) # type: ignore return self - def add_background_rectangle_to_submobjects(self, **kwargs) -> Self: + def add_background_rectangle_to_submobjects(self, **kwargs): for submobject in self.submobjects: submobject.add_background_rectangle(**kwargs) return self - def add_background_rectangle_to_family_members_with_points(self, **kwargs) -> Self: + def add_background_rectangle_to_family_members_with_points(self, **kwargs): for mob in self.family_members_with_points(): mob.add_background_rectangle(**kwargs) return self # Getters - def get_bounding_box_point(self, direction: Vector3D) -> Point3D: + def get_bounding_box_point(self, direction): bb = self.get_bounding_box() indices = (np.sign(direction) + 1).astype(int) return np.array([bb[indices[i]][i] for i in range(3)]) - def get_edge_center(self, direction: Vector3D) -> Point3D: + def get_edge_center(self, direction) -> np.ndarray: """Get edge coordinates for certain direction.""" return self.get_bounding_box_point(direction) - def get_corner(self, direction: Vector3D) -> Point3D: + def get_corner(self, direction) -> np.ndarray: """Get corner coordinates for certain direction.""" return self.get_bounding_box_point(direction) - def get_center(self) -> Point3D: + def get_all_corners(self): + bb = self.get_bounding_box() + return np.array( + [ + [bb[indices[-i + 1]][i] for i in range(3)] + for indices in it.product([0, 2], repeat=3) + ] + ) + + def get_center(self) -> np.ndarray: """Get center coordinates.""" return self.get_bounding_box()[1] - def get_center_of_mass(self) -> Point3D: + def get_center_of_mass(self): return self.get_all_points().mean(0) - def get_boundary_point(self, direction: Vector3D) -> Point3D: + def get_boundary_point(self, direction): all_points = self.get_all_points() boundary_directions = all_points - self.get_center() norms = np.linalg.norm(boundary_directions, axis=1) @@ -2266,8 +2362,8 @@ def get_boundary_point(self, direction: Vector3D) -> Point3D: index = np.argmax(np.dot(boundary_directions, np.array(direction).T)) return all_points[index] - def get_continuous_bounding_box_point(self, direction: Vector3D) -> Point3D: - dl, center, ur = self.get_bounding_box() + def get_continuous_bounding_box_point(self, direction): + _dl, center, ur = self.get_bounding_box() corner_vect = ur - center return center + direction / np.max( np.abs( @@ -2280,86 +2376,85 @@ def get_continuous_bounding_box_point(self, direction: Vector3D) -> Point3D: ), ) - def get_top(self) -> Point3D: + def get_top(self) -> np.ndarray: """Get top coordinates of a box bounding the :class:`~.OpenGLMobject`""" return self.get_edge_center(UP) - def get_bottom(self) -> Point3D: + def get_bottom(self) -> np.ndarray: """Get bottom coordinates of a box bounding the :class:`~.OpenGLMobject`""" return self.get_edge_center(DOWN) - def get_right(self) -> Point3D: + def get_right(self) -> np.ndarray: """Get right coordinates of a box bounding the :class:`~.OpenGLMobject`""" return self.get_edge_center(RIGHT) - def get_left(self) -> Point3D: + def get_left(self) -> np.ndarray: """Get left coordinates of a box bounding the :class:`~.OpenGLMobject`""" return self.get_edge_center(LEFT) - def get_zenith(self) -> Point3D: + def get_zenith(self) -> np.ndarray: """Get zenith coordinates of a box bounding a 3D :class:`~.OpenGLMobject`.""" return self.get_edge_center(OUT) - def get_nadir(self) -> Point3D: + def get_nadir(self) -> np.ndarray: """Get nadir (opposite the zenith) coordinates of a box bounding a 3D :class:`~.OpenGLMobject`.""" return self.get_edge_center(IN) - def length_over_dim(self, dim: int) -> float: + def length_over_dim(self, dim): bb = self.get_bounding_box() return abs((bb[2] - bb[0])[dim]) - def get_width(self) -> float: + def get_width(self): """Returns the width of the mobject.""" return self.length_over_dim(0) - def get_height(self) -> float: + def get_height(self): """Returns the height of the mobject.""" return self.length_over_dim(1) - def get_depth(self) -> float: + def get_depth(self): """Returns the depth of the mobject.""" return self.length_over_dim(2) - def get_coord(self, dim: int, direction: Vector3D = ORIGIN) -> ManimFloat: + def get_coord(self, dim: int, direction=ORIGIN): """Meant to generalize ``get_x``, ``get_y`` and ``get_z``""" return self.get_bounding_box_point(direction)[dim] - def get_x(self, direction: Vector3D = ORIGIN) -> ManimFloat: + def get_x(self, direction=ORIGIN) -> np.float64: """Returns x coordinate of the center of the :class:`~.OpenGLMobject` as ``float``""" return self.get_coord(0, direction) - def get_y(self, direction: Vector3D = ORIGIN) -> ManimFloat: + def get_y(self, direction=ORIGIN) -> np.float64: """Returns y coordinate of the center of the :class:`~.OpenGLMobject` as ``float``""" return self.get_coord(1, direction) - def get_z(self, direction: Vector3D = ORIGIN) -> ManimFloat: + def get_z(self, direction=ORIGIN) -> np.float64: """Returns z coordinate of the center of the :class:`~.OpenGLMobject` as ``float``""" return self.get_coord(2, direction) - def get_start(self) -> Point3D: + def get_start(self): """Returns the point, where the stroke that surrounds the :class:`~.OpenGLMobject` starts.""" self.throw_error_if_no_points() return np.array(self.points[0]) - def get_end(self) -> Point3D: + def get_end(self): """Returns the point, where the stroke that surrounds the :class:`~.OpenGLMobject` ends.""" self.throw_error_if_no_points() return np.array(self.points[-1]) - def get_start_and_end(self) -> tuple[Point3D, Point3D]: + def get_start_and_end(self): """Returns starting and ending point of a stroke as a ``tuple``.""" return self.get_start(), self.get_end() - def point_from_proportion(self, alpha: float) -> Point3D: + def point_from_proportion(self, alpha): points = self.points i, subalpha = integer_interpolate(0, len(points) - 1, alpha) return interpolate(points[i], points[i + 1], subalpha) - def pfp(self, alpha: float) -> Point3D: - """Abbreviation for point_from_proportion""" - return self.point_from_proportion(alpha) + pfp = point_from_proportion + """Abbreviation for point_from_proportion""" - def get_pieces(self, n_pieces: int) -> OpenGLMobject: + def get_pieces(self, n_pieces): template = self.copy() template.submobjects = [] alphas = np.linspace(0, 1, n_pieces + 1) @@ -2370,60 +2465,58 @@ def get_pieces(self, n_pieces: int) -> OpenGLMobject: ) ) - def get_z_index_reference_point(self) -> Point3D: + def get_z_index_reference_point(self): # TODO, better place to define default z_index_group? z_index_group = getattr(self, "z_index_group", self) return z_index_group.get_center() # Match other mobject properties - def match_color(self, mobject: OpenGLMobject) -> Self: + def match_color(self, mobject: OpenGLMobject): """Match the color with the color of another :class:`~.OpenGLMobject`.""" return self.set_color(mobject.get_color()) - def match_dim_size(self, mobject: OpenGLMobject, dim: int, **kwargs) -> Self: + def match_dim_size(self, mobject: OpenGLMobject, dim, **kwargs): """Match the specified dimension with the dimension of another :class:`~.OpenGLMobject`.""" return self.rescale_to_fit(mobject.length_over_dim(dim), dim, **kwargs) - def match_width(self, mobject: OpenGLMobject, **kwargs) -> Self: + def match_width(self, mobject: OpenGLMobject, **kwargs): """Match the width with the width of another :class:`~.OpenGLMobject`.""" return self.match_dim_size(mobject, 0, **kwargs) - def match_height(self, mobject: OpenGLMobject, **kwargs) -> Self: + def match_height(self, mobject: OpenGLMobject, **kwargs): """Match the height with the height of another :class:`~.OpenGLMobject`.""" return self.match_dim_size(mobject, 1, **kwargs) - def match_depth(self, mobject: OpenGLMobject, **kwargs) -> Self: + def match_depth(self, mobject: OpenGLMobject, **kwargs): """Match the depth with the depth of another :class:`~.OpenGLMobject`.""" return self.match_dim_size(mobject, 2, **kwargs) - def match_coord( - self, mobject: OpenGLMobject, dim: int, direction: Vector3D = ORIGIN - ) -> Self: + def match_coord(self, mobject_or_point: OpenGLMobject, dim, direction=ORIGIN): """Match the coordinates with the coordinates of another :class:`~.OpenGLMobject`.""" - return self.set_coord( - mobject.get_coord(dim, direction), - dim=dim, - direction=direction, - ) + if isinstance(mobject_or_point, OpenGLMobject): + coord = mobject_or_point.get_coord(dim, direction) + else: + coord = mobject_or_point[dim] + return self.set_coord(coord, dim=dim, direction=direction) - def match_x(self, mobject: OpenGLMobject, direction: Vector3D = ORIGIN) -> Self: + def match_x(self, mobject_or_point, direction=ORIGIN): """Match x coord. to the x coord. of another :class:`~.OpenGLMobject`.""" - return self.match_coord(mobject, 0, direction) + return self.match_coord(mobject_or_point, 0, direction) - def match_y(self, mobject: OpenGLMobject, direction: Vector3D = ORIGIN) -> Self: + def match_y(self, mobject_or_point, direction=ORIGIN): """Match y coord. to the x coord. of another :class:`~.OpenGLMobject`.""" - return self.match_coord(mobject, 1, direction) + return self.match_coord(mobject_or_point, 1, direction) - def match_z(self, mobject: OpenGLMobject, direction: Vector3D = ORIGIN) -> Self: + def match_z(self, mobject_or_point, direction=ORIGIN): """Match z coord. to the x coord. of another :class:`~.OpenGLMobject`.""" - return self.match_coord(mobject, 2, direction) + return self.match_coord(mobject_or_point, 2, direction) def align_to( self, - mobject_or_point: OpenGLMobject | Point3D, - direction: Vector3D = ORIGIN, - ) -> Self: + mobject_or_point: OpenGLMobject | Sequence[float], + direction=ORIGIN, + ): """ Examples: mob1.align_to(mob2, UP) moves mob1 vertically so that its @@ -2443,46 +2536,39 @@ def align_to( self.set_coord(point[dim], dim, direction) return self - def get_group_class(self) -> type[OpenGLGroup]: + def get_group_class(self): return OpenGLGroup @staticmethod - def get_mobject_type_class() -> type[OpenGLMobject]: + def get_mobject_type_class(): """Return the base class of this mobject type.""" return OpenGLMobject # Alignment - def align_data_and_family(self, mobject: OpenGLMobject) -> Self: + def is_aligned_with(self, mobject: OpenGLMobject) -> bool: + return len(self.submobjects) == len(mobject.submobjects) and all( + sm1.is_aligned_with(sm2) + for sm1, sm2 in zip(self.submobjects, mobject.submobjects) + ) + + def align_data_and_family(self, mobject): self.align_family(mobject) self.align_data(mobject) - return self - def align_data(self, mobject: OpenGLMobject) -> Self: - # In case any data arrays get resized when aligned to shader data - # self.refresh_shader_data() + def align_data(self, mobject) -> None: for mob1, mob2 in zip(self.get_family(), mobject.get_family()): # Separate out how points are treated so that subclasses # can handle that case differently if they choose mob1.align_points(mob2) - for key in mob1.data.keys() & mob2.data.keys(): - if key == "points": - continue - arr1 = mob1.data[key] - arr2 = mob2.data[key] - if len(arr2) > len(arr1): - mob1.data[key] = resize_preserving_order(arr1, len(arr2)) - elif len(arr1) > len(arr2): - mob2.data[key] = resize_preserving_order(arr2, len(arr1)) - return self - - def align_points(self, mobject: OpenGLMobject) -> Self: + + def align_points(self, mobject) -> Self: max_len = max(self.get_num_points(), mobject.get_num_points()) for mob in (self, mobject): mob.resize_points(max_len, resize_func=resize_preserving_order) return self - def align_family(self, mobject: OpenGLMobject) -> Self: + def align_family(self, mobject) -> Self: mob1 = self mob2 = mobject n1 = len(mob1) @@ -2498,11 +2584,11 @@ def align_family(self, mobject: OpenGLMobject) -> Self: def push_self_into_submobjects(self) -> Self: copy = self.deepcopy() copy.submobjects = [] - self.resize_points(0) + self.clear_points() self.add(copy) return self - def add_n_more_submobjects(self, n: int) -> Self: + def add_n_more_submobjects(self, n) -> Self: if n == 0: return self @@ -2512,6 +2598,7 @@ def add_n_more_submobjects(self, n: int) -> Self: null_mob = self.copy() null_mob.set_points([self.get_center()]) self.submobjects = [null_mob.copy() for k in range(n)] + self.note_changed_family() return self target = curr + n repeat_indices = (np.arange(target) * curr) // target @@ -2527,16 +2614,13 @@ def add_n_more_submobjects(self, n: int) -> Self: new_submob.set_opacity(0) new_submobs.append(new_submob) self.submobjects = new_submobs + self.note_changed_family() return self # Interpolate def interpolate( - self, - mobject1: OpenGLMobject, - mobject2: OpenGLMobject, - alpha: float, - path_func: PathFuncType = straight_path(), + self, mobject1, mobject2, alpha, path_func: PathFuncType = straight_path() ) -> Self: """Turns this :class:`~.OpenGLMobject` into an interpolation between ``mobject1`` and ``mobject2``. @@ -2558,38 +2642,14 @@ def construct(self): self.add(dotL, dotR, dotMiddle) """ - for key in self.data: - if key in self.locked_data_keys: - continue - if len(self.data[key]) == 0: - continue - if key not in mobject1.data or key not in mobject2.data: - continue - - func = path_func if key in ("points", "bounding_box") else interpolate - - self.data[key][:] = func(mobject1.data[key], mobject2.data[key], alpha) - - for key in self.uniforms: - if key != "fixed_orientation_center": - self.uniforms[key] = interpolate( - mobject1.uniforms[key], - mobject2.uniforms[key], - alpha, - ) - else: - self.uniforms["fixed_orientation_center"] = tuple( - interpolate( - np.array(mobject1.uniforms["fixed_orientation_center"]), - np.array(mobject2.uniforms["fixed_orientation_center"]), - alpha, - ) - ) + self.points = path_func(mobject1.points, mobject2.points, alpha) + self.interpolate_color(mobject1, mobject2, alpha) return self - def pointwise_become_partial( - self, mobject: OpenGLMobject, a: float, b: float - ) -> None: + def interpolate_color(self, mobject1, mobject2, alpha): + raise NotImplementedError("Implemented in subclasses") + + def pointwise_become_partial(self, mobject, a, b): """ Set points in such a way as to become only part of mobject. @@ -2601,12 +2661,13 @@ def pointwise_become_partial( def become( self, mobject: OpenGLMobject, + match_updaters=False, match_height: bool = False, match_width: bool = False, match_depth: bool = False, match_center: bool = False, stretch: bool = False, - ) -> Self: + ): """Edit all data and submobjects to be identical to another :class:`~.OpenGLMobject` @@ -2641,6 +2702,7 @@ def construct(self): circ.become(square) self.wait(0.5) """ + # Manim CE Weird stretching thing which also modifies the original mobject if stretch: mobject.stretch_to_fit_height(self.height) mobject.stretch_to_fit_width(self.width) @@ -2656,225 +2718,168 @@ def construct(self): if match_center: mobject.move_to(self.get_center()) + # Original 3b1b/manim behaviour self.align_family(mobject) - for sm1, sm2 in zip(self.get_family(), mobject.get_family()): - sm1.set_data(sm2.data) - sm1.set_uniforms(sm2.uniforms) + family1 = self.get_family() + family2 = mobject.get_family() + for sm1, sm2 in zip(family1, family2): + sm1.depth_test = sm2.depth_test + sm1.points = ( + sm2.points + ) # TODO TS: Just a hack here but it seems to fix updaters + # Make sure named family members carry over + for attr, value in mobject.__dict__.items(): + if isinstance(value, OpenGLMobject) and value in family2: + setattr(self, attr, family1[family2.index(value)]) self.refresh_bounding_box(recurse_down=True) + if match_updaters: + self.match_updaters(mobject) + self.note_changed_family() return self - # Locking data - - def lock_data(self, keys: Iterable[str]) -> None: - """ - To speed up some animations, particularly transformations, - it can be handy to acknowledge which pieces of data - won't change during the animation so that calls to - interpolate can skip this, and so that it's not - read into the shader_wrapper objects needlessly - """ - if self.has_updaters: - return - # Be sure shader data has most up to date information - self.refresh_shader_data() - self.locked_data_keys = set(keys) - - def lock_matching_data( - self, mobject1: OpenGLMobject, mobject2: OpenGLMobject - ) -> Self: - for sm, sm1, sm2 in zip( - self.get_family(), - mobject1.get_family(), - mobject2.get_family(), - ): - keys = sm.data.keys() & sm1.data.keys() & sm2.data.keys() - sm.lock_data( - list( - filter( - lambda key: np.all(sm1.data[key] == sm2.data[key]), - keys, - ), - ), - ) - return self - - def unlock_data(self) -> None: - for mob in self.get_family(): - mob.locked_data_keys = set() + def looks_identical(self, mobject: OpenGLMobject) -> bool: + fam1 = self.family_members_with_points() + fam2 = mobject.family_members_with_points() + return len(fam1) == len(fam2) - # Operations touching shader uniforms + def has_same_shape_as(self, mobject: OpenGLMobject) -> bool: + # Normalize both point sets by centering and making height 1 + points1, points2 = ( + (m.get_all_points() - m.get_center()) / m.get_height() + for m in (self, mobject) + ) + if len(points1) != len(points2): + return False + return bool(np.isclose(points1, points2).all()) - @affects_shader_info_id def fix_in_frame(self) -> Self: - self.is_fixed_in_frame = 1.0 + for mob in self.get_family(): + mob.is_fixed_in_frame = True return self - @affects_shader_info_id def fix_orientation(self) -> Self: - self.is_fixed_orientation = 1.0 - self.fixed_orientation_center = tuple(self.get_center()) - self.depth_test = True + for mob in self.get_family(): + mob.is_fixed_orientation = 1.0 + mob.fixed_orientation_center = tuple(self.get_center()) return self - @affects_shader_info_id def unfix_from_frame(self) -> Self: - self.is_fixed_in_frame = 0.0 + for mob in self.get_family(): + mob.is_fixed_in_frame = 0.0 return self - @affects_shader_info_id - def unfix_orientation(self) -> Self: - self.is_fixed_orientation = 0.0 - self.fixed_orientation_center = (0, 0, 0) - self.depth_test = False + def unfix_orientation(self): + for mob in self.get_family(): + mob.is_fixed_orientation = 0.0 + mob.fixed_orientation_center = (0, 0, 0) return self - @affects_shader_info_id - def apply_depth_test(self) -> Self: - self.depth_test = True + def apply_depth_test(self): + for mob in self.get_family(): + mob.depth_test = True return self - @affects_shader_info_id - def deactivate_depth_test(self) -> Self: - self.depth_test = False + def deactivate_depth_test(self): + for mob in self.get_family(): + mob.depth_test = False return self - # Shader code manipulation + # Event Handlers + """ + Event handling follows the Event Bubbling model of DOM in javascript. + Return false to stop the event bubbling. + To learn more visit https://www.quirksmode.org/js/events_order.html - def replace_shader_code(self, old_code: str, new_code: str) -> Self: - # TODO, will this work with VMobject structure, given - # that it does not simpler return shader_wrappers of - # family? - for wrapper in self.get_shader_wrapper_list(): - wrapper.replace_code(old_code, new_code) - return self + Event Callback Argument is a callable function taking two arguments: + 1. Mobject + 2. EventData + """ - def set_color_by_code(self, glsl_code: str) -> Self: - """ - Takes a snippet of code and inserts it into a - context which has the following variables: - vec4 color, vec3 point, vec3 unit_normal. - The code should change the color variable - """ - self.replace_shader_code("///// INSERT COLOR FUNCTION HERE /////", glsl_code) - return self + def init_event_listeners(self): + self.event_listeners: list[EventListener] = [] - def set_color_by_xyz_func( + def add_event_listener( self, - glsl_snippet: str, - min_value: float = -5.0, - max_value: float = 5.0, - colormap: str = "viridis", - ) -> Self: - """ - Pass in a glsl expression in terms of x, y and z which returns - a float. - """ - # TODO, add a version of this which changes the point data instead - # of the shader code - for char in "xyz": - glsl_snippet = glsl_snippet.replace(char, "point." + char) - rgb_list = get_colormap_list(colormap) - self.set_color_by_code( - f"color.rgb = float_to_color({glsl_snippet}, {float(min_value)}, {float(max_value)}, {get_colormap_code(rgb_list)});", - ) + event_type: EventType, + event_callback: Callable[[OpenGLMobject, dict[str, str]], None], + ): + event_listener = EventListener(self, event_type, event_callback) + self.event_listeners.append(event_listener) + EVENT_DISPATCHER.add_listener(event_listener) return self - # For shader data + def remove_event_listener( + self, + event_type: EventType, + event_callback: Callable[[OpenGLMobject, dict[str, str]], None], + ): + event_listener = EventListener(self, event_type, event_callback) + while event_listener in self.event_listeners: + self.event_listeners.remove(event_listener) + EVENT_DISPATCHER.remove_listener(event_listener) + return self - def refresh_shader_wrapper_id(self) -> Self: - self.get_shader_wrapper().refresh_id() + def clear_event_listeners(self, recurse: bool = True): + self.event_listeners = [] + if recurse: + for submob in self.submobjects: + submob.clear_event_listeners(recurse=recurse) return self - def get_shader_wrapper(self) -> ShaderWrapper: - from manim.renderer.shader_wrapper import ShaderWrapper + def get_event_listeners(self): + return self.event_listeners - # if hasattr(self, "__shader_wrapper"): - # return self.__shader_wrapper + def get_family_event_listeners(self): + return list(it.chain(*[sm.get_event_listeners() for sm in self.get_family()])) - self.shader_wrapper = ShaderWrapper( - vert_data=self.get_shader_data(), - vert_indices=self.get_shader_vert_indices(), - uniforms=self.get_shader_uniforms(), - depth_test=self.depth_test, - texture_paths=self.texture_paths, - render_primitive=self.render_primitive, - shader_folder=self.__class__.shader_folder, - ) - return self.shader_wrapper + def get_has_event_listener(self): + return any(mob.get_event_listeners() for mob in self.get_family()) - def get_shader_wrapper_list(self) -> Sequence[ShaderWrapper]: - shader_wrappers = it.chain( - [self.get_shader_wrapper()], - *(sm.get_shader_wrapper_list() for sm in self.submobjects), - ) - batches = batch_by_property(shader_wrappers, lambda sw: sw.get_id()) - - result = [] - for wrapper_group, _ in batches: - shader_wrapper = wrapper_group[0] - if not shader_wrapper.is_valid(): - continue - shader_wrapper.combine_with(*wrapper_group[1:]) - if len(shader_wrapper.vert_data) > 0: - result.append(shader_wrapper) - return result + def add_mouse_motion_listener(self, callback): + self.add_event_listener(EventType.MouseMotionEvent, callback) - def check_data_alignment(self, array: npt.NDArray, data_key: str) -> Self: - # Makes sure that self.data[key] can be broadcast into - # the given array, meaning its length has to be either 1 - # or the length of the array - d_len = len(self.data[data_key]) - if d_len != 1 and d_len != len(array): - self.data[data_key] = resize_with_interpolation( - self.data[data_key], - len(array), - ) - return self + def remove_mouse_motion_listener(self, callback): + self.remove_event_listener(EventType.MouseMotionEvent, callback) - def get_resized_shader_data_array(self, length: float) -> npt.NDArray: - # If possible, try to populate an existing array, rather - # than recreating it each frame - points = self.points - shader_data = np.zeros(len(points), dtype=self.shader_dtype) - return shader_data + def add_mouse_press_listener(self, callback): + self.add_event_listener(EventType.MousePressEvent, callback) - def read_data_to_shader( - self, - shader_data: npt.NDArray, # has structured data type, ex. ("point", np.float32, (3,)) - shader_data_key: str, - data_key: str, - ) -> None: - if data_key in self.locked_data_keys: - return - self.check_data_alignment(shader_data, data_key) - shader_data[shader_data_key] = self.data[data_key] + def remove_mouse_press_listener(self, callback): + self.remove_event_listener(EventType.MousePressEvent, callback) - def get_shader_data(self) -> npt.NDArray: - shader_data = self.get_resized_shader_data_array(self.get_num_points()) - self.read_data_to_shader(shader_data, "point", "points") - return shader_data + def add_mouse_release_listener(self, callback): + self.add_event_listener(EventType.MouseReleaseEvent, callback) - def refresh_shader_data(self) -> None: - self.get_shader_data() + def remove_mouse_release_listener(self, callback): + self.remove_event_listener(EventType.MouseReleaseEvent, callback) - def get_shader_uniforms(self) -> dict[str, Any]: - return self.uniforms + def add_mouse_drag_listener(self, callback): + self.add_event_listener(EventType.MouseDragEvent, callback) - def get_shader_vert_indices(self) -> Sequence[int]: - return self.shader_indices + def remove_mouse_drag_listener(self, callback): + self.remove_event_listener(EventType.MouseDragEvent, callback) - @property - def submobjects(self) -> Sequence[OpenGLMobject]: - return self._submobjects if hasattr(self, "_submobjects") else [] + def add_mouse_scroll_listener(self, callback): + self.add_event_listener(EventType.MouseScrollEvent, callback) - @submobjects.setter - def submobjects(self, submobject_list: Iterable[OpenGLMobject]) -> None: - self.remove(*self.submobjects) - self.add(*submobject_list) + def remove_mouse_scroll_listener(self, callback): + self.remove_event_listener(EventType.MouseScrollEvent, callback) + + def add_key_press_listener(self, callback): + self.add_event_listener(EventType.KeyPressEvent, callback) + + def remove_key_press_listener(self, callback): + self.remove_event_listener(EventType.KeyPressEvent, callback) + + def add_key_release_listener(self, callback): + self.add_event_listener(EventType.KeyReleaseEvent, callback) + + def remove_key_release_listener(self, callback): + self.remove_event_listener(EventType.KeyReleaseEvent, callback) # Errors - def throw_error_if_no_points(self) -> None: + def throw_error_if_no_points(self): if not self.has_points(): message = ( "Cannot call OpenGLMobject.{} " + "for a OpenGLMobject with no points" @@ -2882,44 +2887,79 @@ def throw_error_if_no_points(self) -> None: caller_name = sys._getframe(1).f_code.co_name raise Exception(message.format(caller_name)) + def set(self, **kwargs) -> Self: + """Sets attributes. + + Mainly to be used along with :attr:`animate` to + animate setting attributes. + + Examples + -------- + :: + + >>> mob = OpenGLMobject() + >>> mob.set(foo=0) + OpenGLMobject + >>> mob.foo + 0 + + Parameters + ---------- + **kwargs + The attributes and corresponding values to set. + + Returns + ------- + :class:`OpenGLMobject` + ``self`` + + + """ + for attr, value in kwargs.items(): + setattr(self, attr, value) + + return self + -class OpenGLGroup(OpenGLMobject): - def __init__(self, *mobjects: OpenGLMobject, **kwargs): +class OpenGLGroup(OpenGLMobject, InvisibleMobject): + def __init__(self, *mobjects, **kwargs): + if not all(isinstance(m, OpenGLMobject) for m in mobjects): + raise Exception("All submobjects must be of type OpenGLMobject") super().__init__(**kwargs) self.add(*mobjects) + def __add__(self, other: OpenGLMobject | OpenGLGroup): + assert isinstance(other, OpenGLMobject) + return self.add(other) + class OpenGLPoint(OpenGLMobject): def __init__( - self, - location: Point3D = ORIGIN, - artificial_width: float = 1e-6, - artificial_height: float = 1e-6, - **kwargs, + self, location=ORIGIN, artificial_width=1e-6, artificial_height=1e-6, **kwargs ): self.artificial_width = artificial_width self.artificial_height = artificial_height super().__init__(**kwargs) self.set_location(location) - def get_width(self) -> float: + def get_width(self): return self.artificial_width - def get_height(self) -> float: + def get_height(self): return self.artificial_height - def get_location(self) -> Point3D: + def get_location(self): return self.points[0].copy() - def get_bounding_box_point(self, *args, **kwargs) -> Point3D: + def get_bounding_box_point(self, *args, **kwargs): return self.get_location() - def set_location(self, new_loc: Point3D) -> None: + def set_location(self, new_loc): self.set_points(np.array(new_loc, ndmin=2, dtype=float)) -class _AnimationBuilder: - def __init__(self, mobject: OpenGLMobject): +class _AnimationBuilder(Generic[T_co]): + def __init__(self, mobject: T_co): self.mobject = mobject self.mobject.generate_target() @@ -2931,7 +2971,7 @@ def __init__(self, mobject: OpenGLMobject): self.cannot_pass_args = False self.anim_args = {} - def __call__(self, **kwargs) -> Self: + def __call__(self, **kwargs) -> _AnimationBuilder[T_co]: if self.cannot_pass_args: raise ValueError( "Animation arguments must be passed before accessing methods and can only be passed once", @@ -2942,7 +2982,7 @@ def __call__(self, **kwargs) -> Self: return self - def __getattr__(self, method_name: str) -> Callable[..., Self]: + def __getattr__(self, method_name: str): method = getattr(self.mobject.target, method_name) has_overridden_animation = hasattr(method, "_override_animate") @@ -2970,7 +3010,7 @@ def update_target(*method_args, **method_kwargs): return update_target - def build(self) -> _MethodAnimation: + def build(self) -> Animation: from manim.animation.transform import _MethodAnimation if self.overridden_animation: @@ -2984,7 +3024,7 @@ def build(self) -> _MethodAnimation: return anim -def override_animate(method: types.FunctionType) -> types.FunctionType: +def override_animate(method): r"""Decorator for overriding method animations. This allows to specify a method (returning an :class:`~.Animation`) diff --git a/manim/mobject/opengl/opengl_surface.py b/manim/mobject/opengl/opengl_surface.py index 565b8c71cf..5d346a0a69 100644 --- a/manim/mobject/opengl/opengl_surface.py +++ b/manim/mobject/opengl/opengl_surface.py @@ -19,6 +19,7 @@ __all__ = ["OpenGLSurface", "OpenGLTexturedSurface"] +# TODO: Those will not work in the current state we will have to think about a different method to render these with shaders in our current pipeline class OpenGLSurface(OpenGLMobject): r"""Creates a Surface. @@ -57,7 +58,6 @@ class OpenGLSurface(OpenGLMobject): ("dv_point", np.float32, (3,)), ("color", np.float32, (4,)), ] - shader_folder = "surface" def __init__( self, @@ -81,7 +81,6 @@ def __init__( epsilon=1e-5, render_primitive=moderngl.TRIANGLES, depth_test=True, - shader_folder=None, **kwargs, ): self.passed_uv_func = uv_func @@ -105,8 +104,6 @@ def __init__( opacity=opacity, gloss=gloss, shadow=shadow, - shader_folder=shader_folder if shader_folder is not None else "surface", - render_primitive=render_primitive, depth_test=depth_test, **kwargs, ) @@ -388,7 +385,8 @@ def __init__( if isinstance(image_mode, (str, Path)): image_mode = [image_mode] * 2 image_mode_light, image_mode_dark = image_mode - texture_paths = { + # TODO: move to renderer + _texture_paths = { "LightTexture": self.get_image_from_file( image_file, image_mode_light, @@ -407,7 +405,7 @@ def __init__( self.v_range = uv_surface.v_range self.resolution = uv_surface.resolution self.gloss = self.uv_surface.gloss - super().__init__(texture_paths=texture_paths, **kwargs) + super().__init__(**kwargs) def get_image_from_file( self, diff --git a/manim/mobject/opengl/opengl_vectorized_mobject.py b/manim/mobject/opengl/opengl_vectorized_mobject.py index b31934e999..6d10ee3d25 100644 --- a/manim/mobject/opengl/opengl_vectorized_mobject.py +++ b/manim/mobject/opengl/opengl_vectorized_mobject.py @@ -2,41 +2,50 @@ import itertools as it import operator as op -from collections.abc import Iterable, Sequence -from functools import reduce, wraps -from typing import Callable +from functools import reduce +from typing import TYPE_CHECKING, Literal -import moderngl import numpy as np +from typing_extensions import Unpack -from manim import config from manim.constants import * -from manim.mobject.opengl.opengl_mobject import OpenGLMobject, OpenGLPoint -from manim.renderer.shader_wrapper import ShaderWrapper +from manim.mobject.opengl.opengl_mobject import ( + MobjectKwargs, + OpenGLMobject, + OpenGLPoint, +) from manim.utils.bezier import ( bezier, bezier_remap, get_quadratic_approximation_of_cubic, get_smooth_cubic_bezier_handle_points, + get_smooth_quadratic_bezier_handle_points, integer_interpolate, interpolate, partial_bezier_points, proportions_along_bezier_curve_for_point, ) -from manim.utils.color import BLACK, WHITE, ManimColor, ParsableManimColor -from manim.utils.config_ops import _Data -from manim.utils.iterables import make_even, resize_with_interpolation, tuplify +from manim.utils.color import * +from manim.utils.color.core import ParsableManimColor +from manim.utils.deprecation import deprecated +from manim.utils.iterables import ( + listify, + make_even, +) from manim.utils.space_ops import ( angle_between_vectors, - cross2d, - earclip_triangulation, + get_norm, get_unit_normal, shoelace_direction, - z_to_vector, ) +if TYPE_CHECKING: + from collections.abc import Callable, Iterable, Sequence + + import numpy.typing as npt + from typing_extensions import Self + __all__ = [ - "triggers_refreshed_triangulation", "OpenGLVMobject", "OpenGLVGroup", "OpenGLVectorizedPoint", @@ -45,162 +54,125 @@ ] -def triggers_refreshed_triangulation(func): - @wraps(func) - def wrapper(self, *args, **kwargs): - old_points = np.empty((0, 3)) - for mob in self.family_members_with_points(): - old_points = np.concatenate((old_points, mob.points), axis=0) - func(self, *args, **kwargs) - new_points = np.empty((0, 3)) - for mob in self.family_members_with_points(): - new_points = np.concatenate((new_points, mob.points), axis=0) - if not np.array_equal(new_points, old_points): - self.refresh_triangulation() - self.refresh_unit_normal() - return self +DEFAULT_STROKE_COLOR = GREY_A +DEFAULT_FILL_COLOR = GREY_C - return wrapper + +# TODO: add this to the **kwargs of all mobjects that use OpenGLVMobject +class VMobjectKwargs(MobjectKwargs, total=False): + color: ParsableManimColor | Sequence[ParsableManimColor] | None + fill_color: ParsableManimColor | Sequence[ParsableManimColor] | None + fill_opacity: float | None + stroke_color: ParsableManimColor | Sequence[ParsableManimColor] | None + stroke_opacity: float | None + stroke_width: float + draw_stroke_behind_fill: bool + background_image_file: str | None + long_lines: bool + joint_type: LineJointType + flat_stroke: bool + shade_in_3d: bool + checkerboard_colors: bool # TODO: remove class OpenGLVMobject(OpenGLMobject): """A vectorized mobject.""" - fill_dtype = [ - ("point", np.float32, (3,)), - ("unit_normal", np.float32, (3,)), - ("color", np.float32, (4,)), - ("vert_index", np.float32, (1,)), - ] - stroke_dtype = [ - ("point", np.float32, (3,)), - ("prev_point", np.float32, (3,)), - ("next_point", np.float32, (3,)), - ("unit_normal", np.float32, (3,)), - ("stroke_width", np.float32, (1,)), - ("color", np.float32, (4,)), - ] - stroke_shader_folder = "quadratic_bezier_stroke" - fill_shader_folder = "quadratic_bezier_fill" - - fill_rgba = _Data() - stroke_rgba = _Data() - stroke_width = _Data() - unit_normal = _Data() + n_points_per_curve: int = 3 + pre_function_handle_to_anchor_scale_factor: float = 0.01 + make_smooth_after_applying_functions: bool = False + tolerance_for_point_equality: float = 1e-8 + # WARNING: before updating the __init__ update the VMobjectKwargs TypedDict + # so users can get autocomplete def __init__( self, - fill_color: ParsableManimColor | None = None, - fill_opacity: float = 0.0, - stroke_color: ParsableManimColor | None = None, - stroke_opacity: float = 1.0, + fill_color: ParsableManimColor | Sequence[ParsableManimColor] | None = None, + fill_opacity: float | None = None, + stroke_color: ParsableManimColor | Sequence[ParsableManimColor] | None = None, + stroke_opacity: float | None = None, stroke_width: float = DEFAULT_STROKE_WIDTH, draw_stroke_behind_fill: bool = False, - # Indicates that it will not be displayed, but - # that it should count in parent mobject's path - pre_function_handle_to_anchor_scale_factor: float = 0.01, - make_smooth_after_applying_functions: float = False, background_image_file: str | None = None, - # This is within a pixel - # TODO, do we care about accounting for - # varying zoom levels? - tolerance_for_point_equality: float = 1e-8, - n_points_per_curve: int = 3, long_lines: bool = False, - should_subdivide_sharp_curves: bool = False, - should_remove_null_curves: bool = False, - # Could also be "bevel", "miter", "round" - joint_type: LineJointType | None = None, - flat_stroke: bool = True, - render_primitive=moderngl.TRIANGLES, - triangulation_locked: bool = False, - **kwargs, + joint_type: LineJointType = LineJointType.AUTO, + flat_stroke: bool = False, + shade_in_3d: bool = False, # TODO: Can be ignored for now but we should think about using some sort of shader to introduce lighting after deferred rendering has completed + checkerboard_colors: bool = False, # ignore, + **kwargs: Unpack[MobjectKwargs], ): - self.data = {} - self.fill_opacity = fill_opacity - self.stroke_opacity = stroke_opacity - self.stroke_width = stroke_width + self.stroke_width = listify(stroke_width) self.draw_stroke_behind_fill = draw_stroke_behind_fill - # Indicates that it will not be displayed, but - # that it should count in parent mobject's path - self.pre_function_handle_to_anchor_scale_factor = ( - pre_function_handle_to_anchor_scale_factor - ) - self.make_smooth_after_applying_functions = make_smooth_after_applying_functions self.background_image_file = background_image_file - # This is within a pixel - # TODO, do we care about accounting for - # varying zoom levels? - self.tolerance_for_point_equality = tolerance_for_point_equality - self.n_points_per_curve = n_points_per_curve self.long_lines = long_lines - self.should_subdivide_sharp_curves = should_subdivide_sharp_curves - self.should_remove_null_curves = should_remove_null_curves - if joint_type is None: - joint_type = LineJointType.AUTO self.joint_type = joint_type self.flat_stroke = flat_stroke - self.render_primitive = render_primitive - self.triangulation_locked = triangulation_locked self.needs_new_triangulation = True self.triangulation = np.zeros(0, dtype="i4") - self.orientation = 1 - - self.fill_data = None - self.stroke_data = None - self.fill_shader_wrapper = None - self.stroke_shader_wrapper = None - self.init_shader_data() super().__init__(**kwargs) - self.refresh_unit_normal() + if fill_color is None: + fill_color = self.color + if stroke_color is None: + stroke_color = self.color + self.set_fill(color=fill_color, opacity=fill_opacity) + self.set_stroke(color=stroke_color, width=stroke_width, opacity=stroke_opacity) - if fill_color is not None: - self.fill_color = ManimColor.parse(fill_color) - if stroke_color is not None: - self.stroke_color = ManimColor.parse(stroke_color) + # self.refresh_unit_normal() def _assert_valid_submobjects(self, submobjects: Iterable[OpenGLVMobject]) -> Self: return self._assert_valid_submobjects_internal(submobjects, OpenGLVMobject) - def get_group_class(self): + def get_group_class(self) -> type[OpenGLVGroup]: # type: ignore return OpenGLVGroup @staticmethod def get_mobject_type_class(): return OpenGLVMobject - def init_data(self): - super().init_data() - self.data.pop("rgbas") - self.fill_rgba = np.zeros((1, 4)) - self.stroke_rgba = np.zeros((1, 4)) - self.unit_normal = np.zeros((1, 3)) - # stroke_width belongs to self.data, but is defined through init_colors+set_stroke + # These are here just to make type checkers happy + def get_family(self, recurse: bool = True) -> Sequence[OpenGLVMobject]: + return super().get_family(recurse) # type: ignore + + def family_members_with_points(self) -> Sequence[OpenGLVMobject]: # type: ignore + return super().family_members_with_points() # type: ignore + + def replicate(self, n: int) -> OpenGLVGroup: # type: ignore + return super().replicate(n) # type: ignore + + def get_grid(self, *args, **kwargs) -> OpenGLVGroup: # type: ignore + return super().get_grid(*args, **kwargs) # type: ignore + + def __getitem__(self, value: int | slice) -> Self: # type: ignore + return super().__getitem__(value) # type: ignore + + def add(self, *vmobjects: OpenGLVMobject) -> Self: # type: ignore + return super().add(*vmobjects) # Colors - def init_colors(self): - self.set_fill( - color=self.fill_color or self.color, - opacity=self.fill_opacity, - ) - self.set_stroke( - color=self.stroke_color or self.color, - width=self.stroke_width, - opacity=self.stroke_opacity, - background=self.draw_stroke_behind_fill, - ) - self.set_gloss(self.gloss) - self.set_flat_stroke(self.flat_stroke) + def init_colors(self) -> Self: + # self.set_fill( + # color=self.fill_color or self.color, + # opacity=self.fill_opacity, + # ) + # self.set_stroke( + # color=self.stroke_color or self.color, + # width=self.stroke_width, + # opacity=self.stroke_opacity, + # background=self.draw_stroke_behind_fill, + # ) + # self.set_gloss(self.gloss) + # self.set_flat_stroke(self.flat_stroke) + # self.color = self.get_color() return self def set_fill( self, - color: ParsableManimColor | None = None, + color: ParsableManimColor | Sequence[ParsableManimColor] | None = None, opacity: float | None = None, recurse: bool = True, - ) -> OpenGLVMobject: + ) -> Self: """Set the fill color and fill opacity of a :class:`OpenGLVMobject`. Parameters @@ -236,13 +208,13 @@ def construct(self): -------- :meth:`~.OpenGLVMobject.set_style` """ - if opacity is not None: - self.fill_opacity = opacity if recurse: - for submobject in self.submobjects: - submobject.set_fill(color, opacity, recurse) - - self.set_rgba_array(color, opacity, "fill_rgba", recurse) + for submob in self.submobjects: + submob.set_fill(color, opacity, recurse=True) + if color is not None: + self.fill_color: list[ManimColor] = listify(ManimColor.parse(color)) + if opacity is not None: + self.fill_color = [c.opacity(opacity) for c in self.fill_color] return self def set_stroke( @@ -253,79 +225,62 @@ def set_stroke( background=None, recurse=True, ): - if opacity is not None: - self.stroke_opacity = opacity - if recurse: - for submobject in self.submobjects: - submobject.set_stroke( - color=color, - width=width, - opacity=opacity, - background=background, - recurse=recurse, - ) - - self.set_rgba_array(color, opacity, "stroke_rgba", recurse) + for mob in self.get_family(recurse): + if color is not None: + mob.stroke_color = listify(ManimColor.parse(color)) + if opacity is not None: + mob.stroke_color = [c.opacity(opacity) for c in mob.stroke_color] - if width is not None: - for mob in self.get_family(recurse): - mob.stroke_width = np.array([[width] for width in tuplify(width)]) + if width is not None: + mob.stroke_width = listify(width) - if background is not None: - for mob in self.get_family(recurse): + if background is not None: mob.draw_stroke_behind_fill = background return self - def set_style( + def set_backstroke( self, - fill_color=None, - fill_opacity=None, - fill_rgba=None, - stroke_color=None, - stroke_opacity=None, - stroke_rgba=None, - stroke_width=None, - gloss=None, - shadow=None, - recurse=True, - ): - if fill_rgba is not None: - self.fill_rgba = resize_with_interpolation(fill_rgba, len(fill_rgba)) - else: - self.set_fill(color=fill_color, opacity=fill_opacity, recurse=recurse) + color: ManimColor | Iterable[ManimColor] | None = None, + width: float | Iterable[float] = 3, + background: bool = True, + ) -> Self: + self.set_stroke(color, width, background=background) + return self - if stroke_rgba is not None: - self.stroke_rgba = resize_with_interpolation(stroke_rgba, len(fill_rgba)) - self.set_stroke(width=stroke_width) - else: - self.set_stroke( + def set_style( + self, + fill_color: ParsableManimColor | Iterable[ParsableManimColor] | None = None, + fill_opacity: float | None = None, + stroke_color: ParsableManimColor | Iterable[ParsableManimColor] | None = None, + stroke_opacity: float | Iterable[float] | None = None, + stroke_width: float | Iterable[float] | None = None, + stroke_background: bool = True, + reflectiveness: float | None = None, + gloss: float | None = None, + shadow: float | None = None, + recurse: bool = True, + ) -> Self: + for mob in self.get_family(recurse): + mob.set_fill(color=fill_color, opacity=fill_opacity, recurse=False) + mob.set_stroke( color=stroke_color, width=stroke_width, opacity=stroke_opacity, - recurse=recurse, + recurse=False, + background=stroke_background, ) - - if gloss is not None: - self.set_gloss(gloss, recurse=recurse) - if shadow is not None: - self.set_shadow(shadow, recurse=recurse) return self def get_style(self): return { - "fill_rgba": self.fill_rgba, - "stroke_rgba": self.stroke_rgba, - "stroke_width": self.stroke_width, - "gloss": self.gloss, - "shadow": self.shadow, + "fill_color": self.fill_color.copy(), + "stroke_color": self.stroke_color.copy(), + "stroke_width": self.stroke_width.copy(), + # "stroke_background": self.draw_stroke_behind_fill, } - def match_style(self, vmobject, recurse=True): - vmobject_style = vmobject.get_style() - if config.renderer == RendererType.OPENGL: - vmobject_style["stroke_width"] = vmobject_style["stroke_width"][0][0] - vmobject_style["fill_opacity"] = self.get_fill_opacity() - self.set_style(**vmobject_style, recurse=False) + def match_style(self, vmobject: OpenGLVMobject, recurse: bool = True): + self.set_style(**vmobject.get_style(), recurse=False) if recurse: # Does its best to match up submobject lists, and # match styles accordingly @@ -338,113 +293,103 @@ def match_style(self, vmobject, recurse=True): sm1.match_style(sm2) return self - def set_color(self, color, opacity=None, recurse=True): - if opacity is not None: - self.opacity = opacity - + def set_color(self, color, opacity=None, recurse=True) -> Self: self.set_fill(color, opacity=opacity, recurse=recurse) self.set_stroke(color, opacity=opacity, recurse=recurse) return self - def set_opacity(self, opacity, recurse=True): + def set_opacity(self, opacity, recurse=True) -> Self: self.set_fill(opacity=opacity, recurse=recurse) self.set_stroke(opacity=opacity, recurse=recurse) return self - def fade(self, darkness=0.5, recurse=True): - factor = 1.0 - darkness - self.set_fill( - opacity=factor * self.get_fill_opacity(), - recurse=False, - ) - self.set_stroke( - opacity=factor * self.get_stroke_opacity(), - recurse=False, - ) - super().fade(darkness, recurse) + def fade(self, darkness=0.5, recurse=True) -> Self: + mobs = self.get_family() if recurse else [self] + for mob in mobs: + factor = 1.0 - darkness + mob.set_fill( + opacity=factor * mob.get_fill_opacity(), + recurse=False, + ) + mob.set_stroke( + opacity=factor * mob.get_stroke_opacity(), + recurse=False, + ) return self # Todo im not quite sure why we are doing this def get_fill_colors(self): - return [ManimColor.from_rgb(rgba[:3]) for rgba in self.fill_rgba] + return self.fill_color - def get_fill_opacities(self): - return self.fill_rgba[:, 3] + def get_fill_opacities(self) -> np.ndarray: + return [c.to_rgba()[3] for c in self.fill_color] def get_stroke_colors(self): - return [ManimColor.from_rgb(rgba[:3]) for rgba in self.stroke_rgba] + return self.stroke_color - def get_stroke_opacities(self): - return self.stroke_rgba[:, 3] + def get_stroke_opacities(self) -> np.ndarray: + return [c.to_rgba()[3] for c in self.stroke_color] - def get_stroke_widths(self): + def get_stroke_widths(self) -> np.ndarray: return self.stroke_width # TODO, it's weird for these to return the first of various lists # rather than the full information - def get_fill_color(self): + def get_fill_color(self) -> ManimColor: """ If there are multiple colors (for gradient) this returns the first one """ return self.get_fill_colors()[0] - def get_fill_opacity(self): + def get_fill_opacity(self) -> float: """ If there are multiple opacities, this returns the first """ return self.get_fill_opacities()[0] - def get_stroke_color(self): + def get_stroke_color(self) -> ManimColor: return self.get_stroke_colors()[0] - def get_stroke_width(self): + def get_stroke_width(self) -> float | np.ndarray: return self.get_stroke_widths()[0] - def get_stroke_opacity(self): + def get_stroke_opacity(self) -> float: return self.get_stroke_opacities()[0] - def get_color(self): - if not self.has_fill(): - return self.get_stroke_color() - return self.get_fill_color() - - def get_colors(self): - if self.has_stroke(): - return self.get_stroke_colors() - return self.get_fill_colors() - - stroke_color = property(get_stroke_color, set_stroke) - color = property(get_color, set_color) - fill_color = property(get_fill_color, set_fill) + def get_color(self) -> ManimColor: + if self.has_fill(): + return self.get_fill_color() + return self.get_stroke_color() - def has_stroke(self): - stroke_widths = self.get_stroke_widths() - stroke_opacities = self.get_stroke_opacities() - return ( - stroke_widths is not None - and stroke_opacities is not None - and any(stroke_widths) - and any(stroke_opacities) - ) + def has_stroke(self) -> bool: + # TODO: This currently doesn't make sense needs fixing + return len(self.stroke_width) > 0 and any(self.get_stroke_opacities()) - def has_fill(self): - fill_opacities = self.get_fill_opacities() - return fill_opacities is not None and any(fill_opacities) + def has_fill(self) -> bool: + return any(self.get_fill_opacities()) - def get_opacity(self): + def get_opacity(self) -> float: if self.has_fill(): return self.get_fill_opacity() return self.get_stroke_opacity() - def set_flat_stroke(self, flat_stroke=True, recurse=True): + def set_flat_stroke(self, flat_stroke: bool = True, recurse: bool = True): + for mob in self.get_family(recurse): + mob.uniforms["flat_stroke"] = float(flat_stroke) + return self + + def get_flat_stroke(self) -> bool: + return self.uniforms["flat_stroke"] == 1.0 + + def set_joint_type(self, joint_type: LineJointType, recurse: bool = True): for mob in self.get_family(recurse): - mob.flat_stroke = flat_stroke + mob.uniforms["joint_type"] = float(joint_type.value) return self - def get_flat_stroke(self): - return self.flat_stroke + def get_joint_type(self) -> LineJointType: + return LineJointType(int(self.uniforms["joint_type"])) # Points def set_anchors_and_handles(self, anchors1, handles, anchors2): @@ -492,7 +437,7 @@ def add_quadratic_bezier_curve_to(self, handle, anchor): else: self.append_points([self.get_last_point(), handle, anchor]) - def add_line_to(self, point: Sequence[float]) -> OpenGLVMobject: + def add_line_to(self, point: Sequence[float] | npt.NDArray[float]) -> Self: """Add a straight line from the last point of OpenGLVMobject to the given point. Parameters @@ -501,6 +446,10 @@ def add_line_to(self, point: Sequence[float]) -> OpenGLVMobject: point end of the straight line. """ + point = np.asarray(point) + if not self.has_points(): + self.points = np.array([point]) + return self end = self.points[-1] alphas = np.linspace(0, 1, self.n_points_per_curve) if self.long_lines: @@ -524,9 +473,12 @@ def add_smooth_curve_to(self, point): self.add_quadratic_bezier_curve_to(new_handle, point) return self - def add_smooth_cubic_curve_to(self, handle, point): + def add_smooth_cubic_curve_to(self, handle: np.ndarray, point: np.ndarray): self.throw_error_if_no_points() - new_handle = self.get_reflection_of_last_handle() + if self.get_num_points() == 1: + new_handle = self.points[-1] + else: + new_handle = self.get_reflection_of_last_handle() self.add_cubic_bezier_curve_to(new_handle, handle, point) def has_new_path_started(self): @@ -547,7 +499,7 @@ def is_closed(self): return self.consider_points_equals(self.points[0], self.points[-1]) def subdivide_sharp_curves(self, angle_threshold=30 * DEGREES, recurse=True): - vmobs = [vm for vm in self.get_family(recurse) if vm.has_points()] + vmobs = [vm for vm in self.get_family(recurse=recurse) if vm.has_points()] for vmob in vmobs: new_points = [] for tup in vmob.get_bezier_tuples(): @@ -569,9 +521,9 @@ def subdivide_sharp_curves(self, angle_threshold=30 * DEGREES, recurse=True): def add_points_as_corners(self, points): for point in points: self.add_line_to(point) - return points + return self - def set_points_as_corners(self, points: Iterable[float]) -> OpenGLVMobject: + def set_points_as_corners(self, points: Iterable[float]) -> Self: """Given an array of points, set them as corner of the vmobject. To achieve that, this algorithm sets handles aligned with the anchors such that the resultant bezier curve will be the segment @@ -594,12 +546,17 @@ def set_points_as_corners(self, points: Iterable[float]) -> OpenGLVMobject: ) return self - def set_points_smoothly(self, points, true_smooth=False): + def set_points_smoothly(self, points, true_smooth=False) -> Self: self.set_points_as_corners(points) - self.make_smooth() + if true_smooth: + self.make_smooth() + else: + self.make_approximately_smooth() return self - def change_anchor_mode(self, mode): + def change_anchor_mode( + self, mode: Literal["jagged", "approx_smooth", "true_smooth"] + ) -> Self: """Changes the anchor mode of the bezier curves. This will modify the handles. There can be only three modes, "jagged", "approx_smooth" and "true_smooth". @@ -618,22 +575,19 @@ def change_anchor_mode(self, mode): anchors = np.vstack([subpath[::nppc], subpath[-1:]]) new_subpath = np.array(subpath) if mode == "approx_smooth": - # TODO: get_smooth_quadratic_bezier_handle_points is not defined new_subpath[1::nppc] = get_smooth_quadratic_bezier_handle_points( - anchors, + anchors ) elif mode == "true_smooth": h1, h2 = get_smooth_cubic_bezier_handle_points(anchors) new_subpath = get_quadratic_approximation_of_cubic( - anchors[:-1], - h1, - h2, - anchors[1:], + anchors[:-1], h1, h2, anchors[1:] ) elif mode == "jagged": new_subpath[1::nppc] = 0.5 * (anchors[:-1] + anchors[1:]) submob.append_points(new_subpath) - submob.refresh_triangulation() + # TODO: not implemented + # submob.refresh_triangulation() return self def make_smooth(self): @@ -672,11 +626,10 @@ def append_vectorized_mobject(self, vectorized_mobject): if self.has_new_path_started(): # Remove last point, which is starting # a new path - self.resize_data(len(self.points - 1)) + self.points = self.points[:-1] self.append_points(new_points) return self - # def consider_points_equals(self, p0, p1): return np.linalg.norm(p1 - p0) < self.tolerance_for_point_equality @@ -835,6 +788,13 @@ def get_num_curves(self) -> int: """ return self.get_num_points() // self.n_points_per_curve + def quick_point_from_proportion(self, alpha: float) -> np.ndarray: + # Assumes all curves have the same length, so is inaccurate + num_curves = self.get_num_curves() + n, residue = integer_interpolate(0, num_curves, alpha) + curve_func = self.get_nth_curve_function(n) + return curve_func(residue) + def get_nth_curve_length( self, n: int, @@ -898,7 +858,7 @@ def get_nth_curve_length_pieces( curve = self.get_nth_curve_function(n) points = np.array([curve(a) for a in np.linspace(0, 1, sample_points)]) diffs = points[1:] - points[:-1] - norms = np.apply_along_axis(np.linalg.norm, 1, diffs) + norms = np.apply_along_axis(np.linalg.norm, 1, diffs) # type: ignore return norms @@ -1003,7 +963,7 @@ def proportion_from_point( num_curves = self.get_num_curves() total_length = self.get_arc_length() - target_length = 0 + target_length = 0.0 for n in range(num_curves): control_points = self.get_nth_curve_points(n) length = self.get_nth_curve_length(n) @@ -1066,10 +1026,16 @@ def get_anchors(self) -> Iterable[np.ndarray]: points = self.points if len(points) == 1: return points - - s = self.get_start_anchors() - e = self.get_end_anchors() - return list(it.chain.from_iterable(zip(s, e))) + return np.array( + list( + it.chain( + *zip( + self.get_start_anchors(), + self.get_end_anchors(), + ) + ) + ) + ) def get_points_without_null_curves(self, atol=1e-9): nppc = self.n_points_per_curve @@ -1083,25 +1049,28 @@ def get_points_without_null_curves(self, atol=1e-9): ) return points[distinct_curves.repeat(nppc)] - def get_arc_length(self, sample_points_per_curve: int | None = None) -> float: + def get_arc_length(self, n_sample_points: int | None = None) -> float: """Return the approximated length of the whole curve. Parameters ---------- - sample_points_per_curve - Number of sample points per curve used to approximate the length. More points result in a better approximation. + n_sample_points + The number of points to sample. If ``None``, the number of points is calculated automatically. + Takes points on the outline of the :class:`OpenGLVMobject` and calculates the distance between them. Returns ------- float The length of the :class:`OpenGLVMobject`. """ - return np.sum( - length - for _, length in self.get_curve_functions_with_lengths( - sample_points=sample_points_per_curve, - ) + if n_sample_points is None: + n_sample_points = 4 * self.get_num_curves() + 1 + points = np.array( + [self.point_from_proportion(a) for a in np.linspace(0, 1, n_sample_points)] ) + diffs = points[1:] - points[:-1] + norms = np.array([get_norm(d) for d in diffs]) + return norms.sum() def get_area_vector(self): # Returns a vector whose length is the area bound by @@ -1116,6 +1085,11 @@ def get_area_vector(self): p0 = points[0::nppc] p1 = points[nppc - 1 :: nppc] + if len(p0) != len(p1): + m = min(len(p0), len(p1)) + p0 = p0[:m] + p1 = p1[:m] + # Each term goes through all edges [(x1, y1, z1), (x2, y2, z2)] return 0.5 * np.array( [ @@ -1151,36 +1125,29 @@ def get_direction(self): """ return shoelace_direction(self.get_start_anchors()) - def get_unit_normal(self, recompute=False): - if not recompute: - return self.unit_normal[0] - - if len(self.points) < 3: + def get_unit_normal(self) -> np.ndarray: + if self.get_num_points() < 3: return OUT area_vect = self.get_area_vector() - area = np.linalg.norm(area_vect) + area = get_norm(area_vect) if area > 0: - return area_vect / area + normal = area_vect / area else: points = self.points - return get_unit_normal( + normal = get_unit_normal( points[1] - points[0], points[2] - points[1], ) - - def refresh_unit_normal(self): - for mob in self.get_family(): - mob.unit_normal[:] = mob.get_unit_normal(recompute=True) - return self + return normal # Alignment - def align_points(self, vmobject): + def align_points(self, vmobject: OpenGLVMobject) -> Self: # TODO: This shortcut can be a bit over eager. What if they have the same length, but different subpath lengths? - if self.get_num_points() == len(vmobject.points): + if self.get_num_points() == vmobject.get_num_points(): return - for mob in self, vmobject: + for mob in (self, vmobject): # If there are no points, add one to # where the "center" is if not mob.has_points(): @@ -1205,14 +1172,14 @@ def get_nth_subpath(path_list, n): # Create a null path at the very end return [path_list[-1][-1]] * nppc path = path_list[n] - # Check for useless points at the end of the path and remove them - # https://github.com/ManimCommunity/manim/issues/1959 - while len(path) > nppc: - # If the last nppc points are all equal to the preceding point - if self.consider_points_equals(path[-nppc:], path[-nppc - 1]): - path = path[:-nppc] - else: - break + # # Check for useless points at the end of the path and remove them + # # https://github.com/ManimCommunity/manim/issues/1959 + # while len(path) > nppc: + # # If the last nppc points are all equal to the preceding point + # if self.consider_points_equals(path[-nppc:], path[-nppc - 1]): + # path = path[:-nppc] + # else: + # break return path for n in range(n_subpaths): @@ -1228,7 +1195,7 @@ def get_nth_subpath(path_list, n): vmobject.set_points(np.vstack(new_subpaths2)) return self - def insert_n_curves(self, n: int, recurse=True) -> OpenGLVMobject: + def insert_n_curves(self, n: int, recurse=True) -> Self: """Inserts n curves to the bezier curves of the vmobject. Parameters @@ -1251,14 +1218,13 @@ def insert_n_curves(self, n: int, recurse=True) -> OpenGLVMobject: return self def insert_n_curves_to_point_list(self, n: int, points: np.ndarray) -> np.ndarray: - """Given an array of k points defining a bezier curves - (anchors and handles), returns points defining exactly - k + n bezier curves. + """Given an array of 3k points defining a Bézier curve (anchors and + handles), return 3(k+n) points defining exactly k + n Bézier curves. Parameters ---------- n - Number of desired curves. + Number of desired curves to insert. points Starting points. @@ -1267,10 +1233,9 @@ def insert_n_curves_to_point_list(self, n: int, points: np.ndarray) -> np.ndarra np.ndarray Points generated. """ - nppc = self.n_points_per_curve if len(points) == 1: + nppc = self.n_points_per_curve return np.repeat(points, nppc * n, 0) - bezier_tuples = self.get_bezier_tuples_from_points(points) current_number_of_curves = len(bezier_tuples) new_number_of_curves = current_number_of_curves + n @@ -1278,21 +1243,48 @@ def insert_n_curves_to_point_list(self, n: int, points: np.ndarray) -> np.ndarra new_points = new_bezier_tuples.reshape(-1, 3) return new_points - def interpolate(self, mobject1, mobject2, alpha, *args, **kwargs): - super().interpolate(mobject1, mobject2, alpha, *args, **kwargs) - if config["use_projection_fill_shaders"]: - self.refresh_triangulation() - else: - if self.has_fill(): - tri1 = mobject1.get_triangulation() - tri2 = mobject2.get_triangulation() - if len(tri1) != len(tri2) or not np.all(tri1 == tri2): - self.refresh_triangulation() - return self + def interpolate_color(self, mobject1, mobject2, alpha): + attrs = [ + "fill_color", + "stroke_color", + # "opacity", # TODO: This probably doesn't exist anymore because opacity is now moved into the colors + "reflectiveness", + "shadow", + "gloss", + "stroke_width", + # TODO: eventually add these attributes to OpenGLVMobject + # "background_stroke_width", + # "sheen_direction", + # "sheen_factor", + ] + def interp(obj1, obj2, alpha): + result = None + if isinstance(obj1, ManimColor) or isinstance(obj2, ManimColor): + result = obj1.interpolate(obj2, alpha) + else: + result = interpolate(obj1, obj2, alpha) + return result + + for attr in attrs: + if alpha == 1.0: + setattr(self, attr, getattr(mobject2, attr)) + continue + + attr1 = getattr(mobject1, attr) + attr2 = getattr(mobject2, attr) + if isinstance(attr1, list) or isinstance(attr2, list): + result = [ + interp(elem1, elem2, alpha) for elem1, elem2 in zip(attr1, attr2) + ] + else: + result = interp(attr1, attr2, alpha) + setattr(self, attr, result) + + # TODO: compare to 3b1b/manim again check if something changed so we don't need the cairo interpolation anymore def pointwise_become_partial( self, vmobject: OpenGLVMobject, a: float, b: float, remap: bool = True - ) -> OpenGLVMobject: + ) -> Self: """Given two bounds a and b, transforms the points of the self vmobject into the points of the vmobject passed as parameter with respect to the bounds. Points here stand for control points of the bezier curves (anchors and handles) @@ -1352,7 +1344,7 @@ def pointwise_become_partial( ) return self - def get_subcurve(self, a: float, b: float) -> OpenGLVMobject: + def get_subcurve(self, a: float, b: float) -> Self: """Returns the subcurve of the OpenGLVMobject between the interval [a, b]. The curve is a OpenGLVMobject itself. @@ -1375,221 +1367,20 @@ def get_subcurve(self, a: float, b: float) -> OpenGLVMobject: # Related to triangulation - def refresh_triangulation(self): - for mob in self.get_family(): - mob.needs_new_triangulation = True - return self - - def get_triangulation(self, normal_vector=None): - # Figure out how to triangulate the interior to know - # how to send the points as to the vertex shader. - # First triangles come directly from the points - if normal_vector is None: - normal_vector = self.get_unit_normal() - - if not self.needs_new_triangulation: - return self.triangulation - - points = self.points - - if len(points) <= 1: - self.triangulation = np.zeros(0, dtype="i4") - self.needs_new_triangulation = False - return self.triangulation - - if not np.isclose(normal_vector, OUT).all(): - # Rotate points such that unit normal vector is OUT - points = np.dot(points, z_to_vector(normal_vector)) - indices = np.arange(len(points), dtype=int) - - b0s = points[0::3] - b1s = points[1::3] - b2s = points[2::3] - v01s = b1s - b0s - v12s = b2s - b1s - - crosses = cross2d(v01s, v12s) - convexities = np.sign(crosses) - - atol = self.tolerance_for_point_equality - end_of_loop = np.zeros(len(b0s), dtype=bool) - end_of_loop[:-1] = (np.abs(b2s[:-1] - b0s[1:]) > atol).any(1) - end_of_loop[-1] = True - - concave_parts = convexities < 0 - - # These are the vertices to which we'll apply a polygon triangulation - inner_vert_indices = np.hstack( - [ - indices[0::3], - indices[1::3][concave_parts], - indices[2::3][end_of_loop], - ], - ) - inner_vert_indices.sort() - rings = np.arange(1, len(inner_vert_indices) + 1)[inner_vert_indices % 3 == 2] - - # Triangulate - inner_verts = points[inner_vert_indices] - inner_tri_indices = inner_vert_indices[ - earclip_triangulation(inner_verts, rings) - ] - - tri_indices = np.hstack([indices, inner_tri_indices]) - self.triangulation = tri_indices - self.needs_new_triangulation = False - return tri_indices - - @triggers_refreshed_triangulation - def set_points(self, points): - super().set_points(points) - return self - - @triggers_refreshed_triangulation - def set_data(self, data): - super().set_data(data) - return self - - # TODO, how to be smart about tangents here? - @triggers_refreshed_triangulation def apply_function(self, function, make_smooth=False, **kwargs): super().apply_function(function, **kwargs) if self.make_smooth_after_applying_functions or make_smooth: self.make_approximately_smooth() return self - @triggers_refreshed_triangulation def apply_points_function(self, *args, **kwargs): super().apply_points_function(*args, **kwargs) return self - @triggers_refreshed_triangulation def flip(self, *args, **kwargs): super().flip(*args, **kwargs) return self - # For shaders - def init_shader_data(self): - self.fill_data = np.zeros(0, dtype=self.fill_dtype) - self.stroke_data = np.zeros(0, dtype=self.stroke_dtype) - self.fill_shader_wrapper = ShaderWrapper( - vert_data=self.fill_data, - vert_indices=np.zeros(0, dtype="i4"), - shader_folder=self.fill_shader_folder, - render_primitive=self.render_primitive, - ) - self.stroke_shader_wrapper = ShaderWrapper( - vert_data=self.stroke_data, - shader_folder=self.stroke_shader_folder, - render_primitive=self.render_primitive, - ) - - def refresh_shader_wrapper_id(self): - for wrapper in [self.fill_shader_wrapper, self.stroke_shader_wrapper]: - wrapper.refresh_id() - return self - - def get_fill_shader_wrapper(self): - self.update_fill_shader_wrapper() - return self.fill_shader_wrapper - - def update_fill_shader_wrapper(self): - self.fill_shader_wrapper.vert_data = self.get_fill_shader_data() - self.fill_shader_wrapper.vert_indices = self.get_triangulation() - self.fill_shader_wrapper.uniforms = self.get_fill_uniforms() - self.fill_shader_wrapper.depth_test = self.depth_test - - def get_stroke_shader_wrapper(self): - self.update_stroke_shader_wrapper() - return self.stroke_shader_wrapper - - def update_stroke_shader_wrapper(self): - self.stroke_shader_wrapper.vert_data = self.get_stroke_shader_data() - self.stroke_shader_wrapper.uniforms = self.get_stroke_uniforms() - self.stroke_shader_wrapper.depth_test = self.depth_test - - def get_shader_wrapper_list(self): - # Build up data lists - fill_shader_wrappers = [] - stroke_shader_wrappers = [] - back_stroke_shader_wrappers = [] - for submob in self.family_members_with_points(): - if submob.has_fill() and not config["use_projection_fill_shaders"]: - fill_shader_wrappers.append(submob.get_fill_shader_wrapper()) - if submob.has_stroke() and not config["use_projection_stroke_shaders"]: - ssw = submob.get_stroke_shader_wrapper() - if submob.draw_stroke_behind_fill: - back_stroke_shader_wrappers.append(ssw) - else: - stroke_shader_wrappers.append(ssw) - - # Combine data lists - wrapper_lists = [ - back_stroke_shader_wrappers, - fill_shader_wrappers, - stroke_shader_wrappers, - ] - result = [] - for wlist in wrapper_lists: - if wlist: - wrapper = wlist[0] - wrapper.combine_with(*wlist[1:]) - result.append(wrapper) - return result - - def get_stroke_uniforms(self): - result = dict(super().get_shader_uniforms()) - result["joint_type"] = self.joint_type.value - result["flat_stroke"] = float(self.flat_stroke) - return result - - def get_fill_uniforms(self): - return { - "is_fixed_in_frame": float(self.is_fixed_in_frame), - "is_fixed_orientation": float(self.is_fixed_orientation), - "fixed_orientation_center": self.fixed_orientation_center, - "gloss": self.gloss, - "shadow": self.shadow, - } - - def get_stroke_shader_data(self): - points = self.points - if len(self.stroke_data) != len(points): - self.stroke_data = np.zeros(len(points), dtype=OpenGLVMobject.stroke_dtype) - - if "points" not in self.locked_data_keys: - nppc = self.n_points_per_curve - self.stroke_data["point"] = points - self.stroke_data["prev_point"][:nppc] = points[-nppc:] - self.stroke_data["prev_point"][nppc:] = points[:-nppc] - self.stroke_data["next_point"][:-nppc] = points[nppc:] - self.stroke_data["next_point"][-nppc:] = points[:nppc] - - self.read_data_to_shader(self.stroke_data, "color", "stroke_rgba") - self.read_data_to_shader(self.stroke_data, "stroke_width", "stroke_width") - self.read_data_to_shader(self.stroke_data, "unit_normal", "unit_normal") - - return self.stroke_data - - def get_fill_shader_data(self): - points = self.points - if len(self.fill_data) != len(points): - self.fill_data = np.zeros(len(points), dtype=OpenGLVMobject.fill_dtype) - self.fill_data["vert_index"][:, 0] = range(len(points)) - - self.read_data_to_shader(self.fill_data, "point", "points") - self.read_data_to_shader(self.fill_data, "color", "fill_rgba") - self.read_data_to_shader(self.fill_data, "unit_normal", "unit_normal") - - return self.fill_data - - def refresh_shader_data(self): - self.get_fill_shader_data() - self.get_stroke_shader_data() - - def get_fill_shader_vert_indices(self): - return self.get_triangulation() - class OpenGLVGroup(OpenGLVMobject): """A group of vectorized mobjects. @@ -1672,6 +1463,18 @@ def __str__(self): f"submobject{'s' if len(self.submobjects) > 0 else ''}" ) + def set_z(self, z: float) -> Self: + self.points[..., -1] = z + return self + + @deprecated( + since="0.20.0", + until="0.21.0", + message="OpenGL has no concept of z_index. Use set_z instead", + ) + def set_z_index(self, z: float) -> Self: + return self.set_z(z) + def add(self, *vmobjects: OpenGLVMobject): """Checks if all passed elements are an instance of OpenGLVMobject and then add them to submobjects @@ -1766,7 +1569,8 @@ def __setitem__(self, key: int, value: OpenGLVMobject | Sequence[OpenGLVMobject] >>> config.renderer = original_renderer """ self._assert_valid_submobjects(tuplify(value)) - self.submobjects[key] = value + self.submobjects[key] = value # type: ignore + self.note_changed_family() class OpenGLVectorizedPoint(OpenGLPoint, OpenGLVMobject): @@ -1776,15 +1580,15 @@ def __init__( color=BLACK, fill_opacity=0, stroke_width=0, - artificial_width=0.01, - artificial_height=0.01, **kwargs, ): - self.artificial_width = artificial_width - self.artificial_height = artificial_height - - super().__init__( - color=color, fill_opacity=fill_opacity, stroke_width=stroke_width, **kwargs + OpenGLPoint.__init__(self, location, **kwargs) + OpenGLVMobject.__init__( + self, + color=color, + fill_opacity=fill_opacity, + stroke_width=stroke_width, + **kwargs, ) self.set_points(np.array([location])) @@ -1858,11 +1662,12 @@ def __init__( color: ParsableManimColor = WHITE, **kwargs, ): + super().__init__(**kwargs) self.dashed_ratio = dashed_ratio self.num_dashes = num_dashes - super().__init__(color=color, **kwargs) r = self.dashed_ratio n = self.num_dashes + if num_dashes > 0: # Assuming total length is 1 dash_len = r / n @@ -1877,6 +1682,28 @@ def __init__( for i in range(n) ) ) + # Family is already taken care of by get_subcurve # implementation self.match_style(vmobject, recurse=False) + + +class VHighlight(OpenGLVGroup): + def __init__( + self, + vmobject: OpenGLVMobject, + n_layers: int = 5, + color_bounds: tuple[ManimColor, ManimColor] = (GREY_C, GREY_E), + max_stroke_addition: float = 5.0, + ): + outline = vmobject.replicate(n_layers) + outline.set_fill(opacity=0) + added_widths = np.linspace(0, max_stroke_addition, n_layers + 1)[1:] + colors = color_gradient(color_bounds, n_layers) + for part, added_width, color in zip(reversed(outline), added_widths, colors): + for sm in part.family_members_with_points(): + sm.set_stroke( + width=sm.get_stroke_width() + added_width, + color=color, + ) + super().__init__(*outline) diff --git a/manim/renderer/shader.py b/manim/mobject/opengl/shader.py similarity index 100% rename from manim/renderer/shader.py rename to manim/mobject/opengl/shader.py diff --git a/manim/mobject/svg/svg_mobject.py b/manim/mobject/svg/svg_mobject.py index 82c121fce7..116de30b1f 100644 --- a/manim/mobject/svg/svg_mobject.py +++ b/manim/mobject/svg/svg_mobject.py @@ -98,33 +98,20 @@ def __init__( should_center: bool = True, height: float | None = 2, width: float | None = None, - color: str | None = None, opacity: float | None = None, - fill_color: str | None = None, - fill_opacity: float | None = None, - stroke_color: str | None = None, - stroke_opacity: float | None = None, - stroke_width: float | None = None, svg_default: dict | None = None, path_string_config: dict | None = None, use_svg_cache: bool = True, **kwargs, ): - super().__init__(color=None, stroke_color=None, fill_color=None, **kwargs) + super().__init__(**kwargs) # process keyword arguments self.file_name = Path(file_name) if file_name is not None else None - self.should_center = should_center self.svg_height = height self.svg_width = width - self.color = color self.opacity = opacity - self.fill_color = fill_color - self.fill_opacity = fill_opacity - self.stroke_color = stroke_color - self.stroke_opacity = stroke_opacity - self.stroke_width = stroke_width if svg_default is None: svg_default = { @@ -132,24 +119,20 @@ def __init__( "opacity": None, "fill_color": None, "fill_opacity": None, - "stroke_width": 0, + "stroke_width": [0], "stroke_color": None, "stroke_opacity": None, } self.svg_default = svg_default - if path_string_config is None: - path_string_config = {} - self.path_string_config = path_string_config + self.path_string_config = path_string_config or {} self.init_svg_mobject(use_svg_cache=use_svg_cache) self.set_style( - fill_color=fill_color, - fill_opacity=fill_opacity, - stroke_color=stroke_color, - stroke_opacity=stroke_opacity, - stroke_width=stroke_width, + fill_color=self.fill_color, + stroke_color=self.stroke_color, + stroke_width=self.stroke_width, ) self.move_into_position() @@ -497,13 +480,12 @@ def init_points(self) -> None: self.handle_commands() - if config.renderer == "opengl": - if self.should_subdivide_sharp_curves: - # For a healthy triangulation later - self.subdivide_sharp_curves() - if self.should_remove_null_curves: - # Get rid of any null curves - self.set_points(self.get_points_without_null_curves()) + if self.should_subdivide_sharp_curves: + # For a healthy triangulation later + self.subdivide_sharp_curves() + if self.should_remove_null_curves: + # Get rid of any null curves + self.set_points(self.get_points_without_null_curves()) generate_points = init_points diff --git a/manim/mobject/table.py b/manim/mobject/table.py index 0810e6d59b..332b1be367 100644 --- a/manim/mobject/table.py +++ b/manim/mobject/table.py @@ -81,7 +81,6 @@ def construct(self): from ..animation.fading import FadeIn from ..mobject.types.vectorized_mobject import VGroup, VMobject from ..utils.color import BLACK, YELLOW, ManimColor, ParsableManimColor -from .utils import get_vectorized_mobject_class class Table(VGroup): @@ -325,7 +324,7 @@ def _add_labels(self, mob_table: VGroup) -> VGroup: else: # Placeholder to use arrange_in_grid if top_left_entry is not set. # Import OpenGLVMobject to work with --renderer=opengl - dummy_mobject = get_vectorized_mobject_class()() + dummy_mobject = VMobject() col_labels = [dummy_mobject] + self.col_labels mob_table.insert(0, col_labels) else: diff --git a/manim/mobject/text/numbers.py b/manim/mobject/text/numbers.py index 5283c24a20..9e8547d7c0 100644 --- a/manim/mobject/text/numbers.py +++ b/manim/mobject/text/numbers.py @@ -154,6 +154,8 @@ def font_size(self, font_val): def _set_submobjects_from_number(self, number): self.number = number + # the self.add below will recalculate the family, + # no need to do it here. self.submobjects = [] num_string = self._get_num_string(number) diff --git a/manim/mobject/text/tex_mobject.py b/manim/mobject/text/tex_mobject.py index 26334a60d9..7171b06df6 100644 --- a/manim/mobject/text/tex_mobject.py +++ b/manim/mobject/text/tex_mobject.py @@ -34,7 +34,7 @@ from manim.constants import * from manim.mobject.geometry.line import Line from manim.mobject.svg.svg_mobject import SVGMobject -from manim.mobject.types.vectorized_mobject import VGroup, VMobject +from manim.mobject.types.vectorized_mobject import VGroup from manim.utils.tex import TexTemplate from manim.utils.tex_file_writing import tex_to_svg_file @@ -65,14 +65,11 @@ def __init__( color: ParsableManimColor | None = None, **kwargs, ): - if color is None: - color = VMobject().color - self._font_size = font_size self.organize_left_to_right = organize_left_to_right self.tex_environment = tex_environment if tex_template is None: - tex_template = config["tex_template"] + tex_template = config.tex_template self.tex_template = tex_template assert isinstance(tex_string, str) @@ -220,6 +217,7 @@ def init_colors(self, propagate_colors=True): submobject.init_colors() elif config.renderer == RendererType.CAIRO: submobject.init_colors(propagate_colors=propagate_colors) + return self class MathTex(SingleStringMathTex): @@ -258,7 +256,7 @@ def __init__( *tex_strings, arg_separator: str = " ", substrings_to_isolate: Iterable[str] | None = None, - tex_to_color_map: dict[str, ManimColor] = None, + tex_to_color_map: dict[str, ManimColor] | None = None, tex_environment: str = "align*", **kwargs, ): @@ -281,7 +279,7 @@ def __init__( **kwargs, ) self._break_up_by_substrings() - except ValueError as compilation_error: + except ValueError: if self.brace_notation_split_occurred: logger.error( dedent( @@ -295,7 +293,7 @@ def __init__( """, ), ) - raise compilation_error + raise self.set_color_by_tex_to_color_map(self.tex_to_color_map) if self.organize_left_to_right: @@ -353,9 +351,16 @@ def _break_up_by_substrings(self): sub_tex_mob.move_to(self.submobjects[last_submob_index], RIGHT) else: sub_tex_mob.submobjects = self.submobjects[curr_index:new_index] + sub_tex_mob.note_changed_family() new_submobjects.append(sub_tex_mob) curr_index = new_index self.submobjects = new_submobjects + + # 5 hours of work went into this line + # and it's still not perfect + # July 18, 2024 + self.note_changed_family() + return self def get_parts_by_tex(self, tex, substring=True, case_sensitive=True): @@ -427,6 +432,7 @@ def index_of_part_by_tex(self, tex, **kwargs): def sort_alphabetically(self): self.submobjects.sort(key=lambda m: m.get_tex_string()) + self.note_changed_family() class Tex(MathTex): diff --git a/manim/mobject/text/text_mobject.py b/manim/mobject/text/text_mobject.py index ef14267891..3955f3dce4 100644 --- a/manim/mobject/text/text_mobject.py +++ b/manim/mobject/text/text_mobject.py @@ -69,8 +69,9 @@ def construct(self): from manim import config, logger from manim.constants import * from manim.mobject.geometry.arc import Dot +from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject from manim.mobject.svg.svg_mobject import SVGMobject -from manim.mobject.types.vectorized_mobject import VGroup, VMobject +from manim.mobject.types.vectorized_mobject import VGroup from manim.utils.color import ManimColor, ParsableManimColor, color_gradient from manim.utils.deprecation import deprecated @@ -508,7 +509,7 @@ def __init__( else: self.line_spacing = self._font_size + self._font_size * self.line_spacing - color: ManimColor = ManimColor(color) if color else VMobject().color + color: ManimColor = ManimColor(color) if color else OpenGLVMobject().color file_name = self._text2svg(color.to_hex()) PangoUtils.remove_last_M(file_name) super().__init__( @@ -524,6 +525,7 @@ def __init__( self.text = text if self.disable_ligatures: self.submobjects = [*self._gen_chars()] + self.note_changed_family() self.chars = self.get_group_class()(*self.submobjects) self.text = text_without_tabs.replace(" ", "").replace("\n", "") nppc = self.n_points_per_curve @@ -586,6 +588,11 @@ def add_line_to(end): # anti-aliasing if height is None and width is None: self.scale(TEXT_MOB_SCALE_FACTOR) + + # Just a temporary hack to get better triangulation + # See pr #1552 for details + for i in self.submobjects: + i.insert_n_curves(len(i.get_all_points())) self.initial_height = self.height def __repr__(self): @@ -856,12 +863,6 @@ def _text2svg(self, color: ManimColor): return svg_file - def init_colors(self, propagate_colors=True): - if config.renderer == RendererType.OPENGL: - super().init_colors() - elif config.renderer == RendererType.CAIRO: - super().init_colors(propagate_colors=propagate_colors) - class MarkupText(SVGMobject): r"""Display (non-LaTeX) text rendered using `Pango `_. @@ -1235,7 +1236,7 @@ def __init__( else: self.line_spacing = self._font_size + self._font_size * self.line_spacing - color: ManimColor = ManimColor(color) if color else VMobject().color + color: ManimColor = ManimColor(color) if color else OpenGLVMobject().color file_name = self._text2svg(color) PangoUtils.remove_last_M(file_name) diff --git a/manim/mobject/three_d/polyhedra.py b/manim/mobject/three_d/polyhedra.py index 8046f6066c..30a78a9c78 100644 --- a/manim/mobject/three_d/polyhedra.py +++ b/manim/mobject/three_d/polyhedra.py @@ -51,9 +51,9 @@ class Polyhedron(VGroup): .. manim:: SquarePyramidScene :save_last_frame: - class SquarePyramidScene(ThreeDScene): + class SquarePyramidScene(Scene): def construct(self): - self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES) + self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES) vertex_coords = [ [1, 1, 0], [1, -1, 0], @@ -85,9 +85,9 @@ def construct(self): .. manim:: PolyhedronSubMobjects :save_last_frame: - class PolyhedronSubMobjects(ThreeDScene): + class PolyhedronSubMobjects(Scene): def construct(self): - self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES) + self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES) octahedron = Octahedron(edge_length = 3) octahedron.graph[0].set_color(RED) octahedron.faces[2].set_color(YELLOW) @@ -174,9 +174,9 @@ class Tetrahedron(Polyhedron): .. manim:: TetrahedronScene :save_last_frame: - class TetrahedronScene(ThreeDScene): + class TetrahedronScene(Scene): def construct(self): - self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES) + self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES) obj = Tetrahedron() self.add(obj) """ @@ -209,9 +209,9 @@ class Octahedron(Polyhedron): .. manim:: OctahedronScene :save_last_frame: - class OctahedronScene(ThreeDScene): + class OctahedronScene(Scene): def construct(self): - self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES) + self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES) obj = Octahedron() self.add(obj) """ @@ -255,9 +255,9 @@ class Icosahedron(Polyhedron): .. manim:: IcosahedronScene :save_last_frame: - class IcosahedronScene(ThreeDScene): + class IcosahedronScene(Scene): def construct(self): - self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES) + self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES) obj = Icosahedron() self.add(obj) """ @@ -320,9 +320,9 @@ class Dodecahedron(Polyhedron): .. manim:: DodecahedronScene :save_last_frame: - class DodecahedronScene(ThreeDScene): + class DodecahedronScene(Scene): def construct(self): - self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES) + self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES) obj = Dodecahedron() self.add(obj) """ @@ -390,9 +390,9 @@ class ConvexHull3D(Polyhedron): :save_last_frame: :quality: high - class ConvexHull3DExample(ThreeDScene): + class ConvexHull3DExample(Scene): def construct(self): - self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES) + self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES) points = [ [ 1.93192757, 0.44134585, -1.52407061], [-0.93302521, 1.23206983, 0.64117067], diff --git a/manim/mobject/three_d/three_dimensions.py b/manim/mobject/three_d/three_dimensions.py index 7b30f9a7ad..9badb91ded 100644 --- a/manim/mobject/three_d/three_dimensions.py +++ b/manim/mobject/three_d/three_dimensions.py @@ -29,10 +29,9 @@ from manim.constants import * from manim.mobject.geometry.arc import Circle from manim.mobject.geometry.polygram import Square -from manim.mobject.mobject import * -from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL from manim.mobject.opengl.opengl_mobject import OpenGLMobject -from manim.mobject.types.vectorized_mobject import VectorizedPoint, VGroup, VMobject +from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject +from manim.mobject.types.vectorized_mobject import VectorizedPoint, VGroup from manim.utils.color import ( ManimColor, ParsableManimColor, @@ -41,12 +40,12 @@ from manim.utils.space_ops import normalize, perpendicular_bisector, z_to_vector -class ThreeDVMobject(VMobject, metaclass=ConvertToOpenGL): +class ThreeDVMobject(OpenGLVMobject): def __init__(self, shade_in_3d: bool = True, **kwargs): super().__init__(shade_in_3d=shade_in_3d, **kwargs) -class Surface(VGroup, metaclass=ConvertToOpenGL): +class Surface(VGroup): """Creates a Parametric Surface using a checkerboard pattern. Parameters @@ -82,7 +81,7 @@ class Surface(VGroup, metaclass=ConvertToOpenGL): .. manim:: ParaSurface :save_last_frame: - class ParaSurface(ThreeDScene): + class ParaSurface(Scene): def func(self, u, v): return np.array([np.cos(u) * np.cos(v), np.cos(u) * np.sin(v), u]) @@ -94,7 +93,7 @@ def construct(self): v_range=[0, TAU], resolution=8, ) - self.set_camera_orientation(theta=70 * DEGREES, phi=75 * DEGREES) + self.camera.set_orientation(theta=70 * DEGREES, phi=75 * DEGREES) self.add(axes, surface) """ @@ -110,6 +109,7 @@ def __init__( checkerboard_colors: Sequence[ParsableManimColor] | bool = [BLUE_D, BLUE_E], stroke_color: ParsableManimColor = LIGHT_GREY, stroke_width: float = 0.5, + stroke_opacity: float = 1.0, # TODO: placed temporarily to have a stroke_opacity should_make_jagged: bool = False, pre_function_handle_to_anchor_scale_factor: float = 0.00001, **kwargs: Any, @@ -120,7 +120,7 @@ def __init__( self.resolution = resolution self.surface_piece_config = surface_piece_config self.fill_color: ManimColor = ManimColor(fill_color) - self.fill_opacity = fill_opacity + self.fill_opacity = fill_opacity # TODO: remove if checkerboard_colors: self.checkerboard_colors: list[ManimColor] = [ ManimColor(x) for x in checkerboard_colors @@ -129,6 +129,7 @@ def __init__( self.checkerboard_colors = checkerboard_colors self.stroke_color: ManimColor = ManimColor(stroke_color) self.stroke_width = stroke_width + self.stroke_opacity = stroke_opacity # TODO: remove self.should_make_jagged = should_make_jagged self.pre_function_handle_to_anchor_scale_factor = ( pre_function_handle_to_anchor_scale_factor @@ -215,7 +216,7 @@ def set_fill_by_checkerboard( def set_fill_by_value( self, - axes: Mobject, + axes: OpenGLMobject, colorscale: list[ParsableManimColor] | ParsableManimColor | None = None, axis: int = 2, **kwargs, @@ -245,16 +246,18 @@ def set_fill_by_value( .. manim:: FillByValueExample :save_last_frame: - class FillByValueExample(ThreeDScene): + class FillByValueExample(Scene): def construct(self): resolution_fa = 8 - self.set_camera_orientation(phi=75 * DEGREES, theta=-160 * DEGREES) + self.camera.set_orientation(theta=-160 * DEGREES, phi=75 * DEGREES) axes = ThreeDAxes(x_range=(0, 5, 1), y_range=(0, 5, 1), z_range=(-1, 1, 0.5)) + def param_surface(u, v): x = u y = v z = np.sin(x) * np.cos(y) return z + surface_plane = Surface( lambda u, v: axes.c2p(u, v, param_surface(u, v)), resolution=(resolution_fa, resolution_fa), @@ -316,10 +319,7 @@ def param_surface(u, v): new_colors[i], color_index, ) - if config.renderer == RendererType.OPENGL: - mob.set_color(mob_color, recurse=False) - elif config.renderer == RendererType.CAIRO: - mob.set_color(mob_color, family=False) + mob.set_color(mob_color, recurse=False) break return self @@ -351,9 +351,9 @@ class Sphere(Surface): .. manim:: ExampleSphere :save_last_frame: - class ExampleSphere(ThreeDScene): + class ExampleSphere(Scene): def construct(self): - self.set_camera_orientation(phi=PI / 6, theta=PI / 6) + self.camera.set_orientation(theta=PI / 6, phi=PI / 6) sphere1 = Sphere( center=(3, 0, 0), radius=1, @@ -435,9 +435,9 @@ class Dot3D(Sphere): .. manim:: Dot3DExample :save_last_frame: - class Dot3DExample(ThreeDScene): + class Dot3DExample(Scene): def construct(self): - self.set_camera_orientation(phi=75*DEGREES, theta=-45*DEGREES) + self.camera.set_orientation(theta=-45*DEGREES, phi=75*DEGREES) axes = ThreeDAxes() dot_1 = Dot3D(point=axes.coords_to_point(0, 0, 1), color=RED) @@ -479,9 +479,9 @@ class Cube(VGroup): .. manim:: CubeExample :save_last_frame: - class CubeExample(ThreeDScene): + class CubeExample(Scene): def construct(self): - self.set_camera_orientation(phi=75*DEGREES, theta=-45*DEGREES) + self.camera.set_orientation(theta=-45*DEGREES, phi=75*DEGREES) axes = ThreeDAxes() cube = Cube(side_length=3, fill_opacity=0.7, fill_color=BLUE) @@ -535,9 +535,9 @@ class Prism(Cube): .. manim:: ExamplePrism :save_last_frame: - class ExamplePrism(ThreeDScene): + class ExamplePrism(Scene): def construct(self): - self.set_camera_orientation(phi=60 * DEGREES, theta=150 * DEGREES) + self.camera.set_orientation(theta=150 * DEGREES, phi=60 * DEGREES) prismSmall = Prism(dimensions=[1, 2, 3]).rotate(PI / 2) prismLarge = Prism(dimensions=[1.5, 3, 4.5]).move_to([2, 0, 0]) self.add(prismSmall, prismLarge) @@ -586,11 +586,11 @@ class Cone(Surface): .. manim:: ExampleCone :save_last_frame: - class ExampleCone(ThreeDScene): + class ExampleCone(Scene): def construct(self): axes = ThreeDAxes() cone = Cone(direction=X_AXIS+Y_AXIS+2*Z_AXIS, resolution=8) - self.set_camera_orientation(phi=5*PI/11, theta=PI/9) + self.camera.set_orientation(theta=PI/9, phi=5*PI/11) self.add(axes, cone) """ @@ -621,8 +621,7 @@ def __init__( self._current_phi = 0 self.base_circle = Circle( radius=base_radius, - color=self.fill_color, - fill_opacity=self.fill_opacity, + color=self.get_fill_colors(), stroke_width=0, ) self.base_circle.shift(height * IN) @@ -748,11 +747,11 @@ class Cylinder(Surface): .. manim:: ExampleCylinder :save_last_frame: - class ExampleCylinder(ThreeDScene): + class ExampleCylinder(Scene): def construct(self): axes = ThreeDAxes() cylinder = Cylinder(radius=2, height=3) - self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES) + self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES) self.add(axes, cylinder) """ @@ -803,12 +802,14 @@ def func(self, u: float, v: float) -> np.ndarray: def add_bases(self) -> None: """Adds the end caps of the cylinder.""" - if config.renderer == RendererType.OPENGL: - color = self.color - opacity = self.opacity - elif config.renderer == RendererType.CAIRO: - color = self.fill_color - opacity = self.fill_opacity + if config.renderer == RendererType.CAIRO: + # TODO: Surface should be made a separate mobject type + # (like it is for OpenGL) for the Cairo renderer too, + # to make them have the same interface. + raise NotImplementedError + + color = self.color + opacity = self.opacity self.base_top = Circle( radius=self.radius, @@ -883,7 +884,7 @@ def get_direction(self) -> np.ndarray: class Line3D(Cylinder): - """A cylindrical line, for use in ThreeDScene. + """A cylindrical line. Parameters ---------- @@ -907,11 +908,11 @@ class Line3D(Cylinder): .. manim:: ExampleLine3D :save_last_frame: - class ExampleLine3D(ThreeDScene): + class ExampleLine3D(Scene): def construct(self): axes = ThreeDAxes() line = Line3D(start=np.array([0, 0, 0]), end=np.array([2, 2, 2])) - self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES) + self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES) self.add(axes, line) """ @@ -966,8 +967,8 @@ def set_start_and_end_attrs( def pointify( self, - mob_or_point: Mobject | Point3D, - direction: Vector3D = None, + mob_or_point: OpenGLMobject | Point3D, + direction: Vector3D | None = None, ) -> np.ndarray: """Gets a point representing the center of the :class:`Mobjects <.Mobject>`. @@ -983,7 +984,7 @@ def pointify( :class:`numpy.array` Center of the :class:`Mobjects <.Mobject>` or point, or edge if direction is given. """ - if isinstance(mob_or_point, (Mobject, OpenGLMobject)): + if isinstance(mob_or_point, OpenGLMobject): mob = mob_or_point if direction is None: return mob.get_center() @@ -1043,9 +1044,9 @@ def parallel_to( .. manim:: ParallelLineExample :save_last_frame: - class ParallelLineExample(ThreeDScene): + class ParallelLineExample(Scene): def construct(self): - self.set_camera_orientation(PI / 3, -PI / 4) + self.camera.set_orientation(theta=-PI / 4, phi=PI / 3) ax = ThreeDAxes((-5, 5), (-5, 5), (-5, 5), 10, 10, 10) line1 = Line3D(RIGHT * 2, UP + OUT, color=RED) line2 = Line3D.parallel_to(line1, color=YELLOW) @@ -1091,9 +1092,9 @@ def perpendicular_to( .. manim:: PerpLineExample :save_last_frame: - class PerpLineExample(ThreeDScene): + class PerpLineExample(Scene): def construct(self): - self.set_camera_orientation(PI / 3, -PI / 4) + self.camera.set_orientation(theta=-PI / 4, phi=PI / 3) ax = ThreeDAxes((-5, 5), (-5, 5), (-5, 5), 10, 10, 10) line1 = Line3D(RIGHT * 2, UP + OUT, color=RED) line2 = Line3D.perpendicular_to(line1, color=BLUE) @@ -1139,7 +1140,7 @@ class Arrow3D(Line3D): .. manim:: ExampleArrow3D :save_last_frame: - class ExampleArrow3D(ThreeDScene): + class ExampleArrow3D(Scene): def construct(self): axes = ThreeDAxes() arrow = Arrow3D( @@ -1147,7 +1148,7 @@ def construct(self): end=np.array([2, 2, 2]), resolution=8 ) - self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES) + self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES) self.add(axes, arrow) """ @@ -1214,11 +1215,11 @@ class Torus(Surface): .. manim :: ExampleTorus :save_last_frame: - class ExampleTorus(ThreeDScene): + class ExampleTorus(Scene): def construct(self): axes = ThreeDAxes() torus = Torus() - self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES) + self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES) self.add(axes, torus) """ diff --git a/manim/mobject/types/point_cloud_mobject.py b/manim/mobject/types/point_cloud_mobject.py index c9f54e6ed2..241d9405a2 100644 --- a/manim/mobject/types/point_cloud_mobject.py +++ b/manim/mobject/types/point_cloud_mobject.py @@ -26,7 +26,7 @@ ) from ...utils.iterables import stretch_array_to_length -__all__ = ["PMobject", "Mobject1D", "Mobject2D", "PGroup", "PointCloudDot", "Point"] +__all__ = ["PMobject", "Mobject1D", "Mobject2D", "PGroup", "PointCloudDot"] if TYPE_CHECKING: from collections.abc import Callable @@ -196,6 +196,7 @@ def ingest_submobjects(self) -> Self: for attr, array in zip(attrs, arrays): setattr(self, attr, array) self.submobjects = [] + self.note_changed_family() return self def get_color(self) -> ManimColor: @@ -219,7 +220,7 @@ def align_points_with_larger(self, larger_mobject: Mobject) -> None: def get_point_mobject(self, center: Point3D | None = None) -> Point: if center is None: center = self.get_center() - return Point(center) + return PMobject().set_points([center]) def interpolate_color( self, mobject1: Mobject, mobject2: Mobject, alpha: float @@ -382,39 +383,3 @@ def generate_points(self) -> None: ] ), ) - - -class Point(PMobject): - """A mobject representing a point. - - Examples - -------- - - .. manim:: ExamplePoint - :save_last_frame: - - class ExamplePoint(Scene): - def construct(self): - colorList = [RED, GREEN, BLUE, YELLOW] - for i in range(200): - point = Point(location=[0.63 * np.random.randint(-4, 4), 0.37 * np.random.randint(-4, 4), 0], color=np.random.choice(colorList)) - self.add(point) - for i in range(200): - point = Point(location=[0.37 * np.random.randint(-4, 4), 0.63 * np.random.randint(-4, 4), 0], color=np.random.choice(colorList)) - self.add(point) - self.add(point) - """ - - def __init__( - self, location: Vector3D = ORIGIN, color: ManimColor = BLACK, **kwargs: Any - ) -> None: - self.location = location - super().__init__(color=color, **kwargs) - - def init_points(self) -> None: - self.reset_points() - self.generate_points() - self.set_points([self.location]) - - def generate_points(self) -> None: - self.add_points(np.array([self.location])) diff --git a/manim/mobject/types/vectorized_mobject.py b/manim/mobject/types/vectorized_mobject.py index a8d32682fd..c540b17db2 100644 --- a/manim/mobject/types/vectorized_mobject.py +++ b/manim/mobject/types/vectorized_mobject.py @@ -39,6 +39,7 @@ proportions_along_bezier_curve_for_point, ) from manim.utils.color import BLACK, WHITE, ManimColor, ParsableManimColor +from manim.utils.deprecation import deprecated from manim.utils.iterables import ( make_even, resize_array, @@ -138,8 +139,6 @@ def __init__( cap_style: CapStyleType = CapStyleType.AUTO, **kwargs: Any, ): - self.fill_opacity = fill_opacity - self.stroke_opacity = stroke_opacity self.stroke_width = stroke_width if background_stroke_color is not None: self.background_stroke_color: ManimColor = ManimColor( @@ -179,6 +178,11 @@ def __init__( if stroke_color is not None: self.stroke_color = ManimColor.parse(stroke_color) + if fill_opacity is not None: + self.fill_color = self.fill_color.set_opacity(fill_opacity) + if stroke_opacity is not None: + self.stroke_color = self.stroke_color.set_opacity(stroke_opacity) + def _assert_valid_submobjects(self, submobjects: Iterable[VMobject]) -> Self: return self._assert_valid_submobjects_internal(submobjects, VMobject) @@ -198,13 +202,11 @@ def get_mobject_type_class() -> type[VMobject]: def init_colors(self, propagate_colors: bool = True) -> Self: self.set_fill( color=self.fill_color, - opacity=self.fill_opacity, family=propagate_colors, ) self.set_stroke( color=self.stroke_color, width=self.stroke_width, - opacity=self.stroke_opacity, family=propagate_colors, ) self.set_background_stroke( @@ -323,10 +325,11 @@ def construct(self): if family: for submobject in self.submobjects: submobject.set_fill(color, opacity, family) - self.update_rgbas_array("fill_rgbas", color, opacity) - self.fill_rgbas: RGBA_Array_Float + + if color is not None: + self.fill_color = ManimColor.parse(color) if opacity is not None: - self.fill_opacity = opacity + self.fill_color = [c.opacity(opacity) for c in self.fill_color] return self def set_stroke( @@ -772,6 +775,18 @@ def set_points(self, points: Point3D_Array) -> Self: self.points: InternalPoint3D_Array = np.array(points) return self + def set_z(self, z: float) -> Self: + self.points[..., -1] = z + return self + + @deprecated( + since="0.18.2", + until="0.19.0", + message="OpenGL has no concept of z_index. Use set_z instead", + ) + def set_z_index(self, z: float) -> Self: + return self.set_z(z) + def resize_points( self, new_length: int, @@ -2133,11 +2148,8 @@ def __str__(self) -> str: f"submobject{'s' if len(self.submobjects) > 0 else ''}" ) - def add( - self, - *vmobjects: VMobject | Iterable[VMobject], - ) -> Self: - """Checks if all passed elements are an instance, or iterables of VMobject and then adds them to submobjects + def add(self, *vmobjects: OpenGLVMobject) -> Self: + """Checks if all passed elements are an instance of VMobject and then add them to submobjects Parameters ---------- @@ -2455,7 +2467,7 @@ def remove(self, key: Hashable) -> Self: my_dict.remove("square") """ if key not in self.submob_dict: - raise KeyError(f"The given key '{key!s}' is not present in the VDict") + raise KeyError(f"The given key {key!r} is not present in the VDict") super().remove(self.submob_dict[key]) del self.submob_dict[key] return self @@ -2660,6 +2672,7 @@ def set_location(self, new_loc: Point3D): self.set_points(np.array([new_loc])) +# TODO: Move somewhere to match opengl class CurvesAsSubmobjects(VGroup): """Convert a curve's elements to submobjects. @@ -2679,9 +2692,9 @@ def construct(self): def __init__(self, vmobject: VMobject, **kwargs) -> None: super().__init__(**kwargs) - tuples = vmobject.get_cubic_bezier_tuples() + tuples = vmobject.get_bezier_tuples() for tup in tuples: - part = VMobject() + part = OpenGLVMobject() part.set_points(tup) part.match_style(vmobject) self.add(part) diff --git a/manim/mobject/utils.py b/manim/mobject/utils.py deleted file mode 100644 index b38bb57f2e..0000000000 --- a/manim/mobject/utils.py +++ /dev/null @@ -1,107 +0,0 @@ -"""Utilities for working with mobjects.""" - -from __future__ import annotations - -__all__ = [ - "get_mobject_class", - "get_point_mobject_class", - "get_vectorized_mobject_class", -] - -from .._config import config -from ..constants import RendererType -from .mobject import Mobject -from .opengl.opengl_mobject import OpenGLMobject -from .opengl.opengl_point_cloud_mobject import OpenGLPMobject -from .opengl.opengl_vectorized_mobject import OpenGLVMobject -from .types.point_cloud_mobject import PMobject -from .types.vectorized_mobject import VMobject - - -def get_mobject_class() -> type: - """Gets the base mobject class, depending on the currently active renderer. - - .. NOTE:: - - This method is intended to be used in the code base of Manim itself - or in plugins where code should work independent of the selected - renderer. - - Examples - -------- - - The function has to be explicitly imported. We test that - the name of the returned class is one of the known mobject - base classes:: - - >>> from manim.mobject.utils import get_mobject_class - >>> get_mobject_class().__name__ in ['Mobject', 'OpenGLMobject'] - True - """ - if config.renderer == RendererType.CAIRO: - return Mobject - if config.renderer == RendererType.OPENGL: - return OpenGLMobject - raise NotImplementedError( - "Base mobjects are not implemented for the active renderer." - ) - - -def get_vectorized_mobject_class() -> type: - """Gets the vectorized mobject class, depending on the currently - active renderer. - - .. NOTE:: - - This method is intended to be used in the code base of Manim itself - or in plugins where code should work independent of the selected - renderer. - - Examples - -------- - - The function has to be explicitly imported. We test that - the name of the returned class is one of the known mobject - base classes:: - - >>> from manim.mobject.utils import get_vectorized_mobject_class - >>> get_vectorized_mobject_class().__name__ in ['VMobject', 'OpenGLVMobject'] - True - """ - if config.renderer == RendererType.CAIRO: - return VMobject - if config.renderer == RendererType.OPENGL: - return OpenGLVMobject - raise NotImplementedError( - "Vectorized mobjects are not implemented for the active renderer." - ) - - -def get_point_mobject_class() -> type: - """Gets the point cloud mobject class, depending on the currently - active renderer. - - .. NOTE:: - - This method is intended to be used in the code base of Manim itself - or in plugins where code should work independent of the selected - renderer. - - Examples - -------- - - The function has to be explicitly imported. We test that - the name of the returned class is one of the known mobject - base classes:: - - >>> from manim.mobject.utils import get_point_mobject_class - >>> get_point_mobject_class().__name__ in ['PMobject', 'OpenGLPMobject'] - True - """ - if config.renderer == RendererType.CAIRO: - return PMobject - if config.renderer == RendererType.OPENGL: - return OpenGLPMobject - raise NotImplementedError( - "Point cloud mobjects are not implemented for the active renderer." - ) diff --git a/manim/mobject/vector_field.py b/manim/mobject/vector_field.py index 28f5c6d26f..2f1dd4b345 100644 --- a/manim/mobject/vector_field.py +++ b/manim/mobject/vector_field.py @@ -27,8 +27,7 @@ from ..animation.indication import ShowPassingFlash from ..constants import OUT, RIGHT, UP, RendererType from ..mobject.mobject import Mobject -from ..mobject.types.vectorized_mobject import VGroup -from ..mobject.utils import get_vectorized_mobject_class +from ..mobject.types.vectorized_mobject import VGroup, VMobject from ..utils.bezier import interpolate, inverse_interpolate from ..utils.color import ( BLUE_E, @@ -824,7 +823,7 @@ def outside_box(p): step = max_steps if not step: continue - line = get_vectorized_mobject_class()() + line = VMobject() line.duration = step * dt step = max(1, int(len(points) / self.max_anchors_per_line)) line.set_points_smoothly(points[::step]) diff --git a/manim/opengl/__init__.py b/manim/opengl/__init__.py deleted file mode 100644 index e5bad5cd2c..0000000000 --- a/manim/opengl/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -from __future__ import annotations - -import contextlib - -with contextlib.suppress(ImportError): - from dearpygui import dearpygui as dpg - - -from manim.mobject.opengl.dot_cloud import * -from manim.mobject.opengl.opengl_image_mobject import * -from manim.mobject.opengl.opengl_mobject import * -from manim.mobject.opengl.opengl_point_cloud_mobject import * -from manim.mobject.opengl.opengl_surface import * -from manim.mobject.opengl.opengl_three_dimensions import * -from manim.mobject.opengl.opengl_vectorized_mobject import * - -from ..renderer.shader import * -from ..utils.opengl import * diff --git a/manim/plugins/__init__.py b/manim/plugins/__init__.py index d6f82f0923..a09b35cb23 100644 --- a/manim/plugins/__init__.py +++ b/manim/plugins/__init__.py @@ -4,8 +4,8 @@ from manim.plugins.plugins_flags import get_plugins, list_plugins __all__ = [ - "get_plugins", "list_plugins", + "get_plugins", ] requested_plugins: set[str] = set(config["plugins"]) @@ -13,4 +13,4 @@ if missing_plugins: - logger.warning("Missing Plugins: %s", missing_plugins) + logger.warning(f"Missing Plugins: {missing_plugins}") diff --git a/manim/plugins/plugins_flags.py b/manim/plugins/plugins_flags.py index 3080b6256a..c1a71e58bf 100644 --- a/manim/plugins/plugins_flags.py +++ b/manim/plugins/plugins_flags.py @@ -2,14 +2,9 @@ from __future__ import annotations -import sys +from importlib.metadata import entry_points from typing import Any -if sys.version_info < (3, 10): - from importlib_metadata import entry_points -else: - from importlib.metadata import entry_points - from manim._config import console __all__ = ["list_plugins"] diff --git a/tests/opengl/__init__.py b/manim/renderer/buffers/__init__.py similarity index 100% rename from tests/opengl/__init__.py rename to manim/renderer/buffers/__init__.py diff --git a/manim/renderer/buffers/buffer.py b/manim/renderer/buffers/buffer.py new file mode 100644 index 0000000000..4ee84ae100 --- /dev/null +++ b/manim/renderer/buffers/buffer.py @@ -0,0 +1,129 @@ +from __future__ import annotations + +import math + +import moderngl as gl +import numpy as np + + +class STD140BufferFormat: + _GL_DTYPES: dict[str, tuple[str, type[np.float_], tuple[int, ...]]] = { + "int": ("i", np.float32, (1,)), + "ivec2": ("i", np.float32, (2,)), + "ivec3": ("i", np.float32, (3,)), + "ivec4": ("i", np.float32, (4,)), + "uint": ("u", np.float32, (1,)), + "uvec2": ("u", np.float32, (2,)), + "uvec3": ("u", np.float32, (3,)), + "uvec4": ("u", np.float32, (4,)), + "float": ("f", np.float32, (1,)), + "vec2": ("f", np.float32, (2, 1)), + "vec3": ("f", np.float32, (3, 1)), + "vec4": ("f", np.float32, (4, 1)), + "mat2": ("f", np.float32, (2, 2)), + "mat2x3": ("f", np.float32, (2, 3)), # TODO: check order + "mat2x4": ("f", np.float32, (2, 4)), + "mat3x2": ("f", np.float32, (3, 2)), + "mat3": ("f", np.float32, (3, 3)), + "mat3x4": ("f", np.float32, (3, 4)), + "mat4x2": ("f", np.float32, (4, 2)), + "mat4x3": ("f", np.float32, (4, 3)), + "mat4": ("f", np.float32, (4, 4)), + "double": ("f", np.float64, (1,)), + "dvec2": ("f", np.float64, (2,)), + "dvec3": ("f", np.float64, (3,)), + "dvec4": ("f", np.float64, (4,)), + "dmat2": ("f", np.float64, (2, 2)), + "dmat2x3": ("f", np.float64, (2, 3)), + "dmat2x4": ("f", np.float64, (2, 4)), + "dmat3x2": ("f", np.float64, (3, 2)), + "dmat3": ("f", np.float64, (3, 3)), + "dmat3x4": ("f", np.float64, (3, 4)), + "dmat4x2": ("f", np.float64, (4, 2)), + "dmat4x3": ("f", np.float64, (4, 3)), + "dmat4": ("f", np.float64, (4, 4)), + } + + def __init__( + self, name: str, struct: tuple[tuple[str, str], ...], binding: int + ) -> None: + self.name = name + self.binding = binding + self.dtype = [] + self._paddings: dict[str, int] = {} # LUT for future writes + byte_offset = 0 # Track the offset so we can calculate padding for alignment -- NOTE: use RenderDoc to debug + for data_type, var_name in struct: + _base_char, base_bytesize, shape = self._GL_DTYPES[data_type] + shape = dict(enumerate(shape)) + col_len, row_len = shape.get(0, 1), shape.get(1, 1) + # Calculate padding for NON (float/vec2) items + col_padding = 0 if row_len == 1 and (col_len in [1, 2]) else 4 - col_len + # Store padding in LUT + self._paddings[var_name] = col_padding + shape = (col_len + col_padding,) + if row_len > 1: + shape = (row_len,) + shape + final_shape = shape + if ( + byte_offset % 16 != 0 and col_len != 1 + ): # Ensure NON (float/vec2) items are aligned to the next 16 bytes alignment + padding_for_alignment = (((16 - byte_offset) % 16) // 4,) + self.dtype.append( + (f"padding-{byte_offset}", base_bytesize, padding_for_alignment) + ) + byte_offset += padding_for_alignment[0] * 4 # padding adds to offset + self.dtype.append((var_name, base_bytesize, final_shape)) + byte_offset += math.prod( + final_shape + (base_bytesize(0).nbytes,) + ) # data adds to offset + self.data = np.zeros(1, dtype=self.dtype) + self.ubo: gl.Buffer = None + + def init_buffer(self, ctx: gl.Context): + self.ubo = ctx.buffer(self.data) + + def _write_padded(self, data: tuple | np.ndarray, var: str) -> np.ndarray: + """Automatically adds padding to data if necessary. Used internally by write + + Parameters + ---------- + data : tuple | np.ndarray + tuple or numpy array containing int/float values + var : str + the variable name used to reference the data. used in LUT to determine required padding + + Returns + ------- + np.ndarray + the same data with 0 or 1 columns of 0s appended + """ + data = np.asarray(data) + if self._paddings[var] == 0 or data.ndim == 0: + return data + + # Make a new array with extra columns of 0s + new_shape = list(data.shape) + new_shape[-1] += self._paddings[var] + padded_data = np.zeros(new_shape) + padded_data[..., : data.shape[-1]] = data + return padded_data + + def write(self, data: dict) -> None: + """Write a dictionary of key value pairs to the STD140BufferFormat's data attribute + + Parameters + ---------- + data : dict + keys/values in the dictionary must match the variable names/data shapes in the constructor + """ + if self.ubo is None: + raise BufferError("Buffer not initialized please use init_buffer first") + + for key, val in data.items(): + self.data[key] = self._write_padded(val, key) + + self.ubo.write(self.data.tobytes()) + + def bind(self): + """Binds the buffer to the specified index, this can be retrieved with the program""" + self.ubo.bind_to_uniform_block(self.binding) diff --git a/manim/renderer/cairo_renderer.py b/manim/renderer/cairo_renderer.py index 6625a49eb0..df2a0c181d 100644 --- a/manim/renderer/cairo_renderer.py +++ b/manim/renderer/cairo_renderer.py @@ -4,14 +4,12 @@ import numpy as np +from manim import config, logger +from manim.camera.camera import Camera +from manim.mobject.mobject import Mobject, _AnimationBuilder +from manim.utils.exceptions import EndSceneEarlyException from manim.utils.hashing import get_hash_from_play_call - -from .. import config, logger -from ..camera.camera import Camera -from ..mobject.mobject import Mobject, _AnimationBuilder -from ..scene.scene_file_writer import SceneFileWriter -from ..utils.exceptions import EndSceneEarlyException -from ..utils.iterables import list_update +from manim.utils.iterables import list_update if typing.TYPE_CHECKING: from typing import Any @@ -31,7 +29,7 @@ class CairoRenderer: def __init__( self, - file_writer_class=SceneFileWriter, + file_writer_class=None, camera_class=None, skip_animations=False, **kwargs, diff --git a/manim/renderer/opengl_renderer.py b/manim/renderer/opengl_renderer.py index 2f0ad398fe..6618d1e408 100644 --- a/manim/renderer/opengl_renderer.py +++ b/manim/renderer/opengl_renderer.py @@ -1,591 +1,602 @@ from __future__ import annotations -import contextlib -import itertools as it -import time -from functools import cached_property -from typing import Any - -import moderngl +import moderngl as gl import numpy as np -from PIL import Image +import manim.constants as const +import manim.utils.color.core as c +import manim.utils.color.manim_colors as color from manim import config, logger -from manim.mobject.opengl.opengl_mobject import OpenGLMobject, OpenGLPoint +from manim.camera.camera import Camera from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject -from manim.utils.caching import handle_caching_play -from manim.utils.color import color_to_rgba -from manim.utils.exceptions import EndSceneEarlyException - -from ..constants import * -from ..scene.scene_file_writer import SceneFileWriter -from ..utils import opengl -from ..utils.config_ops import _Data -from ..utils.simple_functions import clip -from ..utils.space_ops import ( - angle_of_vector, - quaternion_from_angle_axis, - quaternion_mult, - rotation_matrix_transpose, - rotation_matrix_transpose_from_quaternion, +from manim.renderer.buffers.buffer import STD140BufferFormat +from manim.renderer.opengl_shader_program import load_shader_program_by_folder +from manim.renderer.renderer import Renderer, RendererData, RendererProtocol +from manim.typing import PixelArray +from manim.utils.iterables import listify +from manim.utils.space_ops import cross2d, earclip_triangulation, z_to_vector + +ubo_camera = STD140BufferFormat( + "ubo_camera", + ( + ("vec2", "frame_shape"), + ("vec3", "camera_center"), + ("mat3", "camera_rotation"), + ("float", "focal_distance"), + ), + binding=0, ) -from .shader import Mesh, Shader -from .vectorized_mobject_rendering import ( - render_opengl_vectorized_mobject_fill, - render_opengl_vectorized_mobject_stroke, + +ubo_mobject = STD140BufferFormat( + "ubo_mobject", + ( + ("vec3", "light_source_position"), + ("float", "gloss"), + ("float", "shadow"), + ("float", "reflectiveness"), + ("float", "flat_stroke"), + ("float", "joint_type"), + ("float", "is_fixed_in_frame"), + ("float", "is_fixed_orientation"), + ("vec3", "fixed_orientation_center"), + ), + binding=1, ) -__all__ = ["OpenGLCamera", "OpenGLRenderer"] +fill_dtype = [ + ("point", np.float32, (3,)), + ("unit_normal", np.float32, (3,)), + ("color", np.float32, (4,)), + ("vert_index", np.float32, (1,)), +] +stroke_dtype = [ + ("point", np.float32, (3,)), + ("prev_point", np.float32, (3,)), + ("next_point", np.float32, (3,)), + ("stroke_width", np.float32, (1,)), + ("color", np.float32, (4,)), +] +frame_dtype = [("pos", np.float32, (2,)), ("uv", np.float32, (2,))] + + +class GLRenderData(RendererData): + def __init__(self) -> None: + super().__init__() + self.fill_rgbas = np.zeros((1, 4)) + self.stroke_rgbas = np.zeros((1, 4)) + self.stroke_widths = np.zeros((1, 1)) + self.normals = np.zeros((1, 4)) + self.orientation = np.zeros((1, 1)) + self.vert_indices = np.zeros((0, 3)) + self.bounding_box = np.zeros((3, 3)) + + def __repr__(self) -> str: + return f"""GLRenderData +fill: +{self.fill_rgbas} +stroke: +{self.stroke_rgbas} +normals: +{self.normals} +orientation: +{self.orientation} +mesh: +{self.vert_indices} +bounding_box: +{self.bounding_box} + """ -class OpenGLCamera(OpenGLMobject): - euler_angles = _Data() +def prepare_array(values: np.ndarray, desired_length: int): + """Interpolates a given list of colors to match the desired length + + Parameters + ---------- + values : np.ndarray + a 2 dimensional numpy array where values are interpolated on the y axis + desired_length : int + the desired length for the array + + Returns + ------- + np.ndarray + the interpolated array of values + """ + fill_length = len(values) + if fill_length == 1: + return np.repeat(values, desired_length, axis=0) + xm = np.linspace(0, fill_length - 1, desired_length) + rgbas = [] + for x in xm: + minimum = int(np.floor(x)) + maximum = int(np.ceil(x)) + alpha = x - minimum + if alpha == 0: + rgbas.append(values[minimum]) + continue + + val_a = values[minimum] + val_b = values[maximum] + rgbas.append(val_a * (1 - alpha) + val_b * alpha) + return np.array(rgbas) + + +class ProgramManager: + @staticmethod + def get_available_uniforms(prog): + names = [] + for name in prog: + member = prog[name] + if isinstance(member, gl.Uniform): + names.append(name) + + @staticmethod + def write_uniforms(prog, uniforms): + for name in uniforms: + if name in prog and isinstance(prog[name], gl.Uniform): + member = prog[name] + member.value = uniforms[name] + else: + logger.debug(f"The uniform {name} is not in the shader {uniforms}") - def __init__( - self, - frame_shape=None, - center_point=None, - # Theta, phi, gamma - euler_angles=[0, 0, 0], - focal_distance=2, - light_source_position=[-10, 10, 10], - orthographic=False, - minimum_polar_angle=-PI / 2, - maximum_polar_angle=PI / 2, - model_matrix=None, - **kwargs, - ): - self.use_z_index = True - self.frame_rate = 60 - self.orthographic = orthographic - self.minimum_polar_angle = minimum_polar_angle - self.maximum_polar_angle = maximum_polar_angle - if self.orthographic: - self.projection_matrix = opengl.orthographic_projection_matrix() - self.unformatted_projection_matrix = opengl.orthographic_projection_matrix( - format_=False, - ) + @staticmethod + def bind_uniform_block(program: gl.Program, ubo: STD140BufferFormat, index): + if ubo.name in program and isinstance(program[ubo.name], gl.UniformBlock): + ubo.bind() else: - self.projection_matrix = opengl.perspective_projection_matrix() - self.unformatted_projection_matrix = opengl.perspective_projection_matrix( - format_=False, - ) + raise ValueError(f"Buffer block {ubo.name} does not exist in program") - if frame_shape is None: - self.frame_shape = (config["frame_width"], config["frame_height"]) - else: - self.frame_shape = frame_shape - if center_point is None: - self.center_point = ORIGIN - else: - self.center_point = center_point +class OpenGLRenderer(Renderer, RendererProtocol): + pixel_array_dtype = np.uint8 - if model_matrix is None: - model_matrix = opengl.translation_matrix(0, 0, 11) + def __init__( + self, + pixel_width: int | None = None, + pixel_height: int | None = None, + samples: int = 4, + background_color: c.ManimColor = color.BLACK, + background_opacity: float = 1.0, + background_image: str | None = None, + ) -> None: + super().__init__() + self.pixel_width = ( + pixel_width if pixel_width is not None else config.pixel_width + ) + self.pixel_height = ( + pixel_height if pixel_height is not None else config.pixel_height + ) + self.samples = samples + if background_opacity: + background_color = background_color.opacity(background_opacity) + self.background_color = background_color.to_rgba() + self.background_image = background_image + self.rgb_max_val: float = np.iinfo(self.pixel_array_dtype).max + + # Initializing Context + logger.debug("Initializing OpenGL context and framebuffers") + self.ctx: gl.Context = gl.create_context(standalone=not config.preview) + + # Those are the actual buffers that are used for rendering + self.stencil_texture = self.ctx.texture( + (self.pixel_width, self.pixel_height), components=1, samples=0, dtype="f1" + ) + self.render_target_texture = self.ctx.texture( + (self.pixel_width, self.pixel_height), components=4, samples=0, dtype="f1" + ) + self.stencil_buffer = self.ctx.renderbuffer( + (self.pixel_width, self.pixel_height), + components=1, + samples=samples, + dtype="f1", + ) + self.color_buffer = self.ctx.renderbuffer( + (self.pixel_width, self.pixel_height), + components=4, + samples=samples, + dtype="f1", + ) + self.depth_buffer = self.ctx.depth_renderbuffer( + (self.pixel_width, self.pixel_height), samples=samples + ) - self.focal_distance = focal_distance + # Here we create different fbos that can be reused which are basically just targets to use for rendering and copy + # render_target_fbo is used for rendering it can write to color and stencil + self.render_target_fbo = self.ctx.framebuffer( + color_attachments=[self.color_buffer, self.stencil_buffer], + depth_attachment=self.depth_buffer, + ) - if light_source_position is None: - self.light_source_position = [-10, 10, 10] - else: - self.light_source_position = light_source_position - self.light_source = OpenGLPoint(self.light_source_position) - - self.default_model_matrix = model_matrix - super().__init__(model_matrix=model_matrix, should_render=False, **kwargs) - - if euler_angles is None: - euler_angles = [0, 0, 0] - euler_angles = np.array(euler_angles, dtype=float) - - self.euler_angles = euler_angles - self.refresh_rotation_matrix() - - def get_position(self): - return self.model_matrix[:, 3][:3] - - def set_position(self, position): - self.model_matrix[:, 3][:3] = position - return self - - @cached_property - def formatted_view_matrix(self): - return opengl.matrix_to_shader_input(np.linalg.inv(self.model_matrix)) - - @cached_property - def unformatted_view_matrix(self): - return np.linalg.inv(self.model_matrix) - - def init_points(self): - self.set_points([ORIGIN, LEFT, RIGHT, DOWN, UP]) - self.set_width(self.frame_shape[0], stretch=True) - self.set_height(self.frame_shape[1], stretch=True) - self.move_to(self.center_point) - - def to_default_state(self): - self.center() - self.set_height(config["frame_height"]) - self.set_width(config["frame_width"]) - self.set_euler_angles(0, 0, 0) - self.model_matrix = self.default_model_matrix - return self - - def refresh_rotation_matrix(self): - # Rotate based on camera orientation - theta, phi, gamma = self.euler_angles - quat = quaternion_mult( - quaternion_from_angle_axis(theta, OUT, axis_normalized=True), - quaternion_from_angle_axis(phi, RIGHT, axis_normalized=True), - quaternion_from_angle_axis(gamma, OUT, axis_normalized=True), + # this is used as source for stencil copy + self.stencil_buffer_fbo = self.ctx.framebuffer( + color_attachments=[self.stencil_buffer] ) - self.inverse_rotation_matrix = rotation_matrix_transpose_from_quaternion(quat) - - def rotate(self, angle, axis=OUT, **kwargs): - curr_rot_T = self.inverse_rotation_matrix - added_rot_T = rotation_matrix_transpose(angle, axis) - new_rot_T = np.dot(curr_rot_T, added_rot_T) - Fz = new_rot_T[2] - phi = np.arccos(Fz[2]) - theta = angle_of_vector(Fz[:2]) + PI / 2 - partial_rot_T = np.dot( - rotation_matrix_transpose(phi, RIGHT), - rotation_matrix_transpose(theta, OUT), + # this is used as destination for stencil copy + self.stencil_texture_fbo = self.ctx.framebuffer( + color_attachments=[self.stencil_texture] ) - gamma = angle_of_vector(np.dot(partial_rot_T, new_rot_T.T)[:, 0]) - self.set_euler_angles(theta, phi, gamma) - return self - - def set_euler_angles(self, theta=None, phi=None, gamma=None): - if theta is not None: - self.euler_angles[0] = theta - if phi is not None: - self.euler_angles[1] = phi - if gamma is not None: - self.euler_angles[2] = gamma - self.refresh_rotation_matrix() - return self - - def set_theta(self, theta): - return self.set_euler_angles(theta=theta) - - def set_phi(self, phi): - return self.set_euler_angles(phi=phi) - - def set_gamma(self, gamma): - return self.set_euler_angles(gamma=gamma) - - def increment_theta(self, dtheta): - self.euler_angles[0] += dtheta - self.refresh_rotation_matrix() - return self - - def increment_phi(self, dphi): - phi = self.euler_angles[1] - new_phi = clip(phi + dphi, -PI / 2, PI / 2) - self.euler_angles[1] = new_phi - self.refresh_rotation_matrix() - return self - - def increment_gamma(self, dgamma): - self.euler_angles[2] += dgamma - self.refresh_rotation_matrix() - return self - - def get_shape(self): - return (self.get_width(), self.get_height()) - - def get_center(self): - # Assumes first point is at the center - return self.points[0] - - def get_width(self): - points = self.points - return points[2, 0] - points[1, 0] - - def get_height(self): - points = self.points - return points[4, 1] - points[3, 1] - - def get_focal_distance(self): - return self.focal_distance * self.get_height() - - def interpolate(self, *args, **kwargs): - super().interpolate(*args, **kwargs) - self.refresh_rotation_matrix() - - -class OpenGLRenderer: - def __init__( - self, - file_writer_class: type[SceneFileWriter] = SceneFileWriter, - skip_animations: bool = False, - ) -> None: - # Measured in pixel widths, used for vector graphics - self.anti_alias_width = 1.5 - self._file_writer_class = file_writer_class - - self._original_skipping_status = skip_animations - self.skip_animations = skip_animations - self.animation_start_time = 0 - self.animation_elapsed_time = 0 - self.time = 0 - self.animations_hashes = [] - self.num_plays = 0 - - self.camera = OpenGLCamera() - self.pressed_keys = set() - - # Initialize texture map. - self.path_to_texture_id = {} - - self.background_color = config["background_color"] - - def init_scene(self, scene): - self.partial_movie_files = [] - self.file_writer: Any = self._file_writer_class( - self, - scene.__class__.__name__, + # this is used as source for copying color to the output + self.color_buffer_fbo = self.ctx.framebuffer( + color_attachments=[self.color_buffer] ) - self.scene = scene - self.background_color = config["background_color"] - if not hasattr(self, "window"): - if self.should_create_window(): - from .opengl_renderer_window import Window - - self.window = Window(self) - self.context = self.window.ctx - self.frame_buffer_object = self.context.detect_framebuffer() - else: - self.window = None - try: - self.context = moderngl.create_context(standalone=True) - except Exception: - self.context = moderngl.create_context( - standalone=True, - backend="egl", - ) - self.frame_buffer_object = self.get_frame_buffer_object(self.context, 0) - self.frame_buffer_object.use() - self.context.enable(moderngl.BLEND) - self.context.wireframe = config["enable_wireframe"] - self.context.blend_func = ( - moderngl.SRC_ALPHA, - moderngl.ONE_MINUS_SRC_ALPHA, - moderngl.ONE, - moderngl.ONE, - ) - - def should_create_window(self): - if config["force_window"]: - logger.warning( - "'--force_window' is enabled, this is intended for debugging purposes " - "and may impact performance if used when outputting files", - ) - return True - return ( - config["preview"] - and not config["save_last_frame"] - and not config["format"] - and not config["write_to_movie"] - and not config["dry_run"] + + # this is used as destination for copying the rendered target + # and using it as texture on the output_fbo + self.render_target_texture_fbo = self.ctx.framebuffer( + color_attachments=[self.render_target_texture] + ) + self.output_fbo = self.ctx.framebuffer( + color_attachments=[ + self.ctx.renderbuffer( + (self.pixel_width, self.pixel_height), dtype="f1", components=4 + ), + ] ) - def get_pixel_shape(self): - if hasattr(self, "frame_buffer_object"): - return self.frame_buffer_object.viewport[2:4] - else: - return None - - def refresh_perspective_uniforms(self, camera): - pw, ph = self.get_pixel_shape() - fw, fh = camera.get_shape() - # TODO, this should probably be a mobject uniform, with - # the camera taking care of the conversion factor - anti_alias_width = self.anti_alias_width / (ph / fh) - # Orient light - rotation = camera.inverse_rotation_matrix - light_pos = camera.light_source.get_location() - light_pos = np.dot(rotation, light_pos) - - self.perspective_uniforms = { - "frame_shape": camera.get_shape(), - "anti_alias_width": anti_alias_width, - "camera_center": tuple(camera.get_center()), - "camera_rotation": tuple(np.array(rotation).T.flatten()), - "light_source_position": tuple(light_pos), - "focal_distance": camera.get_focal_distance(), + # Preparing vmobject shader + logger.debug("Initializing Shader Programs") + self.vmobject_fill_program = load_shader_program_by_folder( + self.ctx, "quadratic_bezier_fill" + ) + self.vmobject_stroke_program = load_shader_program_by_folder( + self.ctx, "quadratic_bezier_stroke" + ) + self.render_texture_program = load_shader_program_by_folder( + self.ctx, "render_texture" + ) + + # Initializing Buffer blocks + ubo_camera.init_buffer(self.ctx) + ubo_mobject.init_buffer(self.ctx) + self.vmobject_fill_program[ubo_camera.name] = ubo_camera.binding + self.vmobject_stroke_program[ubo_camera.name] = ubo_camera.binding + self.vmobject_fill_program[ubo_mobject.name] = ubo_mobject.binding + self.vmobject_stroke_program[ubo_mobject.name] = ubo_mobject.binding + + def use_window(self) -> None: + self.output_fbo.release() + self.output_fbo = self.ctx.detect_framebuffer() + + # TODO this should also be done with the update decorators because if the camera doesn't change this is pretty rough + def init_camera(self, camera: Camera): + camera_data = { + "frame_shape": camera.get_frame_shape(), + "camera_center": camera.get_center(), + "camera_rotation": camera.get_rotation_matrix(), + "focal_distance": camera.focal_distance, } + ubo_camera.write(camera_data) + ubo_camera.bind() + + uniforms = {} + uniforms["anti_alias_width"] = 0.01977 + uniforms["light_source_position"] = (-10, 10, 10) + uniforms["pixel_shape"] = (self.pixel_width, self.pixel_height) + + # TODO: convert to singular 4x4 matrix after getting *something* to render + # self.vmobject_fill_program['view'].value = camera.get_view()? + ProgramManager.write_uniforms(self.vmobject_fill_program, uniforms) + ProgramManager.write_uniforms(self.vmobject_stroke_program, uniforms) + + def pre_render(self, camera): + self.init_camera(camera=camera) + self.ctx.clear() + self.render_target_fbo.use() + self.render_target_fbo.clear(*self.background_color) + + def post_render(self): + frame_data = np.zeros(6, dtype=frame_dtype) + frame_data["pos"] = np.array( + [[-1, -1], [-1, 1], [1, -1], [1, -1], [-1, 1], [1, 1]] + ) + frame_data["uv"] = np.array([[0, 0], [0, 1], [1, 0], [1, 0], [0, 1], [1, 1]]) + vbo = self.ctx.buffer(frame_data.tobytes()) + format_ = gl.detect_format(self.render_texture_program, frame_data.dtype.names) + vao = self.ctx.vertex_array( + program=self.render_texture_program, + content=[(vbo, format_, *frame_data.dtype.names)], # type: ignore + ) + self.ctx.copy_framebuffer(self.render_target_texture_fbo, self.color_buffer_fbo) + self.render_target_texture.use(0) + self.output_fbo.use() + vao.render(gl.TRIANGLES) + vbo.release() + vao.release() + # self.ctx.copy_framebuffer(self.output_fbo, self.color_buffer_fbo) + + def render_program(self, prog, data, indices=None): + vbo = self.ctx.buffer(data.tobytes()) + ibo = ( + self.ctx.buffer(np.asarray(indices).astype("i4").tobytes()) + if indices is not None + else None + ) + # print(prog,vbo,data) + vert_format = gl.detect_format(prog, data.dtype.names) + # print(vert_format) + vao = self.ctx.vertex_array( + program=prog, + content=[(vbo, vert_format, *data.dtype.names)], + index_buffer=ibo, + ) - def render_mobject(self, mobject): - if isinstance(mobject, OpenGLVMobject): - if config["use_projection_fill_shaders"]: - render_opengl_vectorized_mobject_fill(self, mobject) - - if config["use_projection_stroke_shaders"]: - render_opengl_vectorized_mobject_stroke(self, mobject) - - shader_wrapper_list = mobject.get_shader_wrapper_list() - - # Convert ShaderWrappers to Meshes. - for shader_wrapper in shader_wrapper_list: - shader = Shader(self.context, shader_wrapper.shader_folder) - - # Set textures. - for name, path in shader_wrapper.texture_paths.items(): - tid = self.get_texture_id(path) - shader.shader_program[name].value = tid - - # Set uniforms. - for name, value in it.chain( - shader_wrapper.uniforms.items(), - self.perspective_uniforms.items(), - ): - with contextlib.suppress(KeyError): - shader.set_uniform(name, value) - try: - shader.set_uniform( - "u_view_matrix", self.scene.camera.formatted_view_matrix - ) - shader.set_uniform( - "u_projection_matrix", - self.scene.camera.projection_matrix, - ) - except KeyError: - pass + vao.render(gl.TRIANGLES) + # data, data_size = ibo.read(), ibo.size + vbo.release() + if ibo is not None: + ibo.release() + vao.release() + # return data, data_size + + def render_image(self, mob): + raise NotImplementedError + + def render_previous(self, camera: Camera) -> None: + raise NotImplementedError + + def render_mesh(self, mob) -> None: + raise NotImplementedError + + def render_vmobject(self, mob: OpenGLVMobject) -> None: + self.stencil_buffer_fbo.use() + self.stencil_buffer_fbo.clear() + self.render_target_fbo.use() + # Setting camera uniforms + + self.ctx.enable(gl.BLEND) # type: ignore + # TODO: Because the Triangulation is messing up the normals this won't work + self.ctx.blend_func = ( # type: ignore + gl.SRC_ALPHA, + gl.ONE_MINUS_SRC_ALPHA, + gl.ONE, + gl.ONE, + ) - # Set depth test. - if shader_wrapper.depth_test: - self.context.enable(moderngl.DEPTH_TEST) + def enable_depth(sub): + if sub.depth_test: + self.ctx.enable(gl.DEPTH_TEST) # type: ignore else: - self.context.disable(moderngl.DEPTH_TEST) - - # Render. - mesh = Mesh( - shader, - shader_wrapper.vert_data, - indices=shader_wrapper.vert_indices, - use_depth_test=shader_wrapper.depth_test, - primitive=mobject.render_primitive, - ) - mesh.set_uniforms(self) - mesh.render() - - def get_texture_id(self, path): - if repr(path) not in self.path_to_texture_id: - tid = len(self.path_to_texture_id) - texture = self.context.texture( - size=path.size, - components=len(path.getbands()), - data=path.tobytes(), - ) - texture.repeat_x = False - texture.repeat_y = False - texture.filter = (moderngl.NEAREST, moderngl.NEAREST) - texture.swizzle = "RRR1" if path.mode == "L" else "RGBA" - texture.use(location=tid) - self.path_to_texture_id[repr(path)] = tid - - return self.path_to_texture_id[repr(path)] - - def update_skipping_status(self): - """ - This method is used internally to check if the current - animation needs to be skipped or not. It also checks if - the number of animations that were played correspond to - the number of animations that need to be played, and - raises an EndSceneEarlyException if they don't correspond. - """ - # there is always at least one section -> no out of bounds here - if self.file_writer.sections[-1].skip_animations: - self.skip_animations = True - if ( - config.from_animation_number > 0 - and self.num_plays < config.from_animation_number - ): - self.skip_animations = True - if ( - config.upto_animation_number >= 0 - and self.num_plays > config.upto_animation_number - ): - self.skip_animations = True - raise EndSceneEarlyException() - - @handle_caching_play - def play(self, scene, *args, **kwargs): - # TODO: Handle data locking / unlocking. - self.animation_start_time = time.time() - self.file_writer.begin_animation(not self.skip_animations) - - scene.compile_animation_data(*args, **kwargs) - scene.begin_animations() - if scene.is_current_animation_frozen_frame(): - self.update_frame(scene) - - if not self.skip_animations: - self.file_writer.write_frame( - self, num_frames=int(config.frame_rate * scene.duration) + self.ctx.disable(gl.DEPTH_TEST) # type: ignore + + for sub in mob.family_members_with_points(): + # TODO: review this renderer data optimization attempt + if True: # if sub.renderer_data is None: + # Initialize + GLVMobjectManager.init_render_data(sub) + + if not isinstance(sub.renderer_data, GLRenderData): + return + + # if mob.colors_changed: + + # mob.renderer_data.fill_rgbas = np.resize(mob.fill_color, (len(mob.renderer_data.mesh),4)) + + # if mob.points_changed:3357 + # if(mob.has_fill()): + # mob.renderer_data.mesh = ... # Triangulation todo + + family = mob.family_members_with_points() + num_mobs = len(family) + + # Another stroke pass is needed in the beginning to deal with transparency properly + for counter, sub in enumerate(family): + if not isinstance(sub.renderer_data, GLRenderData): + return + enable_depth(sub) + uniforms = {} + uniforms["index"] = (counter + 1) / num_mobs / 2 + uniforms["disable_stencil"] = float(True) + # uniforms['z_shift'] = counter/9 + 1/20 + self.ctx.copy_framebuffer(self.stencil_texture_fbo, self.stencil_buffer_fbo) + self.stencil_texture.use(0) + self.vmobject_stroke_program["stencil_texture"] = 0 + if sub.has_stroke(): + ubo_mobject.write(GLVMobjectManager.read_uniforms(sub)) + ubo_mobject.bind() + ProgramManager.write_uniforms(self.vmobject_stroke_program, uniforms) + self.render_program( + self.vmobject_stroke_program, + GLVMobjectManager.get_stroke_shader_data(sub), + np.array(range(len(sub.points))), + ) + + for counter, sub in enumerate(family): + if not isinstance(sub.renderer_data, GLRenderData): + return + enable_depth(sub) + uniforms = {} + # uniforms['z_shift'] = counter/9 + uniforms["index"] = (counter + 1) / num_mobs + # uniforms["disable_stencil"] = float(False) + self.ctx.copy_framebuffer(self.stencil_texture_fbo, self.stencil_buffer_fbo) + self.stencil_texture.use(0) + self.vmobject_fill_program["stencil_texture"] = 0 + if sub.has_fill(): + ubo_mobject.write(GLVMobjectManager.read_uniforms(sub)) + ubo_mobject.bind() + ProgramManager.write_uniforms(self.vmobject_fill_program, uniforms) + self.render_program( + self.vmobject_fill_program, + GLVMobjectManager.get_fill_shader_data(sub), + sub.renderer_data.vert_indices, ) - if self.window is not None: - self.window.swap_buffers() - while time.time() - self.animation_start_time < scene.duration: - pass - self.animation_elapsed_time = scene.duration + for counter, sub in enumerate(family): + if not isinstance(sub.renderer_data, GLRenderData): + return + enable_depth(sub) + uniforms = {} + uniforms["index"] = (counter + 1) / num_mobs + uniforms["disable_stencil"] = float(False) + # uniforms['z_shift'] = counter/9 + 1/20 + self.ctx.copy_framebuffer(self.stencil_texture_fbo, self.stencil_buffer_fbo) + self.stencil_texture.use(0) + self.vmobject_stroke_program["stencil_texture"] = 0 + if sub.has_stroke(): + ubo_mobject.write(GLVMobjectManager.read_uniforms(sub)) + ubo_mobject.bind() + ProgramManager.write_uniforms(self.vmobject_stroke_program, uniforms) + self.render_program( + self.vmobject_stroke_program, + GLVMobjectManager.get_stroke_shader_data(sub), + np.array(range(len(sub.points))), + ) + def get_pixels(self) -> PixelArray: + raw = self.output_fbo.read(components=4, dtype="f1", clamp=True) # RGBA, floats + y, x = self.output_fbo.viewport[2:4] + buf = np.frombuffer(raw, dtype=np.uint8).reshape((x, y, 4)) + # this actually has the right type (uint8) but due to + # numpy typing being bad, we have to type: ignore it + return buf[::-1] # type: ignore + + def release(self) -> None: + self.ctx.release() + self.output_fbo.release() + + +class GLVMobjectManager: + @staticmethod + def get_triangulation(smob: OpenGLVMobject, normal_vector=None): + # Figure out how to triangulate the interior to know + # how to send the points as to the vertex shader. + # First triangles come directly from the points + if normal_vector is None: + normal_vector = smob.get_unit_normal() + + points = smob.points + + if len(points) <= 1: + smob.triangulation = np.zeros(0, dtype="i4") + smob.needs_new_triangulation = False + return smob.triangulation + + if not np.isclose(normal_vector, const.OUT).all(): + # Rotate points such that unit normal vector is OUT + points = np.dot(points, z_to_vector(normal_vector)) + indices = np.arange(len(points), dtype=int) + + b0s = points[0::3] + b1s = points[1::3] + b2s = points[2::3] + v01s = b1s - b0s + v12s = b2s - b1s + + crosses = cross2d(v01s, v12s) + convexities = np.sign(crosses) + + atol = smob.tolerance_for_point_equality + end_of_loop = np.zeros(len(b0s), dtype=bool) + end_of_loop[:-1] = (np.abs(b2s[:-1] - b0s[1:]) > atol).any(1) + end_of_loop[-1] = True + + concave_parts = convexities < 0 + + # These are the vertices to which we'll apply a polygon triangulation + inner_vert_indices = np.hstack( + [ + indices[0::3], + indices[1::3][concave_parts], + indices[2::3][end_of_loop], + ], + ) + inner_vert_indices.sort() + rings = np.arange(1, len(inner_vert_indices) + 1)[inner_vert_indices % 3 == 2] + + # Triangulate + inner_verts = points[inner_vert_indices] + inner_tri_indices = inner_vert_indices[ + earclip_triangulation(inner_verts, rings) + ] # type: ignore + + tri_indices = np.hstack([indices, inner_tri_indices]) + smob.triangulation = tri_indices + smob.needs_new_triangulation = False + return tri_indices + + @staticmethod + def compute_bounding_box(mob): + all_points = np.vstack( + [ + mob.points, + *(m.get_bounding_box() for m in mob.get_family()[1:] if m.has_points()), + ], + ) + if len(all_points) == 0: + return np.zeros((3, mob.dim)) else: - scene.play_internal() - - self.file_writer.end_animation(not self.skip_animations) - self.time += scene.duration - self.num_plays += 1 - - def clear_screen(self): - self.frame_buffer_object.clear(*self.background_color) - self.window.swap_buffers() - - def render(self, scene, frame_offset, moving_mobjects): - self.update_frame(scene) - - if self.skip_animations: - return - - self.file_writer.write_frame(self) - - if self.window is not None: - self.window.swap_buffers() - while self.animation_elapsed_time < frame_offset: - self.update_frame(scene) - self.window.swap_buffers() - - def update_frame(self, scene): - self.frame_buffer_object.clear(*self.background_color) - self.refresh_perspective_uniforms(scene.camera) - - for mobject in scene.mobjects: - if not mobject.should_render: - continue - self.render_mobject(mobject) - - for obj in scene.meshes: - for mesh in obj.get_meshes(): - mesh.set_uniforms(self) - mesh.render() - - self.animation_elapsed_time = time.time() - self.animation_start_time - - def scene_finished(self, scene): - # When num_plays is 0, no images have been output, so output a single - # image in this case - if self.num_plays > 0: - self.file_writer.finish() - elif self.num_plays == 0 and config.write_to_movie: - config.write_to_movie = False - - if self.should_save_last_frame(): - config.save_last_frame = True - self.update_frame(scene) - self.file_writer.save_final_image(self.get_image()) - - def should_save_last_frame(self): - if config["save_last_frame"]: - return True - if self.scene.interactive_mode: - return False - return self.num_plays == 0 - - def get_image(self) -> Image.Image: - """Returns an image from the current frame. The first argument passed to image represents - the mode RGB with the alpha channel A. The data we read is from the currently bound frame - buffer. We pass in 'raw' as the name of the decoder, 0 and -1 args are specifically - used for the decoder tand represent the stride and orientation. 0 means there is no - padding expected between bytes and -1 represents the orientation and means the first - line of the image is the bottom line on the screen. - - Returns - ------- - PIL.Image - The PIL image of the array. - """ - raw_buffer_data = self.get_raw_frame_buffer_object_data() - image = Image.frombytes( - "RGBA", - self.get_pixel_shape(), - raw_buffer_data, - "raw", - "RGBA", - 0, - -1, + # Lower left and upper right corners + mins = all_points.min(0) + maxs = all_points.max(0) + mids = (mins + maxs) / 2 + return np.array([mins, mids, maxs]) + + @staticmethod + def init_render_data(mob: OpenGLVMobject): + logger.debug("Initializing GLRenderData") + mob.renderer_data = GLRenderData() + + # Generate Mesh + mob.renderer_data.vert_indices = GLVMobjectManager.get_triangulation(mob) + points_length = len(mob.points) + + # Generate Fill Color + fill_color = np.array([c._internal_value for c in mob.fill_color]) + stroke_color = np.array([c._internal_value for c in mob.stroke_color]) + mob.renderer_data.fill_rgbas = prepare_array(fill_color, points_length) + mob.renderer_data.stroke_rgbas = prepare_array(stroke_color, points_length) + mob.renderer_data.stroke_widths = prepare_array( + np.asarray(listify(mob.stroke_width)), points_length ) - return image - - def save_static_frame_data(self, scene, static_mobjects): - pass - - def get_frame_buffer_object(self, context, samples=0): - pixel_width = config["pixel_width"] - pixel_height = config["pixel_height"] - num_channels = 4 - return context.framebuffer( - color_attachments=context.texture( - (pixel_width, pixel_height), - components=num_channels, - samples=samples, - ), - depth_attachment=context.depth_renderbuffer( - (pixel_width, pixel_height), - samples=samples, - ), + mob.renderer_data.normals = np.repeat( + [mob.get_unit_normal()], points_length, axis=0 ) - - def get_raw_frame_buffer_object_data(self, dtype="f1"): - # Copy blocks from the fbo_msaa to the drawn fbo using Blit - # pw, ph = self.get_pixel_shape() - # gl.glBindFramebuffer(gl.GL_READ_FRAMEBUFFER, self.fbo_msaa.glo) - # gl.glBindFramebuffer(gl.GL_DRAW_FRAMEBUFFER, self.fbo.glo) - # gl.glBlitFramebuffer( - # 0, 0, pw, ph, 0, 0, pw, ph, gl.GL_COLOR_BUFFER_BIT, gl.GL_LINEAR - # ) - num_channels = 4 - ret = self.frame_buffer_object.read( - viewport=self.frame_buffer_object.viewport, - components=num_channels, - dtype=dtype, + mob.renderer_data.bounding_box = GLVMobjectManager.compute_bounding_box(mob) + + @staticmethod + def read_uniforms(mob: OpenGLVMobject): + uniforms = {} + uniforms["reflectiveness"] = mob.reflectiveness + uniforms["is_fixed_in_frame"] = float(mob.is_fixed_in_frame) + uniforms["is_fixed_orientation"] = float(mob.is_fixed_orientation) + uniforms["fixed_orientation_center"] = tuple( + [float(x) for x in mob.fixed_orientation_center] ) - return ret - - def get_frame(self): - # get current pixel values as numpy data in order to test output - raw = self.get_raw_frame_buffer_object_data(dtype="f1") - pixel_shape = self.get_pixel_shape() - result_dimensions = (pixel_shape[1], pixel_shape[0], 4) - np_buf = np.frombuffer(raw, dtype="uint8").reshape(result_dimensions) - np_buf = np.flipud(np_buf) - return np_buf - - # Returns offset from the bottom left corner in pixels. - # top_left flag should be set to True when using a GUI framework - # where the (0,0) is at the top left: e.g. PySide6 - def pixel_coords_to_space_coords(self, px, py, relative=False, top_left=False): - pixel_shape = self.get_pixel_shape() - if pixel_shape is None: - return np.array([0, 0, 0]) - pw, ph = pixel_shape - fh = config["frame_height"] - fc = self.camera.get_center() - if relative: - return 2 * np.array([px / pw, py / ph, 0]) - else: - # Only scale wrt one axis - scale = fh / ph - return fc + scale * np.array( - [(px - pw / 2), (-1 if top_left else 1) * (py - ph / 2), 0] - ) - - @property - def background_color(self): - return self._background_color - - @background_color.setter - def background_color(self, value): - self._background_color = color_to_rgba(value, 1.0) + uniforms["gloss"] = mob.gloss + uniforms["shadow"] = mob.shadow + uniforms["flat_stroke"] = float(mob.flat_stroke) + uniforms["joint_type"] = float(mob.joint_type.value) + uniforms["flat_stroke"] = float(mob.flat_stroke) + return uniforms + + @staticmethod + def get_stroke_shader_data(mob: OpenGLVMobject) -> np.ndarray: + if not isinstance(mob.renderer_data, GLRenderData): + raise TypeError() + + points = mob.points + stroke_data = np.zeros(len(points), dtype=stroke_dtype) + + nppc = mob.n_points_per_curve + stroke_data["point"] = points + stroke_data["prev_point"][:nppc] = points[-nppc:] + stroke_data["prev_point"][nppc:] = points[:-nppc] + stroke_data["next_point"][:-nppc] = points[nppc:] + stroke_data["next_point"][-nppc:] = points[:nppc] + stroke_data["color"] = mob.renderer_data.stroke_rgbas + stroke_data["stroke_width"] = mob.renderer_data.stroke_widths.reshape((-1, 1)) + + return stroke_data + + @staticmethod + def get_fill_shader_data(mob: OpenGLVMobject) -> np.ndarray: + if not isinstance(mob.renderer_data, GLRenderData): + raise TypeError() + + fill_data = np.zeros(len(mob.points), dtype=fill_dtype) + fill_data["point"] = mob.points + fill_data["color"] = mob.renderer_data.fill_rgbas + # fill_data["orientation"] = mob.renderer_data.orientation + fill_data["unit_normal"] = mob.renderer_data.normals + fill_data["vert_index"] = np.reshape(range(len(mob.points)), (-1, 1)) + return fill_data diff --git a/manim/renderer/opengl_renderer_window.py b/manim/renderer/opengl_renderer_window.py index 610f61646b..32cc0bdcf9 100644 --- a/manim/renderer/opengl_renderer_window.py +++ b/manim/renderer/opengl_renderer_window.py @@ -1,23 +1,35 @@ from __future__ import annotations +from typing import TYPE_CHECKING, TypeVar + import moderngl_window as mglw from moderngl_window.context.pyglet.window import Window as PygletWindow from moderngl_window.timers.clock import Timer from screeninfo import get_monitors -from .. import __version__, config +from manim import __version__, config +from manim.event_handler.window import WindowProtocol + +if TYPE_CHECKING: + from typing_extensions import TypeGuard + +T = TypeVar("T") __all__ = ["Window"] -class Window(PygletWindow): - fullscreen = False - resizable = True - gl_version = (3, 3) - vsync = True - cursor = True +class Window(PygletWindow, WindowProtocol): + name = "Manim Community" + fullscreen: bool = False + resizable: bool = False + gl_version: tuple[int, int] = (3, 3) + vsync: bool = True + cursor: bool = True + + def __init__(self, size=config.window_size) -> None: + # TODO: remove size argument from window init, + # move size computation below to config - def __init__(self, renderer, size=config.window_size, **kwargs): monitors = get_monitors() mon_index = config.window_monitor monitor = monitors[min(mon_index, len(monitors) - 1)] @@ -39,91 +51,49 @@ def __init__(self, renderer, size=config.window_size, **kwargs): size = tuple(size) super().__init__(size=size) - + self.pressed_keys = set() self.title = f"Manim Community {__version__}" self.size = size - self.renderer = renderer mglw.activate_context(window=self) self.timer = Timer() self.config = mglw.WindowConfig(ctx=self.ctx, wnd=self, timer=self.timer) self.timer.start() - self.swap_buffers() - - initial_position = self.find_initial_position(size, monitor) + # No idea why, but when self.position is set once + # it sometimes doesn't actually change the position + # to the specified tuple on the rhs, but doing it + # twice seems to make it work. ¯\_(ツ)_/¯ + initial_position = self.find_initial_position(size) + self.position = initial_position self.position = initial_position - # Delegate event handling to scene. - def on_mouse_motion(self, x, y, dx, dy): - super().on_mouse_motion(x, y, dx, dy) - point = self.renderer.pixel_coords_to_space_coords(x, y) - d_point = self.renderer.pixel_coords_to_space_coords(dx, dy, relative=True) - self.renderer.scene.on_mouse_motion(point, d_point) - - def on_mouse_scroll(self, x, y, x_offset: float, y_offset: float): - super().on_mouse_scroll(x, y, x_offset, y_offset) - point = self.renderer.pixel_coords_to_space_coords(x, y) - offset = self.renderer.pixel_coords_to_space_coords( - x_offset, - y_offset, - relative=True, - ) - self.renderer.scene.on_mouse_scroll(point, offset) - - def on_key_press(self, symbol, modifiers): - self.renderer.pressed_keys.add(symbol) - super().on_key_press(symbol, modifiers) - self.renderer.scene.on_key_press(symbol, modifiers) - - def on_key_release(self, symbol, modifiers): - if symbol in self.renderer.pressed_keys: - self.renderer.pressed_keys.remove(symbol) - super().on_key_release(symbol, modifiers) - self.renderer.scene.on_key_release(symbol, modifiers) - - def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers): - super().on_mouse_drag(x, y, dx, dy, buttons, modifiers) - point = self.renderer.pixel_coords_to_space_coords(x, y) - d_point = self.renderer.pixel_coords_to_space_coords(dx, dy, relative=True) - self.renderer.scene.on_mouse_drag(point, d_point, buttons, modifiers) - - def find_initial_position(self, size, monitor): - custom_position = config.window_position + def find_initial_position(self, size: tuple[int, int]) -> tuple[int, int]: + custom_position = config.window_position.replace(" ", "").upper() + monitors = get_monitors() + mon_index = config.window_monitor + monitor = monitors[min(mon_index, len(monitors) - 1)] window_width, window_height = size + # Position might be specified with a string of the form # x,y for integers x and y - if len(custom_position) == 1: - raise ValueError( - "window_position must specify both Y and X positions (Y/X -> UR). Also accepts LEFT/RIGHT/ORIGIN/UP/DOWN.", - ) - # in the form Y/X (UR) - if custom_position in ["LEFT", "RIGHT"]: - custom_position = "O" + custom_position[0] - elif custom_position in ["UP", "DOWN"]: - custom_position = custom_position[0] + "O" - elif custom_position == "ORIGIN": - custom_position = "O" * 2 - elif "," in custom_position: - return tuple(map(int, custom_position.split(","))) + if "," in custom_position: + pos = tuple(int(p) for p in custom_position.split(",")) + if tuple_len_2(pos): + return pos + else: + raise ValueError("Expected position in the form x,y") # Alternatively, it might be specified with a string like # UR, OO, DL, etc. specifying what corner it should go to char_to_n = {"L": 0, "U": 0, "O": 1, "R": 2, "D": 2} width_diff = monitor.width - window_width height_diff = monitor.height - window_height - return ( monitor.x + char_to_n[custom_position[1]] * width_diff // 2, -monitor.y + char_to_n[custom_position[0]] * height_diff // 2, ) - def on_mouse_press(self, x, y, button, modifiers): - super().on_mouse_press(x, y, button, modifiers) - point = self.renderer.pixel_coords_to_space_coords(x, y) - mouse_button_map = { - 1: "LEFT", - 2: "MOUSE", - 4: "RIGHT", - } - self.renderer.scene.on_mouse_press(point, mouse_button_map[button], modifiers) + +def tuple_len_2(pos: tuple[T, ...]) -> TypeGuard[tuple[T, T]]: + return len(pos) == 2 diff --git a/manim/renderer/opengl_shader_program.py b/manim/renderer/opengl_shader_program.py new file mode 100644 index 0000000000..fc61e97bf1 --- /dev/null +++ b/manim/renderer/opengl_shader_program.py @@ -0,0 +1,94 @@ +# For caching +from __future__ import annotations + +import re +from functools import lru_cache +from pathlib import Path + +import moderngl as gl + +from manim._config import logger + +filename_to_code_map: dict = {} + + +def get_shader_dir(): + return Path(__file__).parent / "shaders" + + +def find_file(file_name: Path, directories: list[Path]) -> Path: + # Check if what was passed in is already a valid path to a file + if file_name.exists(): + return file_name + possible_paths = (directory / file_name for directory in directories) + for path in possible_paths: + logger.debug(f"Searching for {file_name} in {path}") + if path.exists(): + return path + else: + logger.debug(f"shader_wrapper.py::find_file() : {path} does not exist.") + raise OSError(f"{file_name} not Found") + + +@lru_cache(maxsize=12) +def get_shader_code_from_file(filename: Path) -> str | None: + if filename in filename_to_code_map: + return filename_to_code_map[filename] + try: + filepath = find_file( + filename, + directories=[get_shader_dir(), Path("/")], + ) + except OSError: + logger.warning(f"Could not find shader file {filename}") + return None + + result = filepath.read_text() + + # To share functionality between shaders, some functions are read in + # from other files an inserted into the relevant strings before + # passing to ctx.program for compiling + # Replace "#include" lines with relevant code + insertions = re.findall( + r"^#include.?\".*\"", + result, + flags=re.MULTILINE, + ) + for line in insertions: + include_path = line.strip().replace("#include", "") + include_path = include_path.replace('"', "") + path = (filepath.parent / Path(include_path.strip())).resolve() + logger.debug(f"Trying to get code from: {path} to include in {filepath.name}") + inserted_code = get_shader_code_from_file( + path, + ) + if inserted_code is None: + return None + + result = result.replace( + line, + f"// Start include of: {include_path}\n\n{inserted_code}\n\n// End include of: {include_path}", + ) + filename_to_code_map[filename] = result + return result + + +def load_shader_program_by_folder(ctx: gl.Context, folder_name: str): + vertex_code = get_shader_code_from_file(Path(folder_name + "/vert.glsl")) + geometry_code = get_shader_code_from_file(Path(folder_name + "/geom.glsl")) + fragment_code = get_shader_code_from_file(Path(folder_name + "/frag.glsl")) + # print(folder_name) + # for i,l in enumerate(geometry_code.splitlines()): + # print(str(i) + ":" + l ) + if vertex_code is None or fragment_code is None: + logger.error( + f"Invalid program definition for {folder_name} vertex or fragment shader not present" + ) + raise RuntimeError("Loading Shader Program Error") + if geometry_code is None: + return ctx.program(vertex_shader=vertex_code, fragment_shader=fragment_code) + return ctx.program( + vertex_shader=vertex_code, + geometry_shader=geometry_code, + fragment_shader=fragment_code, + ) diff --git a/manim/renderer/renderer.py b/manim/renderer/renderer.py new file mode 100644 index 0000000000..b17f38f1e9 --- /dev/null +++ b/manim/renderer/renderer.py @@ -0,0 +1,138 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, Protocol, runtime_checkable + +from manim._config import logger +from manim.mobject.mobject import InvisibleMobject +from manim.mobject.opengl.opengl_mobject import OpenGLMobject +from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject +from manim.mobject.types.image_mobject import ImageMobject + +if TYPE_CHECKING: + from manim.camera.camera import Camera + from manim.scene.scene import SceneState + from manim.typing import PixelArray + + +class RendererData: + pass + + +class Renderer(ABC): + """Abstract class that handles dispatching mobjects to their specialized mobjects. + + Specifically, it maps :class:`.OpenGLVMobject` to :meth:`render_vmobject`, :class:`.ImageMobject` + to :meth:`render_image`, etc. + """ + + def __init__(self): + self.capabilities = [ + (OpenGLVMobject, self.render_vmobject), + (ImageMobject, self.render_image), + ] + self._unsupported_mob_warnings: set[str] = set() + + def render(self, state: SceneState) -> None: + self.pre_render(state.camera) + for mob in state.mobjects: + self.render_mobject(mob) + self.post_render() + + def render_mobject(self, mob: OpenGLMobject) -> None: + for mob_cls, render_func in self.capabilities: + if isinstance(mob, mob_cls): + render_func(mob) + break + else: + if not isinstance(mob, InvisibleMobject): + warning_message = ( + f"{type(mob).__name__} cannot be rendered by {type(self).__name__}. " + "Attempting to render its submobjects..." + ) + # This prevents spamming the console. + if warning_message not in self._unsupported_mob_warnings: + logger.warning(warning_message) + self._unsupported_mob_warnings.add(warning_message) + for submob in mob.submobjects: + self.render_mobject(submob) + + @abstractmethod + def pre_render(self, camera: Camera) -> None: + """Actions before rendering any :class:`.OpenGLMobject`""" + + @abstractmethod + def post_render(self) -> None: + """Actions before rendering any :class:`.OpenGLMobject`""" + + @abstractmethod + def render_vmobject(self, mob: OpenGLVMobject) -> None: + raise NotImplementedError + + @abstractmethod + def render_image(self, mob: ImageMobject) -> None: + raise NotImplementedError + + +# Note: runtime checking is slow, +# but it only happens once or twice so it should be fine +@runtime_checkable +class RendererProtocol(Protocol): + """The Protocol a renderer must implement to be used in :class:`.Manager`.""" + + def render(self, state: SceneState) -> None: + """Render a group of Mobjects""" + ... + + def use_window(self) -> None: + """Hook called after instantiation.""" + ... + + def get_pixels(self) -> PixelArray: + """Get the pixels that should be written to a file.""" + ... + + def release(self) -> None: + """Release any resources the renderer is holding.""" + ... + + +# NOTE: The user should expect depth between renderers not to be handled discussed at 03.09.2023 Between jsonv and MrDiver +# NOTE: Cairo_camera overlay_PIL_image for MultiRenderer + +# class Compositor: +# def __init__(self): +# self.renderers = [] + +# def add_capability(self, renderer) -> None: +# self.renderers.append(renderer) + +# def add(img1, img2): +# raise NotImplementedError + +# def subtract(*images: List[PixelArray]): +# raise NotImplementedError + +# def mix(): +# raise NotImplementedError + +# def multiply(): +# raise NotImplementedError + +# def divide(): +# raise NotImplementedError + + +# class GraphScene(Scene): +# def construct(self): +# config.renderer = + +# class VolumetricScene(Scene): +# def construct(self): +# pass + +# compositor = Compositor() +# compositor.add_capability(GraphScene, OpenGL) # no file writing +# compositor.add_capability(VolumetricScene, Blender, ) # 3 sec +# compositor.addPostFX(CustomShader) +# compositor.render() diff --git a/manim/renderer/shader_wrapper.py b/manim/renderer/shader_wrapper.py index 8a2b0d1fbe..90548dd332 100644 --- a/manim/renderer/shader_wrapper.py +++ b/manim/renderer/shader_wrapper.py @@ -3,11 +3,14 @@ import copy import logging import re +from functools import lru_cache from pathlib import Path import moderngl import numpy as np +from manim.utils.iterables import resize_array + # Mobjects that should be rendered with # the same shader will be organized and # clumped together based on keeping track @@ -29,10 +32,11 @@ def find_file(file_name: Path, directories: list[Path]) -> Path: return file_name possible_paths = (directory / file_name for directory in directories) for path in possible_paths: + logger.debug(f"Searching for {file_name} in {path}") if path.exists(): return path else: - logger.debug(f"{path} does not exist.") + logger.debug(f"shader_wrapper.py::find_file() : {path} does not exist.") raise OSError(f"{file_name} not Found") @@ -58,6 +62,29 @@ def __init__( self.init_program_code() self.refresh_id() + def __eq__(self, shader_wrapper: object): + if not isinstance(shader_wrapper, ShaderWrapper): + raise TypeError( + f"Cannot compare ShaderWrapper with non-ShaderWrapper object of type {type(shader_wrapper)}" + ) + return all( + ( + np.all(self.vert_data == shader_wrapper.vert_data), + np.all(self.vert_indices == shader_wrapper.vert_indices), + self.shader_folder == shader_wrapper.shader_folder, + all( + np.all(self.uniforms[key] == shader_wrapper.uniforms[key]) + for key in self.uniforms + ), + all( + self.texture_paths[key] == shader_wrapper.texture_paths[key] + for key in self.texture_paths + ), + self.depth_test == shader_wrapper.depth_test, + self.render_primitive == shader_wrapper.render_primitive, + ) + ) + def copy(self): result = copy.copy(self) result.vert_data = np.array(self.vert_data) @@ -113,9 +140,14 @@ def create_program_id(self): def init_program_code(self): def get_code(name: str) -> str | None: - return get_shader_code_from_file( - self.shader_folder / f"{name}.glsl", - ) + path = self.shader_folder / f"{name}.glsl" + logger.debug(f"Reading {name}.glsl shader code from {path.absolute()}") + code = get_shader_code_from_file(path) + if code is not None: + logger.debug( + f"=============================================\n{code}\n=============================================" + ) + return code self.program_code = { "vertex_shader": get_code("vert"), @@ -134,24 +166,28 @@ def replace_code(self, old, new): code_map[name] = re.sub(old, new, code_map[name]) self.refresh_id() - def combine_with(self, *shader_wrappers): - # Assume they are of the same type - if len(shader_wrappers) == 0: - return + def combine_with(self, *shader_wrappers: ShaderWrapper) -> ShaderWrapper: + self.read_in(self.copy(), *shader_wrappers) + return self + + def read_in(self, *shader_wrappers: ShaderWrapper) -> ShaderWrapper: + # Assume all are of the same type + total_len = sum(len(sw.vert_data) for sw in shader_wrappers) + self.vert_data = resize_array(self.vert_data, total_len) if self.vert_indices is not None: - num_verts = len(self.vert_data) - indices_list = [self.vert_indices] - data_list = [self.vert_data] - for sw in shader_wrappers: - indices_list.append(sw.vert_indices + num_verts) - data_list.append(sw.vert_data) - num_verts += len(sw.vert_data) - self.vert_indices = np.hstack(indices_list) - self.vert_data = np.hstack(data_list) - else: - self.vert_data = np.hstack( - [self.vert_data, *(sw.vert_data for sw in shader_wrappers)], - ) + total_verts = sum(len(sw.vert_indices) for sw in shader_wrappers) + self.vert_indices = resize_array(self.vert_indices, total_verts) + + n_points = 0 + n_verts = 0 + for sw in shader_wrappers: + new_n_points = n_points + len(sw.vert_data) + self.vert_data[n_points:new_n_points] = sw.vert_data + if self.vert_indices is not None and sw.vert_indices is not None: + new_n_verts = n_verts + len(sw.vert_indices) + self.vert_indices[n_verts:new_n_verts] = sw.vert_indices + n_points + n_verts = new_n_verts + n_points = new_n_points return self @@ -159,16 +195,17 @@ def combine_with(self, *shader_wrappers): filename_to_code_map: dict = {} +@lru_cache(maxsize=12) def get_shader_code_from_file(filename: Path) -> str | None: if filename in filename_to_code_map: return filename_to_code_map[filename] - try: filepath = find_file( filename, directories=[get_shader_dir(), Path("/")], ) except OSError: + logger.warning(f"Could not find shader file {filename.absolute()}") return None result = filepath.read_text() @@ -178,17 +215,25 @@ def get_shader_code_from_file(filename: Path) -> str | None: # passing to ctx.program for compiling # Replace "#INSERT " lines with relevant code insertions = re.findall( - r"^#include ../include/.*\.glsl$", + r"^#include.*", result, flags=re.MULTILINE, ) for line in insertions: + include_path = line.strip().replace("#include", "") + include_path = include_path.replace('"', "") + path = (filepath.parent / Path(include_path.strip())).resolve() + logger.debug(f"Trying to get code from: {path} to include in {filepath.name}") inserted_code = get_shader_code_from_file( - Path() / "include" / line.replace("#include ../include/", ""), + path, ) if inserted_code is None: return None - result = result.replace(line, inserted_code) + + result = result.replace( + line, + f"// Start include of: {include_path}\n\n{inserted_code}\n\n// End include of: {include_path}", + ) filename_to_code_map[filename] = result return result diff --git a/manim/renderer/shaders/default/frag.glsl b/manim/renderer/shaders/default/frag.glsl index 2a721a51ea..cf8bedde45 100644 --- a/manim/renderer/shaders/default/frag.glsl +++ b/manim/renderer/shaders/default/frag.glsl @@ -3,6 +3,7 @@ uniform vec4 u_color; out vec4 frag_color; -void main() { - frag_color = u_color; +void main() +{ + frag_color = u_color; } diff --git a/manim/renderer/shaders/default/vert.glsl b/manim/renderer/shaders/default/vert.glsl index 4d69971c50..149621def8 100644 --- a/manim/renderer/shaders/default/vert.glsl +++ b/manim/renderer/shaders/default/vert.glsl @@ -5,6 +5,7 @@ uniform mat4 u_model_matrix; uniform mat4 u_view_matrix; uniform mat4 u_projection_matrix; -void main() { +void main() +{ gl_Position = u_projection_matrix * u_view_matrix * u_model_matrix * vec4(in_vert, 1.0); } diff --git a/manim/renderer/shaders/design.frag b/manim/renderer/shaders/design.frag deleted file mode 100644 index e84e3984af..0000000000 --- a/manim/renderer/shaders/design.frag +++ /dev/null @@ -1,15 +0,0 @@ -#version 330 - -out vec4 frag_color; - -void main() { - vec2 st = gl_FragCoord.xy / vec2(854, 360); - vec3 color = vec3(0.0); - - st *= 3.0; - st = fract(st); - - color = vec3(st, 0.0); - - frag_color = vec4(color, 1.0); -} diff --git a/manim/renderer/shaders/design_2.frag b/manim/renderer/shaders/design_2.frag deleted file mode 100644 index 02c91490d0..0000000000 --- a/manim/renderer/shaders/design_2.frag +++ /dev/null @@ -1,45 +0,0 @@ -#version 330 - -uniform vec2 u_resolution; -out vec4 frag_color; - - -#define PI 3.14159265358979323846 - -vec2 rotate2D(vec2 _st, float _angle){ - _st -= 0.5; - _st = mat2(cos(_angle),-sin(_angle), - sin(_angle),cos(_angle)) * _st; - _st += 0.5; - return _st; -} - -vec2 tile(vec2 _st, float _zoom){ - _st *= _zoom; - return fract(_st); -} - -float box(vec2 _st, vec2 _size, float _smoothEdges){ - _size = vec2(0.5)-_size*0.5; - vec2 aa = vec2(_smoothEdges*0.5); - vec2 uv = smoothstep(_size,_size+aa,_st); - uv *= smoothstep(_size,_size+aa,vec2(1.0)-_st); - return uv.x*uv.y; -} - -void main(void){ - vec2 st = gl_FragCoord.xy/u_resolution.xy; - vec3 color = vec3(0.0); - - // Divide the space in 4 - st = tile(st,4.); - - // Use a matrix to rotate the space 45 degrees - st = rotate2D(st,PI*0.25); - - // Draw a square - color = vec3(box(st,vec2(0.7),0.01)); - // color = vec3(st,0.0); - - frag_color = vec4(color,1.0); -} diff --git a/manim/renderer/shaders/design_3.frag b/manim/renderer/shaders/design_3.frag deleted file mode 100644 index 6a0e601c70..0000000000 --- a/manim/renderer/shaders/design_3.frag +++ /dev/null @@ -1,59 +0,0 @@ -#version 330 - -uniform vec3 u_resolution; -uniform float u_time; -out vec4 frag_color; - -vec3 palette(float d){ - return mix(vec3(0.2,0.7,0.9),vec3(1.,0.,1.),d); -} - -vec2 rotate(vec2 p,float a){ - float c = cos(a); - float s = sin(a); - return p*mat2(c,s,-s,c); -} - -float map(vec3 p){ - for( int i = 0; i<8; ++i){ - float t = u_time*0.1; - p.xz =rotate(p.xz,t); - p.xy =rotate(p.xy,t*1.89); - p.xz = abs(p.xz); - p.xz-=.5; - } - return dot(sign(p),p)/5.; -} - -vec4 rm (vec3 ro, vec3 rd){ - float t = 0.; - vec3 col = vec3(0.); - float d; - for(float i =0.; i<64.; i++){ - vec3 p = ro + rd*t; - d = map(p)*.5; - if(d<0.02){ - break; - } - if(d>100.){ - break; - } - //col+=vec3(0.6,0.8,0.8)/(400.*(d)); - col+=palette(length(p)*.1)/(400.*(d)); - t+=d; - } - return vec4(col,1./(d*100.)); -} - -void main(void){ - vec2 uv = (gl_FragCoord.xy-(u_resolution.xy/2.))/u_resolution.x; - vec3 ro = vec3(0.,0.,-50.); - ro.xz = rotate(ro.xz,u_time); - vec3 cf = normalize(-ro); - vec3 cs = normalize(cross(cf,vec3(0.,1.,0.))); - vec3 cu = normalize(cross(cf,cs)); - vec3 uuv = ro+cf*3. + uv.x*cs + uv.y*cu; - vec3 rd = normalize(uuv-ro); - vec4 col = rm(ro,rd); - frag_color = vec4(col.xyz, 1); -} diff --git a/manim/renderer/shaders/image/frag.glsl b/manim/renderer/shaders/image/frag.glsl deleted file mode 100644 index fe6cbd4ca2..0000000000 --- a/manim/renderer/shaders/image/frag.glsl +++ /dev/null @@ -1,13 +0,0 @@ -#version 330 - -uniform sampler2D Texture; - -in vec2 v_im_coords; -in float v_opacity; - -out vec4 frag_color; - -void main() { - frag_color = texture(Texture, v_im_coords); - frag_color.a = v_opacity; -} diff --git a/manim/renderer/shaders/image/vert.glsl b/manim/renderer/shaders/image/vert.glsl deleted file mode 100644 index aaaae6bff7..0000000000 --- a/manim/renderer/shaders/image/vert.glsl +++ /dev/null @@ -1,22 +0,0 @@ -#version 330 - -#include ../include/camera_uniform_declarations.glsl - -uniform sampler2D Texture; - -in vec3 point; -in vec2 im_coords; -in float opacity; - -out vec2 v_im_coords; -out float v_opacity; - -// Analog of import for manim only -#include ../include/get_gl_Position.glsl -#include ../include/position_point_into_frame.glsl - -void main(){ - v_im_coords = im_coords; - v_opacity = opacity; - gl_Position = get_gl_Position(position_point_into_frame(point)); -} diff --git a/manim/renderer/shaders/include/NOTE.md b/manim/renderer/shaders/include/NOTE.md index 5aada430b8..e259243482 100644 --- a/manim/renderer/shaders/include/NOTE.md +++ b/manim/renderer/shaders/include/NOTE.md @@ -1,6 +1,6 @@ There seems to be no analog to #include in C++ for OpenGL shaders. While there are other options for sharing code between shaders, a lot of them aren't great, especially if the goal is to have all the logic for which specific bits of code to share handled in the shader file itself. So the way manim currently works is to replace any line which looks like -#INSERT +#include "" with the code from one of the files in this folder. diff --git a/manim/renderer/shaders/include/add_light.glsl b/manim/renderer/shaders/include/add_light.glsl deleted file mode 100644 index cc62919944..0000000000 --- a/manim/renderer/shaders/include/add_light.glsl +++ /dev/null @@ -1,43 +0,0 @@ -///// INSERT COLOR_MAP FUNCTION HERE ///// - -vec4 add_light(vec4 color, - vec3 point, - vec3 unit_normal, - vec3 light_coords, - float gloss, - float shadow){ - ///// INSERT COLOR FUNCTION HERE ///// - // The line above may be replaced by arbitrary code snippets, as per - // the method Mobject.set_color_by_code - if(gloss == 0.0 && shadow == 0.0) return color; - - // TODO, do we actually want this? It effectively treats surfaces as two-sided - if(unit_normal.z < 0){ - unit_normal *= -1; - } - - // TODO, read this in as a uniform? - float camera_distance = 6; - // Assume everything has already been rotated such that camera is in the z-direction - vec3 to_camera = vec3(0, 0, camera_distance) - point; - vec3 to_light = light_coords - point; - vec3 light_reflection = -to_light + 2 * unit_normal * dot(to_light, unit_normal); - float dot_prod = dot(normalize(light_reflection), normalize(to_camera)); - float shine = gloss * exp(-3 * pow(1 - dot_prod, 2)); - float dp2 = dot(normalize(to_light), unit_normal); - float darkening = mix(1, max(dp2, 0), shadow); - return vec4( - darkening * mix(color.rgb, vec3(1.0), shine), - color.a - ); -} - -vec4 finalize_color(vec4 color, - vec3 point, - vec3 unit_normal, - vec3 light_coords, - float gloss, - float shadow){ - // Put insertion here instead - return add_light(color, point, unit_normal, light_coords, gloss, shadow); -} diff --git a/manim/renderer/shaders/include/camera_uniform_declarations.glsl b/manim/renderer/shaders/include/camera_uniform_declarations.glsl index fea450300f..b311abef2d 100644 --- a/manim/renderer/shaders/include/camera_uniform_declarations.glsl +++ b/manim/renderer/shaders/include/camera_uniform_declarations.glsl @@ -1,8 +1,11 @@ -uniform vec2 frame_shape; -uniform float anti_alias_width; -uniform vec3 camera_center; -uniform mat3 camera_rotation; -uniform float is_fixed_in_frame; -uniform float is_fixed_orientation; -uniform vec3 fixed_orientation_center; -uniform float focal_distance; +#ifndef CAMERA_GLSL +#define CAMERA_GLSL +layout(std140) uniform ubo_camera +{ + // mat4 u_projection_view_matrix; # TODO: convert to mat4 instead of the following... + vec2 frame_shape; + vec3 camera_center; + mat3 camera_rotation; + float focal_distance; +}; +#endif // CAMERA_GLSL diff --git a/manim/renderer/shaders/include/complex_functions.glsl b/manim/renderer/shaders/include/complex_functions.glsl new file mode 100644 index 0000000000..7461135416 --- /dev/null +++ b/manim/renderer/shaders/include/complex_functions.glsl @@ -0,0 +1,23 @@ +#ifndef COMPLEX_FUNC_GLSL +#define COMPLEX_FUNC_GLSL + +vec2 complex_mult(vec2 z, vec2 w) +{ + return vec2(z.x * w.x - z.y * w.y, z.x * w.y + z.y * w.x); +} + +vec2 complex_div(vec2 z, vec2 w) +{ + return complex_mult(z, vec2(w.x, -w.y)) / (w.x * w.x + w.y * w.y); +} + +vec2 complex_pow(vec2 z, int n) +{ + vec2 result = vec2(1.0, 0.0); + for (int i = 0; i < n; i++) + { + result = complex_mult(result, z); + } + return result; +} +#endif // COMPLEX_FUNC_GLSL diff --git a/manim/renderer/shaders/include/finalize_color.glsl b/manim/renderer/shaders/include/finalize_color.glsl index 26b10376a2..5d19c58de2 100644 --- a/manim/renderer/shaders/include/finalize_color.glsl +++ b/manim/renderer/shaders/include/finalize_color.glsl @@ -1,25 +1,23 @@ -vec3 float_to_color(float value, float min_val, float max_val, vec3[9] colormap_data){ +#ifndef FINALIZE_COLOR_GLSL +#define FINALIZE_COLOR_GLSL + +vec3 float_to_color(float value, float min_val, float max_val, vec3[9] colormap_data) +{ float alpha = clamp((value - min_val) / (max_val - min_val), 0.0, 1.0); int disc_alpha = min(int(alpha * 8), 7); - return mix( - colormap_data[disc_alpha], - colormap_data[disc_alpha + 1], - 8.0 * alpha - disc_alpha - ); + return mix(colormap_data[disc_alpha], colormap_data[disc_alpha + 1], 8.0 * alpha - disc_alpha); } - -vec4 add_light(vec4 color, - vec3 point, - vec3 unit_normal, - vec3 light_coords, - float gloss, - float shadow){ - if(gloss == 0.0 && shadow == 0.0) return color; +vec4 add_light(vec4 color, vec3 point, vec3 unit_normal, vec3 light_coords, float gloss, float shadow, + float reflectiveness) +{ + if (gloss == 0.0 && shadow == 0.0 && reflectiveness == 0.0) + return color; // TODO, do we actually want this? It effectively treats surfaces as two-sided - if(unit_normal.z < 0){ - unit_normal *= -1; + if (unit_normal.z < 0) + { + unit_normal *= -1; } // TODO, read this in as a uniform? @@ -32,20 +30,15 @@ vec4 add_light(vec4 color, float shine = gloss * exp(-3 * pow(1 - dot_prod, 2)); float dp2 = dot(normalize(to_light), unit_normal); float darkening = mix(1, max(dp2, 0), shadow); - return vec4( - darkening * mix(color.rgb, vec3(1.0), shine), - color.a - ); + return vec4(darkening * mix(color.rgb, vec3(1.0), shine), color.a); } -vec4 finalize_color(vec4 color, - vec3 point, - vec3 unit_normal, - vec3 light_coords, - float gloss, - float shadow){ +vec4 finalize_color(vec4 color, vec3 point, vec3 unit_normal, vec3 light_coords, float gloss, float shadow, + float reflectiveness) +{ ///// INSERT COLOR FUNCTION HERE ///// // The line above may be replaced by arbitrary code snippets, as per // the method Mobject.set_color_by_code - return add_light(color, point, unit_normal, light_coords, gloss, shadow); + return add_light(color, point, unit_normal, light_coords, gloss, shadow, reflectiveness); } +#endif // FINALIZE_COLOR_GLSL diff --git a/manim/renderer/shaders/include/get_gl_Position.glsl b/manim/renderer/shaders/include/get_gl_Position.glsl index 9c16c93212..f491a4c67e 100644 --- a/manim/renderer/shaders/include/get_gl_Position.glsl +++ b/manim/renderer/shaders/include/get_gl_Position.glsl @@ -1,38 +1,40 @@ -// Assumes the following uniforms exist in the surrounding context: -// uniform vec2 frame_shape; -// uniform float focal_distance; -// uniform float is_fixed_in_frame; -// uniform float is_fixed_orientation; -// uniform vec3 fixed_orientation_center; +#ifndef GET_GL_POSITION_GLSL +#define GET_GL_POSITION_GLSL + +#include "./camera_uniform_declarations.glsl" +#include "./mobject_uniform_declarations.glsl" const vec2 DEFAULT_FRAME_SHAPE = vec2(8.0 * 16.0 / 9.0, 8.0); -float perspective_scale_factor(float z, float focal_distance){ +float perspective_scale_factor(float z, float focal_distance) +{ return max(0.0, focal_distance / (focal_distance - z)); } - -vec4 get_gl_Position(vec3 point){ +vec4 get_gl_Position(vec3 point) +{ vec4 result = vec4(point, 1.0); - if(!bool(is_fixed_in_frame)){ + if (!bool(is_fixed_in_frame)) + { result.x *= 2.0 / frame_shape.x; result.y *= 2.0 / frame_shape.y; float psf = perspective_scale_factor(result.z, focal_distance); - if (psf > 0){ + if (psf > 0) + { result.xy *= psf; // TODO, what's the better way to do this? // This is to keep vertices too far out of frame from getting cut. - result.z *= 0.01; - } - } else{ - if (!bool(is_fixed_orientation)){ - result.x *= 2.0 / DEFAULT_FRAME_SHAPE.x; - result.y *= 2.0 / DEFAULT_FRAME_SHAPE.y; - } else{ - result.x *= 2.0 / frame_shape.x; - result.y *= 2.0 / frame_shape.y; + // TODO This will be done by the clipping plane in the future with the + // transformation matrix result.z += z_shift; + result.z *= (1.0 / 100.0); } } + else + { + result.x *= 2.0 / DEFAULT_FRAME_SHAPE.x; + result.y *= 2.0 / DEFAULT_FRAME_SHAPE.y; + } result.z *= -1; return result; } +#endif // GET_GL_POSITION_GLSL diff --git a/manim/renderer/shaders/include/get_rotated_surface_unit_normal_vector.glsl b/manim/renderer/shaders/include/get_rotated_surface_unit_normal_vector.glsl index 8c6350d7a0..5e2013b02f 100644 --- a/manim/renderer/shaders/include/get_rotated_surface_unit_normal_vector.glsl +++ b/manim/renderer/shaders/include/get_rotated_surface_unit_normal_vector.glsl @@ -1,16 +1,17 @@ -// Assumes the following uniforms exist in the surrounding context: -// uniform vec3 camera_center; -// uniform mat3 camera_rotation; +#ifndef GET_ROTATED_SURFACE_GLSL +#define GET_ROTATED_SURFACE_GLSL -vec3 get_rotated_surface_unit_normal_vector(vec3 point, vec3 du_point, vec3 dv_point){ - vec3 cp = cross( - (du_point - point), - (dv_point - point) - ); - if(length(cp) == 0){ +#include "./camera_uniform_declarations.glsl" + +vec3 get_rotated_surface_unit_normal_vector(vec3 point, vec3 du_point, vec3 dv_point) +{ + vec3 cp = cross((du_point - point), (dv_point - point)); + if (length(cp) == 0) + { // Instead choose a normal to just dv_point - point in the direction of point vec3 v2 = dv_point - point; cp = cross(cross(v2, point), v2); } return normalize(rotate_point_into_frame(cp)); } +#endif // GET_ROTATED_SURFACE_GLSL diff --git a/manim/renderer/shaders/include/get_unit_normal.glsl b/manim/renderer/shaders/include/get_unit_normal.glsl index 65bb9c71e7..895f99b783 100644 --- a/manim/renderer/shaders/include/get_unit_normal.glsl +++ b/manim/renderer/shaders/include/get_unit_normal.glsl @@ -1,22 +1,29 @@ -vec3 get_unit_normal(in vec3[3] points){ +#ifndef GET_UNIT_NORMAL_GLSL +#define GET_UNIT_NORMAL_GLSL + +vec3 get_unit_normal(in vec3[3] points) +{ float tol = 1e-6; vec3 v1 = normalize(points[1] - points[0]); - vec3 v2 = normalize(points[2] - points[0]); + vec3 v2 = normalize(points[2] - points[1]); vec3 cp = cross(v1, v2); float cp_norm = length(cp); - if(cp_norm < tol){ + if (cp_norm < tol) + { // Three points form a line, so find a normal vector // to that line in the plane shared with the z-axis vec3 k_hat = vec3(0.0, 0.0, 1.0); - vec3 new_cp = cross(cross(v2, k_hat), v2); + vec3 comb = v1 + v2; + vec3 new_cp = cross(cross(comb, k_hat), comb); float new_cp_norm = length(new_cp); - if(new_cp_norm < tol){ + if (new_cp_norm < tol) + { // We only come here if all three points line up // on the z-axis. return vec3(0.0, -1.0, 0.0); - // return k_hat; } return new_cp / new_cp_norm; } return cp / cp_norm; } +#endif // GET_UNIT_NORMAL_GLSL diff --git a/manim/renderer/shaders/include/mobject_uniform_declarations.glsl b/manim/renderer/shaders/include/mobject_uniform_declarations.glsl new file mode 100644 index 0000000000..8761b9c0e8 --- /dev/null +++ b/manim/renderer/shaders/include/mobject_uniform_declarations.glsl @@ -0,0 +1,17 @@ +#ifndef MOBJECT_GLSL +#define MOBJECT_GLSL + +layout(std140) uniform ubo_mobject +{ + vec3 light_source_position; + float gloss; + float shadow; + float reflectiveness; + float flat_stroke; + float joint_type; + float is_fixed_in_frame; + float is_fixed_orientation; + vec3 fixed_orientation_center; +}; + +#endif // MOBJECT_GLSL diff --git a/manim/renderer/shaders/include/position_point_into_frame.glsl b/manim/renderer/shaders/include/position_point_into_frame.glsl index 67b5080f56..d90c099d10 100644 --- a/manim/renderer/shaders/include/position_point_into_frame.glsl +++ b/manim/renderer/shaders/include/position_point_into_frame.glsl @@ -1,25 +1,29 @@ -// Assumes the following uniforms exist in the surrounding context: -// uniform float is_fixed_in_frame; -// uniform float is_fixed_orientation; -// uniform vec3 fixed_orientation_center; -// uniform vec3 camera_center; -// uniform mat3 camera_rotation; +#ifndef POSITION_POINT_INTO_FRAME_GLSL +#define POSITION_POINT_INTO_FRAME_GLSL -vec3 rotate_point_into_frame(vec3 point){ - if(bool(is_fixed_in_frame)){ +#include "./camera_uniform_declarations.glsl" +#include "./mobject_uniform_declarations.glsl" + +vec3 rotate_point_into_frame(vec3 point) +{ + if (bool(is_fixed_in_frame)) + { return point; } return camera_rotation * point; } - -vec3 position_point_into_frame(vec3 point){ - if(bool(is_fixed_in_frame)){ +vec3 position_point_into_frame(vec3 point) +{ + if (bool(is_fixed_in_frame)) + { return point; } - if(bool(is_fixed_orientation)){ + if (bool(is_fixed_orientation)) + { vec3 new_center = rotate_point_into_frame(fixed_orientation_center); return point + (new_center - fixed_orientation_center); } return rotate_point_into_frame(point - camera_center); } +#endif // POSITION_POINT_INTO_FRAME_GLSL diff --git a/manim/renderer/shaders/include/quadratic_bezier_distance.glsl b/manim/renderer/shaders/include/quadratic_bezier_distance.glsl index 847fa4ab6d..7e26bbf20f 100644 --- a/manim/renderer/shaders/include/quadratic_bezier_distance.glsl +++ b/manim/renderer/shaders/include/quadratic_bezier_distance.glsl @@ -1,38 +1,43 @@ +#ifndef QUADRATIC_BEZIER_DISTANCE +#define QUADRATIC_BEZIER_DISTANCE + // Must be inserted in a context with a definition for modify_distance_for_endpoints +float modify_distance_for_endpoints(vec2 p, float dist, float t); // All of this is with respect to a curve that's been rotated/scaled // so that b0 = (0, 0) and b1 = (1, 0). That is, b2 entirely // determines the shape of the curve -vec2 bezier(float t, vec2 b2){ +vec2 bezier(float t, vec2 b2) +{ // Quick returns for the 0 and 1 cases - if (t == 0) return vec2(0, 0); - else if (t == 1) return b2; + if (t == 0) + return vec2(0, 0); + else if (t == 1) + return b2; // Everything else - return vec2( - 2 * t * (1 - t) + b2.x * t*t, - b2.y * t * t - ); + return vec2(2 * t * (1 - t) + b2.x * t * t, b2.y * t * t); } - -float cube_root(float x){ +float cube_root(float x) +{ return sign(x) * pow(abs(x), 1.0 / 3.0); } - -int cubic_solve(float a, float b, float c, float d, out float roots[3]){ +int cubic_solve(float a, float b, float c, float d, out float roots[3]) +{ // Normalize so a = 1 b = b / a; c = c / a; d = d / a; - float p = c - b*b / 3.0; - float q = b * (2.0*b*b - 9.0*c) / 27.0 + d; - float p3 = p*p*p; - float disc = q*q + 4.0*p3 / 27.0; + float p = c - b * b / 3.0; + float q = b * (2.0 * b * b - 9.0 * c) / 27.0 + d; + float p3 = p * p * p; + float disc = q * q + 4.0 * p3 / 27.0; float offset = -b / 3.0; - if(disc >= 0.0){ + if (disc >= 0.0) + { float z = sqrt(disc); float u = (-q + z) / 2.0; float v = (-q - z) / 2.0; @@ -42,21 +47,19 @@ int cubic_solve(float a, float b, float c, float d, out float roots[3]){ return 1; } float u = sqrt(-p / 3.0); - float v = acos(-sqrt( -27.0 / p3) * q / 2.0) / 3.0; + float v = acos(-sqrt(-27.0 / p3) * q / 2.0) / 3.0; float m = cos(v); float n = sin(v) * 1.732050808; - float all_roots[3] = float[3]( - offset + u * (n - m), - offset - u * (n + m), - offset + u * (m + m) - ); + float all_roots[3] = float[3](offset + u * (n - m), offset - u * (n + m), offset + u * (m + m)); // Only accept roots with a positive derivative int n_valid_roots = 0; - for(int i = 0; i < 3; i++){ + for (int i = 0; i < 3; i++) + { float r = all_roots[i]; - if(3*r*r + 2*b*r + c > 0){ + if (3 * r * r + 2 * b * r + c > 0) + { roots[n_valid_roots] = r; n_valid_roots++; } @@ -64,44 +67,49 @@ int cubic_solve(float a, float b, float c, float d, out float roots[3]){ return n_valid_roots; } -float dist_to_line(vec2 p, vec2 b2){ +float dist_to_line(vec2 p, vec2 b2) +{ float t = clamp(p.x / b2.x, 0, 1); float dist; - if(t == 0) dist = length(p); - else if(t == 1) dist = distance(p, b2); - else dist = abs(p.y); + if (t == 0) + dist = length(p); + else if (t == 1) + dist = distance(p, b2); + else + dist = abs(p.y); return modify_distance_for_endpoints(p, dist, t); } - -float dist_to_point_on_curve(vec2 p, float t, vec2 b2){ +float dist_to_point_on_curve(vec2 p, float t, vec2 b2) +{ t = clamp(t, 0, 1); - return modify_distance_for_endpoints( - p, length(p - bezier(t, b2)), t - ); + return modify_distance_for_endpoints(p, length(p - bezier(t, b2)), t); } - -float min_dist_to_curve(vec2 p, vec2 b2, float degree){ +float min_dist_to_curve(vec2 p, vec2 b2, float degree) +{ // Check if curve is really a a line - if(degree == 1) return dist_to_line(p, b2); + if (degree == 1) + return dist_to_line(p, b2); // Try finding the exact sdf by solving the equation // (d/dt) dist^2(t) = 0, which amount to the following // cubic. - float xm2 = uv_b2.x - 2.0; - float y = uv_b2.y; - float a = xm2*xm2 + y*y; + float xm2 = b2.x - 2.0; + float y = b2.y; + float a = xm2 * xm2 + y * y; float b = 3 * xm2; - float c = -(p.x*xm2 + p.y*y) + 2; + float c = -(p.x * xm2 + p.y * y) + 2; float d = -p.x; float roots[3]; int n = cubic_solve(a, b, c, d, roots); // At most 2 roots will have been populated. float d0 = dist_to_point_on_curve(p, roots[0], b2); - if(n == 1) return d0; + if (n == 1) + return d0; float d1 = dist_to_point_on_curve(p, roots[1], b2); return min(d0, d1); } +#endif // QUADRATIC_BEZIER_DISTANCE diff --git a/manim/renderer/shaders/include/quadratic_bezier_geometry_functions.glsl b/manim/renderer/shaders/include/quadratic_bezier_geometry_functions.glsl index d2e3e0fb71..ef39822de1 100644 --- a/manim/renderer/shaders/include/quadratic_bezier_geometry_functions.glsl +++ b/manim/renderer/shaders/include/quadratic_bezier_geometry_functions.glsl @@ -1,36 +1,27 @@ -float cross2d(vec2 v, vec2 w){ +#ifndef QUADRATIC_BEZIER_GEOMETRY_GLSL +#define QUADRATIC_BEZIER_GEOMETRY_GLSL + +float cross2d(vec2 v, vec2 w) +{ return v.x * w.y - w.x * v.y; } - -mat3 get_xy_to_uv(vec2 b0, vec2 b1){ - mat3 shift = mat3( - 1.0, 0.0, 0.0, - 0.0, 1.0, 0.0, - -b0.x, -b0.y, 1.0 - ); +mat3 get_xy_to_uv(vec2 b0, vec2 b1) +{ + mat3 shift = mat3(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, -b0.x, -b0.y, 1.0); float sf = length(b1 - b0); vec2 I = (b1 - b0) / sf; vec2 J = vec2(-I.y, I.x); - mat3 rotate = mat3( - I.x, J.x, 0.0, - I.y, J.y, 0.0, - 0.0, 0.0, 1.0 - ); - return (1 / sf) * rotate * shift; + mat3 rotate = mat3(I.x, J.x, 0.0, I.y, J.y, 0.0, 0.0, 0.0, 1.0); + return (1.0 / sf) * rotate * shift; } - // Orthogonal matrix to convert to a uv space defined so that // b0 goes to [0, 0] and b1 goes to [1, 0] -mat4 get_xyz_to_uv(vec3 b0, vec3 b1, vec3 unit_normal){ - mat4 shift = mat4( - 1, 0, 0, 0, - 0, 1, 0, 0, - 0, 0, 1, 0, - -b0.x, -b0.y, -b0.z, 1 - ); +mat4 get_xyz_to_uv(vec3 b0, vec3 b1, vec3 unit_normal) +{ + mat4 shift = mat4(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -b0.x, -b0.y, -b0.z, 1); float scale_factor = length(b1 - b0); vec3 I = (b1 - b0) / scale_factor; @@ -38,25 +29,20 @@ mat4 get_xyz_to_uv(vec3 b0, vec3 b1, vec3 unit_normal){ vec3 J = cross(K, I); // Transpose (hence inverse) of matrix taking // i-hat to I, k-hat to unit_normal, and j-hat to their cross - mat4 rotate = mat4( - I.x, J.x, K.x, 0.0, - I.y, J.y, K.y, 0.0, - I.z, J.z, K.z, 0.0, - 0.0, 0.0, 0.0, 1.0 - ); - return (1 / scale_factor) * rotate * shift; + mat4 rotate = mat4(I.x, J.x, K.x, 0.0, I.y, J.y, K.y, 0.0, I.z, J.z, K.z, 0.0, 0.0, 0.0, 0.0, 1.0); + return (1.0 / scale_factor) * rotate * shift; } - // Returns 0 for null curve, 1 for linear, 2 for quadratic. // Populates new_points with bezier control points for the curve, // which for quadratics will be the same, but for linear and null // might change. The idea is to inform the caller of the degree, // while also passing tangency information in the linear case. // float get_reduced_control_points(vec3 b0, vec3 b1, vec3 b2, out vec3 new_points[3]){ -float get_reduced_control_points(in vec3 points[3], out vec3 new_points[3]){ - float length_threshold = 1e-6; - float angle_threshold = 5e-2; +float get_reduced_control_points(in vec3 points[3], out vec3 new_points[3]) +{ + float length_threshold = 1e-8; + float angle_threshold = 1e-3; vec3 p0 = points[0]; vec3 p1 = points[1]; @@ -66,27 +52,33 @@ float get_reduced_control_points(in vec3 points[3], out vec3 new_points[3]){ float dot_prod = clamp(dot(normalize(v01), normalize(v12)), -1, 1); bool aligned = acos(dot_prod) < angle_threshold; - bool distinct_01 = length(v01) > length_threshold; // v01 is considered nonzero - bool distinct_12 = length(v12) > length_threshold; // v12 is considered nonzero + bool distinct_01 = length(v01) > length_threshold; // v01 is considered nonzero + bool distinct_12 = length(v12) > length_threshold; // v12 is considered nonzero int n_uniques = int(distinct_01) + int(distinct_12); bool quadratic = (n_uniques == 2) && !aligned; bool linear = (n_uniques == 1) || ((n_uniques == 2) && aligned); bool constant = (n_uniques == 0); - if(quadratic){ + if (quadratic) + { new_points[0] = p0; new_points[1] = p1; new_points[2] = p2; return 2.0; - }else if(linear){ + } + else if (linear) + { new_points[0] = p0; - new_points[1] = (p0 + p2) / 2.0; + new_points[1] = 0.5 * (p0 + p2); new_points[2] = p2; return 1.0; - }else{ + } + else + { new_points[0] = p0; new_points[1] = p0; new_points[2] = p0; return 0.0; } } +#endif // QUADRATIC_BEZIER_GEOMETRY_GLSL diff --git a/manim/renderer/shaders/manim_coords/frag.glsl b/manim/renderer/shaders/manim_coords/frag.glsl deleted file mode 100644 index 2a721a51ea..0000000000 --- a/manim/renderer/shaders/manim_coords/frag.glsl +++ /dev/null @@ -1,8 +0,0 @@ -#version 330 - -uniform vec4 u_color; -out vec4 frag_color; - -void main() { - frag_color = u_color; -} diff --git a/manim/renderer/shaders/manim_coords/vert.glsl b/manim/renderer/shaders/manim_coords/vert.glsl deleted file mode 100644 index e12bd25a37..0000000000 --- a/manim/renderer/shaders/manim_coords/vert.glsl +++ /dev/null @@ -1,11 +0,0 @@ -#version 330 - -in vec4 in_vert; -uniform mat4 u_model_view_matrix; -uniform mat4 u_projection_matrix; - -void main() { - vec4 camera_space_vertex = u_model_view_matrix * in_vert; - vec4 clip_space_vertex = u_projection_matrix * camera_space_vertex; - gl_Position = clip_space_vertex; -} diff --git a/manim/renderer/shaders/quadratic_bezier_fill/frag.glsl b/manim/renderer/shaders/quadratic_bezier_fill/frag.glsl index 28a1879799..38ed74d6b8 100644 --- a/manim/renderer/shaders/quadratic_bezier_fill/frag.glsl +++ b/manim/renderer/shaders/quadratic_bezier_fill/frag.glsl @@ -1,66 +1,75 @@ #version 330 -#include ../include/camera_uniform_declarations.glsl +#include "../include/camera_uniform_declarations.glsl" + +uniform vec2 pixel_shape; +uniform float index; in vec4 color; -in float fill_all; // Either 0 or 1e -in float uv_anti_alias_width; +in float fill_all; // Either 0 or 1e -in vec3 xyz_coords; in float orientation; in vec2 uv_coords; -in vec2 uv_b2; in float bezier_degree; -out vec4 frag_color; - -// Needed for quadratic_bezier_distance insertion below -float modify_distance_for_endpoints(vec2 p, float dist, float t){ - return dist; -} +uniform sampler2D stencil_texture; -#include ../include/quadratic_bezier_distance.glsl +layout(location = 0) out vec4 frag_color; +layout(location = 1) out vec4 stencil_value; +#define ANTI_ALIASING -float sdf(){ - if(bezier_degree < 2){ +float sdf() +{ + if (bezier_degree < 2) + { return abs(uv_coords[1]); } - float u2 = uv_b2.x; - float v2 = uv_b2.y; - // For really flat curves, just take the distance to x-axis - if(abs(v2 / u2) < 0.1 * uv_anti_alias_width){ - return abs(uv_coords[1]); - } - // For flat-ish curves, take the curve - else if(abs(v2 / u2) < 0.5 * uv_anti_alias_width){ - return min_dist_to_curve(uv_coords, uv_b2, bezier_degree); - } - // I know, I don't love this amount of arbitrary-seeming branching either, - // but a number of strange dimples and bugs pop up otherwise. - - // This converts uv_coords to yet another space where the bezier points sit on - // (0, 0), (1/2, 0) and (1, 1), so that the curve can be expressed implicityly - // as y = x^2. - mat2 to_simple_space = mat2( - v2, 0, - 2 - u2, 4 * v2 - ); - vec2 p = to_simple_space * uv_coords; - // Sign takes care of whether we should be filling the inside or outside of curve. - float sgn = orientation * sign(v2); - float Fp = (p.x * p.x - p.y); - if(sgn * Fp < 0){ - return 0.0; - }else{ - return min_dist_to_curve(uv_coords, uv_b2, bezier_degree); - } + vec2 p = uv_coords; + float sgn = orientation; + float q = (p.x * p.x - p.y); +#ifdef ANTI_ALIASING + return sgn * q / sqrt(dFdx(q) * dFdx(q) + dFdy(q) * dFdy(q)); +#endif +#ifndef ANTI_ALIASING + return -sgn * q; +#endif } +void main() +{ + gl_FragDepth = gl_FragCoord.z; + if (color.a == 0) + discard; + + float previous_index = + texture2D(stencil_texture, vec2(gl_FragCoord.x / pixel_shape.x, gl_FragCoord.y / pixel_shape.y)).r; -void main() { - if (color.a == 0) discard; + // Check if we are behind another fill and if yes discard the current fragment + if (previous_index > index) + { + discard; + } + // If we are on top of a previously drawn fill we need to shift ourselves forward by the index amount to compensate + // for different shifting and avoid z_fighting + if (previous_index < index && previous_index != 0) + { + gl_FragDepth = gl_FragCoord.z - index / 1000.0; + } + stencil_value.rgb = vec3(index); + stencil_value.a = 1.0; frag_color = color; - if (fill_all == 1.0) return; - frag_color.a *= smoothstep(1, 0, sdf() / uv_anti_alias_width); + if (fill_all == 1.0) + return; +#ifdef ANTI_ALIASING + float fac = max(0.0, min(1.0, 0.5 - sdf())); + frag_color.a *= fac; // Anti-aliasing +#endif +#ifndef ANTI_ALIASING + frag_color.a *= float(sdf() > 0); // No anti-aliasing +#endif + if (frag_color.a <= 0.0) + { + discard; + } } diff --git a/manim/renderer/shaders/quadratic_bezier_fill/geom.glsl b/manim/renderer/shaders/quadratic_bezier_fill/geom.glsl index b6a9fceea2..65e359efca 100644 --- a/manim/renderer/shaders/quadratic_bezier_fill/geom.glsl +++ b/manim/renderer/shaders/quadratic_bezier_fill/geom.glsl @@ -1,20 +1,18 @@ #version 330 -layout (triangles) in; -layout (triangle_strip, max_vertices = 5) out; +#include "../include/camera_uniform_declarations.glsl" +#include "../include/finalize_color.glsl" +#include "../include/get_gl_Position.glsl" +#include "../include/get_unit_normal.glsl" +#include "../include/mobject_uniform_declarations.glsl" +#include "../include/quadratic_bezier_geometry_functions.glsl" -uniform float anti_alias_width; +layout(triangles) in; +layout(triangle_strip, max_vertices = 5) out; // Needed for get_gl_Position -uniform vec2 frame_shape; -uniform float focal_distance; -uniform float is_fixed_in_frame; -uniform float is_fixed_orientation; -uniform vec3 fixed_orientation_center; -// Needed for finalize_color -uniform vec3 light_source_position; -uniform float gloss; -uniform float shadow; +// uniform vec2 frame_shape; +// uniform float focal_distance; in vec3 bp[3]; in vec3 v_global_unit_normal[3]; @@ -23,102 +21,38 @@ in float v_vert_index[3]; out vec4 color; out float fill_all; -out float uv_anti_alias_width; -out vec3 xyz_coords; out float orientation; -// uv space is where b0 = (0, 0), b1 = (1, 0), and transform is orthogonal out vec2 uv_coords; -out vec2 uv_b2; out float bezier_degree; +const vec2 uv_coords_arr[3] = vec2[3](vec2(0, 0), vec2(0.5, 0), vec2(1, 1)); -// Analog of import for manim only -#include ../include/quadratic_bezier_geometry_functions.glsl -#include ../include/get_gl_Position.glsl -#include ../include/get_unit_normal.glsl -#include ../include/finalize_color.glsl - - -void emit_vertex_wrapper(vec3 point, int index){ - color = finalize_color( - v_color[index], - point, - v_global_unit_normal[index], - light_source_position, - gloss, - shadow - ); - xyz_coords = point; - gl_Position = get_gl_Position(xyz_coords); +void emit_vertex_wrapper(vec3 point, int index) +{ + color = finalize_color(v_color[index], point, v_global_unit_normal[index], light_source_position, gloss, shadow, + reflectiveness); + gl_Position = get_gl_Position(point); + uv_coords = uv_coords_arr[index]; EmitVertex(); } - -void emit_simple_triangle(){ - for(int i = 0; i < 3; i++){ +void emit_simple_triangle() +{ + for (int i = 0; i < 3; i++) + { emit_vertex_wrapper(bp[i], i); } EndPrimitive(); } - -void emit_pentagon(vec3[3] points, vec3 normal){ - vec3 p0 = points[0]; - vec3 p1 = points[1]; - vec3 p2 = points[2]; - // Tangent vectors - vec3 t01 = normalize(p1 - p0); - vec3 t12 = normalize(p2 - p1); - // Vectors perpendicular to the curve in the plane of the curve pointing outside the curve - vec3 p0_perp = cross(t01, normal); - vec3 p2_perp = cross(t12, normal); - - bool fill_inside = orientation > 0; - float aaw = anti_alias_width; - vec3 corners[5]; - if(fill_inside){ - // Note, straight lines will also fall into this case, and since p0_perp and p2_perp - // will point to the right of the curve, it's just what we want - corners = vec3[5]( - p0 + aaw * p0_perp, - p0, - p1 + 0.5 * aaw * (p0_perp + p2_perp), - p2, - p2 + aaw * p2_perp - ); - }else{ - corners = vec3[5]( - p0, - p0 - aaw * p0_perp, - p1, - p2 - aaw * p2_perp, - p2 - ); - } - - mat4 xyz_to_uv = get_xyz_to_uv(p0, p1, normal); - uv_b2 = (xyz_to_uv * vec4(p2, 1)).xy; - uv_anti_alias_width = anti_alias_width / length(p1 - p0); - - for(int i = 0; i < 5; i++){ - vec3 corner = corners[i]; - uv_coords = (xyz_to_uv * vec4(corner, 1)).xy; - int j = int(sign(i - 1) + 1); // Maps i = [0, 1, 2, 3, 4] onto j = [0, 0, 1, 2, 2] - emit_vertex_wrapper(corner, j); - } - EndPrimitive(); -} - - -void main(){ +void main() +{ // If vert indices are sequential, don't fill all - fill_all = float( - (v_vert_index[1] - v_vert_index[0]) != 1.0 || - (v_vert_index[2] - v_vert_index[1]) != 1.0 - ); + fill_all = float((v_vert_index[1] - v_vert_index[0]) != 1.0 || (v_vert_index[2] - v_vert_index[1]) != 1.0); - if(fill_all == 1.0){ + if (fill_all == 1.0) + { emit_simple_triangle(); return; } @@ -128,8 +62,9 @@ void main(){ vec3 local_unit_normal = get_unit_normal(new_bp); orientation = sign(dot(v_global_unit_normal[0], local_unit_normal)); - if(bezier_degree >= 1){ - emit_pentagon(new_bp, local_unit_normal); + if (bezier_degree >= 1) + { + emit_simple_triangle(); } // Don't emit any vertices for bezier_degree 0 } diff --git a/manim/renderer/shaders/quadratic_bezier_fill/vert.glsl b/manim/renderer/shaders/quadratic_bezier_fill/vert.glsl index e891339887..30c9722a4b 100644 --- a/manim/renderer/shaders/quadratic_bezier_fill/vert.glsl +++ b/manim/renderer/shaders/quadratic_bezier_fill/vert.glsl @@ -1,23 +1,23 @@ #version 330 -#include ../include/camera_uniform_declarations.glsl +#include "../include/camera_uniform_declarations.glsl" +#include "../include/mobject_uniform_declarations.glsl" +#include "../include/position_point_into_frame.glsl" in vec3 point; in vec3 unit_normal; in vec4 color; in float vert_index; -out vec3 bp; // Bezier control point -out vec3 v_global_unit_normal; +out vec3 bp; // Bezier control point out vec4 v_color; out float v_vert_index; +out vec3 v_global_unit_normal; -// Analog of import for manim only -#include ../include/position_point_into_frame.glsl - -void main(){ +void main() +{ bp = position_point_into_frame(point.xyz); - v_global_unit_normal = rotate_point_into_frame(unit_normal.xyz); + v_global_unit_normal = rotate_point_into_frame(unit_normal); v_color = color; v_vert_index = vert_index; } diff --git a/manim/renderer/shaders/quadratic_bezier_stroke/frag.glsl b/manim/renderer/shaders/quadratic_bezier_stroke/frag.glsl index 8cf383a058..2bbf2b56da 100644 --- a/manim/renderer/shaders/quadratic_bezier_stroke/frag.glsl +++ b/manim/renderer/shaders/quadratic_bezier_stroke/frag.glsl @@ -1,6 +1,11 @@ #version 330 -#include ../include/camera_uniform_declarations.glsl +#include "../include/camera_uniform_declarations.glsl" +#include "../include/quadratic_bezier_distance.glsl" + +uniform vec2 pixel_shape; +uniform float index; +uniform float disable_stencil; in vec2 uv_coords; in vec2 uv_b2; @@ -15,30 +20,32 @@ in float bevel_start; in float bevel_end; in float angle_from_prev; in float angle_to_next; - in float bezier_degree; -out vec4 frag_color; +uniform sampler2D stencil_texture; +layout(location = 0) out vec4 frag_color; +layout(location = 1) out vec4 stencil_value; -float cross2d(vec2 v, vec2 w){ +float cross2d(vec2 v, vec2 w) +{ return v.x * w.y - w.x * v.y; } - -float modify_distance_for_endpoints(vec2 p, float dist, float t){ +float modify_distance_for_endpoints(vec2 p, float dist, float t) +{ float buff = 0.5 * uv_stroke_width - uv_anti_alias_width; // Check the beginning of the curve - if(t == 0){ + if (t == 0) + { // Clip the start - if(has_prev == 0) return max(dist, -p.x + buff); + if (has_prev == 0) + return max(dist, -p.x + buff); // Bevel start - if(bevel_start == 1){ + if (bevel_start == 1) + { float a = angle_from_prev; - mat2 rot = mat2( - cos(a), sin(a), - -sin(a), cos(a) - ); + mat2 rot = mat2(cos(a), sin(a), -sin(a), cos(a)); // Dist for intersection of two lines float bevel_d = max(abs(p.y), abs((rot * p).y)); // Dist for union of this intersection with the real curve @@ -47,30 +54,29 @@ float modify_distance_for_endpoints(vec2 p, float dist, float t){ return max(min(dist, bevel_d), dist / 2); } // Otherwise, start will be rounded off - }else if(t == 1){ + } + else if (t == 1) + { // Check the end of the curve // TODO, too much code repetition vec2 v21 = (bezier_degree == 2) ? vec2(1, 0) - uv_b2 : vec2(-1, 0); float len_v21 = length(v21); - if(len_v21 == 0){ + if (len_v21 == 0) + { v21 = -uv_b2; len_v21 = length(v21); } float perp_dist = dot(p - uv_b2, v21) / len_v21; - if(has_next == 0) return max(dist, -perp_dist + buff); + if (has_next == 0) + return max(dist, -perp_dist + buff); // Bevel end - if(bevel_end == 1){ + if (bevel_end == 1) + { float a = -angle_to_next; - mat2 rot = mat2( - cos(a), sin(a), - -sin(a), cos(a) - ); + mat2 rot = mat2(cos(a), sin(a), -sin(a), cos(a)); vec2 v21_unit = v21 / length(v21); - float bevel_d = max( - abs(cross2d(p - uv_b2, v21_unit)), - abs(cross2d((rot * (p - uv_b2)), v21_unit)) - ); + float bevel_d = max(abs(cross2d(p - uv_b2, v21_unit)), abs(cross2d((rot * (p - uv_b2)), v21_unit))); return max(min(dist, bevel_d), dist / 2); } // Otherwise, end will be rounded off @@ -78,16 +84,60 @@ float modify_distance_for_endpoints(vec2 p, float dist, float t){ return dist; } - -#include ../include/quadratic_bezier_distance.glsl - - -void main() { - if (uv_stroke_width == 0) discard; +void main() +{ + // Use the default value as standard output + if (disable_stencil == 1.0) + { + stencil_value = vec4(0.0); + } + else + { + stencil_value.rgb = vec3(index); + stencil_value.a = 1.0; + } + gl_FragDepth = gl_FragCoord.z; + // Get the previous index that was written to this fragment + float previous_index = + texture2D(stencil_texture, vec2(gl_FragCoord.x / pixel_shape.x, gl_FragCoord.y / pixel_shape.y)).r; + // If the index is the same that means we are overlapping with the fill and + // crossing through so we push the stroke forward a tiny bit + if (previous_index < index && previous_index != 0) + { + gl_FragDepth = gl_FragCoord.z - 1.7 * index / 1000.0; + } + if (previous_index == index) + { + gl_FragDepth = gl_FragCoord.z - index / 1000.0; + } + // If the stroke is overlapping with a shape that is of higher index that + // means it is behind another mobject on the same plane so we discard the + // fragment + if (previous_index > index) + { + // But for stroke transparency we shouldn't discard but move the stroke in + // front so it is not discarded by the depth test + // TODO: This is highly experimental and should later be rethought and if no + // good solution is found it should just be a discard; + if (color.a == 1.0) + discard; + else + gl_FragDepth = gl_FragCoord.z + index / 1000.0; + } + if (disable_stencil == 1.0) + { + gl_FragDepth = gl_FragCoord.z + 4.5 * index / 1000.0; + } + if (uv_stroke_width == 0) + discard; float dist_to_curve = min_dist_to_curve(uv_coords, uv_b2, bezier_degree); // An sdf for the region around the curve we wish to color. float signed_dist = abs(dist_to_curve) - 0.5 * uv_stroke_width; frag_color = color; frag_color.a *= smoothstep(0.5, -0.5, signed_dist / uv_anti_alias_width); + if (frag_color.a <= 0.0) + { + discard; + } } diff --git a/manim/renderer/shaders/quadratic_bezier_stroke/geom.glsl b/manim/renderer/shaders/quadratic_bezier_stroke/geom.glsl index 2433142410..3ec1fdad89 100644 --- a/manim/renderer/shaders/quadratic_bezier_stroke/geom.glsl +++ b/manim/renderer/shaders/quadratic_bezier_stroke/geom.glsl @@ -1,23 +1,16 @@ #version 330 -layout (triangles) in; -layout (triangle_strip, max_vertices = 5) out; +#include "../include/camera_uniform_declarations.glsl" +#include "../include/finalize_color.glsl" +#include "../include/get_gl_Position.glsl" +#include "../include/get_unit_normal.glsl" +#include "../include/mobject_uniform_declarations.glsl" +#include "../include/quadratic_bezier_geometry_functions.glsl" -// Needed for get_gl_Position -uniform vec2 frame_shape; -uniform float focal_distance; -uniform float is_fixed_in_frame; -uniform float is_fixed_orientation; -uniform vec3 fixed_orientation_center; +layout(triangles) in; +layout(triangle_strip, max_vertices = 5) out; uniform float anti_alias_width; -uniform float flat_stroke; - -//Needed for lighting -uniform vec3 light_source_position; -uniform float joint_type; -uniform float gloss; -uniform float shadow; in vec3 bp[3]; in vec3 prev_bp[3]; @@ -50,55 +43,56 @@ const float BEVEL_JOINT = 2; const float MITER_JOINT = 3; const float PI = 3.141592653; - -#include ../include/quadratic_bezier_geometry_functions.glsl -#include ../include/get_gl_Position.glsl -#include ../include/get_unit_normal.glsl -#include ../include/finalize_color.glsl - - -void flatten_points(in vec3[3] points, out vec2[3] flat_points){ - for(int i = 0; i < 3; i++){ +void flatten_points(in vec3[3] points, out vec2[3] flat_points) +{ + for (int i = 0; i < 3; i++) + { float sf = perspective_scale_factor(points[i].z, focal_distance); flat_points[i] = sf * points[i].xy; } } - -float angle_between_vectors(vec2 v1, vec2 v2){ +float angle_between_vectors(vec2 v1, vec2 v2) +{ float v1_norm = length(v1); float v2_norm = length(v2); - if(v1_norm == 0 || v2_norm == 0) return 0.0; + if (v1_norm == 0 || v2_norm == 0) + return 0.0; float dp = dot(v1, v2) / (v1_norm * v2_norm); float angle = acos(clamp(dp, -1.0, 1.0)); float sn = sign(cross2d(v1, v2)); return sn * angle; } - -bool find_intersection(vec2 p0, vec2 v0, vec2 p1, vec2 v1, out vec2 intersection){ +bool find_intersection(vec2 p0, vec2 v0, vec2 p1, vec2 v1, out vec2 intersection) +{ // Find the intersection of a line passing through // p0 in the direction v0 and one passing through p1 in // the direction p1. // That is, find a solutoin to p0 + v0 * t = p1 + v1 * s float det = -v0.x * v1.y + v1.x * v0.y; - if(det == 0) return false; + if (det == 0) + return false; float t = cross2d(p0 - p1, v1) / det; intersection = p0 + v0 * t; return true; } - -void create_joint(float angle, vec2 unit_tan, float buff, - vec2 static_c0, out vec2 changing_c0, - vec2 static_c1, out vec2 changing_c1){ +void create_joint(float angle, vec2 unit_tan, float buff, vec2 static_c0, out vec2 changing_c0, vec2 static_c1, + out vec2 changing_c1) +{ float shift; - if(abs(angle) < 1e-3){ + if (abs(angle) < 1e-3) + { // No joint shift = 0; - }else if(joint_type == MITER_JOINT){ + } + else if (joint_type == MITER_JOINT) + { shift = buff * (-1.0 - cos(angle)) / sin(angle); - }else{ + } + else + { // For a Bevel joint shift = buff * (1.0 - cos(angle)) / sin(angle); } @@ -106,11 +100,11 @@ void create_joint(float angle, vec2 unit_tan, float buff, changing_c1 = static_c1 + shift * unit_tan; } - // This function is responsible for finding the corners of // a bounding region around the bezier curve, which can be // emitted as a triangle fan -int get_corners(vec2 controls[3], int degree, float stroke_widths[3], out vec2 corners[5]){ +int get_corners(vec2 controls[3], int degree, float stroke_widths[3], out vec2 corners[5]) +{ vec2 p0 = controls[0]; vec2 p1 = controls[1]; vec2 p2 = controls[2]; @@ -121,8 +115,8 @@ int get_corners(vec2 controls[3], int degree, float stroke_widths[3], out vec2 c vec2 v01 = -v10; vec2 v21 = -v12; - vec2 p0_perp = vec2(-v01.y, v01.x); // Pointing to the left of the curve from p0 - vec2 p2_perp = vec2(-v12.y, v12.x); // Pointing to the left of the curve from p2 + vec2 p0_perp = vec2(-v01.y, v01.x); // Pointing to the left of the curve from p0 + vec2 p2_perp = vec2(-v12.y, v12.x); // Pointing to the left of the curve from p2 // aaw is the added width given around the polygon for antialiasing. // In case the normal is faced away from (0, 0, 1), the vector to the @@ -139,85 +133,80 @@ int get_corners(vec2 controls[3], int degree, float stroke_widths[3], out vec2 c vec2 c3 = p2 - buff2 * p2_perp + aaw2 * v12; // Account for previous and next control points - if(has_prev > 0) create_joint(angle_from_prev, v01, buff0, c0, c0, c1, c1); - if(has_next > 0) create_joint(angle_to_next, v21, buff2, c3, c3, c2, c2); + if (has_prev > 0) + create_joint(angle_from_prev, v01, buff0, c0, c0, c1, c1); + if (has_next > 0) + create_joint(angle_to_next, v21, buff2, c3, c3, c2, c2); // Linear case is the simplest - if(degree == 1){ + if (degree == 1) + { // The order of corners should be for a triangle_strip. Last entry is a dummy corners = vec2[5](c0, c1, c3, c2, vec2(0.0)); return 4; } // Otherwise, form a pentagon around the curve - float orientation = sign(cross2d(v01, v12)); // Positive for ccw curves - if(orientation > 0) corners = vec2[5](c0, c1, p1, c2, c3); - else corners = vec2[5](c1, c0, p1, c3, c2); + float orientation = sign(cross2d(v01, v12)); // Positive for ccw curves + if (orientation > 0) + corners = vec2[5](c0, c1, p1, c2, c3); + else + corners = vec2[5](c1, c0, p1, c3, c2); // Replace corner[2] with convex hull point accounting for stroke width find_intersection(corners[0], v01, corners[4], v21, corners[2]); return 5; } - -void set_adjascent_info(vec2 c0, vec2 tangent, - int degree, - vec2 adj[3], - out float bevel, - out float angle - ){ +void set_adjascent_info(vec2 c0, vec2 tangent, int degree, vec2 adj[3], out float bevel, out float angle) +{ bool linear_adj = (angle_between_vectors(adj[1] - adj[0], adj[2] - adj[1]) < 1e-3); angle = angle_between_vectors(c0 - adj[1], tangent); // Decide on joint type bool one_linear = (degree == 1 || linear_adj); - bool should_bevel = ( - (joint_type == AUTO_JOINT && one_linear) || - joint_type == BEVEL_JOINT - ); + bool should_bevel = ((joint_type == AUTO_JOINT && one_linear) || joint_type == BEVEL_JOINT); bevel = should_bevel ? 1.0 : 0.0; } - -void find_joint_info(vec2 controls[3], vec2 prev[3], vec2 next[3], int degree){ +void find_joint_info(vec2 controls[3], vec2 prev[3], vec2 next[3], int degree) +{ float tol = 1e-6; // Made as floats not bools so they can be passed to the frag shader has_prev = float(distance(prev[2], controls[0]) < tol); has_next = float(distance(next[0], controls[2]) < tol); - if(bool(has_prev)){ + if (bool(has_prev)) + { vec2 tangent = controls[1] - controls[0]; - set_adjascent_info( - controls[0], tangent, degree, prev, - bevel_start, angle_from_prev - ); + set_adjascent_info(controls[0], tangent, degree, prev, bevel_start, angle_from_prev); } - if(bool(has_next)){ + if (bool(has_next)) + { vec2 tangent = controls[1] - controls[2]; - set_adjascent_info( - controls[2], tangent, degree, next, - bevel_end, angle_to_next - ); + set_adjascent_info(controls[2], tangent, degree, next, bevel_end, angle_to_next); angle_to_next *= -1; } } - -void main() { +void main() +{ // Convert control points to a standard form if they are linear or null vec3 controls[3]; vec3 prev[3]; vec3 next[3]; bezier_degree = get_reduced_control_points(vec3[3](bp[0], bp[1], bp[2]), controls); - if(bezier_degree == 0.0) return; // Null curve + if (bezier_degree == 0.0) + return; // Null curve int degree = int(bezier_degree); get_reduced_control_points(vec3[3](prev_bp[0], prev_bp[1], prev_bp[2]), prev); get_reduced_control_points(vec3[3](next_bp[0], next_bp[1], next_bp[2]), next); - // Adjust stroke width based on distance from the camera float scaled_strokes[3]; - for(int i = 0; i < 3; i++){ + for (int i = 0; i < 3; i++) + { float sf = perspective_scale_factor(controls[i].z, focal_distance); - if(bool(flat_stroke)){ + if (bool(flat_stroke)) + { vec3 to_cam = normalize(vec3(0.0, 0.0, focal_distance) - controls[i]); sf *= abs(dot(v_global_unit_normal[i], to_cam)); } @@ -241,7 +230,8 @@ void main() { int n_corners = get_corners(flat_controls, degree, scaled_strokes, corners); int index_map[5] = int[5](0, 0, 1, 2, 2); - if(n_corners == 4) index_map[2] = 2; + if (n_corners == 4) + index_map[2] = 2; // Find uv conversion matrix mat3 xy_to_uv = get_xy_to_uv(flat_controls[0], flat_controls[1]); @@ -250,24 +240,16 @@ void main() { uv_b2 = (xy_to_uv * vec3(flat_controls[2], 1.0)).xy; // Emit each corner - for(int i = 0; i < n_corners; i++){ + for (int i = 0; i < n_corners; i++) + { uv_coords = (xy_to_uv * vec3(corners[i], 1.0)).xy; uv_stroke_width = scaled_strokes[index_map[i]] / scale_factor; // Apply some lighting to the color before sending out. // vec3 xyz_coords = vec3(corners[i], controls[index_map[i]].z); vec3 xyz_coords = vec3(corners[i], controls[index_map[i]].z); - color = finalize_color( - v_color[index_map[i]], - xyz_coords, - v_global_unit_normal[index_map[i]], - light_source_position, - gloss, - shadow - ); - gl_Position = vec4( - get_gl_Position(vec3(corners[i], 0.0)).xy, - get_gl_Position(controls[index_map[i]]).zw - ); + color = finalize_color(v_color[index_map[i]], xyz_coords, v_global_unit_normal[index_map[i]], + light_source_position, gloss, shadow, reflectiveness); + gl_Position = vec4(get_gl_Position(vec3(corners[i], 0.0)).xy, get_gl_Position(controls[index_map[i]]).zw); EmitVertex(); } EndPrimitive(); diff --git a/manim/renderer/shaders/quadratic_bezier_stroke/vert.glsl b/manim/renderer/shaders/quadratic_bezier_stroke/vert.glsl index 4ed9d0a7e2..6ff68eb141 100644 --- a/manim/renderer/shaders/quadratic_bezier_stroke/vert.glsl +++ b/manim/renderer/shaders/quadratic_bezier_stroke/vert.glsl @@ -1,6 +1,8 @@ #version 330 -#include ../include/camera_uniform_declarations.glsl +#include "../include/camera_uniform_declarations.glsl" +#include "../include/mobject_uniform_declarations.glsl" +#include "../include/position_point_into_frame.glsl" in vec3 point; in vec3 prev_point; @@ -21,9 +23,8 @@ out vec4 v_color; const float STROKE_WIDTH_CONVERSION = 0.01; -#include ../include/position_point_into_frame.glsl - -void main(){ +void main() +{ bp = position_point_into_frame(point); prev_bp = position_point_into_frame(prev_point); next_bp = position_point_into_frame(next_point); diff --git a/manim/renderer/shaders/render_texture/frag.glsl b/manim/renderer/shaders/render_texture/frag.glsl new file mode 100644 index 0000000000..48328d34b5 --- /dev/null +++ b/manim/renderer/shaders/render_texture/frag.glsl @@ -0,0 +1,12 @@ +#version 330 + +uniform sampler2D tex; +in vec2 f_uv; + +out vec4 frag_color; + +void main() +{ + frag_color = texture(tex, f_uv); + frag_color.a = 1.0; +} diff --git a/manim/renderer/shaders/render_texture/vert.glsl b/manim/renderer/shaders/render_texture/vert.glsl new file mode 100644 index 0000000000..e8e5e0667c --- /dev/null +++ b/manim/renderer/shaders/render_texture/vert.glsl @@ -0,0 +1,12 @@ +#version 330 + +in vec2 pos; +in vec2 uv; + +out vec2 f_uv; + +void main() +{ + gl_Position = vec4(pos, 0.0, 1.0); + f_uv = uv; +} diff --git a/manim/renderer/shaders/simple_vert.glsl b/manim/renderer/shaders/simple_vert.glsl deleted file mode 100644 index f3f8162ee3..0000000000 --- a/manim/renderer/shaders/simple_vert.glsl +++ /dev/null @@ -1,13 +0,0 @@ -#version 330 - -#include ../include/camera_uniform_declarations.glsl - -in vec3 point; - -// Analog of import for manim only -#include ../include/get_gl_Position.glsl -#include ../include/position_point_into_frame.glsl - -void main(){ - gl_Position = get_gl_Position(position_point_into_frame(point)); -} diff --git a/manim/renderer/shaders/surface/frag.glsl b/manim/renderer/shaders/surface/frag.glsl deleted file mode 100644 index fb7cee13d9..0000000000 --- a/manim/renderer/shaders/surface/frag.glsl +++ /dev/null @@ -1,24 +0,0 @@ -#version 330 - -uniform vec3 light_source_position; -uniform float gloss; -uniform float shadow; - -in vec3 xyz_coords; -in vec3 v_normal; -in vec4 v_color; - -out vec4 frag_color; - -#include ../include/finalize_color.glsl - -void main() { - frag_color = finalize_color( - v_color, - xyz_coords, - normalize(v_normal), - light_source_position, - gloss, - shadow - ); -} diff --git a/manim/renderer/shaders/surface/vert.glsl b/manim/renderer/shaders/surface/vert.glsl deleted file mode 100644 index 9f9293e796..0000000000 --- a/manim/renderer/shaders/surface/vert.glsl +++ /dev/null @@ -1,23 +0,0 @@ -#version 330 - -#include ../include/camera_uniform_declarations.glsl - -in vec3 point; -in vec3 du_point; -in vec3 dv_point; -in vec4 color; - -out vec3 xyz_coords; -out vec3 v_normal; -out vec4 v_color; - -#include ../include/position_point_into_frame.glsl -#include ../include/get_gl_Position.glsl -#include ../include/get_rotated_surface_unit_normal_vector.glsl - -void main(){ - xyz_coords = position_point_into_frame(point); - v_normal = get_rotated_surface_unit_normal_vector(point, du_point, dv_point); - v_color = color; - gl_Position = get_gl_Position(xyz_coords); -} diff --git a/manim/renderer/shaders/test/frag.glsl b/manim/renderer/shaders/test/frag.glsl deleted file mode 100644 index 2cf48d39c4..0000000000 --- a/manim/renderer/shaders/test/frag.glsl +++ /dev/null @@ -1,9 +0,0 @@ -#version 330 - -in vec4 v_color; - -out vec4 frag_color; - -void main() { - frag_color = v_color; -} diff --git a/manim/renderer/shaders/test/vert.glsl b/manim/renderer/shaders/test/vert.glsl deleted file mode 100644 index 850b85ce32..0000000000 --- a/manim/renderer/shaders/test/vert.glsl +++ /dev/null @@ -1,11 +0,0 @@ -#version 330 - -in vec2 in_vert; -in vec4 in_color; - -out vec4 v_color; - -void main() { - v_color = in_color; - gl_Position = vec4(in_vert, 0.0, 1.0); -} diff --git a/manim/renderer/shaders/textured_surface/frag.glsl b/manim/renderer/shaders/textured_surface/frag.glsl deleted file mode 100644 index ff162ffa71..0000000000 --- a/manim/renderer/shaders/textured_surface/frag.glsl +++ /dev/null @@ -1,42 +0,0 @@ -#version 330 - -uniform sampler2D LightTexture; -uniform sampler2D DarkTexture; -uniform float num_textures; -uniform vec3 light_source_position; -uniform float gloss; -uniform float shadow; - -in vec3 xyz_coords; -in vec3 v_normal; -in vec2 v_im_coords; -in float v_opacity; - -out vec4 frag_color; - -#include ../include/finalize_color.glsl - -const float dark_shift = 0.2; - -void main() { - vec4 color = texture(LightTexture, v_im_coords); - if(num_textures == 2.0){ - vec4 dark_color = texture(DarkTexture, v_im_coords); - float dp = dot( - normalize(light_source_position - xyz_coords), - normalize(v_normal) - ); - float alpha = smoothstep(-dark_shift, dark_shift, dp); - color = mix(dark_color, color, alpha); - } - - frag_color = finalize_color( - color, - xyz_coords, - normalize(v_normal), - light_source_position, - gloss, - shadow - ); - frag_color.a = v_opacity; -} diff --git a/manim/renderer/shaders/textured_surface/vert.glsl b/manim/renderer/shaders/textured_surface/vert.glsl deleted file mode 100644 index f0573ea39b..0000000000 --- a/manim/renderer/shaders/textured_surface/vert.glsl +++ /dev/null @@ -1,26 +0,0 @@ -#version 330 - -#include ../include/camera_uniform_declarations.glsl - -in vec3 point; -in vec3 du_point; -in vec3 dv_point; -in vec2 im_coords; -in float opacity; - -out vec3 xyz_coords; -out vec3 v_normal; -out vec2 v_im_coords; -out float v_opacity; - -#include ../include/position_point_into_frame.glsl -#include ../include/get_gl_Position.glsl -#include ../include/get_rotated_surface_unit_normal_vector.glsl - -void main(){ - xyz_coords = position_point_into_frame(point); - v_normal = get_rotated_surface_unit_normal_vector(point, du_point, dv_point); - v_im_coords = im_coords; - v_opacity = opacity; - gl_Position = get_gl_Position(xyz_coords); -} diff --git a/manim/renderer/shaders/true_dot/frag.glsl b/manim/renderer/shaders/true_dot/frag.glsl deleted file mode 100644 index e5838ef7c6..0000000000 --- a/manim/renderer/shaders/true_dot/frag.glsl +++ /dev/null @@ -1,34 +0,0 @@ -#version 330 - -uniform vec3 light_source_position; -uniform float gloss; -uniform float shadow; -uniform float anti_alias_width; - -in vec4 color; -in float point_radius; -in vec2 center; -in vec2 point; - -out vec4 frag_color; - -#include ../include/finalize_color.glsl - -void main() { - vec2 diff = point - center; - float dist = length(diff); - float signed_dist = dist - point_radius; - if (signed_dist > 0.5 * anti_alias_width){ - discard; - } - vec3 normal = vec3(diff / point_radius, sqrt(1 - (dist * dist) / (point_radius * point_radius))); - frag_color = finalize_color( - color, - vec3(point.xy, 0.0), - normal, - light_source_position, - gloss, - shadow - ); - frag_color.a *= smoothstep(0.5, -0.5, signed_dist / anti_alias_width); -} diff --git a/manim/renderer/shaders/true_dot/geom.glsl b/manim/renderer/shaders/true_dot/geom.glsl deleted file mode 100644 index beca4029e6..0000000000 --- a/manim/renderer/shaders/true_dot/geom.glsl +++ /dev/null @@ -1,45 +0,0 @@ -#version 330 - -layout (points) in; -layout (triangle_strip, max_vertices = 4) out; - -// Needed for get_gl_Position -uniform vec2 frame_shape; -uniform float focal_distance; -uniform float is_fixed_in_frame; -uniform float is_fixed_orientation; -uniform vec3 fixed_orientation_center; -uniform float anti_alias_width; - -in vec3 v_point[1]; -in float v_point_radius[1]; -in vec4 v_color[1]; - -out vec4 color; -out float point_radius; -out vec2 center; -out vec2 point; - -#include ../include/get_gl_Position.glsl - -void main() { - color = v_color[0]; - point_radius = v_point_radius[0]; - center = v_point[0].xy; - - point_radius = v_point_radius[0] / max(1.0 - v_point[0].z / focal_distance / frame_shape.y, 0.0); - float rpa = point_radius + anti_alias_width; - - for(int i = 0; i < 4; i++){ - // To account for perspective - - int x_index = 2 * (i % 2) - 1; - int y_index = 2 * (i / 2) - 1; - vec3 corner = v_point[0] + vec3(x_index * rpa, y_index * rpa, 0.0); - - gl_Position = get_gl_Position(corner); - point = corner.xy; - EmitVertex(); - } - EndPrimitive(); -} diff --git a/manim/renderer/shaders/true_dot/vert.glsl b/manim/renderer/shaders/true_dot/vert.glsl deleted file mode 100644 index 4f071018e3..0000000000 --- a/manim/renderer/shaders/true_dot/vert.glsl +++ /dev/null @@ -1,20 +0,0 @@ -#version 330 - -#include ../include/camera_uniform_declarations.glsl - -in vec3 point; -in vec4 color; - -uniform float point_radius; - -out vec3 v_point; -out float v_point_radius; -out vec4 v_color; - -#include ../include/position_point_into_frame.glsl - -void main(){ - v_point = position_point_into_frame(point); - v_point_radius = point_radius; - v_color = color; -} diff --git a/manim/renderer/shaders/vectorized_mobject_fill/frag.glsl b/manim/renderer/shaders/vectorized_mobject_fill/frag.glsl deleted file mode 100644 index 70887f2874..0000000000 --- a/manim/renderer/shaders/vectorized_mobject_fill/frag.glsl +++ /dev/null @@ -1,16 +0,0 @@ -#version 330 - -in vec4 v_color; -in vec2 v_texture_coords; -flat in int v_texture_mode; - -out vec4 frag_color; - -void main() { - float curve_func = v_texture_coords[0] * v_texture_coords[0] - v_texture_coords[1]; - if (v_texture_mode * curve_func >= 0.0) { - frag_color = v_color; - } else { - discard; - } -} diff --git a/manim/renderer/shaders/vectorized_mobject_fill/vert.glsl b/manim/renderer/shaders/vectorized_mobject_fill/vert.glsl deleted file mode 100644 index 9456665f14..0000000000 --- a/manim/renderer/shaders/vectorized_mobject_fill/vert.glsl +++ /dev/null @@ -1,20 +0,0 @@ -#version 330 - -uniform mat4 u_model_view_matrix; -uniform mat4 u_projection_matrix; - -in vec3 in_vert; -in vec4 in_color; -in vec2 texture_coords; -in int texture_mode; - -out vec4 v_color; -out vec2 v_texture_coords; -flat out int v_texture_mode; - -void main() { - v_color = in_color; - v_texture_coords = texture_coords; - v_texture_mode = texture_mode; - gl_Position = u_projection_matrix * u_model_view_matrix * vec4(in_vert, 1.0); -} diff --git a/manim/renderer/shaders/vectorized_mobject_stroke/frag.glsl b/manim/renderer/shaders/vectorized_mobject_stroke/frag.glsl deleted file mode 100644 index 2eca6f163c..0000000000 --- a/manim/renderer/shaders/vectorized_mobject_stroke/frag.glsl +++ /dev/null @@ -1,85 +0,0 @@ -#version 330 - -in float v_degree; -in float v_thickness; -in vec2 uv_point; -in vec2[3] uv_curve; -in vec4 v_color; - -out vec4 frag_color; - -// https://www.shadertoy.com/view/ltXSDB -// Test if point p crosses line (a, b), returns sign of result -float testCross(vec2 a, vec2 b, vec2 p) { - return sign((b.y-a.y) * (p.x-a.x) - (b.x-a.x) * (p.y-a.y)); -} - -// Determine which side we're on (using barycentric parameterization) -float signBezier(vec2 A, vec2 B, vec2 C, vec2 p) -{ - vec2 a = C - A, b = B - A, c = p - A; - vec2 bary = vec2(c.x*b.y-b.x*c.y,a.x*c.y-c.x*a.y) / (a.x*b.y-b.x*a.y); - vec2 d = vec2(bary.y * 0.5, 0.0) + 1.0 - bary.x - bary.y; - return mix(sign(d.x * d.x - d.y), mix(-1.0, 1.0, - step(testCross(A, B, p) * testCross(B, C, p), 0.0)), - step((d.x - d.y), 0.0)) * testCross(A, C, B); -} - -// Solve cubic equation for roots -vec3 solveCubic(float a, float b, float c) -{ - float p = b - a*a / 3.0, p3 = p*p*p; - float q = a * (2.0*a*a - 9.0*b) / 27.0 + c; - float d = q*q + 4.0*p3 / 27.0; - float offset = -a / 3.0; - if(d >= 0.0) { - float z = sqrt(d); - vec2 x = (vec2(z, -z) - q) / 2.0; - vec2 uv = sign(x)*pow(abs(x), vec2(1.0/3.0)); - return vec3(offset + uv.x + uv.y); - } - float v = acos(-sqrt(-27.0 / p3) * q / 2.0) / 3.0; - float m = cos(v), n = sin(v)*1.732050808; - return vec3(m + m, -n - m, n - m) * sqrt(-p / 3.0) + offset; -} - -// Find the signed distance from a point to a bezier curve -float sdBezier(vec2 A, vec2 B, vec2 C, vec2 p) -{ - B = mix(B + vec2(1e-4), B, abs(sign(B * 2.0 - A - C))); - vec2 a = B - A, b = A - B * 2.0 + C, c = a * 2.0, d = A - p; - vec3 k = vec3(3.*dot(a,b),2.*dot(a,a)+dot(d,b),dot(d,a)) / dot(b,b); - vec3 t = clamp(solveCubic(k.x, k.y, k.z), 0.0, 1.0); - vec2 pos = A + (c + b*t.x)*t.x; - float dis = length(pos - p); - pos = A + (c + b*t.y)*t.y; - dis = min(dis, length(pos - p)); - pos = A + (c + b*t.z)*t.z; - dis = min(dis, length(pos - p)); - return dis * signBezier(A, B, C, p); -} - -// https://www.shadertoy.com/view/llcfR7 -float dLine(vec2 p1, vec2 p2, vec2 x) { - vec4 colA = vec4(clamp (5.0 - length (x - p1), 0.0, 1.0)); - vec4 colB = vec4(clamp (5.0 - length (x - p2), 0.0, 1.0)); - - vec2 a_p1 = x - p1; - vec2 p2_p1 = p2 - p1; - float h = clamp (dot (a_p1, p2_p1) / dot (p2_p1, p2_p1), 0.0, 1.0); - return length (a_p1 - p2_p1 * h); -} - -void main() { - float distance; - if (v_degree == 2.0) { - distance = sdBezier(uv_curve[0], uv_curve[1], uv_curve[2], uv_point); - } else { - distance = dLine(uv_curve[0], uv_curve[2], uv_point); - } - if (abs(distance) < v_thickness) { - frag_color = v_color; - } else { - discard; - } -} diff --git a/manim/renderer/shaders/vectorized_mobject_stroke/vert.glsl b/manim/renderer/shaders/vectorized_mobject_stroke/vert.glsl deleted file mode 100644 index 63982ee71f..0000000000 --- a/manim/renderer/shaders/vectorized_mobject_stroke/vert.glsl +++ /dev/null @@ -1,125 +0,0 @@ -#version 330 - -uniform vec3 manim_unit_normal; -uniform mat4 u_model_view_matrix; -uniform mat4 u_projection_matrix; - -in vec3[3] current_curve; -in vec2 tile_coordinate; -in vec4 in_color; -in float in_width; - -out float v_degree; -out float v_thickness; -out vec2 uv_point; -out vec2[3] uv_curve; -out vec4 v_color; - -int get_degree(in vec3 points[3], out vec3 normal) { - float length_threshold = 1e-6; - float angle_threshold = 5e-2; - - vec3 v01 = (points[1] - points[0]); - vec3 v12 = (points[2] - points[1]); - - float dot_prod = clamp(dot(normalize(v01), normalize(v12)), -1, 1); - bool aligned = acos(dot_prod) < angle_threshold; - bool distinct_01 = length(v01) > length_threshold; // v01 is considered nonzero - bool distinct_12 = length(v12) > length_threshold; // v12 is considered nonzero - int num_distinct = int(distinct_01) + int(distinct_12); - - bool quadratic = (num_distinct == 2) && !aligned; - bool linear = (num_distinct == 1) || ((num_distinct == 2) && aligned); - bool constant = (num_distinct == 0); - - if (quadratic) { - // If the curve is quadratic pass a normal vector to the caller. - normal = normalize(cross(v01, v12)); - return 2; - } else if (linear) { - return 1; - } else { - return 0; - } -} - -// https://iquilezles.org/www/articles/bezierbbox/bezierbbox.htm -vec4 bboxBezier(in vec2 p0, in vec2 p1, in vec2 p2) { - vec2 mi = min(p0, p2); - vec2 ma = max(p0, p2); - - if (p1.x < mi.x || p1.x > ma.x || p1.y < mi.y || p1.y > ma.y) { - vec2 t = clamp((p0 - p1) / (p0 - 2.0 * p1 + p2), 0.0, 1.0); - vec2 s = 1.0 - t; - vec2 q = s * s * p0 + 2.0 * s * t * p1 + t * t * p2; - mi = min(mi, q); - ma = max(ma, q); - } - - return vec4(mi, ma); -} - -vec2 convert_to_uv(vec3 x_unit, vec3 y_unit, vec3 point) { - return vec2(dot(point, x_unit), dot(point, y_unit)); -} - -vec3 convert_from_uv(vec3 translation, vec3 x_unit, vec3 y_unit, vec2 point) { - vec3 untranslated_point = point[0] * x_unit + point[1] * y_unit; - return untranslated_point + translation; -} - -void main() { - float thickness_multiplier = 0.004; - v_color = in_color; - - vec3 computed_normal; - v_degree = get_degree(current_curve, computed_normal); - - vec3 tile_x_unit = normalize(current_curve[2] - current_curve[0]); - vec3 unit_normal; - vec3 tile_y_unit; - if (v_degree == 0) { - tile_y_unit = vec3(0.0, 0.0, 0.0); - } else if (v_degree == 1) { - // Since the curve forms a straight line there's no way to compute a normal. - unit_normal = manim_unit_normal; - - tile_y_unit = cross(unit_normal, tile_x_unit); - } else { - // Prefer to use a computed normal vector rather than the one from manim. - unit_normal = computed_normal; - - // Ensure tile_y_unit is pointing toward p1 from p0. - tile_y_unit = cross(unit_normal, tile_x_unit); - if (dot(tile_y_unit, current_curve[1] - current_curve[0]) < 0) { - tile_y_unit *= -1; - } - } - - // Project the curve onto the tile. - for(int i = 0; i < 3; i++) { - uv_curve[i] = convert_to_uv(tile_x_unit, tile_y_unit, current_curve[i]); - } - - // Compute the curve's bounding box. - vec4 uv_bounding_box = bboxBezier(uv_curve[0], uv_curve[1], uv_curve[2]); - vec3 tile_translation = unit_normal * dot(current_curve[0], unit_normal); - vec3 bounding_box_min = convert_from_uv(tile_translation, tile_x_unit, tile_y_unit, uv_bounding_box.xy); - vec3 bounding_box_max = convert_from_uv(tile_translation, tile_x_unit, tile_y_unit, uv_bounding_box.zw); - vec3 bounding_box_vec = bounding_box_max - bounding_box_min; - vec3 tile_origin = bounding_box_min; - vec3 tile_x_vec = tile_x_unit * dot(tile_x_unit, bounding_box_vec); - vec3 tile_y_vec = tile_y_unit * dot(tile_y_unit, bounding_box_vec); - - // Expand the tile according to the line's thickness. - v_thickness = thickness_multiplier * in_width; - tile_origin = current_curve[0] - v_thickness * (tile_x_unit + tile_y_unit); - tile_x_vec += 2 * v_thickness * tile_x_unit; - tile_y_vec += 2 * v_thickness * tile_y_unit; - - vec3 tile_point = tile_origin + \ - tile_coordinate[0] * tile_x_vec + \ - tile_coordinate[1] * tile_y_vec; - gl_Position = u_projection_matrix * u_model_view_matrix * vec4(tile_point, 1.0); - uv_point = convert_to_uv(tile_x_unit, tile_y_unit, tile_point); -} diff --git a/manim/renderer/shaders/vertex_colors/frag.glsl b/manim/renderer/shaders/vertex_colors/frag.glsl deleted file mode 100644 index dc5d0d7664..0000000000 --- a/manim/renderer/shaders/vertex_colors/frag.glsl +++ /dev/null @@ -1,8 +0,0 @@ -#version 330 - -in vec4 v_color; -out vec4 frag_color; - -void main() { - frag_color = v_color; -} diff --git a/manim/renderer/shaders/vertex_colors/vert.glsl b/manim/renderer/shaders/vertex_colors/vert.glsl deleted file mode 100644 index 944dc92806..0000000000 --- a/manim/renderer/shaders/vertex_colors/vert.glsl +++ /dev/null @@ -1,13 +0,0 @@ -#version 330 - -uniform mat4 u_model_matrix; -uniform mat4 u_view_matrix; -uniform mat4 u_projection_matrix; -in vec4 in_vert; -in vec4 in_color; -out vec4 v_color; - -void main() { - v_color = in_color; - gl_Position = u_projection_matrix * u_view_matrix * u_model_matrix * in_vert; -} diff --git a/manim/scene/moving_camera_scene.py b/manim/scene/moving_camera_scene.py deleted file mode 100644 index eafc992ef5..0000000000 --- a/manim/scene/moving_camera_scene.py +++ /dev/null @@ -1,135 +0,0 @@ -"""A scene whose camera can be moved around. - -.. SEEALSO:: - - :mod:`.moving_camera` - - -Examples --------- - -.. manim:: ChangingCameraWidthAndRestore - - class ChangingCameraWidthAndRestore(MovingCameraScene): - def construct(self): - text = Text("Hello World").set_color(BLUE) - self.add(text) - self.camera.frame.save_state() - self.play(self.camera.frame.animate.set(width=text.width * 1.2)) - self.wait(0.3) - self.play(Restore(self.camera.frame)) - - -.. manim:: MovingCameraCenter - - class MovingCameraCenter(MovingCameraScene): - def construct(self): - s = Square(color=RED, fill_opacity=0.5).move_to(2 * LEFT) - t = Triangle(color=GREEN, fill_opacity=0.5).move_to(2 * RIGHT) - self.wait(0.3) - self.add(s, t) - self.play(self.camera.frame.animate.move_to(s)) - self.wait(0.3) - self.play(self.camera.frame.animate.move_to(t)) - - -.. manim:: MovingAndZoomingCamera - - class MovingAndZoomingCamera(MovingCameraScene): - def construct(self): - s = Square(color=BLUE, fill_opacity=0.5).move_to(2 * LEFT) - t = Triangle(color=YELLOW, fill_opacity=0.5).move_to(2 * RIGHT) - self.add(s, t) - self.play(self.camera.frame.animate.move_to(s).set(width=s.width*2)) - self.wait(0.3) - self.play(self.camera.frame.animate.move_to(t).set(width=t.width*2)) - - self.play(self.camera.frame.animate.move_to(ORIGIN).set(width=14)) - -.. manim:: MovingCameraOnGraph - - class MovingCameraOnGraph(MovingCameraScene): - def construct(self): - self.camera.frame.save_state() - - ax = Axes(x_range=[-1, 10], y_range=[-1, 10]) - graph = ax.plot(lambda x: np.sin(x), color=WHITE, x_range=[0, 3 * PI]) - - dot_1 = Dot(ax.i2gp(graph.t_min, graph)) - dot_2 = Dot(ax.i2gp(graph.t_max, graph)) - self.add(ax, graph, dot_1, dot_2) - - self.play(self.camera.frame.animate.scale(0.5).move_to(dot_1)) - self.play(self.camera.frame.animate.move_to(dot_2)) - self.play(Restore(self.camera.frame)) - self.wait() - -.. manim:: SlidingMultipleScenes - - class SlidingMultipleScenes(MovingCameraScene): - def construct(self): - def create_scene(number): - frame = Rectangle(width=16,height=9) - circ = Circle().shift(LEFT) - text = Tex(f"This is Scene {str(number)}").next_to(circ, RIGHT) - frame.add(circ,text) - return frame - - group = VGroup(*(create_scene(i) for i in range(4))).arrange_in_grid(buff=4) - self.add(group) - self.camera.auto_zoom(group[0], animate=False) - for scene in group: - self.play(self.camera.auto_zoom(scene)) - self.wait() - - self.play(self.camera.auto_zoom(group, margin=2)) -""" - -from __future__ import annotations - -__all__ = ["MovingCameraScene"] - -from manim.animation.animation import Animation - -from ..camera.moving_camera import MovingCamera -from ..scene.scene import Scene -from ..utils.family import extract_mobject_family_members -from ..utils.iterables import list_update - - -class MovingCameraScene(Scene): - """ - This is a Scene, with special configurations and properties that - make it suitable for cases where the camera must be moved around. - - Note: Examples are included in the moving_camera_scene module - documentation, see below in the 'see also' section. - - .. SEEALSO:: - - :mod:`.moving_camera_scene` - :class:`.MovingCamera` - """ - - def __init__(self, camera_class=MovingCamera, **kwargs): - super().__init__(camera_class=camera_class, **kwargs) - - def get_moving_mobjects(self, *animations: Animation): - """ - This method returns a list of all of the Mobjects in the Scene that - are moving, that are also in the animations passed. - - Parameters - ---------- - *animations - The Animations whose mobjects will be checked. - """ - moving_mobjects = super().get_moving_mobjects(*animations) - all_moving_mobjects = extract_mobject_family_members(moving_mobjects) - movement_indicators = self.renderer.camera.get_mobjects_indicating_movement() - for movement_indicator in movement_indicators: - if movement_indicator in all_moving_mobjects: - # When one of these is moving, the camera should - # consider all mobjects to be moving - return list_update(self.mobjects, moving_mobjects) - return moving_mobjects diff --git a/manim/scene/scene.py b/manim/scene/scene.py index bf8e526bf9..9b586824ed 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -1,583 +1,239 @@ -"""Basic canvas for animations.""" - from __future__ import annotations -from manim.utils.parameter_parsing import flatten_iterable_parameters - -__all__ = ["Scene"] - -import copy -import datetime import inspect -import platform import random -import threading -import time -import types -from queue import Queue - -import srt - -from manim.scene.section import DefaultSectionType - -try: - import dearpygui.dearpygui as dpg - - dearpygui_imported = True -except ImportError: - dearpygui_imported = False +from collections import OrderedDict, deque from typing import TYPE_CHECKING import numpy as np -from tqdm import tqdm -from watchdog.events import FileSystemEventHandler -from watchdog.observers import Observer - -from manim.mobject.mobject import Mobject -from manim.mobject.opengl.opengl_mobject import OpenGLPoint - -from .. import config, logger -from ..animation.animation import Animation, Wait, prepare_animation -from ..camera.camera import Camera -from ..constants import * -from ..gui.gui import configure_pygui -from ..renderer.cairo_renderer import CairoRenderer -from ..renderer.opengl_renderer import OpenGLRenderer -from ..renderer.shader import Object3D -from ..utils import opengl, space_ops -from ..utils.exceptions import EndSceneEarlyException, RerunSceneException -from ..utils.family import extract_mobject_family_members -from ..utils.family_ops import restructure_list_to_exclude_certain_family_members -from ..utils.file_ops import open_media_file -from ..utils.iterables import list_difference_update, list_update +from pyglet.window import key +from typing_extensions import assert_never + +from manim import config, logger +from manim.animation.animation import prepare_animation +from manim.animation.scene_buffer import SceneBuffer, SceneOperation +from manim.camera.camera import Camera +from manim.constants import DEFAULT_WAIT_TIME +from manim.event_handler import EVENT_DISPATCHER +from manim.event_handler.event_type import EventType +from manim.mobject.mobject import Group, Point +from manim.mobject.opengl.opengl_mobject import OpenGLMobject, _AnimationBuilder +from manim.mobject.types.vectorized_mobject import VGroup, VMobject +from manim.scene.sections import group as SceneGroup +from manim.utils.iterables import list_difference_update if TYPE_CHECKING: - from collections.abc import Sequence - from typing import Callable + from collections.abc import Iterable, Reversible, Sequence + from typing import Any, Callable, Self - from manim.mobject.mobject import _AnimationBuilder + from manim.animation.protocol import AnimationProtocol + from manim.manager import Manager +# TODO: these keybindings should be made configurable -class RerunSceneHandler(FileSystemEventHandler): - """A class to handle rerunning a Scene after the input file is modified.""" - - def __init__(self, queue): - super().__init__() - self.queue = queue - - def on_modified(self, event): - self.queue.put(("rerun_file", [], {})) +PAN_3D_KEY = "d" +FRAME_SHIFT_KEY = "f" +ZOOM_KEY = "z" +RESET_FRAME_KEY = "r" +QUIT_KEY = "q" class Scene: - """A Scene is the canvas of your animation. - - The primary role of :class:`Scene` is to provide the user with tools to manage - mobjects and animations. Generally speaking, a manim script consists of a class - that derives from :class:`Scene` whose :meth:`Scene.construct` method is overridden - by the user's code. + """The Canvas of Manim. - Mobjects are displayed on screen by calling :meth:`Scene.add` and removed from - screen by calling :meth:`Scene.remove`. All mobjects currently on screen are kept - in :attr:`Scene.mobjects`. Animations are played by calling :meth:`Scene.play`. + You can use it by putting the following into a + file ``manimation.py`` - A :class:`Scene` is rendered internally by calling :meth:`Scene.render`. This in - turn calls :meth:`Scene.setup`, :meth:`Scene.construct`, and - :meth:`Scene.tear_down`, in that order. + .. manim:: SceneWithSettings - It is not recommended to override the ``__init__`` method in user Scenes. For code - that should be ran before a Scene is rendered, use :meth:`Scene.setup` instead. + class SceneWithSettings(Scene): + # set configuration attributes + random_seed = 1 - Examples - -------- - Override the :meth:`Scene.construct` method with your code. + # all the action happens here + def construct(self): + self.play(Create(ManimBanner())) - .. code-block:: python + And then run ``manim -p manimation.py``. To write the result to a file, + do ``manim -w manimation.py``. - class MyScene(Scene): - def construct(self): - self.play(Write(Text("Hello World!"))) + Attributes + ---------- + random_seed : The seed for random and numpy.random + pan_sensitivity : """ - def __init__( - self, - renderer: CairoRenderer | OpenGLRenderer | None = None, - camera_class: type[Camera] = Camera, - always_update_mobjects: bool = False, - random_seed: int | None = None, - skip_animations: bool = False, - ) -> None: - self.camera_class = camera_class - self.always_update_mobjects = always_update_mobjects - self.random_seed = random_seed - self.skip_animations = skip_animations - - self.animations = None - self.stop_condition = None - self.moving_mobjects = [] - self.static_mobjects = [] - self.time_progression = None - self.duration = None - self.last_t = None - self.queue = Queue() - self.skip_animation_preview = False - self.meshes = [] - self.camera_target = ORIGIN - self.widgets = [] - self.dearpygui_imported = dearpygui_imported - self.updaters = [] - self.point_lights = [] - self.ambient_light = None - self.key_to_function_map = {} - self.mouse_press_callbacks = [] - self.interactive_mode = False - - if config.renderer == RendererType.OPENGL: - # Items associated with interaction - self.mouse_point = OpenGLPoint() - self.mouse_drag_point = OpenGLPoint() - if renderer is None: - renderer = OpenGLRenderer() - - if renderer is None: - self.renderer = CairoRenderer( - camera_class=self.camera_class, - skip_animations=self.skip_animations, - ) - else: - self.renderer = renderer - self.renderer.init_scene(self) + random_seed: int | None = None + pan_sensitivity: float = 3.0 + max_num_saved_states: int = 50 + + always_update_mobjects: bool = False + start_at_animation_number: int = 0 + end_at_animation_number: int | None = None + presenter_mode: bool = False + embed_exception_mode: str = "" + embed_error_sound: bool = False + + groups_api: bool = False + + def __init__(self, manager: Manager[Self]): + # Core state of the scene + self.camera: Camera = Camera() + self.manager = manager + self.mobjects: list[OpenGLMobject] = [] + self.num_plays: int = 0 + # the time is updated by the manager + self.time: float = 0 + self.undo_stack: deque[SceneState] = deque() + self.redo_stack: list[SceneState] = [] + + # Items associated with interaction + self.mouse_point = Point() + self.mouse_drag_point = Point() + self.hold_on_wait = self.presenter_mode + self.quit_interaction = False - self.mobjects = [] - # TODO, remove need for foreground mobjects - self.foreground_mobjects = [] + # Much nicer to work with deterministic scenes if self.random_seed is not None: random.seed(self.random_seed) np.random.seed(self.random_seed) - @property - def camera(self): - return self.renderer.camera - - @property - def time(self) -> float: - """The time since the start of the scene.""" - return self.renderer.time - - def __deepcopy__(self, clone_from_id): - cls = self.__class__ - result = cls.__new__(cls) - clone_from_id[id(self)] = result - for k, v in self.__dict__.items(): - if k in ["renderer", "time_progression"]: - continue - if k == "camera_class": - setattr(result, k, v) - setattr(result, k, copy.deepcopy(v, clone_from_id)) - result.mobject_updater_lists = [] - - # Update updaters - for mobject in self.mobjects: - cloned_updaters = [] - for updater in mobject.updaters: - # Make the cloned updater use the cloned Mobjects as free variables - # rather than the original ones. Analyzing function bytecode with the - # dis module will help in understanding this. - # https://docs.python.org/3/library/dis.html - # TODO: Do the same for function calls recursively. - free_variable_map = inspect.getclosurevars(updater).nonlocals - cloned_co_freevars = [] - cloned_closure = [] - for free_variable_name in updater.__code__.co_freevars: - free_variable_value = free_variable_map[free_variable_name] - - # If the referenced variable has not been cloned, raise. - if id(free_variable_value) not in clone_from_id: - raise Exception( - f"{free_variable_name} is referenced from an updater " - "but is not an attribute of the Scene, which isn't " - "allowed.", - ) - - # Add the cloned object's name to the free variable list. - cloned_co_freevars.append(free_variable_name) - - # Add a cell containing the cloned object's reference to the - # closure list. - cloned_closure.append( - types.CellType(clone_from_id[id(free_variable_value)]), - ) - - cloned_updater = types.FunctionType( - updater.__code__.replace(co_freevars=tuple(cloned_co_freevars)), - updater.__globals__, - updater.__name__, - updater.__defaults__, - tuple(cloned_closure), - ) - cloned_updaters.append(cloned_updater) - mobject_clone = clone_from_id[id(mobject)] - mobject_clone.updaters = cloned_updaters - if len(cloned_updaters) > 0: - result.mobject_updater_lists.append((mobject_clone, cloned_updaters)) - return result - - def render(self, preview: bool = False): - """ - Renders this Scene. - - Parameters - --------- - preview - If true, opens scene in a file viewer. - """ - self.setup() - try: - self.construct() - except EndSceneEarlyException: - pass - except RerunSceneException: - self.remove(*self.mobjects) - self.renderer.clear_screen() - self.renderer.num_plays = 0 - return True - self.tear_down() - # We have to reset these settings in case of multiple renders. - self.renderer.scene_finished(self) - - # Show info only if animations are rendered or to get image - if ( - self.renderer.num_plays - or config["format"] == "png" - or config["save_last_frame"] - ): - logger.info( - f"Rendered {str(self)}\nPlayed {self.renderer.num_plays} animations", - ) - - # If preview open up the render after rendering. - if preview: - config["preview"] = True - - if config["preview"] or config["show_in_file_browser"]: - open_media_file(self.renderer.file_writer) + def __str__(self) -> str: + return self.__class__.__name__ - def setup(self): - """ - This is meant to be implemented by any scenes which - are commonly subclassed, and have some common setup + def get_default_scene_name(self) -> str: + name = str(self) + saan = self.start_at_animation_number + eaan = self.end_at_animation_number + if saan is not None: + name += f"_{saan}" + if eaan is not None: + name += f"_{eaan}" + return name + + def process_buffer(self, buffer: SceneBuffer) -> None: + for op, args, kwargs in buffer: + match op: + case SceneOperation.ADD: + self.add(*args, **kwargs) + case SceneOperation.REMOVE: + self.remove(*args, **kwargs) + case SceneOperation.REPLACE: + self.replace(*args, **kwargs) + case _: + assert_never(op) + buffer.clear() + + def setup(self) -> None: + """ + This method is used to set up scenes to do any setup involved before the construct method is called. """ - pass - def tear_down(self): + def construct(self) -> None: """ - This is meant to be implemented by any scenes which - are commonly subclassed, and have some common method - to be invoked before the scene ends. - """ - pass - - def construct(self): - """Add content to the Scene. - - From within :meth:`Scene.construct`, display mobjects on screen by calling - :meth:`Scene.add` and remove them from screen by calling :meth:`Scene.remove`. - All mobjects currently on screen are kept in :attr:`Scene.mobjects`. Play - animations by calling :meth:`Scene.play`. - - Notes - ----- - Initialization code should go in :meth:`Scene.setup`. Termination code should - go in :meth:`Scene.tear_down`. - - Examples - -------- - A typical manim script includes a class derived from :class:`Scene` with an - overridden :meth:`Scene.construct` method: - - .. code-block:: python - - class MyScene(Scene): - def construct(self): - self.play(Write(Text("Hello World!"))) - - See Also - -------- - :meth:`Scene.setup` - :meth:`Scene.render` - :meth:`Scene.tear_down` - - """ - pass # To be implemented in subclasses - - def next_section( - self, - name: str = "unnamed", - section_type: str = DefaultSectionType.NORMAL, - skip_animations: bool = False, - ) -> None: - """Create separation here; the last section gets finished and a new one gets created. - ``skip_animations`` skips the rendering of all animations in this section. - Refer to :doc:`the documentation` on how to use sections. - """ - self.renderer.file_writer.next_section(name, section_type, skip_animations) - - def __str__(self): - return self.__class__.__name__ - - def get_attrs(self, *keys: str): + The entrypoint to animations in Manim. + Should be overridden in the subclass to produce animations """ - Gets attributes of a scene given the attribute's identifier/name. + raise RuntimeError( + "Could not find the construct method, did you misspell the name?" + ) - Parameters - ---------- - *keys - Name(s) of the argument(s) to return the attribute of. + def tear_down(self) -> None: + """This method is used to clean up scenes""" - Returns - ------- - list - List of attributes of the passed identifiers. - """ - return [getattr(self, key) for key in keys] + def find_groups(self) -> list[SceneGroup]: + """Find all groups in a :class:`.Scene`""" + sections: list[SceneGroup] = [ + bound + for _, bound in inspect.getmembers( + self, predicate=lambda x: isinstance(x, SceneGroup) + ) + ] + sections.sort() + return sections - def update_mobjects(self, dt: float): - """ - Begins updating all mobjects in the Scene. + # Only these methods should touch the camera + # Related to updating - Parameters - ---------- - dt - Change in time between updates. Defaults (mostly) to 1/frames_per_second - """ + def _update_mobjects(self, dt: float) -> None: for mobject in self.mobjects: mobject.update(dt) - def update_meshes(self, dt): - for obj in self.meshes: - for mesh in obj.get_family(): - mesh.update(dt) - - def update_self(self, dt: float): - """Run all scene updater functions. - - Among all types of update functions (mobject updaters, mesh updaters, - scene updaters), scene update functions are called last. - - Parameters - ---------- - dt - Scene time since last update. - - See Also - -------- - :meth:`.Scene.add_updater` - :meth:`.Scene.remove_updater` - """ - for func in self.updaters: - func(dt) - def should_update_mobjects(self) -> bool: """ - Returns True if the mobjects of this scene should be updated. - - In particular, this checks whether - - - the :attr:`always_update_mobjects` attribute of :class:`.Scene` - is set to ``True``, - - the :class:`.Scene` itself has time-based updaters attached, - - any mobject in this :class:`.Scene` has time-based updaters attached. - - This is only called when a single Wait animation is played. - """ - wait_animation = self.animations[0] - if wait_animation.is_static_wait is None: - should_update = ( - self.always_update_mobjects - or self.updaters - or wait_animation.stop_condition is not None - or any( - mob.has_time_based_updater() - for mob in self.get_mobject_family_members() - ) - ) - wait_animation.is_static_wait = not should_update - return not wait_animation.is_static_wait - - def get_top_level_mobjects(self): - """ - Returns all mobjects which are not submobjects. + This is called to check if a wait frame should be frozen Returns ------- - list - List of top level mobjects. + bool: does it have to be rerendered or is it static """ - # Return only those which are not in the family - # of another mobject from the scene - families = [m.get_family() for m in self.mobjects] - - def is_top_level(mobject): - num_families = sum((mobject in family) for family in families) - return num_families == 1 + # always rerender by returning True + # TODO: Apply caching here + return self.always_update_mobjects or any( + mob.has_updaters for mob in self.mobjects + ) - return list(filter(is_top_level, self.mobjects)) + def has_time_based_updaters(self) -> bool: + return any( + sm.has_time_based_updater() + for mob in self.mobjects + for sm in mob.get_family() + ) - def get_mobject_family_members(self): - """ - Returns list of family-members of all mobjects in scene. - If a Circle() and a VGroup(Rectangle(),Triangle()) were added, - it returns not only the Circle(), Rectangle() and Triangle(), but - also the VGroup() object. + # Related to internal mobject organization - Returns - ------- - list - List of mobject family members. - """ - if config.renderer == RendererType.OPENGL: - family_members = [] - for mob in self.mobjects: - family_members.extend(mob.get_family()) - return family_members - elif config.renderer == RendererType.CAIRO: - return extract_mobject_family_members( - self.mobjects, - use_z_index=self.renderer.camera.use_z_index, - ) - - def add(self, *mobjects: Mobject): + def add(self, *new_mobjects: OpenGLMobject): """ Mobjects will be displayed, from background to foreground in the order with which they are added. - - Parameters - --------- - *mobjects - Mobjects to add. - - Returns - ------- - Scene - The same scene after adding the Mobjects in. - """ - if config.renderer == RendererType.OPENGL: - new_mobjects = [] - new_meshes = [] - for mobject_or_mesh in mobjects: - if isinstance(mobject_or_mesh, Object3D): - new_meshes.append(mobject_or_mesh) - else: - new_mobjects.append(mobject_or_mesh) - self.remove(*new_mobjects) - self.mobjects += new_mobjects - self.remove(*new_meshes) - self.meshes += new_meshes - elif config.renderer == RendererType.CAIRO: - mobjects = [*mobjects, *self.foreground_mobjects] - self.restructure_mobjects(to_remove=mobjects) - self.mobjects += mobjects - if self.moving_mobjects: - self.restructure_mobjects( - to_remove=mobjects, - mobject_list_name="moving_mobjects", - ) - self.moving_mobjects += mobjects + self.remove(*new_mobjects) + self.mobjects += new_mobjects return self - def add_mobjects_from_animations(self, animations): - curr_mobjects = self.get_mobject_family_members() - for animation in animations: - if animation.is_introducer(): - continue - # Anything animated that's not already in the - # scene gets added to the scene - mob = animation.mobject - if mob is not None and mob not in curr_mobjects: - self.add(mob) - curr_mobjects += mob.get_family() - - def remove(self, *mobjects: Mobject): + def remove(self, *mobjects_to_remove: OpenGLMobject): """ - Removes mobjects in the passed list of mobjects - from the scene and the foreground, by removing them - from "mobjects" and "foreground_mobjects" + Removes anything in mobjects from scenes mobject list, but in the event that one + of the items to be removed is a member of the family of an item in mobject_list, + the other family members are added back into the list. - Parameters - ---------- - *mobjects - The mobjects to remove. + For example, if the scene includes Group(m1, m2, m3), and we call scene.remove(m1), + the desired behavior is for the scene to then include m2 and m3 (ungrouped). """ - if config.renderer == RendererType.OPENGL: - mobjects_to_remove = [] - meshes_to_remove = set() - for mobject_or_mesh in mobjects: - if isinstance(mobject_or_mesh, Object3D): - meshes_to_remove.add(mobject_or_mesh) - else: - mobjects_to_remove.append(mobject_or_mesh) - self.mobjects = restructure_list_to_exclude_certain_family_members( - self.mobjects, - mobjects_to_remove, - ) - self.meshes = list( - filter(lambda mesh: mesh not in set(meshes_to_remove), self.meshes), - ) - return self - elif config.renderer == RendererType.CAIRO: - for list_name in "mobjects", "foreground_mobjects": - self.restructure_mobjects(mobjects, list_name, False) - return self + for mob in mobjects_to_remove: + # First restructure self.mobjects so that parents/grandparents/etc. are replaced + # with their children, likewise for all ancestors in the extended family. + for ancestor in mob.get_ancestors(extended=True): + self.replace(ancestor, *ancestor.submobjects) + self.mobjects = list_difference_update(self.mobjects, mob.get_family()) + return self - def replace(self, old_mobject: Mobject, new_mobject: Mobject) -> None: - """Replace one mobject in the scene with another, preserving draw order. + def replace(self, mobject: OpenGLMobject, *replacements: OpenGLMobject): + """Replace one Mobject in the scene with one or more other Mobjects, + preserving draw order. - If ``old_mobject`` is a submobject of some other Mobject (e.g. a - :class:`.Group`), the new_mobject will replace it inside the group, - without otherwise changing the parent mobject. + If ``mobject`` is a submobject of some other :class:`OpenGLMobject` + (e.g. a :class:`.Group`), the ``replacements`` will replace it inside + the group, without otherwise changing the parent mobject. Parameters ---------- - old_mobject + mobject The mobject to be replaced. Must be present in the scene. - new_mobject - A mobject which must not already be in the scene. + replacements + One or more Mobjects which must not already be in the scene. """ - if old_mobject is None or new_mobject is None: - raise ValueError("Specified mobjects cannot be None") - - def replace_in_list( - mobj_list: list[Mobject], old_m: Mobject, new_m: Mobject - ) -> bool: - # We use breadth-first search because some Mobjects get very deep and - # we expect top-level elements to be the most common targets for replace. - for i in range(0, len(mobj_list)): - # Is this the old mobject? - if mobj_list[i] == old_m: - # If so, write the new object to the same spot and stop looking. - mobj_list[i] = new_m - return True - # Now check all the children of all these mobs. - for mob in mobj_list: # noqa: SIM110 - if replace_in_list(mob.submobjects, old_m, new_m): - # If we found it in a submobject, stop looking. - return True - # If we did not find the mobject in the mobject list or any submobjects, - # (or the list was empty), indicate we did not make the replacement. - return False - - # Make use of short-circuiting conditionals to check mobjects and then - # foreground_mobjects - replaced = replace_in_list( - self.mobjects, old_mobject, new_mobject - ) or replace_in_list(self.foreground_mobjects, old_mobject, new_mobject) - - if not replaced: - raise ValueError(f"Could not find {old_mobject} in scene") + if mobject in self.mobjects: + index = self.mobjects.index(mobject) + self.mobjects = [ + *self.mobjects[:index], + *replacements, + *self.mobjects[index + 1 :], + ] + return self def add_updater(self, func: Callable[[float], None]) -> None: """Add an update function to the scene. @@ -625,402 +281,77 @@ def remove_updater(self, func: Callable[[float], None]) -> None: """ self.updaters = [f for f in self.updaters if f is not func] - def restructure_mobjects( - self, - to_remove: Sequence[Mobject], - mobject_list_name: str = "mobjects", - extract_families: bool = True, - ): - """ - tl:wr - If your scene has a Group(), and you removed a mobject from the Group, - this dissolves the group and puts the rest of the mobjects directly - in self.mobjects or self.foreground_mobjects. - - In cases where the scene contains a group, e.g. Group(m1, m2, m3), but one - of its submobjects is removed, e.g. scene.remove(m1), the list of mobjects - will be edited to contain other submobjects, but not m1, e.g. it will now - insert m2 and m3 to where the group once was. - - Parameters - ---------- - to_remove - The Mobject to remove. - - mobject_list_name - The list of mobjects ("mobjects", "foreground_mobjects" etc) to remove from. - - extract_families - Whether the mobject's families should be recursively extracted. - - Returns - ------- - Scene - The Scene mobject with restructured Mobjects. - """ - if extract_families: - to_remove = extract_mobject_family_members( - to_remove, - use_z_index=self.renderer.camera.use_z_index, - ) - _list = getattr(self, mobject_list_name) - new_list = self.get_restructured_mobject_list(_list, to_remove) - setattr(self, mobject_list_name, new_list) - return self - - def get_restructured_mobject_list(self, mobjects: list, to_remove: list): - """ - Given a list of mobjects and a list of mobjects to be removed, this - filters out the removable mobjects from the list of mobjects. - - Parameters - ---------- - - mobjects - The Mobjects to check. - - to_remove - The list of mobjects to remove. - - Returns - ------- - list - The list of mobjects with the mobjects to remove removed. - """ - new_mobjects = [] - - def add_safe_mobjects_from_list(list_to_examine, set_to_remove): - for mob in list_to_examine: - if mob in set_to_remove: - continue - intersect = set_to_remove.intersection(mob.get_family()) - if intersect: - add_safe_mobjects_from_list(mob.submobjects, intersect) - else: - new_mobjects.append(mob) - - add_safe_mobjects_from_list(mobjects, set(to_remove)) - return new_mobjects - - # TODO, remove this, and calls to this - def add_foreground_mobjects(self, *mobjects: Mobject): - """ - Adds mobjects to the foreground, and internally to the list - foreground_mobjects, and mobjects. - - Parameters - ---------- - *mobjects - The Mobjects to add to the foreground. - - Returns - ------ - Scene - The Scene, with the foreground mobjects added. - """ - self.foreground_mobjects = list_update(self.foreground_mobjects, mobjects) + def bring_to_front(self, *mobjects: OpenGLMobject): self.add(*mobjects) return self - def add_foreground_mobject(self, mobject: Mobject): - """ - Adds a single mobject to the foreground, and internally to the list - foreground_mobjects, and mobjects. - - Parameters - ---------- - mobject - The Mobject to add to the foreground. - - Returns - ------ - Scene - The Scene, with the foreground mobject added. - """ - return self.add_foreground_mobjects(mobject) - - def remove_foreground_mobjects(self, *to_remove: Mobject): - """ - Removes mobjects from the foreground, and internally from the list - foreground_mobjects. - - Parameters - ---------- - *to_remove - The mobject(s) to remove from the foreground. - - Returns - ------ - Scene - The Scene, with the foreground mobjects removed. - """ - self.restructure_mobjects(to_remove, "foreground_mobjects") - return self - - def remove_foreground_mobject(self, mobject: Mobject): - """ - Removes a single mobject from the foreground, and internally from the list - foreground_mobjects. - - Parameters - ---------- - mobject - The mobject to remove from the foreground. - - Returns - ------ - Scene - The Scene, with the foreground mobject removed. - """ - return self.remove_foreground_mobjects(mobject) - - def bring_to_front(self, *mobjects: Mobject): - """ - Adds the passed mobjects to the scene again, - pushing them to he front of the scene. - - Parameters - ---------- - *mobjects - The mobject(s) to bring to the front of the scene. - - Returns - ------ - Scene - The Scene, with the mobjects brought to the front - of the scene. - """ - self.add(*mobjects) - return self - - def bring_to_back(self, *mobjects: Mobject): - """ - Removes the mobject from the scene and - adds them to the back of the scene. - - Parameters - ---------- - *mobjects - The mobject(s) to push to the back of the scene. - - Returns - ------ - Scene - The Scene, with the mobjects pushed to the back - of the scene. - """ + def bring_to_back(self, *mobjects: OpenGLMobject): self.remove(*mobjects) - self.mobjects = list(mobjects) + self.mobjects + self.mobjects = [*mobjects, *self.mobjects] return self def clear(self): - """ - Removes all mobjects present in self.mobjects - and self.foreground_mobjects from the scene. - - Returns - ------ - Scene - The Scene, with all of its mobjects in - self.mobjects and self.foreground_mobjects - removed. - """ - self.mobjects = [] - self.foreground_mobjects = [] + self.mobjects.clear() return self - def get_moving_mobjects(self, *animations: Animation): - """ - Gets all moving mobjects in the passed animation(s). + def get_mobjects(self) -> Sequence[OpenGLMobject]: + return list(self.mobjects) - Parameters - ---------- - *animations - The animations to check for moving mobjects. + def get_mobject_copies(self) -> Sequence[OpenGLMobject]: + return [m.copy() for m in self.mobjects] - Returns - ------ - list - The list of mobjects that could be moving in - the Animation(s) - """ - # Go through mobjects from start to end, and - # as soon as there's one that needs updating of - # some kind per frame, return the list from that - # point forward. - animation_mobjects = [anim.mobject for anim in animations] - mobjects = self.get_mobject_family_members() - for i, mob in enumerate(mobjects): - update_possibilities = [ - mob in animation_mobjects, - len(mob.get_family_updaters()) > 0, - mob in self.foreground_mobjects, - ] - if any(update_possibilities): - return mobjects[i:] - return [] - - def get_moving_and_static_mobjects(self, animations): - all_mobjects = list_update(self.mobjects, self.foreground_mobjects) - all_mobject_families = extract_mobject_family_members( - all_mobjects, - use_z_index=self.renderer.camera.use_z_index, - only_those_with_points=True, - ) - moving_mobjects = self.get_moving_mobjects(*animations) - all_moving_mobject_families = extract_mobject_family_members( - moving_mobjects, - use_z_index=self.renderer.camera.use_z_index, - ) - static_mobjects = list_difference_update( - all_mobject_families, - all_moving_mobject_families, - ) - return all_moving_mobject_families, static_mobjects - - def compile_animations( + def point_to_mobject( self, - *args: Animation | Mobject | _AnimationBuilder, - **kwargs, - ): - """ - Creates _MethodAnimations from any _AnimationBuilders and updates animation - kwargs with kwargs passed to play(). - - Parameters - ---------- - *args - Animations to be played. - **kwargs - Configuration for the call to play(). - - Returns - ------- - Tuple[:class:`Animation`] - Animations to be played. - """ - animations = [] - arg_anims = flatten_iterable_parameters(args) - # Allow passing a generator to self.play instead of comma separated arguments - for arg in arg_anims: - try: - animations.append(prepare_animation(arg)) - except TypeError as e: - if inspect.ismethod(arg): - raise TypeError( - "Passing Mobject methods to Scene.play is no longer" - " supported. Use Mobject.animate instead.", - ) from e - else: - raise TypeError( - f"Unexpected argument {arg} passed to Scene.play().", - ) from e - - for animation in animations: - for k, v in kwargs.items(): - setattr(animation, k, v) - - return animations - - def _get_animation_time_progression( - self, animations: list[Animation], duration: float - ): - """ - You will hardly use this when making your own animations. - This method is for Manim's internal use. - - Uses :func:`~.get_time_progression` to obtain a - CommandLine ProgressBar whose ``fill_time`` is - dependent on the qualities of the passed Animation, - - Parameters - ---------- - animations - The list of animations to get - the time progression for. - - duration - duration of wait time - - Returns - ------- - time_progression - The CommandLine Progress Bar. - """ - if len(animations) == 1 and isinstance(animations[0], Wait): - stop_condition = animations[0].stop_condition - if stop_condition is not None: - time_progression = self.get_time_progression( - duration, - f"Waiting for {stop_condition.__name__}", - n_iterations=-1, # So it doesn't show % progress - override_skip_animations=True, - ) - else: - time_progression = self.get_time_progression( - duration, - f"Waiting {self.renderer.num_plays}", - ) + point: np.ndarray, + search_set: Reversible[OpenGLMobject] | None = None, + buff: float = 0, + ) -> OpenGLMobject | None: + """ + E.g. if clicking on the scene, this returns the top layer mobject + under a given point + """ + if search_set is None: + search_set = self.mobjects + for mobject in reversed(search_set): + if mobject.is_point_touching(point, buff=buff): + return mobject + return None + + def get_group(self, *mobjects): + if all(isinstance(m, VMobject) for m in mobjects): + return VGroup(*mobjects) else: - time_progression = self.get_time_progression( - duration, - "".join( - [ - f"Animation {self.renderer.num_plays}: ", - str(animations[0]), - (", etc." if len(animations) > 1 else ""), - ], - ), - ) - return time_progression + return Group(*mobjects) - def get_time_progression( - self, - run_time: float, - description, - n_iterations: int | None = None, - override_skip_animations: bool = False, - ): - """ - You will hardly use this when making your own animations. - This method is for Manim's internal use. + # Related to skipping - Returns a CommandLine ProgressBar whose ``fill_time`` - is dependent on the ``run_time`` of an animation, - the iterations to perform in that animation - and a bool saying whether or not to consider - the skipped animations. + # Methods associated with running animations + def pre_play(self) -> None: + """To be implemented in subclasses.""" - Parameters - ---------- - run_time - The ``run_time`` of the animation. + def post_play(self) -> None: + self.num_plays += 1 - n_iterations - The number of iterations in the animation. + def begin_animations(self, animations: Iterable[AnimationProtocol]) -> None: + for animation in animations: + animation.begin() + self.process_buffer(animation.buffer) - override_skip_animations - Whether or not to show skipped animations in the progress bar. + def _update_animations( + self, animations: Iterable[AnimationProtocol], t: float, dt: float + ): + for animation in animations: + animation.update_mobjects(dt) + alpha = t / animation.get_run_time() + animation.interpolate(alpha) + if animation.apply_buffer: + self.process_buffer(animation.buffer) + animation.apply_buffer = False - Returns - ------- - time_progression - The CommandLine Progress Bar. - """ - if self.renderer.skip_animations and not override_skip_animations: - times = [run_time] - else: - step = 1 / config["frame_rate"] - times = np.arange(0, run_time, step) - time_progression = tqdm( - times, - desc=description, - total=n_iterations, - leave=config["progress_bar"] == "leave", - ascii=True if platform.system() == "Windows" else None, - disable=config["progress_bar"] == "none", - ) - return time_progression + def finish_animations(self, animations: Iterable[AnimationProtocol]) -> None: + for animation in animations: + animation.finish() + self.process_buffer(animation.buffer) @classmethod def validate_run_time( @@ -1051,719 +382,276 @@ def validate_run_time( return run_time - def get_run_time(self, animations: list[Animation]): - """ - Gets the total run time for a list of animations. - - Parameters - ---------- - animations - A list of the animations whose total - ``run_time`` is to be calculated. - - Returns - ------- - float - The total ``run_time`` of all of the animations in the list. - """ - run_time = max(animation.run_time for animation in animations) - run_time = self.validate_run_time(run_time, self.play, "total run_time") - return run_time - def play( self, - *args: Animation | Mobject | _AnimationBuilder, - subcaption=None, - subcaption_duration=None, - subcaption_offset=0, - **kwargs, - ): - r"""Plays an animation in this scene. - - Parameters - ---------- + # the OpenGLMobject is a side-effect of the return type of animate, it will + # raise a ValueError + *proto_animations: AnimationProtocol + | _AnimationBuilder[OpenGLMobject] + | OpenGLMobject, + run_time: float | None = None, + rate_func: Callable[[float], float] | None = None, + lag_ratio: float | None = None, + ) -> None: + if len(proto_animations) == 0: + logger.warning("Called Scene.play with no animations") + return - args - Animations to be played. - subcaption - The content of the external subcaption that should - be added during the animation. - subcaption_duration - The duration for which the specified subcaption is - added. If ``None`` (the default), the run time of the - animation is taken. - subcaption_offset - An offset (in seconds) for the start time of the - added subcaption. - kwargs - All other keywords are passed to the renderer. + # Build _AnimationBuilders. + animations = [prepare_animation(x) for x in proto_animations] + for anim in animations: + anim.update_rate_info(run_time, rate_func, lag_ratio) - """ - # If we are in interactive embedded mode, make sure this is running on the main thread (required for OpenGL) - if ( - self.interactive_mode - and config.renderer == RendererType.OPENGL - and threading.current_thread().name != "MainThread" - ): - kwargs.update( - { - "subcaption": subcaption, - "subcaption_duration": subcaption_duration, - "subcaption_offset": subcaption_offset, - } - ) - self.queue.put( - ( - "play", - args, - kwargs, - ) - ) - return + # Validate the final run_time. + total_run_time = max(anim.get_run_time() for anim in animations) + new_total_run_time = self.validate_run_time( + total_run_time, self.play, "total run_time" + ) + if new_total_run_time != total_run_time: + for anim in animations: + anim.update_rate_info(new_total_run_time) - start_time = self.time - self.renderer.play(self, *args, **kwargs) - run_time = self.time - start_time - if subcaption: - if subcaption_duration is None: - subcaption_duration = run_time - # The start of the subcaption needs to be offset by the - # run_time of the animation because it is added after - # the animation has already been played (and Scene.time - # has already been updated). - self.add_subcaption( - content=subcaption, - duration=subcaption_duration, - offset=-run_time + subcaption_offset, - ) + # NOTE: Should be changed at some point with the 2 pass rendering system 21.06.2024 + self.manager._play(*animations) def wait( self, duration: float = DEFAULT_WAIT_TIME, stop_condition: Callable[[], bool] | None = None, - frozen_frame: bool | None = None, + note: str | None = None, + ignore_presenter_mode: bool = False, ): - """Plays a "no operation" animation. - - Parameters - ---------- - duration - The run time of the animation. - stop_condition - A function without positional arguments that is evaluated every time - a frame is rendered. The animation only stops when the return value - of the function is truthy, or when the time specified in ``duration`` - passes. - frozen_frame - If True, updater functions are not evaluated, and the animation outputs - a frozen frame. If False, updater functions are called and frames - are rendered as usual. If None (the default), the scene tries to - determine whether or not the frame is frozen on its own. - - See also - -------- - :class:`.Wait`, :meth:`.should_mobjects_update` - """ duration = self.validate_run_time(duration, self.wait, "duration") - self.play( - Wait( - run_time=duration, - stop_condition=stop_condition, - frozen_frame=frozen_frame, - ) - ) - - def pause(self, duration: float = DEFAULT_WAIT_TIME): - """Pauses the scene (i.e., displays a frozen frame). - - This is an alias for :meth:`.wait` with ``frozen_frame`` - set to ``True``. - - Parameters - ---------- - duration - The duration of the pause. - - See also - -------- - :meth:`.wait`, :class:`.Wait` - """ - duration = self.validate_run_time(duration, self.pause, "duration") - self.wait(duration=duration, frozen_frame=True) + self.manager._wait(duration, stop_condition=stop_condition) + # if ( + # self.presenter_mode + # and not self.skip_animations + # and not ignore_presenter_mode + # ): + # if note: + # logger.info(note) + # self.hold_loop() def wait_until(self, stop_condition: Callable[[], bool], max_time: float = 60): - """Wait until a condition is satisfied, up to a given maximum duration. - - Parameters - ---------- - stop_condition - A function with no arguments that determines whether or not the - scene should keep waiting. - max_time - The maximum wait time in seconds. - """ max_time = self.validate_run_time(max_time, self.wait_until, "max_time") self.wait(max_time, stop_condition=stop_condition) - def compile_animation_data( + def add_sound( self, - *animations: Animation | Mobject | _AnimationBuilder, - **play_kwargs, + sound_file: str, + time_offset: float = 0, + gain: float | None = None, + gain_to_background: float | None = None, ): - """Given a list of animations, compile the corresponding - static and moving mobjects, and gather the animation durations. + raise NotImplementedError("TODO") + time = self.time + time_offset + self.file_writer.add_sound(sound_file, time, gain, gain_to_background) - This also begins the animations. + def get_state(self) -> SceneState: + return SceneState(self) - Parameters - ---------- - animations - Animation or mobject with mobject method and params - play_kwargs - Named parameters affecting what was passed in ``animations``, - e.g. ``run_time``, ``lag_ratio`` and so on. + def restore_state(self, scene_state: SceneState): + scene_state.restore_scene(self) - Returns - ------- - self, None - None if there is nothing to play, or self otherwise. - """ - # NOTE TODO : returns statement of this method are wrong. It should return nothing, as it makes a little sense to get any information from this method. - # The return are kept to keep webgl renderer from breaking. - if len(animations) == 0: - raise ValueError("Called Scene.play with no animations") - - self.animations = self.compile_animations(*animations, **play_kwargs) - self.add_mobjects_from_animations(self.animations) - - self.last_t = 0 - self.stop_condition = None - self.moving_mobjects = [] - self.static_mobjects = [] - - self.duration = self.get_run_time(self.animations) - if len(self.animations) == 1 and isinstance(self.animations[0], Wait): - if self.should_update_mobjects(): - self.update_mobjects(dt=0) # Any problems with this? - self.stop_condition = self.animations[0].stop_condition - else: - # Static image logic when the wait is static is done by the renderer, not here. - self.animations[0].is_static_wait = True - return None + def save_state(self) -> None: + if not config.preview: + return + state = self.get_state() + if self.undo_stack and state.mobjects_match(self.undo_stack[-1]): + return + self.redo_stack = [] + self.undo_stack.append(state) + if len(self.undo_stack) > self.max_num_saved_states: + self.undo_stack.popleft() - return self + def undo(self): + if self.undo_stack: + self.redo_stack.append(self.get_state()) + self.restore_state(self.undo_stack.pop()) - def begin_animations(self) -> None: - """Start the animations of the scene.""" - for animation in self.animations: - animation._setup_scene(self) - animation.begin() + def redo(self): + if self.redo_stack: + self.undo_stack.append(self.get_state()) + self.restore_state(self.redo_stack.pop()) - if config.renderer == RendererType.CAIRO: - # 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 ( - isinstance(self.animations[0], Wait) - and len(self.animations) == 1 - and self.animations[0].is_static_wait - ) + # TODO: reimplement checkpoint feature with CE's section API + # Event handling - def play_internal(self, skip_rendering: bool = False): - """ - This method is used to prep the animations for rendering, - apply the arguments and parameters required to them, - render them, and write them to the video file. + def on_mouse_motion(self, point: np.ndarray, d_point: np.ndarray) -> None: + self.mouse_point.move_to(point) - Parameters - ---------- - skip_rendering - Whether the rendering should be skipped, by default False - """ - self.duration = self.get_run_time(self.animations) - self.time_progression = self._get_animation_time_progression( - self.animations, - self.duration, + event_data = {"point": point, "d_point": d_point} + propagate_event = EVENT_DISPATCHER.dispatch( + EventType.MouseMotionEvent, **event_data ) - for t in self.time_progression: - self.update_to_time(t) - if not skip_rendering and not self.skip_animation_preview: - self.renderer.render(self, t, self.moving_mobjects) - if self.stop_condition is not None and self.stop_condition(): - self.time_progression.close() - break - - for animation in self.animations: - animation.finish() - animation.clean_up_from_scene(self) - if not self.renderer.skip_animations: - self.update_mobjects(0) - self.renderer.static_image = None - # Closing the progress bar at the end of the play. - self.time_progression.close() - - def check_interactive_embed_is_valid(self): - if config["force_window"]: - return True - if self.skip_animation_preview: - logger.warning( - "Disabling interactive embed as 'skip_animation_preview' is enabled", - ) - return False - elif config["write_to_movie"]: - logger.warning("Disabling interactive embed as 'write_to_movie' is enabled") - return False - elif config["format"]: - logger.warning( - "Disabling interactive embed as '--format' is set as " - + config["format"], - ) - return False - elif not self.renderer.window: - logger.warning("Disabling interactive embed as no window was created") - return False - elif config.dry_run: - logger.warning("Disabling interactive embed as dry_run is enabled") - return False - return True - - def interactive_embed(self): - """Like embed(), but allows for screen interaction.""" - if not self.check_interactive_embed_is_valid(): + if propagate_event is not None and propagate_event is False: return - self.interactive_mode = True - - def ipython(shell, namespace): - import manim.opengl - - def load_module_into_namespace(module, namespace): - for name in dir(module): - namespace[name] = getattr(module, name) - - load_module_into_namespace(manim, namespace) - load_module_into_namespace(manim.opengl, namespace) - - def embedded_rerun(*args, **kwargs): - self.queue.put(("rerun_keyboard", args, kwargs)) - shell.exiter() - - namespace["rerun"] = embedded_rerun - shell(local_ns=namespace) - self.queue.put(("exit_keyboard", [], {})) - - def get_embedded_method(method_name): - return lambda *args, **kwargs: self.queue.put((method_name, args, kwargs)) - - local_namespace = inspect.currentframe().f_back.f_locals - for method in ("play", "wait", "add", "remove"): - embedded_method = get_embedded_method(method) - # Allow for calling scene methods without prepending 'self.'. - local_namespace[method] = embedded_method - - from sqlite3 import connect - - from IPython.core.getipython import get_ipython - from IPython.terminal.embed import InteractiveShellEmbed - from traitlets.config import Config + # TODO + return + frame = self.camera.frame + # Handle perspective changes + if self.window.is_key_pressed(ord(PAN_3D_KEY)): + frame.increment_theta(-self.pan_sensitivity * d_point[0]) + frame.increment_phi(self.pan_sensitivity * d_point[1]) + # Handle frame movements + elif self.window.is_key_pressed(ord(FRAME_SHIFT_KEY)): + shift = -d_point + shift[0] *= frame.get_width() / 2 + shift[1] *= frame.get_height() / 2 + transform = frame.get_inverse_camera_rotation_matrix() + shift = np.dot(np.transpose(transform), shift) + frame.shift(shift) - cfg = Config() - cfg.TerminalInteractiveShell.confirm_exit = False - if get_ipython() is None: - shell = InteractiveShellEmbed.instance(config=cfg) - else: - shell = InteractiveShellEmbed(config=cfg) - hist = get_ipython().history_manager - hist.db = connect(hist.hist_file, check_same_thread=False) + def on_mouse_drag( + self, point: np.ndarray, d_point: np.ndarray, buttons: int, modifiers: int + ) -> None: + self.mouse_drag_point.move_to(point) - keyboard_thread = threading.Thread( - target=ipython, - args=(shell, local_namespace), + event_data = { + "point": point, + "d_point": d_point, + "buttons": buttons, + "modifiers": modifiers, + } + propagate_event = EVENT_DISPATCHER.dispatch( + EventType.MouseDragEvent, **event_data ) - # run as daemon to kill thread when main thread exits - if not shell.pt_app: - keyboard_thread.daemon = True - keyboard_thread.start() - - if self.dearpygui_imported and config["enable_gui"]: - if not dpg.is_dearpygui_running(): - gui_thread = threading.Thread( - target=configure_pygui, - args=(self.renderer, self.widgets), - kwargs={"update": False}, - ) - gui_thread.start() - else: - configure_pygui(self.renderer, self.widgets, update=True) - - self.camera.model_matrix = self.camera.default_model_matrix - - self.interact(shell, keyboard_thread) - - def interact(self, shell, keyboard_thread): - event_handler = RerunSceneHandler(self.queue) - file_observer = Observer() - file_observer.schedule(event_handler, config["input_file"], recursive=True) - file_observer.start() - - self.quit_interaction = False - keyboard_thread_needs_join = shell.pt_app is not None - assert self.queue.qsize() == 0 - - last_time = time.time() - while not (self.renderer.window.is_closing or self.quit_interaction): - if not self.queue.empty(): - tup = self.queue.get_nowait() - if tup[0].startswith("rerun"): - # Intentionally skip calling join() on the file thread to save time. - if not tup[0].endswith("keyboard"): - if shell.pt_app: - shell.pt_app.app.exit(exception=EOFError) - file_observer.unschedule_all() - raise RerunSceneException - keyboard_thread.join() - - kwargs = tup[2] - if "from_animation_number" in kwargs: - config["from_animation_number"] = kwargs[ - "from_animation_number" - ] - # # TODO: This option only makes sense if interactive_embed() is run at the - # # end of a scene by default. - # if "upto_animation_number" in kwargs: - # config["upto_animation_number"] = kwargs[ - # "upto_animation_number" - # ] - - keyboard_thread.join() - file_observer.unschedule_all() - raise RerunSceneException - elif tup[0].startswith("exit"): - # Intentionally skip calling join() on the file thread to save time. - if not tup[0].endswith("keyboard") and shell.pt_app: - shell.pt_app.app.exit(exception=EOFError) - keyboard_thread.join() - # Remove exit_keyboard from the queue if necessary. - while self.queue.qsize() > 0: - self.queue.get() - keyboard_thread_needs_join = False - break - else: - method, args, kwargs = tup - getattr(self, method)(*args, **kwargs) - else: - self.renderer.animation_start_time = 0 - dt = time.time() - last_time - last_time = time.time() - self.renderer.render(self, dt, self.moving_mobjects) - self.update_mobjects(dt) - self.update_meshes(dt) - self.update_self(dt) - - # Join the keyboard thread if necessary. - if shell is not None and keyboard_thread_needs_join: - shell.pt_app.app.exit(exception=EOFError) - keyboard_thread.join() - # Remove exit_keyboard from the queue if necessary. - while self.queue.qsize() > 0: - self.queue.get() - - file_observer.stop() - file_observer.join() - - if self.dearpygui_imported and config["enable_gui"]: - dpg.stop_dearpygui() - - if self.renderer.window.is_closing: - self.renderer.window.destroy() - - def embed(self): - if not config["preview"]: - logger.warning("Called embed() while no preview window is available.") - return - if config["write_to_movie"]: - logger.warning("embed() is skipped while writing to a file.") + if propagate_event is not None and propagate_event is False: return - self.renderer.animation_start_time = 0 - self.renderer.render(self, -1, self.moving_mobjects) - - # Configure IPython shell. - from IPython.terminal.embed import InteractiveShellEmbed - - shell = InteractiveShellEmbed() - - # Have the frame update after each command - shell.events.register( - "post_run_cell", - lambda *a, **kw: self.renderer.render(self, -1, self.moving_mobjects), + def on_mouse_press(self, point: np.ndarray, button: int, mods: int) -> None: + self.mouse_drag_point.move_to(point) + event_data = {"point": point, "button": button, "mods": mods} + propagate_event = EVENT_DISPATCHER.dispatch( + EventType.MousePressEvent, **event_data ) + if propagate_event is not None and propagate_event is False: + return - # Use the locals of the caller as the local namespace - # once embedded, and add a few custom shortcuts. - local_ns = inspect.currentframe().f_back.f_locals - # local_ns["touch"] = self.interact - for method in ( - "play", - "wait", - "add", - "remove", - "interact", - # "clear", - # "save_state", - # "restore", - ): - local_ns[method] = getattr(self, method) - shell(local_ns=local_ns, stack_depth=2) - - # End scene when exiting an embed. - raise Exception("Exiting scene.") - - def update_to_time(self, t): - dt = t - self.last_t - self.last_t = t - for animation in self.animations: - animation.update_mobjects(dt) - alpha = t / animation.run_time - animation.interpolate(alpha) - self.update_mobjects(dt) - self.update_meshes(dt) - self.update_self(dt) - - def add_subcaption( - self, content: str, duration: float = 1, offset: float = 0 - ) -> None: - r"""Adds an entry in the corresponding subcaption file - at the current time stamp. - - The current time stamp is obtained from ``Scene.time``. - - Parameters - ---------- - - content - The subcaption content. - duration - The duration (in seconds) for which the subcaption is shown. - offset - This offset (in seconds) is added to the starting time stamp - of the subcaption. - - Examples - -------- - - This example illustrates both possibilities for adding - subcaptions to Manimations:: - - class SubcaptionExample(Scene): - def construct(self): - square = Square() - circle = Circle() - - # first option: via the add_subcaption method - self.add_subcaption("Hello square!", duration=1) - self.play(Create(square)) - - # second option: within the call to Scene.play - self.play( - Transform(square, circle), subcaption="The square transforms." - ) - - """ - subtitle = srt.Subtitle( - index=len(self.renderer.file_writer.subcaptions), - content=content, - start=datetime.timedelta(seconds=float(self.time + offset)), - end=datetime.timedelta(seconds=float(self.time + offset + duration)), + def on_mouse_release(self, point: np.ndarray, button: int, mods: int) -> None: + event_data = {"point": point, "button": button, "mods": mods} + propagate_event = EVENT_DISPATCHER.dispatch( + EventType.MouseReleaseEvent, **event_data ) - self.renderer.file_writer.subcaptions.append(subtitle) - - def add_sound( - self, - sound_file: str, - time_offset: float = 0, - gain: float | None = None, - **kwargs, - ): - """ - This method is used to add a sound to the animation. - - Parameters - ---------- - - sound_file - The path to the sound file. - time_offset - The offset in the sound file after which - the sound can be played. - gain - Amplification of the sound. - - Examples - -------- - .. manim:: SoundExample - :no_autoplay: - - class SoundExample(Scene): - # Source of sound under Creative Commons 0 License. https://freesound.org/people/Druminfected/sounds/250551/ - def construct(self): - dot = Dot().set_color(GREEN) - self.add_sound("click.wav") - self.add(dot) - self.wait() - self.add_sound("click.wav") - dot.set_color(BLUE) - self.wait() - self.add_sound("click.wav") - dot.set_color(RED) - self.wait() - - Download the resource for the previous example `here `_ . - """ - if self.renderer.skip_animations: + if propagate_event is not None and propagate_event is False: return - time = self.time + time_offset - self.renderer.file_writer.add_sound(sound_file, time, gain, **kwargs) - def on_mouse_motion(self, point, d_point): - self.mouse_point.move_to(point) - if SHIFT_VALUE in self.renderer.pressed_keys: - shift = -d_point - shift[0] *= self.camera.get_width() / 2 - shift[1] *= self.camera.get_height() / 2 - transform = self.camera.inverse_rotation_matrix - shift = np.dot(np.transpose(transform), shift) - self.camera.shift(shift) + def on_mouse_scroll(self, point: np.ndarray, offset: np.ndarray) -> None: + event_data = {"point": point, "offset": offset} + propagate_event = EVENT_DISPATCHER.dispatch( + EventType.MouseScrollEvent, **event_data + ) + if propagate_event is not None and propagate_event is False: + return - def on_mouse_scroll(self, point, offset): - if not config.use_projection_stroke_shaders: - factor = 1 + np.arctan(-2.1 * offset[1]) - self.camera.scale(factor, about_point=self.camera_target) - self.mouse_scroll_orbit_controls(point, offset) + frame = self.camera.frame + if self.window.is_key_pressed(ord(ZOOM_KEY)): + factor = 1 + np.arctan(10 * offset[1]) + frame.scale(1 / factor, about_point=point) + else: + transform = frame.get_inverse_camera_rotation_matrix() + shift = np.dot(np.transpose(transform), offset) + frame.shift(-20.0 * shift) + + def on_key_release(self, symbol: int, modifiers: int) -> None: + event_data = {"symbol": symbol, "modifiers": modifiers} + propagate_event = EVENT_DISPATCHER.dispatch( + EventType.KeyReleaseEvent, **event_data + ) + if propagate_event is not None and propagate_event is False: + return - def on_key_press(self, symbol, modifiers): + def on_key_press(self, symbol: int, modifiers: int) -> None: try: char = chr(symbol) except OverflowError: logger.warning("The value of the pressed key is too large.") return - if char == "r": - self.camera.to_default_state() - self.camera_target = np.array([0, 0, 0], dtype=np.float32) - elif char == "q": + event_data = {"symbol": symbol, "modifiers": modifiers} + propagate_event = EVENT_DISPATCHER.dispatch( + EventType.KeyPressEvent, **event_data + ) + if propagate_event is not None and propagate_event is False: + return + + if char == RESET_FRAME_KEY: + self.play(self.camera.frame.animate.to_default_state()) + elif char == "z" and modifiers == key.MOD_COMMAND: + self.undo() + elif char == "z" and modifiers == key.MOD_COMMAND | key.MOD_SHIFT: + self.redo() + # command + q or esc + elif (char == QUIT_KEY and modifiers == key.MOD_COMMAND) or char == key.ESCAPE: self.quit_interaction = True - else: - if char in self.key_to_function_map: - self.key_to_function_map[char]() + # Space or right arrow + elif char == " " or symbol == key.RIGHT: + self.hold_on_wait = False - def on_key_release(self, symbol, modifiers): + def on_resize(self, width: int, height: int) -> None: pass - def on_mouse_drag(self, point, d_point, buttons, modifiers): - self.mouse_drag_point.move_to(point) - if buttons == 1: - self.camera.increment_theta(-d_point[0]) - self.camera.increment_phi(d_point[1]) - elif buttons == 4: - camera_x_axis = self.camera.model_matrix[:3, 0] - horizontal_shift_vector = -d_point[0] * camera_x_axis - vertical_shift_vector = -d_point[1] * np.cross(OUT, camera_x_axis) - total_shift_vector = horizontal_shift_vector + vertical_shift_vector - self.camera.shift(1.1 * total_shift_vector) - - self.mouse_drag_orbit_controls(point, d_point, buttons, modifiers) - - def mouse_scroll_orbit_controls(self, point, offset): - camera_to_target = self.camera_target - self.camera.get_position() - camera_to_target *= np.sign(offset[1]) - shift_vector = 0.01 * camera_to_target - self.camera.model_matrix = ( - opengl.translation_matrix(*shift_vector) @ self.camera.model_matrix - ) + def on_show(self) -> None: + pass - def mouse_drag_orbit_controls(self, point, d_point, buttons, modifiers): - # Left click drag. - if buttons == 1: - # Translate to target the origin and rotate around the z axis. - self.camera.model_matrix = ( - opengl.rotation_matrix(z=-d_point[0]) - @ opengl.translation_matrix(*-self.camera_target) - @ self.camera.model_matrix - ) + def on_hide(self) -> None: + pass - # Rotation off of the z axis. - camera_position = self.camera.get_position() - camera_y_axis = self.camera.model_matrix[:3, 1] - axis_of_rotation = space_ops.normalize( - np.cross(camera_y_axis, camera_position), - ) - rotation_matrix = space_ops.rotation_matrix( - d_point[1], - axis_of_rotation, - homogeneous=True, - ) + def on_close(self) -> None: + pass - maximum_polar_angle = self.camera.maximum_polar_angle - minimum_polar_angle = self.camera.minimum_polar_angle - potential_camera_model_matrix = rotation_matrix @ self.camera.model_matrix - potential_camera_location = potential_camera_model_matrix[:3, 3] - potential_camera_y_axis = potential_camera_model_matrix[:3, 1] - sign = ( - np.sign(potential_camera_y_axis[2]) - if potential_camera_y_axis[2] != 0 - else 1 - ) - potential_polar_angle = sign * np.arccos( - potential_camera_location[2] - / np.linalg.norm(potential_camera_location), - ) - if minimum_polar_angle <= potential_polar_angle <= maximum_polar_angle: - self.camera.model_matrix = potential_camera_model_matrix +class SceneState: + def __init__( + self, scene: Scene, ignore: Iterable[OpenGLMobject] | None = None + ) -> None: + self.time = scene.time + self.num_plays = scene.num_plays + self.camera = scene.camera.copy() + self.mobjects_to_copies = OrderedDict.fromkeys(scene.mobjects) + if ignore: + for mob in ignore: + self.mobjects_to_copies.pop(mob, None) + + last_m2c = scene.undo_stack[-1].mobjects_to_copies if scene.undo_stack else {} + for mob in self.mobjects_to_copies: + # If it hasn't changed since the last state, just point to the + # same copy as before + if mob in last_m2c and last_m2c[mob].looks_identical(mob): + self.mobjects_to_copies[mob] = last_m2c[mob] else: - sign = np.sign(camera_y_axis[2]) if camera_y_axis[2] != 0 else 1 - current_polar_angle = sign * np.arccos( - camera_position[2] / np.linalg.norm(camera_position), - ) - if potential_polar_angle > maximum_polar_angle: - polar_angle_delta = maximum_polar_angle - current_polar_angle - else: - polar_angle_delta = minimum_polar_angle - current_polar_angle - rotation_matrix = space_ops.rotation_matrix( - polar_angle_delta, - axis_of_rotation, - homogeneous=True, - ) - self.camera.model_matrix = rotation_matrix @ self.camera.model_matrix - - # Translate to target the original target. - self.camera.model_matrix = ( - opengl.translation_matrix(*self.camera_target) - @ self.camera.model_matrix - ) - # Right click drag. - elif buttons == 4: - camera_x_axis = self.camera.model_matrix[:3, 0] - horizontal_shift_vector = -d_point[0] * camera_x_axis - vertical_shift_vector = -d_point[1] * np.cross(OUT, camera_x_axis) - total_shift_vector = horizontal_shift_vector + vertical_shift_vector - - self.camera.model_matrix = ( - opengl.translation_matrix(*total_shift_vector) - @ self.camera.model_matrix + self.mobjects_to_copies[mob] = mob.copy() + + @property + def mobjects(self) -> Sequence[OpenGLMobject]: + return tuple(self.mobjects_to_copies.keys()) + + def __eq__(self, state: Any) -> bool: + return isinstance(state, SceneState) and all( + ( + self.time == state.time, + self.num_plays == state.num_plays, + self.mobjects_to_copies == state.mobjects_to_copies, ) - self.camera_target += total_shift_vector + ) - def set_key_function(self, char, func): - self.key_to_function_map[char] = func + def __repr__(self) -> str: + return f"{type(self).__name__} of {len(self.mobjects_to_copies)} Mobjects" + + def mobjects_match(self, state: SceneState): + return self.mobjects_to_copies == state.mobjects_to_copies + + def n_changes(self, state: SceneState): + m2c = state.mobjects_to_copies + return sum( + 1 - int(mob in m2c and mob.looks_identical(m2c[mob])) + for mob in self.mobjects_to_copies + ) - def on_mouse_press(self, point, button, modifiers): - for func in self.mouse_press_callbacks: - func() + def restore_scene(self, scene: Scene): + scene.time = self.time + scene.num_plays = self.num_plays + scene.mobjects = [ + mob.become(mob_copy) for mob, mob_copy in self.mobjects_to_copies.items() + ] diff --git a/manim/scene/sections.py b/manim/scene/sections.py new file mode 100644 index 0000000000..ea7a6e5437 --- /dev/null +++ b/manim/scene/sections.py @@ -0,0 +1,88 @@ +from __future__ import annotations + +import types +from collections.abc import Callable +from typing import TYPE_CHECKING, ClassVar, Generic, ParamSpec, TypeVar, final + +from typing_extensions import Self + +if TYPE_CHECKING: + from .scene import Scene + +__all__ = ["group"] + + +P = ParamSpec("P") +T = TypeVar("T") + + +# mark as final because _cls_instance_count doesn't +# work with inheritance +@final +class group(Generic[P, T]): + """A group in a :class:`.Scene`. + + It holds data about each subsection, and keeps track of the order + of the sections via :attr:`order`. + + Example + ------- + + .. code-block:: python + + class MyScene(Scene): + groups_api = True + + @group + def my_section(self): + pass + + @my_section + def my_subsection(self): + pass + + @my_section + def my_subsection2(self): + pass + + """ + + _cls_instance_count: ClassVar[int] = 0 + """How many times the class has been instantiated. + + This is also used for ordering sections, because of the order + decorators are called in a class. + """ + + def __init__(self, func: Callable[P, T]) -> None: + self._func = func + self._order = self.__class__._cls_instance_count + + self.__class__._cls_instance_count += 1 + + @property + def name(self) -> str: + return self._func.__name__ + + def __str__(self) -> str: + name = self.name + return f"{self.__class__.__name__}({name=})" + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self._func!r})" + + def __lt__(self, other: object) -> bool: + if not isinstance(other, group): + return NotImplemented + return self._order < other._order + + def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T: + return self._func(*args, **kwargs) + + def __get__(self, instance: Scene, _owner: type[Scene]) -> Self: + """Descriptor to bind the group to the scene instance. + + This is called implicitly by python when methods are being bound. + """ + self._func = types.MethodType(self._func, instance) + return self diff --git a/manim/scene/three_d_scene.py b/manim/scene/three_d_scene.py deleted file mode 100644 index 7f39f4cf32..0000000000 --- a/manim/scene/three_d_scene.py +++ /dev/null @@ -1,544 +0,0 @@ -"""A scene suitable for rendering three-dimensional objects and animations.""" - -from __future__ import annotations - -__all__ = ["ThreeDScene", "SpecialThreeDScene"] - - -import warnings -from collections.abc import Iterable, Sequence - -import numpy as np - -from manim.mobject.geometry.line import Line -from manim.mobject.graphing.coordinate_systems import ThreeDAxes -from manim.mobject.opengl.opengl_mobject import OpenGLMobject -from manim.mobject.three_d.three_dimensions import Sphere -from manim.mobject.value_tracker import ValueTracker - -from .. import config -from ..animation.animation import Animation -from ..animation.transform import Transform -from ..camera.three_d_camera import ThreeDCamera -from ..constants import DEGREES, RendererType -from ..mobject.mobject import Mobject -from ..mobject.types.vectorized_mobject import VectorizedPoint, VGroup -from ..renderer.opengl_renderer import OpenGLCamera -from ..scene.scene import Scene -from ..utils.config_ops import merge_dicts_recursively - - -class ThreeDScene(Scene): - """ - This is a Scene, with special configurations and properties that - make it suitable for Three Dimensional Scenes. - """ - - def __init__( - self, - camera_class=ThreeDCamera, - ambient_camera_rotation=None, - default_angled_camera_orientation_kwargs=None, - **kwargs, - ): - self.ambient_camera_rotation = ambient_camera_rotation - if default_angled_camera_orientation_kwargs is None: - default_angled_camera_orientation_kwargs = { - "phi": 70 * DEGREES, - "theta": -135 * DEGREES, - } - self.default_angled_camera_orientation_kwargs = ( - default_angled_camera_orientation_kwargs - ) - super().__init__(camera_class=camera_class, **kwargs) - - def set_camera_orientation( - self, - phi: float | None = None, - theta: float | None = None, - gamma: float | None = None, - zoom: float | None = None, - focal_distance: float | None = None, - frame_center: Mobject | Sequence[float] | None = None, - **kwargs, - ): - """ - This method sets the orientation of the camera in the scene. - - Parameters - ---------- - phi - The polar angle i.e the angle between Z_AXIS and Camera through ORIGIN in radians. - - theta - The azimuthal angle i.e the angle that spins the camera around the Z_AXIS. - - focal_distance - The focal_distance of the Camera. - - gamma - The rotation of the camera about the vector from the ORIGIN to the Camera. - - zoom - The zoom factor of the scene. - - frame_center - The new center of the camera frame in cartesian coordinates. - - """ - if phi is not None: - self.renderer.camera.set_phi(phi) - if theta is not None: - self.renderer.camera.set_theta(theta) - if focal_distance is not None: - self.renderer.camera.set_focal_distance(focal_distance) - if gamma is not None: - self.renderer.camera.set_gamma(gamma) - if zoom is not None: - self.renderer.camera.set_zoom(zoom) - if frame_center is not None: - self.renderer.camera._frame_center.move_to(frame_center) - - def begin_ambient_camera_rotation(self, rate: float = 0.02, about: str = "theta"): - """ - This method begins an ambient rotation of the camera about the Z_AXIS, - in the anticlockwise direction - - Parameters - ---------- - rate - The rate at which the camera should rotate about the Z_AXIS. - Negative rate means clockwise rotation. - about - one of 3 options: ["theta", "phi", "gamma"]. defaults to theta. - """ - # TODO, use a ValueTracker for rate, so that it - # can begin and end smoothly - about: str = about.lower() - try: - if config.renderer == RendererType.CAIRO: - trackers = { - "theta": self.camera.theta_tracker, - "phi": self.camera.phi_tracker, - "gamma": self.camera.gamma_tracker, - } - x: ValueTracker = trackers[about] - x.add_updater(lambda m, dt: x.increment_value(rate * dt)) - self.add(x) - elif config.renderer == RendererType.OPENGL: - cam: OpenGLCamera = self.camera - methods = { - "theta": cam.increment_theta, - "phi": cam.increment_phi, - "gamma": cam.increment_gamma, - } - cam.add_updater(lambda m, dt: methods[about](rate * dt)) - self.add(self.camera) - except Exception as e: - raise ValueError("Invalid ambient rotation angle.") from e - - def stop_ambient_camera_rotation(self, about="theta"): - """This method stops all ambient camera rotation.""" - about: str = about.lower() - try: - if config.renderer == RendererType.CAIRO: - trackers = { - "theta": self.camera.theta_tracker, - "phi": self.camera.phi_tracker, - "gamma": self.camera.gamma_tracker, - } - x: ValueTracker = trackers[about] - x.clear_updaters() - self.remove(x) - elif config.renderer == RendererType.OPENGL: - self.camera.clear_updaters() - except Exception as e: - raise ValueError("Invalid ambient rotation angle.") from e - - def begin_3dillusion_camera_rotation( - self, - rate: float = 1, - origin_phi: float | None = None, - origin_theta: float | None = None, - ): - """ - This method creates a 3D camera rotation illusion around - the current camera orientation. - - Parameters - ---------- - rate - The rate at which the camera rotation illusion should operate. - origin_phi - The polar angle the camera should move around. Defaults - to the current phi angle. - origin_theta - The azimutal angle the camera should move around. Defaults - to the current theta angle. - """ - if origin_theta is None: - origin_theta = self.renderer.camera.theta_tracker.get_value() - if origin_phi is None: - origin_phi = self.renderer.camera.phi_tracker.get_value() - - val_tracker_theta = ValueTracker(0) - - def update_theta(m, dt): - val_tracker_theta.increment_value(dt * rate) - val_for_left_right = 0.2 * np.sin(val_tracker_theta.get_value()) - return m.set_value(origin_theta + val_for_left_right) - - self.renderer.camera.theta_tracker.add_updater(update_theta) - self.add(self.renderer.camera.theta_tracker) - - val_tracker_phi = ValueTracker(0) - - def update_phi(m, dt): - val_tracker_phi.increment_value(dt * rate) - val_for_up_down = 0.1 * np.cos(val_tracker_phi.get_value()) - 0.1 - return m.set_value(origin_phi + val_for_up_down) - - self.renderer.camera.phi_tracker.add_updater(update_phi) - self.add(self.renderer.camera.phi_tracker) - - def stop_3dillusion_camera_rotation(self): - """This method stops all illusion camera rotations.""" - self.renderer.camera.theta_tracker.clear_updaters() - self.remove(self.renderer.camera.theta_tracker) - self.renderer.camera.phi_tracker.clear_updaters() - self.remove(self.renderer.camera.phi_tracker) - - def move_camera( - self, - phi: float | None = None, - theta: float | None = None, - gamma: float | None = None, - zoom: float | None = None, - focal_distance: float | None = None, - frame_center: Mobject | Sequence[float] | None = None, - added_anims: Iterable[Animation] = [], - **kwargs, - ): - """ - This method animates the movement of the camera - to the given spherical coordinates. - - Parameters - ---------- - phi - The polar angle i.e the angle between Z_AXIS and Camera through ORIGIN in radians. - - theta - The azimuthal angle i.e the angle that spins the camera around the Z_AXIS. - - focal_distance - The radial focal_distance between ORIGIN and Camera. - - gamma - The rotation of the camera about the vector from the ORIGIN to the Camera. - - zoom - The zoom factor of the camera. - - frame_center - The new center of the camera frame in cartesian coordinates. - - added_anims - Any other animations to be played at the same time. - - """ - anims = [] - - if config.renderer == RendererType.CAIRO: - self.camera: ThreeDCamera - value_tracker_pairs = [ - (phi, self.camera.phi_tracker), - (theta, self.camera.theta_tracker), - (focal_distance, self.camera.focal_distance_tracker), - (gamma, self.camera.gamma_tracker), - (zoom, self.camera.zoom_tracker), - ] - for value, tracker in value_tracker_pairs: - if value is not None: - anims.append(tracker.animate.set_value(value)) - if frame_center is not None: - anims.append(self.camera._frame_center.animate.move_to(frame_center)) - elif config.renderer == RendererType.OPENGL: - cam: OpenGLCamera = self.camera - cam2 = cam.copy() - methods = { - "theta": cam2.set_theta, - "phi": cam2.set_phi, - "gamma": cam2.set_gamma, - "zoom": cam2.scale, - "frame_center": cam2.move_to, - } - if frame_center is not None: - if isinstance(frame_center, OpenGLMobject): - frame_center = frame_center.get_center() - frame_center = list(frame_center) - - zoom_value = None - if zoom is not None: - zoom_value = config.frame_height / (zoom * cam.height) - - for value, method in [ - [theta, "theta"], - [phi, "phi"], - [gamma, "gamma"], - [zoom_value, "zoom"], - [frame_center, "frame_center"], - ]: - if value is not None: - methods[method](value) - - if focal_distance is not None: - warnings.warn( - "focal distance of OpenGLCamera can not be adjusted.", - stacklevel=2, - ) - - anims += [Transform(cam, cam2)] - - self.play(*anims + added_anims, **kwargs) - - # These lines are added to improve performance. If manim thinks that frame_center is moving, - # it is required to redraw every object. These lines remove frame_center from the Scene once - # its animation is done, ensuring that manim does not think that it is moving. Since the - # frame_center is never actually drawn, this shouldn't break anything. - if frame_center is not None and config.renderer == RendererType.CAIRO: - self.remove(self.camera._frame_center) - - def get_moving_mobjects(self, *animations: Animation): - """ - This method returns a list of all of the Mobjects in the Scene that - are moving, that are also in the animations passed. - - Parameters - ---------- - *animations - The animations whose mobjects will be checked. - """ - moving_mobjects = super().get_moving_mobjects(*animations) - camera_mobjects = self.renderer.camera.get_value_trackers() + [ - self.renderer.camera._frame_center, - ] - if any(cm in moving_mobjects for cm in camera_mobjects): - return self.mobjects - return moving_mobjects - - def add_fixed_orientation_mobjects(self, *mobjects: Mobject, **kwargs): - """ - This method is used to prevent the rotation and tilting - of mobjects as the camera moves around. The mobject can - still move in the x,y,z directions, but will always be - at the angle (relative to the camera) that it was at - when it was passed through this method.) - - Parameters - ---------- - *mobjects - The Mobject(s) whose orientation must be fixed. - - **kwargs - Some valid kwargs are - use_static_center_func : bool - center_func : function - """ - if config.renderer == RendererType.CAIRO: - self.add(*mobjects) - self.renderer.camera.add_fixed_orientation_mobjects(*mobjects, **kwargs) - elif config.renderer == RendererType.OPENGL: - for mob in mobjects: - mob: OpenGLMobject - mob.fix_orientation() - self.add(mob) - - def add_fixed_in_frame_mobjects(self, *mobjects: Mobject): - """ - This method is used to prevent the rotation and movement - of mobjects as the camera moves around. The mobject is - essentially overlaid, and is not impacted by the camera's - movement in any way. - - Parameters - ---------- - *mobjects - The Mobjects whose orientation must be fixed. - """ - if config.renderer == RendererType.CAIRO: - self.add(*mobjects) - self.camera: ThreeDCamera - self.camera.add_fixed_in_frame_mobjects(*mobjects) - elif config.renderer == RendererType.OPENGL: - for mob in mobjects: - mob: OpenGLMobject - mob.fix_in_frame() - self.add(mob) - - def remove_fixed_orientation_mobjects(self, *mobjects: Mobject): - """ - This method "unfixes" the orientation of the mobjects - passed, meaning they will no longer be at the same angle - relative to the camera. This only makes sense if the - mobject was passed through add_fixed_orientation_mobjects first. - - Parameters - ---------- - *mobjects - The Mobjects whose orientation must be unfixed. - """ - if config.renderer == RendererType.CAIRO: - self.renderer.camera.remove_fixed_orientation_mobjects(*mobjects) - elif config.renderer == RendererType.OPENGL: - for mob in mobjects: - mob: OpenGLMobject - mob.unfix_orientation() - self.remove(mob) - - def remove_fixed_in_frame_mobjects(self, *mobjects: Mobject): - """ - This method undoes what add_fixed_in_frame_mobjects does. - It allows the mobject to be affected by the movement of - the camera. - - Parameters - ---------- - *mobjects - The Mobjects whose position and orientation must be unfixed. - """ - if config.renderer == RendererType.CAIRO: - self.renderer.camera.remove_fixed_in_frame_mobjects(*mobjects) - elif config.renderer == RendererType.OPENGL: - for mob in mobjects: - mob: OpenGLMobject - mob.unfix_from_frame() - self.remove(mob) - - ## - def set_to_default_angled_camera_orientation(self, **kwargs): - """ - This method sets the default_angled_camera_orientation to the - keyword arguments passed, and sets the camera to that orientation. - - Parameters - ---------- - **kwargs - Some recognised kwargs are phi, theta, focal_distance, gamma, - which have the same meaning as the parameters in set_camera_orientation. - """ - config = dict( - self.default_camera_orientation_kwargs, - ) # Where doe this come from? - config.update(kwargs) - self.set_camera_orientation(**config) - - -class SpecialThreeDScene(ThreeDScene): - """An extension of :class:`ThreeDScene` with more settings. - - It has some extra configuration for axes, spheres, - and an override for low quality rendering. Further key differences - are: - - * The camera shades applicable 3DMobjects by default, - except if rendering in low quality. - * Some default params for Spheres and Axes have been added. - - """ - - def __init__( - self, - cut_axes_at_radius=True, - camera_config={"should_apply_shading": True, "exponential_projection": True}, - three_d_axes_config={ - "num_axis_pieces": 1, - "axis_config": { - "unit_size": 2, - "tick_frequency": 1, - "numbers_with_elongated_ticks": [0, 1, 2], - "stroke_width": 2, - }, - }, - sphere_config={"radius": 2, "resolution": (24, 48)}, - default_angled_camera_position={ - "phi": 70 * DEGREES, - "theta": -110 * DEGREES, - }, - # When scene is extracted with -l flag, this - # configuration will override the above configuration. - low_quality_config={ - "camera_config": {"should_apply_shading": False}, - "three_d_axes_config": {"num_axis_pieces": 1}, - "sphere_config": {"resolution": (12, 24)}, - }, - **kwargs, - ): - self.cut_axes_at_radius = cut_axes_at_radius - self.camera_config = camera_config - self.three_d_axes_config = three_d_axes_config - self.sphere_config = sphere_config - self.default_angled_camera_position = default_angled_camera_position - self.low_quality_config = low_quality_config - if self.renderer.camera_config["pixel_width"] == config["pixel_width"]: - _config = {} - else: - _config = self.low_quality_config - _config = merge_dicts_recursively(_config, kwargs) - super().__init__(**_config) - - def get_axes(self): - """Return a set of 3D axes. - - Returns - ------- - :class:`.ThreeDAxes` - A set of 3D axes. - """ - axes = ThreeDAxes(**self.three_d_axes_config) - for axis in axes: - if self.cut_axes_at_radius: - p0 = axis.get_start() - p1 = axis.number_to_point(-1) - p2 = axis.number_to_point(1) - p3 = axis.get_end() - new_pieces = VGroup(Line(p0, p1), Line(p1, p2), Line(p2, p3)) - for piece in new_pieces: - piece.shade_in_3d = True - new_pieces.match_style(axis.pieces) - axis.pieces.submobjects = new_pieces.submobjects - for tick in axis.tick_marks: - tick.add(VectorizedPoint(1.5 * tick.get_center())) - return axes - - def get_sphere(self, **kwargs): - """ - Returns a sphere with the passed keyword arguments as properties. - - Parameters - ---------- - **kwargs - Any valid parameter of :class:`~.Sphere` or :class:`~.Surface`. - - Returns - ------- - :class:`~.Sphere` - The sphere object. - """ - config = merge_dicts_recursively(self.sphere_config, kwargs) - return Sphere(**config) - - def get_default_camera_position(self): - """ - Returns the default_angled_camera position. - - Returns - ------- - dict - Dictionary of phi, theta, focal_distance, and gamma. - """ - return self.default_angled_camera_position - - def set_camera_to_default_position(self): - """Sets the camera to its default position.""" - self.set_camera_orientation(**self.default_angled_camera_position) diff --git a/manim/scene/vector_space_scene.py b/manim/scene/vector_space_scene.py index be75151471..a1a52056d6 100644 --- a/manim/scene/vector_space_scene.py +++ b/manim/scene/vector_space_scene.py @@ -13,6 +13,7 @@ from manim.mobject.geometry.polygram import Rectangle from manim.mobject.graphing.coordinate_systems import Axes, NumberPlane from manim.mobject.opengl.opengl_mobject import OpenGLMobject +from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject from manim.mobject.text.tex_mobject import MathTex, Tex from manim.utils.config_ops import update_dict_recursively @@ -1002,7 +1003,7 @@ def get_piece_movement(self, pieces: list | tuple | np.ndarray): Animation The animation of the movement. """ - v_pieces = [piece for piece in pieces if isinstance(piece, VMobject)] + v_pieces = [piece for piece in pieces if isinstance(piece, OpenGLVMobject)] start = VGroup(*v_pieces) target = VGroup(*(mob.target for mob in v_pieces)) diff --git a/manim/scene/zoomed_scene.py b/manim/scene/zoomed_scene.py deleted file mode 100644 index 361c4eaf55..0000000000 --- a/manim/scene/zoomed_scene.py +++ /dev/null @@ -1,209 +0,0 @@ -"""A scene supporting zooming in on a specified section. - - -Examples --------- - -.. manim:: UseZoomedScene - - class UseZoomedScene(ZoomedScene): - def construct(self): - dot = Dot().set_color(GREEN) - self.add(dot) - self.wait(1) - self.activate_zooming(animate=False) - self.wait(1) - self.play(dot.animate.shift(LEFT)) - -.. manim:: ChangingZoomScale - - class ChangingZoomScale(ZoomedScene): - def __init__(self, **kwargs): - ZoomedScene.__init__( - self, - zoom_factor=0.3, - zoomed_display_height=1, - zoomed_display_width=3, - image_frame_stroke_width=20, - zoomed_camera_config={ - "default_frame_stroke_width": 3, - }, - **kwargs - ) - - def construct(self): - dot = Dot().set_color(GREEN) - sq = Circle(fill_opacity=1, radius=0.2).next_to(dot, RIGHT) - self.add(dot, sq) - self.wait(1) - self.activate_zooming(animate=False) - self.wait(1) - self.play(dot.animate.shift(LEFT * 0.3)) - - self.play(self.zoomed_camera.frame.animate.scale(4)) - self.play(self.zoomed_camera.frame.animate.shift(0.5 * DOWN)) - -""" - -from __future__ import annotations - -__all__ = ["ZoomedScene"] - - -from ..animation.transform import ApplyMethod -from ..camera.moving_camera import MovingCamera -from ..camera.multi_camera import MultiCamera -from ..constants import * -from ..mobject.types.image_mobject import ImageMobjectFromCamera -from ..scene.moving_camera_scene import MovingCameraScene - -# Note, any scenes from old videos using ZoomedScene will almost certainly -# break, as it was restructured. - - -class ZoomedScene(MovingCameraScene): - """ - This is a Scene with special configurations made for when - a particular part of the scene must be zoomed in on and displayed - separately. - """ - - def __init__( - self, - camera_class=MultiCamera, - zoomed_display_height=3, - zoomed_display_width=3, - zoomed_display_center=None, - zoomed_display_corner=UP + RIGHT, - zoomed_display_corner_buff=DEFAULT_MOBJECT_TO_EDGE_BUFFER, - zoomed_camera_config={ - "default_frame_stroke_width": 2, - "background_opacity": 1, - }, - zoomed_camera_image_mobject_config={}, - zoomed_camera_frame_starting_position=ORIGIN, - zoom_factor=0.15, - image_frame_stroke_width=3, - zoom_activated=False, - **kwargs, - ): - self.zoomed_display_height = zoomed_display_height - self.zoomed_display_width = zoomed_display_width - self.zoomed_display_center = zoomed_display_center - self.zoomed_display_corner = zoomed_display_corner - self.zoomed_display_corner_buff = zoomed_display_corner_buff - self.zoomed_camera_config = zoomed_camera_config - self.zoomed_camera_image_mobject_config = zoomed_camera_image_mobject_config - self.zoomed_camera_frame_starting_position = ( - zoomed_camera_frame_starting_position - ) - self.zoom_factor = zoom_factor - self.image_frame_stroke_width = image_frame_stroke_width - self.zoom_activated = zoom_activated - super().__init__(camera_class=camera_class, **kwargs) - - def setup(self): - """ - This method is used internally by Manim to - setup the scene for proper use. - """ - super().setup() - # Initialize camera and display - zoomed_camera = MovingCamera(**self.zoomed_camera_config) - zoomed_display = ImageMobjectFromCamera( - zoomed_camera, **self.zoomed_camera_image_mobject_config - ) - zoomed_display.add_display_frame() - for mob in zoomed_camera.frame, zoomed_display: - mob.stretch_to_fit_height(self.zoomed_display_height) - mob.stretch_to_fit_width(self.zoomed_display_width) - zoomed_camera.frame.scale(self.zoom_factor) - - # Position camera and display - zoomed_camera.frame.move_to(self.zoomed_camera_frame_starting_position) - if self.zoomed_display_center is not None: - zoomed_display.move_to(self.zoomed_display_center) - else: - zoomed_display.to_corner( - self.zoomed_display_corner, - buff=self.zoomed_display_corner_buff, - ) - - self.zoomed_camera = zoomed_camera - self.zoomed_display = zoomed_display - - def activate_zooming(self, animate: bool = False): - """ - This method is used to activate the zooming for - the zoomed_camera. - - Parameters - ---------- - animate - Whether or not to animate the activation - of the zoomed camera. - """ - self.zoom_activated = True - self.renderer.camera.add_image_mobject_from_camera(self.zoomed_display) - if animate: - self.play(self.get_zoom_in_animation()) - self.play(self.get_zoomed_display_pop_out_animation()) - self.add_foreground_mobjects( - self.zoomed_camera.frame, - self.zoomed_display, - ) - - def get_zoom_in_animation(self, run_time: float = 2, **kwargs): - """ - Returns the animation of camera zooming in. - - Parameters - ---------- - run_time - The run_time of the animation of the camera zooming in. - **kwargs - Any valid keyword arguments of ApplyMethod() - - Returns - ------- - ApplyMethod - The animation of the camera zooming in. - """ - frame = self.zoomed_camera.frame - full_frame_height = self.camera.frame_height - full_frame_width = self.camera.frame_width - frame.save_state() - frame.stretch_to_fit_width(full_frame_width) - frame.stretch_to_fit_height(full_frame_height) - frame.center() - frame.set_stroke(width=0) - return ApplyMethod(frame.restore, run_time=run_time, **kwargs) - - def get_zoomed_display_pop_out_animation(self, **kwargs): - """ - This is the animation of the popping out of the - mini-display that shows the content of the zoomed - camera. - - Returns - ------- - ApplyMethod - The Animation of the Zoomed Display popping out. - """ - display = self.zoomed_display - display.save_state() - display.replace(self.zoomed_camera.frame, stretch=True) - return ApplyMethod(display.restore) - - def get_zoom_factor(self): - """ - Returns the Zoom factor of the Zoomed camera. - Defined as the ratio between the height of the - zoomed camera and the height of the zoomed mini - display. - Returns - ------- - float - The zoom factor. - """ - return self.zoomed_camera.frame.height / self.zoomed_display.height diff --git a/manim/typing.py b/manim/typing.py index 92815e95e9..8458b8b690 100644 --- a/manim/typing.py +++ b/manim/typing.py @@ -659,6 +659,9 @@ MappingFunction: TypeAlias = Callable[[Point3D], Point3D] """A function mapping a `Point3D` to another `Point3D`.""" +RateFunc: TypeAlias = Callable[[float], float] +r"""A rate function :math:`f: [0, 1] \to [0, 1]`.""" + """ [CATEGORY] diff --git a/manim/utils/bezier.py b/manim/utils/bezier.py index 09f6f60cae..efd5d3677a 100644 --- a/manim/utils/bezier.py +++ b/manim/utils/bezier.py @@ -13,6 +13,7 @@ "mid", "inverse_interpolate", "match_interpolate", + "get_smooth_quadratic_bezier_handle_points", "get_smooth_cubic_bezier_handle_points", "is_closed", "proportions_along_bezier_curve_for_point", @@ -1231,6 +1232,35 @@ def match_interpolate( ) +def get_smooth_quadratic_bezier_handle_points(points: Point3D_Array) -> Point3D_Array: + """Given three successive points, P0, P1 and P2, you can compute that by defining + h = (1/4) P0 + P1 - (1/4)P2, the bezier curve defined by (P0, h, P1) will pass + through the point P2. + + So for a given set of four successive points, P0, P1, P2, P3, if we want to add + a handle point h between P1 and P2 so that the quadratic bezier (P1, h, P2) is + part of a smooth curve passing through all four points, we calculate one solution + for h that would produce a parbola passing through P3, call it smooth_to_right, and + another that would produce a parabola passing through P0, call it smooth_to_left, + and use the midpoint between the two. + """ + if len(points) == 2: + return 0.5 * (points[0] + points[1]) + + smooth_to_right, smooth_to_left = ( + 0.25 * ps[0:-2] + ps[1:-1] - 0.25 * ps[2:] for ps in (points, points[::-1]) + ) + if np.isclose(points[0], points[-1]).all(): + last_str = 0.25 * points[-2] + points[-1] - 0.25 * points[1] + last_stl = 0.25 * points[1] + points[0] - 0.25 * points[-2] + else: + last_str = smooth_to_left[0] + last_stl = smooth_to_right[0] + handles = 0.5 * np.vstack([smooth_to_right, [last_str]]) + handles += 0.5 * np.vstack([last_stl, smooth_to_left[::-1]]) + return handles + + # Figuring out which Bézier curves most smoothly connect a sequence of points def get_smooth_cubic_bezier_handle_points( anchors: Point3D_Array, diff --git a/manim/utils/caching.py b/manim/utils/caching.py index a0e6772443..60f6051162 100644 --- a/manim/utils/caching.py +++ b/manim/utils/caching.py @@ -29,8 +29,6 @@ def handle_caching_play(func: Callable[..., None]): # method has to be deleted. def wrapper(self, scene, *args, **kwargs): - self.skip_animations = self._original_skipping_status - self.update_skipping_status() animations = scene.compile_animations(*args, **kwargs) scene.add_mobjects_from_animations(animations) if self.skip_animations: diff --git a/manim/utils/color/core.py b/manim/utils/color/core.py index 23864921e0..4642922ad1 100644 --- a/manim/utils/color/core.py +++ b/manim/utils/color/core.py @@ -139,7 +139,7 @@ def __init__( alpha: float = 1.0, ) -> None: if value is None: - self._internal_value = np.array((0, 0, 0, alpha), dtype=ManimColorDType) + self._internal_value = np.array((1, 1, 1, alpha), dtype=ManimColorDType) elif isinstance(value, ManimColor): # logger.info( # "ManimColor was passed another ManimColor. This is probably not what " @@ -505,7 +505,7 @@ def to_int_rgba_with_alpha(self, alpha: float) -> RGBA_Array_Int: tmp[3] = alpha * 255 return tmp.astype(int) - def to_hex(self, with_alpha: bool = False) -> str: + def to_hex(self, *, with_alpha: bool = False) -> str: """Converts the manim color to a hexadecimal representation of the color Parameters @@ -560,7 +560,7 @@ def to_hsl(self) -> HSL_Array_Float: """ return np.array(colorsys.rgb_to_hls(*self.to_rgb())) - def invert(self, with_alpha=False) -> Self: + def invert(self, *, with_alpha: bool = False) -> Self: """Returns an linearly inverted version of the color (no inplace changes) Parameters @@ -699,8 +699,17 @@ def contrasting( return dark return self._from_internal(BLACK._internal_value) - def opacity(self, opacity: float) -> Self: - """Creates a new ManimColor with the given opacity and the same color value as before + @overload + def opacity(self, opacity: float) -> Self: ... + + @overload + def opacity(self, opacity: None = None) -> float: ... + + def opacity(self, opacity: float | None = None) -> float | Self: + """Creates a new ManimColor with the given opacity and the same color value as before, or returns opacity. + + If no opacity is passed it will return the current opacity value. Otherwise, it will set the opacity + to the given value, returning a new ManimColor object. Parameters ---------- @@ -712,6 +721,8 @@ def opacity(self, opacity: float) -> Self: ManimColor The new ManimColor with the same color value but the new opacity """ + if opacity is None: + return self._internal_space[-1] tmp = self._internal_space.copy() tmp[-1] = opacity return self._construct_from_space(tmp) diff --git a/manim/utils/directories.py b/manim/utils/directories.py new file mode 100644 index 0000000000..e51f3c3c59 --- /dev/null +++ b/manim/utils/directories.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +import os + +from manim._config import config +from manim.utils.file_ops import guarantee_existence + + +def get_directories() -> dict[str, str]: + return config["directories"] + + +def get_temp_dir() -> str: + return get_directories()["temporary_storage"] + + +def get_tex_dir() -> str: + return guarantee_existence(os.path.join(get_temp_dir(), "Tex")) + + +def get_text_dir() -> str: + return guarantee_existence(os.path.join(get_temp_dir(), "Text")) + + +def get_mobject_data_dir() -> str: + return guarantee_existence(os.path.join(get_temp_dir(), "mobject_data")) + + +def get_downloads_dir() -> str: + return guarantee_existence(os.path.join(get_temp_dir(), "manim_downloads")) + + +def get_output_dir() -> str: + return guarantee_existence(get_directories()["output"]) + + +def get_raster_image_dir() -> str: + return get_directories()["raster_images"] + + +def get_vector_image_dir() -> str: + return get_directories()["vector_images"] + + +def get_sound_dir() -> str: + return get_directories()["sounds"] + + +def get_shader_dir() -> str: + return get_directories()["shaders"] diff --git a/manim/utils/docbuild/manim_directive.py b/manim/utils/docbuild/manim_directive.py index 8d68f2aa53..02cf764bd7 100644 --- a/manim/utils/docbuild/manim_directive.py +++ b/manim/utils/docbuild/manim_directive.py @@ -82,6 +82,7 @@ def construct(self): import csv import itertools as it +import os import re import shutil import sys @@ -176,6 +177,7 @@ def run(self) -> list[nodes.Element]: should_skip = ( "skip-manim" in self.state.document.settings.env.app.builder.tags or self.state.document.settings.env.app.builder.name == "gettext" + or os.getenv("READTHEDOCS_VERSION_NAME", None) in ["3112", "3475"] ) if should_skip: clsname = self.arguments[0] @@ -291,7 +293,7 @@ def run(self) -> list[nodes.Element]: code = [ "from manim import *", *user_code, - f"{clsname}().render()", + f"Manager({clsname}).render()", ] try: @@ -345,7 +347,7 @@ def run(self) -> list[nodes.Element]: rendering_times_file_path = Path("../rendering_times.csv") -def _write_rendering_stats(scene_name: str, run_time: str, file_name: str) -> None: +def _write_rendering_stats(scene_name: str, run_time: float, file_name: str) -> None: with rendering_times_file_path.open("a") as file: csv.writer(file).writerow( [ diff --git a/manim/utils/family_ops.py b/manim/utils/family_ops.py index 8d4af9d5a5..8b0aaa70ea 100644 --- a/manim/utils/family_ops.py +++ b/manim/utils/family_ops.py @@ -7,15 +7,26 @@ "restructure_list_to_exclude_certain_family_members", ] +from typing import TYPE_CHECKING -def extract_mobject_family_members(mobject_list, only_those_with_points=False): +if TYPE_CHECKING: + from collections.abc import Iterable, Sequence + + from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject + + +def extract_mobject_family_members( + mobject_list: Iterable[Mobject], only_those_with_points: bool = False +) -> Sequence[Mobject]: result = list(it.chain(*(mob.get_family() for mob in mobject_list))) if only_those_with_points: result = [mob for mob in result if mob.has_points()] return result -def restructure_list_to_exclude_certain_family_members(mobject_list, to_remove): +def restructure_list_to_exclude_certain_family_members( + mobject_list: Iterable[Mobject], to_remove: Iterable[Mobject] +) -> Sequence[Mobject]: """ Removes anything in to_remove from mobject_list, but in the event that one of the items to be removed is a member of the family of an item in mobject_list, @@ -40,3 +51,31 @@ def add_safe_mobjects_from_list(list_to_examine, set_to_remove): add_safe_mobjects_from_list(mobject_list, set(to_remove)) return new_list + + +def recursive_mobject_remove( + mobjects: list[Mobject], to_remove: set[Mobject] +) -> tuple[Sequence[Mobject], bool]: + """ + Takes in a list of mobjects, together with a set of mobjects to remove. + The first component of what's removed is a new list such that any mobject + with one of the elements from `to_remove` in its family is no longer in + the list, and in its place are its family members which aren't in `to_remove` + The second component is a boolean value indicating whether any removals were made + """ + result = [] + found_in_list = False + for mob in mobjects: + if mob in to_remove: + found_in_list = True + continue + # Recursive call + sub_list, found_in_submobjects = recursive_mobject_remove( + mob.submobjects, to_remove + ) + if found_in_submobjects: + result.extend(sub_list) + found_in_list = True + else: + result.append(mob) + return result, found_in_list diff --git a/manim/utils/file_ops.py b/manim/utils/file_ops.py index e4fbdebfe5..dcbc6acf56 100644 --- a/manim/utils/file_ops.py +++ b/manim/utils/file_ops.py @@ -27,14 +27,14 @@ from shutil import copyfile from typing import TYPE_CHECKING +import numpy as np + if TYPE_CHECKING: from manim.typing import StrPath - from ..scene.scene_file_writer import SceneFileWriter - -from manim import __version__, config, logger + from ..file_writer import FileWriter -from .. import console +from manim import __version__, config, console, logger def is_mp4_format() -> bool: @@ -211,7 +211,7 @@ def open_file(file_path: Path, in_browser: bool = False) -> None: sp.run(commands) -def open_media_file(file_writer: SceneFileWriter) -> None: +def open_media_file(file_writer: FileWriter) -> None: file_paths = [] if config["save_last_frame"]: @@ -294,3 +294,34 @@ def copy_template_files( copyfile(template_scene_path, Path.resolve(project_dir / "main.py")) console.print("\n\t[green]copied[/green] [blue]main.py[/blue]\n") add_import_statement(Path.resolve(project_dir / "main.py")) + + +def get_sorted_integer_files( + directory: str, + min_index: float = 0, + max_index: float = np.inf, + remove_non_integer_files: bool = False, + remove_indices_greater_than: float | None = None, + extension: str | None = None, +) -> list[str]: + indexed_files = [] + for file in os.listdir(directory): + index_str = file[: file.index(".")] if "." in file else file + + full_path = os.path.join(directory, file) + if index_str.isdigit(): + index = int(index_str) + if ( + remove_indices_greater_than is not None + and index > remove_indices_greater_than + ): + os.remove(full_path) + continue + if extension is not None and not file.endswith(extension): + continue + if index >= min_index and index < max_index: + indexed_files.append((index, file)) + elif remove_non_integer_files: + os.remove(full_path) + indexed_files.sort(key=lambda p: p[0]) + return [os.path.join(directory, p[1]) for p in indexed_files] diff --git a/manim/utils/hashing.py b/manim/utils/hashing.py index 92b5191503..24154d335f 100644 --- a/manim/utils/hashing.py +++ b/manim/utils/hashing.py @@ -2,27 +2,28 @@ from __future__ import annotations -import collections import copy import inspect import json import typing import zlib +from collections.abc import Callable, Hashable from time import perf_counter from types import FunctionType, MappingProxyType, MethodType, ModuleType from typing import Any import numpy as np -from manim.animation.animation import Animation -from manim.camera.camera import Camera -from manim.mobject.mobject import Mobject - from .. import config, logger if typing.TYPE_CHECKING: + from manim.animation.protocol import AnimationProtocol + from manim.camera.camera import Camera + from manim.mobject.opengl.opengl_mobject import OpenGLMobject from manim.scene.scene import Scene + T = typing.TypeVar("T") + __all__ = ["KEYS_TO_FILTER_OUT", "get_hash_from_play_call", "get_json"] # Sometimes there are elements that are not suitable for hashing (too long or @@ -59,7 +60,7 @@ def reset_already_processed(cls): cls._already_processed.clear() @classmethod - def check_already_processed_decorator(cls: _Memoizer, is_method: bool = False): + def check_already_processed_decorator(cls, is_method: bool = False): """Decorator to handle the arguments that goes through the decorated function. Returns _ALREADY_PROCESSED_PLACEHOLDER if the obj has been processed, or lets the decorated function call go ahead. @@ -102,7 +103,7 @@ def check_already_processed(cls, obj: Any) -> Any: return cls._handle_already_processed(obj, lambda x: x) @classmethod - def mark_as_processed(cls, obj: Any) -> None: + def mark_as_processed(cls, obj: Any) -> str: """Marks an object as processed. Parameters @@ -131,7 +132,7 @@ def _handle_already_processed( # It makes no sense (and it'd slower) to memoize objects of these primitive # types. Hence, we simply return the object. return obj - if isinstance(obj, collections.abc.Hashable): + if isinstance(obj, Hashable): try: return cls._return(obj, hash, default_function) except TypeError: @@ -144,11 +145,11 @@ def _handle_already_processed( @classmethod def _return( cls, - obj: typing.Any, + obj: T, obj_to_membership_sign: typing.Callable[[Any], int], - default_func, + default_func: Callable[[T], str], memoizing=True, - ) -> str | Any: + ) -> str: obj_membership_sign = obj_to_membership_sign(obj) if obj_membership_sign in cls._already_processed: return cls.ALREADY_PROCESSED_PLACEHOLDER @@ -173,7 +174,7 @@ def _return( class _CustomEncoder(json.JSONEncoder): - def default(self, obj: Any): + def default(self, o: Any): """ This method is used to serialize objects to JSON format. @@ -196,11 +197,11 @@ def default(self, obj: Any): Python object that JSON encoder will recognize """ - if not (isinstance(obj, ModuleType)) and isinstance( - obj, + if not (isinstance(o, ModuleType)) and isinstance( + o, (MethodType, FunctionType), ): - cvars = inspect.getclosurevars(obj) + cvars = inspect.getclosurevars(o) cvardict = {**copy.copy(cvars.globals), **copy.copy(cvars.nonlocals)} for i in list(cvardict): # NOTE : All module types objects are removed, because otherwise it @@ -208,7 +209,7 @@ def default(self, obj: Any): if isinstance(cvardict[i], ModuleType): del cvardict[i] try: - code = inspect.getsource(obj) + code = inspect.getsource(o) except (OSError, TypeError): # This happens when rendering videos included in the documentation # within doctests and should be replaced by a solution avoiding @@ -216,23 +217,23 @@ def default(self, obj: Any): # See https://github.com/ManimCommunity/manim/pull/402. code = "" return self._cleaned_iterable({"code": code, "nonlocals": cvardict}) - elif isinstance(obj, np.ndarray): - if obj.size > 1000: - obj = np.resize(obj, (100, 100)) - return f"TRUNCATED ARRAY: {repr(obj)}" + elif isinstance(o, np.ndarray): + if o.size > 1000: + o = np.resize(o, (100, 100)) + return f"TRUNCATED ARRAY: {repr(o)}" # We return the repr and not a list to avoid the JsonEncoder to iterate over it. - return repr(obj) - elif hasattr(obj, "__dict__"): - temp = obj.__dict__ + return repr(o) + elif hasattr(o, "__dict__"): + temp = o.__dict__ # MappingProxy is scene-caching nightmare. It contains all of the object methods and attributes. We skip it as the mechanism will at some point process the object, but instantiated. # Indeed, there is certainly no case where scene-caching will receive only a non instancied object, as this is never used in the library or encouraged to be used user-side. if isinstance(temp, MappingProxyType): return "MappingProxy" return self._cleaned_iterable(temp) - elif isinstance(obj, np.uint8): - return int(obj) + elif isinstance(o, np.uint8): + return int(o) # Serialize it with only the type of the object. You can change this to whatever string when debugging the serialization process. - return str(type(obj)) + return str(type(o)) def _cleaned_iterable(self, iterable: typing.Iterable[Any]): """Check for circular reference at each iterable that will go through the JSONEncoder, as well as key of the wrong format. @@ -325,8 +326,8 @@ def get_json(obj: dict): def get_hash_from_play_call( scene_object: Scene, camera_object: Camera, - animations_list: typing.Iterable[Animation], - current_mobjects_list: typing.Iterable[Mobject], + animations_list: typing.Iterable[AnimationProtocol], + current_mobjects_list: typing.Iterable[OpenGLMobject], ) -> str: """Take the list of animations and a list of mobjects and output their hashes. This is meant to be used for `scene.play` function. diff --git a/manim/utils/ipython_magic.py b/manim/utils/ipython_magic.py index 1b6c7b316e..a3967012b0 100644 --- a/manim/utils/ipython_magic.py +++ b/manim/utils/ipython_magic.py @@ -10,9 +10,10 @@ from manim import config, logger, tempconfig from manim.__main__ import main +from manim.manager import Manager from manim.renderer.shader import shader_program_cache -from ..constants import RendererType +__all__ = ["ManimMagic"] __all__ = ["ManimMagic"] @@ -129,25 +130,20 @@ def construct(self): args = main(modified_args, standalone_mode=False, prog_name="manim") with tempconfig(local_ns.get("config", {})): config.digest_args(args) - - renderer = None - if config.renderer == RendererType.OPENGL: - from manim.renderer.opengl_renderer import OpenGLRenderer - - renderer = OpenGLRenderer() + manager: Manager | None = None try: SceneClass = local_ns[config["scene_names"][0]] - scene = SceneClass(renderer=renderer) - scene.render() + manager = Manager(SceneClass) + manager.render() finally: # Shader cache becomes invalid as the context is destroyed shader_program_cache.clear() # Close OpenGL window here instead of waiting for the main thread to # finish causing the window to stay open and freeze - if renderer is not None and renderer.window is not None: - renderer.window.close() + if manager is not None and manager.window is not None: + manager.window.close() if config["output_file"] is None: logger.info("No output file produced") @@ -174,7 +170,11 @@ def construct(self): # set explicitly. embed = "google.colab" in str(get_ipython()) - if file_type.startswith("image"): + if file_type is None: + raise Exception( + "Could not guess file type, please contact the developers" + ) + elif file_type.startswith("image"): result = Image(filename=config["output_file"]) else: result = Video( diff --git a/manim/utils/module_ops.py b/manim/utils/module_ops.py index 03f297030d..88cfd9d597 100644 --- a/manim/utils/module_ops.py +++ b/manim/utils/module_ops.py @@ -6,15 +6,26 @@ import sys import types import warnings -from pathlib import Path +from typing import TYPE_CHECKING -from .. import config, console, constants, logger -from ..scene.scene_file_writer import SceneFileWriter +from manim import config, console, constants, logger +from manim.file_writer import FileWriter + +if TYPE_CHECKING: + from collections.abc import Iterable, Sequence + from pathlib import Path + + from typing_extensions import Any + + from manim.scene.scene import Scene __all__ = ["scene_classes_from_file"] -def get_module(file_name: Path): +__all__ = ["scene_classes_from_file"] + + +def get_module(file_name: Path) -> types.ModuleType: if str(file_name) == "-": module = types.ModuleType("input_scenes") logger.info( @@ -56,10 +67,10 @@ def get_module(file_name: Path): raise FileNotFoundError(f"{file_name} not found") -def get_scene_classes_from_module(module): - from ..scene.scene import Scene +def get_scene_classes_from_module(module: types.ModuleType) -> list[type[Scene]]: + from manim.scene.scene import Scene - def is_child_scene(obj, module): + def is_child_scene(obj: Any, module: types.ModuleType) -> bool: return ( inspect.isclass(obj) and issubclass(obj, Scene) @@ -73,33 +84,33 @@ def is_child_scene(obj, module): ] -def get_scenes_to_render(scene_classes): +def get_scenes_to_render(scene_classes: Sequence[type[Scene]]) -> Sequence[type[Scene]]: if not scene_classes: logger.error(constants.NO_SCENE_MESSAGE) return [] - if config["write_all"]: + if config.write_all: return scene_classes result = [] - for scene_name in config["scene_names"]: - found = False + for scene_name in config.scene_names: + if not scene_name: + continue for scene_class in scene_classes: if scene_class.__name__ == scene_name: result.append(scene_class) - found = True break - if not found and (scene_name != ""): + else: logger.error(constants.SCENE_NOT_FOUND_MESSAGE.format(scene_name)) if result: return result if len(scene_classes) == 1: - config["scene_names"] = [scene_classes[0].__name__] + config.scene_names = [scene_classes[0].__name__] return [scene_classes[0]] return prompt_user_for_choice(scene_classes) -def prompt_user_for_choice(scene_classes): +def prompt_user_for_choice(scene_classes: Iterable[type[Scene]]) -> list[type[Scene]]: num_to_class = {} - SceneFileWriter.force_output_as_scene_name = True + FileWriter.use_output_as_scene_name() for count, scene_class in enumerate(scene_classes, 1): name = scene_class.__name__ console.print(f"{count}: {name}", style="logging.level.info") @@ -125,8 +136,8 @@ def prompt_user_for_choice(scene_classes): def scene_classes_from_file( - file_path: Path, require_single_scene=False, full_list=False -): + file_path: Path, require_single_scene: bool = False, full_list: bool = False +) -> Sequence[type[Scene]]: module = get_module(file_path) all_scene_classes = get_scene_classes_from_module(module) if full_list: @@ -134,5 +145,5 @@ def scene_classes_from_file( scene_classes_to_render = get_scenes_to_render(all_scene_classes) if require_single_scene: assert len(scene_classes_to_render) == 1 - return scene_classes_to_render[0] + return [scene_classes_to_render[0]] return scene_classes_to_render diff --git a/manim/utils/progressbar.py b/manim/utils/progressbar.py new file mode 100644 index 0000000000..a941460426 --- /dev/null +++ b/manim/utils/progressbar.py @@ -0,0 +1,68 @@ +"""Create an abstraction over the progress bar used.""" + +from __future__ import annotations + +import contextlib +from typing import Protocol, cast + +from tqdm.asyncio import tqdm as asyncio_tqdm +from tqdm.auto import tqdm as auto_tqdm +from tqdm.rich import tqdm as rich_tqdm +from tqdm.std import TqdmExperimentalWarning as ExperimentalProgressBarWarning + +__all__ = [ + "ProgressBar", + "ProgressBarProtocol", + "NullProgressBar", + "ExperimentalProgressBarWarning", +] + + +# let tqdm figure out whether we're in a notebook +# but replace the basic tqdm with tqdm.rich.tqdm +if auto_tqdm is asyncio_tqdm: # noqa: SIM108 + tqdm = rich_tqdm +else: + # we're in a notebook + # tell typecheckers to pretend like it's tqdm.rich.tqdm + tqdm = cast(type[rich_tqdm], auto_tqdm) + + +class ProgressBarProtocol(Protocol): + def update(self, n: int) -> object: ... + + +class ProgressBar(tqdm, contextlib.AbstractContextManager[ProgressBarProtocol]): + """A manim progress bar. + + This abstracts away whether a progress bar is used in a notebook, or via the terminal, + or something else. + + You may need to ignore warnings from ``tqdm``, due to the experimental nature of + ``tqdm.notebook.tqdm`` and ``tqdm.rich.tqdm``. This can be done with something like: + + .. code-block:: python + + import warnings + + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=ExperimentalProgressBarWarning) + return ProgressBar(...) + + + .. note:: + + This warning filtering could have been done in the constructor, but would + have caused the loss of autocomplete with the ``__init__`` of ``tqdm``, as + well as possibly hide issues with the progressbar. Therefore, the warning + filtering is left to the user. + """ + + pass + + +class NullProgressBar(ProgressBarProtocol): + """Fake progressbar.""" + + def update(self, n: int) -> None: + """Do nothing""" diff --git a/manim/utils/sounds.py b/manim/utils/sounds.py index 5e0ea060f3..9621f4f4d2 100644 --- a/manim/utils/sounds.py +++ b/manim/utils/sounds.py @@ -6,13 +6,14 @@ "get_full_sound_file_path", ] +from pathlib import Path -from .. import config -from ..utils.file_ops import seek_full_path_from_defaults +from manim import config +from manim.utils.file_ops import seek_full_path_from_defaults # Still in use by add_sound() function in scene_file_writer.py -def get_full_sound_file_path(sound_file_name): +def get_full_sound_file_path(sound_file_name: str) -> Path: return seek_full_path_from_defaults( sound_file_name, default_dir=config.get_dir("assets_dir"), diff --git a/manim/utils/space_ops.py b/manim/utils/space_ops.py index 19d2989388..31a4e9d581 100644 --- a/manim/utils/space_ops.py +++ b/manim/utils/space_ops.py @@ -2,7 +2,6 @@ from __future__ import annotations -import itertools as it from collections.abc import Sequence from typing import TYPE_CHECKING @@ -283,6 +282,22 @@ def rotation_about_z(angle: float) -> np.ndarray: ) +def get_norm(vector: np.ndarray) -> float: + """Returns the norm of the vector. + + Parameters + ---------- + vector + The vector for which you want to find the norm. + + Returns + ------- + float + The norm of the vector. + """ + return np.linalg.norm(vector) + + def z_to_vector(vector: np.ndarray) -> np.ndarray: """ Returns some matrix in SO(3) which takes the z-axis to the @@ -344,12 +359,16 @@ def angle_between_vectors(v1: np.ndarray, v2: np.ndarray) -> float: ) -def normalize(vect: np.ndarray | tuple[float], fall_back=None) -> np.ndarray: - norm = np.linalg.norm(vect) +def normalize( + vect: npt.NDArray[float], fall_back: npt.NDArray[float] | None = None +) -> npt.NDArray[float]: + norm = get_norm(vect) if norm > 0: return np.array(vect) / norm + elif fall_back is not None: + return np.array(fall_back) else: - return fall_back or np.zeros(len(vect)) + return np.zeros(len(vect)) def normalize_along_axis(array: np.ndarray, axis: np.ndarray) -> np.ndarray: @@ -710,78 +729,69 @@ def earclip_triangulation(verts: np.ndarray, ring_ends: list) -> list: list A list of indices giving a triangulation of a polygon. """ - # First, connect all the rings so that the polygon - # with holes is instead treated as a (very convex) - # polygon with one edge. Do this by drawing connections - # between rings close to each other rings = [list(range(e0, e1)) for e0, e1 in zip([0, *ring_ends], ring_ends)] - attached_rings = rings[:1] - detached_rings = rings[1:] - loop_connections = {} - - while detached_rings: - i_range, j_range = ( - list( - filter( - # Ignore indices that are already being - # used to draw some connection - lambda i: i not in loop_connections, - it.chain(*ring_group), - ), - ) - for ring_group in (attached_rings, detached_rings) + + def is_in(point, ring_id): + return ( + abs(abs(get_winding_number([i - point for i in verts[rings[ring_id]]])) - 1) + < 1e-5 ) - # Closest point on the attached rings to an estimated midpoint - # of the detached rings - tmp_j_vert = midpoint(verts[j_range[0]], verts[j_range[len(j_range) // 2]]) - i = min(i_range, key=lambda i: norm_squared(verts[i] - tmp_j_vert)) - # Closest point of the detached rings to the aforementioned - # point of the attached rings - j = min(j_range, key=lambda j: norm_squared(verts[i] - verts[j])) - # Recalculate i based on new j - i = min(i_range, key=lambda i: norm_squared(verts[i] - verts[j])) - - # Remember to connect the polygon at these points - loop_connections[i] = j - loop_connections[j] = i - - # Move the ring which j belongs to from the - # attached list to the detached list - new_ring = next( - (ring for ring in detached_rings if ring[0] <= j < ring[-1]), None + def ring_area(ring_id): + ring = rings[ring_id] + s = 0 + for i, j in zip(ring[1:], ring): + s += cross2d(verts[i], verts[j]) + return abs(s) / 2 + + # Points at the same position may cause problems + for i in rings: + verts[i[0]] += (verts[i[1]] - verts[i[0]]) * 1e-6 + verts[i[-1]] += (verts[i[-2]] - verts[i[-1]]) * 1e-6 + + # First, we should know which rings are directly contained in it for each ring + + right = [max(verts[rings[i], 0]) for i in range(len(rings))] + left = [min(verts[rings[i], 0]) for i in range(len(rings))] + top = [max(verts[rings[i], 1]) for i in range(len(rings))] + bottom = [min(verts[rings[i], 1]) for i in range(len(rings))] + area = [ring_area(i) for i in range(len(rings))] + + # The larger ring must be outside + rings_sorted = list(range(len(rings))) + rings_sorted.sort(key=lambda x: area[x], reverse=True) + + def is_in_fast(ring_a, ring_b): + # Whether a is in b + return ( + left[ring_b] <= left[ring_a] <= right[ring_a] <= right[ring_b] + and bottom[ring_b] <= bottom[ring_a] <= top[ring_a] <= top[ring_b] + and is_in(verts[rings[ring_a][0]], ring_b) ) - if new_ring is not None: - detached_rings.remove(new_ring) - attached_rings.append(new_ring) - else: - raise Exception("Could not find a ring to attach") - - # Setup linked list - after = [] - end0 = 0 - for end1 in ring_ends: - after.extend(range(end0 + 1, end1)) - after.append(end0) - end0 = end1 - - # Find an ordering of indices walking around the polygon - indices = [] - i = 0 - for _ in range(len(verts) + len(ring_ends) - 1): - # starting = False - if i in loop_connections: - j = loop_connections[i] - indices.extend([i, j]) - i = after[j] - else: - indices.append(i) - i = after[i] - if i == 0: - break - - meta_indices = earcut(verts[indices, :2], [len(indices)]) - return [indices[mi] for mi in meta_indices] + + children = [[]] * len(rings) + for idx, i in enumerate(rings_sorted): + for j in rings_sorted[:idx][::-1]: + if is_in_fast(i, j): + children[j].append(i) + break + + res = [] + + # Then, we can use earcut for each part + used = [False] * len(rings) + for i in rings_sorted: + if used[i]: + continue + v = rings[i] + ring_ends = [len(v)] + for j in children[i]: + used[j] = True + v += rings[j] + ring_ends.append(len(v)) + res += [v[i] for i in earcut(verts[v, :2], ring_ends)] + + return res def cartesian_to_spherical(vec: Sequence[float]) -> np.ndarray: diff --git a/manim/utils/testing/_frames_testers.py b/manim/utils/testing/_frames_testers.py index bef6184937..6622dc9a56 100644 --- a/manim/utils/testing/_frames_testers.py +++ b/manim/utils/testing/_frames_testers.py @@ -7,8 +7,12 @@ import numpy as np +from manim.typing import PixelArray + from ._show_diff import show_diff_helper +__all__ = ["_FramesTester", "_ControlDataWriter"] + FRAME_ABSOLUTE_TOLERANCE = 1.01 FRAME_MISMATCH_RATIO_TOLERANCE = 1e-5 @@ -27,18 +31,18 @@ def __init__(self, file_path: Path, show_diff=False) -> None: def testing(self): with np.load(self._file_path) as data: self._frames = data["frame_data"] - # For backward compatibility, when the control data contains only one frame (<= v0.8.0) - if len(self._frames.shape) != 4: - self._frames = np.expand_dims(self._frames, axis=0) - logger.debug(self._frames.shape) - self._number_frames = np.ma.size(self._frames, axis=0) - yield - assert self._frames_compared == self._number_frames, ( - f"The scene tested contained {self._frames_compared} frames, " - f"when there are {self._number_frames} control frames for this test." - ) + # For backward compatibility, when the control data contains only one frame (<= v0.8.0) + if len(self._frames.shape) != 4: + self._frames = np.expand_dims(self._frames, axis=0) + logger.debug(self._frames.shape) + self._number_frames = np.ma.size(self._frames, axis=0) + yield + assert self._frames_compared == self._number_frames, ( + f"The scene tested contained {self._frames_compared} frames, " + f"when there are {self._number_frames} control frames for this test." + ) - def check_frame(self, frame_number: int, frame: np.ndarray): + def check_frame(self, frame_number: int, frame: PixelArray): assert frame_number < self._number_frames, ( f"The tested scene is at frame number {frame_number} " f"when there are {self._number_frames} control frames." @@ -52,7 +56,7 @@ def check_frame(self, frame_number: int, frame: np.ndarray): verbose=False, ) self._frames_compared += 1 - except AssertionError as e: + except AssertionError: number_of_matches = np.isclose( frame, self._frames[frame_number], atol=FRAME_ABSOLUTE_TOLERANCE ).sum() @@ -76,7 +80,7 @@ def check_frame(self, frame_number: int, frame: np.ndarray): self._frames[frame_number], self._file_path.name, ) - raise e + raise class _ControlDataWriter(_FramesTester): @@ -86,7 +90,7 @@ def __init__(self, file_path: Path, size_frame: tuple) -> None: self._number_frames_written: int = 0 # Actually write a frame. - def check_frame(self, index: int, frame: np.ndarray): + def check_frame(self, frame_number: int, frame: np.ndarray): frame = frame[np.newaxis, ...] self.frames = np.concatenate((self.frames, frame)) self._number_frames_written += 1 diff --git a/manim/utils/testing/_show_diff.py b/manim/utils/testing/_show_diff.py index 0cb2aab0f5..e1fb28d475 100644 --- a/manim/utils/testing/_show_diff.py +++ b/manim/utils/testing/_show_diff.py @@ -5,6 +5,8 @@ import numpy as np +__all__ = ["show_diff_helper"] + def show_diff_helper( frame_number: int, diff --git a/manim/utils/testing/_test_class_makers.py b/manim/utils/testing/_test_class_makers.py index fe127be1c7..a44153c826 100644 --- a/manim/utils/testing/_test_class_makers.py +++ b/manim/utils/testing/_test_class_makers.py @@ -2,76 +2,61 @@ from typing import Callable +from manim.file_writer.protocols import FileWriterProtocol from manim.scene.scene import Scene -from manim.scene.scene_file_writer import SceneFileWriter from ._frames_testers import _FramesTester +__all__ = ["_make_test_scene_class", "_make_scene_file_writer_class"] + def _make_test_scene_class( base_scene: type[Scene], - construct_test: Callable[[Scene], None], - test_renderer, + construct_test: Callable[[Scene], object], ) -> type[Scene]: class _TestedScene(base_scene): - def __init__(self, *args, **kwargs): - super().__init__(*args, renderer=test_renderer, **kwargs) - def construct(self): + from manim import config + construct_test(self) # Manim hack to render the very last frame (normally the last frame is not the very end of the animation) - if self.animations is not None: - self.update_to_time(self.get_run_time(self.animations)) - self.renderer.render(self, 1, self.moving_mobjects) + self.wait(1 / config.frame_rate) return _TestedScene -def _make_test_renderer_class(from_renderer): - # Just for inheritance. - class _TestRenderer(from_renderer): - pass - - return _TestRenderer - - -class DummySceneFileWriter(SceneFileWriter): +class DummySceneFileWriter(FileWriterProtocol): """Delegate of SceneFileWriter used to test the frames.""" - def __init__(self, renderer, scene_name, **kwargs): - super().__init__(renderer, scene_name, **kwargs) - self.i = 0 + def __init__(self, scene_name: str): + # we still need num_plays to satisfy the protocol + self.num_plays = 0 + self.frames = 0 - def init_output_directories(self, scene_name): + def begin_animation(self, allow_write: bool = False): pass - def add_partial_movie_file(self, hash_animation): - pass - - def begin_animation(self, allow_write=True): - pass + def end_animation(self, allow_write: bool = False): + self.num_plays += 1 - def end_animation(self, allow_write): - pass + def is_already_cached(self, hash_invocation: str) -> bool: + return False - def combine_to_movie(self): + def add_partial_movie_file(self, hash_animation: str) -> None: pass - def combine_to_section_videos(self): - pass + def write_frame(self, frame): + self.frames += 1 - def clean_cache(self): + def finish(self): pass - def write_frame(self, frame_or_renderer, num_frames=1): - self.i += 1 - -def _make_scene_file_writer_class(tester: _FramesTester) -> type[SceneFileWriter]: +def _make_scene_file_writer_class(tester: _FramesTester) -> type[FileWriterProtocol]: class TestSceneFileWriter(DummySceneFileWriter): - def write_frame(self, frame_or_renderer, num_frames=1): - tester.check_frame(self.i, frame_or_renderer) - super().write_frame(frame_or_renderer, num_frames=num_frames) + def write_frame(self, frame): + tester.check_frame(self.frames, frame) + super().write_frame(frame) return TestSceneFileWriter diff --git a/manim/utils/testing/frames_comparison.py b/manim/utils/testing/frames_comparison.py index a298585b29..b287639cf4 100644 --- a/manim/utils/testing/frames_comparison.py +++ b/manim/utils/testing/frames_comparison.py @@ -3,27 +3,30 @@ import functools import inspect from pathlib import Path -from typing import Callable +from typing import TYPE_CHECKING -import cairo import pytest -from _pytest.fixtures import FixtureRequest -from manim import Scene +from manim import Manager, Scene from manim._config import tempconfig from manim._config.utils import ManimConfig -from manim.camera.three_d_camera import ThreeDCamera -from manim.renderer.cairo_renderer import CairoRenderer -from manim.scene.three_d_scene import ThreeDScene from ._frames_testers import _ControlDataWriter, _FramesTester from ._test_class_makers import ( DummySceneFileWriter, _make_scene_file_writer_class, - _make_test_renderer_class, _make_test_scene_class, ) +if TYPE_CHECKING: + from collections.abc import Callable + + from typing_extensions import Concatenate, ParamSpec + + P = ParamSpec("P") + +__all__ = ["frames_comparison"] + SCENE_PARAMETER_NAME = "scene" _tests_root_dir_path = Path(__file__).absolute().parents[2] PATH_CONTROL_DATA = _tests_root_dir_path / Path("control_data", "graphical_units_data") @@ -31,29 +34,26 @@ def frames_comparison( - func=None, + func: Callable[P, object] | None = None, *, last_frame: bool = True, - renderer_class=CairoRenderer, - base_scene=Scene, + base_scene: type[Scene] = Scene, **custom_config, -): +) -> Callable[Concatenate[pytest.FixtureRequest, Path, P], object]: """Compares the frames generated by the test with control frames previously registered. If there is no control frames for this test, the test will fail. To generate control frames for a given test, pass ``--set_test`` flag to pytest while running the test. - Note that this decorator can be use with or without parentheses. + Note that this decorator can be used with or without parentheses. Parameters ---------- last_frame whether the test should test the last frame, by default True. - renderer_class - The base renderer to use (OpenGLRenderer/CairoRenderer), by default CairoRenderer base_scene - The base class for the scene (ThreeDScene, etc.), by default Scene + The base class for the scene (VectorScene, etc.), by default Scene .. warning:: By default, last_frame is True, which means that only the last frame is tested. @@ -65,8 +65,8 @@ def decorator_maker(tested_scene_construct): SCENE_PARAMETER_NAME not in inspect.getfullargspec(tested_scene_construct).args ): - raise Exception( - f"Invalid graphical test function test function : must have '{SCENE_PARAMETER_NAME}'as one of the parameters.", + raise ValueError( + f"Invalid graphical test function test function : must have {SCENE_PARAMETER_NAME!r} as one of the parameters.", ) # Exclude "scene" from the argument list of the signature. @@ -74,22 +74,22 @@ def decorator_maker(tested_scene_construct): functools.partial(tested_scene_construct, scene=None), ) - if "__module_test__" not in tested_scene_construct.__globals__: - raise Exception( + module_name = tested_scene_construct.__globals__.get("__module_test__") + if module_name is None: + raise AttributeError( "There is no module test name indicated for the graphical unit test. You have to declare __module_test__ in the test file.", ) - module_name = tested_scene_construct.__globals__.get("__module_test__") - test_name = tested_scene_construct.__name__[len("test_") :] + + test_name = tested_scene_construct.__name__.removeprefix("test_") @functools.wraps(tested_scene_construct) # The "request" parameter is meant to be used as a fixture by pytest. See below. - def wrapper(*args, request: FixtureRequest, tmp_path, **kwargs): - # check for cairo version - if ( - renderer_class is CairoRenderer - and cairo.cairo_version() < MIN_CAIRO_VERSION - ): - pytest.skip("Cairo version is too old. Skipping cairo graphical tests.") + def wrapper( + request: pytest.FixtureRequest, + tmp_path: Path, + *args: P.args, + **kwargs: P.kwargs, + ): # Wraps the test_function to a construct method, to "freeze" the eventual additional arguments (parametrizations fixtures). construct = functools.partial(tested_scene_construct, *args, **kwargs) @@ -99,23 +99,21 @@ def wrapper(*args, request: FixtureRequest, tmp_path, **kwargs): # Example: if "length" is parametrized from 0 to 20, the kwargs # will be once with {"length" : 1}, etc. test_name_with_param = test_name + "_".join( - f"_{str(tup[0])}[{str(tup[1])}]" for tup in kwargs.items() + f"_{k}[{v}]" for k, v in kwargs.items() ) config_tests = _config_test(last_frame) - config_tests["text_dir"] = tmp_path - config_tests["tex_dir"] = tmp_path + config_tests.text_dir = tmp_path + config_tests.tex_dir = tmp_path if last_frame: - config_tests["frame_rate"] = 1 - config_tests["dry_run"] = True + config_tests.frame_rate = 1 + else: + config_tests.write_to_movie = True setting_test = request.config.getoption("--set_test") - try: - test_file_path = tested_scene_construct.__globals__["__file__"] - except Exception: - test_file_path = None + test_file_path = tested_scene_construct.__globals__.get("__file__") real_test = _make_test_comparing_frames( file_path=_control_data_path( test_file_path, @@ -125,7 +123,6 @@ def wrapper(*args, request: FixtureRequest, tmp_path, **kwargs): ), base_scene=base_scene, construct=construct, - renderer_class=renderer_class, is_set_test_data_test=setting_test, last_frame=last_frame, show_diff=request.config.getoption("--show_diff"), @@ -146,13 +143,13 @@ def wrapper(*args, request: FixtureRequest, tmp_path, **kwargs): inspect.Parameter("tmp_path", inspect.Parameter.KEYWORD_ONLY), ] new_sig = old_sig.replace(parameters=parameters) - wrapper.__signature__ = new_sig + wrapper.__signature__ = new_sig # type: ignore # Reach a bit into pytest internals to hoist the marks from our wrapped # function. - wrapper.pytestmark = [] + wrapper.pytestmark = [] # type: ignore new_marks = getattr(tested_scene_construct, "pytestmark", []) - wrapper.pytestmark = new_marks + wrapper.pytestmark = new_marks # type: ignore return wrapper # Case where the decorator is called with and without parentheses. @@ -165,8 +162,7 @@ def wrapper(*args, request: FixtureRequest, tmp_path, **kwargs): def _make_test_comparing_frames( file_path: Path, base_scene: type[Scene], - construct: Callable[[Scene], None], - renderer_class: type, # Renderer type, there is no superclass renderer yet ..... + construct: Callable[[Scene], object], is_set_test_data_test: bool, last_frame: bool, show_diff: bool, @@ -202,30 +198,20 @@ def _make_test_comparing_frames( if not last_frame else DummySceneFileWriter ) - testRenderer = _make_test_renderer_class(renderer_class) def real_test(): with frames_tester.testing(): - sceneTested = _make_test_scene_class( + scene_tested: type[Scene] = _make_test_scene_class( base_scene=base_scene, construct_test=construct, - # NOTE this is really ugly but it's due to the very bad design of the two renderers. - # If you pass a custom renderer to the Scene, the Camera class given as an argument in the Scene - # is not passed to the renderer. See __init__ of Scene. - # This potentially prevents OpenGL testing. - test_renderer=( - testRenderer(file_writer_class=file_writer_class) - if base_scene is not ThreeDScene - else testRenderer( - file_writer_class=file_writer_class, - camera_class=ThreeDCamera, - ) - ), # testRenderer(file_writer_class=file_writer_class), ) - scene_tested = sceneTested(skip_animations=True) - scene_tested.render() + manager = Manager(scene_tested) + manager.file_writer = file_writer_class( + manager.scene.get_default_scene_name() + ) + manager.render() if last_frame: - frames_tester.check_frame(-1, scene_tested.renderer.get_frame()) + frames_tester.check_frame(-1, manager.renderer.get_pixels()) return real_test diff --git a/poetry.lock b/poetry.lock index 4cce1eba32..aa9988989d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. [[package]] name = "alabaster" @@ -11,26 +11,37 @@ files = [ {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, ] +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + [[package]] name = "anyio" -version = "4.6.2.post1" +version = "4.7.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = true python-versions = ">=3.9" files = [ - {file = "anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d"}, - {file = "anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c"}, + {file = "anyio-4.7.0-py3-none-any.whl", hash = "sha256:ea60c3723ab42ba6fff7e8ccb0488c898ec538ff4df1f1d5e642c3601d07e352"}, + {file = "anyio-4.7.0.tar.gz", hash = "sha256:2f834749c602966b7d456a7567cafcb309f96482b5081d14ac93ccd457f9dd48"}, ] [package.dependencies] exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} idna = ">=2.8" sniffio = ">=1.1" -typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} +typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] -doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21.0b1)"] +doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] trio = ["trio (>=0.26.1)"] [[package]] @@ -124,7 +135,7 @@ test = ["dateparser (==1.*)", "pre-commit", "pytest", "pytest-cov", "pytest-mock name = "asttokens" version = "3.0.0" description = "Annotate AST trees with source code positions" -optional = true +optional = false python-versions = ">=3.8" files = [ {file = "asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2"}, @@ -151,19 +162,19 @@ typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} [[package]] name = "attrs" -version = "24.2.0" +version = "24.3.0" description = "Classes Without Boilerplate" optional = true -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"}, - {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"}, + {file = "attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308"}, + {file = "attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff"}, ] [package.extras] benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] @@ -316,13 +327,13 @@ css = ["tinycss2 (>=1.1.0,<1.5)"] [[package]] name = "certifi" -version = "2024.8.30" +version = "2024.12.14" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, - {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, + {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, + {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, ] [[package]] @@ -587,76 +598,65 @@ test = ["pytest"] [[package]] name = "contourpy" -version = "1.3.0" +version = "1.3.1" description = "Python library for calculating contours of 2D quadrilateral grids" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "contourpy-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:880ea32e5c774634f9fcd46504bf9f080a41ad855f4fef54f5380f5133d343c7"}, - {file = "contourpy-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:76c905ef940a4474a6289c71d53122a4f77766eef23c03cd57016ce19d0f7b42"}, - {file = "contourpy-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92f8557cbb07415a4d6fa191f20fd9d2d9eb9c0b61d1b2f52a8926e43c6e9af7"}, - {file = "contourpy-1.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36f965570cff02b874773c49bfe85562b47030805d7d8360748f3eca570f4cab"}, - {file = "contourpy-1.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cacd81e2d4b6f89c9f8a5b69b86490152ff39afc58a95af002a398273e5ce589"}, - {file = "contourpy-1.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69375194457ad0fad3a839b9e29aa0b0ed53bb54db1bfb6c3ae43d111c31ce41"}, - {file = "contourpy-1.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a52040312b1a858b5e31ef28c2e865376a386c60c0e248370bbea2d3f3b760d"}, - {file = "contourpy-1.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3faeb2998e4fcb256542e8a926d08da08977f7f5e62cf733f3c211c2a5586223"}, - {file = "contourpy-1.3.0-cp310-cp310-win32.whl", hash = "sha256:36e0cff201bcb17a0a8ecc7f454fe078437fa6bda730e695a92f2d9932bd507f"}, - {file = "contourpy-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:87ddffef1dbe5e669b5c2440b643d3fdd8622a348fe1983fad7a0f0ccb1cd67b"}, - {file = "contourpy-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0fa4c02abe6c446ba70d96ece336e621efa4aecae43eaa9b030ae5fb92b309ad"}, - {file = "contourpy-1.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:834e0cfe17ba12f79963861e0f908556b2cedd52e1f75e6578801febcc6a9f49"}, - {file = "contourpy-1.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dbc4c3217eee163fa3984fd1567632b48d6dfd29216da3ded3d7b844a8014a66"}, - {file = "contourpy-1.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4865cd1d419e0c7a7bf6de1777b185eebdc51470800a9f42b9e9decf17762081"}, - {file = "contourpy-1.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:303c252947ab4b14c08afeb52375b26781ccd6a5ccd81abcdfc1fafd14cf93c1"}, - {file = "contourpy-1.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637f674226be46f6ba372fd29d9523dd977a291f66ab2a74fbeb5530bb3f445d"}, - {file = "contourpy-1.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:76a896b2f195b57db25d6b44e7e03f221d32fe318d03ede41f8b4d9ba1bff53c"}, - {file = "contourpy-1.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e1fd23e9d01591bab45546c089ae89d926917a66dceb3abcf01f6105d927e2cb"}, - {file = "contourpy-1.3.0-cp311-cp311-win32.whl", hash = "sha256:d402880b84df3bec6eab53cd0cf802cae6a2ef9537e70cf75e91618a3801c20c"}, - {file = "contourpy-1.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:6cb6cc968059db9c62cb35fbf70248f40994dfcd7aa10444bbf8b3faeb7c2d67"}, - {file = "contourpy-1.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:570ef7cf892f0afbe5b2ee410c507ce12e15a5fa91017a0009f79f7d93a1268f"}, - {file = "contourpy-1.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:da84c537cb8b97d153e9fb208c221c45605f73147bd4cadd23bdae915042aad6"}, - {file = "contourpy-1.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0be4d8425bfa755e0fd76ee1e019636ccc7c29f77a7c86b4328a9eb6a26d0639"}, - {file = "contourpy-1.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c0da700bf58f6e0b65312d0a5e695179a71d0163957fa381bb3c1f72972537c"}, - {file = "contourpy-1.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb8b141bb00fa977d9122636b16aa67d37fd40a3d8b52dd837e536d64b9a4d06"}, - {file = "contourpy-1.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3634b5385c6716c258d0419c46d05c8aa7dc8cb70326c9a4fb66b69ad2b52e09"}, - {file = "contourpy-1.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0dce35502151b6bd35027ac39ba6e5a44be13a68f55735c3612c568cac3805fd"}, - {file = "contourpy-1.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea348f053c645100612b333adc5983d87be69acdc6d77d3169c090d3b01dc35"}, - {file = "contourpy-1.3.0-cp312-cp312-win32.whl", hash = "sha256:90f73a5116ad1ba7174341ef3ea5c3150ddf20b024b98fb0c3b29034752c8aeb"}, - {file = "contourpy-1.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:b11b39aea6be6764f84360fce6c82211a9db32a7c7de8fa6dd5397cf1d079c3b"}, - {file = "contourpy-1.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3e1c7fa44aaae40a2247e2e8e0627f4bea3dd257014764aa644f319a5f8600e3"}, - {file = "contourpy-1.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:364174c2a76057feef647c802652f00953b575723062560498dc7930fc9b1cb7"}, - {file = "contourpy-1.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32b238b3b3b649e09ce9aaf51f0c261d38644bdfa35cbaf7b263457850957a84"}, - {file = "contourpy-1.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d51fca85f9f7ad0b65b4b9fe800406d0d77017d7270d31ec3fb1cc07358fdea0"}, - {file = "contourpy-1.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:732896af21716b29ab3e988d4ce14bc5133733b85956316fb0c56355f398099b"}, - {file = "contourpy-1.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d73f659398a0904e125280836ae6f88ba9b178b2fed6884f3b1f95b989d2c8da"}, - {file = "contourpy-1.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c6c7c2408b7048082932cf4e641fa3b8ca848259212f51c8c59c45aa7ac18f14"}, - {file = "contourpy-1.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f317576606de89da6b7e0861cf6061f6146ead3528acabff9236458a6ba467f8"}, - {file = "contourpy-1.3.0-cp313-cp313-win32.whl", hash = "sha256:31cd3a85dbdf1fc002280c65caa7e2b5f65e4a973fcdf70dd2fdcb9868069294"}, - {file = "contourpy-1.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:4553c421929ec95fb07b3aaca0fae668b2eb5a5203d1217ca7c34c063c53d087"}, - {file = "contourpy-1.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:345af746d7766821d05d72cb8f3845dfd08dd137101a2cb9b24de277d716def8"}, - {file = "contourpy-1.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3bb3808858a9dc68f6f03d319acd5f1b8a337e6cdda197f02f4b8ff67ad2057b"}, - {file = "contourpy-1.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:420d39daa61aab1221567b42eecb01112908b2cab7f1b4106a52caaec8d36973"}, - {file = "contourpy-1.3.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d63ee447261e963af02642ffcb864e5a2ee4cbfd78080657a9880b8b1868e18"}, - {file = "contourpy-1.3.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:167d6c890815e1dac9536dca00828b445d5d0df4d6a8c6adb4a7ec3166812fa8"}, - {file = "contourpy-1.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:710a26b3dc80c0e4febf04555de66f5fd17e9cf7170a7b08000601a10570bda6"}, - {file = "contourpy-1.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:75ee7cb1a14c617f34a51d11fa7524173e56551646828353c4af859c56b766e2"}, - {file = "contourpy-1.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:33c92cdae89ec5135d036e7218e69b0bb2851206077251f04a6c4e0e21f03927"}, - {file = "contourpy-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a11077e395f67ffc2c44ec2418cfebed032cd6da3022a94fc227b6faf8e2acb8"}, - {file = "contourpy-1.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e8134301d7e204c88ed7ab50028ba06c683000040ede1d617298611f9dc6240c"}, - {file = "contourpy-1.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e12968fdfd5bb45ffdf6192a590bd8ddd3ba9e58360b29683c6bb71a7b41edca"}, - {file = "contourpy-1.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fd2a0fc506eccaaa7595b7e1418951f213cf8255be2600f1ea1b61e46a60c55f"}, - {file = "contourpy-1.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4cfb5c62ce023dfc410d6059c936dcf96442ba40814aefbfa575425a3a7f19dc"}, - {file = "contourpy-1.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68a32389b06b82c2fdd68276148d7b9275b5f5cf13e5417e4252f6d1a34f72a2"}, - {file = "contourpy-1.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:94e848a6b83da10898cbf1311a815f770acc9b6a3f2d646f330d57eb4e87592e"}, - {file = "contourpy-1.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d78ab28a03c854a873787a0a42254a0ccb3cb133c672f645c9f9c8f3ae9d0800"}, - {file = "contourpy-1.3.0-cp39-cp39-win32.whl", hash = "sha256:81cb5ed4952aae6014bc9d0421dec7c5835c9c8c31cdf51910b708f548cf58e5"}, - {file = "contourpy-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:14e262f67bd7e6eb6880bc564dcda30b15e351a594657e55b7eec94b6ef72843"}, - {file = "contourpy-1.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fe41b41505a5a33aeaed2a613dccaeaa74e0e3ead6dd6fd3a118fb471644fd6c"}, - {file = "contourpy-1.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eca7e17a65f72a5133bdbec9ecf22401c62bcf4821361ef7811faee695799779"}, - {file = "contourpy-1.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1ec4dc6bf570f5b22ed0d7efba0dfa9c5b9e0431aeea7581aa217542d9e809a4"}, - {file = "contourpy-1.3.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:00ccd0dbaad6d804ab259820fa7cb0b8036bda0686ef844d24125d8287178ce0"}, - {file = "contourpy-1.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ca947601224119117f7c19c9cdf6b3ab54c5726ef1d906aa4a69dfb6dd58102"}, - {file = "contourpy-1.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c6ec93afeb848a0845a18989da3beca3eec2c0f852322efe21af1931147d12cb"}, - {file = "contourpy-1.3.0.tar.gz", hash = "sha256:7ffa0db17717a8ffb127efd0c95a4362d996b892c2904db72428d5b52e1938a4"}, + {file = "contourpy-1.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a045f341a77b77e1c5de31e74e966537bba9f3c4099b35bf4c2e3939dd54cdab"}, + {file = "contourpy-1.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:500360b77259914f7805af7462e41f9cb7ca92ad38e9f94d6c8641b089338124"}, + {file = "contourpy-1.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2f926efda994cdf3c8d3fdb40b9962f86edbc4457e739277b961eced3d0b4c1"}, + {file = "contourpy-1.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:adce39d67c0edf383647a3a007de0a45fd1b08dedaa5318404f1a73059c2512b"}, + {file = "contourpy-1.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abbb49fb7dac584e5abc6636b7b2a7227111c4f771005853e7d25176daaf8453"}, + {file = "contourpy-1.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0cffcbede75c059f535725c1680dfb17b6ba8753f0c74b14e6a9c68c29d7ea3"}, + {file = "contourpy-1.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ab29962927945d89d9b293eabd0d59aea28d887d4f3be6c22deaefbb938a7277"}, + {file = "contourpy-1.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:974d8145f8ca354498005b5b981165b74a195abfae9a8129df3e56771961d595"}, + {file = "contourpy-1.3.1-cp310-cp310-win32.whl", hash = "sha256:ac4578ac281983f63b400f7fe6c101bedc10651650eef012be1ccffcbacf3697"}, + {file = "contourpy-1.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:174e758c66bbc1c8576992cec9599ce8b6672b741b5d336b5c74e35ac382b18e"}, + {file = "contourpy-1.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3e8b974d8db2c5610fb4e76307e265de0edb655ae8169e8b21f41807ccbeec4b"}, + {file = "contourpy-1.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:20914c8c973f41456337652a6eeca26d2148aa96dd7ac323b74516988bea89fc"}, + {file = "contourpy-1.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19d40d37c1c3a4961b4619dd9d77b12124a453cc3d02bb31a07d58ef684d3d86"}, + {file = "contourpy-1.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:113231fe3825ebf6f15eaa8bc1f5b0ddc19d42b733345eae0934cb291beb88b6"}, + {file = "contourpy-1.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4dbbc03a40f916a8420e420d63e96a1258d3d1b58cbdfd8d1f07b49fcbd38e85"}, + {file = "contourpy-1.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a04ecd68acbd77fa2d39723ceca4c3197cb2969633836ced1bea14e219d077c"}, + {file = "contourpy-1.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c414fc1ed8ee1dbd5da626cf3710c6013d3d27456651d156711fa24f24bd1291"}, + {file = "contourpy-1.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:31c1b55c1f34f80557d3830d3dd93ba722ce7e33a0b472cba0ec3b6535684d8f"}, + {file = "contourpy-1.3.1-cp311-cp311-win32.whl", hash = "sha256:f611e628ef06670df83fce17805c344710ca5cde01edfdc72751311da8585375"}, + {file = "contourpy-1.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:b2bdca22a27e35f16794cf585832e542123296b4687f9fd96822db6bae17bfc9"}, + {file = "contourpy-1.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0ffa84be8e0bd33410b17189f7164c3589c229ce5db85798076a3fa136d0e509"}, + {file = "contourpy-1.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805617228ba7e2cbbfb6c503858e626ab528ac2a32a04a2fe88ffaf6b02c32bc"}, + {file = "contourpy-1.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade08d343436a94e633db932e7e8407fe7de8083967962b46bdfc1b0ced39454"}, + {file = "contourpy-1.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47734d7073fb4590b4a40122b35917cd77be5722d80683b249dac1de266aac80"}, + {file = "contourpy-1.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2ba94a401342fc0f8b948e57d977557fbf4d515f03c67682dd5c6191cb2d16ec"}, + {file = "contourpy-1.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efa874e87e4a647fd2e4f514d5e91c7d493697127beb95e77d2f7561f6905bd9"}, + {file = "contourpy-1.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1bf98051f1045b15c87868dbaea84f92408337d4f81d0e449ee41920ea121d3b"}, + {file = "contourpy-1.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:61332c87493b00091423e747ea78200659dc09bdf7fd69edd5e98cef5d3e9a8d"}, + {file = "contourpy-1.3.1-cp312-cp312-win32.whl", hash = "sha256:e914a8cb05ce5c809dd0fe350cfbb4e881bde5e2a38dc04e3afe1b3e58bd158e"}, + {file = "contourpy-1.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:08d9d449a61cf53033612cb368f3a1b26cd7835d9b8cd326647efe43bca7568d"}, + {file = "contourpy-1.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a761d9ccfc5e2ecd1bf05534eda382aa14c3e4f9205ba5b1684ecfe400716ef2"}, + {file = "contourpy-1.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:523a8ee12edfa36f6d2a49407f705a6ef4c5098de4f498619787e272de93f2d5"}, + {file = "contourpy-1.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece6df05e2c41bd46776fbc712e0996f7c94e0d0543af1656956d150c4ca7c81"}, + {file = "contourpy-1.3.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:573abb30e0e05bf31ed067d2f82500ecfdaec15627a59d63ea2d95714790f5c2"}, + {file = "contourpy-1.3.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9fa36448e6a3a1a9a2ba23c02012c43ed88905ec80163f2ffe2421c7192a5d7"}, + {file = "contourpy-1.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ea9924d28fc5586bf0b42d15f590b10c224117e74409dd7a0be3b62b74a501c"}, + {file = "contourpy-1.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b75aa69cb4d6f137b36f7eb2ace9280cfb60c55dc5f61c731fdf6f037f958a3"}, + {file = "contourpy-1.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:041b640d4ec01922083645a94bb3b2e777e6b626788f4095cf21abbe266413c1"}, + {file = "contourpy-1.3.1-cp313-cp313-win32.whl", hash = "sha256:36987a15e8ace5f58d4d5da9dca82d498c2bbb28dff6e5d04fbfcc35a9cb3a82"}, + {file = "contourpy-1.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:a7895f46d47671fa7ceec40f31fae721da51ad34bdca0bee83e38870b1f47ffd"}, + {file = "contourpy-1.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9ddeb796389dadcd884c7eb07bd14ef12408aaae358f0e2ae24114d797eede30"}, + {file = "contourpy-1.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:19c1555a6801c2f084c7ddc1c6e11f02eb6a6016ca1318dd5452ba3f613a1751"}, + {file = "contourpy-1.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:841ad858cff65c2c04bf93875e384ccb82b654574a6d7f30453a04f04af71342"}, + {file = "contourpy-1.3.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4318af1c925fb9a4fb190559ef3eec206845f63e80fb603d47f2d6d67683901c"}, + {file = "contourpy-1.3.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:14c102b0eab282427b662cb590f2e9340a9d91a1c297f48729431f2dcd16e14f"}, + {file = "contourpy-1.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05e806338bfeaa006acbdeba0ad681a10be63b26e1b17317bfac3c5d98f36cda"}, + {file = "contourpy-1.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4d76d5993a34ef3df5181ba3c92fabb93f1eaa5729504fb03423fcd9f3177242"}, + {file = "contourpy-1.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:89785bb2a1980c1bd87f0cb1517a71cde374776a5f150936b82580ae6ead44a1"}, + {file = "contourpy-1.3.1-cp313-cp313t-win32.whl", hash = "sha256:8eb96e79b9f3dcadbad2a3891672f81cdcab7f95b27f28f1c67d75f045b6b4f1"}, + {file = "contourpy-1.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:287ccc248c9e0d0566934e7d606201abd74761b5703d804ff3df8935f523d546"}, + {file = "contourpy-1.3.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b457d6430833cee8e4b8e9b6f07aa1c161e5e0d52e118dc102c8f9bd7dd060d6"}, + {file = "contourpy-1.3.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb76c1a154b83991a3cbbf0dfeb26ec2833ad56f95540b442c73950af2013750"}, + {file = "contourpy-1.3.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:44a29502ca9c7b5ba389e620d44f2fbe792b1fb5734e8b931ad307071ec58c53"}, + {file = "contourpy-1.3.1.tar.gz", hash = "sha256:dfd97abd83335045a913e3bcc4a09c0ceadbe66580cf573fe961f4a825efa699"}, ] [package.dependencies] @@ -671,73 +671,73 @@ test-no-images = ["pytest", "pytest-cov", "pytest-rerunfailures", "pytest-xdist" [[package]] name = "coverage" -version = "7.6.8" +version = "7.6.9" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" files = [ - {file = "coverage-7.6.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b39e6011cd06822eb964d038d5dff5da5d98652b81f5ecd439277b32361a3a50"}, - {file = "coverage-7.6.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:63c19702db10ad79151a059d2d6336fe0c470f2e18d0d4d1a57f7f9713875dcf"}, - {file = "coverage-7.6.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3985b9be361d8fb6b2d1adc9924d01dec575a1d7453a14cccd73225cb79243ee"}, - {file = "coverage-7.6.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:644ec81edec0f4ad17d51c838a7d01e42811054543b76d4ba2c5d6af741ce2a6"}, - {file = "coverage-7.6.8-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f188a2402f8359cf0c4b1fe89eea40dc13b52e7b4fd4812450da9fcd210181d"}, - {file = "coverage-7.6.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e19122296822deafce89a0c5e8685704c067ae65d45e79718c92df7b3ec3d331"}, - {file = "coverage-7.6.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:13618bed0c38acc418896005732e565b317aa9e98d855a0e9f211a7ffc2d6638"}, - {file = "coverage-7.6.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:193e3bffca48ad74b8c764fb4492dd875038a2f9925530cb094db92bb5e47bed"}, - {file = "coverage-7.6.8-cp310-cp310-win32.whl", hash = "sha256:3988665ee376abce49613701336544041f2117de7b7fbfe91b93d8ff8b151c8e"}, - {file = "coverage-7.6.8-cp310-cp310-win_amd64.whl", hash = "sha256:f56f49b2553d7dd85fd86e029515a221e5c1f8cb3d9c38b470bc38bde7b8445a"}, - {file = "coverage-7.6.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:86cffe9c6dfcfe22e28027069725c7f57f4b868a3f86e81d1c62462764dc46d4"}, - {file = "coverage-7.6.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d82ab6816c3277dc962cfcdc85b1efa0e5f50fb2c449432deaf2398a2928ab94"}, - {file = "coverage-7.6.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13690e923a3932e4fad4c0ebfb9cb5988e03d9dcb4c5150b5fcbf58fd8bddfc4"}, - {file = "coverage-7.6.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4be32da0c3827ac9132bb488d331cb32e8d9638dd41a0557c5569d57cf22c9c1"}, - {file = "coverage-7.6.8-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44e6c85bbdc809383b509d732b06419fb4544dca29ebe18480379633623baafb"}, - {file = "coverage-7.6.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:768939f7c4353c0fac2f7c37897e10b1414b571fd85dd9fc49e6a87e37a2e0d8"}, - {file = "coverage-7.6.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e44961e36cb13c495806d4cac67640ac2866cb99044e210895b506c26ee63d3a"}, - {file = "coverage-7.6.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ea8bb1ab9558374c0ab591783808511d135a833c3ca64a18ec927f20c4030f0"}, - {file = "coverage-7.6.8-cp311-cp311-win32.whl", hash = "sha256:629a1ba2115dce8bf75a5cce9f2486ae483cb89c0145795603d6554bdc83e801"}, - {file = "coverage-7.6.8-cp311-cp311-win_amd64.whl", hash = "sha256:fb9fc32399dca861584d96eccd6c980b69bbcd7c228d06fb74fe53e007aa8ef9"}, - {file = "coverage-7.6.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e683e6ecc587643f8cde8f5da6768e9d165cd31edf39ee90ed7034f9ca0eefee"}, - {file = "coverage-7.6.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1defe91d41ce1bd44b40fabf071e6a01a5aa14de4a31b986aa9dfd1b3e3e414a"}, - {file = "coverage-7.6.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7ad66e8e50225ebf4236368cc43c37f59d5e6728f15f6e258c8639fa0dd8e6d"}, - {file = "coverage-7.6.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fe47da3e4fda5f1abb5709c156eca207eacf8007304ce3019eb001e7a7204cb"}, - {file = "coverage-7.6.8-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:202a2d645c5a46b84992f55b0a3affe4f0ba6b4c611abec32ee88358db4bb649"}, - {file = "coverage-7.6.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4674f0daa1823c295845b6a740d98a840d7a1c11df00d1fd62614545c1583787"}, - {file = "coverage-7.6.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:74610105ebd6f33d7c10f8907afed696e79c59e3043c5f20eaa3a46fddf33b4c"}, - {file = "coverage-7.6.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37cda8712145917105e07aab96388ae76e787270ec04bcb9d5cc786d7cbb8443"}, - {file = "coverage-7.6.8-cp312-cp312-win32.whl", hash = "sha256:9e89d5c8509fbd6c03d0dd1972925b22f50db0792ce06324ba069f10787429ad"}, - {file = "coverage-7.6.8-cp312-cp312-win_amd64.whl", hash = "sha256:379c111d3558272a2cae3d8e57e6b6e6f4fe652905692d54bad5ea0ca37c5ad4"}, - {file = "coverage-7.6.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0b0c69f4f724c64dfbfe79f5dfb503b42fe6127b8d479b2677f2b227478db2eb"}, - {file = "coverage-7.6.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c15b32a7aca8038ed7644f854bf17b663bc38e1671b5d6f43f9a2b2bd0c46f63"}, - {file = "coverage-7.6.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63068a11171e4276f6ece913bde059e77c713b48c3a848814a6537f35afb8365"}, - {file = "coverage-7.6.8-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f4548c5ead23ad13fb7a2c8ea541357474ec13c2b736feb02e19a3085fac002"}, - {file = "coverage-7.6.8-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b4b4299dd0d2c67caaaf286d58aef5e75b125b95615dda4542561a5a566a1e3"}, - {file = "coverage-7.6.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9ebfb2507751f7196995142f057d1324afdab56db1d9743aab7f50289abd022"}, - {file = "coverage-7.6.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c1b4474beee02ede1eef86c25ad4600a424fe36cff01a6103cb4533c6bf0169e"}, - {file = "coverage-7.6.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d9fd2547e6decdbf985d579cf3fc78e4c1d662b9b0ff7cc7862baaab71c9cc5b"}, - {file = "coverage-7.6.8-cp313-cp313-win32.whl", hash = "sha256:8aae5aea53cbfe024919715eca696b1a3201886ce83790537d1c3668459c7146"}, - {file = "coverage-7.6.8-cp313-cp313-win_amd64.whl", hash = "sha256:ae270e79f7e169ccfe23284ff5ea2d52a6f401dc01b337efb54b3783e2ce3f28"}, - {file = "coverage-7.6.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:de38add67a0af869b0d79c525d3e4588ac1ffa92f39116dbe0ed9753f26eba7d"}, - {file = "coverage-7.6.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b07c25d52b1c16ce5de088046cd2432b30f9ad5e224ff17c8f496d9cb7d1d451"}, - {file = "coverage-7.6.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62a66ff235e4c2e37ed3b6104d8b478d767ff73838d1222132a7a026aa548764"}, - {file = "coverage-7.6.8-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09b9f848b28081e7b975a3626e9081574a7b9196cde26604540582da60235fdf"}, - {file = "coverage-7.6.8-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:093896e530c38c8e9c996901858ac63f3d4171268db2c9c8b373a228f459bbc5"}, - {file = "coverage-7.6.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a7b8ac36fd688c8361cbc7bf1cb5866977ece6e0b17c34aa0df58bda4fa18a4"}, - {file = "coverage-7.6.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:38c51297b35b3ed91670e1e4efb702b790002e3245a28c76e627478aa3c10d83"}, - {file = "coverage-7.6.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2e4e0f60cb4bd7396108823548e82fdab72d4d8a65e58e2c19bbbc2f1e2bfa4b"}, - {file = "coverage-7.6.8-cp313-cp313t-win32.whl", hash = "sha256:6535d996f6537ecb298b4e287a855f37deaf64ff007162ec0afb9ab8ba3b8b71"}, - {file = "coverage-7.6.8-cp313-cp313t-win_amd64.whl", hash = "sha256:c79c0685f142ca53256722a384540832420dff4ab15fec1863d7e5bc8691bdcc"}, - {file = "coverage-7.6.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3ac47fa29d8d41059ea3df65bd3ade92f97ee4910ed638e87075b8e8ce69599e"}, - {file = "coverage-7.6.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:24eda3a24a38157eee639ca9afe45eefa8d2420d49468819ac5f88b10de84f4c"}, - {file = "coverage-7.6.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4c81ed2820b9023a9a90717020315e63b17b18c274a332e3b6437d7ff70abe0"}, - {file = "coverage-7.6.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd55f8fc8fa494958772a2a7302b0354ab16e0b9272b3c3d83cdb5bec5bd1779"}, - {file = "coverage-7.6.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f39e2f3530ed1626c66e7493be7a8423b023ca852aacdc91fb30162c350d2a92"}, - {file = "coverage-7.6.8-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:716a78a342679cd1177bc8c2fe957e0ab91405bd43a17094324845200b2fddf4"}, - {file = "coverage-7.6.8-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:177f01eeaa3aee4a5ffb0d1439c5952b53d5010f86e9d2667963e632e30082cc"}, - {file = "coverage-7.6.8-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:912e95017ff51dc3d7b6e2be158dedc889d9a5cc3382445589ce554f1a34c0ea"}, - {file = "coverage-7.6.8-cp39-cp39-win32.whl", hash = "sha256:4db3ed6a907b555e57cc2e6f14dc3a4c2458cdad8919e40b5357ab9b6db6c43e"}, - {file = "coverage-7.6.8-cp39-cp39-win_amd64.whl", hash = "sha256:428ac484592f780e8cd7b6b14eb568f7c85460c92e2a37cb0c0e5186e1a0d076"}, - {file = "coverage-7.6.8-pp39.pp310-none-any.whl", hash = "sha256:5c52a036535d12590c32c49209e79cabaad9f9ad8aa4cbd875b68c4d67a9cbce"}, - {file = "coverage-7.6.8.tar.gz", hash = "sha256:8b2b8503edb06822c86d82fa64a4a5cb0760bb8f31f26e138ec743f422f37cfc"}, + {file = "coverage-7.6.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:85d9636f72e8991a1706b2b55b06c27545448baf9f6dbf51c4004609aacd7dcb"}, + {file = "coverage-7.6.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:608a7fd78c67bee8936378299a6cb9f5149bb80238c7a566fc3e6717a4e68710"}, + {file = "coverage-7.6.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96d636c77af18b5cb664ddf12dab9b15a0cfe9c0bde715da38698c8cea748bfa"}, + {file = "coverage-7.6.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d75cded8a3cff93da9edc31446872d2997e327921d8eed86641efafd350e1df1"}, + {file = "coverage-7.6.9-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7b15f589593110ae767ce997775d645b47e5cbbf54fd322f8ebea6277466cec"}, + {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:44349150f6811b44b25574839b39ae35291f6496eb795b7366fef3bd3cf112d3"}, + {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d891c136b5b310d0e702e186d70cd16d1119ea8927347045124cb286b29297e5"}, + {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:db1dab894cc139f67822a92910466531de5ea6034ddfd2b11c0d4c6257168073"}, + {file = "coverage-7.6.9-cp310-cp310-win32.whl", hash = "sha256:41ff7b0da5af71a51b53f501a3bac65fb0ec311ebed1632e58fc6107f03b9198"}, + {file = "coverage-7.6.9-cp310-cp310-win_amd64.whl", hash = "sha256:35371f8438028fdccfaf3570b31d98e8d9eda8bb1d6ab9473f5a390969e98717"}, + {file = "coverage-7.6.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:932fc826442132dde42ee52cf66d941f581c685a6313feebed358411238f60f9"}, + {file = "coverage-7.6.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:085161be5f3b30fd9b3e7b9a8c301f935c8313dcf928a07b116324abea2c1c2c"}, + {file = "coverage-7.6.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ccc660a77e1c2bf24ddbce969af9447a9474790160cfb23de6be4fa88e3951c7"}, + {file = "coverage-7.6.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c69e42c892c018cd3c8d90da61d845f50a8243062b19d228189b0224150018a9"}, + {file = "coverage-7.6.9-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0824a28ec542a0be22f60c6ac36d679e0e262e5353203bea81d44ee81fe9c6d4"}, + {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4401ae5fc52ad8d26d2a5d8a7428b0f0c72431683f8e63e42e70606374c311a1"}, + {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:98caba4476a6c8d59ec1eb00c7dd862ba9beca34085642d46ed503cc2d440d4b"}, + {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ee5defd1733fd6ec08b168bd4f5387d5b322f45ca9e0e6c817ea6c4cd36313e3"}, + {file = "coverage-7.6.9-cp311-cp311-win32.whl", hash = "sha256:f2d1ec60d6d256bdf298cb86b78dd715980828f50c46701abc3b0a2b3f8a0dc0"}, + {file = "coverage-7.6.9-cp311-cp311-win_amd64.whl", hash = "sha256:0d59fd927b1f04de57a2ba0137166d31c1a6dd9e764ad4af552912d70428c92b"}, + {file = "coverage-7.6.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:99e266ae0b5d15f1ca8d278a668df6f51cc4b854513daab5cae695ed7b721cf8"}, + {file = "coverage-7.6.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9901d36492009a0a9b94b20e52ebfc8453bf49bb2b27bca2c9706f8b4f5a554a"}, + {file = "coverage-7.6.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abd3e72dd5b97e3af4246cdada7738ef0e608168de952b837b8dd7e90341f015"}, + {file = "coverage-7.6.9-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff74026a461eb0660366fb01c650c1d00f833a086b336bdad7ab00cc952072b3"}, + {file = "coverage-7.6.9-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65dad5a248823a4996724a88eb51d4b31587aa7aa428562dbe459c684e5787ae"}, + {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:22be16571504c9ccea919fcedb459d5ab20d41172056206eb2994e2ff06118a4"}, + {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f957943bc718b87144ecaee70762bc2bc3f1a7a53c7b861103546d3a403f0a6"}, + {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ae1387db4aecb1f485fb70a6c0148c6cdaebb6038f1d40089b1fc84a5db556f"}, + {file = "coverage-7.6.9-cp312-cp312-win32.whl", hash = "sha256:1a330812d9cc7ac2182586f6d41b4d0fadf9be9049f350e0efb275c8ee8eb692"}, + {file = "coverage-7.6.9-cp312-cp312-win_amd64.whl", hash = "sha256:b12c6b18269ca471eedd41c1b6a1065b2f7827508edb9a7ed5555e9a56dcfc97"}, + {file = "coverage-7.6.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:899b8cd4781c400454f2f64f7776a5d87bbd7b3e7f7bda0cb18f857bb1334664"}, + {file = "coverage-7.6.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:61f70dc68bd36810972e55bbbe83674ea073dd1dcc121040a08cdf3416c5349c"}, + {file = "coverage-7.6.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a289d23d4c46f1a82d5db4abeb40b9b5be91731ee19a379d15790e53031c014"}, + {file = "coverage-7.6.9-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e216d8044a356fc0337c7a2a0536d6de07888d7bcda76febcb8adc50bdbbd00"}, + {file = "coverage-7.6.9-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c026eb44f744acaa2bda7493dad903aa5bf5fc4f2554293a798d5606710055d"}, + {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e77363e8425325384f9d49272c54045bbed2f478e9dd698dbc65dbc37860eb0a"}, + {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:777abfab476cf83b5177b84d7486497e034eb9eaea0d746ce0c1268c71652077"}, + {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:447af20e25fdbe16f26e84eb714ba21d98868705cb138252d28bc400381f6ffb"}, + {file = "coverage-7.6.9-cp313-cp313-win32.whl", hash = "sha256:d872ec5aeb086cbea771c573600d47944eea2dcba8be5f3ee649bfe3cb8dc9ba"}, + {file = "coverage-7.6.9-cp313-cp313-win_amd64.whl", hash = "sha256:fd1213c86e48dfdc5a0cc676551db467495a95a662d2396ecd58e719191446e1"}, + {file = "coverage-7.6.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ba9e7484d286cd5a43744e5f47b0b3fb457865baf07bafc6bee91896364e1419"}, + {file = "coverage-7.6.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1cf0872ee455c03e5674b5bca5e3e68e159379c1af0903e89f5eba9ccc3a"}, + {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d10e07aa2b91835d6abec555ec8b2733347956991901eea6ffac295f83a30e4"}, + {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:13a9e2d3ee855db3dd6ea1ba5203316a1b1fd8eaeffc37c5b54987e61e4194ae"}, + {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c38bf15a40ccf5619fa2fe8f26106c7e8e080d7760aeccb3722664c8656b030"}, + {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d5275455b3e4627c8e7154feaf7ee0743c2e7af82f6e3b561967b1cca755a0be"}, + {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8f8770dfc6e2c6a2d4569f411015c8d751c980d17a14b0530da2d7f27ffdd88e"}, + {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8d2dfa71665a29b153a9681edb1c8d9c1ea50dfc2375fb4dac99ea7e21a0bcd9"}, + {file = "coverage-7.6.9-cp313-cp313t-win32.whl", hash = "sha256:5e6b86b5847a016d0fbd31ffe1001b63355ed309651851295315031ea7eb5a9b"}, + {file = "coverage-7.6.9-cp313-cp313t-win_amd64.whl", hash = "sha256:97ddc94d46088304772d21b060041c97fc16bdda13c6c7f9d8fcd8d5ae0d8611"}, + {file = "coverage-7.6.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:adb697c0bd35100dc690de83154627fbab1f4f3c0386df266dded865fc50a902"}, + {file = "coverage-7.6.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:be57b6d56e49c2739cdf776839a92330e933dd5e5d929966fbbd380c77f060be"}, + {file = "coverage-7.6.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1592791f8204ae9166de22ba7e6705fa4ebd02936c09436a1bb85aabca3e599"}, + {file = "coverage-7.6.9-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e12ae8cc979cf83d258acb5e1f1cf2f3f83524d1564a49d20b8bec14b637f08"}, + {file = "coverage-7.6.9-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb5555cff66c4d3d6213a296b360f9e1a8e323e74e0426b6c10ed7f4d021e464"}, + {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b9389a429e0e5142e69d5bf4a435dd688c14478a19bb901735cdf75e57b13845"}, + {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:592ac539812e9b46046620341498caf09ca21023c41c893e1eb9dbda00a70cbf"}, + {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a27801adef24cc30871da98a105f77995e13a25a505a0161911f6aafbd66e678"}, + {file = "coverage-7.6.9-cp39-cp39-win32.whl", hash = "sha256:8e3c3e38930cfb729cb8137d7f055e5a473ddaf1217966aa6238c88bd9fd50e6"}, + {file = "coverage-7.6.9-cp39-cp39-win_amd64.whl", hash = "sha256:e28bf44afa2b187cc9f41749138a64435bf340adfcacb5b2290c070ce99839d4"}, + {file = "coverage-7.6.9-pp39.pp310-none-any.whl", hash = "sha256:f3ca78518bc6bc92828cd11867b121891d75cae4ea9e908d72030609b996db1b"}, + {file = "coverage-7.6.9.tar.gz", hash = "sha256:4a8d8977b0c6ef5aeadcb644da9e69ae0dcfe66ec7f368c89c72e058bd71164d"}, ] [package.dependencies] @@ -748,51 +748,51 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "43.0.3" +version = "44.0.0" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false -python-versions = ">=3.7" -files = [ - {file = "cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e"}, - {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e"}, - {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f"}, - {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6"}, - {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18"}, - {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd"}, - {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73"}, - {file = "cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2"}, - {file = "cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd"}, - {file = "cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984"}, - {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5"}, - {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4"}, - {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7"}, - {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405"}, - {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16"}, - {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73"}, - {file = "cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995"}, - {file = "cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362"}, - {file = "cryptography-43.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c"}, - {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3"}, - {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83"}, - {file = "cryptography-43.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7"}, - {file = "cryptography-43.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664"}, - {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08"}, - {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa"}, - {file = "cryptography-43.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff"}, - {file = "cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805"}, +python-versions = "!=3.9.0,!=3.9.1,>=3.7" +files = [ + {file = "cryptography-44.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:84111ad4ff3f6253820e6d3e58be2cc2a00adb29335d4cacb5ab4d4d34f2a123"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15492a11f9e1b62ba9d73c210e2416724633167de94607ec6069ef724fad092"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831c3c4d0774e488fdc83a1923b49b9957d33287de923d58ebd3cec47a0ae43f"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543"}, + {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e"}, + {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e"}, + {file = "cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053"}, + {file = "cryptography-44.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:abc998e0c0eee3c8a1904221d3f67dcfa76422b23620173e28c11d3e626c21bd"}, + {file = "cryptography-44.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:660cb7312a08bc38be15b696462fa7cc7cd85c3ed9c576e81f4dc4d8b2b31591"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1923cb251c04be85eec9fda837661c67c1049063305d6be5721643c22dd4e2b7"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:404fdc66ee5f83a1388be54300ae978b2efd538018de18556dde92575e05defc"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c"}, + {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64"}, + {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285"}, + {file = "cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417"}, + {file = "cryptography-44.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:708ee5f1bafe76d041b53a4f95eb28cdeb8d18da17e597d46d7833ee59b97ede"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:37d76e6863da3774cd9db5b409a9ecfd2c71c981c38788d3fcfaf177f447b731"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:f677e1268c4e23420c3acade68fac427fffcb8d19d7df95ed7ad17cdef8404f4"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f5e7cb1e5e56ca0933b4873c0220a78b773b24d40d186b6738080b73d3d0a756"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:8b3e6eae66cf54701ee7d9c83c30ac0a1e3fa17be486033000f2a73a12ab507c"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:be4ce505894d15d5c5037167ffb7f0ae90b7be6f2a98f9a5c3442395501c32fa"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:62901fb618f74d7d81bf408c8719e9ec14d863086efe4185afd07c352aee1d2c"}, + {file = "cryptography-44.0.0.tar.gz", hash = "sha256:cd4e834f340b4293430701e772ec543b0fbe6c2dea510a5286fe0acabe153a02"}, ] [package.dependencies] cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} [package.extras] -docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] -docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] -nox = ["nox"] -pep8test = ["check-sdist", "click", "mypy", "ruff"] -sdist = ["build"] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=3.0.0)"] +docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] +nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2)"] +pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] +sdist = ["build (>=1.0.0)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi", "cryptography-vectors (==43.0.3)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test = ["certifi (>=2024)", "cryptography-vectors (==44.0.0)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] test-randomorder = ["pytest-randomly"] [[package]] @@ -918,37 +918,37 @@ files = [ [[package]] name = "debugpy" -version = "1.8.9" +version = "1.8.11" description = "An implementation of the Debug Adapter Protocol for Python" optional = true python-versions = ">=3.8" files = [ - {file = "debugpy-1.8.9-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:cfe1e6c6ad7178265f74981edf1154ffce97b69005212fbc90ca22ddfe3d017e"}, - {file = "debugpy-1.8.9-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ada7fb65102a4d2c9ab62e8908e9e9f12aed9d76ef44880367bc9308ebe49a0f"}, - {file = "debugpy-1.8.9-cp310-cp310-win32.whl", hash = "sha256:c36856343cbaa448171cba62a721531e10e7ffb0abff838004701454149bc037"}, - {file = "debugpy-1.8.9-cp310-cp310-win_amd64.whl", hash = "sha256:17c5e0297678442511cf00a745c9709e928ea4ca263d764e90d233208889a19e"}, - {file = "debugpy-1.8.9-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:b74a49753e21e33e7cf030883a92fa607bddc4ede1aa4145172debc637780040"}, - {file = "debugpy-1.8.9-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62d22dacdb0e296966d7d74a7141aaab4bec123fa43d1a35ddcb39bf9fd29d70"}, - {file = "debugpy-1.8.9-cp311-cp311-win32.whl", hash = "sha256:8138efff315cd09b8dcd14226a21afda4ca582284bf4215126d87342bba1cc66"}, - {file = "debugpy-1.8.9-cp311-cp311-win_amd64.whl", hash = "sha256:ff54ef77ad9f5c425398efb150239f6fe8e20c53ae2f68367eba7ece1e96226d"}, - {file = "debugpy-1.8.9-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:957363d9a7a6612a37458d9a15e72d03a635047f946e5fceee74b50d52a9c8e2"}, - {file = "debugpy-1.8.9-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e565fc54b680292b418bb809f1386f17081d1346dca9a871bf69a8ac4071afe"}, - {file = "debugpy-1.8.9-cp312-cp312-win32.whl", hash = "sha256:3e59842d6c4569c65ceb3751075ff8d7e6a6ada209ceca6308c9bde932bcef11"}, - {file = "debugpy-1.8.9-cp312-cp312-win_amd64.whl", hash = "sha256:66eeae42f3137eb428ea3a86d4a55f28da9bd5a4a3d369ba95ecc3a92c1bba53"}, - {file = "debugpy-1.8.9-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:957ecffff80d47cafa9b6545de9e016ae8c9547c98a538ee96ab5947115fb3dd"}, - {file = "debugpy-1.8.9-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1efbb3ff61487e2c16b3e033bc8595aea578222c08aaf3c4bf0f93fadbd662ee"}, - {file = "debugpy-1.8.9-cp313-cp313-win32.whl", hash = "sha256:7c4d65d03bee875bcb211c76c1d8f10f600c305dbd734beaed4077e902606fee"}, - {file = "debugpy-1.8.9-cp313-cp313-win_amd64.whl", hash = "sha256:e46b420dc1bea64e5bbedd678148be512442bc589b0111bd799367cde051e71a"}, - {file = "debugpy-1.8.9-cp38-cp38-macosx_14_0_x86_64.whl", hash = "sha256:472a3994999fe6c0756945ffa359e9e7e2d690fb55d251639d07208dbc37caea"}, - {file = "debugpy-1.8.9-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:365e556a4772d7d0d151d7eb0e77ec4db03bcd95f26b67b15742b88cacff88e9"}, - {file = "debugpy-1.8.9-cp38-cp38-win32.whl", hash = "sha256:54a7e6d3014c408eb37b0b06021366ee985f1539e12fe49ca2ee0d392d9ceca5"}, - {file = "debugpy-1.8.9-cp38-cp38-win_amd64.whl", hash = "sha256:8e99c0b1cc7bf86d83fb95d5ccdc4ad0586d4432d489d1f54e4055bcc795f693"}, - {file = "debugpy-1.8.9-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:7e8b079323a56f719977fde9d8115590cb5e7a1cba2fcee0986ef8817116e7c1"}, - {file = "debugpy-1.8.9-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6953b335b804a41f16a192fa2e7851bdcfd92173cbb2f9f777bb934f49baab65"}, - {file = "debugpy-1.8.9-cp39-cp39-win32.whl", hash = "sha256:7e646e62d4602bb8956db88b1e72fe63172148c1e25c041e03b103a25f36673c"}, - {file = "debugpy-1.8.9-cp39-cp39-win_amd64.whl", hash = "sha256:3d9755e77a2d680ce3d2c5394a444cf42be4a592caaf246dbfbdd100ffcf7ae5"}, - {file = "debugpy-1.8.9-py2.py3-none-any.whl", hash = "sha256:cc37a6c9987ad743d9c3a14fa1b1a14b7e4e6041f9dd0c8abf8895fe7a97b899"}, - {file = "debugpy-1.8.9.zip", hash = "sha256:1339e14c7d980407248f09824d1b25ff5c5616651689f1e0f0e51bdead3ea13e"}, + {file = "debugpy-1.8.11-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:2b26fefc4e31ff85593d68b9022e35e8925714a10ab4858fb1b577a8a48cb8cd"}, + {file = "debugpy-1.8.11-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61bc8b3b265e6949855300e84dc93d02d7a3a637f2aec6d382afd4ceb9120c9f"}, + {file = "debugpy-1.8.11-cp310-cp310-win32.whl", hash = "sha256:c928bbf47f65288574b78518449edaa46c82572d340e2750889bbf8cd92f3737"}, + {file = "debugpy-1.8.11-cp310-cp310-win_amd64.whl", hash = "sha256:8da1db4ca4f22583e834dcabdc7832e56fe16275253ee53ba66627b86e304da1"}, + {file = "debugpy-1.8.11-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:85de8474ad53ad546ff1c7c7c89230db215b9b8a02754d41cb5a76f70d0be296"}, + {file = "debugpy-1.8.11-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ffc382e4afa4aee367bf413f55ed17bd91b191dcaf979890af239dda435f2a1"}, + {file = "debugpy-1.8.11-cp311-cp311-win32.whl", hash = "sha256:40499a9979c55f72f4eb2fc38695419546b62594f8af194b879d2a18439c97a9"}, + {file = "debugpy-1.8.11-cp311-cp311-win_amd64.whl", hash = "sha256:987bce16e86efa86f747d5151c54e91b3c1e36acc03ce1ddb50f9d09d16ded0e"}, + {file = "debugpy-1.8.11-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:84e511a7545d11683d32cdb8f809ef63fc17ea2a00455cc62d0a4dbb4ed1c308"}, + {file = "debugpy-1.8.11-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce291a5aca4985d82875d6779f61375e959208cdf09fcec40001e65fb0a54768"}, + {file = "debugpy-1.8.11-cp312-cp312-win32.whl", hash = "sha256:28e45b3f827d3bf2592f3cf7ae63282e859f3259db44ed2b129093ca0ac7940b"}, + {file = "debugpy-1.8.11-cp312-cp312-win_amd64.whl", hash = "sha256:44b1b8e6253bceada11f714acf4309ffb98bfa9ac55e4fce14f9e5d4484287a1"}, + {file = "debugpy-1.8.11-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:8988f7163e4381b0da7696f37eec7aca19deb02e500245df68a7159739bbd0d3"}, + {file = "debugpy-1.8.11-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c1f6a173d1140e557347419767d2b14ac1c9cd847e0b4c5444c7f3144697e4e"}, + {file = "debugpy-1.8.11-cp313-cp313-win32.whl", hash = "sha256:bb3b15e25891f38da3ca0740271e63ab9db61f41d4d8541745cfc1824252cb28"}, + {file = "debugpy-1.8.11-cp313-cp313-win_amd64.whl", hash = "sha256:d8768edcbeb34da9e11bcb8b5c2e0958d25218df7a6e56adf415ef262cd7b6d1"}, + {file = "debugpy-1.8.11-cp38-cp38-macosx_14_0_x86_64.whl", hash = "sha256:ad7efe588c8f5cf940f40c3de0cd683cc5b76819446abaa50dc0829a30c094db"}, + {file = "debugpy-1.8.11-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:189058d03a40103a57144752652b3ab08ff02b7595d0ce1f651b9acc3a3a35a0"}, + {file = "debugpy-1.8.11-cp38-cp38-win32.whl", hash = "sha256:32db46ba45849daed7ccf3f2e26f7a386867b077f39b2a974bb5c4c2c3b0a280"}, + {file = "debugpy-1.8.11-cp38-cp38-win_amd64.whl", hash = "sha256:116bf8342062246ca749013df4f6ea106f23bc159305843491f64672a55af2e5"}, + {file = "debugpy-1.8.11-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:654130ca6ad5de73d978057eaf9e582244ff72d4574b3e106fb8d3d2a0d32458"}, + {file = "debugpy-1.8.11-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23dc34c5e03b0212fa3c49a874df2b8b1b8fda95160bd79c01eb3ab51ea8d851"}, + {file = "debugpy-1.8.11-cp39-cp39-win32.whl", hash = "sha256:52d8a3166c9f2815bfae05f386114b0b2d274456980d41f320299a8d9a5615a7"}, + {file = "debugpy-1.8.11-cp39-cp39-win_amd64.whl", hash = "sha256:52c3cf9ecda273a19cc092961ee34eb9ba8687d67ba34cc7b79a521c1c64c4c0"}, + {file = "debugpy-1.8.11-py2.py3-none-any.whl", hash = "sha256:0e22f846f4211383e6a416d04b4c13ed174d24cc5d43f5fd52e7821d0ebc8920"}, + {file = "debugpy-1.8.11.tar.gz", hash = "sha256:6ad2688b69235c43b020e04fecccdf6a96c8943ca9c2fb340b8adc103c655e57"}, ] [[package]] @@ -1044,7 +1044,7 @@ testing = ["hatch", "pre-commit", "pytest", "tox"] name = "executing" version = "2.1.0" description = "Get the currently executing AST node of a frame, and other information" -optional = true +optional = false python-versions = ">=3.8" files = [ {file = "executing-2.1.0-py2.py3-none-any.whl", hash = "sha256:8d63781349375b5ebccc3142f4b30350c0cd9c79f921cde38be2be4637e98eaf"}, @@ -1086,61 +1086,61 @@ typing = ["typing-extensions (>=4.12.2)"] [[package]] name = "fonttools" -version = "4.55.1" +version = "4.55.3" description = "Tools to manipulate font files" optional = false python-versions = ">=3.8" files = [ - {file = "fonttools-4.55.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c17a6f9814f83772cd6d9c9009928e1afa4ab66210a31ced721556651075a9a0"}, - {file = "fonttools-4.55.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c4d14eecc814826a01db87a40af3407c892ba49996bc6e49961e386cd78b537c"}, - {file = "fonttools-4.55.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8589f9a15dc005592b94ecdc45b4dfae9bbe9e73542e89af5a5e776e745db83b"}, - {file = "fonttools-4.55.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfee95bd9395bcd9e6c78955387554335109b6a613db71ef006020b42f761c58"}, - {file = "fonttools-4.55.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:34fa2ecc0bf1923d1a51bf2216a006de2c3c0db02c6aa1470ea50b62b8619bd5"}, - {file = "fonttools-4.55.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9c1c48483148bfb1b9ad951133ceea957faa004f6cb475b67e7bc75d482b48f8"}, - {file = "fonttools-4.55.1-cp310-cp310-win32.whl", hash = "sha256:3e2fc388ca7d023b3c45badd71016fd4185f93e51a22cfe4bd65378af7fba759"}, - {file = "fonttools-4.55.1-cp310-cp310-win_amd64.whl", hash = "sha256:c4c36c71f69d2b3ee30394b0986e5f8b2c461e7eff48dde49b08a90ded9fcdbd"}, - {file = "fonttools-4.55.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5daab3a55d460577f45bb8f5a8eca01fa6cde43ef2ab943b527991f54b735c41"}, - {file = "fonttools-4.55.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:acf1e80cf96c2fbc79e46f669d8713a9a79faaebcc68e31a9fbe600cf8027992"}, - {file = "fonttools-4.55.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e88a0329f7f88a210f09f79c088fb64f8032fc3ab65e2390a40b7d3a11773026"}, - {file = "fonttools-4.55.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03105b42259a8a94b2f0cbf1bee45f7a8a34e7b26c946a8fb89b4967e44091a8"}, - {file = "fonttools-4.55.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9af3577e821649879ab5774ad0e060af34816af556c77c6d3820345d12bf415e"}, - {file = "fonttools-4.55.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:34bd5de3d0ad085359b79a96575cd6bd1bc2976320ef24a2aa152ead36dbf656"}, - {file = "fonttools-4.55.1-cp311-cp311-win32.whl", hash = "sha256:5da92c4b637f0155a41f345fa81143c8e17425260fcb21521cb2ad4d2cea2a95"}, - {file = "fonttools-4.55.1-cp311-cp311-win_amd64.whl", hash = "sha256:f70234253d15f844e6da1178f019a931f03181463ce0c7b19648b8c370527b07"}, - {file = "fonttools-4.55.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9c372e527d58ba64b695f15f8014e97bc8826cf64d3380fc89b4196edd3c0fa8"}, - {file = "fonttools-4.55.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:845a967d3bef3245ba81fb5582dc731f6c2c8417fa211f1068c56893504bc000"}, - {file = "fonttools-4.55.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f03be82bcd4ba4418adf10e6165743f824bb09d6594c2743d7f93ea50968805b"}, - {file = "fonttools-4.55.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c42e935cf146f826f556d977660dac88f2fa3fb2efa27d5636c0b89a60c16edf"}, - {file = "fonttools-4.55.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:96328bf91e05621d8e40d9f854af7a262cb0e8313e9b38e7f3a7f3c4c0caaa8b"}, - {file = "fonttools-4.55.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:291acec4d774e8cd2d8472d88c04643a77a3324a15247951bd6cfc969799b69e"}, - {file = "fonttools-4.55.1-cp312-cp312-win32.whl", hash = "sha256:6d768d6632809aec1c3fa8f195b173386d85602334701a6894a601a4d3c80368"}, - {file = "fonttools-4.55.1-cp312-cp312-win_amd64.whl", hash = "sha256:2a3850afdb0be1f79a1e95340a2059226511675c5b68098d4e49bfbeb48a8aab"}, - {file = "fonttools-4.55.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:0c88d427eaf8bd8497b9051f56e0f5f9fb96a311aa7c72cda35e03e18d59cd16"}, - {file = "fonttools-4.55.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f062c95a725a79fd908fe8407b6ad63e230e1c7d6dece2d5d6ecaf843d6927f6"}, - {file = "fonttools-4.55.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f298c5324c45cad073475146bf560f4110ce2dc2488ff12231a343ec489f77bc"}, - {file = "fonttools-4.55.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f06dbb71344ffd85a6cb7e27970a178952f0bdd8d319ed938e64ba4bcc41700"}, - {file = "fonttools-4.55.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4c46b3525166976f5855b1f039b02433dc51eb635fb54d6a111e0c5d6e6cdc4c"}, - {file = "fonttools-4.55.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:af46f52a21e086a2f89b87bd941c9f0f91e5f769e1a5eb3b37c912228814d3e5"}, - {file = "fonttools-4.55.1-cp313-cp313-win32.whl", hash = "sha256:cd7f36335c5725a3fd724cc667c10c3f5254e779bdc5bffefebb33cf5a75ecb1"}, - {file = "fonttools-4.55.1-cp313-cp313-win_amd64.whl", hash = "sha256:5d6394897710ccac7f74df48492d7f02b9586ff0588c66a2c218844e90534b22"}, - {file = "fonttools-4.55.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:52c4f4b383c56e1a4fe8dab1b63c2269ba9eab0695d2d8e033fa037e61e6f1ef"}, - {file = "fonttools-4.55.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d83892dafdbd62b56545c77b6bd4fa49eef6ec1d6b95e042ee2c930503d1831e"}, - {file = "fonttools-4.55.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:604d5bf16f811fcaaaec2dde139f7ce958462487565edcd54b6fadacb2942083"}, - {file = "fonttools-4.55.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3324b92feb5fd084923a8e89a8248afd5b9f9d81ab9517d7b07cc84403bd448"}, - {file = "fonttools-4.55.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:30f8b1ca9b919c04850678d026fc330c19acaa9e3b282fcacc09a5eb3c8d20c3"}, - {file = "fonttools-4.55.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:1835c98df2cf28c86a66d234895c87df7b9325fd079a8019c5053a389ff55d23"}, - {file = "fonttools-4.55.1-cp38-cp38-win32.whl", hash = "sha256:9f202703720a7cc0049f2ed1a2047925e264384eb5cc4d34f80200d7b17f1b6a"}, - {file = "fonttools-4.55.1-cp38-cp38-win_amd64.whl", hash = "sha256:2efff20aed0338d37c2ff58766bd67f4b9607ded61cf3d6baf1b3e25ea74e119"}, - {file = "fonttools-4.55.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3032d9bf010c395e6eca2851666cafb1f4ecde85d420188555e928ad0144326e"}, - {file = "fonttools-4.55.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0794055588c30ffe25426048e8a7c0a5271942727cd61fc939391e37f4d580d5"}, - {file = "fonttools-4.55.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13ba980e3ffd3206b8c63a365f90dc10eeec27da946d5ee5373c3a325a46d77c"}, - {file = "fonttools-4.55.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d7063babd7434a17a5e355e87de9b2306c85a5c19c7da0794be15c58aab0c39"}, - {file = "fonttools-4.55.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ed84c15144015a58ef550dd6312884c9fb31a2dbc31a6467bcdafd63be7db476"}, - {file = "fonttools-4.55.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e89419d88b0bbfdb55209e03a17afa2d20db3c2fa0d785543c9d0875668195d5"}, - {file = "fonttools-4.55.1-cp39-cp39-win32.whl", hash = "sha256:6eb781e401b93cda99356bc043ababead2a5096550984d8a4ecf3d5c9f859dc2"}, - {file = "fonttools-4.55.1-cp39-cp39-win_amd64.whl", hash = "sha256:db1031acf04523c5a51c3e1ae19c21a1c32bc5f820a477dd4659a02f9cb82002"}, - {file = "fonttools-4.55.1-py3-none-any.whl", hash = "sha256:4bcfb11f90f48b48c366dd638d773a52fca0d1b9e056dc01df766bf5835baa08"}, - {file = "fonttools-4.55.1.tar.gz", hash = "sha256:85bb2e985718b0df96afc659abfe194c171726054314b019dbbfed31581673c7"}, + {file = "fonttools-4.55.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1dcc07934a2165ccdc3a5a608db56fb3c24b609658a5b340aee4ecf3ba679dc0"}, + {file = "fonttools-4.55.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f7d66c15ba875432a2d2fb419523f5d3d347f91f48f57b8b08a2dfc3c39b8a3f"}, + {file = "fonttools-4.55.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27e4ae3592e62eba83cd2c4ccd9462dcfa603ff78e09110680a5444c6925d841"}, + {file = "fonttools-4.55.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62d65a3022c35e404d19ca14f291c89cc5890032ff04f6c17af0bd1927299674"}, + {file = "fonttools-4.55.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d342e88764fb201286d185093781bf6628bbe380a913c24adf772d901baa8276"}, + {file = "fonttools-4.55.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dd68c87a2bfe37c5b33bcda0fba39b65a353876d3b9006fde3adae31f97b3ef5"}, + {file = "fonttools-4.55.3-cp310-cp310-win32.whl", hash = "sha256:1bc7ad24ff98846282eef1cbeac05d013c2154f977a79886bb943015d2b1b261"}, + {file = "fonttools-4.55.3-cp310-cp310-win_amd64.whl", hash = "sha256:b54baf65c52952db65df39fcd4820668d0ef4766c0ccdf32879b77f7c804d5c5"}, + {file = "fonttools-4.55.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8c4491699bad88efe95772543cd49870cf756b019ad56294f6498982408ab03e"}, + {file = "fonttools-4.55.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5323a22eabddf4b24f66d26894f1229261021dacd9d29e89f7872dd8c63f0b8b"}, + {file = "fonttools-4.55.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5480673f599ad410695ca2ddef2dfefe9df779a9a5cda89503881e503c9c7d90"}, + {file = "fonttools-4.55.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da9da6d65cd7aa6b0f806556f4985bcbf603bf0c5c590e61b43aa3e5a0f822d0"}, + {file = "fonttools-4.55.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e894b5bd60d9f473bed7a8f506515549cc194de08064d829464088d23097331b"}, + {file = "fonttools-4.55.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:aee3b57643827e237ff6ec6d28d9ff9766bd8b21e08cd13bff479e13d4b14765"}, + {file = "fonttools-4.55.3-cp311-cp311-win32.whl", hash = "sha256:eb6ca911c4c17eb51853143624d8dc87cdcdf12a711fc38bf5bd21521e79715f"}, + {file = "fonttools-4.55.3-cp311-cp311-win_amd64.whl", hash = "sha256:6314bf82c54c53c71805318fcf6786d986461622dd926d92a465199ff54b1b72"}, + {file = "fonttools-4.55.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f9e736f60f4911061235603a6119e72053073a12c6d7904011df2d8fad2c0e35"}, + {file = "fonttools-4.55.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7a8aa2c5e5b8b3bcb2e4538d929f6589a5c6bdb84fd16e2ed92649fb5454f11c"}, + {file = "fonttools-4.55.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07f8288aacf0a38d174445fc78377a97fb0b83cfe352a90c9d9c1400571963c7"}, + {file = "fonttools-4.55.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8d5e8916c0970fbc0f6f1bece0063363bb5857a7f170121a4493e31c3db3314"}, + {file = "fonttools-4.55.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ae3b6600565b2d80b7c05acb8e24d2b26ac407b27a3f2e078229721ba5698427"}, + {file = "fonttools-4.55.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:54153c49913f45065c8d9e6d0c101396725c5621c8aee744719300f79771d75a"}, + {file = "fonttools-4.55.3-cp312-cp312-win32.whl", hash = "sha256:827e95fdbbd3e51f8b459af5ea10ecb4e30af50221ca103bea68218e9615de07"}, + {file = "fonttools-4.55.3-cp312-cp312-win_amd64.whl", hash = "sha256:e6e8766eeeb2de759e862004aa11a9ea3d6f6d5ec710551a88b476192b64fd54"}, + {file = "fonttools-4.55.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a430178ad3e650e695167cb53242dae3477b35c95bef6525b074d87493c4bf29"}, + {file = "fonttools-4.55.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:529cef2ce91dc44f8e407cc567fae6e49a1786f2fefefa73a294704c415322a4"}, + {file = "fonttools-4.55.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e75f12c82127486fac2d8bfbf5bf058202f54bf4f158d367e41647b972342ca"}, + {file = "fonttools-4.55.3-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:859c358ebf41db18fb72342d3080bce67c02b39e86b9fbcf1610cca14984841b"}, + {file = "fonttools-4.55.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:546565028e244a701f73df6d8dd6be489d01617863ec0c6a42fa25bf45d43048"}, + {file = "fonttools-4.55.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:aca318b77f23523309eec4475d1fbbb00a6b133eb766a8bdc401faba91261abe"}, + {file = "fonttools-4.55.3-cp313-cp313-win32.whl", hash = "sha256:8c5ec45428edaa7022f1c949a632a6f298edc7b481312fc7dc258921e9399628"}, + {file = "fonttools-4.55.3-cp313-cp313-win_amd64.whl", hash = "sha256:11e5de1ee0d95af4ae23c1a138b184b7f06e0b6abacabf1d0db41c90b03d834b"}, + {file = "fonttools-4.55.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:caf8230f3e10f8f5d7593eb6d252a37caf58c480b19a17e250a63dad63834cf3"}, + {file = "fonttools-4.55.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b586ab5b15b6097f2fb71cafa3c98edfd0dba1ad8027229e7b1e204a58b0e09d"}, + {file = "fonttools-4.55.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8c2794ded89399cc2169c4d0bf7941247b8d5932b2659e09834adfbb01589aa"}, + {file = "fonttools-4.55.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf4fe7c124aa3f4e4c1940880156e13f2f4d98170d35c749e6b4f119a872551e"}, + {file = "fonttools-4.55.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:86721fbc389ef5cc1e2f477019e5069e8e4421e8d9576e9c26f840dbb04678de"}, + {file = "fonttools-4.55.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:89bdc5d88bdeec1b15af790810e267e8332d92561dce4f0748c2b95c9bdf3926"}, + {file = "fonttools-4.55.3-cp38-cp38-win32.whl", hash = "sha256:bc5dbb4685e51235ef487e4bd501ddfc49be5aede5e40f4cefcccabc6e60fb4b"}, + {file = "fonttools-4.55.3-cp38-cp38-win_amd64.whl", hash = "sha256:cd70de1a52a8ee2d1877b6293af8a2484ac82514f10b1c67c1c5762d38073e56"}, + {file = "fonttools-4.55.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bdcc9f04b36c6c20978d3f060e5323a43f6222accc4e7fcbef3f428e216d96af"}, + {file = "fonttools-4.55.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c3ca99e0d460eff46e033cd3992a969658c3169ffcd533e0a39c63a38beb6831"}, + {file = "fonttools-4.55.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22f38464daa6cdb7b6aebd14ab06609328fe1e9705bb0fcc7d1e69de7109ee02"}, + {file = "fonttools-4.55.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed63959d00b61959b035c7d47f9313c2c1ece090ff63afea702fe86de00dbed4"}, + {file = "fonttools-4.55.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5e8d657cd7326eeaba27de2740e847c6b39dde2f8d7cd7cc56f6aad404ddf0bd"}, + {file = "fonttools-4.55.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:fb594b5a99943042c702c550d5494bdd7577f6ef19b0bc73877c948a63184a32"}, + {file = "fonttools-4.55.3-cp39-cp39-win32.whl", hash = "sha256:dc5294a3d5c84226e3dbba1b6f61d7ad813a8c0238fceea4e09aa04848c3d851"}, + {file = "fonttools-4.55.3-cp39-cp39-win_amd64.whl", hash = "sha256:aedbeb1db64496d098e6be92b2e63b5fac4e53b1b92032dfc6988e1ea9134a4d"}, + {file = "fonttools-4.55.3-py3-none-any.whl", hash = "sha256:f412604ccbeee81b091b420272841e5ec5ef68967a9790e80bffd0e30b8e2977"}, + {file = "fonttools-4.55.3.tar.gz", hash = "sha256:3983313c2a04d6cc1fe9251f8fc647754cf49a61dac6cb1e7249ae67afaafc45"}, ] [package.extras] @@ -1324,13 +1324,13 @@ trio = ["trio (>=0.22.0,<1.0)"] [[package]] name = "httpx" -version = "0.28.0" +version = "0.28.1" description = "The next generation HTTP client." optional = true python-versions = ">=3.8" files = [ - {file = "httpx-0.28.0-py3-none-any.whl", hash = "sha256:dc0b419a0cfeb6e8b34e85167c0da2671206f5095f1baa9663d23bcfd6b535fc"}, - {file = "httpx-0.28.0.tar.gz", hash = "sha256:0858d3bab51ba7e386637f22a61d8ccddaeec5f3fe4209da3a6168dbb91573e0"}, + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, ] [package.dependencies] @@ -1385,51 +1385,6 @@ files = [ {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, ] -[[package]] -name = "importlib-metadata" -version = "8.5.0" -description = "Read metadata from Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"}, - {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"}, -] - -[package.dependencies] -zipp = ">=3.20" - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -perf = ["ipython"] -test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] -type = ["pytest-mypy"] - -[[package]] -name = "importlib-resources" -version = "6.4.5" -description = "Read resources from Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "importlib_resources-6.4.5-py3-none-any.whl", hash = "sha256:ac29d5f956f01d5e4bb63102a5a19957f1b9175e45649977264a1416783bb717"}, - {file = "importlib_resources-6.4.5.tar.gz", hash = "sha256:980862a1d16c9e147a59603677fa2aa5fd82b87f223b6cb870695bcfce830065"}, -] - -[package.dependencies] -zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["jaraco.test (>=5.4)", "pytest (>=6,!=8.1.*)", "zipp (>=3.17)"] -type = ["pytest-mypy"] - [[package]] name = "iniconfig" version = "2.0.0" @@ -1476,13 +1431,13 @@ test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0)", "pytest-asyncio [[package]] name = "ipython" -version = "8.18.1" +version = "8.30.0" description = "IPython: Productive Interactive Computing" -optional = true -python-versions = ">=3.9" +optional = false +python-versions = ">=3.10" files = [ - {file = "ipython-8.18.1-py3-none-any.whl", hash = "sha256:e8267419d72d81955ec1177f8a29aaa90ac80ad647499201119e2f05e99aa397"}, - {file = "ipython-8.18.1.tar.gz", hash = "sha256:ca6f079bb33457c66e233e4580ebfc4128855b4cf6370dddd73842a9563e8a27"}, + {file = "ipython-8.30.0-py3-none-any.whl", hash = "sha256:85ec56a7e20f6c38fce7727dcca699ae4ffc85985aa7b23635a8008f918ae321"}, + {file = "ipython-8.30.0.tar.gz", hash = "sha256:cb0a405a306d2995a5cbb9901894d240784a9f341394c6ba3f4fe8c6eb89ff6e"}, ] [package.dependencies] @@ -1491,25 +1446,26 @@ decorator = "*" exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} jedi = ">=0.16" matplotlib-inline = "*" -pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} -prompt-toolkit = ">=3.0.41,<3.1.0" +pexpect = {version = ">4.3", markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\""} +prompt_toolkit = ">=3.0.41,<3.1.0" pygments = ">=2.4.0" -stack-data = "*" -traitlets = ">=5" -typing-extensions = {version = "*", markers = "python_version < \"3.10\""} +stack_data = "*" +traitlets = ">=5.13.0" +typing_extensions = {version = ">=4.6", markers = "python_version < \"3.12\""} [package.extras] -all = ["black", "curio", "docrepr", "exceptiongroup", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio (<0.22)", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] +all = ["ipython[black,doc,kernel,matplotlib,nbconvert,nbformat,notebook,parallel,qtconsole]", "ipython[test,test-extra]"] black = ["black"] -doc = ["docrepr", "exceptiongroup", "ipykernel", "matplotlib", "pickleshare", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio (<0.22)", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"] +doc = ["docrepr", "exceptiongroup", "intersphinx_registry", "ipykernel", "ipython[test]", "matplotlib", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "sphinxcontrib-jquery", "tomli", "typing_extensions"] kernel = ["ipykernel"] +matplotlib = ["matplotlib"] nbconvert = ["nbconvert"] nbformat = ["nbformat"] notebook = ["ipywidgets", "notebook"] parallel = ["ipyparallel"] qtconsole = ["qtconsole"] -test = ["pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath"] -test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath", "trio"] +test = ["packaging", "pickleshare", "pytest", "pytest-asyncio (<0.22)", "testpath"] +test-extra = ["curio", "ipython[test]", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.23)", "pandas", "trio"] [[package]] name = "isoduration" @@ -1557,7 +1513,7 @@ numpy = "*" name = "jedi" version = "0.19.2" description = "An autocompletion tool for Python that can be used for text editors." -optional = true +optional = false python-versions = ">=3.6" files = [ {file = "jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9"}, @@ -1669,7 +1625,6 @@ files = [ ] [package.dependencies] -importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" python-dateutil = ">=2.8.2" pyzmq = ">=23.0" @@ -1737,7 +1692,6 @@ files = [ ] [package.dependencies] -importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} jupyter-server = ">=1.1.2" [[package]] @@ -1797,19 +1751,18 @@ test = ["jupyter-server (>=2.0.0)", "pytest (>=7.0)", "pytest-jupyter[server] (> [[package]] name = "jupyterlab" -version = "4.3.1" +version = "4.3.3" description = "JupyterLab computational environment" optional = true python-versions = ">=3.8" files = [ - {file = "jupyterlab-4.3.1-py3-none-any.whl", hash = "sha256:2d9a1c305bc748e277819a17a5d5e22452e533e835f4237b2f30f3b0e491e01f"}, - {file = "jupyterlab-4.3.1.tar.gz", hash = "sha256:a4a338327556443521731d82f2a6ccf926df478914ca029616621704d47c3c65"}, + {file = "jupyterlab-4.3.3-py3-none-any.whl", hash = "sha256:32a8fd30677e734ffcc3916a4758b9dab21b02015b668c60eb36f84357b7d4b1"}, + {file = "jupyterlab-4.3.3.tar.gz", hash = "sha256:76fa39e548fdac94dc1204af5956c556f54c785f70ee26aa47ea08eda4d5bbcd"}, ] [package.dependencies] async-lru = ">=1.0.0" httpx = ">=0.25.0" -importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} ipykernel = ">=6.5.0" jinja2 = ">=3.0.3" jupyter-core = "*" @@ -1818,7 +1771,7 @@ jupyter-server = ">=2.4.0,<3" jupyterlab-server = ">=2.27.1,<3" notebook-shim = ">=0.2" packaging = "*" -setuptools = ">=40.1.0" +setuptools = ">=40.8.0" tomli = {version = ">=1.2.2", markers = "python_version < \"3.11\""} tornado = ">=6.2.0" traitlets = "*" @@ -1854,7 +1807,6 @@ files = [ [package.dependencies] babel = ">=2.10" -importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} jinja2 = ">=3.0.3" json5 = ">=0.9.0" jsonschema = ">=4.18.0" @@ -2164,59 +2116,51 @@ files = [ [[package]] name = "matplotlib" -version = "3.9.3" +version = "3.10.0" description = "Python plotting package" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "matplotlib-3.9.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:41b016e3be4e740b66c79a031a0a6e145728dbc248142e751e8dab4f3188ca1d"}, - {file = "matplotlib-3.9.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e0143975fc2a6d7136c97e19c637321288371e8f09cff2564ecd73e865ea0b9"}, - {file = "matplotlib-3.9.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f459c8ee2c086455744723628264e43c884be0c7d7b45d84b8cd981310b4815"}, - {file = "matplotlib-3.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:687df7ceff57b8f070d02b4db66f75566370e7ae182a0782b6d3d21b0d6917dc"}, - {file = "matplotlib-3.9.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:edd14cf733fdc4f6e6fe3f705af97676a7e52859bf0044aa2c84e55be739241c"}, - {file = "matplotlib-3.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:1c40c244221a1adbb1256692b1133c6fb89418df27bf759a31a333e7912a4010"}, - {file = "matplotlib-3.9.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:cf2a60daf6cecff6828bc608df00dbc794380e7234d2411c0ec612811f01969d"}, - {file = "matplotlib-3.9.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:213d6dc25ce686516208d8a3e91120c6a4fdae4a3e06b8505ced5b716b50cc04"}, - {file = "matplotlib-3.9.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c52f48eb75fcc119a4fdb68ba83eb5f71656999420375df7c94cc68e0e14686e"}, - {file = "matplotlib-3.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3c93796b44fa111049b88a24105e947f03c01966b5c0cc782e2ee3887b790a3"}, - {file = "matplotlib-3.9.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cd1077b9a09b16d8c3c7075a8add5ffbfe6a69156a57e290c800ed4d435bef1d"}, - {file = "matplotlib-3.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:c96eeeb8c68b662c7747f91a385688d4b449687d29b691eff7068a4602fe6dc4"}, - {file = "matplotlib-3.9.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0a361bd5583bf0bcc08841df3c10269617ee2a36b99ac39d455a767da908bbbc"}, - {file = "matplotlib-3.9.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e14485bb1b83eeb3d55b6878f9560240981e7bbc7a8d4e1e8c38b9bd6ec8d2de"}, - {file = "matplotlib-3.9.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a8d279f78844aad213c4935c18f8292a9432d51af2d88bca99072c903948045"}, - {file = "matplotlib-3.9.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6c12514329ac0d03128cf1dcceb335f4fbf7c11da98bca68dca8dcb983153a9"}, - {file = "matplotlib-3.9.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6e9de2b390d253a508dd497e9b5579f3a851f208763ed67fdca5dc0c3ea6849c"}, - {file = "matplotlib-3.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:d796272408f8567ff7eaa00eb2856b3a00524490e47ad505b0b4ca6bb8a7411f"}, - {file = "matplotlib-3.9.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:203d18df84f5288973b2d56de63d4678cc748250026ca9e1ad8f8a0fd8a75d83"}, - {file = "matplotlib-3.9.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b651b0d3642991259109dc0351fc33ad44c624801367bb8307be9bfc35e427ad"}, - {file = "matplotlib-3.9.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:66d7b171fecf96940ce069923a08ba3df33ef542de82c2ff4fe8caa8346fa95a"}, - {file = "matplotlib-3.9.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6be0ba61f6ff2e6b68e4270fb63b6813c9e7dec3d15fc3a93f47480444fd72f0"}, - {file = "matplotlib-3.9.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d6b2e8856dec3a6db1ae51aec85c82223e834b228c1d3228aede87eee2b34f9"}, - {file = "matplotlib-3.9.3-cp313-cp313-win_amd64.whl", hash = "sha256:90a85a004fefed9e583597478420bf904bb1a065b0b0ee5b9d8d31b04b0f3f70"}, - {file = "matplotlib-3.9.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3119b2f16de7f7b9212ba76d8fe6a0e9f90b27a1e04683cd89833a991682f639"}, - {file = "matplotlib-3.9.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:87ad73763d93add1b6c1f9fcd33af662fd62ed70e620c52fcb79f3ac427cf3a6"}, - {file = "matplotlib-3.9.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:026bdf3137ab6022c866efa4813b6bbeddc2ed4c9e7e02f0e323a7bca380dfa0"}, - {file = "matplotlib-3.9.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:760a5e89ebbb172989e8273024a1024b0f084510b9105261b3b00c15e9c9f006"}, - {file = "matplotlib-3.9.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a42b9dc42de2cfe357efa27d9c50c7833fc5ab9b2eb7252ccd5d5f836a84e1e4"}, - {file = "matplotlib-3.9.3-cp313-cp313t-win_amd64.whl", hash = "sha256:e0fcb7da73fbf67b5f4bdaa57d85bb585a4e913d4a10f3e15b32baea56a67f0a"}, - {file = "matplotlib-3.9.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:031b7f5b8e595cc07def77ec5b58464e9bb67dc5760be5d6f26d9da24892481d"}, - {file = "matplotlib-3.9.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9fa6e193c14d6944e0685cdb527cb6b38b0e4a518043e7212f214113af7391da"}, - {file = "matplotlib-3.9.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e6eefae6effa0c35bbbc18c25ee6e0b1da44d2359c3cd526eb0c9e703cf055d"}, - {file = "matplotlib-3.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10d3e5c7a99bd28afb957e1ae661323b0800d75b419f24d041ed1cc5d844a764"}, - {file = "matplotlib-3.9.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:816a966d5d376bf24c92af8f379e78e67278833e4c7cbc9fa41872eec629a060"}, - {file = "matplotlib-3.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fb0b37c896172899a4a93d9442ffdc6f870165f59e05ce2e07c6fded1c15749"}, - {file = "matplotlib-3.9.3-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f2a4ea08e6876206d511365b0bc234edc813d90b930be72c3011bbd7898796f"}, - {file = "matplotlib-3.9.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:9b081dac96ab19c54fd8558fac17c9d2c9cb5cc4656e7ed3261ddc927ba3e2c5"}, - {file = "matplotlib-3.9.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a0a63cb8404d1d1f94968ef35738900038137dab8af836b6c21bb6f03d75465"}, - {file = "matplotlib-3.9.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:896774766fd6be4571a43bc2fcbcb1dcca0807e53cab4a5bf88c4aa861a08e12"}, - {file = "matplotlib-3.9.3.tar.gz", hash = "sha256:cd5dbbc8e25cad5f706845c4d100e2c8b34691b412b93717ce38d8ae803bcfa5"}, + {file = "matplotlib-3.10.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2c5829a5a1dd5a71f0e31e6e8bb449bc0ee9dbfb05ad28fc0c6b55101b3a4be6"}, + {file = "matplotlib-3.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2a43cbefe22d653ab34bb55d42384ed30f611bcbdea1f8d7f431011a2e1c62e"}, + {file = "matplotlib-3.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:607b16c8a73943df110f99ee2e940b8a1cbf9714b65307c040d422558397dac5"}, + {file = "matplotlib-3.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01d2b19f13aeec2e759414d3bfe19ddfb16b13a1250add08d46d5ff6f9be83c6"}, + {file = "matplotlib-3.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e6c6461e1fc63df30bf6f80f0b93f5b6784299f721bc28530477acd51bfc3d1"}, + {file = "matplotlib-3.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:994c07b9d9fe8d25951e3202a68c17900679274dadfc1248738dcfa1bd40d7f3"}, + {file = "matplotlib-3.10.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:fd44fc75522f58612ec4a33958a7e5552562b7705b42ef1b4f8c0818e304a363"}, + {file = "matplotlib-3.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c58a9622d5dbeb668f407f35f4e6bfac34bb9ecdcc81680c04d0258169747997"}, + {file = "matplotlib-3.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:845d96568ec873be63f25fa80e9e7fae4be854a66a7e2f0c8ccc99e94a8bd4ef"}, + {file = "matplotlib-3.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5439f4c5a3e2e8eab18e2f8c3ef929772fd5641876db71f08127eed95ab64683"}, + {file = "matplotlib-3.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4673ff67a36152c48ddeaf1135e74ce0d4bce1bbf836ae40ed39c29edf7e2765"}, + {file = "matplotlib-3.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:7e8632baebb058555ac0cde75db885c61f1212e47723d63921879806b40bec6a"}, + {file = "matplotlib-3.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4659665bc7c9b58f8c00317c3c2a299f7f258eeae5a5d56b4c64226fca2f7c59"}, + {file = "matplotlib-3.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d44cb942af1693cced2604c33a9abcef6205601c445f6d0dc531d813af8a2f5a"}, + {file = "matplotlib-3.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a994f29e968ca002b50982b27168addfd65f0105610b6be7fa515ca4b5307c95"}, + {file = "matplotlib-3.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b0558bae37f154fffda54d779a592bc97ca8b4701f1c710055b609a3bac44c8"}, + {file = "matplotlib-3.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:503feb23bd8c8acc75541548a1d709c059b7184cde26314896e10a9f14df5f12"}, + {file = "matplotlib-3.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:c40ba2eb08b3f5de88152c2333c58cee7edcead0a2a0d60fcafa116b17117adc"}, + {file = "matplotlib-3.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96f2886f5c1e466f21cc41b70c5a0cd47bfa0015eb2d5793c88ebce658600e25"}, + {file = "matplotlib-3.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:12eaf48463b472c3c0f8dbacdbf906e573013df81a0ab82f0616ea4b11281908"}, + {file = "matplotlib-3.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fbbabc82fde51391c4da5006f965e36d86d95f6ee83fb594b279564a4c5d0d2"}, + {file = "matplotlib-3.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad2e15300530c1a94c63cfa546e3b7864bd18ea2901317bae8bbf06a5ade6dcf"}, + {file = "matplotlib-3.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3547d153d70233a8496859097ef0312212e2689cdf8d7ed764441c77604095ae"}, + {file = "matplotlib-3.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:c55b20591ced744aa04e8c3e4b7543ea4d650b6c3c4b208c08a05b4010e8b442"}, + {file = "matplotlib-3.10.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9ade1003376731a971e398cc4ef38bb83ee8caf0aee46ac6daa4b0506db1fd06"}, + {file = "matplotlib-3.10.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95b710fea129c76d30be72c3b38f330269363fbc6e570a5dd43580487380b5ff"}, + {file = "matplotlib-3.10.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cdbaf909887373c3e094b0318d7ff230b2ad9dcb64da7ade654182872ab2593"}, + {file = "matplotlib-3.10.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d907fddb39f923d011875452ff1eca29a9e7f21722b873e90db32e5d8ddff12e"}, + {file = "matplotlib-3.10.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3b427392354d10975c1d0f4ee18aa5844640b512d5311ef32efd4dd7db106ede"}, + {file = "matplotlib-3.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5fd41b0ec7ee45cd960a8e71aea7c946a28a0b8a4dcee47d2856b2af051f334c"}, + {file = "matplotlib-3.10.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:81713dd0d103b379de4516b861d964b1d789a144103277769238c732229d7f03"}, + {file = "matplotlib-3.10.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:359f87baedb1f836ce307f0e850d12bb5f1936f70d035561f90d41d305fdacea"}, + {file = "matplotlib-3.10.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae80dc3a4add4665cf2faa90138384a7ffe2a4e37c58d83e115b54287c4f06ef"}, + {file = "matplotlib-3.10.0.tar.gz", hash = "sha256:b886d02a581b96704c9d1ffe55709e49b4d2d52709ccebc4be42db856e511278"}, ] [package.dependencies] contourpy = ">=1.0.1" cycler = ">=0.10" fonttools = ">=4.22.0" -importlib-resources = {version = ">=3.2.0", markers = "python_version < \"3.10\""} kiwisolver = ">=1.3.1" numpy = ">=1.23" packaging = ">=20.0" @@ -2225,13 +2169,13 @@ pyparsing = ">=2.3.1" python-dateutil = ">=2.7" [package.extras] -dev = ["meson-python (>=0.13.1)", "numpy (>=1.25)", "pybind11 (>=2.6,!=2.13.3)", "setuptools (>=64)", "setuptools_scm (>=7)"] +dev = ["meson-python (>=0.13.1,<0.17.0)", "pybind11 (>=2.13.2,!=2.13.3)", "setuptools (>=64)", "setuptools_scm (>=7)"] [[package]] name = "matplotlib-inline" version = "0.1.7" description = "Inline Matplotlib backend for Jupyter" -optional = true +optional = false python-versions = ">=3.8" files = [ {file = "matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca"}, @@ -2443,7 +2387,6 @@ files = [ beautifulsoup4 = "*" bleach = "!=5.0.0" defusedxml = "*" -importlib-metadata = {version = ">=3.6", markers = "python_version < \"3.10\""} jinja2 = ">=3.0" jupyter-core = ">=4.7" jupyterlab-pygments = "*" @@ -2500,20 +2443,21 @@ files = [ [[package]] name = "networkx" -version = "3.2.1" +version = "3.4.2" description = "Python package for creating and manipulating graphs and networks" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "networkx-3.2.1-py3-none-any.whl", hash = "sha256:f18c69adc97877c42332c170849c96cefa91881c99a7cb3e95b7c659ebdc1ec2"}, - {file = "networkx-3.2.1.tar.gz", hash = "sha256:9f1bb5cf3409bf324e0a722c20bdb4c20ee39bf1c30ce8ae499c8502b0b5e0c6"}, + {file = "networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f"}, + {file = "networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1"}, ] [package.extras] -default = ["matplotlib (>=3.5)", "numpy (>=1.22)", "pandas (>=1.4)", "scipy (>=1.9,!=1.11.0,!=1.11.1)"] -developer = ["changelist (==0.4)", "mypy (>=1.1)", "pre-commit (>=3.2)", "rtoml"] -doc = ["nb2plots (>=0.7)", "nbconvert (<7.9)", "numpydoc (>=1.6)", "pillow (>=9.4)", "pydata-sphinx-theme (>=0.14)", "sphinx (>=7)", "sphinx-gallery (>=0.14)", "texext (>=0.6.7)"] -extra = ["lxml (>=4.6)", "pydot (>=1.4.2)", "pygraphviz (>=1.11)", "sympy (>=1.10)"] +default = ["matplotlib (>=3.7)", "numpy (>=1.24)", "pandas (>=2.0)", "scipy (>=1.10,!=1.11.0,!=1.11.1)"] +developer = ["changelist (==0.5)", "mypy (>=1.1)", "pre-commit (>=3.2)", "rtoml"] +doc = ["intersphinx-registry", "myst-nb (>=1.1)", "numpydoc (>=1.8.0)", "pillow (>=9.4)", "pydata-sphinx-theme (>=0.15)", "sphinx (>=7.3)", "sphinx-gallery (>=0.16)", "texext (>=0.6.7)"] +example = ["cairocffi (>=1.7)", "contextily (>=1.6)", "igraph (>=0.11)", "momepy (>=0.7.2)", "osmnx (>=1.9)", "scikit-learn (>=1.5)", "seaborn (>=0.13)"] +extra = ["lxml (>=4.6)", "pydot (>=3.0.1)", "pygraphviz (>=1.14)", "sympy (>=1.10)"] test = ["pytest (>=7.2)", "pytest-cov (>=4.0)"] [[package]] @@ -2529,26 +2473,26 @@ files = [ [[package]] name = "notebook" -version = "7.0.7" +version = "7.3.1" description = "Jupyter Notebook - A web-based notebook environment for interactive computing" optional = true python-versions = ">=3.8" files = [ - {file = "notebook-7.0.7-py3-none-any.whl", hash = "sha256:289b606d7e173f75a18beb1406ef411b43f97f7a9c55ba03efa3622905a62346"}, - {file = "notebook-7.0.7.tar.gz", hash = "sha256:3bcff00c17b3ac142ef5f436d50637d936b274cfa0b41f6ac0175363de9b4e09"}, + {file = "notebook-7.3.1-py3-none-any.whl", hash = "sha256:212e1486b2230fe22279043f33c7db5cf9a01d29feb063a85cb139747b7c9483"}, + {file = "notebook-7.3.1.tar.gz", hash = "sha256:84381c2a82d867517fd25b86e986dae1fe113a70b98f03edff9b94e499fec8fa"}, ] [package.dependencies] jupyter-server = ">=2.4.0,<3" -jupyterlab = ">=4.0.2,<5" -jupyterlab-server = ">=2.22.1,<3" +jupyterlab = ">=4.3.2,<4.4" +jupyterlab-server = ">=2.27.1,<3" notebook-shim = ">=0.2,<0.3" tornado = ">=6.2.0" [package.extras] dev = ["hatch", "pre-commit"] docs = ["myst-parser", "nbsphinx", "pydata-sphinx-theme", "sphinx (>=1.3.6)", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] -test = ["importlib-resources (>=5.0)", "ipykernel", "jupyter-server[test] (>=2.4.0,<3)", "jupyterlab-server[test] (>=2.22.1,<3)", "nbval", "pytest (>=7.0)", "pytest-console-scripts", "pytest-timeout", "pytest-tornasync", "requests"] +test = ["importlib-resources (>=5.0)", "ipykernel", "jupyter-server[test] (>=2.4.0,<3)", "jupyterlab-server[test] (>=2.27.1,<3)", "nbval", "pytest (>=7.0)", "pytest-console-scripts", "pytest-timeout", "pytest-tornasync", "requests"] [[package]] name = "notebook-shim" @@ -2569,120 +2513,66 @@ test = ["pytest", "pytest-console-scripts", "pytest-jupyter", "pytest-tornasync" [[package]] name = "numpy" -version = "2.0.2" -description = "Fundamental package for array computing in Python" -optional = false -python-versions = ">=3.9" -files = [ - {file = "numpy-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece"}, - {file = "numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04"}, - {file = "numpy-2.0.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8c5713284ce4e282544c68d1c3b2c7161d38c256d2eefc93c1d683cf47683e66"}, - {file = "numpy-2.0.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:becfae3ddd30736fe1889a37f1f580e245ba79a5855bff5f2a29cb3ccc22dd7b"}, - {file = "numpy-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2da5960c3cf0df7eafefd806d4e612c5e19358de82cb3c343631188991566ccd"}, - {file = "numpy-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:496f71341824ed9f3d2fd36cf3ac57ae2e0165c143b55c3a035ee219413f3318"}, - {file = "numpy-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a61ec659f68ae254e4d237816e33171497e978140353c0c2038d46e63282d0c8"}, - {file = "numpy-2.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d731a1c6116ba289c1e9ee714b08a8ff882944d4ad631fd411106a30f083c326"}, - {file = "numpy-2.0.2-cp310-cp310-win32.whl", hash = "sha256:984d96121c9f9616cd33fbd0618b7f08e0cfc9600a7ee1d6fd9b239186d19d97"}, - {file = "numpy-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:c7b0be4ef08607dd04da4092faee0b86607f111d5ae68036f16cc787e250a131"}, - {file = "numpy-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:49ca4decb342d66018b01932139c0961a8f9ddc7589611158cb3c27cbcf76448"}, - {file = "numpy-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11a76c372d1d37437857280aa142086476136a8c0f373b2e648ab2c8f18fb195"}, - {file = "numpy-2.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:807ec44583fd708a21d4a11d94aedf2f4f3c3719035c76a2bbe1fe8e217bdc57"}, - {file = "numpy-2.0.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8cafab480740e22f8d833acefed5cc87ce276f4ece12fdaa2e8903db2f82897a"}, - {file = "numpy-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a15f476a45e6e5a3a79d8a14e62161d27ad897381fecfa4a09ed5322f2085669"}, - {file = "numpy-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13e689d772146140a252c3a28501da66dfecd77490b498b168b501835041f951"}, - {file = "numpy-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9ea91dfb7c3d1c56a0e55657c0afb38cf1eeae4544c208dc465c3c9f3a7c09f9"}, - {file = "numpy-2.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c1c9307701fec8f3f7a1e6711f9089c06e6284b3afbbcd259f7791282d660a15"}, - {file = "numpy-2.0.2-cp311-cp311-win32.whl", hash = "sha256:a392a68bd329eafac5817e5aefeb39038c48b671afd242710b451e76090e81f4"}, - {file = "numpy-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:286cd40ce2b7d652a6f22efdfc6d1edf879440e53e76a75955bc0c826c7e64dc"}, - {file = "numpy-2.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:df55d490dea7934f330006d0f81e8551ba6010a5bf035a249ef61a94f21c500b"}, - {file = "numpy-2.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8df823f570d9adf0978347d1f926b2a867d5608f434a7cff7f7908c6570dcf5e"}, - {file = "numpy-2.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9a92ae5c14811e390f3767053ff54eaee3bf84576d99a2456391401323f4ec2c"}, - {file = "numpy-2.0.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a842d573724391493a97a62ebbb8e731f8a5dcc5d285dfc99141ca15a3302d0c"}, - {file = "numpy-2.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05e238064fc0610c840d1cf6a13bf63d7e391717d247f1bf0318172e759e692"}, - {file = "numpy-2.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0123ffdaa88fa4ab64835dcbde75dcdf89c453c922f18dced6e27c90d1d0ec5a"}, - {file = "numpy-2.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:96a55f64139912d61de9137f11bf39a55ec8faec288c75a54f93dfd39f7eb40c"}, - {file = "numpy-2.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec9852fb39354b5a45a80bdab5ac02dd02b15f44b3804e9f00c556bf24b4bded"}, - {file = "numpy-2.0.2-cp312-cp312-win32.whl", hash = "sha256:671bec6496f83202ed2d3c8fdc486a8fc86942f2e69ff0e986140339a63bcbe5"}, - {file = "numpy-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a"}, - {file = "numpy-2.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9059e10581ce4093f735ed23f3b9d283b9d517ff46009ddd485f1747eb22653c"}, - {file = "numpy-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:423e89b23490805d2a5a96fe40ec507407b8ee786d66f7328be214f9679df6dd"}, - {file = "numpy-2.0.2-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:2b2955fa6f11907cf7a70dab0d0755159bca87755e831e47932367fc8f2f2d0b"}, - {file = "numpy-2.0.2-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:97032a27bd9d8988b9a97a8c4d2c9f2c15a81f61e2f21404d7e8ef00cb5be729"}, - {file = "numpy-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e795a8be3ddbac43274f18588329c72939870a16cae810c2b73461c40718ab1"}, - {file = "numpy-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b258c385842546006213344c50655ff1555a9338e2e5e02a0756dc3e803dd"}, - {file = "numpy-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fec9451a7789926bcf7c2b8d187292c9f93ea30284802a0ab3f5be8ab36865d"}, - {file = "numpy-2.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9189427407d88ff25ecf8f12469d4d39d35bee1db5d39fc5c168c6f088a6956d"}, - {file = "numpy-2.0.2-cp39-cp39-win32.whl", hash = "sha256:905d16e0c60200656500c95b6b8dca5d109e23cb24abc701d41c02d74c6b3afa"}, - {file = "numpy-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:a3f4ab0caa7f053f6797fcd4e1e25caee367db3112ef2b6ef82d749530768c73"}, - {file = "numpy-2.0.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7f0a0c6f12e07fa94133c8a67404322845220c06a9e80e85999afe727f7438b8"}, - {file = "numpy-2.0.2-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:312950fdd060354350ed123c0e25a71327d3711584beaef30cdaa93320c392d4"}, - {file = "numpy-2.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26df23238872200f63518dd2aa984cfca675d82469535dc7162dc2ee52d9dd5c"}, - {file = "numpy-2.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a46288ec55ebbd58947d31d72be2c63cbf839f0a63b49cb755022310792a3385"}, - {file = "numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78"}, -] - -[[package]] -name = "numpy" -version = "2.1.3" +version = "2.2.0" description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.10" files = [ - {file = "numpy-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c894b4305373b9c5576d7a12b473702afdf48ce5369c074ba304cc5ad8730dff"}, - {file = "numpy-2.1.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b47fbb433d3260adcd51eb54f92a2ffbc90a4595f8970ee00e064c644ac788f5"}, - {file = "numpy-2.1.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:825656d0743699c529c5943554d223c021ff0494ff1442152ce887ef4f7561a1"}, - {file = "numpy-2.1.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:6a4825252fcc430a182ac4dee5a505053d262c807f8a924603d411f6718b88fd"}, - {file = "numpy-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e711e02f49e176a01d0349d82cb5f05ba4db7d5e7e0defd026328e5cfb3226d3"}, - {file = "numpy-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78574ac2d1a4a02421f25da9559850d59457bac82f2b8d7a44fe83a64f770098"}, - {file = "numpy-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c7662f0e3673fe4e832fe07b65c50342ea27d989f92c80355658c7f888fcc83c"}, - {file = "numpy-2.1.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fa2d1337dc61c8dc417fbccf20f6d1e139896a30721b7f1e832b2bb6ef4eb6c4"}, - {file = "numpy-2.1.3-cp310-cp310-win32.whl", hash = "sha256:72dcc4a35a8515d83e76b58fdf8113a5c969ccd505c8a946759b24e3182d1f23"}, - {file = "numpy-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:ecc76a9ba2911d8d37ac01de72834d8849e55473457558e12995f4cd53e778e0"}, - {file = "numpy-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4d1167c53b93f1f5d8a139a742b3c6f4d429b54e74e6b57d0eff40045187b15d"}, - {file = "numpy-2.1.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c80e4a09b3d95b4e1cac08643f1152fa71a0a821a2d4277334c88d54b2219a41"}, - {file = "numpy-2.1.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:576a1c1d25e9e02ed7fa5477f30a127fe56debd53b8d2c89d5578f9857d03ca9"}, - {file = "numpy-2.1.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:973faafebaae4c0aaa1a1ca1ce02434554d67e628b8d805e61f874b84e136b09"}, - {file = "numpy-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:762479be47a4863e261a840e8e01608d124ee1361e48b96916f38b119cfda04a"}, - {file = "numpy-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc6f24b3d1ecc1eebfbf5d6051faa49af40b03be1aaa781ebdadcbc090b4539b"}, - {file = "numpy-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:17ee83a1f4fef3c94d16dc1802b998668b5419362c8a4f4e8a491de1b41cc3ee"}, - {file = "numpy-2.1.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15cb89f39fa6d0bdfb600ea24b250e5f1a3df23f901f51c8debaa6a5d122b2f0"}, - {file = "numpy-2.1.3-cp311-cp311-win32.whl", hash = "sha256:d9beb777a78c331580705326d2367488d5bc473b49a9bc3036c154832520aca9"}, - {file = "numpy-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:d89dd2b6da69c4fff5e39c28a382199ddedc3a5be5390115608345dec660b9e2"}, - {file = "numpy-2.1.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f55ba01150f52b1027829b50d70ef1dafd9821ea82905b63936668403c3b471e"}, - {file = "numpy-2.1.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13138eadd4f4da03074851a698ffa7e405f41a0845a6b1ad135b81596e4e9958"}, - {file = "numpy-2.1.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a6b46587b14b888e95e4a24d7b13ae91fa22386c199ee7b418f449032b2fa3b8"}, - {file = "numpy-2.1.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:0fa14563cc46422e99daef53d725d0c326e99e468a9320a240affffe87852564"}, - {file = "numpy-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8637dcd2caa676e475503d1f8fdb327bc495554e10838019651b76d17b98e512"}, - {file = "numpy-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2312b2aa89e1f43ecea6da6ea9a810d06aae08321609d8dc0d0eda6d946a541b"}, - {file = "numpy-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a38c19106902bb19351b83802531fea19dee18e5b37b36454f27f11ff956f7fc"}, - {file = "numpy-2.1.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:02135ade8b8a84011cbb67dc44e07c58f28575cf9ecf8ab304e51c05528c19f0"}, - {file = "numpy-2.1.3-cp312-cp312-win32.whl", hash = "sha256:e6988e90fcf617da2b5c78902fe8e668361b43b4fe26dbf2d7b0f8034d4cafb9"}, - {file = "numpy-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:0d30c543f02e84e92c4b1f415b7c6b5326cbe45ee7882b6b77db7195fb971e3a"}, - {file = "numpy-2.1.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96fe52fcdb9345b7cd82ecd34547fca4321f7656d500eca497eb7ea5a926692f"}, - {file = "numpy-2.1.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f653490b33e9c3a4c1c01d41bc2aef08f9475af51146e4a7710c450cf9761598"}, - {file = "numpy-2.1.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:dc258a761a16daa791081d026f0ed4399b582712e6fc887a95af09df10c5ca57"}, - {file = "numpy-2.1.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:016d0f6f5e77b0f0d45d77387ffa4bb89816b57c835580c3ce8e099ef830befe"}, - {file = "numpy-2.1.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c181ba05ce8299c7aa3125c27b9c2167bca4a4445b7ce73d5febc411ca692e43"}, - {file = "numpy-2.1.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5641516794ca9e5f8a4d17bb45446998c6554704d888f86df9b200e66bdcce56"}, - {file = "numpy-2.1.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ea4dedd6e394a9c180b33c2c872b92f7ce0f8e7ad93e9585312b0c5a04777a4a"}, - {file = "numpy-2.1.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0df3635b9c8ef48bd3be5f862cf71b0a4716fa0e702155c45067c6b711ddcef"}, - {file = "numpy-2.1.3-cp313-cp313-win32.whl", hash = "sha256:50ca6aba6e163363f132b5c101ba078b8cbd3fa92c7865fd7d4d62d9779ac29f"}, - {file = "numpy-2.1.3-cp313-cp313-win_amd64.whl", hash = "sha256:747641635d3d44bcb380d950679462fae44f54b131be347d5ec2bce47d3df9ed"}, - {file = "numpy-2.1.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:996bb9399059c5b82f76b53ff8bb686069c05acc94656bb259b1d63d04a9506f"}, - {file = "numpy-2.1.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:45966d859916ad02b779706bb43b954281db43e185015df6eb3323120188f9e4"}, - {file = "numpy-2.1.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:baed7e8d7481bfe0874b566850cb0b85243e982388b7b23348c6db2ee2b2ae8e"}, - {file = "numpy-2.1.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a9f7f672a3388133335589cfca93ed468509cb7b93ba3105fce780d04a6576a0"}, - {file = "numpy-2.1.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7aac50327da5d208db2eec22eb11e491e3fe13d22653dce51b0f4109101b408"}, - {file = "numpy-2.1.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4394bc0dbd074b7f9b52024832d16e019decebf86caf909d94f6b3f77a8ee3b6"}, - {file = "numpy-2.1.3-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:50d18c4358a0a8a53f12a8ba9d772ab2d460321e6a93d6064fc22443d189853f"}, - {file = "numpy-2.1.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:14e253bd43fc6b37af4921b10f6add6925878a42a0c5fe83daee390bca80bc17"}, - {file = "numpy-2.1.3-cp313-cp313t-win32.whl", hash = "sha256:08788d27a5fd867a663f6fc753fd7c3ad7e92747efc73c53bca2f19f8bc06f48"}, - {file = "numpy-2.1.3-cp313-cp313t-win_amd64.whl", hash = "sha256:2564fbdf2b99b3f815f2107c1bbc93e2de8ee655a69c261363a1172a79a257d4"}, - {file = "numpy-2.1.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4f2015dfe437dfebbfce7c85c7b53d81ba49e71ba7eadbf1df40c915af75979f"}, - {file = "numpy-2.1.3-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:3522b0dfe983a575e6a9ab3a4a4dfe156c3e428468ff08ce582b9bb6bd1d71d4"}, - {file = "numpy-2.1.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c006b607a865b07cd981ccb218a04fc86b600411d83d6fc261357f1c0966755d"}, - {file = "numpy-2.1.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e14e26956e6f1696070788252dcdff11b4aca4c3e8bd166e0df1bb8f315a67cb"}, - {file = "numpy-2.1.3.tar.gz", hash = "sha256:aa08e04e08aaf974d4458def539dece0d28146d866a39da5639596f4921fd761"}, + {file = "numpy-2.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1e25507d85da11ff5066269d0bd25d06e0a0f2e908415534f3e603d2a78e4ffa"}, + {file = "numpy-2.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a62eb442011776e4036af5c8b1a00b706c5bc02dc15eb5344b0c750428c94219"}, + {file = "numpy-2.2.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:b606b1aaf802e6468c2608c65ff7ece53eae1a6874b3765f69b8ceb20c5fa78e"}, + {file = "numpy-2.2.0-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:36b2b43146f646642b425dd2027730f99bac962618ec2052932157e213a040e9"}, + {file = "numpy-2.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fe8f3583e0607ad4e43a954e35c1748b553bfe9fdac8635c02058023277d1b3"}, + {file = "numpy-2.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:122fd2fcfafdefc889c64ad99c228d5a1f9692c3a83f56c292618a59aa60ae83"}, + {file = "numpy-2.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3f2f5cddeaa4424a0a118924b988746db6ffa8565e5829b1841a8a3bd73eb59a"}, + {file = "numpy-2.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7fe4bb0695fe986a9e4deec3b6857003b4cfe5c5e4aac0b95f6a658c14635e31"}, + {file = "numpy-2.2.0-cp310-cp310-win32.whl", hash = "sha256:b30042fe92dbd79f1ba7f6898fada10bdaad1847c44f2dff9a16147e00a93661"}, + {file = "numpy-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:54dc1d6d66f8d37843ed281773c7174f03bf7ad826523f73435deb88ba60d2d4"}, + {file = "numpy-2.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9874bc2ff574c40ab7a5cbb7464bf9b045d617e36754a7bc93f933d52bd9ffc6"}, + {file = "numpy-2.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0da8495970f6b101ddd0c38ace92edea30e7e12b9a926b57f5fabb1ecc25bb90"}, + {file = "numpy-2.2.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:0557eebc699c1c34cccdd8c3778c9294e8196df27d713706895edc6f57d29608"}, + {file = "numpy-2.2.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:3579eaeb5e07f3ded59298ce22b65f877a86ba8e9fe701f5576c99bb17c283da"}, + {file = "numpy-2.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40deb10198bbaa531509aad0cd2f9fadb26c8b94070831e2208e7df543562b74"}, + {file = "numpy-2.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2aed8fcf8abc3020d6a9ccb31dbc9e7d7819c56a348cc88fd44be269b37427e"}, + {file = "numpy-2.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a222d764352c773aa5ebde02dd84dba3279c81c6db2e482d62a3fa54e5ece69b"}, + {file = "numpy-2.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4e58666988605e251d42c2818c7d3d8991555381be26399303053b58a5bbf30d"}, + {file = "numpy-2.2.0-cp311-cp311-win32.whl", hash = "sha256:4723a50e1523e1de4fccd1b9a6dcea750c2102461e9a02b2ac55ffeae09a4410"}, + {file = "numpy-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:16757cf28621e43e252c560d25b15f18a2f11da94fea344bf26c599b9cf54b73"}, + {file = "numpy-2.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cff210198bb4cae3f3c100444c5eaa573a823f05c253e7188e1362a5555235b3"}, + {file = "numpy-2.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58b92a5828bd4d9aa0952492b7de803135038de47343b2aa3cc23f3b71a3dc4e"}, + {file = "numpy-2.2.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:ebe5e59545401fbb1b24da76f006ab19734ae71e703cdb4a8b347e84a0cece67"}, + {file = "numpy-2.2.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e2b8cd48a9942ed3f85b95ca4105c45758438c7ed28fff1e4ce3e57c3b589d8e"}, + {file = "numpy-2.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57fcc997ffc0bef234b8875a54d4058afa92b0b0c4223fc1f62f24b3b5e86038"}, + {file = "numpy-2.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85ad7d11b309bd132d74397fcf2920933c9d1dc865487128f5c03d580f2c3d03"}, + {file = "numpy-2.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cb24cca1968b21355cc6f3da1a20cd1cebd8a023e3c5b09b432444617949085a"}, + {file = "numpy-2.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0798b138c291d792f8ea40fe3768610f3c7dd2574389e37c3f26573757c8f7ef"}, + {file = "numpy-2.2.0-cp312-cp312-win32.whl", hash = "sha256:afe8fb968743d40435c3827632fd36c5fbde633b0423da7692e426529b1759b1"}, + {file = "numpy-2.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:3a4199f519e57d517ebd48cb76b36c82da0360781c6a0353e64c0cac30ecaad3"}, + {file = "numpy-2.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f8c8b141ef9699ae777c6278b52c706b653bf15d135d302754f6b2e90eb30367"}, + {file = "numpy-2.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f0986e917aca18f7a567b812ef7ca9391288e2acb7a4308aa9d265bd724bdae"}, + {file = "numpy-2.2.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:1c92113619f7b272838b8d6702a7f8ebe5edea0df48166c47929611d0b4dea69"}, + {file = "numpy-2.2.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5a145e956b374e72ad1dff82779177d4a3c62bc8248f41b80cb5122e68f22d13"}, + {file = "numpy-2.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18142b497d70a34b01642b9feabb70156311b326fdddd875a9981f34a369b671"}, + {file = "numpy-2.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7d41d1612c1a82b64697e894b75db6758d4f21c3ec069d841e60ebe54b5b571"}, + {file = "numpy-2.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a98f6f20465e7618c83252c02041517bd2f7ea29be5378f09667a8f654a5918d"}, + {file = "numpy-2.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e09d40edfdb4e260cb1567d8ae770ccf3b8b7e9f0d9b5c2a9992696b30ce2742"}, + {file = "numpy-2.2.0-cp313-cp313-win32.whl", hash = "sha256:3905a5fffcc23e597ee4d9fb3fcd209bd658c352657548db7316e810ca80458e"}, + {file = "numpy-2.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:a184288538e6ad699cbe6b24859206e38ce5fba28f3bcfa51c90d0502c1582b2"}, + {file = "numpy-2.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7832f9e8eb00be32f15fdfb9a981d6955ea9adc8574c521d48710171b6c55e95"}, + {file = "numpy-2.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f0dd071b95bbca244f4cb7f70b77d2ff3aaaba7fa16dc41f58d14854a6204e6c"}, + {file = "numpy-2.2.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:b0b227dcff8cdc3efbce66d4e50891f04d0a387cce282fe1e66199146a6a8fca"}, + {file = "numpy-2.2.0-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:6ab153263a7c5ccaf6dfe7e53447b74f77789f28ecb278c3b5d49db7ece10d6d"}, + {file = "numpy-2.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e500aba968a48e9019e42c0c199b7ec0696a97fa69037bea163b55398e390529"}, + {file = "numpy-2.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:440cfb3db4c5029775803794f8638fbdbf71ec702caf32735f53b008e1eaece3"}, + {file = "numpy-2.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a55dc7a7f0b6198b07ec0cd445fbb98b05234e8b00c5ac4874a63372ba98d4ab"}, + {file = "numpy-2.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4bddbaa30d78c86329b26bd6aaaea06b1e47444da99eddac7bf1e2fab717bd72"}, + {file = "numpy-2.2.0-cp313-cp313t-win32.whl", hash = "sha256:30bf971c12e4365153afb31fc73f441d4da157153f3400b82db32d04de1e4066"}, + {file = "numpy-2.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:d35717333b39d1b6bb8433fa758a55f1081543de527171543a2b710551d40881"}, + {file = "numpy-2.2.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e12c6c1ce84628c52d6367863773f7c8c8241be554e8b79686e91a43f1733773"}, + {file = "numpy-2.2.0-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:b6207dc8fb3c8cb5668e885cef9ec7f70189bec4e276f0ff70d5aa078d32c88e"}, + {file = "numpy-2.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a50aeff71d0f97b6450d33940c7181b08be1441c6c193e678211bff11aa725e7"}, + {file = "numpy-2.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:df12a1f99b99f569a7c2ae59aa2d31724e8d835fc7f33e14f4792e3071d11221"}, + {file = "numpy-2.2.0.tar.gz", hash = "sha256:140dd80ff8981a583a60980be1a655068f8adebf7a45a06a6858c873fcdcd4a0"}, ] [[package]] @@ -2722,7 +2612,7 @@ files = [ name = "parso" version = "0.8.4" description = "A Python Parser" -optional = true +optional = false python-versions = ">=3.6" files = [ {file = "parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18"}, @@ -2737,7 +2627,7 @@ testing = ["docopt", "pytest"] name = "pexpect" version = "4.9.0" description = "Pexpect allows easy control of interactive console applications." -optional = true +optional = false python-versions = "*" files = [ {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, @@ -2890,13 +2780,13 @@ virtualenv = ">=20.10.0" [[package]] name = "prometheus-client" -version = "0.21.0" +version = "0.21.1" description = "Python client for the Prometheus monitoring system." optional = true python-versions = ">=3.8" files = [ - {file = "prometheus_client-0.21.0-py3-none-any.whl", hash = "sha256:4fa6b4dd0ac16d58bb587c04b1caae65b8c5043e85f778f42f5f632f6af2e166"}, - {file = "prometheus_client-0.21.0.tar.gz", hash = "sha256:96c83c606b71ff2b0a433c98889d275f51ffec6c5e267de37c7a2b5c9aa9233e"}, + {file = "prometheus_client-0.21.1-py3-none-any.whl", hash = "sha256:594b45c410d6f4f8888940fe80b5cc2521b305a1fafe1c58609ef715a001f301"}, + {file = "prometheus_client-0.21.1.tar.gz", hash = "sha256:252505a722ac04b0456be05c05f75f45d760c2911ffc45f2a06bcaed9f3ae3fb"}, ] [package.extras] @@ -2906,7 +2796,7 @@ twisted = ["twisted"] name = "prompt-toolkit" version = "3.0.48" description = "Library for building powerful interactive command lines in Python" -optional = true +optional = false python-versions = ">=3.7.0" files = [ {file = "prompt_toolkit-3.0.48-py3-none-any.whl", hash = "sha256:f49a827f90062e411f1ce1f854f2aedb3c23353244f8108b89283587397ac10e"}, @@ -2918,31 +2808,33 @@ wcwidth = "*" [[package]] name = "psutil" -version = "5.9.8" +version = "6.1.0" description = "Cross-platform lib for process and system monitoring in Python." -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" -files = [ - {file = "psutil-5.9.8-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:26bd09967ae00920df88e0352a91cff1a78f8d69b3ecabbfe733610c0af486c8"}, - {file = "psutil-5.9.8-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:05806de88103b25903dff19bb6692bd2e714ccf9e668d050d144012055cbca73"}, - {file = "psutil-5.9.8-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:611052c4bc70432ec770d5d54f64206aa7203a101ec273a0cd82418c86503bb7"}, - {file = "psutil-5.9.8-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:50187900d73c1381ba1454cf40308c2bf6f34268518b3f36a9b663ca87e65e36"}, - {file = "psutil-5.9.8-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:02615ed8c5ea222323408ceba16c60e99c3f91639b07da6373fb7e6539abc56d"}, - {file = "psutil-5.9.8-cp27-none-win32.whl", hash = "sha256:36f435891adb138ed3c9e58c6af3e2e6ca9ac2f365efe1f9cfef2794e6c93b4e"}, - {file = "psutil-5.9.8-cp27-none-win_amd64.whl", hash = "sha256:bd1184ceb3f87651a67b2708d4c3338e9b10c5df903f2e3776b62303b26cb631"}, - {file = "psutil-5.9.8-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:aee678c8720623dc456fa20659af736241f575d79429a0e5e9cf88ae0605cc81"}, - {file = "psutil-5.9.8-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cb6403ce6d8e047495a701dc7c5bd788add903f8986d523e3e20b98b733e421"}, - {file = "psutil-5.9.8-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d06016f7f8625a1825ba3732081d77c94589dca78b7a3fc072194851e88461a4"}, - {file = "psutil-5.9.8-cp36-cp36m-win32.whl", hash = "sha256:7d79560ad97af658a0f6adfef8b834b53f64746d45b403f225b85c5c2c140eee"}, - {file = "psutil-5.9.8-cp36-cp36m-win_amd64.whl", hash = "sha256:27cc40c3493bb10de1be4b3f07cae4c010ce715290a5be22b98493509c6299e2"}, - {file = "psutil-5.9.8-cp37-abi3-win32.whl", hash = "sha256:bc56c2a1b0d15aa3eaa5a60c9f3f8e3e565303b465dbf57a1b730e7a2b9844e0"}, - {file = "psutil-5.9.8-cp37-abi3-win_amd64.whl", hash = "sha256:8db4c1b57507eef143a15a6884ca10f7c73876cdf5d51e713151c1236a0e68cf"}, - {file = "psutil-5.9.8-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:d16bbddf0693323b8c6123dd804100241da461e41d6e332fb0ba6058f630f8c8"}, - {file = "psutil-5.9.8.tar.gz", hash = "sha256:6be126e3225486dff286a8fb9a06246a5253f4c7c53b475ea5f5ac934e64194c"}, +optional = true +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +files = [ + {file = "psutil-6.1.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ff34df86226c0227c52f38b919213157588a678d049688eded74c76c8ba4a5d0"}, + {file = "psutil-6.1.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:c0e0c00aa18ca2d3b2b991643b799a15fc8f0563d2ebb6040f64ce8dc027b942"}, + {file = "psutil-6.1.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:000d1d1ebd634b4efb383f4034437384e44a6d455260aaee2eca1e9c1b55f047"}, + {file = "psutil-6.1.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:5cd2bcdc75b452ba2e10f0e8ecc0b57b827dd5d7aaffbc6821b2a9a242823a76"}, + {file = "psutil-6.1.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:045f00a43c737f960d273a83973b2511430d61f283a44c96bf13a6e829ba8fdc"}, + {file = "psutil-6.1.0-cp27-none-win32.whl", hash = "sha256:9118f27452b70bb1d9ab3198c1f626c2499384935aaf55388211ad982611407e"}, + {file = "psutil-6.1.0-cp27-none-win_amd64.whl", hash = "sha256:a8506f6119cff7015678e2bce904a4da21025cc70ad283a53b099e7620061d85"}, + {file = "psutil-6.1.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6e2dcd475ce8b80522e51d923d10c7871e45f20918e027ab682f94f1c6351688"}, + {file = "psutil-6.1.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0895b8414afafc526712c498bd9de2b063deaac4021a3b3c34566283464aff8e"}, + {file = "psutil-6.1.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9dcbfce5d89f1d1f2546a2090f4fcf87c7f669d1d90aacb7d7582addece9fb38"}, + {file = "psutil-6.1.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:498c6979f9c6637ebc3a73b3f87f9eb1ec24e1ce53a7c5173b8508981614a90b"}, + {file = "psutil-6.1.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d905186d647b16755a800e7263d43df08b790d709d575105d419f8b6ef65423a"}, + {file = "psutil-6.1.0-cp36-cp36m-win32.whl", hash = "sha256:6d3fbbc8d23fcdcb500d2c9f94e07b1342df8ed71b948a2649b5cb060a7c94ca"}, + {file = "psutil-6.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:1209036fbd0421afde505a4879dee3b2fd7b1e14fee81c0069807adcbbcca747"}, + {file = "psutil-6.1.0-cp37-abi3-win32.whl", hash = "sha256:1ad45a1f5d0b608253b11508f80940985d1d0c8f6111b5cb637533a0e6ddc13e"}, + {file = "psutil-6.1.0-cp37-abi3-win_amd64.whl", hash = "sha256:a8fb3752b491d246034fa4d279ff076501588ce8cbcdbb62c32fd7a377d996be"}, + {file = "psutil-6.1.0.tar.gz", hash = "sha256:353815f59a7f64cdaca1c0307ee13558a0512f6db064e92fe833784f08539c7a"}, ] [package.extras] -test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] +dev = ["black", "check-manifest", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pytest-cov", "requests", "rstcheck", "ruff", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "virtualenv", "wheel"] +test = ["pytest", "pytest-xdist", "setuptools"] [[package]] name = "psutil-wheels" @@ -2973,7 +2865,7 @@ test = ["enum34", "ipaddress", "mock", "pywin32", "unittest2", "wmi"] name = "ptyprocess" version = "0.7.0" description = "Run a subprocess in a pseudo terminal" -optional = true +optional = false python-versions = "*" files = [ {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, @@ -2984,7 +2876,7 @@ files = [ name = "pure-eval" version = "0.2.3" description = "Safely evaluate AST nodes without side effects" -optional = true +optional = false python-versions = "*" files = [ {file = "pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0"}, @@ -3005,26 +2897,6 @@ files = [ {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, ] -[[package]] -name = "pycairo" -version = "1.27.0" -description = "Python interface for cairo" -optional = false -python-versions = ">=3.9" -files = [ - {file = "pycairo-1.27.0-cp310-cp310-win32.whl", hash = "sha256:e20f431244634cf244ab6b4c3a2e540e65746eed1324573cf291981c3e65fc05"}, - {file = "pycairo-1.27.0-cp310-cp310-win_amd64.whl", hash = "sha256:03bf570e3919901572987bc69237b648fe0de242439980be3e606b396e3318c9"}, - {file = "pycairo-1.27.0-cp311-cp311-win32.whl", hash = "sha256:9a9b79f92a434dae65c34c830bb9abdbd92654195e73d52663cbe45af1ad14b2"}, - {file = "pycairo-1.27.0-cp311-cp311-win_amd64.whl", hash = "sha256:d40a6d80b15dacb3672dc454df4bc4ab3988c6b3f36353b24a255dc59a1c8aea"}, - {file = "pycairo-1.27.0-cp312-cp312-win32.whl", hash = "sha256:e2239b9bb6c05edae5f3be97128e85147a155465e644f4d98ea0ceac7afc04ee"}, - {file = "pycairo-1.27.0-cp312-cp312-win_amd64.whl", hash = "sha256:27cb4d3a80e3b9990af552818515a8e466e0317063a6e61585533f1a86f1b7d5"}, - {file = "pycairo-1.27.0-cp313-cp313-win32.whl", hash = "sha256:01505c138a313df2469f812405963532fc2511fb9bca9bdc8e0ab94c55d1ced8"}, - {file = "pycairo-1.27.0-cp313-cp313-win_amd64.whl", hash = "sha256:b0349d744c068b6644ae23da6ada111c8a8a7e323b56cbce3707cba5bdb474cc"}, - {file = "pycairo-1.27.0-cp39-cp39-win32.whl", hash = "sha256:f9ca8430751f1fdcd3f072377560c9e15608b9a42d61375469db853566993c9b"}, - {file = "pycairo-1.27.0-cp39-cp39-win_amd64.whl", hash = "sha256:1b1321652a6e27c4de3069709b1cae22aed2707fd8c5e889c04a95669228af2a"}, - {file = "pycairo-1.27.0.tar.gz", hash = "sha256:5cb21e7a00a2afcafea7f14390235be33497a2cce53a98a19389492a60628430"}, -] - [[package]] name = "pycparser" version = "2.22" @@ -3036,6 +2908,138 @@ files = [ {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, ] +[[package]] +name = "pydantic" +version = "2.10.3" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic-2.10.3-py3-none-any.whl", hash = "sha256:be04d85bbc7b65651c5f8e6b9976ed9c6f41782a55524cef079a34a0bb82144d"}, + {file = "pydantic-2.10.3.tar.gz", hash = "sha256:cb5ac360ce894ceacd69c403187900a02c4b20b693a9dd1d643e1effab9eadf9"}, +] + +[package.dependencies] +annotated-types = ">=0.6.0" +pydantic-core = "2.27.1" +typing-extensions = ">=4.12.2" + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata"] + +[[package]] +name = "pydantic-core" +version = "2.27.1" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:71a5e35c75c021aaf400ac048dacc855f000bdfed91614b4a726f7432f1f3d6a"}, + {file = "pydantic_core-2.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f82d068a2d6ecfc6e054726080af69a6764a10015467d7d7b9f66d6ed5afa23b"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:121ceb0e822f79163dd4699e4c54f5ad38b157084d97b34de8b232bcaad70278"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4603137322c18eaf2e06a4495f426aa8d8388940f3c457e7548145011bb68e05"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a33cd6ad9017bbeaa9ed78a2e0752c5e250eafb9534f308e7a5f7849b0b1bfb4"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15cc53a3179ba0fcefe1e3ae50beb2784dede4003ad2dfd24f81bba4b23a454f"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45d9c5eb9273aa50999ad6adc6be5e0ecea7e09dbd0d31bd0c65a55a2592ca08"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8bf7b66ce12a2ac52d16f776b31d16d91033150266eb796967a7e4621707e4f6"}, + {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:655d7dd86f26cb15ce8a431036f66ce0318648f8853d709b4167786ec2fa4807"}, + {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:5556470f1a2157031e676f776c2bc20acd34c1990ca5f7e56f1ebf938b9ab57c"}, + {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f69ed81ab24d5a3bd93861c8c4436f54afdf8e8cc421562b0c7504cf3be58206"}, + {file = "pydantic_core-2.27.1-cp310-none-win32.whl", hash = "sha256:f5a823165e6d04ccea61a9f0576f345f8ce40ed533013580e087bd4d7442b52c"}, + {file = "pydantic_core-2.27.1-cp310-none-win_amd64.whl", hash = "sha256:57866a76e0b3823e0b56692d1a0bf722bffb324839bb5b7226a7dbd6c9a40b17"}, + {file = "pydantic_core-2.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac3b20653bdbe160febbea8aa6c079d3df19310d50ac314911ed8cc4eb7f8cb8"}, + {file = "pydantic_core-2.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a5a8e19d7c707c4cadb8c18f5f60c843052ae83c20fa7d44f41594c644a1d330"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f7059ca8d64fea7f238994c97d91f75965216bcbe5f695bb44f354893f11d52"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bed0f8a0eeea9fb72937ba118f9db0cb7e90773462af7962d382445f3005e5a4"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3cb37038123447cf0f3ea4c74751f6a9d7afef0eb71aa07bf5f652b5e6a132c"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84286494f6c5d05243456e04223d5a9417d7f443c3b76065e75001beb26f88de"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acc07b2cfc5b835444b44a9956846b578d27beeacd4b52e45489e93276241025"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4fefee876e07a6e9aad7a8c8c9f85b0cdbe7df52b8a9552307b09050f7512c7e"}, + {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:258c57abf1188926c774a4c94dd29237e77eda19462e5bb901d88adcab6af919"}, + {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:35c14ac45fcfdf7167ca76cc80b2001205a8d5d16d80524e13508371fb8cdd9c"}, + {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d1b26e1dff225c31897696cab7d4f0a315d4c0d9e8666dbffdb28216f3b17fdc"}, + {file = "pydantic_core-2.27.1-cp311-none-win32.whl", hash = "sha256:2cdf7d86886bc6982354862204ae3b2f7f96f21a3eb0ba5ca0ac42c7b38598b9"}, + {file = "pydantic_core-2.27.1-cp311-none-win_amd64.whl", hash = "sha256:3af385b0cee8df3746c3f406f38bcbfdc9041b5c2d5ce3e5fc6637256e60bbc5"}, + {file = "pydantic_core-2.27.1-cp311-none-win_arm64.whl", hash = "sha256:81f2ec23ddc1b476ff96563f2e8d723830b06dceae348ce02914a37cb4e74b89"}, + {file = "pydantic_core-2.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9cbd94fc661d2bab2bc702cddd2d3370bbdcc4cd0f8f57488a81bcce90c7a54f"}, + {file = "pydantic_core-2.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f8c4718cd44ec1580e180cb739713ecda2bdee1341084c1467802a417fe0f02"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15aae984e46de8d376df515f00450d1522077254ef6b7ce189b38ecee7c9677c"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ba5e3963344ff25fc8c40da90f44b0afca8cfd89d12964feb79ac1411a260ac"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:992cea5f4f3b29d6b4f7f1726ed8ee46c8331c6b4eed6db5b40134c6fe1768bb"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0325336f348dbee6550d129b1627cb8f5351a9dc91aad141ffb96d4937bd9529"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7597c07fbd11515f654d6ece3d0e4e5093edc30a436c63142d9a4b8e22f19c35"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3bbd5d8cc692616d5ef6fbbbd50dbec142c7e6ad9beb66b78a96e9c16729b089"}, + {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:dc61505e73298a84a2f317255fcc72b710b72980f3a1f670447a21efc88f8381"}, + {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:e1f735dc43da318cad19b4173dd1ffce1d84aafd6c9b782b3abc04a0d5a6f5bb"}, + {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f4e5658dbffe8843a0f12366a4c2d1c316dbe09bb4dfbdc9d2d9cd6031de8aae"}, + {file = "pydantic_core-2.27.1-cp312-none-win32.whl", hash = "sha256:672ebbe820bb37988c4d136eca2652ee114992d5d41c7e4858cdd90ea94ffe5c"}, + {file = "pydantic_core-2.27.1-cp312-none-win_amd64.whl", hash = "sha256:66ff044fd0bb1768688aecbe28b6190f6e799349221fb0de0e6f4048eca14c16"}, + {file = "pydantic_core-2.27.1-cp312-none-win_arm64.whl", hash = "sha256:9a3b0793b1bbfd4146304e23d90045f2a9b5fd5823aa682665fbdaf2a6c28f3e"}, + {file = "pydantic_core-2.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073"}, + {file = "pydantic_core-2.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:764be71193f87d460a03f1f7385a82e226639732214b402f9aa61f0d025f0737"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c00666a3bd2f84920a4e94434f5974d7bbc57e461318d6bb34ce9cdbbc1f6b2"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccaa88b24eebc0f849ce0a4d09e8a408ec5a94afff395eb69baf868f5183107"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c65af9088ac534313e1963443d0ec360bb2b9cba6c2909478d22c2e363d98a51"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206b5cf6f0c513baffaeae7bd817717140770c74528f3e4c3e1cec7871ddd61a"}, + {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:062f60e512fc7fff8b8a9d680ff0ddaaef0193dba9fa83e679c0c5f5fbd018bc"}, + {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:a0697803ed7d4af5e4c1adf1670af078f8fcab7a86350e969f454daf598c4960"}, + {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:58ca98a950171f3151c603aeea9303ef6c235f692fe555e883591103da709b23"}, + {file = "pydantic_core-2.27.1-cp313-none-win32.whl", hash = "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05"}, + {file = "pydantic_core-2.27.1-cp313-none-win_amd64.whl", hash = "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337"}, + {file = "pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5"}, + {file = "pydantic_core-2.27.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:5897bec80a09b4084aee23f9b73a9477a46c3304ad1d2d07acca19723fb1de62"}, + {file = "pydantic_core-2.27.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d0165ab2914379bd56908c02294ed8405c252250668ebcb438a55494c69f44ab"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b9af86e1d8e4cfc82c2022bfaa6f459381a50b94a29e95dcdda8442d6d83864"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f6c8a66741c5f5447e047ab0ba7a1c61d1e95580d64bce852e3df1f895c4067"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a42d6a8156ff78981f8aa56eb6394114e0dedb217cf8b729f438f643608cbcd"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64c65f40b4cd8b0e049a8edde07e38b476da7e3aaebe63287c899d2cff253fa5"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdcf339322a3fae5cbd504edcefddd5a50d9ee00d968696846f089b4432cf78"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bf99c8404f008750c846cb4ac4667b798a9f7de673ff719d705d9b2d6de49c5f"}, + {file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8f1edcea27918d748c7e5e4d917297b2a0ab80cad10f86631e488b7cddf76a36"}, + {file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:159cac0a3d096f79ab6a44d77a961917219707e2a130739c64d4dd46281f5c2a"}, + {file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:029d9757eb621cc6e1848fa0b0310310de7301057f623985698ed7ebb014391b"}, + {file = "pydantic_core-2.27.1-cp38-none-win32.whl", hash = "sha256:a28af0695a45f7060e6f9b7092558a928a28553366519f64083c63a44f70e618"}, + {file = "pydantic_core-2.27.1-cp38-none-win_amd64.whl", hash = "sha256:2d4567c850905d5eaaed2f7a404e61012a51caf288292e016360aa2b96ff38d4"}, + {file = "pydantic_core-2.27.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:e9386266798d64eeb19dd3677051f5705bf873e98e15897ddb7d76f477131967"}, + {file = "pydantic_core-2.27.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4228b5b646caa73f119b1ae756216b59cc6e2267201c27d3912b592c5e323b60"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3dfe500de26c52abe0477dde16192ac39c98f05bf2d80e76102d394bd13854"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aee66be87825cdf72ac64cb03ad4c15ffef4143dbf5c113f64a5ff4f81477bf9"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b748c44bb9f53031c8cbc99a8a061bc181c1000c60a30f55393b6e9c45cc5bd"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ca038c7f6a0afd0b2448941b6ef9d5e1949e999f9e5517692eb6da58e9d44be"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e0bd57539da59a3e4671b90a502da9a28c72322a4f17866ba3ac63a82c4498e"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ac6c2c45c847bbf8f91930d88716a0fb924b51e0c6dad329b793d670ec5db792"}, + {file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b94d4ba43739bbe8b0ce4262bcc3b7b9f31459ad120fb595627eaeb7f9b9ca01"}, + {file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:00e6424f4b26fe82d44577b4c842d7df97c20be6439e8e685d0d715feceb9fb9"}, + {file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:38de0a70160dd97540335b7ad3a74571b24f1dc3ed33f815f0880682e6880131"}, + {file = "pydantic_core-2.27.1-cp39-none-win32.whl", hash = "sha256:7ccebf51efc61634f6c2344da73e366c75e735960b5654b63d7e6f69a5885fa3"}, + {file = "pydantic_core-2.27.1-cp39-none-win_amd64.whl", hash = "sha256:a57847b090d7892f123726202b7daa20df6694cbd583b67a592e856bff603d6c"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3fa80ac2bd5856580e242dbc202db873c60a01b20309c8319b5c5986fbe53ce6"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d950caa237bb1954f1b8c9227b5065ba6875ac9771bb8ec790d956a699b78676"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e4216e64d203e39c62df627aa882f02a2438d18a5f21d7f721621f7a5d3611d"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02a3d637bd387c41d46b002f0e49c52642281edacd2740e5a42f7017feea3f2c"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:161c27ccce13b6b0c8689418da3885d3220ed2eae2ea5e9b2f7f3d48f1d52c27"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:19910754e4cc9c63bc1c7f6d73aa1cfee82f42007e407c0f413695c2f7ed777f"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e173486019cc283dc9778315fa29a363579372fe67045e971e89b6365cc035ed"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:af52d26579b308921b73b956153066481f064875140ccd1dfd4e77db89dbb12f"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:981fb88516bd1ae8b0cbbd2034678a39dedc98752f264ac9bc5839d3923fa04c"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5fde892e6c697ce3e30c61b239330fc5d569a71fefd4eb6512fc6caec9dd9e2f"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:816f5aa087094099fff7edabb5e01cc370eb21aa1a1d44fe2d2aefdfb5599b31"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c10c309e18e443ddb108f0ef64e8729363adbfd92d6d57beec680f6261556f3"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98476c98b02c8e9b2eec76ac4156fd006628b1b2d0ef27e548ffa978393fd154"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3027001c28434e7ca5a6e1e527487051136aa81803ac812be51802150d880dd"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:7699b1df36a48169cdebda7ab5a2bac265204003f153b4bd17276153d997670a"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1c39b07d90be6b48968ddc8c19e7585052088fd7ec8d568bb31ff64c70ae3c97"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:46ccfe3032b3915586e469d4972973f893c0a2bb65669194a5bdea9bacc088c2"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:62ba45e21cf6571d7f716d903b5b7b6d2617e2d5d67c0923dc47b9d41369f840"}, + {file = "pydantic_core-2.27.1.tar.gz", hash = "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + [[package]] name = "pydub" version = "0.25.1" @@ -3068,13 +3072,13 @@ urllib3 = ">=1.26.0" [[package]] name = "pyglet" -version = "2.0.18" +version = "2.0.20" description = "pyglet is a cross-platform games and multimedia package." optional = false python-versions = ">=3.8" files = [ - {file = "pyglet-2.0.18-py3-none-any.whl", hash = "sha256:e592952ae0297e456c587b6486ed8c3e5f9d0c3519d517bb92dde5fdf4c26b41"}, - {file = "pyglet-2.0.18.tar.gz", hash = "sha256:7cf9238d70082a2da282759679f8a011cc979753a32224a8ead8ed80e48f99dc"}, + {file = "pyglet-2.0.20-py3-none-any.whl", hash = "sha256:341cdc506fe97c4d8c4fb35aac89cefcb0ca6bf59eddcf2d1078c327dde1f02e"}, + {file = "pyglet-2.0.20.tar.gz", hash = "sha256:702ea52b1fc1b6447904d2edd579212b29f1b3475e098ac49b57647a064accb7"}, ] [[package]] @@ -3266,6 +3270,17 @@ files = [ [package.dependencies] pyobjc-core = ">=10.3.2" +[[package]] +name = "pyopengl" +version = "3.1.7" +description = "Standard OpenGL bindings for Python" +optional = false +python-versions = "*" +files = [ + {file = "PyOpenGL-3.1.7-py3-none-any.whl", hash = "sha256:a6ab19cf290df6101aaf7470843a9c46207789855746399d0af92521a0a92b7a"}, + {file = "PyOpenGL-3.1.7.tar.gz", hash = "sha256:eef31a3888e6984fd4d8e6c9961b184c9813ca82604d37fe3da80eb000a76c86"}, +] + [[package]] name = "pyparsing" version = "3.2.0" @@ -3372,15 +3387,18 @@ six = ">=1.5" [[package]] name = "python-json-logger" -version = "2.0.7" -description = "A python library adding a json log formatter" +version = "3.2.1" +description = "JSON Log Formatter for the Python Logging Package" optional = true -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "python-json-logger-2.0.7.tar.gz", hash = "sha256:23e7ec02d34237c5aa1e29a070193a4ea87583bb4e7f8fd06d3de8264c4b2e1c"}, - {file = "python_json_logger-2.0.7-py3-none-any.whl", hash = "sha256:f380b826a991ebbe3de4d897aeec42760035ac760345e57b812938dc8b35e2bd"}, + {file = "python_json_logger-3.2.1-py3-none-any.whl", hash = "sha256:cdc17047eb5374bd311e748b42f99d71223f3b0e186f4206cc5d52aefe85b090"}, + {file = "python_json_logger-3.2.1.tar.gz", hash = "sha256:8eb0554ea17cb75b05d2848bc14fb02fbdbd9d6972120781b974380bfa162008"}, ] +[package.extras] +dev = ["backports.zoneinfo", "black", "build", "freezegun", "mdx_truly_sane_lists", "mike", "mkdocs", "mkdocs-awesome-pages-plugin", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-material (>=8.5)", "mkdocstrings[python]", "msgspec", "msgspec-python313-pre", "mypy", "orjson", "pylint", "pytest", "tzdata", "validate-pyproject[all]"] + [[package]] name = "pywin32" version = "308" @@ -3688,183 +3706,143 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "rpds-py" -version = "0.22.0" +version = "0.22.3" description = "Python bindings to Rust's persistent data structures (rpds)" optional = true python-versions = ">=3.9" files = [ - {file = "rpds_py-0.22.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a4366f264fa60d3c109f0b27af0cd9eb8d46746bd70bd3d9d425f035b6c7e286"}, - {file = "rpds_py-0.22.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e34a3e665d38d0749072e6565400c8ce9abae976e338919a0dfbfb0e1ba43068"}, - {file = "rpds_py-0.22.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38cacf1f378571450576f2c8ce87da6f3fddc59d744de5c12b37acc23285b1e1"}, - {file = "rpds_py-0.22.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8cbb040fec8eddd5a6a75e737fd73c9ce37e51f94bacdd0b178d0174a4758395"}, - {file = "rpds_py-0.22.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d80fd710b3307a3c63809048b72c536689b9b0b31a2518339c3f1a4d29c73d7a"}, - {file = "rpds_py-0.22.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b5d17d8f5b885ce50e0cda85f99c0719e365e98b587338535fa566a48375afb"}, - {file = "rpds_py-0.22.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7a048ec1ebc991331d709be4884dc318c9eaafa66dcde8be0933ac0e702149"}, - {file = "rpds_py-0.22.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:306da3dfa174b489a3fc63b0872e2226a5ddf94c59875a770d72aff945d5ed96"}, - {file = "rpds_py-0.22.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c7b4450093c0c909299770226fb0285be47b0a57545bae25b5c4e51566b0e587"}, - {file = "rpds_py-0.22.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0903ffdb5b9007e503203b6285e4ff0faf96d875c19f1d103b475acf7d9f7311"}, - {file = "rpds_py-0.22.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d1522025cda9e57329aade769f56e5793b2a5da7759a21914ee10e67e17e601e"}, - {file = "rpds_py-0.22.0-cp310-cp310-win32.whl", hash = "sha256:49e084d47a66027ac72844f9f52f13d347a9a1f05d4f84381b420e47f836a7fd"}, - {file = "rpds_py-0.22.0-cp310-cp310-win_amd64.whl", hash = "sha256:d9ceca96df54cb1675a0b7f52f1c6d5d1df62c5b40741ba211780f1b05a282a2"}, - {file = "rpds_py-0.22.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:771c9a3851beaa617d8c8115d65f834a2b52490f42ee2b88b13f1fc5529e9e0c"}, - {file = "rpds_py-0.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:341a07a4b55126bfae68c9bf24220a73d456111e5eb3dcbdab9fd16de2341224"}, - {file = "rpds_py-0.22.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7649c8b8e4bd1ccc5fcbd51a855d57a617deeba19c66e3d04b1abecc61036b2"}, - {file = "rpds_py-0.22.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2f513758e7cda8bc262e80299a8e3395d7ef7f4ae705be62632f229bc6c33208"}, - {file = "rpds_py-0.22.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba1fc34d0b2f6fd53377a4c954116251eba6d076bf64f903311f4a7d27d10acd"}, - {file = "rpds_py-0.22.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:632d2fdddd9fbe3ac8896a119fd18a71fc95ca9c4cbe5223096c142d8c4a2b1d"}, - {file = "rpds_py-0.22.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:326e42f2b49462e05f8527a1311ce98f9f97c484b3e443ec0ea4638bed3aebcf"}, - {file = "rpds_py-0.22.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e9bbdba9e75b1a9ee1dd1335034dad998ef1acc08492226c6fd50aa773bdfa7d"}, - {file = "rpds_py-0.22.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:41f65a97bf2c4b161c9f8f89bc37058346bec9b36e373c8ad00a16c957bff625"}, - {file = "rpds_py-0.22.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0686f2c16eafdc2c6b4ce6e86e5b3092e87db09ae64be2787616444eb35b9756"}, - {file = "rpds_py-0.22.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4e7c9aa2353eb0b0d845323857197daa036c2ff8624df990b0d886d22a8f665e"}, - {file = "rpds_py-0.22.0-cp311-cp311-win32.whl", hash = "sha256:2d2fc3ab021be3e0b5aec6d4164f2689d231b8bfc5185cc454314746aa4aee72"}, - {file = "rpds_py-0.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:87453d491369cd8018016d2714a13e8461975161703c18ee31eecf087a8ae5d4"}, - {file = "rpds_py-0.22.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:e9d4293b21c69ee4f9e1a99ac4f772951d345611c614a0cfae2ec6b565279bc9"}, - {file = "rpds_py-0.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:67e013a17a3db4d98cc228fd5aeb36a51b0f5cf7330b9102a552060f1fe4e560"}, - {file = "rpds_py-0.22.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b639a19e1791b646d27f15d17530a51722cc728d43b2dff3aeb904f92d91bac"}, - {file = "rpds_py-0.22.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1357c3092702078b7782b6ebd5ba9b22c1a291c34fbf9d8f1a48237466ac7758"}, - {file = "rpds_py-0.22.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:842855bbb113a19c393c6de5aa6ed9a26c6b13c2fead5e49114d39f0d08b94d8"}, - {file = "rpds_py-0.22.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ae7927cd2b869ca4dc645169d8af5494a29c99afd0ea0f24dd00c811ab1d8b8"}, - {file = "rpds_py-0.22.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91bfef5daa2a5a4fe62f8d317fc91a626073639f951f851bd2cb252d01bc6c5"}, - {file = "rpds_py-0.22.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4fc4824e38c1e91a73bc820e7caacaf19d0acd557465aceef0420ca59489b390"}, - {file = "rpds_py-0.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:92d28a608127b357da47c99e0d0e0655ca2060286540fe9f2a25a2e8ac666e05"}, - {file = "rpds_py-0.22.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c637188b930175c256f13adbfc427b83ec7e64476d1ec9d6608f312bb84e06c3"}, - {file = "rpds_py-0.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:93bbd66f46dddc41e8c656130c97c0fb515e0fa44e1eebb2592769dbbd41b2f5"}, - {file = "rpds_py-0.22.0-cp312-cp312-win32.whl", hash = "sha256:54d8f94dec5765a9edc19610fecf0fdf9cab36cbb9def1213188215f735a6f98"}, - {file = "rpds_py-0.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:931bf3d0705b2834fed29354f35170fa022fe22a95542b61b7c66aca5f8a224f"}, - {file = "rpds_py-0.22.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:2a57300cc8b034c5707085249efd09f19116bb80278d0ec925d7f3710165c510"}, - {file = "rpds_py-0.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c398a5a8e258dfdc5ea2aa4e5aa2ca3207f654a8eb268693dd1a76939074a588"}, - {file = "rpds_py-0.22.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a6cc4eb1e86364331928acafb2bb41d8ab735ca3caf2d6019b9f6dac3f4f65d"}, - {file = "rpds_py-0.22.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:574c5c94213bc9990805bfd7e4ba3826d3c098516cbc19f0d0ef0433ad93fa06"}, - {file = "rpds_py-0.22.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c0321bc03a1c513eca1837e3bba948b975bcf3a172aebc197ab3573207f137a"}, - {file = "rpds_py-0.22.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d276280649305c1da6cdd84585d48ae1f0efa67434d8b10d2df95228e59a05bb"}, - {file = "rpds_py-0.22.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c17b43fe9c6da16885e3fe28922bcd1a029e61631fb771c7d501019b40bcc904"}, - {file = "rpds_py-0.22.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:48c95997af9314f4034fe5ba2d837399e786586e220835a578d28fe8161e6ae5"}, - {file = "rpds_py-0.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e9aa4af6b879bb75a3c7766fbf49d77f4097dd12b548ecbbd8b3f85caa833281"}, - {file = "rpds_py-0.22.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8426f97117b914b9bfb2a7bd46edc148e8defda728a55a5df3a564abe70cd7a4"}, - {file = "rpds_py-0.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:034964ea0ea09645bdde13038b38abb14be0aa747f20fcfab6181207dd9e0483"}, - {file = "rpds_py-0.22.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:3dc7c64b56b82428894f056e9ff6e8ee917ff74fc26b65211a33602c2372e928"}, - {file = "rpds_py-0.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:1212cb231f2002934cd8d71a0d718fdd9d9a2dd671e0feef8501038df3508026"}, - {file = "rpds_py-0.22.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f21e1278c9456cd601832375c778ca44614d3433996488221a56572c223f04a"}, - {file = "rpds_py-0.22.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:875fe8dffb43c20f68379ee098b035a7038d7903c795d46715f66575a7050b19"}, - {file = "rpds_py-0.22.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e23dcdd4b2ff9c6b3317ea7921b210d39592f8ca1cdea58ada25b202c65c0a69"}, - {file = "rpds_py-0.22.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0fb8efc9e579acf1e556fd86277fecec320c21ca9b5d39db96433ad8c45bc4a"}, - {file = "rpds_py-0.22.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe23687924b25a2dee52fab15976fd6577ed8518072bcda9ff2e2b88ab1f168b"}, - {file = "rpds_py-0.22.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d5469b347445d1c31105f33e7bfc9a8ba213d48e42641a610dda65bf9e3c83f5"}, - {file = "rpds_py-0.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a810a57ce5e8ecf8eac6ec4dab534ff80c34e5a2c31db60e992009cd20f58e0f"}, - {file = "rpds_py-0.22.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d9bb9242b38a664f307b3b897f093896f7ed51ef4fe25a0502e5a368de9151ea"}, - {file = "rpds_py-0.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b4660943030406aaa40ec9f51960dd88049903d9536bc3c8ebb5cc4e1f119bbe"}, - {file = "rpds_py-0.22.0-cp313-cp313t-win32.whl", hash = "sha256:208ce1d8e3af138d1d9b21d7206356b7f29b96675e0113aea652cf024e4ddfdc"}, - {file = "rpds_py-0.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:e6da2e0500742e0f157f005924a0589f2e2dcbfdd6cd0cc0abce367433e989be"}, - {file = "rpds_py-0.22.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:f980a0640599a74f27fd9d50c84c293f1cb7afc2046c5c6d3efaf8ec7cdbc326"}, - {file = "rpds_py-0.22.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ca505fd3767a09a139737f3278bc8a485cb64043062da89bcba27e2f2ea78d33"}, - {file = "rpds_py-0.22.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba235e00e0878ba1080b0f2a761f143b2a2d1c354f3d8e507fbf2f3de401bf18"}, - {file = "rpds_py-0.22.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:81e7a27365b02fe70a77f1365376879917235b3fec551d19b4c91b51d0bc1d07"}, - {file = "rpds_py-0.22.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:32a0e24cab2daae0503b06666d516e90a080c1a95aff0406b9f03c6489177c4b"}, - {file = "rpds_py-0.22.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a73ed43d64209e853bba567a543170267a5cd64f359540b0ca2d597e329ba172"}, - {file = "rpds_py-0.22.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0abcce5e874474d3eab5ad53be03dae2abe651d248bdeaabe83708e82969e78"}, - {file = "rpds_py-0.22.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f4e9946c8c7def17e4fcb5eddb14c4eb6ebc7f6f309075e6c8d23b133c104607"}, - {file = "rpds_py-0.22.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:758098b38c344d9a7f279baf0689261777e601f620078ef5afdc9bd3339965c3"}, - {file = "rpds_py-0.22.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9ad4640a409bc2b7d22b7921e7660f0db96c5c8c69fbb2e8f3261d4f71d33983"}, - {file = "rpds_py-0.22.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8c48fc7458fe3a74dcdf56ba3534ff41bd421f69436df09ff3497fdaac18b431"}, - {file = "rpds_py-0.22.0-cp39-cp39-win32.whl", hash = "sha256:fde778947304e55fc732bc8ea5c6063e74244ac1808471cb498983a210aaf62c"}, - {file = "rpds_py-0.22.0-cp39-cp39-win_amd64.whl", hash = "sha256:5fdf91a7c07f40e47b193f2acae0ed9da35d09325d7c3c3279f722b7cbf3d264"}, - {file = "rpds_py-0.22.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c8fd7a16f7a047e06c747cfcf2acef3ac316132df1c6077445b29ee6f3f3a70b"}, - {file = "rpds_py-0.22.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:6b6e4bcfc32f831bfe3d6d8a5acedfbfd5e252a03c83fa24813b277a3a8a13ca"}, - {file = "rpds_py-0.22.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eadd2417e83a77ce3ae4a0efd08cb0ebdfd317b6406d11020354a53ad458ec84"}, - {file = "rpds_py-0.22.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f9dc2113e0cf0dd637751ca736186fca63664939ceb9f9f67e93ade88c69c0c9"}, - {file = "rpds_py-0.22.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc2c00acdf68f1f69a476b770af311a7dc3955b7de228b04a40bcc51ac4d743b"}, - {file = "rpds_py-0.22.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dfdabdf8519c93908b2bf0f87c3f86f9e88bab279fb4acfd0907519ca5a1739f"}, - {file = "rpds_py-0.22.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8338db3c76833d02dc21c3e2c42534091341d26e4f7ba32c6032bb558a02e07b"}, - {file = "rpds_py-0.22.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8ad4dfda52e64af3202ceb2143a62deba97894b71c64a4405ee80f6b3ea77285"}, - {file = "rpds_py-0.22.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:3b94b074dcce39976db22ea75c7aea8b22d95e6d3b62f76e20e1179a278521d8"}, - {file = "rpds_py-0.22.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:d4f2af3107fe4dc40c0d1a2409863f5249c6796398a1d83c1d99a0b3fa6cfb8d"}, - {file = "rpds_py-0.22.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:bb11809b0de643a292a82f728c494a2bbef0e30a7c42d37464abbd6bef7ca7b1"}, - {file = "rpds_py-0.22.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c1c21030ed494deb10226f90e2dbd84a012d59810c409832714a3dd576527be2"}, - {file = "rpds_py-0.22.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:64a0c965a1e299c9b280006bdb15c276c427c45360aed676305dc36bcaa4d13c"}, - {file = "rpds_py-0.22.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:2498ff422823be087b48bc82710deb87ac34f6b7c8034ee39920647647de1e60"}, - {file = "rpds_py-0.22.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59e63da174ff287db05ef7c21d75974a5bac727ed60452aeb3a14278477842a8"}, - {file = "rpds_py-0.22.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e1c04fb380bc8efaae2fdf17ed6cd5d223da78a8b0b18a610f53d4c5d6e31dfd"}, - {file = "rpds_py-0.22.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e04919ffa9a728c446b27b6b625fa1d00ece221bdb9d633e978a7e0353a12c0e"}, - {file = "rpds_py-0.22.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24c28df05bd284879d0fac850ba697077d2a33b7ebcaea6318d6b6cdfdc86ddc"}, - {file = "rpds_py-0.22.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d33622dc63c295788eed09dbb1d11bed178909d3267b02d873116ee6be368244"}, - {file = "rpds_py-0.22.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7539dbb8f705e13629ba6f23388976aad809e387f32a6e5c0712e4e8d9bfcce7"}, - {file = "rpds_py-0.22.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:b8906f537978da3f7f0bd1ba37b69f6a877bb43312023b086582707d2835bf2f"}, - {file = "rpds_py-0.22.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:62ab12fe03ffc49978d29de9c31bbb216610157f7e5ca8e172fed6642aead3be"}, - {file = "rpds_py-0.22.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:762206ba3bf1d6c8c9e0055871d3c0d5b074b7c3120193e6c067e7866f106ab1"}, - {file = "rpds_py-0.22.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ed0102146574e5e9f079b2e1a06e6b5b12a691f9c74a65b93b7f3d4feda566c6"}, - {file = "rpds_py-0.22.0.tar.gz", hash = "sha256:32de71c393f126d8203e9815557c7ff4d72ed1ad3aa3f52f6c7938413176750a"}, + {file = "rpds_py-0.22.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:6c7b99ca52c2c1752b544e310101b98a659b720b21db00e65edca34483259967"}, + {file = "rpds_py-0.22.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:be2eb3f2495ba669d2a985f9b426c1797b7d48d6963899276d22f23e33d47e37"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70eb60b3ae9245ddea20f8a4190bd79c705a22f8028aaf8bbdebe4716c3fab24"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4041711832360a9b75cfb11b25a6a97c8fb49c07b8bd43d0d02b45d0b499a4ff"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64607d4cbf1b7e3c3c8a14948b99345eda0e161b852e122c6bb71aab6d1d798c"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e69b0a0e2537f26d73b4e43ad7bc8c8efb39621639b4434b76a3de50c6966e"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc27863442d388870c1809a87507727b799c8460573cfbb6dc0eeaef5a11b5ec"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e79dd39f1e8c3504be0607e5fc6e86bb60fe3584bec8b782578c3b0fde8d932c"}, + {file = "rpds_py-0.22.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e0fa2d4ec53dc51cf7d3bb22e0aa0143966119f42a0c3e4998293a3dd2856b09"}, + {file = "rpds_py-0.22.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fda7cb070f442bf80b642cd56483b5548e43d366fe3f39b98e67cce780cded00"}, + {file = "rpds_py-0.22.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cff63a0272fcd259dcc3be1657b07c929c466b067ceb1c20060e8d10af56f5bf"}, + {file = "rpds_py-0.22.3-cp310-cp310-win32.whl", hash = "sha256:9bd7228827ec7bb817089e2eb301d907c0d9827a9e558f22f762bb690b131652"}, + {file = "rpds_py-0.22.3-cp310-cp310-win_amd64.whl", hash = "sha256:9beeb01d8c190d7581a4d59522cd3d4b6887040dcfc744af99aa59fef3e041a8"}, + {file = "rpds_py-0.22.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d20cfb4e099748ea39e6f7b16c91ab057989712d31761d3300d43134e26e165f"}, + {file = "rpds_py-0.22.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:68049202f67380ff9aa52f12e92b1c30115f32e6895cd7198fa2a7961621fc5a"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb4f868f712b2dd4bcc538b0a0c1f63a2b1d584c925e69a224d759e7070a12d5"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bc51abd01f08117283c5ebf64844a35144a0843ff7b2983e0648e4d3d9f10dbb"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f3cec041684de9a4684b1572fe28c7267410e02450f4561700ca5a3bc6695a2"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7ef9d9da710be50ff6809fed8f1963fecdfecc8b86656cadfca3bc24289414b0"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59f4a79c19232a5774aee369a0c296712ad0e77f24e62cad53160312b1c1eaa1"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a60bce91f81ddaac922a40bbb571a12c1070cb20ebd6d49c48e0b101d87300d"}, + {file = "rpds_py-0.22.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e89391e6d60251560f0a8f4bd32137b077a80d9b7dbe6d5cab1cd80d2746f648"}, + {file = "rpds_py-0.22.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e3fb866d9932a3d7d0c82da76d816996d1667c44891bd861a0f97ba27e84fc74"}, + {file = "rpds_py-0.22.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1352ae4f7c717ae8cba93421a63373e582d19d55d2ee2cbb184344c82d2ae55a"}, + {file = "rpds_py-0.22.3-cp311-cp311-win32.whl", hash = "sha256:b0b4136a252cadfa1adb705bb81524eee47d9f6aab4f2ee4fa1e9d3cd4581f64"}, + {file = "rpds_py-0.22.3-cp311-cp311-win_amd64.whl", hash = "sha256:8bd7c8cfc0b8247c8799080fbff54e0b9619e17cdfeb0478ba7295d43f635d7c"}, + {file = "rpds_py-0.22.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:27e98004595899949bd7a7b34e91fa7c44d7a97c40fcaf1d874168bb652ec67e"}, + {file = "rpds_py-0.22.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1978d0021e943aae58b9b0b196fb4895a25cc53d3956b8e35e0b7682eefb6d56"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:655ca44a831ecb238d124e0402d98f6212ac527a0ba6c55ca26f616604e60a45"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:feea821ee2a9273771bae61194004ee2fc33f8ec7db08117ef9147d4bbcbca8e"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:22bebe05a9ffc70ebfa127efbc429bc26ec9e9b4ee4d15a740033efda515cf3d"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3af6e48651c4e0d2d166dc1b033b7042ea3f871504b6805ba5f4fe31581d8d38"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67ba3c290821343c192f7eae1d8fd5999ca2dc99994114643e2f2d3e6138b15"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:02fbb9c288ae08bcb34fb41d516d5eeb0455ac35b5512d03181d755d80810059"}, + {file = "rpds_py-0.22.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f56a6b404f74ab372da986d240e2e002769a7d7102cc73eb238a4f72eec5284e"}, + {file = "rpds_py-0.22.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0a0461200769ab3b9ab7e513f6013b7a97fdeee41c29b9db343f3c5a8e2b9e61"}, + {file = "rpds_py-0.22.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8633e471c6207a039eff6aa116e35f69f3156b3989ea3e2d755f7bc41754a4a7"}, + {file = "rpds_py-0.22.3-cp312-cp312-win32.whl", hash = "sha256:593eba61ba0c3baae5bc9be2f5232430453fb4432048de28399ca7376de9c627"}, + {file = "rpds_py-0.22.3-cp312-cp312-win_amd64.whl", hash = "sha256:d115bffdd417c6d806ea9069237a4ae02f513b778e3789a359bc5856e0404cc4"}, + {file = "rpds_py-0.22.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:ea7433ce7e4bfc3a85654aeb6747babe3f66eaf9a1d0c1e7a4435bbdf27fea84"}, + {file = "rpds_py-0.22.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6dd9412824c4ce1aca56c47b0991e65bebb7ac3f4edccfd3f156150c96a7bf25"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20070c65396f7373f5df4005862fa162db5d25d56150bddd0b3e8214e8ef45b4"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0b09865a9abc0ddff4e50b5ef65467cd94176bf1e0004184eb915cbc10fc05c5"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3453e8d41fe5f17d1f8e9c383a7473cd46a63661628ec58e07777c2fff7196dc"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f5d36399a1b96e1a5fdc91e0522544580dbebeb1f77f27b2b0ab25559e103b8b"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:009de23c9c9ee54bf11303a966edf4d9087cd43a6003672e6aa7def643d06518"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1aef18820ef3e4587ebe8b3bc9ba6e55892a6d7b93bac6d29d9f631a3b4befbd"}, + {file = "rpds_py-0.22.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f60bd8423be1d9d833f230fdbccf8f57af322d96bcad6599e5a771b151398eb2"}, + {file = "rpds_py-0.22.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:62d9cfcf4948683a18a9aff0ab7e1474d407b7bab2ca03116109f8464698ab16"}, + {file = "rpds_py-0.22.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9253fc214112405f0afa7db88739294295f0e08466987f1d70e29930262b4c8f"}, + {file = "rpds_py-0.22.3-cp313-cp313-win32.whl", hash = "sha256:fb0ba113b4983beac1a2eb16faffd76cb41e176bf58c4afe3e14b9c681f702de"}, + {file = "rpds_py-0.22.3-cp313-cp313-win_amd64.whl", hash = "sha256:c58e2339def52ef6b71b8f36d13c3688ea23fa093353f3a4fee2556e62086ec9"}, + {file = "rpds_py-0.22.3-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:f82a116a1d03628a8ace4859556fb39fd1424c933341a08ea3ed6de1edb0283b"}, + {file = "rpds_py-0.22.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3dfcbc95bd7992b16f3f7ba05af8a64ca694331bd24f9157b49dadeeb287493b"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59259dc58e57b10e7e18ce02c311804c10c5a793e6568f8af4dead03264584d1"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5725dd9cc02068996d4438d397e255dcb1df776b7ceea3b9cb972bdb11260a83"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99b37292234e61325e7a5bb9689e55e48c3f5f603af88b1642666277a81f1fbd"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:27b1d3b3915a99208fee9ab092b8184c420f2905b7d7feb4aeb5e4a9c509b8a1"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f612463ac081803f243ff13cccc648578e2279295048f2a8d5eb430af2bae6e3"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f73d3fef726b3243a811121de45193c0ca75f6407fe66f3f4e183c983573e130"}, + {file = "rpds_py-0.22.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3f21f0495edea7fdbaaa87e633a8689cd285f8f4af5c869f27bc8074638ad69c"}, + {file = "rpds_py-0.22.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:1e9663daaf7a63ceccbbb8e3808fe90415b0757e2abddbfc2e06c857bf8c5e2b"}, + {file = "rpds_py-0.22.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a76e42402542b1fae59798fab64432b2d015ab9d0c8c47ba7addddbaf7952333"}, + {file = "rpds_py-0.22.3-cp313-cp313t-win32.whl", hash = "sha256:69803198097467ee7282750acb507fba35ca22cc3b85f16cf45fb01cb9097730"}, + {file = "rpds_py-0.22.3-cp313-cp313t-win_amd64.whl", hash = "sha256:f5cf2a0c2bdadf3791b5c205d55a37a54025c6e18a71c71f82bb536cf9a454bf"}, + {file = "rpds_py-0.22.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:378753b4a4de2a7b34063d6f95ae81bfa7b15f2c1a04a9518e8644e81807ebea"}, + {file = "rpds_py-0.22.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3445e07bf2e8ecfeef6ef67ac83de670358abf2996916039b16a218e3d95e97e"}, + {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b2513ba235829860b13faa931f3b6846548021846ac808455301c23a101689d"}, + {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eaf16ae9ae519a0e237a0f528fd9f0197b9bb70f40263ee57ae53c2b8d48aeb3"}, + {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:583f6a1993ca3369e0f80ba99d796d8e6b1a3a2a442dd4e1a79e652116413091"}, + {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4617e1915a539a0d9a9567795023de41a87106522ff83fbfaf1f6baf8e85437e"}, + {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c150c7a61ed4a4f4955a96626574e9baf1adf772c2fb61ef6a5027e52803543"}, + {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2fa4331c200c2521512595253f5bb70858b90f750d39b8cbfd67465f8d1b596d"}, + {file = "rpds_py-0.22.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:214b7a953d73b5e87f0ebece4a32a5bd83c60a3ecc9d4ec8f1dca968a2d91e99"}, + {file = "rpds_py-0.22.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f47ad3d5f3258bd7058d2d506852217865afefe6153a36eb4b6928758041d831"}, + {file = "rpds_py-0.22.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f276b245347e6e36526cbd4a266a417796fc531ddf391e43574cf6466c492520"}, + {file = "rpds_py-0.22.3-cp39-cp39-win32.whl", hash = "sha256:bbb232860e3d03d544bc03ac57855cd82ddf19c7a07651a7c0fdb95e9efea8b9"}, + {file = "rpds_py-0.22.3-cp39-cp39-win_amd64.whl", hash = "sha256:cfbc454a2880389dbb9b5b398e50d439e2e58669160f27b60e5eca11f68ae17c"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:d48424e39c2611ee1b84ad0f44fb3b2b53d473e65de061e3f460fc0be5f1939d"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:24e8abb5878e250f2eb0d7859a8e561846f98910326d06c0d51381fed59357bd"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b232061ca880db21fa14defe219840ad9b74b6158adb52ddf0e87bead9e8493"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac0a03221cdb5058ce0167ecc92a8c89e8d0decdc9e99a2ec23380793c4dcb96"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb0c341fa71df5a4595f9501df4ac5abfb5a09580081dffbd1ddd4654e6e9123"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf9db5488121b596dbfc6718c76092fda77b703c1f7533a226a5a9f65248f8ad"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b8db6b5b2d4491ad5b6bdc2bc7c017eec108acbf4e6785f42a9eb0ba234f4c9"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b3d504047aba448d70cf6fa22e06cb09f7cbd761939fdd47604f5e007675c24e"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:e61b02c3f7a1e0b75e20c3978f7135fd13cb6cf551bf4a6d29b999a88830a338"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:e35ba67d65d49080e8e5a1dd40101fccdd9798adb9b050ff670b7d74fa41c566"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:26fd7cac7dd51011a245f29a2cc6489c4608b5a8ce8d75661bb4a1066c52dfbe"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:177c7c0fce2855833819c98e43c262007f42ce86651ffbb84f37883308cb0e7d"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:bb47271f60660803ad11f4c61b42242b8c1312a31c98c578f79ef9387bbde21c"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:70fb28128acbfd264eda9bf47015537ba3fe86e40d046eb2963d75024be4d055"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44d61b4b7d0c2c9ac019c314e52d7cbda0ae31078aabd0f22e583af3e0d79723"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f0e260eaf54380380ac3808aa4ebe2d8ca28b9087cf411649f96bad6900c728"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b25bc607423935079e05619d7de556c91fb6adeae9d5f80868dde3468657994b"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fb6116dfb8d1925cbdb52595560584db42a7f664617a1f7d7f6e32f138cdf37d"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a63cbdd98acef6570c62b92a1e43266f9e8b21e699c363c0fef13bd530799c11"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2b8f60e1b739a74bab7e01fcbe3dddd4657ec685caa04681df9d562ef15b625f"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2e8b55d8517a2fda8d95cb45d62a5a8bbf9dd0ad39c5b25c8833efea07b880ca"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:2de29005e11637e7a2361fa151f780ff8eb2543a0da1413bb951e9f14b699ef3"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:666ecce376999bf619756a24ce15bb14c5bfaf04bf00abc7e663ce17c3f34fe7"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:5246b14ca64a8675e0a7161f7af68fe3e910e6b90542b4bfb5439ba752191df6"}, + {file = "rpds_py-0.22.3.tar.gz", hash = "sha256:e32fee8ab45d3c2db6da19a5323bc3362237c8b653c70194414b892fd06a080d"}, ] [[package]] name = "ruff" -version = "0.8.1" +version = "0.8.3" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.8.1-py3-none-linux_armv6l.whl", hash = "sha256:fae0805bd514066f20309f6742f6ee7904a773eb9e6c17c45d6b1600ca65c9b5"}, - {file = "ruff-0.8.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b8a4f7385c2285c30f34b200ca5511fcc865f17578383db154e098150ce0a087"}, - {file = "ruff-0.8.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cd054486da0c53e41e0086e1730eb77d1f698154f910e0cd9e0d64274979a209"}, - {file = "ruff-0.8.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2029b8c22da147c50ae577e621a5bfbc5d1fed75d86af53643d7a7aee1d23871"}, - {file = "ruff-0.8.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2666520828dee7dfc7e47ee4ea0d928f40de72056d929a7c5292d95071d881d1"}, - {file = "ruff-0.8.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:333c57013ef8c97a53892aa56042831c372e0bb1785ab7026187b7abd0135ad5"}, - {file = "ruff-0.8.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:288326162804f34088ac007139488dcb43de590a5ccfec3166396530b58fb89d"}, - {file = "ruff-0.8.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b12c39b9448632284561cbf4191aa1b005882acbc81900ffa9f9f471c8ff7e26"}, - {file = "ruff-0.8.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:364e6674450cbac8e998f7b30639040c99d81dfb5bbc6dfad69bc7a8f916b3d1"}, - {file = "ruff-0.8.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b22346f845fec132aa39cd29acb94451d030c10874408dbf776af3aaeb53284c"}, - {file = "ruff-0.8.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b2f2f7a7e7648a2bfe6ead4e0a16745db956da0e3a231ad443d2a66a105c04fa"}, - {file = "ruff-0.8.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:adf314fc458374c25c5c4a4a9270c3e8a6a807b1bec018cfa2813d6546215540"}, - {file = "ruff-0.8.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a885d68342a231b5ba4d30b8c6e1b1ee3a65cf37e3d29b3c74069cdf1ee1e3c9"}, - {file = "ruff-0.8.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d2c16e3508c8cc73e96aa5127d0df8913d2290098f776416a4b157657bee44c5"}, - {file = "ruff-0.8.1-py3-none-win32.whl", hash = "sha256:93335cd7c0eaedb44882d75a7acb7df4b77cd7cd0d2255c93b28791716e81790"}, - {file = "ruff-0.8.1-py3-none-win_amd64.whl", hash = "sha256:2954cdbe8dfd8ab359d4a30cd971b589d335a44d444b6ca2cb3d1da21b75e4b6"}, - {file = "ruff-0.8.1-py3-none-win_arm64.whl", hash = "sha256:55873cc1a473e5ac129d15eccb3c008c096b94809d693fc7053f588b67822737"}, - {file = "ruff-0.8.1.tar.gz", hash = "sha256:3583db9a6450364ed5ca3f3b4225958b24f78178908d5c4bc0f46251ccca898f"}, -] - -[[package]] -name = "scipy" -version = "1.13.1" -description = "Fundamental algorithms for scientific computing in Python" -optional = false -python-versions = ">=3.9" -files = [ - {file = "scipy-1.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:20335853b85e9a49ff7572ab453794298bcf0354d8068c5f6775a0eabf350aca"}, - {file = "scipy-1.13.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:d605e9c23906d1994f55ace80e0125c587f96c020037ea6aa98d01b4bd2e222f"}, - {file = "scipy-1.13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfa31f1def5c819b19ecc3a8b52d28ffdcc7ed52bb20c9a7589669dd3c250989"}, - {file = "scipy-1.13.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26264b282b9da0952a024ae34710c2aff7d27480ee91a2e82b7b7073c24722f"}, - {file = "scipy-1.13.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eccfa1906eacc02de42d70ef4aecea45415f5be17e72b61bafcfd329bdc52e94"}, - {file = "scipy-1.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:2831f0dc9c5ea9edd6e51e6e769b655f08ec6db6e2e10f86ef39bd32eb11da54"}, - {file = "scipy-1.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:27e52b09c0d3a1d5b63e1105f24177e544a222b43611aaf5bc44d4a0979e32f9"}, - {file = "scipy-1.13.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:54f430b00f0133e2224c3ba42b805bfd0086fe488835effa33fa291561932326"}, - {file = "scipy-1.13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e89369d27f9e7b0884ae559a3a956e77c02114cc60a6058b4e5011572eea9299"}, - {file = "scipy-1.13.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a78b4b3345f1b6f68a763c6e25c0c9a23a9fd0f39f5f3d200efe8feda560a5fa"}, - {file = "scipy-1.13.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45484bee6d65633752c490404513b9ef02475b4284c4cfab0ef946def50b3f59"}, - {file = "scipy-1.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:5713f62f781eebd8d597eb3f88b8bf9274e79eeabf63afb4a737abc6c84ad37b"}, - {file = "scipy-1.13.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5d72782f39716b2b3509cd7c33cdc08c96f2f4d2b06d51e52fb45a19ca0c86a1"}, - {file = "scipy-1.13.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:017367484ce5498445aade74b1d5ab377acdc65e27095155e448c88497755a5d"}, - {file = "scipy-1.13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:949ae67db5fa78a86e8fa644b9a6b07252f449dcf74247108c50e1d20d2b4627"}, - {file = "scipy-1.13.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de3ade0e53bc1f21358aa74ff4830235d716211d7d077e340c7349bc3542e884"}, - {file = "scipy-1.13.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2ac65fb503dad64218c228e2dc2d0a0193f7904747db43014645ae139c8fad16"}, - {file = "scipy-1.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:cdd7dacfb95fea358916410ec61bbc20440f7860333aee6d882bb8046264e949"}, - {file = "scipy-1.13.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:436bbb42a94a8aeef855d755ce5a465479c721e9d684de76bf61a62e7c2b81d5"}, - {file = "scipy-1.13.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:8335549ebbca860c52bf3d02f80784e91a004b71b059e3eea9678ba994796a24"}, - {file = "scipy-1.13.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d533654b7d221a6a97304ab63c41c96473ff04459e404b83275b60aa8f4b7004"}, - {file = "scipy-1.13.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637e98dcf185ba7f8e663e122ebf908c4702420477ae52a04f9908707456ba4d"}, - {file = "scipy-1.13.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a014c2b3697bde71724244f63de2476925596c24285c7a637364761f8710891c"}, - {file = "scipy-1.13.1-cp39-cp39-win_amd64.whl", hash = "sha256:392e4ec766654852c25ebad4f64e4e584cf19820b980bc04960bca0b0cd6eaa2"}, - {file = "scipy-1.13.1.tar.gz", hash = "sha256:095a87a0312b08dfd6a6155cbbd310a8c51800fc931b8c0b84003014b874ed3c"}, + {file = "ruff-0.8.3-py3-none-linux_armv6l.whl", hash = "sha256:8d5d273ffffff0acd3db5bf626d4b131aa5a5ada1276126231c4174543ce20d6"}, + {file = "ruff-0.8.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e4d66a21de39f15c9757d00c50c8cdd20ac84f55684ca56def7891a025d7e939"}, + {file = "ruff-0.8.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c356e770811858bd20832af696ff6c7e884701115094f427b64b25093d6d932d"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c0a60a825e3e177116c84009d5ebaa90cf40dfab56e1358d1df4e29a9a14b13"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fb782f4db39501210ac093c79c3de581d306624575eddd7e4e13747e61ba18"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f26bc76a133ecb09a38b7868737eded6941b70a6d34ef53a4027e83913b6502"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:01b14b2f72a37390c1b13477c1c02d53184f728be2f3ffc3ace5b44e9e87b90d"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53babd6e63e31f4e96ec95ea0d962298f9f0d9cc5990a1bbb023a6baf2503a82"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ae441ce4cf925b7f363d33cd6570c51435972d697e3e58928973994e56e1452"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7c65bc0cadce32255e93c57d57ecc2cca23149edd52714c0c5d6fa11ec328cd"}, + {file = "ruff-0.8.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5be450bb18f23f0edc5a4e5585c17a56ba88920d598f04a06bd9fd76d324cb20"}, + {file = "ruff-0.8.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8faeae3827eaa77f5721f09b9472a18c749139c891dbc17f45e72d8f2ca1f8fc"}, + {file = "ruff-0.8.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:db503486e1cf074b9808403991663e4277f5c664d3fe237ee0d994d1305bb060"}, + {file = "ruff-0.8.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6567be9fb62fbd7a099209257fef4ad2c3153b60579818b31a23c886ed4147ea"}, + {file = "ruff-0.8.3-py3-none-win32.whl", hash = "sha256:19048f2f878f3ee4583fc6cb23fb636e48c2635e30fb2022b3a1cd293402f964"}, + {file = "ruff-0.8.3-py3-none-win_amd64.whl", hash = "sha256:f7df94f57d7418fa7c3ffb650757e0c2b96cf2501a0b192c18e4fb5571dfada9"}, + {file = "ruff-0.8.3-py3-none-win_arm64.whl", hash = "sha256:fe2756edf68ea79707c8d68b78ca9a58ed9af22e430430491ee03e718b5e4936"}, + {file = "ruff-0.8.3.tar.gz", hash = "sha256:5e7558304353b84279042fc584a4f4cb8a07ae79b2bf3da1a7551d960b5626d3"}, ] -[package.dependencies] -numpy = ">=1.22.4,<2.3" - -[package.extras] -dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy", "pycodestyle", "pydevtool", "rich-click", "ruff", "types-psutil", "typing_extensions"] -doc = ["jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.12.0)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0)", "sphinx-design (>=0.4.0)"] -test = ["array-api-strict", "asv", "gmpy2", "hypothesis (>=6.30)", "mpmath", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] - [[package]] name = "scipy" version = "1.14.1" @@ -3967,13 +3945,13 @@ type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (>=1.12 [[package]] name = "six" -version = "1.16.0" +version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] [[package]] @@ -4095,7 +4073,6 @@ babel = ">=2.13" colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""} docutils = ">=0.20,<0.22" imagesize = ">=1.3" -importlib-metadata = {version = ">=6.0", markers = "python_version < \"3.10\""} Jinja2 = ">=3.1" packaging = ">=23.0" Pygments = ">=2.17" @@ -4324,7 +4301,7 @@ files = [ name = "stack-data" version = "0.6.3" description = "Extract data from python stack frames and tracebacks for informative displays" -optional = true +optional = false python-versions = "*" files = [ {file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"}, @@ -4475,7 +4452,7 @@ telegram = ["requests"] name = "traitlets" version = "5.14.3" description = "Traitlets Python configuration system" -optional = true +optional = false python-versions = ">=3.8" files = [ {file = "traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f"}, @@ -4536,13 +4513,13 @@ types-setuptools = "*" [[package]] name = "types-python-dateutil" -version = "2.9.0.20241003" +version = "2.9.0.20241206" description = "Typing stubs for python-dateutil" optional = true python-versions = ">=3.8" files = [ - {file = "types-python-dateutil-2.9.0.20241003.tar.gz", hash = "sha256:58cb85449b2a56d6684e41aeefb4c4280631246a0da1a719bdbe6f3fb0317446"}, - {file = "types_python_dateutil-2.9.0.20241003-py3-none-any.whl", hash = "sha256:250e1d8e80e7bbc3a6c99b907762711d1a1cdd00e978ad39cb5940f6f0a87f3d"}, + {file = "types_python_dateutil-2.9.0.20241206-py3-none-any.whl", hash = "sha256:e248a4bc70a486d3e3ec84d0dc30eec3a5f979d6e7ee4123ae043eedbb987f53"}, + {file = "types_python_dateutil-2.9.0.20241206.tar.gz", hash = "sha256:18f493414c26ffba692a72369fea7a154c502646301ebfe3d56a04b3767284cb"}, ] [[package]] @@ -4664,7 +4641,7 @@ watchmedo = ["PyYAML (>=3.10)"] name = "wcwidth" version = "0.2.13" description = "Measures the displayed width of unicode strings in a terminal" -optional = true +optional = false python-versions = "*" files = [ {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, @@ -4783,30 +4760,11 @@ files = [ {file = "wrapt-1.17.0.tar.gz", hash = "sha256:16187aa2317c731170a88ef35e8937ae0f533c402872c1ee5e6d079fcf320801"}, ] -[[package]] -name = "zipp" -version = "3.21.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false -python-versions = ">=3.9" -files = [ - {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"}, - {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"}, -] - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] -type = ["pytest-mypy"] - [extras] gui = ["dearpygui"] jupyterlab = ["jupyterlab", "notebook"] [metadata] lock-version = "2.0" -python-versions = ">=3.9" -content-hash = "06dc9ff9e7fe83005f978bd3e555c37073b542161a645ee1c83990d8f1445119" +python-versions = ">=3.10" +content-hash = "2775ea53971d275f93c6c997d95f09f8d520ae1b30c5705ad810f77719605926" diff --git a/pyproject.toml b/pyproject.toml index ffe54554a3..1678a6a50c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,6 @@ classifiers= [ "Topic :: Scientific/Engineering", "Topic :: Multimedia :: Video", "Topic :: Multimedia :: Graphics", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -27,13 +26,14 @@ packages = [ ] [tool.poetry.dependencies] -python = ">=3.9" +python = ">=3.10" av = ">=9.0.0,<14.0.0" # v14.0.0 contains breaking changes, remove after dropping python 3.9 click = ">=8.0" cloup = ">=2.0.0" dearpygui = { version = ">=1.0.0", optional = true } decorator = ">=4.3.2" importlib-metadata = {version = ">=3.6", python = "<=3.9"} # Required to discover plugins +ipython = "^8.7.0" isosurfaces = ">=0.1.0" jupyterlab = { version = ">=3.0.0", optional = true } manimpango = ">=0.5.0,<1.0.0" # Complete API change in 1.0.0 @@ -50,7 +50,7 @@ numpy = [ {version = ">=2.0", python = "<3.10"}, ] Pillow = ">=9.1" -pycairo = ">=1.13,<2.0.0" +pyopengl = "^3.1.6" pydub = ">=0.20.0" audioop-lts = { version = ">=0.2.0", python = ">=3.13" } # for pydub Pygments = ">=2.0.0" @@ -68,6 +68,7 @@ svgelements = ">=1.8.0" tqdm = ">=4.0.0" typing-extensions = ">=4.0.0" watchdog = ">=2.0.0" +pydantic = "^2.8.0" [tool.poetry.extras] jupyterlab = ["jupyterlab", "notebook"] @@ -106,6 +107,7 @@ sphinx-reredirects = "^0.1.5" [tool.pytest.ini_options] markers = "slow: Mark the test as slow. Can be skipped with --skip_slow" addopts = "--no-cov-on-fail --cov=manim --cov-report xml --cov-report term -n auto --dist=loadfile --durations=0" +doctest_optionflags = "IGNORE_EXCEPTION_DETAIL" [tool.isort] profile = "black" @@ -169,6 +171,8 @@ ignore = [ # due to the import * used in manim "F403", "F405", + # generic type: ignore + "PGH003", # fixtures not returning anything should have leading underscore "PT004", # Exception too broad (this would require lots of changes + re.escape) for little benefit diff --git a/tests/conftest.py b/tests/conftest.py index 4de34bbbc1..8dd7de17d9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,7 +4,6 @@ import sys from pathlib import Path -import cairo import moderngl import pytest @@ -20,7 +19,6 @@ def pytest_report_header(config): raise Exception("Error while creating moderngl context") from e return ( - f"\nCairo Version: {cairo.cairo_version()}", "\nOpenGL information", "------------------", f"vendor: {info['GL_VENDOR'].strip()}", @@ -118,10 +116,15 @@ def reset_cfg_file(): @pytest.fixture -def using_opengl_renderer(config): - """Standard fixture for running with opengl that makes tests use a standard_config.cfg with a temp dir.""" - config.renderer = "opengl" - yield - # as a special case needed to manually revert back to cairo - # due to side effects of setting the renderer - config.renderer = "cairo" +def using_opengl_renderer(): + """Standard fixture for running with opengl that makes tests use a standard_config.cfg with a temp dir. + + .. warning:: + + As of experimental, this fixture is deprecated and should not be using + """ + + +@pytest.fixture +def write_to_movie(config): + config.write_to_movie = True diff --git a/tests/experimental/test_vmobject_init.py b/tests/experimental/test_vmobject_init.py new file mode 100644 index 0000000000..92d47cf729 --- /dev/null +++ b/tests/experimental/test_vmobject_init.py @@ -0,0 +1,14 @@ +from manim import manim_colors as col +from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject + + +def test_vmobject_init(): + vm = OpenGLVMobject() + assert vm.fill_color == [col.WHITE] + assert vm.stroke_color == [col.WHITE] + vm = OpenGLVMobject(color=col.RED) + assert vm.fill_color == [col.RED] + assert vm.stroke_color == [col.RED] + vm = OpenGLVMobject(fill_color=col.GREEN, stroke_color=col.YELLOW) + assert vm.fill_color == [col.GREEN] + assert vm.stroke_color == [col.YELLOW] diff --git a/tests/helpers/graphical_units.py b/tests/helpers/graphical_units.py index 1395559e52..060bbfe2ab 100644 --- a/tests/helpers/graphical_units.py +++ b/tests/helpers/graphical_units.py @@ -8,6 +8,7 @@ import numpy as np +from manim import Manager from manim.scene.scene import Scene logger = logging.getLogger("manim") @@ -31,29 +32,29 @@ def set_test_scene(scene_object: type[Scene], module_name: str, config): set_test_scene(DotTest, "geometry") """ - config["write_to_movie"] = False - config["disable_caching"] = True - config["format"] = "png" - config["pixel_height"] = 480 - config["pixel_width"] = 854 - config["frame_rate"] = 15 + config.write_to_movie = False + config.disable_caching = True + config.format = "png" + config.pixel_height = 480 + config.pixel_width = 854 + config.frame_rate = 15 with tempfile.TemporaryDirectory() as tmpdir: temp_path = Path(tmpdir) config["text_dir"] = temp_path / "text" config["tex_dir"] = temp_path / "tex" - scene = scene_object(skip_animations=True) - scene.render() - data = scene.renderer.get_frame() + manager = Manager(scene_object) + manager.render() + data = manager.renderer.get_pixels() assert not np.all( data == np.array([0, 0, 0, 255]), - ), f"Control data generated for {scene!s} only contains empty pixels." + ), f"Control data generated for {manager.scene!s} only contains empty pixels." assert data.shape == (480, 854, 4) tests_directory = Path(__file__).absolute().parent.parent path_control_data = Path(tests_directory) / "control_data" / "graphical_units_data" path = Path(path_control_data) / module_name if not path.is_dir(): path.mkdir(parents=True) - np.savez_compressed(path / str(scene), frame_data=data) - logger.info(f"Test data for {str(scene)} saved in {path}\n") + np.savez_compressed(path / str(manager.scene), frame_data=data) + logger.info(f"Test data for {str(manager.scene)} saved in {path}\n") diff --git a/tests/module/animation/test_animation.py b/tests/module/animation/test_animation.py index 5991aab074..731f8eb1a0 100644 --- a/tests/module/animation/test_animation.py +++ b/tests/module/animation/test_animation.py @@ -2,11 +2,12 @@ import pytest -from manim import FadeIn, Scene +from manim import FadeIn, Manager, Scene def test_animation_zero_total_run_time(): - test_scene = Scene() + manager = Manager(Scene) + test_scene = manager.scene with pytest.raises( ValueError, match="The total run_time must be a positive number." ): @@ -14,7 +15,8 @@ def test_animation_zero_total_run_time(): def test_single_animation_zero_run_time_with_more_animations(): - test_scene = Scene() + manager = Manager(Scene) + test_scene = manager.scene test_scene.play(FadeIn(None, run_time=0), FadeIn(None, run_time=1)) @@ -24,34 +26,39 @@ def test_animation_negative_run_time(): def test_animation_run_time_shorter_than_frame_rate(manim_caplog, config): - test_scene = Scene() + manager = Manager(Scene) + test_scene = manager.scene test_scene.play(FadeIn(None, run_time=1 / (config.frame_rate + 1))) assert "too short for the current frame rate" in manim_caplog.text @pytest.mark.parametrize("duration", [0, -1]) def test_wait_invalid_duration(duration): - test_scene = Scene() + manager = Manager(Scene) + test_scene = manager.scene with pytest.raises(ValueError, match="The duration must be a positive number."): test_scene.wait(duration) @pytest.mark.parametrize("frozen_frame", [False, True]) def test_wait_duration_shorter_than_frame_rate(manim_caplog, frozen_frame): - test_scene = Scene() + manager = Manager(Scene) + test_scene = manager.scene test_scene.wait(1e-9, frozen_frame=frozen_frame) assert "too short for the current frame rate" in manim_caplog.text @pytest.mark.parametrize("duration", [0, -1]) def test_pause_invalid_duration(duration): - test_scene = Scene() + manager = Manager(Scene) + test_scene = manager.scene with pytest.raises(ValueError, match="The duration must be a positive number."): test_scene.pause(duration) @pytest.mark.parametrize("max_time", [0, -1]) def test_wait_until_invalid_max_time(max_time): - test_scene = Scene() + manager = Manager(Scene) + test_scene = manager.scene with pytest.raises(ValueError, match="The max_time must be a positive number."): test_scene.wait_until(lambda: True, max_time) diff --git a/tests/module/animation/test_composition.py b/tests/module/animation/test_composition.py index 8c5f044b2a..172624b372 100644 --- a/tests/module/animation/test_composition.py +++ b/tests/module/animation/test_composition.py @@ -4,15 +4,24 @@ import pytest -from manim.animation.animation import Animation, Wait -from manim.animation.composition import AnimationGroup, Succession -from manim.animation.creation import Create, Write -from manim.animation.fading import FadeIn, FadeOut -from manim.constants import DOWN, UP -from manim.mobject.geometry.arc import Circle -from manim.mobject.geometry.line import Line -from manim.mobject.geometry.polygram import RegularPolygon, Square -from manim.scene.scene import Scene +from manim import ( + DOWN, + UP, + Animation, + AnimationGroup, + Circle, + Create, + FadeIn, + FadeOut, + Line, + Manager, + RegularPolygon, + Scene, + Square, + Succession, + Wait, + Write, +) def test_succession_timing(): @@ -22,7 +31,6 @@ 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(MagicMock()) succession.begin() assert succession.active_index == 0 # The first animation takes 20% of the total run time. @@ -54,7 +62,6 @@ def test_succession_in_succession_timing(): ) assert nested_succession.get_run_time() == 5.0 assert succession.get_run_time() == 10.0 - succession._setup_scene(MagicMock()) succession.begin() succession.interpolate(0.1) assert succession.active_index == 0 @@ -138,7 +145,8 @@ def test_animationgroup_with_wait(): def test_animationgroup_is_passing_remover_to_animations( animation_remover, animation_group_remover ): - scene = Scene() + manager = Manager(Scene) + scene = manager.scene sqr_animation = Create(Square(), remover=animation_remover) circ_animation = Write(Circle(), remover=animation_remover) animation_group = AnimationGroup( @@ -153,7 +161,8 @@ def test_animationgroup_is_passing_remover_to_animations( def test_animationgroup_is_passing_remover_to_nested_animationgroups(): - scene = Scene() + manager = Manager(Scene) + scene = manager.scene sqr_animation = Create(Square()) circ_animation = Write(Circle(), remover=True) polygon_animation = Create(RegularPolygon(5)) diff --git a/tests/module/mobject/mobject/test_mobject.py b/tests/module/mobject/mobject/test_mobject.py index 89deea93c9..2166b0d858 100644 --- a/tests/module/mobject/mobject/test_mobject.py +++ b/tests/module/mobject/mobject/test_mobject.py @@ -144,13 +144,15 @@ def test_mobject_dimensions_mobjects_with_no_points_are_at_origin(): assert outer_group.width == 2 assert outer_group.height == 3 - # Adding a mobject with no points has a quirk of adding a "point" - # to [0, 0, 0] (the origin). This changes the size of the outer - # group because now the bottom left corner is at [-5, -6.5, 0] - # but the upper right corner is [0, 0, 0] instead of [-3, -3.5, 0] + # TODO: remove the following 8 lines? + # Originally, adding a mobject with no points had a quirk of adding a + # "point" to [0, 0, 0] (the origin). This changed the size of the outer + # group, because the bottom was corner is at [-5, -6.5, 0], but the + # upper right corner became [0, 0, 0] instead of [-3, -3.5, 0]. + # However, this no longer happens. outer_group.add(VGroup()) - assert outer_group.width == 5 - assert outer_group.height == 6.5 + assert outer_group.width == 2 + assert outer_group.height == 3 def test_mobject_dimensions_has_points_and_children(): diff --git a/tests/module/mobject/test_boolean_ops.py b/tests/module/mobject/test_boolean_ops.py index b3560e87fa..368fcc3e2a 100644 --- a/tests/module/mobject/test_boolean_ops.py +++ b/tests/module/mobject/test_boolean_ops.py @@ -1,11 +1,17 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import numpy as np import pytest from manim import Circle, Square from manim.mobject.geometry.boolean_ops import _BooleanOps +if TYPE_CHECKING: + from manim.mobject.types.vectorized_mobject import VMobject + from manim.typing import Point2D_Array, Point3D_Array + @pytest.mark.parametrize( ("test_input", "expected"), @@ -25,7 +31,9 @@ ), ], ) -def test_convert_2d_to_3d_array(test_input, expected): +def test_convert_2d_to_3d_array( + test_input: Point2D_Array, expected: Point3D_Array +) -> None: a = _BooleanOps() result = a._convert_2d_to_3d_array(test_input) assert len(result) == len(expected) @@ -33,7 +41,7 @@ def test_convert_2d_to_3d_array(test_input, expected): assert (result[i] == expected[i]).all() -def test_convert_2d_to_3d_array_zdim(): +def test_convert_2d_to_3d_array_zdim() -> None: a = _BooleanOps() result = a._convert_2d_to_3d_array([(1.0, 2.0)], z_dim=1.0) assert (result[0] == np.array([1.0, 2.0, 1.0])).all() @@ -48,11 +56,12 @@ def test_convert_2d_to_3d_array_zdim(): Circle(radius=3), ], ) -def test_vmobject_to_skia_path_and_inverse(test_input): +def test_vmobject_to_skia_path_and_inverse(test_input: VMobject) -> None: a = _BooleanOps() path = a._convert_vmobject_to_skia_path(test_input) assert len(list(path.segments)) > 1 new_vmobject = a._convert_skia_path_to_vmobject(path) - # for some reason there is an extra 4 points in new vmobject than original - np.testing.assert_allclose(new_vmobject.points[:-4], test_input.points) + # For some reason, there are 3 more points in the new VMobject than in the + # original input. + np.testing.assert_allclose(new_vmobject.points[:-3], test_input.points) diff --git a/tests/module/mobject/test_graph.py b/tests/module/mobject/test_graph.py index b23ccff622..fd75bde432 100644 --- a/tests/module/mobject/test_graph.py +++ b/tests/module/mobject/test_graph.py @@ -2,7 +2,7 @@ import pytest -from manim import DiGraph, Graph, Scene, Text, tempconfig +from manim import DiGraph, Graph, Manager, Scene, Text, tempconfig from manim.mobject.graph import _layouts @@ -93,7 +93,8 @@ def test_graph_remove_edges(): def test_custom_animation_mobject_list(): G = Graph([1, 2, 3], [(1, 2), (2, 3)]) - scene = Scene() + manager = Manager(Scene) + scene = manager.scene scene.add(G) assert scene.mobjects == [G] with tempconfig({"dry_run": True, "quality": "low_quality"}): diff --git a/tests/module/mobject/types/vectorized_mobject/test_vectorized_mobject.py b/tests/module/mobject/types/vectorized_mobject/test_vectorized_mobject.py index 4d604f2dfb..bf70e8760e 100644 --- a/tests/module/mobject/types/vectorized_mobject/test_vectorized_mobject.py +++ b/tests/module/mobject/types/vectorized_mobject/test_vectorized_mobject.py @@ -8,51 +8,52 @@ CurvesAsSubmobjects, Line, Mobject, + OpenGLMobject, Polygon, RegularPolygon, Square, VDict, VGroup, - VMobject, ) from manim.constants import PI +from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject def test_vmobject_add(): + def get_type_error_message(invalid_obj, invalid_indices): + return ( + f"Only values of type OpenGLVMobject can be added " + "as submobjects of OpenGLVMobject, but the value " + f"{repr(invalid_obj)} (at index {invalid_indices[1]}) " + f"is of type " + f"{type(invalid_obj).__name__}." + ) + """Test the VMobject add method.""" - obj = VMobject() + obj = OpenGLVMobject() assert len(obj.submobjects) == 0 - obj.add(VMobject()) + obj.add(OpenGLVMobject()) assert len(obj.submobjects) == 1 # Can't add non-VMobject values to a VMobject. with pytest.raises(TypeError) as add_int_info: obj.add(3) - assert str(add_int_info.value) == ( - "Only values of type VMobject can be added as submobjects of VMobject, " - "but the value 3 (at index 0) is of type int." - ) + assert str(add_int_info.value) == (get_type_error_message(3, [0, 0])) assert len(obj.submobjects) == 1 # Plain Mobjects can't be added to a VMobject if they're not # VMobjects. Suggest adding them into a Group instead. with pytest.raises(TypeError) as add_mob_info: obj.add(Mobject()) - assert str(add_mob_info.value) == ( - "Only values of type VMobject can be added as submobjects of VMobject, " - "but the value Mobject (at index 0) is of type Mobject. You can try " - "adding this value into a Group instead." - ) + assert str(add_mob_info.value) == (get_type_error_message(Mobject(), [0, 0])) assert len(obj.submobjects) == 1 with pytest.raises(TypeError) as add_vmob_and_mob_info: # If only one of the added objects is not an instance of VMobject, none of them should be added - obj.add(VMobject(), Mobject()) + obj.add(OpenGLVMobject(), Mobject()) assert str(add_vmob_and_mob_info.value) == ( - "Only values of type VMobject can be added as submobjects of VMobject, " - "but the value Mobject (at index 1) is of type Mobject. You can try " - "adding this value into a Group instead." + get_type_error_message(Mobject(), [0, 1]) ) assert len(obj.submobjects) == 1 @@ -60,13 +61,13 @@ def test_vmobject_add(): with pytest.raises(ValueError) as add_self_info: obj.add(obj) assert str(add_self_info.value) == ( - "Cannot add VMobject as a submobject of itself (at index 0)." + "Cannot add OpenGLVMobject as a submobject of itself (at index 0)." ) assert len(obj.submobjects) == 1 def test_vmobject_point_from_proportion(): - obj = VMobject() + obj = OpenGLVMobject() # One long line, one short line obj.set_points_as_corners( @@ -97,7 +98,7 @@ def test_curves_as_submobjects_point_from_proportion(): with pytest.raises(Exception, match="with no submobjects"): obj.point_from_proportion(0) - obj.add(VMobject()) + obj.add(OpenGLVMobject()) with pytest.raises(Exception, match="have no points"): obj.point_from_proportion(0) @@ -108,7 +109,7 @@ def test_curves_as_submobjects_point_from_proportion(): np.array([4, 0, 0]), ], ) - obj.add(VMobject()) + obj.add(OpenGLVMobject()) # submobject[1] is a line of length 2 obj.submobjects[1].set_points_as_corners( [ @@ -124,30 +125,30 @@ def test_curves_as_submobjects_point_from_proportion(): def test_vgroup_init(): """Test the VGroup instantiation.""" VGroup() - VGroup(VMobject()) - VGroup(VMobject(), VMobject()) + VGroup(OpenGLVMobject()) + VGroup(OpenGLVMobject(), OpenGLVMobject()) # A VGroup cannot contain non-VMobject values. with pytest.raises(TypeError) as init_with_float_info: VGroup(3.0) assert str(init_with_float_info.value) == ( - "Only values of type VMobject can be added as submobjects of VGroup, " + "Only values of type OpenGLVMobject can be added as submobjects of VGroup, " "but the value 3.0 (at index 0 of parameter 0) is of type float." ) with pytest.raises(TypeError) as init_with_mob_info: - VGroup(Mobject()) + VGroup(OpenGLMobject()) assert str(init_with_mob_info.value) == ( - "Only values of type VMobject can be added as submobjects of VGroup, " - "but the value Mobject (at index 0 of parameter 0) is of type Mobject. You can try " + "Only values of type OpenGLVMobject can be added as submobjects of VGroup, " + "but the value OpenGLMobject (at index 0 of parameter 0) is of type OpenGLMobject. You can try " "adding this value into a Group instead." ) with pytest.raises(TypeError) as init_with_vmob_and_mob_info: - VGroup(VMobject(), Mobject()) + VGroup(OpenGLVMobject(), OpenGLMobject()) assert str(init_with_vmob_and_mob_info.value) == ( - "Only values of type VMobject can be added as submobjects of VGroup, " - "but the value Mobject (at index 0 of parameter 1) is of type Mobject. You can try " + "Only values of type OpenGLVMobject can be added as submobjects of VGroup, " + "but the value OpenGLMobject (at index 0 of parameter 1) is of type OpenGLMobject. You can try " "adding this value into a Group instead." ) @@ -164,36 +165,40 @@ def mixed_type_generator(major_type, minor_type, minor_type_positions, n): for i in range(n) ) - obj = VGroup(VMobject()) + obj = VGroup(OpenGLVMobject()) assert len(obj.submobjects) == 1 - obj = VGroup(type_generator(VMobject, 38)) + obj = VGroup(type_generator(OpenGLVMobject, 38)) assert len(obj.submobjects) == 38 - obj = VGroup(VMobject(), [VMobject(), VMobject()], type_generator(VMobject, 38)) + obj = VGroup( + OpenGLVMobject(), + [OpenGLVMobject(), OpenGLVMobject()], + type_generator(OpenGLVMobject, 38), + ) assert len(obj.submobjects) == 41 # A VGroup cannot be initialised with an iterable containing a Mobject with pytest.raises(TypeError) as init_with_mob_iterable: - VGroup(type_generator(Mobject, 5)) + VGroup(type_generator(OpenGLMobject, 5)) assert str(init_with_mob_iterable.value) == ( - "Only values of type VMobject can be added as submobjects of VGroup, " - "but the value Mobject (at index 0 of parameter 0) is of type Mobject." + "Only values of type OpenGLVMobject can be added as submobjects of VGroup, " + "but the value OpenGLMobject (at index 0 of parameter 0) is of type OpenGLMobject." ) # A VGroup cannot be initialised with an iterable containing a Mobject in any position with pytest.raises(TypeError) as init_with_mobs_and_vmobs_iterable: - VGroup(mixed_type_generator(VMobject, Mobject, [3, 5], 7)) + VGroup(mixed_type_generator(OpenGLVMobject, OpenGLMobject, [3, 5], 7)) assert str(init_with_mobs_and_vmobs_iterable.value) == ( - "Only values of type VMobject can be added as submobjects of VGroup, " - "but the value Mobject (at index 3 of parameter 0) is of type Mobject." + "Only values of type OpenGLVMobject can be added as submobjects of VGroup, " + "but the value OpenGLMobject (at index 3 of parameter 0) is of type OpenGLMobject." ) # A VGroup cannot be initialised with an iterable containing non VMobject's in any position with pytest.raises(TypeError) as init_with_float_and_vmobs_iterable: - VGroup(mixed_type_generator(VMobject, float, [6, 7], 9)) + VGroup(mixed_type_generator(OpenGLVMobject, float, [6, 7], 9)) assert str(init_with_float_and_vmobs_iterable.value) == ( - "Only values of type VMobject can be added as submobjects of VGroup, " + "Only values of type OpenGLVMobject can be added as submobjects of VGroup, " "but the value 0.0 (at index 6 of parameter 0) is of type float." ) @@ -203,14 +208,14 @@ def test_vgroup_add(): obj = VGroup() assert len(obj.submobjects) == 0 - obj.add(VMobject()) + obj.add(OpenGLVMobject()) assert len(obj.submobjects) == 1 # Can't add non-VMobject values to a VMobject or VGroup. with pytest.raises(TypeError) as add_int_info: obj.add(3) assert str(add_int_info.value) == ( - "Only values of type VMobject can be added as submobjects of VGroup, " + "Only values of type OpenGLVMobject can be added as submobjects of VGroup, " "but the value 3 (at index 0 of parameter 0) is of type int." ) assert len(obj.submobjects) == 1 @@ -218,20 +223,20 @@ def test_vgroup_add(): # Plain Mobjects can't be added to a VMobject or VGroup if they're not # VMobjects. Suggest adding them into a Group instead. with pytest.raises(TypeError) as add_mob_info: - obj.add(Mobject()) + obj.add(OpenGLMobject()) assert str(add_mob_info.value) == ( - "Only values of type VMobject can be added as submobjects of VGroup, " - "but the value Mobject (at index 0 of parameter 0) is of type Mobject. You can try " + "Only values of type OpenGLVMobject can be added as submobjects of VGroup, " + "but the value OpenGLMobject (at index 0 of parameter 0) is of type OpenGLMobject. You can try " "adding this value into a Group instead." ) assert len(obj.submobjects) == 1 with pytest.raises(TypeError) as add_vmob_and_mob_info: # If only one of the added objects is not an instance of VMobject, none of them should be added - obj.add(VMobject(), Mobject()) + obj.add(OpenGLVMobject(), OpenGLMobject()) assert str(add_vmob_and_mob_info.value) == ( - "Only values of type VMobject can be added as submobjects of VGroup, " - "but the value Mobject (at index 0 of parameter 1) is of type Mobject. You can try " + "Only values of type OpenGLVMobject can be added as submobjects of VGroup, " + "but the value OpenGLMobject (at index 0 of parameter 1) is of type OpenGLMobject. You can try " "adding this value into a Group instead." ) assert len(obj.submobjects) == 1 @@ -249,16 +254,16 @@ def test_vgroup_add_dunder(): """Test the VGroup __add__ magic method.""" obj = VGroup() assert len(obj.submobjects) == 0 - obj + VMobject() + obj + OpenGLVMobject() assert len(obj.submobjects) == 0 - obj += VMobject() + obj += OpenGLVMobject() assert len(obj.submobjects) == 1 with pytest.raises(TypeError): obj += Mobject() assert len(obj.submobjects) == 1 with pytest.raises(TypeError): # If only one of the added object is not an instance of VMobject, none of them should be added - obj += (VMobject(), Mobject()) + obj += (OpenGLVMobject(), Mobject()) assert len(obj.submobjects) == 1 with pytest.raises(ValueError): # a Mobject cannot contain itself @@ -267,8 +272,8 @@ def test_vgroup_add_dunder(): def test_vgroup_remove(): """Test the VGroup remove method.""" - a = VMobject() - c = VMobject() + a = OpenGLVMobject() + c = OpenGLVMobject() b = VGroup(c) obj = VGroup(a, b) assert len(obj.submobjects) == 2 @@ -283,8 +288,8 @@ def test_vgroup_remove(): def test_vgroup_remove_dunder(): """Test the VGroup __sub__ magic method.""" - a = VMobject() - c = VMobject() + a = OpenGLVMobject() + c = OpenGLVMobject() b = VGroup(c) obj = VGroup(a, b) assert len(obj.submobjects) == 2 @@ -301,7 +306,7 @@ def test_vgroup_remove_dunder(): def test_vmob_add_to_back(): """Test the Mobject add_to_back method.""" - a = VMobject() + a = OpenGLVMobject() b = Line() c = "text" with pytest.raises(ValueError): @@ -334,11 +339,11 @@ def test_vdict_init(): # Test empty VDict VDict() # Test VDict made from list of pairs - VDict([("a", VMobject()), ("b", VMobject()), ("c", VMobject())]) + VDict([("a", OpenGLVMobject()), ("b", OpenGLVMobject()), ("c", OpenGLVMobject())]) # Test VDict made from a python dict - VDict({"a": VMobject(), "b": VMobject(), "c": VMobject()}) + VDict({"a": OpenGLVMobject(), "b": OpenGLVMobject(), "c": OpenGLVMobject()}) # Test VDict made using zip - VDict(zip(["a", "b", "c"], [VMobject(), VMobject(), VMobject()])) + VDict(zip(["a", "b", "c"], [OpenGLVMobject(), OpenGLVMobject(), OpenGLVMobject()])) # If the value is of type Mobject, must raise a TypeError with pytest.raises(TypeError): VDict({"a": Mobject()}) @@ -348,7 +353,7 @@ def test_vdict_add(): """Test the VDict add method.""" obj = VDict() assert len(obj.submob_dict) == 0 - obj.add([("a", VMobject())]) + obj.add([("a", OpenGLVMobject())]) assert len(obj.submob_dict) == 1 with pytest.raises(TypeError): obj.add([("b", Mobject())]) @@ -356,7 +361,7 @@ def test_vdict_add(): def test_vdict_remove(): """Test the VDict remove method.""" - obj = VDict([("a", VMobject())]) + obj = VDict([("a", OpenGLVMobject())]) assert len(obj.submob_dict) == 1 obj.remove("a") assert len(obj.submob_dict) == 0 @@ -366,8 +371,8 @@ def test_vdict_remove(): def test_vgroup_supports_item_assigment(): """Test VGroup supports array-like assignment for VMObjects""" - a = VMobject() - b = VMobject() + a = OpenGLVMobject() + b = OpenGLVMobject() vgroup = VGroup(a) assert vgroup[0] == a vgroup[0] = b @@ -380,8 +385,8 @@ def test_vgroup_item_assignment_at_correct_position(): n_items = 10 vgroup = VGroup() for _i in range(n_items): - vgroup.add(VMobject()) - new_obj = VMobject() + vgroup.add(OpenGLVMobject()) + new_obj = OpenGLVMobject() vgroup[6] = new_obj assert vgroup[6] == new_obj assert len(vgroup) == n_items @@ -389,17 +394,17 @@ def test_vgroup_item_assignment_at_correct_position(): def test_vgroup_item_assignment_only_allows_vmobjects(): """Test VGroup item-assignment raises TypeError when invalid type is passed""" - vgroup = VGroup(VMobject()) + vgroup = VGroup(OpenGLVMobject()) with pytest.raises(TypeError) as assign_str_info: vgroup[0] = "invalid object" assert str(assign_str_info.value) == ( - "Only values of type VMobject can be added as submobjects of VGroup, " + "Only values of type OpenGLVMobject can be added as submobjects of VGroup, " "but the value invalid object (at index 0) is of type str." ) def test_trim_dummy(): - o = VMobject() + o = OpenGLVMobject() o.start_new_path(np.array([0, 0, 0])) o.add_line_to(np.array([1, 0, 0])) o.add_line_to(np.array([2, 0, 0])) @@ -407,7 +412,7 @@ def test_trim_dummy(): o.start_new_path(np.array([0, 1, 0])) o.add_line_to(np.array([1, 2, 0])) - o2 = VMobject() + o2 = OpenGLVMobject() o2.start_new_path(np.array([0, 0, 0])) o2.add_line_to(np.array([0, 1, 0])) o2.start_new_path(np.array([1, 0, 0])) @@ -415,7 +420,7 @@ def test_trim_dummy(): o2.add_line_to(np.array([1, 2, 0])) def path_length(p): - return len(p) // o.n_points_per_cubic_curve + return len(p) // o.n_points_per_curve assert tuple(map(path_length, o.get_subpaths())) == (3, 1) assert tuple(map(path_length, o2.get_subpaths())) == (1, 2) @@ -430,9 +435,9 @@ def test_bounded_become(): """Tests that align_points generates a bounded number of points. https://github.com/ManimCommunity/manim/issues/1959 """ - o = VMobject() + o = OpenGLVMobject() - def draw_circle(m: VMobject, n_points, x=0, y=0, r=1): + def draw_circle(m: OpenGLVMobject, n_points, x=0, y=0, r=1): center = np.array([x, y, 0]) m.start_new_path(center + [r, 0, 0]) for i in range(1, n_points + 1): @@ -444,10 +449,10 @@ def draw_circle(m: VMobject, n_points, x=0, y=0, r=1): for _ in range(20): # Alternate between calls to become with different subpath sizes - a = VMobject() + a = OpenGLVMobject() draw_circle(a, 20) o.become(a) - b = VMobject() + b = OpenGLVMobject() draw_circle(b, 15) draw_circle(b, 15, x=3) o.become(b) diff --git a/tests/module/scene/test_auto_zoom.py b/tests/module/scene/test_auto_zoom.py index 4c95ed83ca..e6d23fc23a 100644 --- a/tests/module/scene/test_auto_zoom.py +++ b/tests/module/scene/test_auto_zoom.py @@ -10,7 +10,8 @@ def test_zoom(): s2.set_x(10) with tempconfig({"dry_run": True, "quality": "low_quality"}): - scene = MovingCameraScene() + manager = Manager(MovingCameraScene) + scene = manager.scene scene.add(s1, s2) scene.play(scene.camera.auto_zoom([s1, s2])) diff --git a/tests/module/scene/test_scene.py b/tests/module/scene/test_scene.py index 70ea9eaf2a..7f34b0921e 100644 --- a/tests/module/scene/test_scene.py +++ b/tests/module/scene/test_scene.py @@ -4,12 +4,13 @@ import pytest -from manim import Circle, FadeIn, Group, Mobject, Scene, Square +from manim import Circle, FadeIn, Group, Manager, Mobject, Scene, Square from manim.animation.animation import Wait def test_scene_add_remove(dry_run): - scene = Scene() + manager = Manager(Scene) + scene = manager.scene assert len(scene.mobjects) == 0 scene.add(Mobject()) assert len(scene.mobjects) == 1 @@ -25,6 +26,9 @@ def test_scene_add_remove(dry_run): # Check that Scene.add() returns the Scene (for chained calls) assert scene.add(Mobject()) is scene + + manager = Manager(Scene) + scene = manager.scene to_remove = Mobject() scene = Scene() scene.add(to_remove) @@ -40,19 +44,21 @@ def test_scene_add_remove(dry_run): def test_scene_time(dry_run): - scene = Scene() + manager = Manager(Scene) + scene = manager.scene assert scene.time == 0 scene.wait(2) assert scene.time == 2 scene.play(FadeIn(Circle()), run_time=0.5) assert pytest.approx(scene.time) == 2.5 - scene.renderer._original_skipping_status = True + scene._original_skipping_status = True scene.play(FadeIn(Square()), run_time=5) # this animation gets skipped. assert pytest.approx(scene.time) == 7.5 def test_subcaption(dry_run): - scene = Scene() + manager = Manager(Scene) + scene = manager.scene scene.add_subcaption("Testing add_subcaption", duration=1, offset=0) scene.wait() scene.play( @@ -78,7 +84,8 @@ def assert_names(mobjs, names): for i in range(0, len(mobjs)): assert mobjs[i].name == names[i] - scene = Scene() + manager = Manager(Scene) + scene = manager.scene first = Mobject(name="first") second = Mobject(name="second") diff --git a/tests/module/scene/test_sound.py b/tests/module/scene/test_sound.py index cfa9a4da42..ecea8b4d21 100644 --- a/tests/module/scene/test_sound.py +++ b/tests/module/scene/test_sound.py @@ -4,7 +4,7 @@ import wave from pathlib import Path -from manim import Scene +from manim import Manager, Scene def test_add_sound(tmpdir): @@ -17,5 +17,6 @@ def test_add_sound(tmpdir): f.writeframes(packed_value) f.writeframes(packed_value) - scene = Scene() + manager = Manager(Scene) + scene = manager.scene scene.add_sound(sound_loc) diff --git a/tests/module/scene/test_threed_scene.py b/tests/module/scene/test_threed_scene.py deleted file mode 100644 index 24f6f26330..0000000000 --- a/tests/module/scene/test_threed_scene.py +++ /dev/null @@ -1,17 +0,0 @@ -from manim import Circle, Square, ThreeDScene - - -def test_fixed_mobjects(): - scene = ThreeDScene() - s = Square() - c = Circle() - scene.add_fixed_in_frame_mobjects(s, c) - assert set(scene.mobjects) == {s, c} - assert set(scene.camera.fixed_in_frame_mobjects) == {s, c} - scene.remove_fixed_in_frame_mobjects(s) - assert set(scene.mobjects) == {s, c} - assert set(scene.camera.fixed_in_frame_mobjects) == {c} - scene.add_fixed_orientation_mobjects(s) - assert set(scene.camera.fixed_orientation_mobjects) == {s} - scene.remove_fixed_orientation_mobjects(s) - assert len(scene.camera.fixed_orientation_mobjects) == 0 diff --git a/tests/module/utils/test_color.py b/tests/module/utils/test_color.py index c3d468328b..a2e442d696 100644 --- a/tests/module/utils/test_color.py +++ b/tests/module/utils/test_color.py @@ -2,7 +2,7 @@ import numpy as np -from manim import BLACK, Mobject, Scene, VMobject +from manim import BLACK, Manager, Mobject, Scene, VMobject def test_import_color(): @@ -12,7 +12,8 @@ def test_import_color(): def test_background_color(): - S = Scene() + manager = Manager(Scene) + S = manager.scene S.camera.background_color = "#ff0000" S.renderer.update_frame(S) np.testing.assert_array_equal( diff --git a/tests/opengl/control_data/coordinate_system_opengl/gradient_line_graph_x_axis_using_opengl_renderer[None].npz b/tests/opengl/control_data/coordinate_system_opengl/gradient_line_graph_x_axis_using_opengl_renderer[None].npz deleted file mode 100644 index 6efda8b2e0..0000000000 Binary files a/tests/opengl/control_data/coordinate_system_opengl/gradient_line_graph_x_axis_using_opengl_renderer[None].npz and /dev/null differ diff --git a/tests/opengl/control_data/coordinate_system_opengl/gradient_line_graph_y_axis_using_opengl_renderer[None].npz b/tests/opengl/control_data/coordinate_system_opengl/gradient_line_graph_y_axis_using_opengl_renderer[None].npz deleted file mode 100644 index 3c7afcda7d..0000000000 Binary files a/tests/opengl/control_data/coordinate_system_opengl/gradient_line_graph_y_axis_using_opengl_renderer[None].npz and /dev/null differ diff --git a/tests/opengl/test_animate_opengl.py b/tests/opengl/test_animate_opengl.py deleted file mode 100644 index b5cf21afa6..0000000000 --- a/tests/opengl/test_animate_opengl.py +++ /dev/null @@ -1,118 +0,0 @@ -from __future__ import annotations - -import numpy as np -import pytest - -from manim.animation.creation import Uncreate -from manim.mobject.geometry.arc import Dot -from manim.mobject.geometry.line import Line -from manim.mobject.geometry.polygram import Square -from manim.mobject.mobject import override_animate -from manim.mobject.types.vectorized_mobject import VGroup - - -def test_simple_animate(using_opengl_renderer): - s = Square() - scale_factor = 2 - anim = s.animate.scale(scale_factor).build() - assert anim.mobject.target.width == scale_factor * s.width - - -def test_chained_animate(using_opengl_renderer): - s = Square() - scale_factor = 2 - direction = np.array((1, 1, 0)) - anim = s.animate.scale(scale_factor).shift(direction).build() - assert anim.mobject.target.width == scale_factor * s.width - assert (anim.mobject.target.get_center() == direction).all() - - -def test_overridden_animate(using_opengl_renderer): - class DotsWithLine(VGroup): - def __init__(self): - super().__init__() - self.left_dot = Dot().shift((-1, 0, 0)) - self.right_dot = Dot().shift((1, 0, 0)) - self.line = Line(self.left_dot, self.right_dot) - self.add(self.left_dot, self.right_dot, self.line) - - def remove_line(self): - self.remove(self.line) - - @override_animate(remove_line) - def _remove_line_animation(self, anim_args=None): - if anim_args is None: - anim_args = {} - self.remove_line() - return Uncreate(self.line, **anim_args) - - dots_with_line = DotsWithLine() - anim = dots_with_line.animate.remove_line().build() - assert len(dots_with_line.submobjects) == 2 - assert type(anim) is Uncreate - - -def test_chaining_overridden_animate(using_opengl_renderer): - class DotsWithLine(VGroup): - def __init__(self): - super().__init__() - self.left_dot = Dot().shift((-1, 0, 0)) - self.right_dot = Dot().shift((1, 0, 0)) - self.line = Line(self.left_dot, self.right_dot) - self.add(self.left_dot, self.right_dot, self.line) - - def remove_line(self): - self.remove(self.line) - - @override_animate(remove_line) - def _remove_line_animation(self, anim_args=None): - if anim_args is None: - anim_args = {} - self.remove_line() - return Uncreate(self.line, **anim_args) - - with pytest.raises( - NotImplementedError, - match="not supported for overridden animations", - ): - DotsWithLine().animate.shift((1, 0, 0)).remove_line() - - with pytest.raises( - NotImplementedError, - match="not supported for overridden animations", - ): - DotsWithLine().animate.remove_line().shift((1, 0, 0)) - - -def test_animate_with_args(using_opengl_renderer): - s = Square() - scale_factor = 2 - run_time = 2 - - anim = s.animate(run_time=run_time).scale(scale_factor).build() - assert anim.mobject.target.width == scale_factor * s.width - assert anim.run_time == run_time - - -def test_chained_animate_with_args(using_opengl_renderer): - s = Square() - scale_factor = 2 - direction = np.array((1, 1, 0)) - run_time = 2 - - anim = s.animate(run_time=run_time).scale(scale_factor).shift(direction).build() - assert anim.mobject.target.width == scale_factor * s.width - assert (anim.mobject.target.get_center() == direction).all() - assert anim.run_time == run_time - - -def test_animate_with_args_misplaced(using_opengl_renderer): - s = Square() - scale_factor = 2 - run_time = 2 - - with pytest.raises(ValueError, match="must be passed before"): - s.animate.scale(scale_factor)(run_time=run_time) - - with pytest.raises(ValueError, match="must be passed before"): - s.animate(run_time=run_time)(run_time=run_time).scale(scale_factor) diff --git a/tests/opengl/test_axes_shift_opengl.py b/tests/opengl/test_axes_shift_opengl.py deleted file mode 100644 index 1ae58afff4..0000000000 --- a/tests/opengl/test_axes_shift_opengl.py +++ /dev/null @@ -1,15 +0,0 @@ -from __future__ import annotations - -import numpy as np - -from manim.mobject.graphing.coordinate_systems import Axes - - -def test_axes_origin_shift(using_opengl_renderer): - ax = Axes(x_range=(5, 10, 1), y_range=(40, 45, 0.5)) - np.testing.assert_allclose( - ax.coords_to_point(5.0, 40.0), ax.x_axis.number_to_point(5) - ) - np.testing.assert_allclose( - ax.coords_to_point(5.0, 40.0), ax.y_axis.number_to_point(40) - ) diff --git a/tests/opengl/test_color_opengl.py b/tests/opengl/test_color_opengl.py deleted file mode 100644 index 3aeb2d6021..0000000000 --- a/tests/opengl/test_color_opengl.py +++ /dev/null @@ -1,173 +0,0 @@ -from __future__ import annotations - -import numpy as np - -from manim import BLACK, BLUE, GREEN, PURE_BLUE, PURE_GREEN, PURE_RED, Scene -from manim.mobject.opengl.opengl_mobject import OpenGLMobject -from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject - - -def test_import_color(using_opengl_renderer): - import manim.utils.color as C - - C.WHITE - - -def test_background_color(using_opengl_renderer): - S = Scene() - S.renderer.background_color = "#FF0000" - S.renderer.update_frame(S) - np.testing.assert_array_equal( - S.renderer.get_frame()[0, 0], np.array([255, 0, 0, 255]) - ) - - S.renderer.background_color = "#436F80" - S.renderer.update_frame(S) - np.testing.assert_array_equal( - S.renderer.get_frame()[0, 0], np.array([67, 111, 128, 255]) - ) - - S.renderer.background_color = "#FFFFFF" - S.renderer.update_frame(S) - np.testing.assert_array_equal( - S.renderer.get_frame()[0, 0], np.array([255, 255, 255, 255]) - ) - - -def test_set_color(using_opengl_renderer): - m = OpenGLMobject() - assert m.color.to_hex() == "#FFFFFF" - np.all(m.rgbas == np.array((0.0, 0.0, 0.0, 1.0))) - - m.set_color(BLACK) - assert m.color.to_hex() == "#000000" - np.all(m.rgbas == np.array((1.0, 1.0, 1.0, 1.0))) - - m.set_color(PURE_GREEN, opacity=0.5) - assert m.color.to_hex() == "#00FF00" - np.all(m.rgbas == np.array((0.0, 1.0, 0.0, 0.5))) - - m = OpenGLVMobject() - assert m.color.to_hex() == "#FFFFFF" - np.all(m.fill_rgba == np.array((0.0, 0.0, 0.0, 1.0))) - np.all(m.stroke_rgba == np.array((0.0, 0.0, 0.0, 1.0))) - - m.set_color(BLACK) - assert m.color.to_hex() == "#000000" - np.all(m.fill_rgba == np.array((1.0, 1.0, 1.0, 1.0))) - np.all(m.stroke_rgba == np.array((1.0, 1.0, 1.0, 1.0))) - - m.set_color(PURE_GREEN, opacity=0.5) - assert m.color.to_hex() == "#00FF00" - np.all(m.fill_rgba == np.array((0.0, 1.0, 0.0, 0.5))) - np.all(m.stroke_rgba == np.array((0.0, 1.0, 0.0, 0.5))) - - -def test_set_fill_color(using_opengl_renderer): - m = OpenGLVMobject() - assert m.fill_color.to_hex() == "#FFFFFF" - np.all(m.fill_rgba == np.array((0.0, 1.0, 0.0, 0.5))) - - m.set_fill(BLACK) - assert m.fill_color.to_hex() == "#000000" - np.all(m.fill_rgba == np.array((1.0, 1.0, 1.0, 1.0))) - - m.set_fill(PURE_GREEN, opacity=0.5) - assert m.fill_color.to_hex() == "#00FF00" - np.all(m.fill_rgba == np.array((0.0, 1.0, 0.0, 0.5))) - - -def test_set_stroke_color(using_opengl_renderer): - m = OpenGLVMobject() - assert m.stroke_color.to_hex() == "#FFFFFF" - np.all(m.stroke_rgba == np.array((0.0, 1.0, 0.0, 0.5))) - - m.set_stroke(BLACK) - assert m.stroke_color.to_hex() == "#000000" - np.all(m.stroke_rgba == np.array((1.0, 1.0, 1.0, 1.0))) - - m.set_stroke(PURE_GREEN, opacity=0.5) - assert m.stroke_color.to_hex() == "#00FF00" - np.all(m.stroke_rgba == np.array((0.0, 1.0, 0.0, 0.5))) - - -def test_set_fill(using_opengl_renderer): - m = OpenGLMobject() - assert m.color.to_hex() == "#FFFFFF" - m.set_color(BLACK) - assert m.color.to_hex() == "#000000" - - m = OpenGLVMobject() - assert m.color.to_hex() == "#FFFFFF" - m.set_color(BLACK) - assert m.color.to_hex() == "#000000" - - -def test_set_color_handles_lists_of_strs(using_opengl_renderer): - m = OpenGLVMobject() - assert m.color.to_hex() == "#FFFFFF" - m.set_color([BLACK, BLUE, GREEN]) - assert m.get_colors()[0] == BLACK - assert m.get_colors()[1] == BLUE - assert m.get_colors()[2] == GREEN - - assert m.get_fill_colors()[0] == BLACK - assert m.get_fill_colors()[1] == BLUE - assert m.get_fill_colors()[2] == GREEN - - assert m.get_stroke_colors()[0] == BLACK - assert m.get_stroke_colors()[1] == BLUE - assert m.get_stroke_colors()[2] == GREEN - - -def test_set_color_handles_lists_of_color_objects(using_opengl_renderer): - m = OpenGLVMobject() - assert m.color.to_hex() == "#FFFFFF" - m.set_color([PURE_BLUE, PURE_GREEN, PURE_RED]) - assert m.get_colors()[0].to_hex() == "#0000FF" - assert m.get_colors()[1].to_hex() == "#00FF00" - assert m.get_colors()[2].to_hex() == "#FF0000" - - assert m.get_fill_colors()[0].to_hex() == "#0000FF" - assert m.get_fill_colors()[1].to_hex() == "#00FF00" - assert m.get_fill_colors()[2].to_hex() == "#FF0000" - - assert m.get_stroke_colors()[0].to_hex() == "#0000FF" - assert m.get_stroke_colors()[1].to_hex() == "#00FF00" - assert m.get_stroke_colors()[2].to_hex() == "#FF0000" - - -def test_set_fill_handles_lists_of_strs(using_opengl_renderer): - m = OpenGLVMobject() - assert m.fill_color.to_hex() == "#FFFFFF" - m.set_fill([BLACK.to_hex(), BLUE.to_hex(), GREEN.to_hex()]) - assert m.get_fill_colors()[0].to_hex() == BLACK.to_hex() - assert m.get_fill_colors()[1].to_hex() == BLUE.to_hex() - assert m.get_fill_colors()[2].to_hex() == GREEN.to_hex() - - -def test_set_fill_handles_lists_of_color_objects(using_opengl_renderer): - m = OpenGLVMobject() - assert m.fill_color.to_hex() == "#FFFFFF" - m.set_fill([PURE_BLUE, PURE_GREEN, PURE_RED]) - assert m.get_fill_colors()[0].to_hex() == "#0000FF" - assert m.get_fill_colors()[1].to_hex() == "#00FF00" - assert m.get_fill_colors()[2].to_hex() == "#FF0000" - - -def test_set_stroke_handles_lists_of_strs(using_opengl_renderer): - m = OpenGLVMobject() - assert m.stroke_color.to_hex() == "#FFFFFF" - m.set_stroke([BLACK.to_hex(), BLUE.to_hex(), GREEN.to_hex()]) - assert m.get_stroke_colors()[0].to_hex() == BLACK.to_hex() - assert m.get_stroke_colors()[1].to_hex() == BLUE.to_hex() - assert m.get_stroke_colors()[2].to_hex() == GREEN.to_hex() - - -def test_set_stroke_handles_lists_of_color_objects(using_opengl_renderer): - m = OpenGLVMobject() - assert m.stroke_color.to_hex() == "#FFFFFF" - m.set_stroke([PURE_BLUE, PURE_GREEN, PURE_RED]) - assert m.get_stroke_colors()[0].to_hex() == "#0000FF" - assert m.get_stroke_colors()[1].to_hex() == "#00FF00" - assert m.get_stroke_colors()[2].to_hex() == "#FF0000" diff --git a/tests/opengl/test_composition_opengl.py b/tests/opengl/test_composition_opengl.py deleted file mode 100644 index c09cd691a1..0000000000 --- a/tests/opengl/test_composition_opengl.py +++ /dev/null @@ -1,107 +0,0 @@ -from __future__ import annotations - -from unittest.mock import MagicMock - -from manim.animation.animation import Animation, Wait -from manim.animation.composition import AnimationGroup, Succession -from manim.animation.fading import FadeIn, FadeOut -from manim.constants import DOWN, UP -from manim.mobject.geometry.arc import Circle -from manim.mobject.geometry.line import Line -from manim.mobject.geometry.polygram import Square - - -def test_succession_timing(using_opengl_renderer): - """Test timing of animations in a succession.""" - line = Line() - animation_1s = FadeIn(line, shift=UP, run_time=1.0) - 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(MagicMock()) - succession.begin() - assert succession.active_index == 0 - # The first animation takes 20% of the total run time. - succession.interpolate(0.199) - assert succession.active_index == 0 - succession.interpolate(0.2) - assert succession.active_index == 1 - succession.interpolate(0.8) - assert succession.active_index == 1 - # At 100% and more, no animation must be active anymore. - succession.interpolate(1.0) - assert succession.active_index == 2 - assert succession.active_animation is None - succession.interpolate(1.2) - assert succession.active_index == 2 - assert succession.active_animation is None - - -def test_succession_in_succession_timing(using_opengl_renderer): - """Test timing of nested successions.""" - line = Line() - animation_1s = FadeIn(line, shift=UP, run_time=1.0) - animation_4s = FadeOut(line, shift=DOWN, run_time=4.0) - nested_succession = Succession(animation_1s, animation_4s) - succession = Succession( - FadeIn(line, shift=UP, run_time=4.0), - nested_succession, - FadeIn(line, shift=UP, run_time=1.0), - ) - assert nested_succession.get_run_time() == 5.0 - assert succession.get_run_time() == 10.0 - succession._setup_scene(MagicMock()) - succession.begin() - succession.interpolate(0.1) - assert succession.active_index == 0 - # The nested succession must not be active yet, and as a result hasn't set active_animation yet. - assert not hasattr(nested_succession, "active_animation") - succession.interpolate(0.39) - assert succession.active_index == 0 - assert not hasattr(nested_succession, "active_animation") - # The nested succession starts at 40% of total run time - succession.interpolate(0.4) - assert succession.active_index == 1 - assert nested_succession.active_index == 0 - # The nested succession second animation starts at 50% of total run time. - succession.interpolate(0.49) - assert succession.active_index == 1 - assert nested_succession.active_index == 0 - succession.interpolate(0.5) - assert succession.active_index == 1 - assert nested_succession.active_index == 1 - # The last animation starts at 90% of total run time. The nested succession must be finished at that time. - succession.interpolate(0.89) - assert succession.active_index == 1 - assert nested_succession.active_index == 1 - succession.interpolate(0.9) - assert succession.active_index == 2 - assert nested_succession.active_index == 2 - assert nested_succession.active_animation is None - # After 100%, nothing must be playing anymore. - succession.interpolate(1.0) - assert succession.active_index == 3 - assert succession.active_animation is None - assert nested_succession.active_index == 2 - assert nested_succession.active_animation is None - - -def test_animationbuilder_in_group(using_opengl_renderer): - sqr = Square() - circ = Circle() - animation_group = AnimationGroup(sqr.animate.shift(DOWN).scale(2), FadeIn(circ)) - assert all(isinstance(anim, Animation) for anim in animation_group.animations) - succession = Succession(sqr.animate.shift(DOWN).scale(2), FadeIn(circ)) - assert all(isinstance(anim, Animation) for anim in succession.animations) - - -def test_animationgroup_with_wait(using_opengl_renderer): - sqr = Square() - sqr_anim = FadeIn(sqr) - wait = Wait() - animation_group = AnimationGroup(wait, sqr_anim, lag_ratio=1) - - animation_group.begin() - timings = animation_group.anims_with_timings - - assert timings.tolist() == [(wait, 0.0, 1.0), (sqr_anim, 1.0, 2.0)] diff --git a/tests/opengl/test_config_opengl.py b/tests/opengl/test_config_opengl.py deleted file mode 100644 index d0ca0e5a81..0000000000 --- a/tests/opengl/test_config_opengl.py +++ /dev/null @@ -1,142 +0,0 @@ -from __future__ import annotations - -import tempfile -from pathlib import Path - -import numpy as np - -from manim import WHITE, Scene, Square, tempconfig - - -def test_tempconfig(config, using_opengl_renderer): - """Test the tempconfig context manager.""" - original = config.copy() - - with tempconfig({"frame_width": 100, "frame_height": 42}): - # check that config was modified correctly - assert config["frame_width"] == 100 - assert config["frame_height"] == 42 - - # check that no keys are missing and no new keys were added - assert set(original.keys()) == set(config.keys()) - - # check that the keys are still untouched - assert set(original.keys()) == set(config.keys()) - - # check that config is correctly restored - for k, v in original.items(): - if isinstance(v, np.ndarray): - np.testing.assert_allclose(config[k], v) - else: - assert config[k] == v - - -class MyScene(Scene): - def construct(self): - self.add(Square()) - self.wait(1) - - -def test_background_color(config, using_opengl_renderer, dry_run): - """Test the 'background_color' config option.""" - config.background_color = WHITE - config.verbose = "ERROR" - - scene = MyScene() - scene.render() - frame = scene.renderer.get_frame() - np.testing.assert_allclose(frame[0, 0], [255, 255, 255, 255]) - - -def test_digest_file(config, using_opengl_renderer, tmp_path): - """Test that a config file can be digested programmatically.""" - with tempfile.NamedTemporaryFile("w", dir=tmp_path, delete=False) as tmp_cfg: - tmp_cfg.write( - """ - [CLI] - media_dir = this_is_my_favorite_path - video_dir = {media_dir}/videos - frame_height = 10 - """, - ) - config.digest_file(tmp_cfg.name) - - assert config.get_dir("media_dir") == Path("this_is_my_favorite_path") - assert config.get_dir("video_dir") == Path("this_is_my_favorite_path/videos") - - -def test_frame_size(config, using_opengl_renderer, tmp_path): - """Test that the frame size can be set via config file.""" - np.testing.assert_allclose( - config.aspect_ratio, config.pixel_width / config.pixel_height - ) - np.testing.assert_allclose(config.frame_height, 8.0) - - with tempconfig({}): - with tempfile.NamedTemporaryFile("w", dir=tmp_path, delete=False) as tmp_cfg: - tmp_cfg.write( - """ - [CLI] - pixel_height = 10 - pixel_width = 10 - """, - ) - config.digest_file(tmp_cfg.name) - - # aspect ratio is set using pixel measurements - np.testing.assert_allclose(config.aspect_ratio, 1.0) - # if not specified in the cfg file, frame_width is set using the aspect ratio - np.testing.assert_allclose(config.frame_height, 8.0) - np.testing.assert_allclose(config.frame_width, 8.0) - - -def test_frame_size_if_frame_width(config, using_opengl_renderer, tmp_path): - with tempfile.NamedTemporaryFile("w", dir=tmp_path, delete=False) as tmp_cfg: - tmp_cfg.write( - """ - [CLI] - pixel_height = 10 - pixel_width = 10 - frame_height = 10 - frame_width = 10 - """, - ) - tmp_cfg.close() - config.digest_file(tmp_cfg.name) - - np.testing.assert_allclose(config.aspect_ratio, 1.0) - # if both are specified in the cfg file, the aspect ratio is ignored - np.testing.assert_allclose(config.frame_height, 10.0) - np.testing.assert_allclose(config.frame_width, 10.0) - - -def test_temporary_dry_run(config, using_opengl_renderer): - """Test that tempconfig correctly restores after setting dry_run.""" - assert config["write_to_movie"] - assert not config["save_last_frame"] - - with tempconfig({"dry_run": True}): - assert not config["write_to_movie"] - assert not config["save_last_frame"] - - assert config["write_to_movie"] - assert not config["save_last_frame"] - - -def test_dry_run_with_png_format(config, using_opengl_renderer, dry_run): - """Test that there are no exceptions when running a png without output""" - config.disable_caching = True - assert config["dry_run"] is True - scene = MyScene() - scene.render() - - -def test_dry_run_with_png_format_skipped_animations( - config, using_opengl_renderer, dry_run -): - """Test that there are no exceptions when running a png without output and skipped animations""" - config.write_to_movie = False - config.disable_caching = True - assert config["dry_run"] is True - scene = MyScene(skip_animations=True) - scene.render() diff --git a/tests/opengl/test_coordinate_system_opengl.py b/tests/opengl/test_coordinate_system_opengl.py deleted file mode 100644 index c9f5cc81b6..0000000000 --- a/tests/opengl/test_coordinate_system_opengl.py +++ /dev/null @@ -1,174 +0,0 @@ -from __future__ import annotations - -import math - -import numpy as np -import pytest - -from manim import ( - LEFT, - ORIGIN, - PI, - UR, - Axes, - Circle, - ComplexPlane, - NumberPlane, - PolarPlane, - ThreeDAxes, - config, - tempconfig, -) -from manim import CoordinateSystem as CS -from manim.utils.color import BLUE, GREEN, ORANGE, RED, YELLOW -from manim.utils.testing.frames_comparison import frames_comparison - -__module_test__ = "coordinate_system_opengl" - - -def test_initial_config(using_opengl_renderer): - """Check that all attributes are defined properly from the config.""" - cs = CS() - assert cs.x_range[0] == round(-config["frame_x_radius"]) - assert cs.x_range[1] == round(config["frame_x_radius"]) - assert cs.x_range[2] == 1.0 - assert cs.y_range[0] == round(-config["frame_y_radius"]) - assert cs.y_range[1] == round(config["frame_y_radius"]) - assert cs.y_range[2] == 1.0 - - ax = Axes() - np.testing.assert_allclose(ax.get_center(), ORIGIN) - np.testing.assert_allclose(ax.y_axis_config["label_direction"], LEFT) - - with tempconfig({"frame_x_radius": 100, "frame_y_radius": 200}): - cs = CS() - assert cs.x_range[0] == -100 - assert cs.x_range[1] == 100 - assert cs.y_range[0] == -200 - assert cs.y_range[1] == 200 - - -def test_dimension(using_opengl_renderer): - """Check that objects have the correct dimension.""" - assert Axes().dimension == 2 - assert NumberPlane().dimension == 2 - assert PolarPlane().dimension == 2 - assert ComplexPlane().dimension == 2 - assert ThreeDAxes().dimension == 3 - - -def test_abstract_base_class(using_opengl_renderer): - """Check that CoordinateSystem has some abstract methods.""" - with pytest.raises(NotImplementedError): - CS().get_axes() - - -@pytest.mark.skip( - reason="Causes conflicts with other tests due to axis_config changing default config", -) -def test_NumberPlane(using_opengl_renderer): - """Test that NumberPlane generates the correct number of lines when its ranges do not cross 0.""" - pos_x_range = (0, 7) - neg_x_range = (-7, 0) - - pos_y_range = (2, 6) - neg_y_range = (-6, -2) - - x_vals = [0, 1.5, 2, 2.8, 4, 6.25] - y_vals = [2, 5, 4.25, 6, 4.5, 2.75] - - testing_data = [ - (pos_x_range, pos_y_range, x_vals, y_vals), - (pos_x_range, neg_y_range, x_vals, [-v for v in y_vals]), - (neg_x_range, pos_y_range, [-v for v in x_vals], y_vals), - (neg_x_range, neg_y_range, [-v for v in x_vals], [-v for v in y_vals]), - ] - - for test_data in testing_data: - x_range, y_range, x_vals, y_vals = test_data - - x_start, x_end = x_range - y_start, y_end = y_range - - plane = NumberPlane( - x_range=x_range, - y_range=y_range, - # x_length = 7, - axis_config={"include_numbers": True}, - ) - - # normally these values would be need to be added by one to pass since there's an - # overlapping pair of lines at the origin, but since these planes do not cross 0, - # this is not needed. - num_y_lines = math.ceil(x_end - x_start) - num_x_lines = math.floor(y_end - y_start) - - assert len(plane.y_lines) == num_y_lines - assert len(plane.x_lines) == num_x_lines - - plane = NumberPlane((-5, 5, 0.5), (-8, 8, 2)) # <- test for different step values - assert len(plane.x_lines) == 8 - assert len(plane.y_lines) == 20 - - -def test_point_to_coords(using_opengl_renderer): - ax = Axes(x_range=[0, 10, 2]) - circ = Circle(radius=0.5).shift(UR * 2) - - # get the coordinates of the circle with respect to the axes - coords = np.around(ax.point_to_coords(circ.get_right()), decimals=4) - np.testing.assert_array_equal(coords, (7.0833, 2.6667)) - - -def test_coords_to_point(using_opengl_renderer): - ax = Axes() - - # a point with respect to the axes - c2p_coord = np.around(ax.coords_to_point(2, 2), decimals=4) - np.testing.assert_array_equal(c2p_coord, (1.7143, 1.5, 0)) - - -def test_input_to_graph_point(using_opengl_renderer): - ax = Axes() - curve = ax.plot(lambda x: np.cos(x)) - line_graph = ax.plot_line_graph([1, 3, 5], [-1, 2, -2], add_vertex_dots=False)[ - "line_graph" - ] - - # move a square to PI on the cosine curve. - position = np.around(ax.input_to_graph_point(x=PI, graph=curve), decimals=4) - np.testing.assert_array_equal(position, (2.6928, -0.75, 0)) - - # test the line_graph implementation - position = np.around(ax.input_to_graph_point(x=PI, graph=line_graph), decimals=4) - np.testing.assert_array_equal(position, (2.6928, 1.2876, 0)) - - -@frames_comparison -def test_gradient_line_graph_x_axis(scene, using_opengl_renderer): - """Test that using `colorscale` generates a line whose gradient matches the y-axis""" - axes = Axes(x_range=[-3, 3], y_range=[-3, 3]) - - curve = axes.plot( - lambda x: 0.1 * x**3, - x_range=(-3, 3, 0.001), - colorscale=[BLUE, GREEN, YELLOW, ORANGE, RED], - colorscale_axis=0, - ) - - scene.add(axes, curve) - - -@frames_comparison -def test_gradient_line_graph_y_axis(scene, using_opengl_renderer): - """Test that using `colorscale` generates a line whose gradient matches the y-axis""" - axes = Axes(x_range=[-3, 3], y_range=[-3, 3]) - - curve = axes.plot( - lambda x: 0.1 * x**3, - x_range=(-3, 3, 0.001), - colorscale=[BLUE, GREEN, YELLOW, ORANGE, RED], - colorscale_axis=1, - ) - - scene.add(axes, curve) diff --git a/tests/opengl/test_copy_opengl.py b/tests/opengl/test_copy_opengl.py deleted file mode 100644 index 504b7f26a5..0000000000 --- a/tests/opengl/test_copy_opengl.py +++ /dev/null @@ -1,51 +0,0 @@ -from __future__ import annotations - -from pathlib import Path - -from manim import BraceLabel -from manim.mobject.opengl.opengl_mobject import OpenGLMobject - - -def test_opengl_mobject_copy(using_opengl_renderer): - """Test that a copy is a deepcopy.""" - orig = OpenGLMobject() - orig.add(*(OpenGLMobject() for _ in range(10))) - copy = orig.copy() - - assert orig is orig - assert orig is not copy - assert orig.submobjects is not copy.submobjects - for i in range(10): - assert orig.submobjects[i] is not copy.submobjects[i] - - -def test_bracelabel_copy(config, using_opengl_renderer, tmp_path): - """Test that a copy is a deepcopy.""" - # For this test to work, we need to tweak some folders temporarily - original_text_dir = config["text_dir"] - original_tex_dir = config["tex_dir"] - mediadir = Path(tmp_path) / "deepcopy" - config["text_dir"] = str(mediadir.joinpath("Text")) - config["tex_dir"] = str(mediadir.joinpath("Tex")) - for el in ["text_dir", "tex_dir"]: - Path(config[el]).mkdir(parents=True) - - # Before the refactoring of OpenGLMobject.copy(), the class BraceLabel was the - # only one to have a non-trivial definition of copy. Here we test that it - # still works after the refactoring. - orig = BraceLabel(OpenGLMobject(), "label") - copy = orig.copy() - - assert orig is orig - assert orig is not copy - assert orig.brace is not copy.brace - assert orig.label is not copy.label - assert orig.submobjects is not copy.submobjects - assert orig.submobjects[0] is orig.brace - assert copy.submobjects[0] is copy.brace - assert orig.submobjects[0] is not copy.brace - assert copy.submobjects[0] is not orig.brace - - # Restore the original folders - config["text_dir"] = original_text_dir - config["tex_dir"] = original_tex_dir diff --git a/tests/opengl/test_family_opengl.py b/tests/opengl/test_family_opengl.py deleted file mode 100644 index f16d0e4756..0000000000 --- a/tests/opengl/test_family_opengl.py +++ /dev/null @@ -1,93 +0,0 @@ -from __future__ import annotations - -import numpy as np - -from manim import RIGHT, Circle -from manim.mobject.opengl.opengl_mobject import OpenGLMobject - - -def test_family(using_opengl_renderer): - """Check that the family is gathered correctly.""" - # Check that an empty OpenGLMobject's family only contains itself - mob = OpenGLMobject() - assert mob.get_family() == [mob] - - # Check that all children are in the family - mob = OpenGLMobject() - children = [OpenGLMobject() for _ in range(10)] - mob.add(*children) - family = mob.get_family() - assert len(family) == 1 + 10 - assert mob in family - for c in children: - assert c in family - - # Nested children should be in the family - mob = OpenGLMobject() - grandchildren = {} - for _ in range(10): - child = OpenGLMobject() - grandchildren[child] = [OpenGLMobject() for _ in range(10)] - child.add(*grandchildren[child]) - mob.add(*list(grandchildren.keys())) - family = mob.get_family() - assert len(family) == 1 + 10 + 10 * 10 - assert mob in family - for c in grandchildren: - assert c in family - for gc in grandchildren[c]: - assert gc in family - - -def test_overlapping_family(using_opengl_renderer): - """Check that each member of the family is only gathered once.""" - ( - mob, - child1, - child2, - ) = ( - OpenGLMobject(), - OpenGLMobject(), - OpenGLMobject(), - ) - gchild1, gchild2, gchild_common = OpenGLMobject(), OpenGLMobject(), OpenGLMobject() - child1.add(gchild1, gchild_common) - child2.add(gchild2, gchild_common) - mob.add(child1, child2) - family = mob.get_family() - assert mob in family - assert len(family) == 6 - assert family.count(gchild_common) == 1 - - -def test_shift_family(using_opengl_renderer): - """Check that each member of the family is shifted along with the parent. - - Importantly, here we add a common grandchild to each of the children. So - this test will fail if the grandchild moves twice as much as it should. - - """ - # Note shift() needs the OpenGLMobject to have a non-empty `points` attribute, so - # we cannot use a plain OpenGLMobject or OpenGLVMobject. We use Circle instead. - ( - mob, - child1, - child2, - ) = ( - Circle(), - Circle(), - Circle(), - ) - gchild1, gchild2, gchild_common = Circle(), Circle(), Circle() - - child1.add(gchild1, gchild_common) - child2.add(gchild2, gchild_common) - mob.add(child1, child2) - family = mob.get_family() - - positions_before = {m: m.get_center().copy() for m in family} - mob.shift(RIGHT) - positions_after = {m: m.get_center().copy() for m in family} - - for m in family: - np.testing.assert_allclose(positions_before[m] + RIGHT, positions_after[m]) diff --git a/tests/opengl/test_graph_opengl.py b/tests/opengl/test_graph_opengl.py deleted file mode 100644 index eb56935b9c..0000000000 --- a/tests/opengl/test_graph_opengl.py +++ /dev/null @@ -1,100 +0,0 @@ -from __future__ import annotations - -from manim import Dot, Graph, Line, Text - - -def test_graph_creation(using_opengl_renderer): - vertices = [1, 2, 3, 4] - edges = [(1, 2), (2, 3), (3, 4), (4, 1)] - layout = {1: [0, 0, 0], 2: [1, 1, 0], 3: [1, -1, 0], 4: [-1, 0, 0]} - G_manual = Graph(vertices=vertices, edges=edges, layout=layout) - assert len(G_manual.vertices) == 4 - assert len(G_manual.edges) == 4 - G_spring = Graph(vertices=vertices, edges=edges) - assert len(G_spring.vertices) == 4 - assert len(G_spring.edges) == 4 - - -def test_graph_add_vertices(using_opengl_renderer): - G = Graph([1, 2, 3], [(1, 2), (2, 3)]) - G.add_vertices(4) - assert len(G.vertices) == 4 - assert len(G.edges) == 2 - G.add_vertices(5, labels={5: Text("5")}) - assert len(G.vertices) == 5 - assert len(G.edges) == 2 - assert 5 in G._labels - assert 5 in G._vertex_config - G.add_vertices(6, 7, 8) - assert len(G.vertices) == 8 - assert len(G._graph.nodes()) == 8 - - -def test_graph_remove_vertices(using_opengl_renderer): - G = Graph([1, 2, 3, 4, 5], [(1, 2), (2, 3), (3, 4), (4, 5)]) - removed_mobjects = G.remove_vertices(3) - assert len(removed_mobjects) == 3 - assert len(G.vertices) == 4 - assert len(G.edges) == 2 - assert list(G.vertices.keys()) == [1, 2, 4, 5] - assert list(G.edges.keys()) == [(1, 2), (4, 5)] - removed_mobjects = G.remove_vertices(4, 5) - assert len(removed_mobjects) == 3 - assert len(G.vertices) == 2 - assert len(G.edges) == 1 - assert list(G.vertices.keys()) == [1, 2] - assert list(G.edges.keys()) == [(1, 2)] - - -def test_graph_add_edges(using_opengl_renderer): - G = Graph([1, 2, 3, 4, 5], [(1, 2), (2, 3)]) - added_mobjects = G.add_edges((1, 3)) - assert isinstance(added_mobjects.submobjects[0], Line) - assert len(G.vertices) == 5 - assert len(G.edges) == 3 - assert set(G.vertices.keys()) == {1, 2, 3, 4, 5} - assert set(G.edges.keys()) == {(1, 2), (2, 3), (1, 3)} - - added_mobjects = G.add_edges((1, 42)) - removed_mobjects = added_mobjects.submobjects - assert isinstance(removed_mobjects[0], Dot) - assert isinstance(removed_mobjects[1], Line) - - assert len(G.vertices) == 6 - assert len(G.edges) == 4 - assert set(G.vertices.keys()) == {1, 2, 3, 4, 5, 42} - assert set(G.edges.keys()) == {(1, 2), (2, 3), (1, 3), (1, 42)} - - added_mobjects = G.add_edges((4, 5), (5, 6), (6, 7)) - assert len(added_mobjects) == 5 - assert len(G.vertices) == 8 - assert len(G.edges) == 7 - assert set(G.vertices.keys()) == {1, 2, 3, 4, 5, 42, 6, 7} - assert set(G._graph.nodes()) == set(G.vertices.keys()) - assert set(G.edges.keys()) == { - (1, 2), - (2, 3), - (1, 3), - (1, 42), - (4, 5), - (5, 6), - (6, 7), - } - assert set(G._graph.edges()) == set(G.edges.keys()) - - -def test_graph_remove_edges(using_opengl_renderer): - G = Graph([1, 2, 3, 4, 5], [(1, 2), (2, 3), (3, 4), (4, 5), (1, 5)]) - removed_mobjects = G.remove_edges((1, 2)) - assert isinstance(removed_mobjects.submobjects[0], Line) - assert len(G.vertices) == 5 - assert len(G.edges) == 4 - assert set(G.edges.keys()) == {(2, 3), (3, 4), (4, 5), (1, 5)} - assert set(G._graph.edges()) == set(G.edges.keys()) - - removed_mobjects = G.remove_edges((2, 3), (3, 4), (4, 5), (1, 5)) - assert len(removed_mobjects) == 4 - assert len(G.vertices) == 5 - assert len(G.edges) == 0 - assert set(G._graph.edges()) == set() - assert set(G.edges.keys()) == set() diff --git a/tests/opengl/test_ipython_magic_opengl.py b/tests/opengl/test_ipython_magic_opengl.py deleted file mode 100644 index 2d4617af0f..0000000000 --- a/tests/opengl/test_ipython_magic_opengl.py +++ /dev/null @@ -1,30 +0,0 @@ -from __future__ import annotations - -import re - -from manim.utils.ipython_magic import _generate_file_name - - -def test_jupyter_file_naming(config, using_opengl_renderer): - """Check the format of file names for jupyter""" - scene_name = "SimpleScene" - expected_pattern = r"[0-9a-zA-Z_]+[@_-]\d\d\d\d-\d\d-\d\d[@_-]\d\d-\d\d-\d\d" - config.scene_names = [scene_name] - file_name = _generate_file_name() - match = re.match(expected_pattern, file_name) - assert scene_name in file_name, ( - "Expected file to contain " + scene_name + " but got " + file_name - ) - assert match, "file name does not match expected pattern " + expected_pattern - - -def test_jupyter_file_output(tmp_path, config, using_opengl_renderer): - """Check the jupyter file naming is valid and can be created""" - scene_name = "SimpleScene" - config.scene_names = [scene_name] - file_name = _generate_file_name() - actual_path = tmp_path.with_name(file_name) - with actual_path.open("w") as outfile: - outfile.write("") - assert actual_path.exists() - assert actual_path.is_file() diff --git a/tests/opengl/test_markup_opengl.py b/tests/opengl/test_markup_opengl.py deleted file mode 100644 index b96acbeb97..0000000000 --- a/tests/opengl/test_markup_opengl.py +++ /dev/null @@ -1,46 +0,0 @@ -from __future__ import annotations - -from manim import MarkupText - - -def test_good_markup(using_opengl_renderer): - """Test creation of valid :class:`MarkupText` object""" - try: - MarkupText("foo") - MarkupText("foo") - success = True - except ValueError: - success = False - assert success, "'foo' and 'foo' should not fail validation" - - -def test_special_tags_markup(using_opengl_renderer): - """Test creation of valid :class:`MarkupText` object with unofficial tags""" - try: - MarkupText('foo') - MarkupText('foo') - success = True - except ValueError: - success = False - assert success, '\'foo\' and \'foo\' should not fail validation' - - -def test_unbalanced_tag_markup(using_opengl_renderer): - """Test creation of invalid :class:`MarkupText` object (unbalanced tag)""" - try: - MarkupText("foo") - success = False - except ValueError: - success = True - assert success, "'foo' should fail validation" - - -def test_invalid_tag_markup(using_opengl_renderer): - """Test creation of invalid :class:`MarkupText` object (invalid tag)""" - try: - MarkupText("foo") - success = False - except ValueError: - success = True - - assert success, "'foo' should fail validation" diff --git a/tests/opengl/test_number_line_opengl.py b/tests/opengl/test_number_line_opengl.py deleted file mode 100644 index 5eb52eb914..0000000000 --- a/tests/opengl/test_number_line_opengl.py +++ /dev/null @@ -1,70 +0,0 @@ -from __future__ import annotations - -import numpy as np - -from manim import NumberLine -from manim.mobject.text.numbers import Integer - - -def test_unit_vector(): - """Check if the magnitude of unit vector along - the NumberLine is equal to its unit_size. - """ - axis1 = NumberLine(unit_size=0.4) - axis2 = NumberLine(x_range=[-2, 5], length=12) - for axis in (axis1, axis2): - assert np.linalg.norm(axis.get_unit_vector()) == axis.unit_size - - -def test_decimal_determined_by_step(): - """Checks that step size is considered when determining the number of decimal - places. - """ - axis = NumberLine(x_range=[-2, 2, 0.5]) - expected_decimal_places = 1 - actual_decimal_places = axis.decimal_number_config["num_decimal_places"] - assert actual_decimal_places == expected_decimal_places, ( - "Expected 1 decimal place but got " + actual_decimal_places - ) - - axis2 = NumberLine(x_range=[-1, 1, 0.25]) - expected_decimal_places = 2 - actual_decimal_places = axis2.decimal_number_config["num_decimal_places"] - assert actual_decimal_places == expected_decimal_places, ( - "Expected 1 decimal place but got " + actual_decimal_places - ) - - -def test_decimal_config_overrides_defaults(): - """Checks that ``num_decimal_places`` is determined by step size and gets overridden by ``decimal_number_config``.""" - axis = NumberLine( - x_range=[-2, 2, 0.5], - decimal_number_config={"num_decimal_places": 0}, - ) - expected_decimal_places = 0 - actual_decimal_places = axis.decimal_number_config["num_decimal_places"] - assert actual_decimal_places == expected_decimal_places, ( - "Expected 1 decimal place but got " + actual_decimal_places - ) - - -def test_whole_numbers_step_size_default_to_0_decimal_places(): - """Checks that ``num_decimal_places`` defaults to 0 when a whole number step size is passed.""" - axis = NumberLine(x_range=[-2, 2, 1]) - expected_decimal_places = 0 - actual_decimal_places = axis.decimal_number_config["num_decimal_places"] - assert actual_decimal_places == expected_decimal_places, ( - "Expected 1 decimal place but got " + actual_decimal_places - ) - - -def test_add_labels(): - expected_label_length = 6 - num_line = NumberLine(x_range=[-4, 4]) - num_line.add_labels( - dict(zip(list(range(-3, 3)), [Integer(m) for m in range(-1, 5)])), - ) - actual_label_length = len(num_line.labels) - assert ( - actual_label_length == expected_label_length - ), f"Expected a VGroup with {expected_label_length} integers but got {actual_label_length}." diff --git a/tests/opengl/test_numbers_opengl.py b/tests/opengl/test_numbers_opengl.py deleted file mode 100644 index 78cd4fac0d..0000000000 --- a/tests/opengl/test_numbers_opengl.py +++ /dev/null @@ -1,38 +0,0 @@ -from __future__ import annotations - -from manim.mobject.text.numbers import DecimalNumber - - -def test_font_size(): - """Test that DecimalNumber returns the correct font_size value - after being scaled. - """ - num = DecimalNumber(0).scale(0.3) - - assert round(num.font_size, 5) == 14.4 - - -def test_font_size_vs_scale(): - """Test that scale produces the same results as .scale()""" - num = DecimalNumber(0, font_size=12) - num_scale = DecimalNumber(0).scale(1 / 4) - - assert num.height == num_scale.height - - -def test_changing_font_size(): - """Test that the font_size property properly scales DecimalNumber.""" - num = DecimalNumber(0, font_size=12) - num.font_size = 48 - - assert num.height == DecimalNumber(0, font_size=48).height - - -def test_set_value_size(): - """Test that the size of DecimalNumber after set_value is correct.""" - num = DecimalNumber(0).scale(0.3) - test_num = num.copy() - num.set_value(0) - - # round because the height is off by 1e-17 - assert round(num.height, 12) == round(test_num.height, 12) diff --git a/tests/opengl/test_opengl_mobject.py b/tests/opengl/test_opengl_mobject.py deleted file mode 100644 index d5bfac97f9..0000000000 --- a/tests/opengl/test_opengl_mobject.py +++ /dev/null @@ -1,62 +0,0 @@ -from __future__ import annotations - -import pytest - -from manim.mobject.opengl.opengl_mobject import OpenGLMobject - - -def test_opengl_mobject_add(using_opengl_renderer): - """Test OpenGLMobject.add().""" - """Call this function with a Container instance to test its add() method.""" - # check that obj.submobjects is updated correctly - obj = OpenGLMobject() - assert len(obj.submobjects) == 0 - obj.add(OpenGLMobject()) - assert len(obj.submobjects) == 1 - obj.add(*(OpenGLMobject() for _ in range(10))) - assert len(obj.submobjects) == 11 - - # check that adding a OpenGLMobject twice does not actually add it twice - repeated = OpenGLMobject() - obj.add(repeated) - assert len(obj.submobjects) == 12 - obj.add(repeated) - assert len(obj.submobjects) == 12 - - # check that OpenGLMobject.add() returns the OpenGLMobject (for chained calls) - assert obj.add(OpenGLMobject()) is obj - assert len(obj.submobjects) == 13 - - obj = OpenGLMobject() - - # an OpenGLMobject cannot contain itself - with pytest.raises(ValueError) as add_self_info: - obj.add(OpenGLMobject(), obj, OpenGLMobject()) - assert str(add_self_info.value) == ( - "Cannot add OpenGLMobject as a submobject of itself (at index 1)." - ) - assert len(obj.submobjects) == 0 - - # can only add Mobjects - with pytest.raises(TypeError) as add_str_info: - obj.add(OpenGLMobject(), OpenGLMobject(), "foo") - assert str(add_str_info.value) == ( - "Only values of type OpenGLMobject can be added as submobjects of " - "OpenGLMobject, but the value foo (at index 2) is of type str." - ) - assert len(obj.submobjects) == 0 - - -def test_opengl_mobject_remove(using_opengl_renderer): - """Test OpenGLMobject.remove().""" - obj = OpenGLMobject() - to_remove = OpenGLMobject() - obj.add(to_remove) - obj.add(*(OpenGLMobject() for _ in range(10))) - assert len(obj.submobjects) == 11 - obj.remove(to_remove) - assert len(obj.submobjects) == 10 - obj.remove(to_remove) - assert len(obj.submobjects) == 10 - - assert obj.remove(OpenGLMobject()) is obj diff --git a/tests/opengl/test_opengl_surface.py b/tests/opengl/test_opengl_surface.py deleted file mode 100644 index d6897691d4..0000000000 --- a/tests/opengl/test_opengl_surface.py +++ /dev/null @@ -1,14 +0,0 @@ -import numpy as np - -from manim.mobject.opengl.opengl_surface import OpenGLSurface -from manim.mobject.opengl.opengl_three_dimensions import OpenGLSurfaceMesh - - -def test_surface_initialization(using_opengl_renderer): - surface = OpenGLSurface( - lambda u, v: (u, v, u * np.sin(v) + v * np.cos(u)), - u_range=(-3, 3), - v_range=(-3, 3), - ) - - mesh = OpenGLSurfaceMesh(surface) diff --git a/tests/opengl/test_opengl_vectorized_mobject.py b/tests/opengl/test_opengl_vectorized_mobject.py deleted file mode 100644 index ae41f83b61..0000000000 --- a/tests/opengl/test_opengl_vectorized_mobject.py +++ /dev/null @@ -1,368 +0,0 @@ -from __future__ import annotations - -import numpy as np -import pytest - -from manim import Circle, Line, Square, VDict, VGroup, VMobject -from manim.mobject.opengl.opengl_mobject import OpenGLMobject -from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject - - -def test_opengl_vmobject_add(using_opengl_renderer): - """Test the OpenGLVMobject add method.""" - obj = OpenGLVMobject() - assert len(obj.submobjects) == 0 - - obj.add(OpenGLVMobject()) - assert len(obj.submobjects) == 1 - - # Can't add non-OpenGLVMobject values to a VMobject. - with pytest.raises(TypeError) as add_int_info: - obj.add(3) - assert str(add_int_info.value) == ( - "Only values of type OpenGLVMobject can be added as submobjects of " - "OpenGLVMobject, but the value 3 (at index 0) is of type int." - ) - assert len(obj.submobjects) == 1 - - # Plain OpenGLMobjects can't be added to a OpenGLVMobject if they're not - # OpenGLVMobjects. Suggest adding them into an OpenGLGroup instead. - with pytest.raises(TypeError) as add_mob_info: - obj.add(OpenGLMobject()) - assert str(add_mob_info.value) == ( - "Only values of type OpenGLVMobject can be added as submobjects of " - "OpenGLVMobject, but the value OpenGLMobject (at index 0) is of type " - "OpenGLMobject. You can try adding this value into a Group instead." - ) - assert len(obj.submobjects) == 1 - - with pytest.raises(TypeError) as add_vmob_and_mob_info: - # If only one of the added objects is not an instance of VMobject, none of them should be added - obj.add(OpenGLVMobject(), OpenGLMobject()) - assert str(add_vmob_and_mob_info.value) == ( - "Only values of type OpenGLVMobject can be added as submobjects of " - "OpenGLVMobject, but the value OpenGLMobject (at index 1) is of type " - "OpenGLMobject. You can try adding this value into a Group instead." - ) - assert len(obj.submobjects) == 1 - - # A VMobject or VGroup cannot contain itself. - with pytest.raises(ValueError) as add_self_info: - obj.add(obj) - assert str(add_self_info.value) == ( - "Cannot add OpenGLVMobject as a submobject of itself (at index 0)." - ) - assert len(obj.submobjects) == 1 - - -def test_opengl_vmobject_point_from_proportion(using_opengl_renderer): - obj = OpenGLVMobject() - - # One long line, one short line - obj.set_points_as_corners( - [ - np.array([0, 0, 0]), - np.array([4, 0, 0]), - np.array([4, 2, 0]), - ], - ) - - # Total length of 6, so halfway along the object - # would be at length 3, which lands in the first, long line. - np.testing.assert_array_equal(obj.point_from_proportion(0.5), np.array([3, 0, 0])) - - with pytest.raises(ValueError, match="between 0 and 1"): - obj.point_from_proportion(2) - - obj.clear_points() - with pytest.raises(Exception, match="with no points"): - obj.point_from_proportion(0) - - -def test_vgroup_init(using_opengl_renderer): - """Test the VGroup instantiation.""" - VGroup() - VGroup(OpenGLVMobject()) - VGroup(OpenGLVMobject(), OpenGLVMobject()) - - # A VGroup cannot contain non-VMobject values. - with pytest.raises(TypeError) as init_with_float_info: - VGroup(3.0) - assert str(init_with_float_info.value) == ( - "Only values of type OpenGLVMobject can be added as submobjects of " - "VGroup, but the value 3.0 (at index 0 of parameter 0) is of type float." - ) - - with pytest.raises(TypeError) as init_with_mob_info: - VGroup(OpenGLMobject()) - assert str(init_with_mob_info.value) == ( - "Only values of type OpenGLVMobject can be added as submobjects of " - "VGroup, but the value OpenGLMobject (at index 0 of parameter 0) is of type " - "OpenGLMobject. You can try adding this value into a Group instead." - ) - - with pytest.raises(TypeError) as init_with_vmob_and_mob_info: - VGroup(OpenGLVMobject(), OpenGLMobject()) - assert str(init_with_vmob_and_mob_info.value) == ( - "Only values of type OpenGLVMobject can be added as submobjects of " - "VGroup, but the value OpenGLMobject (at index 0 of parameter 1) is of type " - "OpenGLMobject. You can try adding this value into a Group instead." - ) - - -def test_vgroup_init_with_iterable(using_opengl_renderer): - """Test VGroup instantiation with an iterable type.""" - - def type_generator(type_to_generate, n): - return (type_to_generate() for _ in range(n)) - - def mixed_type_generator(major_type, minor_type, minor_type_positions, n): - return ( - minor_type() if i in minor_type_positions else major_type() - for i in range(n) - ) - - obj = VGroup(OpenGLVMobject()) - assert len(obj.submobjects) == 1 - - obj = VGroup(type_generator(OpenGLVMobject, 38)) - assert len(obj.submobjects) == 38 - - obj = VGroup( - OpenGLVMobject(), - [OpenGLVMobject(), OpenGLVMobject()], - type_generator(OpenGLVMobject, 38), - ) - assert len(obj.submobjects) == 41 - - # A VGroup cannot be initialised with an iterable containing a OpenGLMobject - with pytest.raises(TypeError) as init_with_mob_iterable: - VGroup(type_generator(OpenGLMobject, 5)) - assert str(init_with_mob_iterable.value) == ( - "Only values of type OpenGLVMobject can be added as submobjects of VGroup, " - "but the value OpenGLMobject (at index 0 of parameter 0) is of type OpenGLMobject." - ) - - # A VGroup cannot be initialised with an iterable containing a OpenGLMobject in any position - with pytest.raises(TypeError) as init_with_mobs_and_vmobs_iterable: - VGroup(mixed_type_generator(OpenGLVMobject, OpenGLMobject, [3, 5], 7)) - assert str(init_with_mobs_and_vmobs_iterable.value) == ( - "Only values of type OpenGLVMobject can be added as submobjects of VGroup, " - "but the value OpenGLMobject (at index 3 of parameter 0) is of type OpenGLMobject." - ) - - # A VGroup cannot be initialised with an iterable containing non OpenGLVMobject's in any position - with pytest.raises(TypeError) as init_with_float_and_vmobs_iterable: - VGroup(mixed_type_generator(OpenGLVMobject, float, [6, 7], 9)) - assert str(init_with_float_and_vmobs_iterable.value) == ( - "Only values of type OpenGLVMobject can be added as submobjects of VGroup, " - "but the value 0.0 (at index 6 of parameter 0) is of type float." - ) - - # A VGroup cannot be initialised with an iterable containing both OpenGLVMobject's and VMobject's - with pytest.raises(TypeError) as init_with_mobs_and_vmobs_iterable: - VGroup(mixed_type_generator(OpenGLVMobject, VMobject, [3, 5], 7)) - assert str(init_with_mobs_and_vmobs_iterable.value) == ( - "Only values of type OpenGLVMobject can be added as submobjects of VGroup, " - "but the value VMobject (at index 3 of parameter 0) is of type VMobject." - ) - - -def test_vgroup_add(using_opengl_renderer): - """Test the VGroup add method.""" - obj = VGroup() - assert len(obj.submobjects) == 0 - - obj.add(OpenGLVMobject()) - assert len(obj.submobjects) == 1 - - # Can't add non-OpenGLVMobject values to a VMobject. - with pytest.raises(TypeError) as add_int_info: - obj.add(3) - assert str(add_int_info.value) == ( - "Only values of type OpenGLVMobject can be added as submobjects of " - "VGroup, but the value 3 (at index 0 of parameter 0) is of type int." - ) - assert len(obj.submobjects) == 1 - - # Plain OpenGLMobjects can't be added to a OpenGLVMobject if they're not - # OpenGLVMobjects. Suggest adding them into an OpenGLGroup instead. - with pytest.raises(TypeError) as add_mob_info: - obj.add(OpenGLMobject()) - assert str(add_mob_info.value) == ( - "Only values of type OpenGLVMobject can be added as submobjects of " - "VGroup, but the value OpenGLMobject (at index 0 of parameter 0) is of type " - "OpenGLMobject. You can try adding this value into a Group instead." - ) - assert len(obj.submobjects) == 1 - - with pytest.raises(TypeError) as add_vmob_and_mob_info: - # If only one of the added objects is not an instance of VMobject, none of them should be added - obj.add(OpenGLVMobject(), OpenGLMobject()) - assert str(add_vmob_and_mob_info.value) == ( - "Only values of type OpenGLVMobject can be added as submobjects of " - "VGroup, but the value OpenGLMobject (at index 0 of parameter 1) is of type " - "OpenGLMobject. You can try adding this value into a Group instead." - ) - assert len(obj.submobjects) == 1 - - # A VMobject or VGroup cannot contain itself. - with pytest.raises(ValueError) as add_self_info: - obj.add(obj) - assert str(add_self_info.value) == ( - "Cannot add VGroup as a submobject of itself (at index 0)." - ) - assert len(obj.submobjects) == 1 - - -def test_vgroup_add_dunder(using_opengl_renderer): - """Test the VGroup __add__ magic method.""" - obj = VGroup() - assert len(obj.submobjects) == 0 - obj + OpenGLVMobject() - assert len(obj.submobjects) == 0 - obj += OpenGLVMobject() - assert len(obj.submobjects) == 1 - with pytest.raises(TypeError): - obj += OpenGLMobject() - assert len(obj.submobjects) == 1 - with pytest.raises(TypeError): - # If only one of the added object is not an instance of OpenGLVMobject, none of them should be added - obj += (OpenGLVMobject(), OpenGLMobject()) - assert len(obj.submobjects) == 1 - with pytest.raises(ValueError): - # a OpenGLMobject cannot contain itself - obj += obj - - -def test_vgroup_remove(using_opengl_renderer): - """Test the VGroup remove method.""" - a = OpenGLVMobject() - c = OpenGLVMobject() - b = VGroup(c) - obj = VGroup(a, b) - assert len(obj.submobjects) == 2 - assert len(b.submobjects) == 1 - obj.remove(a) - b.remove(c) - assert len(obj.submobjects) == 1 - assert len(b.submobjects) == 0 - obj.remove(b) - assert len(obj.submobjects) == 0 - - -def test_vgroup_remove_dunder(using_opengl_renderer): - """Test the VGroup __sub__ magic method.""" - a = OpenGLVMobject() - c = OpenGLVMobject() - b = VGroup(c) - obj = VGroup(a, b) - assert len(obj.submobjects) == 2 - assert len(b.submobjects) == 1 - assert len(obj - a) == 1 - assert len(obj.submobjects) == 2 - obj -= a - b -= c - assert len(obj.submobjects) == 1 - assert len(b.submobjects) == 0 - obj -= b - assert len(obj.submobjects) == 0 - - -def test_vmob_add_to_back(using_opengl_renderer): - """Test the OpenGLMobject add_to_back method.""" - a = OpenGLVMobject() - b = Line() - c = "text" - with pytest.raises(ValueError): - # OpenGLMobject cannot contain self - a.add_to_back(a) - with pytest.raises(TypeError): - # All submobjects must be of type OpenGLMobject - a.add_to_back(c) - - # No submobject gets added twice - a.add_to_back(b) - a.add_to_back(b, b) - assert len(a.submobjects) == 1 - a.submobjects.clear() - a.add_to_back(b, b, b) - a.add_to_back(b, b) - assert len(a.submobjects) == 1 - a.submobjects.clear() - - # Make sure the ordering has not changed - o1, o2, o3 = Square(), Line(), Circle() - a.add_to_back(o1, o2, o3) - assert a.submobjects.pop() == o3 - assert a.submobjects.pop() == o2 - assert a.submobjects.pop() == o1 - - -def test_vdict_init(using_opengl_renderer): - """Test the VDict instantiation.""" - # Test empty VDict - VDict() - # Test VDict made from list of pairs - VDict([("a", OpenGLVMobject()), ("b", OpenGLVMobject()), ("c", OpenGLVMobject())]) - # Test VDict made from a python dict - VDict({"a": OpenGLVMobject(), "b": OpenGLVMobject(), "c": OpenGLVMobject()}) - # Test VDict made using zip - VDict(zip(["a", "b", "c"], [OpenGLVMobject(), OpenGLVMobject(), OpenGLVMobject()])) - # If the value is of type OpenGLMobject, must raise a TypeError - with pytest.raises(TypeError): - VDict({"a": OpenGLMobject()}) - - -def test_vdict_add(using_opengl_renderer): - """Test the VDict add method.""" - obj = VDict() - assert len(obj.submob_dict) == 0 - obj.add([("a", OpenGLVMobject())]) - assert len(obj.submob_dict) == 1 - with pytest.raises(TypeError): - obj.add([("b", OpenGLMobject())]) - - -def test_vdict_remove(using_opengl_renderer): - """Test the VDict remove method.""" - obj = VDict([("a", OpenGLVMobject())]) - assert len(obj.submob_dict) == 1 - obj.remove("a") - assert len(obj.submob_dict) == 0 - with pytest.raises(KeyError): - obj.remove("a") - - -def test_vgroup_supports_item_assigment(using_opengl_renderer): - """Test VGroup supports array-like assignment for OpenGLVMObjects""" - a = OpenGLVMobject() - b = OpenGLVMobject() - vgroup = VGroup(a) - assert vgroup[0] == a - vgroup[0] = b - assert vgroup[0] == b - assert len(vgroup) == 1 - - -def test_vgroup_item_assignment_at_correct_position(using_opengl_renderer): - """Test VGroup item-assignment adds to correct position for OpenGLVMobjects""" - n_items = 10 - vgroup = VGroup() - for _i in range(n_items): - vgroup.add(OpenGLVMobject()) - new_obj = OpenGLVMobject() - vgroup[6] = new_obj - assert vgroup[6] == new_obj - assert len(vgroup) == n_items - - -def test_vgroup_item_assignment_only_allows_vmobjects(using_opengl_renderer): - """Test VGroup item-assignment raises TypeError when invalid type is passed""" - vgroup = VGroup(OpenGLVMobject()) - with pytest.raises(TypeError) as assign_str_info: - vgroup[0] = "invalid object" - assert str(assign_str_info.value) == ( - "Only values of type OpenGLVMobject can be added as submobjects of " - "VGroup, but the value invalid object (at index 0) is of type str." - ) diff --git a/tests/opengl/test_override_animation_opengl.py b/tests/opengl/test_override_animation_opengl.py deleted file mode 100644 index cb150201c7..0000000000 --- a/tests/opengl/test_override_animation_opengl.py +++ /dev/null @@ -1,100 +0,0 @@ -from __future__ import annotations - -import pytest - -from manim import Animation, override_animation -from manim.mobject.opengl.opengl_mobject import OpenGLMobject -from manim.utils.exceptions import MultiAnimationOverrideException - - -class AnimationA1(Animation): - pass - - -class AnimationA2(Animation): - pass - - -class AnimationA3(Animation): - pass - - -class AnimationB1(AnimationA1): - pass - - -class AnimationC1(AnimationB1): - pass - - -class AnimationX(Animation): - pass - - -class OpenGLMobjectA(OpenGLMobject): - @override_animation(AnimationA1) - def anim_a1(self): - return AnimationA2(self) - - @override_animation(AnimationX) - def anim_x(self, *args, **kwargs): - return args, kwargs - - -class OpenGLMobjectB(OpenGLMobjectA): - pass - - -class OpenGLMobjectC(OpenGLMobjectB): - @override_animation(AnimationA1) - def anim_a1(self): - return AnimationA3(self) - - -class OpenGLMobjectX(OpenGLMobject): - @override_animation(AnimationB1) - def animation(self): - return "Overridden" - - -@pytest.mark.xfail(reason="Needs investigating") -def test_opengl_mobject_inheritance(): - mob = OpenGLMobject() - a = OpenGLMobjectA() - b = OpenGLMobjectB() - c = OpenGLMobjectC() - - assert type(AnimationA1(mob)) is AnimationA1 - assert type(AnimationA1(a)) is AnimationA2 - assert type(AnimationA1(b)) is AnimationA2 - assert type(AnimationA1(c)) is AnimationA3 - - -@pytest.mark.xfail(reason="Needs investigating") -def test_arguments(): - a = OpenGLMobjectA() - args = (1, "two", {"three": 3}, ["f", "o", "u", "r"]) - kwargs = {"test": "manim", "keyword": 42, "arguments": []} - animA = AnimationX(a, *args, **kwargs) - - assert animA[0] == args - assert animA[1] == kwargs - - -@pytest.mark.xfail(reason="Needs investigating") -def test_multi_animation_override_exception(): - with pytest.raises(MultiAnimationOverrideException): - - class OpenGLMobjectB2(OpenGLMobjectA): - @override_animation(AnimationA1) - def anim_a1_different_name(self): - pass - - -@pytest.mark.xfail(reason="Needs investigating") -def test_animation_inheritance(): - x = OpenGLMobjectX() - - assert type(AnimationA1(x)) is AnimationA1 - assert AnimationB1(x) == "Overridden" - assert type(AnimationC1(x)) is AnimationC1 diff --git a/tests/opengl/test_scene_opengl.py b/tests/opengl/test_scene_opengl.py deleted file mode 100644 index e202fa9a7d..0000000000 --- a/tests/opengl/test_scene_opengl.py +++ /dev/null @@ -1,36 +0,0 @@ -from __future__ import annotations - -from manim import Scene, tempconfig -from manim.mobject.opengl.opengl_mobject import OpenGLMobject - - -def test_scene_add_remove(using_opengl_renderer): - with tempconfig({"dry_run": True}): - scene = Scene() - assert len(scene.mobjects) == 0 - scene.add(OpenGLMobject()) - assert len(scene.mobjects) == 1 - scene.add(*(OpenGLMobject() for _ in range(10))) - assert len(scene.mobjects) == 11 - - # Check that adding a mobject twice does not actually add it twice - repeated = OpenGLMobject() - scene.add(repeated) - assert len(scene.mobjects) == 12 - scene.add(repeated) - assert len(scene.mobjects) == 12 - - # Check that Scene.add() returns the Scene (for chained calls) - assert scene.add(OpenGLMobject()) is scene - to_remove = OpenGLMobject() - scene = Scene() - scene.add(to_remove) - scene.add(*(OpenGLMobject() for _ in range(10))) - assert len(scene.mobjects) == 11 - scene.remove(to_remove) - assert len(scene.mobjects) == 10 - scene.remove(to_remove) - assert len(scene.mobjects) == 10 - - # Check that Scene.remove() returns the instance (for chained calls) - assert scene.add(OpenGLMobject()) is scene diff --git a/tests/opengl/test_sound_opengl.py b/tests/opengl/test_sound_opengl.py deleted file mode 100644 index cc4ccceaec..0000000000 --- a/tests/opengl/test_sound_opengl.py +++ /dev/null @@ -1,24 +0,0 @@ -from __future__ import annotations - -import struct -import wave -from pathlib import Path - -import pytest - -from manim import Scene - - -@pytest.mark.xfail(reason="Not currently implemented for opengl") -def test_add_sound(using_opengl_renderer, tmpdir): - # create sound file - sound_loc = Path(tmpdir, "noise.wav") - with wave.open(str(sound_loc), "w") as f: - f.setparams((2, 2, 44100, 0, "NONE", "not compressed")) - for _ in range(22050): # half a second of sound - packed_value = struct.pack("h", 14242) - f.writeframes(packed_value) - f.writeframes(packed_value) - - scene = Scene() - scene.add_sound(sound_loc) diff --git a/tests/opengl/test_stroke_opengl.py b/tests/opengl/test_stroke_opengl.py deleted file mode 100644 index d5eff1ac71..0000000000 --- a/tests/opengl/test_stroke_opengl.py +++ /dev/null @@ -1,18 +0,0 @@ -from __future__ import annotations - -import manim.utils.color as C -from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject - - -def test_stroke_props_in_ctor(using_opengl_renderer): - m = OpenGLVMobject(stroke_color=C.ORANGE, stroke_width=10) - assert m.stroke_color.to_hex() == C.ORANGE.to_hex() - assert m.stroke_width == 10 - - -def test_set_stroke(using_opengl_renderer): - m = OpenGLVMobject() - m.set_stroke(color=C.ORANGE, width=2, opacity=0.8) - assert m.stroke_width == 2 - assert m.stroke_opacity == 0.8 - assert m.stroke_color.to_hex() == C.ORANGE.to_hex() diff --git a/tests/opengl/test_svg_mobject_opengl.py b/tests/opengl/test_svg_mobject_opengl.py deleted file mode 100644 index c1d11f42e5..0000000000 --- a/tests/opengl/test_svg_mobject_opengl.py +++ /dev/null @@ -1,56 +0,0 @@ -from __future__ import annotations - -from manim import * -from tests.helpers.path_utils import get_svg_resource - - -def test_set_fill_color(using_opengl_renderer): - expected_color = "#FF862F" - svg = SVGMobject(get_svg_resource("heart.svg"), fill_color=expected_color) - assert svg.fill_color.to_hex() == expected_color - - -def test_set_stroke_color(using_opengl_renderer): - expected_color = "#FFFDDD" - svg = SVGMobject(get_svg_resource("heart.svg"), stroke_color=expected_color) - assert svg.stroke_color.to_hex() == expected_color - - -def test_set_color_sets_fill_and_stroke(using_opengl_renderer): - expected_color = "#EEE777" - svg = SVGMobject(get_svg_resource("heart.svg"), color=expected_color) - assert svg.color.to_hex() == expected_color - assert svg.fill_color.to_hex() == expected_color - assert svg.stroke_color.to_hex() == expected_color - - -def test_set_fill_opacity(using_opengl_renderer): - expected_opacity = 0.5 - svg = SVGMobject(get_svg_resource("heart.svg"), fill_opacity=expected_opacity) - assert svg.fill_opacity == expected_opacity - - -def test_stroke_opacity(using_opengl_renderer): - expected_opacity = 0.4 - svg = SVGMobject(get_svg_resource("heart.svg"), stroke_opacity=expected_opacity) - assert svg.stroke_opacity == expected_opacity - - -def test_fill_overrides_color(using_opengl_renderer): - expected_color = "#343434" - svg = SVGMobject( - get_svg_resource("heart.svg"), - color="#123123", - fill_color=expected_color, - ) - assert svg.fill_color.to_hex() == expected_color - - -def test_stroke_overrides_color(using_opengl_renderer): - expected_color = "#767676" - svg = SVGMobject( - get_svg_resource("heart.svg"), - color="#334433", - stroke_color=expected_color, - ) - assert svg.stroke_color.to_hex() == expected_color diff --git a/tests/opengl/test_texmobject_opengl.py b/tests/opengl/test_texmobject_opengl.py deleted file mode 100644 index e9826f9d8f..0000000000 --- a/tests/opengl/test_texmobject_opengl.py +++ /dev/null @@ -1,110 +0,0 @@ -from __future__ import annotations - -from pathlib import Path - -import pytest - -from manim import MathTex, SingleStringMathTex, Tex - - -def test_MathTex(config, using_opengl_renderer): - MathTex("a^2 + b^2 = c^2") - assert Path(config.media_dir, "Tex", "e4be163a00cf424f.svg").exists() - - -def test_SingleStringMathTex(config, using_opengl_renderer): - SingleStringMathTex("test") - assert Path(config.media_dir, "Tex", "8ce17c7f5013209f.svg").exists() - - -@pytest.mark.parametrize( # : PT006 - ("text_input", "length_sub"), - [("{{ a }} + {{ b }} = {{ c }}", 5), (r"\frac{1}{a+b\sqrt{2}}", 1)], -) -def test_double_braces_testing(using_opengl_renderer, text_input, length_sub): - t1 = MathTex(text_input) - assert len(t1.submobjects) == length_sub - - -def test_tex(config, using_opengl_renderer): - Tex("The horse does not eat cucumber salad.") - assert Path(config.media_dir, "Tex", "c3945e23e546c95a.svg").exists() - - -def test_tex_whitespace_arg(using_opengl_renderer): - """Check that correct number of submobjects are created per string with whitespace separator""" - separator = "\t" - str_part_1 = "Hello" - str_part_2 = "world" - str_part_3 = "It is" - str_part_4 = "me!" - tex = Tex(str_part_1, str_part_2, str_part_3, str_part_4, arg_separator=separator) - assert len(tex) == 4 - assert len(tex[0]) == len("".join((str_part_1 + separator).split())) - assert len(tex[1]) == len("".join((str_part_2 + separator).split())) - assert len(tex[2]) == len("".join((str_part_3 + separator).split())) - assert len(tex[3]) == len("".join(str_part_4.split())) - - -def test_tex_non_whitespace_arg(using_opengl_renderer): - """Check that correct number of submobjects are created per string with non_whitespace characters""" - separator = "," - str_part_1 = "Hello" - str_part_2 = "world" - str_part_3 = "It is" - str_part_4 = "me!" - tex = Tex(str_part_1, str_part_2, str_part_3, str_part_4, arg_separator=separator) - assert len(tex) == 4 - assert len(tex[0]) == len("".join((str_part_1 + separator).split())) - assert len(tex[1]) == len("".join((str_part_2 + separator).split())) - assert len(tex[2]) == len("".join((str_part_3 + separator).split())) - assert len(tex[3]) == len("".join(str_part_4.split())) - - -def test_tex_white_space_and_non_whitespace_args(using_opengl_renderer): - """Check that correct number of submobjects are created per string when mixing characters with whitespace""" - separator = ", \n . \t\t" - str_part_1 = "Hello" - str_part_2 = "world" - str_part_3 = "It is" - str_part_4 = "me!" - tex = Tex(str_part_1, str_part_2, str_part_3, str_part_4, arg_separator=separator) - assert len(tex) == 4 - assert len(tex[0]) == len("".join((str_part_1 + separator).split())) - assert len(tex[1]) == len("".join((str_part_2 + separator).split())) - assert len(tex[2]) == len("".join((str_part_3 + separator).split())) - assert len(tex[3]) == len("".join(str_part_4.split())) - - -def test_tex_size(using_opengl_renderer): - """Check that the size of a :class:`Tex` string is not changed.""" - text = Tex("what").center() - vertical = text.get_top() - text.get_bottom() - horizontal = text.get_right() - text.get_left() - assert round(vertical[1], 4) == 0.3512 - assert round(horizontal[0], 4) == 1.0420 - - -def test_font_size(using_opengl_renderer): - """Test that tex_mobject classes return - the correct font_size value after being scaled. - """ - string = MathTex(0).scale(0.3) - - assert round(string.font_size, 5) == 14.4 - - -def test_font_size_vs_scale(using_opengl_renderer): - """Test that scale produces the same results as .scale()""" - num = MathTex(0, font_size=12) - num_scale = MathTex(0).scale(1 / 4) - - assert num.height == num_scale.height - - -def test_changing_font_size(using_opengl_renderer): - """Test that the font_size property properly scales tex_mobject.py classes.""" - num = Tex("0", font_size=12) - num.font_size = 48 - - assert num.height == Tex("0", font_size=48).height diff --git a/tests/opengl/test_text_mobject_opengl.py b/tests/opengl/test_text_mobject_opengl.py deleted file mode 100644 index db6a6532f4..0000000000 --- a/tests/opengl/test_text_mobject_opengl.py +++ /dev/null @@ -1,14 +0,0 @@ -from __future__ import annotations - -from manim.mobject.text.text_mobject import MarkupText, Text - - -def test_font_size(using_opengl_renderer): - """Test that Text and MarkupText return the - correct font_size value after being scaled. - """ - text_string = Text("0").scale(0.3) - markuptext_string = MarkupText("0").scale(0.3) - - assert round(text_string.font_size, 5) == 14.4 - assert round(markuptext_string.font_size, 5) == 14.4 diff --git a/tests/opengl/test_ticks_opengl.py b/tests/opengl/test_ticks_opengl.py deleted file mode 100644 index 3fe4a4f3a0..0000000000 --- a/tests/opengl/test_ticks_opengl.py +++ /dev/null @@ -1,60 +0,0 @@ -from __future__ import annotations - -import numpy as np - -from manim import PI, Axes, NumberLine - - -def test_duplicate_ticks_removed_for_axes(using_opengl_renderer): - axis = NumberLine( - x_range=[-10, 10], - ) - ticks = axis.get_tick_range() - assert np.unique(ticks).size == ticks.size - - -def test_ticks_not_generated_on_origin_for_axes(using_opengl_renderer): - axes = Axes( - x_range=[-10, 10], - y_range=[-10, 10], - axis_config={"include_ticks": True}, - ) - - x_axis_range = axes.x_axis.get_tick_range() - y_axis_range = axes.y_axis.get_tick_range() - - assert 0 not in x_axis_range - assert 0 not in y_axis_range - - -def test_expected_ticks_generated(using_opengl_renderer): - axes = Axes(x_range=[-2, 2], y_range=[-2, 2], axis_config={"include_ticks": True}) - x_axis_range = axes.x_axis.get_tick_range() - y_axis_range = axes.y_axis.get_tick_range() - - assert 1 in x_axis_range - assert 1 in y_axis_range - assert -1 in x_axis_range - assert -1 in y_axis_range - - -def test_ticks_generated_from_origin_for_axes(using_opengl_renderer): - axes = Axes( - x_range=[-PI, PI], - y_range=[-PI, PI], - axis_config={"include_ticks": True}, - ) - x_axis_range = axes.x_axis.get_tick_range() - y_axis_range = axes.y_axis.get_tick_range() - - assert -2 in x_axis_range - assert -1 in x_axis_range - assert 0 not in x_axis_range - assert 1 in x_axis_range - assert 2 in x_axis_range - - assert -2 in y_axis_range - assert -1 in y_axis_range - assert 0 not in y_axis_range - assert 1 in y_axis_range - assert 2 in y_axis_range diff --git a/tests/opengl/test_unit_geometry_opengl.py b/tests/opengl/test_unit_geometry_opengl.py deleted file mode 100644 index f92bc9ff37..0000000000 --- a/tests/opengl/test_unit_geometry_opengl.py +++ /dev/null @@ -1,11 +0,0 @@ -from __future__ import annotations - -import numpy as np - -from manim import Sector - - -def test_get_arc_center(using_opengl_renderer): - np.testing.assert_array_equal( - Sector(arc_center=[1, 2, 0]).get_arc_center(), [1, 2, 0] - ) diff --git a/tests/opengl/test_value_tracker_opengl.py b/tests/opengl/test_value_tracker_opengl.py deleted file mode 100644 index ad8ac3acfe..0000000000 --- a/tests/opengl/test_value_tracker_opengl.py +++ /dev/null @@ -1,101 +0,0 @@ -from __future__ import annotations - -from manim.mobject.value_tracker import ComplexValueTracker, ValueTracker - - -def test_value_tracker_set_value(using_opengl_renderer): - """Test ValueTracker.set_value()""" - tracker = ValueTracker() - tracker.set_value(10.0) - assert tracker.get_value() == 10.0 - - -def test_value_tracker_get_value(using_opengl_renderer): - """Test ValueTracker.get_value()""" - tracker = ValueTracker(10.0) - assert tracker.get_value() == 10.0 - - -def test_value_tracker_interpolate(using_opengl_renderer): - """Test ValueTracker.interpolate()""" - tracker1 = ValueTracker(1.0) - tracker2 = ValueTracker(2.5) - tracker3 = ValueTracker().interpolate(tracker1, tracker2, 0.7) - assert tracker3.get_value() == 2.05 - - -def test_value_tracker_increment_value(using_opengl_renderer): - """Test ValueTracker.increment_value()""" - tracker = ValueTracker(0.0) - tracker.increment_value(10.0) - assert tracker.get_value() == 10.0 - - -def test_value_tracker_bool(using_opengl_renderer): - """Test ValueTracker.__bool__()""" - tracker = ValueTracker(0.0) - assert not tracker - tracker.increment_value(1.0) - assert tracker - - -def test_value_tracker_iadd(using_opengl_renderer): - """Test ValueTracker.__iadd__()""" - tracker = ValueTracker(0.0) - tracker += 10.0 - assert tracker.get_value() == 10.0 - - -def test_value_tracker_ifloordiv(using_opengl_renderer): - """Test ValueTracker.__ifloordiv__()""" - tracker = ValueTracker(5.0) - tracker //= 2.0 - assert tracker.get_value() == 2.0 - - -def test_value_tracker_imod(using_opengl_renderer): - """Test ValueTracker.__imod__()""" - tracker = ValueTracker(20.0) - tracker %= 3.0 - assert tracker.get_value() == 2.0 - - -def test_value_tracker_imul(using_opengl_renderer): - """Test ValueTracker.__imul__()""" - tracker = ValueTracker(3.0) - tracker *= 4.0 - assert tracker.get_value() == 12.0 - - -def test_value_tracker_ipow(using_opengl_renderer): - """Test ValueTracker.__ipow__()""" - tracker = ValueTracker(3.0) - tracker **= 3.0 - assert tracker.get_value() == 27.0 - - -def test_value_tracker_isub(using_opengl_renderer): - """Test ValueTracker.__isub__()""" - tracker = ValueTracker(20.0) - tracker -= 10.0 - assert tracker.get_value() == 10.0 - - -def test_value_tracker_itruediv(using_opengl_renderer): - """Test ValueTracker.__itruediv__()""" - tracker = ValueTracker(5.0) - tracker /= 2.0 - assert tracker.get_value() == 2.5 - - -def test_complex_value_tracker_set_value(using_opengl_renderer): - """Test ComplexValueTracker.set_value()""" - tracker = ComplexValueTracker() - tracker.set_value(1 + 2j) - assert tracker.get_value() == 1 + 2j - - -def test_complex_value_tracker_get_value(using_opengl_renderer): - """Test ComplexValueTracker.get_value()""" - tracker = ComplexValueTracker(2.0 - 3.0j) - assert tracker.get_value() == 2.0 - 3.0j diff --git a/tests/test_config.py b/tests/test_config.py index 0703b31bf4..d020ef5a84 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -6,7 +6,7 @@ import numpy as np import pytest -from manim import WHITE, Scene, Square, Tex, Text, tempconfig +from manim import WHITE, Manager, Scene, Square, Tex, Text, tempconfig from manim._config.utils import ManimConfig from tests.assert_utils import assert_dir_exists, assert_dir_filled, assert_file_exists @@ -61,16 +61,16 @@ def test_transparent(config): config.verbosity = "ERROR" config.dry_run = True - scene = MyScene() - scene.render() - frame = scene.renderer.get_frame() + manager = Manager(MyScene) + manager.render() + frame = manager.renderer.get_pixels() np.testing.assert_allclose(frame[0, 0], [0, 0, 0, 255]) config.transparent = True - scene = MyScene() - scene.render() - frame = scene.renderer.get_frame() + manager = Manager(MyScene) + manager.render() + frame = manager.renderer.get_pixels() np.testing.assert_allclose(frame[0, 0], [0, 0, 0, 0]) @@ -78,9 +78,9 @@ def test_transparent_by_background_opacity(config, dry_run): config.background_opacity = 0.5 assert config.transparent is True - scene = MyScene() - scene.render() - frame = scene.renderer.get_frame() + manager = Manager(MyScene) + manager.render() + frame = manager.renderer.get_pixels() np.testing.assert_allclose(frame[0, 0], [0, 0, 0, 127]) assert config.movie_file_extension == ".mov" assert config.transparent is True @@ -92,9 +92,9 @@ def test_background_color(config): config.verbosity = "ERROR" config.dry_run = True - scene = MyScene() - scene.render() - frame = scene.renderer.get_frame() + manager = Manager(MyScene) + manager.render() + frame = manager.renderer.get_pixels() np.testing.assert_allclose(frame[0, 0], [255, 255, 255, 255]) @@ -134,8 +134,8 @@ def test_custom_dirs(tmp_path, config): config.tex_dir = "{media_dir}/test_tex" config.log_dir = "{media_dir}/test_log" - scene = MyScene() - scene.render() + manager = Manager(MyScene) + manager.render() tmp_path = Path(tmp_path) assert_dir_filled(tmp_path / "test_sections") assert_file_exists(tmp_path / "test_sections/MyScene.json") @@ -215,8 +215,8 @@ def test_dry_run_with_png_format(config, dry_run): config.write_to_movie = False config.disable_caching = True assert config.dry_run is True - scene = MyScene() - scene.render() + manager = Manager(MyScene) + manager.render() def test_dry_run_with_png_format_skipped_animations(config, dry_run): @@ -224,8 +224,8 @@ def test_dry_run_with_png_format_skipped_animations(config, dry_run): config.write_to_movie = False config.disable_caching = True assert config["dry_run"] is True - scene = MyScene(skip_animations=True) - scene.render() + manager = Manager(MyScene) + manager.render() def test_tex_template_file(tmp_path): diff --git a/tests/test_graphical_units/control_data/opengl/Circle.npz b/tests/test_graphical_units/control_data/opengl/Circle.npz deleted file mode 100644 index 8cc6e43c48..0000000000 Binary files a/tests/test_graphical_units/control_data/opengl/Circle.npz and /dev/null differ diff --git a/tests/test_graphical_units/control_data/opengl/FixedMobjects3D.npz b/tests/test_graphical_units/control_data/opengl/FixedMobjects3D.npz deleted file mode 100644 index 204e542a8d..0000000000 Binary files a/tests/test_graphical_units/control_data/opengl/FixedMobjects3D.npz and /dev/null differ diff --git a/tests/test_graphical_units/test_axes.py b/tests/test_graphical_units/test_axes.py index 2e06300e46..2337918e90 100644 --- a/tests/test_graphical_units/test_axes.py +++ b/tests/test_graphical_units/test_axes.py @@ -7,7 +7,7 @@ @frames_comparison -def test_axes(scene): +def test_axes(scene: Scene) -> None: graph = Axes( x_range=[-10, 10, 1], y_range=[-10, 10, 1], @@ -21,14 +21,14 @@ def test_axes(scene): @frames_comparison -def test_plot_functions(scene, use_vectorized): +def test_plot_functions(scene: Scene, use_vectorized: bool) -> None: ax = Axes(x_range=(-10, 10.3), y_range=(-1.5, 1.5)) graph = ax.plot(lambda x: x**2, use_vectorized=use_vectorized) scene.add(ax, graph) @frames_comparison -def test_custom_coordinates(scene): +def test_custom_coordinates(scene: Scene) -> None: ax = Axes(x_range=[0, 10]) ax.add_coordinates( @@ -38,14 +38,14 @@ def test_custom_coordinates(scene): @frames_comparison -def test_get_axis_labels(scene): +def test_get_axis_labels(scene: Scene) -> None: ax = Axes() labels = ax.get_axis_labels(Tex("$x$-axis").scale(0.7), Tex("$y$-axis").scale(0.45)) scene.add(ax, labels) @frames_comparison -def test_get_x_axis_label(scene): +def test_get_x_axis_label(scene: Scene) -> None: ax = Axes(x_range=(0, 8), y_range=(0, 5), x_length=8, y_length=5) x_label = ax.get_x_axis_label( Tex("$x$-values").scale(0.65), @@ -57,7 +57,7 @@ def test_get_x_axis_label(scene): @frames_comparison -def test_get_y_axis_label(scene): +def test_get_y_axis_label(scene: Scene) -> None: ax = Axes(x_range=(0, 8), y_range=(0, 5), x_length=8, y_length=5) y_label = ax.get_y_axis_label( Tex("$y$-values").scale(0.65).rotate(90 * DEGREES), @@ -69,7 +69,7 @@ def test_get_y_axis_label(scene): @frames_comparison -def test_axis_tip_default_width_height(scene): +def test_axis_tip_default_width_height(scene: Scene) -> None: ax = Axes( x_range=(0, 4), y_range=(0, 4), @@ -80,7 +80,7 @@ def test_axis_tip_default_width_height(scene): @frames_comparison -def test_axis_tip_custom_width_height(scene): +def test_axis_tip_custom_width_height(scene: Scene) -> None: ax = Axes( x_range=(0, 4), y_range=(0, 4), @@ -93,7 +93,7 @@ def test_axis_tip_custom_width_height(scene): @frames_comparison -def test_plot_derivative_graph(scene, use_vectorized): +def test_plot_derivative_graph(scene: Scene, use_vectorized: bool) -> None: ax = NumberPlane(y_range=[-1, 7], background_line_style={"stroke_opacity": 0.4}) curve_1 = ax.plot(lambda x: x**2, color=PURPLE_B, use_vectorized=use_vectorized) @@ -104,7 +104,7 @@ def test_plot_derivative_graph(scene, use_vectorized): @frames_comparison -def test_plot(scene, use_vectorized): +def test_plot(scene: Scene, use_vectorized: bool) -> None: # construct the axes ax_1 = Axes( x_range=[0.001, 6], @@ -150,7 +150,7 @@ def log_func(x): @frames_comparison -def test_get_graph_label(scene): +def test_get_graph_label(scene: Scene) -> None: ax = Axes() sin = ax.plot(lambda x: np.sin(x), color=PURPLE_B) label = ax.get_graph_label( @@ -166,7 +166,7 @@ def test_get_graph_label(scene): @frames_comparison -def test_get_lines_to_point(scene): +def test_get_lines_to_point(scene: Scene) -> None: ax = Axes() circ = Circle(radius=0.5).move_to([-4, -1.5, 0]) @@ -176,7 +176,7 @@ def test_get_lines_to_point(scene): @frames_comparison -def test_plot_line_graph(scene): +def test_plot_line_graph(scene: Scene) -> None: plane = NumberPlane( x_range=(0, 7), y_range=(0, 5), @@ -199,7 +199,7 @@ def test_plot_line_graph(scene): @frames_comparison -def test_t_label(scene): +def test_t_label(scene: Scene) -> None: # defines the axes and linear function axes = Axes(x_range=[-1, 10], y_range=[-1, 10], x_length=9, y_length=6) func = axes.plot(lambda x: x, color=BLUE) @@ -209,7 +209,7 @@ def test_t_label(scene): @frames_comparison -def test_get_area(scene): +def test_get_area(scene: Scene) -> None: ax = Axes().add_coordinates() curve1 = ax.plot( lambda x: 2 * np.sin(x), @@ -235,7 +235,7 @@ def test_get_area(scene): @frames_comparison -def test_get_area_with_boundary_and_few_plot_points(scene): +def test_get_area_with_boundary_and_few_plot_points(scene: Scene) -> None: ax = Axes(x_range=[-2, 2], y_range=[-2, 2], color=WHITE) f1 = ax.plot(lambda t: t, [-1, 1, 0.5]) f2 = ax.plot(lambda t: 1, [-1, 1, 0.5]) @@ -246,7 +246,7 @@ def test_get_area_with_boundary_and_few_plot_points(scene): @frames_comparison -def test_get_riemann_rectangles(scene, use_vectorized): +def test_get_riemann_rectangles(scene: Scene, use_vectorized: bool) -> None: ax = Axes(y_range=[-2, 10]) quadratic = ax.plot(lambda x: 0.5 * x**2 - 0.5, use_vectorized=use_vectorized) @@ -282,16 +282,16 @@ def test_get_riemann_rectangles(scene, use_vectorized): scene.add(ax, bounding_line, quadratic, rects_right, rects_left, bounded_rects) -@frames_comparison(base_scene=ThreeDScene) -def test_get_z_axis_label(scene): +@frames_comparison +def test_get_z_axis_label(scene: Scene) -> None: ax = ThreeDAxes() lab = ax.get_z_axis_label(Tex("$z$-label")) - scene.set_camera_orientation(phi=2 * PI / 5, theta=PI / 5) + scene.camera.set_orientation(theta=PI / 5, phi=2 * PI / 5) scene.add(ax, lab) @frames_comparison -def test_polar_graph(scene): +def test_polar_graph(scene: Scene) -> None: polar = PolarPlane() def r(theta): @@ -302,7 +302,7 @@ def r(theta): @frames_comparison -def test_log_scaling_graph(scene): +def test_log_scaling_graph(scene: Scene) -> None: ax = Axes( x_range=[0, 8], y_range=[-2, 4], diff --git a/tests/test_graphical_units/test_coordinate_systems.py b/tests/test_graphical_units/test_coordinate_systems.py index 7d6dad67af..b0220a4532 100644 --- a/tests/test_graphical_units/test_coordinate_systems.py +++ b/tests/test_graphical_units/test_coordinate_systems.py @@ -37,7 +37,7 @@ def test_line_graph(scene): scene.add(plane, first_line, second_line) -@frames_comparison(base_scene=ThreeDScene) +@frames_comparison def test_plot_surface(scene): axes = ThreeDAxes(x_range=(-5, 5, 1), y_range=(-5, 5, 1), z_range=(-5, 5, 1)) @@ -57,7 +57,7 @@ def param_trig(u, v): scene.add(axes, trig_plane) -@frames_comparison(base_scene=ThreeDScene) +@frames_comparison def test_plot_surface_colorscale(scene): axes = ThreeDAxes(x_range=(-3, 3, 1), y_range=(-3, 3, 1), z_range=(-5, 5, 1)) diff --git a/tests/test_graphical_units/test_mobjects.py b/tests/test_graphical_units/test_mobjects.py index c76c8599d7..612535d7ec 100644 --- a/tests/test_graphical_units/test_mobjects.py +++ b/tests/test_graphical_units/test_mobjects.py @@ -6,7 +6,7 @@ __module_test__ = "mobjects" -@frames_comparison(base_scene=ThreeDScene) +@frames_comparison def test_PointCloudDot(scene): p = PointCloudDot() scene.add(p) diff --git a/tests/test_graphical_units/test_opengl.py b/tests/test_graphical_units/test_opengl.py deleted file mode 100644 index 6d36f8761a..0000000000 --- a/tests/test_graphical_units/test_opengl.py +++ /dev/null @@ -1,27 +0,0 @@ -from __future__ import annotations - -from manim import * -from manim.renderer.opengl_renderer import OpenGLRenderer -from manim.utils.testing.frames_comparison import frames_comparison - -__module_test__ = "opengl" - - -@frames_comparison(renderer_class=OpenGLRenderer, renderer="opengl") -def test_Circle(scene): - circle = Circle().set_color(RED) - scene.add(circle) - scene.wait() - - -@frames_comparison( - renderer_class=OpenGLRenderer, - renderer="opengl", -) -def test_FixedMobjects3D(scene: Scene): - scene.renderer.camera.set_euler_angles(phi=75 * DEGREES, theta=-45 * DEGREES) - circ = Circle(fill_opacity=1).to_edge(LEFT) - square = Square(fill_opacity=1).to_edge(RIGHT) - triangle = Triangle(fill_opacity=1).to_corner(UR) - [i.fix_orientation() for i in (circ, square)] - triangle.fix_in_frame() diff --git a/tests/test_graphical_units/test_threed.py b/tests/test_graphical_units/test_threed.py index b6079e5e4c..d45a4fa1f0 100644 --- a/tests/test_graphical_units/test_threed.py +++ b/tests/test_graphical_units/test_threed.py @@ -6,34 +6,34 @@ __module_test__ = "threed" -@frames_comparison(base_scene=ThreeDScene) -def test_AddFixedInFrameMobjects(scene): - scene.set_camera_orientation(phi=75 * DEGREES, theta=-45 * DEGREES) +@frames_comparison +def test_AddFixedInFrameMobjects(scene: Scene) -> None: + scene.camera.set_orientation(theta=-45 * DEGREES, phi=75 * DEGREES) text = Tex("This is a 3D tex") - scene.add_fixed_in_frame_mobjects(text) + scene.add(text) -@frames_comparison(base_scene=ThreeDScene) -def test_Cube(scene): +@frames_comparison +def test_Cube(scene: Scene) -> None: scene.add(Cube()) -@frames_comparison(base_scene=ThreeDScene) -def test_Sphere(scene): +@frames_comparison +def test_Sphere(scene: Scene) -> None: scene.add(Sphere()) -@frames_comparison(base_scene=ThreeDScene) -def test_Dot3D(scene): +@frames_comparison +def test_Dot3D(scene: Scene) -> None: scene.add(Dot3D()) -@frames_comparison(base_scene=ThreeDScene) -def test_Cone(scene): +@frames_comparison +def test_Cone(scene: Scene) -> None: scene.add(Cone(resolution=16)) -def test_Cone_get_start_and_get_end(): +def test_Cone_get_start_and_get_end() -> None: cone = Cone().shift(RIGHT).rotate(PI / 4, about_point=ORIGIN, about_edge=OUT) start = [0.70710678, 0.70710678, -1.0] end = [0.70710678, 0.70710678, 0.0] @@ -45,13 +45,13 @@ def test_Cone_get_start_and_get_end(): ), "end points of Cone do not match" -@frames_comparison(base_scene=ThreeDScene) -def test_Cylinder(scene): +@frames_comparison +def test_Cylinder(scene: Scene) -> None: scene.add(Cylinder()) -@frames_comparison(base_scene=ThreeDScene) -def test_Line3D(scene): +@frames_comparison +def test_Line3D(scene: Scene) -> None: line1 = Line3D(resolution=16).shift(LEFT * 2) line2 = Line3D(resolution=16).shift(RIGHT * 2) perp_line = Line3D.perpendicular_to(line1, UP + OUT, resolution=16) @@ -59,48 +59,56 @@ def test_Line3D(scene): scene.add(line1, line2, perp_line, parallel_line) -@frames_comparison(base_scene=ThreeDScene) -def test_Arrow3D(scene): +@frames_comparison +def test_Arrow3D(scene: Scene) -> None: scene.add(Arrow3D(resolution=16)) -@frames_comparison(base_scene=ThreeDScene) -def test_Torus(scene): +@frames_comparison +def test_Torus(scene: Scene) -> None: scene.add(Torus()) -@frames_comparison(base_scene=ThreeDScene) -def test_Axes(scene): +@frames_comparison +def test_Axes(scene: Scene) -> None: scene.add(ThreeDAxes()) -@frames_comparison(base_scene=ThreeDScene) -def test_CameraMoveAxes(scene): +@frames_comparison +def test_CameraMoveAxes(scene: Scene) -> None: """Tests camera movement to explore varied views of a static scene.""" axes = ThreeDAxes() scene.add(axes) scene.add(Dot([1, 2, 3])) - scene.move_camera(phi=PI / 8, theta=-PI / 8, frame_center=[1, 2, 3], zoom=2) + scene.play( + scene.camera.animate.set_orientation( + theta=-PI / 8, phi=PI / 8, frame_center=[1, 2, 3], zoom=2 + ) + ) -@frames_comparison(base_scene=ThreeDScene) -def test_CameraMove(scene): +@frames_comparison +def test_CameraMove(scene: Scene) -> None: cube = Cube() scene.add(cube) - scene.move_camera(phi=PI / 4, theta=PI / 4, frame_center=[0, 0, -1], zoom=0.5) + scene.play( + scene.camera.animate.set_orientation( + theta=PI / 4, phi=PI / 4, frame_center=[0, 0, -1], zoom=0.5 + ) + ) -@frames_comparison(base_scene=ThreeDScene) -def test_AmbientCameraMove(scene): +@frames_comparison +def test_AmbientCameraMove(scene: Scene) -> None: cube = Cube() - scene.begin_ambient_camera_rotation(rate=0.5) - scene.add(cube) + scene.camera.begin_ambient_rotation(rate=0.5) + scene.add(cube, scene.camera) scene.wait() -@frames_comparison(base_scene=ThreeDScene) -def test_MovingVertices(scene): - scene.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES) +@frames_comparison +def test_MovingVertices(scene: Scene) -> None: + scene.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES) vertices = [1, 2, 3, 4] edges = [(1, 2), (2, 3), (3, 4), (1, 3), (1, 4)] g = Graph(vertices, edges) @@ -114,10 +122,10 @@ def test_MovingVertices(scene): scene.wait() -@frames_comparison(base_scene=ThreeDScene) -def test_SurfaceColorscale(scene): +@frames_comparison +def test_SurfaceColorscale(scene: Scene) -> None: resolution_fa = 16 - scene.set_camera_orientation(phi=75 * DEGREES, theta=-30 * DEGREES) + scene.camera.set_orientation(theta=-30 * DEGREES, phi=75 * DEGREES) axes = ThreeDAxes(x_range=(-3, 3, 1), y_range=(-3, 3, 1), z_range=(-4, 4, 1)) def param_trig(u, v): @@ -138,10 +146,10 @@ def param_trig(u, v): scene.add(axes, trig_plane) -@frames_comparison(base_scene=ThreeDScene) -def test_Y_Direction(scene): +@frames_comparison +def test_Y_Direction(scene: Scene) -> None: resolution_fa = 16 - scene.set_camera_orientation(phi=75 * DEGREES, theta=-120 * DEGREES) + scene.camera.set_orientation(theta=-120 * DEGREES, phi=75 * DEGREES) axes = ThreeDAxes(x_range=(0, 5, 1), y_range=(0, 5, 1), z_range=(-1, 1, 0.5)) def param_surface(u, v): @@ -163,7 +171,7 @@ def param_surface(u, v): scene.add(axes, surface_plane) -def test_get_start_and_end_Arrow3d(): +def test_get_start_and_end_Arrow3d() -> None: start, end = ORIGIN, np.array([2, 1, 0]) arrow = Arrow3D(start, end) assert np.allclose( diff --git a/tests/test_linear_transformation_scene.py b/tests/test_linear_transformation_scene.py index d0592b8f44..c2b6349d3c 100644 --- a/tests/test_linear_transformation_scene.py +++ b/tests/test_linear_transformation_scene.py @@ -1,25 +1,26 @@ -from manim import RIGHT, UP, LinearTransformationScene, Vector, VGroup +from manim import RIGHT, UP, LinearTransformationScene, Manager, Vector, VGroup __module_test__ = "vector_space_scene" def test_ghost_vectors_len_and_types(): - scene = LinearTransformationScene() + manager = Manager(LinearTransformationScene) + scene: LinearTransformationScene = manager.scene scene.leave_ghost_vectors = True - # prepare vectors (they require a vmobject as their target) + # Prepare vectors. They require a VMobject as their target. v1, v2 = Vector(RIGHT), Vector(RIGHT) v1.target, v2.target = Vector(UP), Vector(UP) - # ghost_vector addition is in this method + # Ghost_vector addition is in this method: scene.get_piece_movement((v1, v2)) ghosts = scene.get_ghost_vectors() assert len(ghosts) == 1 - # check if there are two vectors in the ghost vector VGroup + # Check if there are two vectors in the ghost vector VGroup. assert len(ghosts[0]) == 2 - # check types of ghost vectors + # Check types of ghost vectors. assert isinstance(ghosts, VGroup) assert isinstance(ghosts[0], VGroup) assert all(isinstance(x, Vector) for x in ghosts[0]) diff --git a/tests/test_scene_rendering/opengl/__init__.py b/tests/test_scene_rendering/opengl/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/test_scene_rendering/opengl/test_caching_related_opengl.py b/tests/test_scene_rendering/opengl/test_caching_related_opengl.py deleted file mode 100644 index c9c82a449b..0000000000 --- a/tests/test_scene_rendering/opengl/test_caching_related_opengl.py +++ /dev/null @@ -1,63 +0,0 @@ -from __future__ import annotations - -import sys - -import pytest - -from manim import capture - -from ...utils.video_tester import video_comparison - - -@pytest.mark.slow -@video_comparison( - "SceneWithMultipleWaitCallsWithNFlag.json", - "videos/simple_scenes/480p15/SceneWithMultipleWaitCalls.mp4", -) -def test_wait_skip(tmp_path, manim_cfg_file, simple_scenes_path): - # Test for PR #468. Intended to test if wait calls are correctly skipped. - scene_name = "SceneWithMultipleWaitCalls" - command = [ - sys.executable, - "-m", - "manim", - "--renderer", - "opengl", - "--write_to_movie", - "-ql", - "--media_dir", - str(tmp_path), - "-n", - "3", - str(simple_scenes_path), - scene_name, - ] - out, err, exit_code = capture(command) - assert exit_code == 0, err - - -@pytest.mark.slow -@video_comparison( - "SceneWithMultiplePlayCallsWithNFlag.json", - "videos/simple_scenes/480p15/SceneWithMultipleCalls.mp4", -) -def test_play_skip(tmp_path, manim_cfg_file, simple_scenes_path): - # Intended to test if play calls are correctly skipped. - scene_name = "SceneWithMultipleCalls" - command = [ - sys.executable, - "-m", - "manim", - "--renderer", - "opengl", - "--write_to_movie", - "-ql", - "--media_dir", - str(tmp_path), - "-n", - "3", - str(simple_scenes_path), - scene_name, - ] - out, err, exit_code = capture(command) - assert exit_code == 0, err diff --git a/tests/test_scene_rendering/opengl/test_cli_flags_opengl.py b/tests/test_scene_rendering/opengl/test_cli_flags_opengl.py deleted file mode 100644 index 5b3a299dc5..0000000000 --- a/tests/test_scene_rendering/opengl/test_cli_flags_opengl.py +++ /dev/null @@ -1,692 +0,0 @@ -from __future__ import annotations - -import sys - -import numpy as np -import pytest -from click.testing import CliRunner -from PIL import Image - -from manim import capture, get_video_metadata -from manim.__main__ import __version__, main -from manim.utils.file_ops import add_version_before_extension -from tests.utils.video_tester import video_comparison - - -@pytest.mark.slow -@video_comparison( - "SquareToCircleWithDefaultValues.json", - "videos/simple_scenes/1080p60/SquareToCircle.mp4", -) -def test_basic_scene_with_default_values(tmp_path, manim_cfg_file, simple_scenes_path): - scene_name = "SquareToCircle" - command = [ - sys.executable, - "-m", - "manim", - "--renderer", - "opengl", - "--write_to_movie", - "--media_dir", - str(tmp_path), - str(simple_scenes_path), - scene_name, - ] - out, err, exit_code = capture(command) - assert exit_code == 0, err - - -@pytest.mark.slow -def test_resolution_flag(tmp_path, manim_cfg_file, simple_scenes_path): - scene_name = "NoAnimations" - # test different separators - resolutions = [ - (720, 480, ";"), - (1280, 720, ","), - (1920, 1080, "-"), - (2560, 1440, ";"), - # (3840, 2160, ","), - # (640, 480, "-"), - # (800, 600, ";"), - ] - - for width, height, separator in resolutions: - command = [ - sys.executable, - "-m", - "manim", - "--media_dir", - str(tmp_path), - "--resolution", - f"{width}{separator}{height}", - str(simple_scenes_path), - scene_name, - ] - - _, err, exit_code = capture(command) - assert exit_code == 0, err - - path = ( - tmp_path / "videos" / "simple_scenes" / f"{height}p60" / f"{scene_name}.mp4" - ) - meta = get_video_metadata(path) - assert (width, height) == (meta["width"], meta["height"]) - - -@pytest.mark.slow -@video_comparison( - "SquareToCircleWithlFlag.json", - "videos/simple_scenes/480p15/SquareToCircle.mp4", -) -def test_basic_scene_l_flag(tmp_path, manim_cfg_file, simple_scenes_path): - scene_name = "SquareToCircle" - command = [ - sys.executable, - "-m", - "manim", - "--renderer", - "opengl", - "-ql", - "--write_to_movie", - "--media_dir", - str(tmp_path), - str(simple_scenes_path), - scene_name, - ] - out, err, exit_code = capture(command) - assert exit_code == 0, err - - -@pytest.mark.slow -@video_comparison( - "SceneWithMultipleCallsWithNFlag.json", - "videos/simple_scenes/480p15/SceneWithMultipleCalls.mp4", -) -def test_n_flag(tmp_path, simple_scenes_path): - scene_name = "SceneWithMultipleCalls" - command = [ - sys.executable, - "-m", - "manim", - "-ql", - "--renderer", - "opengl", - "--write_to_movie", - "-n 3,6", - "--media_dir", - str(tmp_path), - str(simple_scenes_path), - scene_name, - ] - _, err, exit_code = capture(command) - assert exit_code == 0, err - - -@pytest.mark.slow -def test_s_flag_no_animations(tmp_path, manim_cfg_file, simple_scenes_path): - scene_name = "NoAnimations" - command = [ - sys.executable, - "-m", - "manim", - "--renderer", - "opengl", - "-ql", - "-s", - "--media_dir", - str(tmp_path), - str(simple_scenes_path), - scene_name, - ] - out, err, exit_code = capture(command) - assert exit_code == 0, err - - exists = (tmp_path / "videos").exists() - assert not exists, "running manim with -s flag rendered a video" - - is_empty = not any((tmp_path / "images" / "simple_scenes").iterdir()) - assert not is_empty, "running manim with -s flag did not render an image" - - -@pytest.mark.slow -def test_image_output_for_static_scene(tmp_path, manim_cfg_file, simple_scenes_path): - scene_name = "StaticScene" - command = [ - sys.executable, - "-m", - "manim", - "--renderer", - "opengl", - "-ql", - "--media_dir", - str(tmp_path), - str(simple_scenes_path), - scene_name, - ] - out, err, exit_code = capture(command) - assert exit_code == 0, err - - exists = (tmp_path / "videos").exists() - assert not exists, "running manim with static scene rendered a video" - - is_empty = not any((tmp_path / "images" / "simple_scenes").iterdir()) - assert not is_empty, "running manim without animations did not render an image" - - -@pytest.mark.slow -def test_no_image_output_with_interactive_embed( - tmp_path, manim_cfg_file, simple_scenes_path -): - """Check no image is output for a static scene when interactive embed is called""" - scene_name = "InteractiveStaticScene" - command = [ - sys.executable, - "-m", - "manim", - "--renderer", - "opengl", - "-ql", - "--media_dir", - str(tmp_path), - str(simple_scenes_path), - scene_name, - ] - out, err, exit_code = capture(command) - assert exit_code == 0, err - - exists = (tmp_path / "videos").exists() - assert not exists, "running manim with static scene rendered a video" - - is_empty = not any((tmp_path / "images" / "simple_scenes").iterdir()) - assert ( - is_empty - ), "running manim static scene with interactive embed rendered an image" - - -@pytest.mark.slow -def test_no_default_image_output_with_non_static_scene( - tmp_path, manim_cfg_file, simple_scenes_path -): - scene_name = "SceneWithNonStaticWait" - command = [ - sys.executable, - "-m", - "manim", - "--renderer", - "opengl", - "-ql", - "--media_dir", - str(tmp_path), - str(simple_scenes_path), - scene_name, - ] - out, err, exit_code = capture(command) - assert exit_code == 0, err - - exists = (tmp_path / "videos").exists() - assert not exists, "running manim with static scene rendered a video" - - is_empty = not any((tmp_path / "images" / "simple_scenes").iterdir()) - assert ( - is_empty - ), "running manim static scene with interactive embed rendered an image" - - -@pytest.mark.slow -def test_image_output_for_static_scene_with_write_to_movie( - tmp_path, manim_cfg_file, simple_scenes_path -): - scene_name = "StaticScene" - command = [ - sys.executable, - "-m", - "manim", - "--write_to_movie", - "--renderer", - "opengl", - "-ql", - "--media_dir", - str(tmp_path), - str(simple_scenes_path), - scene_name, - ] - out, err, exit_code = capture(command) - assert exit_code == 0, err - - is_empty = not any((tmp_path / "videos").iterdir()) - assert not is_empty, "running manim with static scene rendered a video" - - is_empty = not any((tmp_path / "images" / "simple_scenes").iterdir()) - assert not is_empty, "running manim without animations did not render an image" - - -@pytest.mark.slow -def test_s_flag(tmp_path, manim_cfg_file, simple_scenes_path): - scene_name = "SquareToCircle" - command = [ - sys.executable, - "-m", - "manim", - "--renderer", - "opengl", - "-ql", - "-s", - "--media_dir", - str(tmp_path), - str(simple_scenes_path), - scene_name, - ] - out, err, exit_code = capture(command) - assert exit_code == 0, err - - exists = (tmp_path / "videos").exists() - assert not exists, "running manim with -s flag rendered a video" - - is_empty = not any((tmp_path / "images" / "simple_scenes").iterdir()) - assert not is_empty, "running manim with -s flag did not render an image" - - -@pytest.mark.slow -def test_r_flag(tmp_path, manim_cfg_file, simple_scenes_path): - scene_name = "SquareToCircle" - command = [ - sys.executable, - "-m", - "manim", - "--renderer", - "opengl", - "-ql", - "-s", - "--media_dir", - str(tmp_path), - "-r", - "200,100", - str(simple_scenes_path), - scene_name, - ] - out, err, exit_code = capture(command) - assert exit_code == 0, err - - is_not_empty = any((tmp_path / "images").iterdir()) - assert is_not_empty, "running manim with -s, -r flag did not render a file" - - filename = add_version_before_extension( - tmp_path / "images" / "simple_scenes" / "SquareToCircle.png", - ) - assert np.asarray(Image.open(filename)).shape == (100, 200, 4) - - -@pytest.mark.slow -def test_a_flag(tmp_path, manim_cfg_file, infallible_scenes_path): - command = [ - sys.executable, - "-m", - "manim", - "--renderer", - "opengl", - "--write_to_movie", - "-ql", - "--media_dir", - str(tmp_path), - "-a", - str(infallible_scenes_path), - ] - out, err, exit_code = capture(command) - assert exit_code == 0, err - - one_is_not_empty = ( - tmp_path / "videos" / "infallible_scenes" / "480p15" / "Wait1.mp4" - ).is_file() - assert one_is_not_empty, "running manim with -a flag did not render the first scene" - - two_is_not_empty = ( - tmp_path / "images" / "infallible_scenes" / f"Wait2_ManimCE_v{__version__}.png" - ).is_file() - assert two_is_not_empty, "running manim with -a flag did not render an image, possible leak of the config dictionary" - - three_is_not_empty = ( - tmp_path / "videos" / "infallible_scenes" / "480p15" / "Wait3.mp4" - ).is_file() - assert ( - three_is_not_empty - ), "running manim with -a flag did not render the second scene" - - -@pytest.mark.slow -def test_custom_folders(tmp_path, manim_cfg_file, simple_scenes_path): - scene_name = "SquareToCircle" - command = [ - sys.executable, - "-m", - "manim", - "--renderer", - "opengl", - "-ql", - "-s", - "--media_dir", - str(tmp_path), - "--custom_folders", - str(simple_scenes_path), - scene_name, - ] - out, err, exit_code = capture(command) - assert exit_code == 0, err - - exists = (tmp_path / "videos").exists() - assert not exists, "--custom_folders produced a 'videos/' dir" - - exists = add_version_before_extension(tmp_path / "SquareToCircle.png").exists() - assert exists, "--custom_folders did not produce the output file" - - -@pytest.mark.slow -def test_dash_as_filename(tmp_path): - code = ( - "class Test(Scene):\n" - " def construct(self):\n" - " self.add(Circle())\n" - " self.wait()" - ) - command = [ - "-ql", - "--renderer", - "opengl", - "-s", - "--media_dir", - str(tmp_path), - "-", - ] - runner = CliRunner() - result = runner.invoke(main, command, input=code) - assert result.exit_code == 0 - exists = add_version_before_extension( - tmp_path / "images" / "-" / "Test.png", - ).exists() - assert exists, result.output - - -@pytest.mark.slow -def test_gif_format_output(tmp_path, manim_cfg_file, simple_scenes_path): - """Test only gif created with manim version in file name when --format gif is set""" - scene_name = "SquareToCircle" - command = [ - sys.executable, - "-m", - "manim", - "--renderer", - "opengl", - "-ql", - "--media_dir", - str(tmp_path), - "--format", - "gif", - str(simple_scenes_path), - scene_name, - ] - out, err, exit_code = capture(command) - assert exit_code == 0, err - - unexpected_mp4_path = ( - tmp_path / "videos" / "simple_scenes" / "480p15" / "SquareToCircle.mp4" - ) - assert not unexpected_mp4_path.exists(), "unexpected mp4 file found at " + str( - unexpected_mp4_path, - ) - - expected_gif_path = add_version_before_extension( - tmp_path / "videos" / "simple_scenes" / "480p15" / "SquareToCircle.gif" - ) - assert expected_gif_path.exists(), "gif file not found at " + str(expected_gif_path) - - -@pytest.mark.slow -def test_mp4_format_output(tmp_path, manim_cfg_file, simple_scenes_path): - """Test only mp4 created without manim version in file name when --format mp4 is set""" - scene_name = "SquareToCircle" - command = [ - sys.executable, - "-m", - "manim", - "--renderer", - "opengl", - "-ql", - "--media_dir", - str(tmp_path), - "--format", - "mp4", - str(simple_scenes_path), - scene_name, - ] - out, err, exit_code = capture(command) - assert exit_code == 0, err - - unexpected_gif_path = add_version_before_extension( - tmp_path / "videos" / "simple_scenes" / "480p15" / "SquareToCircle.gif" - ) - assert not unexpected_gif_path.exists(), "unexpected gif file found at " + str( - unexpected_gif_path, - ) - - expected_mp4_path = ( - tmp_path / "videos" / "simple_scenes" / "480p15" / "SquareToCircle.mp4" - ) - assert expected_mp4_path.exists(), "expected mp4 file not found at " + str( - expected_mp4_path, - ) - - -@pytest.mark.slow -def test_videos_not_created_when_png_format_set( - tmp_path, - manim_cfg_file, - simple_scenes_path, -): - """Test mp4 and gifs are not created when --format png is set""" - scene_name = "SquareToCircle" - command = [ - sys.executable, - "-m", - "manim", - "--renderer", - "opengl", - "-ql", - "--media_dir", - str(tmp_path), - "--format", - "png", - str(simple_scenes_path), - scene_name, - ] - out, err, exit_code = capture(command) - assert exit_code == 0, err - - unexpected_gif_path = add_version_before_extension( - tmp_path / "videos" / "simple_scenes" / "480p15" / "SquareToCircle.gif" - ) - assert not unexpected_gif_path.exists(), "unexpected gif file found at " + str( - unexpected_gif_path, - ) - - unexpected_mp4_path = ( - tmp_path / "videos" / "simple_scenes" / "480p15" / "SquareToCircle.mp4" - ) - assert not unexpected_mp4_path.exists(), "expected mp4 file not found at " + str( - unexpected_mp4_path, - ) - - -@pytest.mark.slow -def test_images_are_created_when_png_format_set( - tmp_path, - manim_cfg_file, - simple_scenes_path, -): - """Test images are created in media directory when --format png is set""" - scene_name = "SquareToCircle" - command = [ - sys.executable, - "-m", - "manim", - "--renderer", - "opengl", - "-ql", - "--media_dir", - str(tmp_path), - "--format", - "png", - str(simple_scenes_path), - scene_name, - ] - out, err, exit_code = capture(command) - assert exit_code == 0, err - - expected_png_path = tmp_path / "images" / "simple_scenes" / "SquareToCircle0000.png" - assert expected_png_path.exists(), "png file not found at " + str(expected_png_path) - - -@pytest.mark.slow -def test_images_are_zero_padded_when_zero_pad_set( - tmp_path, - manim_cfg_file, - simple_scenes_path, -): - """Test images are zero padded when --format png and --zero_pad n are set""" - scene_name = "SquareToCircle" - command = [ - sys.executable, - "-m", - "manim", - "--renderer", - "opengl", - "-ql", - "--media_dir", - str(tmp_path), - "--format", - "png", - "--zero_pad", - "3", - str(simple_scenes_path), - scene_name, - ] - out, err, exit_code = capture(command) - assert exit_code == 0, err - - unexpected_png_path = tmp_path / "images" / "simple_scenes" / "SquareToCircle0.png" - assert not unexpected_png_path.exists(), "non zero padded png file found at " + str( - unexpected_png_path, - ) - - expected_png_path = tmp_path / "images" / "simple_scenes" / "SquareToCircle000.png" - assert expected_png_path.exists(), "png file not found at " + str(expected_png_path) - - -@pytest.mark.slow -def test_webm_format_output(tmp_path, manim_cfg_file, simple_scenes_path): - """Test only webm created when --format webm is set""" - scene_name = "SquareToCircle" - command = [ - sys.executable, - "-m", - "manim", - "--renderer", - "opengl", - "-ql", - "--media_dir", - str(tmp_path), - "--format", - "webm", - str(simple_scenes_path), - scene_name, - ] - out, err, exit_code = capture(command) - assert exit_code == 0, err - - unexpected_mp4_path = ( - tmp_path / "videos" / "simple_scenes" / "480p15" / "SquareToCircle.mp4" - ) - assert not unexpected_mp4_path.exists(), "unexpected mp4 file found at " + str( - unexpected_mp4_path, - ) - - expected_webm_path = ( - tmp_path / "videos" / "simple_scenes" / "480p15" / "SquareToCircle.webm" - ) - assert expected_webm_path.exists(), "expected webm file not found at " + str( - expected_webm_path, - ) - - -@pytest.mark.slow -def test_default_format_output_for_transparent_flag( - tmp_path, - manim_cfg_file, - simple_scenes_path, -): - """Test .mov is created by default when transparent flag is set""" - scene_name = "SquareToCircle" - command = [ - sys.executable, - "-m", - "manim", - "--renderer", - "opengl", - "-ql", - "--write_to_movie", - "--media_dir", - str(tmp_path), - "-t", - str(simple_scenes_path), - scene_name, - ] - out, err, exit_code = capture(command) - assert exit_code == 0, err - - unexpected_webm_path = ( - tmp_path / "videos" / "simple_scenes" / "480p15" / "SquareToCircle.webm" - ) - assert not unexpected_webm_path.exists(), "unexpected webm file found at " + str( - unexpected_webm_path, - ) - - expected_mov_path = ( - tmp_path / "videos" / "simple_scenes" / "480p15" / "SquareToCircle.mov" - ) - assert expected_mov_path.exists(), "expected .mov file not found at " + str( - expected_mov_path, - ) - - -@pytest.mark.slow -def test_mov_can_be_set_as_output_format(tmp_path, manim_cfg_file, simple_scenes_path): - """Test .mov is created by when set using --format mov arg""" - scene_name = "SquareToCircle" - command = [ - sys.executable, - "-m", - "manim", - "--renderer", - "opengl", - "-ql", - "--media_dir", - str(tmp_path), - "--format", - "mov", - str(simple_scenes_path), - scene_name, - ] - out, err, exit_code = capture(command) - assert exit_code == 0, err - - unexpected_webm_path = ( - tmp_path / "videos" / "simple_scenes" / "480p15" / "SquareToCircle.webm" - ) - assert not unexpected_webm_path.exists(), "unexpected webm file found at " + str( - unexpected_webm_path, - ) - - expected_mov_path = ( - tmp_path / "videos" / "simple_scenes" / "480p15" / "SquareToCircle.mov" - ) - assert expected_mov_path.exists(), "expected .mov file not found at " + str( - expected_mov_path, - ) diff --git a/tests/test_scene_rendering/opengl/test_opengl_renderer.py b/tests/test_scene_rendering/opengl/test_opengl_renderer.py deleted file mode 100644 index f2236cb04d..0000000000 --- a/tests/test_scene_rendering/opengl/test_opengl_renderer.py +++ /dev/null @@ -1,119 +0,0 @@ -from __future__ import annotations - -import platform -from unittest.mock import Mock - -import numpy as np -import pytest - -from manim.renderer.opengl_renderer import OpenGLRenderer -from tests.assert_utils import assert_file_exists -from tests.test_scene_rendering.simple_scenes import * - - -def test_write_to_movie_disables_window( - config, using_temp_opengl_config, disabling_caching -): - """write_to_movie should disable window by default""" - scene = SquareToCircle() - renderer = scene.renderer - renderer.update_frame = Mock(wraps=renderer.update_frame) - scene.render() - assert renderer.window is None - assert_file_exists(config.output_file) - - -@pytest.mark.skip(reason="Temporarily skip due to failing in Windows CI") -def test_force_window_opengl_render_with_movies( - config, - using_temp_opengl_config, - force_window_config_write_to_movie, - disabling_caching, -): - """force_window creates window when write_to_movie is set""" - scene = SquareToCircle() - renderer = scene.renderer - renderer.update_frame = Mock(wraps=renderer.update_frame) - scene.render() - assert renderer.window is not None - assert_file_exists(config["output_file"]) - renderer.window.close() - - -@pytest.mark.skipif( - platform.processor() == "aarch64", reason="Fails on Linux-ARM runners" -) -def test_force_window_opengl_render_with_format( - using_temp_opengl_config, - force_window_config_pngs, - disabling_caching, -): - """force_window creates window when format is set""" - scene = SquareToCircle() - renderer = scene.renderer - renderer.update_frame = Mock(wraps=renderer.update_frame) - scene.render() - assert renderer.window is not None - renderer.window.close() - - -def test_get_frame_with_preview_disabled(config, using_opengl_renderer): - """Get frame is able to fetch frame with the correct dimensions when preview is disabled""" - config.preview = False - - scene = SquareToCircle() - assert isinstance(scene.renderer, OpenGLRenderer) - assert not config.preview - - renderer = scene.renderer - renderer.update_frame(scene) - frame = renderer.get_frame() - - # height and width are flipped - assert renderer.get_pixel_shape()[0] == frame.shape[1] - assert renderer.get_pixel_shape()[1] == frame.shape[0] - - -@pytest.mark.slow -def test_get_frame_with_preview_enabled(config, using_opengl_renderer): - """Get frame is able to fetch frame with the correct dimensions when preview is enabled""" - config.preview = True - - scene = SquareToCircle() - assert isinstance(scene.renderer, OpenGLRenderer) - assert config.preview is True - - renderer = scene.renderer - renderer.update_frame(scene) - frame = renderer.get_frame() - - # height and width are flipped - assert renderer.get_pixel_shape()[0] == frame.shape[1] - assert renderer.get_pixel_shape()[1] == frame.shape[0] - - -def test_pixel_coords_to_space_coords(config, using_opengl_renderer): - config.preview = True - - scene = SquareToCircle() - assert isinstance(scene.renderer, OpenGLRenderer) - - renderer = scene.renderer - renderer.update_frame(scene) - - px, py = 3, 2 - pw, ph = renderer.get_pixel_shape() - _, fh = renderer.camera.get_shape() - fc = renderer.camera.get_center() - - ex = fc[0] + (fh / ph) * (px - pw / 2) - ey = fc[1] + (fh / ph) * (py - ph / 2) - ez = fc[2] - - assert ( - renderer.pixel_coords_to_space_coords(px, py) == np.array([ex, ey, ez]) - ).all() - assert ( - renderer.pixel_coords_to_space_coords(px, py, top_left=True) - == np.array([ex, -ey, ez]) - ).all() diff --git a/tests/test_scene_rendering/opengl/test_play_logic_opengl.py b/tests/test_scene_rendering/opengl/test_play_logic_opengl.py deleted file mode 100644 index 64c4c39204..0000000000 --- a/tests/test_scene_rendering/opengl/test_play_logic_opengl.py +++ /dev/null @@ -1,119 +0,0 @@ -from __future__ import annotations - -import sys -from unittest.mock import Mock - -import pytest - -from manim import ( - Scene, - ValueTracker, - np, -) - -from ..simple_scenes import ( - SceneForFrozenFrameTests, - SceneWithMultipleCalls, - SceneWithNonStaticWait, - SceneWithSceneUpdater, - SceneWithStaticWait, - SquareToCircle, -) - - -@pytest.mark.skipif( - sys.version_info < (3, 8), - reason="Mock object has a different implementation in python 3.7, which makes it broken with this logic.", -) -@pytest.mark.parametrize("frame_rate", argvalues=[15, 30, 60]) -def test_t_values(config, using_temp_opengl_config, disabling_caching, frame_rate): - """Test that the framerate corresponds to the number of t values generated""" - config.frame_rate = frame_rate - scene = SquareToCircle() - scene.update_to_time = Mock() - scene.render() - assert scene.update_to_time.call_count == config["frame_rate"] - np.testing.assert_allclose( - ([call.args[0] for call in scene.update_to_time.call_args_list]), - np.arange(0, 1, 1 / config["frame_rate"]), - ) - - -def test_t_values_with_skip_animations(using_temp_opengl_config, disabling_caching): - """Test the behaviour of scene.skip_animations""" - scene = SquareToCircle() - scene.update_to_time = Mock() - scene.renderer._original_skipping_status = True - scene.render() - assert scene.update_to_time.call_count == 1 - np.testing.assert_almost_equal( - scene.update_to_time.call_args.args[0], - 1.0, - ) - - -def test_static_wait_detection(using_temp_opengl_config, disabling_caching): - """Test if a static wait (wait that freeze the frame) is correctly detected""" - scene = SceneWithStaticWait() - scene.render() - # Test is is_static_wait of the Wait animation has been set to True by compile_animation_ata - assert scene.animations[0].is_static_wait - assert scene.is_current_animation_frozen_frame() - - -def test_non_static_wait_detection(using_temp_opengl_config, disabling_caching): - scene = SceneWithNonStaticWait() - scene.render() - assert not scene.animations[0].is_static_wait - assert not scene.is_current_animation_frozen_frame() - scene = SceneWithSceneUpdater() - scene.render() - assert not scene.animations[0].is_static_wait - assert not scene.is_current_animation_frozen_frame() - - -def test_frozen_frame(using_temp_opengl_config, disabling_caching): - scene = SceneForFrozenFrameTests() - scene.render() - assert scene.mobject_update_count == 0 - assert scene.scene_update_count == 0 - - -@pytest.mark.xfail(reason="Should be fixed in #2133") -def test_t_values_with_cached_data(using_temp_opengl_config): - """Test the proper generation and use of the t values when an animation is cached.""" - scene = SceneWithMultipleCalls() - # Mocking the file_writer will skip all the writing process. - scene.renderer.file_writer = Mock(scene.renderer.file_writer) - # Simulate that all animations are cached. - scene.renderer.file_writer.is_already_cached.return_value = True - scene.update_to_time = Mock() - - scene.render() - assert scene.update_to_time.call_count == 10 - - -@pytest.mark.xfail(reason="Not currently handled correctly for opengl") -def test_t_values_save_last_frame(config, using_temp_opengl_config): - """Test that there is only one t value handled when only saving the last frame""" - config.save_last_frame = True - scene = SquareToCircle() - scene.update_to_time = Mock() - scene.render() - scene.update_to_time.assert_called_once_with(1) - - -def test_animate_with_changed_custom_attribute(using_temp_opengl_config): - """Test that animating the change of a custom attribute - using the animate syntax works correctly. - """ - - class CustomAnimateScene(Scene): - def construct(self): - vt = ValueTracker(0) - vt.custom_attribute = "hello" - self.play(vt.animate.set_value(42).set(custom_attribute="world")) - assert vt.get_value() == 42 - assert vt.custom_attribute == "world" - - CustomAnimateScene().render() diff --git a/tests/test_scene_rendering/test_cairo_renderer.py b/tests/test_scene_rendering/test_cairo_renderer.py deleted file mode 100644 index 5134578aa3..0000000000 --- a/tests/test_scene_rendering/test_cairo_renderer.py +++ /dev/null @@ -1,86 +0,0 @@ -from __future__ import annotations - -from unittest.mock import Mock, patch - -import pytest - -from manim import * - -from ..assert_utils import assert_file_exists -from .simple_scenes import * - - -def test_render(using_temp_config, disabling_caching): - scene = SquareToCircle() - renderer = scene.renderer - renderer.update_frame = Mock(wraps=renderer.update_frame) - renderer.add_frame = Mock(wraps=renderer.add_frame) - scene.render() - assert renderer.add_frame.call_count == config["frame_rate"] - assert renderer.update_frame.call_count == config["frame_rate"] - assert_file_exists(config["output_file"]) - - -def test_skipping_status_with_from_to_and_up_to(using_temp_config, disabling_caching): - """Test if skip_animations is well updated when -n flag is passed""" - config.from_animation_number = 2 - config.upto_animation_number = 6 - - class SceneWithMultipleCalls(Scene): - def construct(self): - number = Integer(0) - self.add(number) - for i in range(10): - self.play(Animation(Square())) - - assert ((i >= 2) and (i <= 6)) or self.renderer.skip_animations - - SceneWithMultipleCalls().render() - - -@pytest.mark.xfail(reason="caching issue") -def test_when_animation_is_cached(using_temp_config): - partial_movie_files = [] - for _ in range(2): - # Render several times to generate a cache. - # In some edgy cases and on some OS, a same scene can produce - # a (finite, generally 2) number of different hash. In this case, the scene wouldn't be detected as cached, making the test fail. - scene = SquareToCircle() - scene.render() - partial_movie_files.append(scene.renderer.file_writer.partial_movie_files) - scene = SquareToCircle() - scene.update_to_time = Mock() - scene.render() - assert scene.renderer.file_writer.is_already_cached( - scene.renderer.animations_hashes[0], - ) - # Check that the same partial movie files has been used (with he same hash). - # As there might have been several hashes, a list is used. - assert scene.renderer.file_writer.partial_movie_files in partial_movie_files - # Check that manim correctly skipped the animation. - scene.update_to_time.assert_called_once_with(1) - # Check that the output video has been generated. - assert_file_exists(config["output_file"]) - - -def test_hash_logic_is_not_called_when_caching_is_disabled( - using_temp_config, - disabling_caching, -): - with patch("manim.renderer.cairo_renderer.get_hash_from_play_call") as mocked: - scene = SquareToCircle() - scene.render() - mocked.assert_not_called() - assert_file_exists(config["output_file"]) - - -def test_hash_logic_is_called_when_caching_is_enabled(using_temp_config): - from manim.renderer.cairo_renderer import get_hash_from_play_call - - with patch( - "manim.renderer.cairo_renderer.get_hash_from_play_call", - wraps=get_hash_from_play_call, - ) as mocked: - scene = SquareToCircle() - scene.render() - mocked.assert_called_once() diff --git a/tests/test_scene_rendering/test_cli_flags.py b/tests/test_scene_rendering/test_cli_flags.py index c9516c6fc4..2a25deea6f 100644 --- a/tests/test_scene_rendering/test_cli_flags.py +++ b/tests/test_scene_rendering/test_cli_flags.py @@ -19,7 +19,9 @@ "SquareToCircleWithDefaultValues.json", "videos/simple_scenes/1080p60/SquareToCircle.mp4", ) -def test_basic_scene_with_default_values(tmp_path, manim_cfg_file, simple_scenes_path): +def test_basic_scene_with_default_values( + tmp_path, manim_cfg_file, simple_scenes_path, write_to_movie +): scene_name = "SquareToCircle" command = [ sys.executable, @@ -35,7 +37,7 @@ def test_basic_scene_with_default_values(tmp_path, manim_cfg_file, simple_scenes @pytest.mark.slow -def test_resolution_flag(tmp_path, manim_cfg_file, simple_scenes_path): +def test_resolution_flag(tmp_path, manim_cfg_file, simple_scenes_path, write_to_movie): scene_name = "NoAnimations" # test different separators resolutions = [ @@ -76,7 +78,9 @@ def test_resolution_flag(tmp_path, manim_cfg_file, simple_scenes_path): "SquareToCircleWithlFlag.json", "videos/simple_scenes/480p15/SquareToCircle.mp4", ) -def test_basic_scene_l_flag(tmp_path, manim_cfg_file, simple_scenes_path): +def test_basic_scene_l_flag( + tmp_path, manim_cfg_file, simple_scenes_path, write_to_movie +): scene_name = "SquareToCircle" command = [ sys.executable, @@ -97,7 +101,7 @@ def test_basic_scene_l_flag(tmp_path, manim_cfg_file, simple_scenes_path): "SceneWithMultipleCallsWithNFlag.json", "videos/simple_scenes/480p15/SceneWithMultipleCalls.mp4", ) -def test_n_flag(tmp_path, simple_scenes_path): +def test_n_flag(tmp_path, simple_scenes_path, write_to_movie): scene_name = "SceneWithMultipleCalls" command = [ sys.executable, @@ -115,7 +119,9 @@ def test_n_flag(tmp_path, simple_scenes_path): @pytest.mark.slow -def test_s_flag_no_animations(tmp_path, manim_cfg_file, simple_scenes_path): +def test_s_flag_no_animations( + tmp_path, manim_cfg_file, simple_scenes_path, write_to_movie +): scene_name = "NoAnimations" command = [ sys.executable, @@ -139,7 +145,7 @@ def test_s_flag_no_animations(tmp_path, manim_cfg_file, simple_scenes_path): @pytest.mark.slow -def test_s_flag(tmp_path, manim_cfg_file, simple_scenes_path): +def test_s_flag(tmp_path, manim_cfg_file, simple_scenes_path, write_to_movie): scene_name = "SquareToCircle" command = [ sys.executable, @@ -163,33 +169,7 @@ def test_s_flag(tmp_path, manim_cfg_file, simple_scenes_path): @pytest.mark.slow -def test_s_flag_opengl_renderer(tmp_path, manim_cfg_file, simple_scenes_path): - scene_name = "SquareToCircle" - command = [ - sys.executable, - "-m", - "manim", - "-ql", - "-s", - "--renderer", - "opengl", - "--media_dir", - str(tmp_path), - str(simple_scenes_path), - scene_name, - ] - out, err, exit_code = capture(command) - assert exit_code == 0, err - - exists = (tmp_path / "videos").exists() - assert not exists, "running manim with -s flag rendered a video" - - is_empty = not any((tmp_path / "images" / "simple_scenes").iterdir()) - assert not is_empty, "running manim with -s flag did not render an image" - - -@pytest.mark.slow -def test_r_flag(tmp_path, manim_cfg_file, simple_scenes_path): +def test_r_flag(tmp_path, manim_cfg_file, simple_scenes_path, write_to_movie): scene_name = "SquareToCircle" command = [ sys.executable, @@ -217,7 +197,7 @@ def test_r_flag(tmp_path, manim_cfg_file, simple_scenes_path): @pytest.mark.slow -def test_a_flag(tmp_path, manim_cfg_file, infallible_scenes_path): +def test_a_flag(tmp_path, manim_cfg_file, infallible_scenes_path, write_to_movie): command = [ sys.executable, "-m", @@ -250,7 +230,7 @@ def test_a_flag(tmp_path, manim_cfg_file, infallible_scenes_path): @pytest.mark.slow -def test_custom_folders(tmp_path, manim_cfg_file, simple_scenes_path): +def test_custom_folders(tmp_path, manim_cfg_file, simple_scenes_path, write_to_movie): scene_name = "SquareToCircle" command = [ sys.executable, @@ -275,7 +255,7 @@ def test_custom_folders(tmp_path, manim_cfg_file, simple_scenes_path): @pytest.mark.slow -def test_custom_output_name_gif(tmp_path, simple_scenes_path): +def test_custom_output_name_gif(tmp_path, simple_scenes_path, write_to_movie): scene_name = "SquareToCircle" custom_name = "custom_name" command = [ @@ -318,7 +298,7 @@ def test_custom_output_name_gif(tmp_path, simple_scenes_path): @pytest.mark.slow -def test_custom_output_name_mp4(tmp_path, simple_scenes_path): +def test_custom_output_name_mp4(tmp_path, simple_scenes_path, write_to_movie): scene_name = "SquareToCircle" custom_name = "custom_name" command = [ @@ -359,7 +339,7 @@ def test_custom_output_name_mp4(tmp_path, simple_scenes_path): @pytest.mark.slow -def test_dash_as_filename(tmp_path): +def test_dash_as_filename(tmp_path, write_to_movie): code = ( "class Test(Scene):\n" " def construct(self):\n" @@ -383,7 +363,9 @@ def test_dash_as_filename(tmp_path): @pytest.mark.slow -def test_gif_format_output(tmp_path, manim_cfg_file, simple_scenes_path): +def test_gif_format_output( + tmp_path, manim_cfg_file, simple_scenes_path, write_to_movie +): """Test only gif created with manim version in file name when --format gif is set""" scene_name = "SquareToCircle" command = [ @@ -415,7 +397,9 @@ def test_gif_format_output(tmp_path, manim_cfg_file, simple_scenes_path): @pytest.mark.slow -def test_mp4_format_output(tmp_path, manim_cfg_file, simple_scenes_path): +def test_mp4_format_output( + tmp_path, manim_cfg_file, simple_scenes_path, write_to_movie +): """Test only mp4 created without manim version in file name when --format mp4 is set""" scene_name = "SquareToCircle" command = [ @@ -453,6 +437,7 @@ def test_videos_not_created_when_png_format_set( tmp_path, manim_cfg_file, simple_scenes_path, + write_to_movie, ): """Test mp4 and gifs are not created when --format png is set""" scene_name = "SquareToCircle" @@ -491,6 +476,7 @@ def test_images_are_created_when_png_format_set( tmp_path, manim_cfg_file, simple_scenes_path, + write_to_movie, ): """Test images are created in media directory when --format png is set""" scene_name = "SquareToCircle" @@ -518,6 +504,7 @@ def test_images_are_created_when_png_format_set_for_opengl( tmp_path, manim_cfg_file, simple_scenes_path, + write_to_movie, ): """Test images are created in media directory when --format png is set for opengl""" scene_name = "SquareToCircle" @@ -547,6 +534,7 @@ def test_images_are_zero_padded_when_zero_pad_set( tmp_path, manim_cfg_file, simple_scenes_path, + write_to_movie, ): """Test images are zero padded when --format png and --zero_pad n are set""" scene_name = "SquareToCircle" @@ -581,6 +569,7 @@ def test_images_are_zero_padded_when_zero_pad_set_for_opengl( tmp_path, manim_cfg_file, simple_scenes_path, + write_to_movie, ): """Test images are zero padded when --format png and --zero_pad n are set with the opengl renderer""" scene_name = "SquareToCircle" @@ -613,7 +602,9 @@ def test_images_are_zero_padded_when_zero_pad_set_for_opengl( @pytest.mark.slow -def test_webm_format_output(tmp_path, manim_cfg_file, simple_scenes_path): +def test_webm_format_output( + tmp_path, manim_cfg_file, simple_scenes_path, write_to_movie +): """Test only webm created when --format webm is set""" scene_name = "SquareToCircle" command = [ @@ -651,6 +642,7 @@ def test_default_format_output_for_transparent_flag( tmp_path, manim_cfg_file, simple_scenes_path, + write_to_movie, ): """Test .mov is created by default when transparent flag is set""" scene_name = "SquareToCircle" @@ -684,7 +676,9 @@ def test_default_format_output_for_transparent_flag( @pytest.mark.slow -def test_mov_can_be_set_as_output_format(tmp_path, manim_cfg_file, simple_scenes_path): +def test_mov_can_be_set_as_output_format( + tmp_path, manim_cfg_file, simple_scenes_path, write_to_movie +): """Test .mov is created by when set using --format mov arg""" scene_name = "SquareToCircle" command = [ @@ -722,7 +716,9 @@ def test_mov_can_be_set_as_output_format(tmp_path, manim_cfg_file, simple_scenes "InputFileViaCfg.json", "videos/simple_scenes/480p15/SquareToCircle.mp4", ) -def test_input_file_via_cfg(tmp_path, manim_cfg_file, simple_scenes_path): +def test_input_file_via_cfg( + tmp_path, manim_cfg_file, simple_scenes_path, write_to_movie +): scene_name = "SquareToCircle" (tmp_path / "manim.cfg").write_text( f""" diff --git a/tests/test_scene_rendering/test_file_writer.py b/tests/test_scene_rendering/test_file_writer.py index 0065547709..ea9dacb02c 100644 --- a/tests/test_scene_rendering/test_file_writer.py +++ b/tests/test_scene_rendering/test_file_writer.py @@ -6,7 +6,7 @@ import numpy as np import pytest -from manim import DR, Circle, Create, Scene, Star, tempconfig +from manim import DR, Circle, Create, Manager, Scene, Star from manim.scene.scene_file_writer import to_av_frame_rate from manim.utils.commands import capture, get_video_metadata @@ -34,18 +34,14 @@ def construct(self): "transparent", [False, True], ) -def test_gif_writing(config, tmp_path, transparent): +def test_gif_writing(tmp_path, config, write_to_movie, transparent): output_filename = f"gif_{'transparent' if transparent else 'opaque'}" - with tempconfig( - { - "media_dir": tmp_path, - "quality": "low_quality", - "format": "gif", - "transparent": transparent, - "output_file": output_filename, - } - ): - StarScene().render() + config.media_dir = tmp_path + config.quality = "low_quality" + config.format = "gif" + config.transparent = transparent + config.output_file = output_filename + Manager(StarScene).render() video_path = tmp_path / "videos" / "480p15" / f"{output_filename}.gif" assert video_path.exists() @@ -92,18 +88,22 @@ def test_gif_writing(config, tmp_path, transparent): ("webm", True, "vp9", "yuv420p"), ], ) -def test_codecs(config, tmp_path, format, transparent, codec, pixel_format): +def test_codecs( + tmp_path, + config, + write_to_movie, + format, + transparent, + codec, + pixel_format, +): output_filename = f"codec_{format}_{'transparent' if transparent else 'opaque'}" - with tempconfig( - { - "media_dir": tmp_path, - "quality": "low_quality", - "format": format, - "transparent": transparent, - "output_file": output_filename, - } - ): - StarScene().render() + config.media_dir = tmp_path + config.quality = "low_quality" + config.format = format + config.transparent = transparent + config.output_file = output_filename + Manager(StarScene).render() video_path = tmp_path / "videos" / "480p15" / f"{output_filename}.{format}" assert video_path.exists() @@ -154,7 +154,7 @@ def construct(self): self.add_sound(file_path) self.wait() - SceneWithMP3().render() + Manager(SceneWithMP3).render() assert "click.mp3 to .wav" in manim_caplog.text diff --git a/tests/test_scene_rendering/test_play_logic.py b/tests/test_scene_rendering/test_play_logic.py index 4aaae3c8ad..812a6b2a85 100644 --- a/tests/test_scene_rendering/test_play_logic.py +++ b/tests/test_scene_rendering/test_play_logic.py @@ -1,12 +1,12 @@ from __future__ import annotations -import sys from unittest.mock import Mock import pytest from manim import ( Dot, + Manager, Mobject, Scene, ValueTracker, @@ -26,40 +26,40 @@ @pytest.mark.parametrize("frame_rate", argvalues=[15, 30, 60]) def test_t_values(config, using_temp_config, disabling_caching, frame_rate): - """Test that the framerate corresponds to the number of t values generated""" + """Test that the framerate corresponds to the number of times animations are updated""" config.frame_rate = frame_rate - scene = SquareToCircle() - scene.update_to_time = Mock() - scene.render() - assert scene.update_to_time.call_count == config["frame_rate"] + manager = Manager(SquareToCircle) + scene = manager.scene + scene._update_animations = Mock() + manager.render() + assert scene._update_animations.call_count == config["frame_rate"] np.testing.assert_allclose( - ([call.args[0] for call in scene.update_to_time.call_args_list]), + ([call.args[1] for call in scene._update_animations.call_args_list]), np.arange(0, 1, 1 / config["frame_rate"]), ) -@pytest.mark.skipif( - sys.version_info < (3, 8), - reason="Mock object has a different implementation in python 3.7, which makes it broken with this logic.", -) def test_t_values_with_skip_animations(using_temp_config, disabling_caching): """Test the behaviour of scene.skip_animations""" - scene = SquareToCircle() - scene.update_to_time = Mock() - scene.renderer._original_skipping_status = True - scene.render() - assert scene.update_to_time.call_count == 1 + manager = Manager(SquareToCircle) + manager._skip_animations = True + scene = manager.scene + scene._update_animations = Mock() + manager.render() + assert scene._update_animations.call_count == 1 np.testing.assert_almost_equal( - scene.update_to_time.call_args.args[0], + scene._update_animations.call_args.args[1], 1.0, ) +# TODO: Rework Wait animation def test_static_wait_detection(using_temp_config, disabling_caching): """Test if a static wait (wait that freeze the frame) is correctly detected""" - scene = SceneWithStaticWait() - scene.render() + manager = Manager(SceneWithStaticWait) + manager.render() # Test is is_static_wait of the Wait animation has been set to True by compile_animation_ata + scene = manager.scene assert scene.animations[0].is_static_wait assert scene.is_current_animation_frozen_frame() @@ -87,12 +87,12 @@ def construct(self): assert len(self.mobjects) > 5 assert self.time < 2 - scene = TestScene() - scene.render() + manager = Manager(TestScene) + manager.render() def test_frozen_frame(using_temp_config, disabling_caching): - scene = SceneForFrozenFrameTests() + scene = Manager(SceneForFrozenFrameTests) scene.render() assert scene.mobject_update_count == 0 assert scene.scene_update_count == 0 @@ -134,4 +134,4 @@ def construct(self): assert vt.get_value() == 42 assert vt.custom_attribute == "world" - CustomAnimateScene().render() + Manager(CustomAnimateScene).render()