manim 0.18.0.post0__py3-none-any.whl → 0.18.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of manim might be problematic. Click here for more details.
- manim/__init__.py +3 -6
- manim/__main__.py +18 -10
- manim/_config/__init__.py +5 -2
- manim/_config/cli_colors.py +12 -8
- manim/_config/default.cfg +1 -1
- manim/_config/logger_utils.py +9 -8
- manim/_config/utils.py +637 -449
- manim/animation/animation.py +9 -2
- manim/animation/composition.py +78 -40
- manim/animation/creation.py +12 -6
- manim/animation/fading.py +0 -1
- manim/animation/indication.py +10 -21
- manim/animation/movement.py +1 -2
- manim/animation/rotation.py +1 -1
- manim/animation/specialized.py +1 -1
- manim/animation/speedmodifier.py +7 -2
- manim/animation/transform_matching_parts.py +1 -1
- manim/camera/camera.py +13 -4
- manim/cli/cfg/group.py +18 -8
- manim/cli/checkhealth/checks.py +2 -0
- manim/cli/checkhealth/commands.py +2 -0
- manim/cli/default_group.py +13 -5
- manim/cli/init/commands.py +4 -1
- manim/cli/plugins/commands.py +3 -0
- manim/cli/render/commands.py +27 -20
- manim/cli/render/ease_of_access_options.py +4 -3
- manim/cli/render/global_options.py +9 -7
- manim/cli/render/output_options.py +6 -5
- manim/cli/render/render_options.py +13 -13
- manim/constants.py +54 -15
- manim/gui/gui.py +2 -0
- manim/mobject/geometry/arc.py +4 -4
- manim/mobject/geometry/boolean_ops.py +13 -9
- manim/mobject/geometry/line.py +16 -8
- manim/mobject/geometry/polygram.py +17 -5
- manim/mobject/geometry/tips.py +2 -2
- manim/mobject/graph.py +379 -106
- manim/mobject/graphing/coordinate_systems.py +17 -20
- manim/mobject/graphing/functions.py +14 -10
- manim/mobject/graphing/number_line.py +1 -1
- manim/mobject/mobject.py +175 -72
- manim/mobject/opengl/opengl_compatibility.py +2 -0
- manim/mobject/opengl/opengl_geometry.py +26 -1
- manim/mobject/opengl/opengl_image_mobject.py +2 -0
- manim/mobject/opengl/opengl_mobject.py +3 -0
- manim/mobject/opengl/opengl_point_cloud_mobject.py +2 -0
- manim/mobject/opengl/opengl_surface.py +2 -0
- manim/mobject/opengl/opengl_three_dimensions.py +2 -0
- manim/mobject/opengl/opengl_vectorized_mobject.py +19 -14
- manim/mobject/svg/brace.py +2 -0
- manim/mobject/svg/svg_mobject.py +2 -2
- manim/mobject/table.py +0 -1
- manim/mobject/text/code_mobject.py +2 -0
- manim/mobject/text/numbers.py +2 -0
- manim/mobject/text/tex_mobject.py +1 -1
- manim/mobject/text/text_mobject.py +43 -6
- manim/mobject/three_d/three_d_utils.py +4 -4
- manim/mobject/three_d/three_dimensions.py +4 -4
- manim/mobject/types/image_mobject.py +5 -1
- manim/mobject/types/point_cloud_mobject.py +2 -0
- manim/mobject/types/vectorized_mobject.py +124 -29
- manim/mobject/value_tracker.py +3 -3
- manim/mobject/vector_field.py +3 -1
- manim/plugins/__init__.py +15 -1
- manim/plugins/plugins_flags.py +11 -5
- manim/renderer/cairo_renderer.py +12 -2
- manim/renderer/opengl_renderer.py +2 -3
- manim/renderer/opengl_renderer_window.py +2 -0
- manim/renderer/shader_wrapper.py +2 -0
- manim/renderer/vectorized_mobject_rendering.py +5 -0
- manim/scene/scene.py +22 -6
- manim/scene/scene_file_writer.py +3 -1
- manim/scene/section.py +2 -0
- manim/scene/three_d_scene.py +5 -6
- manim/scene/vector_space_scene.py +21 -5
- manim/typing.py +567 -67
- manim/utils/bezier.py +9 -18
- manim/utils/caching.py +2 -0
- manim/utils/color/BS381.py +1 -0
- manim/utils/color/XKCD.py +1 -0
- manim/utils/color/core.py +31 -13
- manim/utils/commands.py +8 -1
- manim/utils/debug.py +0 -1
- manim/utils/deprecation.py +3 -2
- manim/utils/docbuild/__init__.py +17 -0
- manim/utils/docbuild/autoaliasattr_directive.py +197 -0
- manim/utils/docbuild/autocolor_directive.py +9 -4
- manim/utils/docbuild/manim_directive.py +18 -9
- manim/utils/docbuild/module_parsing.py +198 -0
- manim/utils/exceptions.py +6 -0
- manim/utils/family.py +2 -0
- manim/utils/family_ops.py +5 -0
- manim/utils/file_ops.py +6 -2
- manim/utils/hashing.py +2 -0
- manim/utils/ipython_magic.py +2 -0
- manim/utils/module_ops.py +2 -0
- manim/utils/opengl.py +14 -0
- manim/utils/parameter_parsing.py +31 -0
- manim/utils/paths.py +12 -20
- manim/utils/rate_functions.py +6 -8
- manim/utils/space_ops.py +81 -36
- manim/utils/testing/__init__.py +17 -0
- manim/utils/testing/frames_comparison.py +7 -5
- manim/utils/tex.py +124 -196
- manim/utils/tex_file_writing.py +2 -0
- manim/utils/tex_templates.py +1 -0
- {manim-0.18.0.post0.dist-info → manim-0.18.1.dist-info}/LICENSE.community +1 -1
- {manim-0.18.0.post0.dist-info → manim-0.18.1.dist-info}/METADATA +29 -35
- manim-0.18.1.dist-info/RECORD +217 -0
- manim/cli/new/__init__.py +0 -0
- manim/cli/new/group.py +0 -189
- manim/plugins/import_plugins.py +0 -43
- manim-0.18.0.post0.dist-info/RECORD +0 -217
- {manim-0.18.0.post0.dist-info → manim-0.18.1.dist-info}/LICENSE +0 -0
- {manim-0.18.0.post0.dist-info → manim-0.18.1.dist-info}/WHEEL +0 -0
- {manim-0.18.0.post0.dist-info → manim-0.18.1.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Read and parse all the Manim modules and extract documentation from them."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from typing_extensions import TypeAlias
|
|
9
|
+
|
|
10
|
+
__all__ = ["parse_module_attributes"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
AliasInfo: TypeAlias = dict[str, str]
|
|
14
|
+
"""Dictionary with a `definition` key containing the definition of
|
|
15
|
+
a :class:`TypeAlias` as a string, and optionally a `doc` key containing
|
|
16
|
+
the documentation for that alias, if it exists.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
AliasCategoryDict: TypeAlias = dict[str, AliasInfo]
|
|
20
|
+
"""Dictionary which holds an `AliasInfo` for every alias name in a same
|
|
21
|
+
category.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
ModuleLevelAliasDict: TypeAlias = dict[str, AliasCategoryDict]
|
|
25
|
+
"""Dictionary containing every :class:`TypeAlias` defined in a module,
|
|
26
|
+
classified by category in different `AliasCategoryDict` objects.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
AliasDocsDict: TypeAlias = dict[str, ModuleLevelAliasDict]
|
|
30
|
+
"""Dictionary which, for every module in Manim, contains documentation
|
|
31
|
+
about their module-level attributes which are explicitly defined as
|
|
32
|
+
:class:`TypeAlias`, separating them from the rest of attributes.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
DataDict: TypeAlias = dict[str, list[str]]
|
|
36
|
+
"""Type for a dictionary which, for every module, contains a list with
|
|
37
|
+
the names of all their DOCUMENTED module-level attributes (identified
|
|
38
|
+
by Sphinx via the ``data`` role, hence the name) which are NOT
|
|
39
|
+
explicitly defined as :class:`TypeAlias`.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
ALIAS_DOCS_DICT: AliasDocsDict = {}
|
|
43
|
+
DATA_DICT: DataDict = {}
|
|
44
|
+
|
|
45
|
+
MANIM_ROOT = Path(__file__).resolve().parent.parent.parent
|
|
46
|
+
|
|
47
|
+
# In the following, we will use ``type(xyz) is xyz_type`` instead of
|
|
48
|
+
# isinstance checks to make sure no subclasses of the type pass the
|
|
49
|
+
# check
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def parse_module_attributes() -> tuple[AliasDocsDict, DataDict]:
|
|
53
|
+
"""Read all files, generate Abstract Syntax Trees from them, and
|
|
54
|
+
extract useful information about the type aliases defined in the
|
|
55
|
+
files: the category they belong to, their definition and their
|
|
56
|
+
description, separating them from the "regular" module attributes.
|
|
57
|
+
|
|
58
|
+
Returns
|
|
59
|
+
-------
|
|
60
|
+
ALIAS_DOCS_DICT : `AliasDocsDict`
|
|
61
|
+
A dictionary containing the information from all the type
|
|
62
|
+
aliases in Manim. See `AliasDocsDict` for more information.
|
|
63
|
+
|
|
64
|
+
DATA_DICT : `DataDict`
|
|
65
|
+
A dictionary containing the names of all DOCUMENTED
|
|
66
|
+
module-level attributes which are not a :class:`TypeAlias`.
|
|
67
|
+
"""
|
|
68
|
+
global ALIAS_DOCS_DICT
|
|
69
|
+
global DATA_DICT
|
|
70
|
+
|
|
71
|
+
if ALIAS_DOCS_DICT or DATA_DICT:
|
|
72
|
+
return ALIAS_DOCS_DICT, DATA_DICT
|
|
73
|
+
|
|
74
|
+
for module_path in MANIM_ROOT.rglob("*.py"):
|
|
75
|
+
module_name = module_path.resolve().relative_to(MANIM_ROOT)
|
|
76
|
+
module_name = list(module_name.parts)
|
|
77
|
+
module_name[-1] = module_name[-1].removesuffix(".py")
|
|
78
|
+
module_name = ".".join(module_name)
|
|
79
|
+
|
|
80
|
+
module_content = module_path.read_text(encoding="utf-8")
|
|
81
|
+
|
|
82
|
+
# For storing TypeAliases
|
|
83
|
+
module_dict: ModuleLevelAliasDict = {}
|
|
84
|
+
category_dict: AliasCategoryDict | None = None
|
|
85
|
+
alias_info: AliasInfo | None = None
|
|
86
|
+
|
|
87
|
+
# For storing regular module attributes
|
|
88
|
+
data_list: list[str] = []
|
|
89
|
+
data_name: str | None = None
|
|
90
|
+
|
|
91
|
+
for node in ast.iter_child_nodes(ast.parse(module_content)):
|
|
92
|
+
# If we encounter a string:
|
|
93
|
+
if (
|
|
94
|
+
type(node) is ast.Expr
|
|
95
|
+
and type(node.value) is ast.Constant
|
|
96
|
+
and type(node.value.value) is str
|
|
97
|
+
):
|
|
98
|
+
string = node.value.value.strip()
|
|
99
|
+
# It can be the start of a category
|
|
100
|
+
section_str = "[CATEGORY]"
|
|
101
|
+
if string.startswith(section_str):
|
|
102
|
+
category_name = string[len(section_str) :].strip()
|
|
103
|
+
module_dict[category_name] = {}
|
|
104
|
+
category_dict = module_dict[category_name]
|
|
105
|
+
alias_info = None
|
|
106
|
+
# or a docstring of the alias defined before
|
|
107
|
+
elif alias_info:
|
|
108
|
+
alias_info["doc"] = string
|
|
109
|
+
# or a docstring of the module attribute defined before
|
|
110
|
+
elif data_name:
|
|
111
|
+
data_list.append(data_name)
|
|
112
|
+
continue
|
|
113
|
+
|
|
114
|
+
# if it's defined under if TYPE_CHECKING
|
|
115
|
+
# go through the body of the if statement
|
|
116
|
+
if (
|
|
117
|
+
# NOTE: This logic does not (and cannot)
|
|
118
|
+
# check if the comparison is against a
|
|
119
|
+
# variable called TYPE_CHECKING
|
|
120
|
+
# It also says that you cannot do the following
|
|
121
|
+
# import typing as foo
|
|
122
|
+
# if foo.TYPE_CHECKING:
|
|
123
|
+
# BAR: TypeAlias = ...
|
|
124
|
+
type(node) is ast.If
|
|
125
|
+
and (
|
|
126
|
+
(
|
|
127
|
+
# if TYPE_CHECKING
|
|
128
|
+
type(node.test) is ast.Name
|
|
129
|
+
and node.test.id == "TYPE_CHECKING"
|
|
130
|
+
)
|
|
131
|
+
or (
|
|
132
|
+
# if typing.TYPE_CHECKING
|
|
133
|
+
type(node.test) is ast.Attribute
|
|
134
|
+
and type(node.test.value) is ast.Name
|
|
135
|
+
and node.test.value.id == "typing"
|
|
136
|
+
and node.test.attr == "TYPE_CHECKING"
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
):
|
|
140
|
+
inner_nodes = node.body
|
|
141
|
+
else:
|
|
142
|
+
inner_nodes = [node]
|
|
143
|
+
|
|
144
|
+
for node in inner_nodes:
|
|
145
|
+
# If we encounter an assignment annotated as "TypeAlias":
|
|
146
|
+
if (
|
|
147
|
+
type(node) is ast.AnnAssign
|
|
148
|
+
and type(node.annotation) is ast.Name
|
|
149
|
+
and node.annotation.id == "TypeAlias"
|
|
150
|
+
and type(node.target) is ast.Name
|
|
151
|
+
and node.value is not None
|
|
152
|
+
):
|
|
153
|
+
alias_name = node.target.id
|
|
154
|
+
def_node = node.value
|
|
155
|
+
# If it's an Union, replace it with vertical bar notation
|
|
156
|
+
if (
|
|
157
|
+
type(def_node) is ast.Subscript
|
|
158
|
+
and type(def_node.value) is ast.Name
|
|
159
|
+
and def_node.value.id == "Union"
|
|
160
|
+
):
|
|
161
|
+
definition = " | ".join(
|
|
162
|
+
ast.unparse(elem) for elem in def_node.slice.elts
|
|
163
|
+
)
|
|
164
|
+
else:
|
|
165
|
+
definition = ast.unparse(def_node)
|
|
166
|
+
|
|
167
|
+
definition = definition.replace("npt.", "")
|
|
168
|
+
if category_dict is None:
|
|
169
|
+
module_dict[""] = {}
|
|
170
|
+
category_dict = module_dict[""]
|
|
171
|
+
category_dict[alias_name] = {"definition": definition}
|
|
172
|
+
alias_info = category_dict[alias_name]
|
|
173
|
+
continue
|
|
174
|
+
|
|
175
|
+
# If here, the node is not a TypeAlias definition
|
|
176
|
+
alias_info = None
|
|
177
|
+
|
|
178
|
+
# It could still be a module attribute definition.
|
|
179
|
+
# Does the assignment have a target of type Name? Then
|
|
180
|
+
# it could be considered a definition of a module attribute.
|
|
181
|
+
if type(node) is ast.AnnAssign:
|
|
182
|
+
target = node.target
|
|
183
|
+
elif type(node) is ast.Assign and len(node.targets) == 1:
|
|
184
|
+
target = node.targets[0]
|
|
185
|
+
else:
|
|
186
|
+
target = None
|
|
187
|
+
|
|
188
|
+
if type(target) is ast.Name:
|
|
189
|
+
data_name = target.id
|
|
190
|
+
else:
|
|
191
|
+
data_name = None
|
|
192
|
+
|
|
193
|
+
if len(module_dict) > 0:
|
|
194
|
+
ALIAS_DOCS_DICT[module_name] = module_dict
|
|
195
|
+
if len(data_list) > 0:
|
|
196
|
+
DATA_DICT[module_name] = data_list
|
|
197
|
+
|
|
198
|
+
return ALIAS_DOCS_DICT, DATA_DICT
|
manim/utils/exceptions.py
CHANGED
manim/utils/family.py
CHANGED
manim/utils/family_ops.py
CHANGED
|
@@ -2,6 +2,11 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import itertools as it
|
|
4
4
|
|
|
5
|
+
__all__ = [
|
|
6
|
+
"extract_mobject_family_members",
|
|
7
|
+
"restructure_list_to_exclude_certain_family_members",
|
|
8
|
+
]
|
|
9
|
+
|
|
5
10
|
|
|
6
11
|
def extract_mobject_family_members(mobject_list, only_those_with_points=False):
|
|
7
12
|
result = list(it.chain(*(mob.get_family() for mob in mobject_list)))
|
manim/utils/file_ops.py
CHANGED
|
@@ -32,7 +32,7 @@ if TYPE_CHECKING:
|
|
|
32
32
|
|
|
33
33
|
from manim import __version__, config, logger
|
|
34
34
|
|
|
35
|
-
from .. import console
|
|
35
|
+
from .. import config, console
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
def is_mp4_format() -> bool:
|
|
@@ -204,8 +204,12 @@ def open_file(file_path, in_browser=False):
|
|
|
204
204
|
commands = ["open"] if not in_browser else ["open", "-R"]
|
|
205
205
|
else:
|
|
206
206
|
raise OSError("Unable to identify your operating system...")
|
|
207
|
+
|
|
208
|
+
# check after so that file path is set correctly
|
|
209
|
+
if config.preview_command:
|
|
210
|
+
commands = [config.preview_command]
|
|
207
211
|
commands.append(file_path)
|
|
208
|
-
sp.
|
|
212
|
+
sp.run(commands)
|
|
209
213
|
|
|
210
214
|
|
|
211
215
|
def open_media_file(file_writer: SceneFileWriter) -> None:
|
manim/utils/hashing.py
CHANGED
|
@@ -23,6 +23,8 @@ from .. import config, logger
|
|
|
23
23
|
if typing.TYPE_CHECKING:
|
|
24
24
|
from manim.scene.scene import Scene
|
|
25
25
|
|
|
26
|
+
__all__ = ["KEYS_TO_FILTER_OUT", "get_hash_from_play_call", "get_json"]
|
|
27
|
+
|
|
26
28
|
# Sometimes there are elements that are not suitable for hashing (too long or
|
|
27
29
|
# run-dependent). This is used to filter them out.
|
|
28
30
|
KEYS_TO_FILTER_OUT = {
|
manim/utils/ipython_magic.py
CHANGED
manim/utils/module_ops.py
CHANGED
manim/utils/opengl.py
CHANGED
|
@@ -7,6 +7,20 @@ from .. import config
|
|
|
7
7
|
|
|
8
8
|
depth = 20
|
|
9
9
|
|
|
10
|
+
__all__ = [
|
|
11
|
+
"matrix_to_shader_input",
|
|
12
|
+
"orthographic_projection_matrix",
|
|
13
|
+
"perspective_projection_matrix",
|
|
14
|
+
"translation_matrix",
|
|
15
|
+
"x_rotation_matrix",
|
|
16
|
+
"y_rotation_matrix",
|
|
17
|
+
"z_rotation_matrix",
|
|
18
|
+
"rotate_in_place_matrix",
|
|
19
|
+
"rotation_matrix",
|
|
20
|
+
"scale_matrix",
|
|
21
|
+
"view_matrix",
|
|
22
|
+
]
|
|
23
|
+
|
|
10
24
|
|
|
11
25
|
def matrix_to_shader_input(matrix):
|
|
12
26
|
return tuple(matrix.T.ravel())
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from types import GeneratorType
|
|
4
|
+
from typing import Iterable, TypeVar
|
|
5
|
+
|
|
6
|
+
T = TypeVar("T")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def flatten_iterable_parameters(
|
|
10
|
+
args: Iterable[T | Iterable[T] | GeneratorType],
|
|
11
|
+
) -> list[T]:
|
|
12
|
+
"""Flattens an iterable of parameters into a list of parameters.
|
|
13
|
+
|
|
14
|
+
Parameters
|
|
15
|
+
----------
|
|
16
|
+
args
|
|
17
|
+
The iterable of parameters to flatten.
|
|
18
|
+
[(generator), [], (), ...]
|
|
19
|
+
|
|
20
|
+
Returns
|
|
21
|
+
-------
|
|
22
|
+
:class:`list`
|
|
23
|
+
The flattened list of parameters.
|
|
24
|
+
"""
|
|
25
|
+
flattened_parameters = []
|
|
26
|
+
for arg in args:
|
|
27
|
+
if isinstance(arg, (Iterable, GeneratorType)):
|
|
28
|
+
flattened_parameters.extend(arg)
|
|
29
|
+
else:
|
|
30
|
+
flattened_parameters.append(arg)
|
|
31
|
+
return flattened_parameters
|
manim/utils/paths.py
CHANGED
|
@@ -10,28 +10,22 @@ __all__ = [
|
|
|
10
10
|
]
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
from typing import
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
14
|
|
|
15
15
|
import numpy as np
|
|
16
16
|
|
|
17
17
|
from ..constants import OUT
|
|
18
18
|
from ..utils.bezier import interpolate
|
|
19
|
-
from ..utils.deprecation import deprecated_params
|
|
20
19
|
from ..utils.space_ops import rotation_matrix
|
|
21
20
|
|
|
22
|
-
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from manim.typing import PathFuncType, Vector3D
|
|
23
|
+
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
STRAIGHT_PATH_THRESHOLD = 0.01
|
|
25
26
|
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
@deprecated_params(
|
|
29
|
-
params="start_points, end_points, alpha",
|
|
30
|
-
since="v0.14",
|
|
31
|
-
until="v0.15",
|
|
32
|
-
message="Straight path is now returning interpolating function to make it consistent with other path functions. Use straight_path()(a,b,c) instead of straight_path(a,b,c).",
|
|
33
|
-
)
|
|
34
|
-
def straight_path(*args) -> PATH_FUNC_TYPE:
|
|
28
|
+
def straight_path():
|
|
35
29
|
"""Simplest path function. Each point in a set goes in a straight path toward its destination.
|
|
36
30
|
|
|
37
31
|
Examples
|
|
@@ -74,14 +68,12 @@ def straight_path(*args) -> PATH_FUNC_TYPE:
|
|
|
74
68
|
self.wait()
|
|
75
69
|
|
|
76
70
|
"""
|
|
77
|
-
if len(args) > 0:
|
|
78
|
-
return interpolate(*args)
|
|
79
71
|
return interpolate
|
|
80
72
|
|
|
81
73
|
|
|
82
74
|
def path_along_circles(
|
|
83
|
-
arc_angle: float, circles_centers: np.ndarray, axis:
|
|
84
|
-
) ->
|
|
75
|
+
arc_angle: float, circles_centers: np.ndarray, axis: Vector3D = OUT
|
|
76
|
+
) -> PathFuncType:
|
|
85
77
|
"""This function transforms each point by moving it roughly along a circle, each with its own specified center.
|
|
86
78
|
|
|
87
79
|
The path may be seen as each point smoothly changing its orbit from its starting position to its destination.
|
|
@@ -158,7 +150,7 @@ def path_along_circles(
|
|
|
158
150
|
return path
|
|
159
151
|
|
|
160
152
|
|
|
161
|
-
def path_along_arc(arc_angle: float, axis:
|
|
153
|
+
def path_along_arc(arc_angle: float, axis: Vector3D = OUT) -> PathFuncType:
|
|
162
154
|
"""This function transforms each point by moving it along a circular arc.
|
|
163
155
|
|
|
164
156
|
Parameters
|
|
@@ -225,7 +217,7 @@ def path_along_arc(arc_angle: float, axis: np.ndarray = OUT) -> PATH_FUNC_TYPE:
|
|
|
225
217
|
return path
|
|
226
218
|
|
|
227
219
|
|
|
228
|
-
def clockwise_path() ->
|
|
220
|
+
def clockwise_path() -> PathFuncType:
|
|
229
221
|
"""This function transforms each point by moving clockwise around a half circle.
|
|
230
222
|
|
|
231
223
|
Examples
|
|
@@ -271,7 +263,7 @@ def clockwise_path() -> PATH_FUNC_TYPE:
|
|
|
271
263
|
return path_along_arc(-np.pi)
|
|
272
264
|
|
|
273
265
|
|
|
274
|
-
def counterclockwise_path() ->
|
|
266
|
+
def counterclockwise_path() -> PathFuncType:
|
|
275
267
|
"""This function transforms each point by moving counterclockwise around a half circle.
|
|
276
268
|
|
|
277
269
|
Examples
|
|
@@ -317,7 +309,7 @@ def counterclockwise_path() -> PATH_FUNC_TYPE:
|
|
|
317
309
|
return path_along_arc(np.pi)
|
|
318
310
|
|
|
319
311
|
|
|
320
|
-
def spiral_path(angle: float, axis:
|
|
312
|
+
def spiral_path(angle: float, axis: Vector3D = OUT) -> PathFuncType:
|
|
321
313
|
"""This function transforms each point by moving along a spiral to its destination.
|
|
322
314
|
|
|
323
315
|
Parameters
|
manim/utils/rate_functions.py
CHANGED
|
@@ -83,7 +83,6 @@ There are primarily 3 kinds of standard easing functions:
|
|
|
83
83
|
self.wait()
|
|
84
84
|
"""
|
|
85
85
|
|
|
86
|
-
|
|
87
86
|
from __future__ import annotations
|
|
88
87
|
|
|
89
88
|
__all__ = [
|
|
@@ -179,13 +178,12 @@ def smoothererstep(t: float) -> float:
|
|
|
179
178
|
The 1st, 2nd and 3rd derivatives (speed, acceleration and jerk) are zero at the endpoints.
|
|
180
179
|
https://en.wikipedia.org/wiki/Smoothstep
|
|
181
180
|
"""
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
)
|
|
181
|
+
alpha = 0
|
|
182
|
+
if 0 < t < 1:
|
|
183
|
+
alpha = 35 * t**4 - 84 * t**5 + 70 * t**6 - 20 * t**7
|
|
184
|
+
elif t >= 1:
|
|
185
|
+
alpha = 1
|
|
186
|
+
return alpha
|
|
189
187
|
|
|
190
188
|
|
|
191
189
|
@unit_interval
|
manim/utils/space_ops.py
CHANGED
|
@@ -2,7 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
import itertools as it
|
|
6
|
+
from typing import TYPE_CHECKING, Sequence
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
from mapbox_earcut import triangulate_float32 as earcut
|
|
10
|
+
from scipy.spatial.transform import Rotation
|
|
11
|
+
|
|
12
|
+
from manim.constants import DOWN, OUT, PI, RIGHT, TAU, UP, RendererType
|
|
13
|
+
from manim.utils.iterables import adjacent_pairs
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
import numpy.typing as npt
|
|
17
|
+
|
|
18
|
+
from manim.typing import (
|
|
19
|
+
ManimFloat,
|
|
20
|
+
Point3D_Array,
|
|
21
|
+
Vector2D,
|
|
22
|
+
Vector2D_Array,
|
|
23
|
+
Vector3D,
|
|
24
|
+
)
|
|
6
25
|
|
|
7
26
|
__all__ = [
|
|
8
27
|
"quaternion_mult",
|
|
@@ -38,21 +57,20 @@ __all__ = [
|
|
|
38
57
|
]
|
|
39
58
|
|
|
40
59
|
|
|
41
|
-
import itertools as it
|
|
42
|
-
from typing import Sequence
|
|
43
|
-
|
|
44
|
-
import numpy as np
|
|
45
|
-
from mapbox_earcut import triangulate_float32 as earcut
|
|
46
|
-
from scipy.spatial.transform import Rotation
|
|
47
|
-
|
|
48
|
-
from ..constants import DOWN, OUT, PI, RIGHT, TAU, UP, RendererType
|
|
49
|
-
from ..utils.iterables import adjacent_pairs
|
|
50
|
-
|
|
51
|
-
|
|
52
60
|
def norm_squared(v: float) -> float:
|
|
53
61
|
return np.dot(v, v)
|
|
54
62
|
|
|
55
63
|
|
|
64
|
+
def cross(v1: Vector3D, v2: Vector3D) -> Vector3D:
|
|
65
|
+
return np.array(
|
|
66
|
+
[
|
|
67
|
+
v1[1] * v2[2] - v1[2] * v2[1],
|
|
68
|
+
v1[2] * v2[0] - v1[0] * v2[2],
|
|
69
|
+
v1[0] * v2[1] - v1[1] * v2[0],
|
|
70
|
+
]
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
56
74
|
# Quaternions
|
|
57
75
|
# TODO, implement quaternion type
|
|
58
76
|
|
|
@@ -104,7 +122,7 @@ def quaternion_from_angle_axis(
|
|
|
104
122
|
|
|
105
123
|
Returns
|
|
106
124
|
-------
|
|
107
|
-
|
|
125
|
+
list[float]
|
|
108
126
|
Gives back a quaternion from the angle and axis
|
|
109
127
|
"""
|
|
110
128
|
if not axis_normalized:
|
|
@@ -273,12 +291,12 @@ def z_to_vector(vector: np.ndarray) -> np.ndarray:
|
|
|
273
291
|
(normalized) vector provided as an argument
|
|
274
292
|
"""
|
|
275
293
|
axis_z = normalize(vector)
|
|
276
|
-
axis_y = normalize(
|
|
277
|
-
axis_x =
|
|
294
|
+
axis_y = normalize(cross(axis_z, RIGHT))
|
|
295
|
+
axis_x = cross(axis_y, axis_z)
|
|
278
296
|
if np.linalg.norm(axis_y) == 0:
|
|
279
297
|
# the vector passed just so happened to be in the x direction.
|
|
280
|
-
axis_x = normalize(
|
|
281
|
-
axis_y = -
|
|
298
|
+
axis_x = normalize(cross(UP, axis_z))
|
|
299
|
+
axis_y = -cross(axis_x, axis_z)
|
|
282
300
|
|
|
283
301
|
return np.array([axis_x, axis_y, axis_z]).T
|
|
284
302
|
|
|
@@ -359,7 +377,7 @@ def normalize_along_axis(array: np.ndarray, axis: np.ndarray) -> np.ndarray:
|
|
|
359
377
|
return array
|
|
360
378
|
|
|
361
379
|
|
|
362
|
-
def get_unit_normal(v1:
|
|
380
|
+
def get_unit_normal(v1: Vector3D, v2: Vector3D, tol: float = 1e-6) -> Vector3D:
|
|
363
381
|
"""Gets the unit normal of the vectors.
|
|
364
382
|
|
|
365
383
|
Parameters
|
|
@@ -376,16 +394,37 @@ def get_unit_normal(v1: np.ndarray, v2: np.ndarray, tol: float = 1e-6) -> np.nda
|
|
|
376
394
|
np.ndarray
|
|
377
395
|
The normal of the two vectors.
|
|
378
396
|
"""
|
|
379
|
-
v1
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
if
|
|
383
|
-
|
|
384
|
-
cp = np.cross(np.cross(v1, OUT), v1)
|
|
385
|
-
cp_norm = np.linalg.norm(cp)
|
|
386
|
-
if cp_norm < tol:
|
|
397
|
+
# Instead of normalizing v1 and v2, just divide by the greatest
|
|
398
|
+
# of all their absolute components, which is just enough
|
|
399
|
+
div1, div2 = max(np.abs(v1)), max(np.abs(v2))
|
|
400
|
+
if div1 == 0.0:
|
|
401
|
+
if div2 == 0.0:
|
|
387
402
|
return DOWN
|
|
388
|
-
|
|
403
|
+
u = v2 / div2
|
|
404
|
+
elif div2 == 0.0:
|
|
405
|
+
u = v1 / div1
|
|
406
|
+
else:
|
|
407
|
+
# Normal scenario: v1 and v2 are both non-null
|
|
408
|
+
u1, u2 = v1 / div1, v2 / div2
|
|
409
|
+
cp = cross(u1, u2)
|
|
410
|
+
cp_norm = np.sqrt(norm_squared(cp))
|
|
411
|
+
if cp_norm > tol:
|
|
412
|
+
return cp / cp_norm
|
|
413
|
+
# Otherwise, v1 and v2 were aligned
|
|
414
|
+
u = u1
|
|
415
|
+
|
|
416
|
+
# If you are here, you have an "unique", non-zero, unit-ish vector u
|
|
417
|
+
# If it's also too aligned to the Z axis, just return DOWN
|
|
418
|
+
if abs(u[0]) < tol and abs(u[1]) < tol:
|
|
419
|
+
return DOWN
|
|
420
|
+
# Otherwise rotate u in the plane it shares with the Z axis,
|
|
421
|
+
# 90° TOWARDS the Z axis. This is done via (u x [0, 0, 1]) x u,
|
|
422
|
+
# which gives [-xz, -yz, x²+y²] (slightly scaled as well)
|
|
423
|
+
cp = np.array([-u[0] * u[2], -u[1] * u[2], u[0] * u[0] + u[1] * u[1]])
|
|
424
|
+
cp_norm = np.sqrt(norm_squared(cp))
|
|
425
|
+
# Because the norm(u) == 0 case was filtered in the beginning,
|
|
426
|
+
# there is no need to check if the norm of cp is 0
|
|
427
|
+
return cp / cp_norm
|
|
389
428
|
|
|
390
429
|
|
|
391
430
|
###
|
|
@@ -529,8 +568,8 @@ def line_intersection(
|
|
|
529
568
|
np.pad(np.array(i)[:, :2], ((0, 0), (0, 1)), constant_values=1)
|
|
530
569
|
for i in (line1, line2)
|
|
531
570
|
)
|
|
532
|
-
line1, line2 = (
|
|
533
|
-
x, y, z =
|
|
571
|
+
line1, line2 = (cross(*i) for i in padded)
|
|
572
|
+
x, y, z = cross(line1, line2)
|
|
534
573
|
|
|
535
574
|
if z == 0:
|
|
536
575
|
raise ValueError(
|
|
@@ -558,7 +597,7 @@ def find_intersection(
|
|
|
558
597
|
result = []
|
|
559
598
|
|
|
560
599
|
for p0, v0, p1, v1 in zip(*[p0s, v0s, p1s, v1s]):
|
|
561
|
-
normal =
|
|
600
|
+
normal = cross(v1, cross(v0, v1))
|
|
562
601
|
denom = max(np.dot(v0, normal), threshold)
|
|
563
602
|
result += [p0 + np.dot(p1 - p0, normal) / denom * v0]
|
|
564
603
|
return result
|
|
@@ -623,8 +662,9 @@ def shoelace_direction(x_y: np.ndarray) -> str:
|
|
|
623
662
|
|
|
624
663
|
|
|
625
664
|
def cross2d(
|
|
626
|
-
a:
|
|
627
|
-
|
|
665
|
+
a: Vector2D | Vector2D_Array,
|
|
666
|
+
b: Vector2D | Vector2D_Array,
|
|
667
|
+
) -> ManimFloat | npt.NDArray[ManimFloat]:
|
|
628
668
|
"""Compute the determinant(s) of the passed
|
|
629
669
|
vector (sequences).
|
|
630
670
|
|
|
@@ -715,9 +755,14 @@ def earclip_triangulation(verts: np.ndarray, ring_ends: list) -> list:
|
|
|
715
755
|
|
|
716
756
|
# Move the ring which j belongs to from the
|
|
717
757
|
# attached list to the detached list
|
|
718
|
-
new_ring = next(
|
|
719
|
-
|
|
720
|
-
|
|
758
|
+
new_ring = next(
|
|
759
|
+
(ring for ring in detached_rings if ring[0] <= j < ring[-1]), None
|
|
760
|
+
)
|
|
761
|
+
if new_ring is not None:
|
|
762
|
+
detached_rings.remove(new_ring)
|
|
763
|
+
attached_rings.append(new_ring)
|
|
764
|
+
else:
|
|
765
|
+
raise Exception("Could not find a ring to attach")
|
|
721
766
|
|
|
722
767
|
# Setup linked list
|
|
723
768
|
after = []
|
|
@@ -814,6 +859,6 @@ def perpendicular_bisector(
|
|
|
814
859
|
"""
|
|
815
860
|
p1 = line[0]
|
|
816
861
|
p2 = line[1]
|
|
817
|
-
direction =
|
|
862
|
+
direction = cross(p1 - p2, norm_vector)
|
|
818
863
|
m = midpoint(p1, p2)
|
|
819
864
|
return [m + direction, m - direction]
|
manim/utils/testing/__init__.py
CHANGED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Utilities for Manim tests using `pytest <https://pytest.org>`_.
|
|
2
|
+
|
|
3
|
+
For more information about Manim testing, see:
|
|
4
|
+
|
|
5
|
+
- :doc:`/contributing/development`, specifically the ``Tests`` bullet
|
|
6
|
+
point under :ref:`polishing-changes-and-submitting-a-pull-request`
|
|
7
|
+
- :doc:`/contributing/testing`
|
|
8
|
+
|
|
9
|
+
.. autosummary::
|
|
10
|
+
:toctree: ../reference
|
|
11
|
+
|
|
12
|
+
frames_comparison
|
|
13
|
+
_frames_testers
|
|
14
|
+
_show_diff
|
|
15
|
+
_test_class_makers
|
|
16
|
+
|
|
17
|
+
"""
|