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.

Files changed (116) hide show
  1. manim/__init__.py +3 -6
  2. manim/__main__.py +18 -10
  3. manim/_config/__init__.py +5 -2
  4. manim/_config/cli_colors.py +12 -8
  5. manim/_config/default.cfg +1 -1
  6. manim/_config/logger_utils.py +9 -8
  7. manim/_config/utils.py +637 -449
  8. manim/animation/animation.py +9 -2
  9. manim/animation/composition.py +78 -40
  10. manim/animation/creation.py +12 -6
  11. manim/animation/fading.py +0 -1
  12. manim/animation/indication.py +10 -21
  13. manim/animation/movement.py +1 -2
  14. manim/animation/rotation.py +1 -1
  15. manim/animation/specialized.py +1 -1
  16. manim/animation/speedmodifier.py +7 -2
  17. manim/animation/transform_matching_parts.py +1 -1
  18. manim/camera/camera.py +13 -4
  19. manim/cli/cfg/group.py +18 -8
  20. manim/cli/checkhealth/checks.py +2 -0
  21. manim/cli/checkhealth/commands.py +2 -0
  22. manim/cli/default_group.py +13 -5
  23. manim/cli/init/commands.py +4 -1
  24. manim/cli/plugins/commands.py +3 -0
  25. manim/cli/render/commands.py +27 -20
  26. manim/cli/render/ease_of_access_options.py +4 -3
  27. manim/cli/render/global_options.py +9 -7
  28. manim/cli/render/output_options.py +6 -5
  29. manim/cli/render/render_options.py +13 -13
  30. manim/constants.py +54 -15
  31. manim/gui/gui.py +2 -0
  32. manim/mobject/geometry/arc.py +4 -4
  33. manim/mobject/geometry/boolean_ops.py +13 -9
  34. manim/mobject/geometry/line.py +16 -8
  35. manim/mobject/geometry/polygram.py +17 -5
  36. manim/mobject/geometry/tips.py +2 -2
  37. manim/mobject/graph.py +379 -106
  38. manim/mobject/graphing/coordinate_systems.py +17 -20
  39. manim/mobject/graphing/functions.py +14 -10
  40. manim/mobject/graphing/number_line.py +1 -1
  41. manim/mobject/mobject.py +175 -72
  42. manim/mobject/opengl/opengl_compatibility.py +2 -0
  43. manim/mobject/opengl/opengl_geometry.py +26 -1
  44. manim/mobject/opengl/opengl_image_mobject.py +2 -0
  45. manim/mobject/opengl/opengl_mobject.py +3 -0
  46. manim/mobject/opengl/opengl_point_cloud_mobject.py +2 -0
  47. manim/mobject/opengl/opengl_surface.py +2 -0
  48. manim/mobject/opengl/opengl_three_dimensions.py +2 -0
  49. manim/mobject/opengl/opengl_vectorized_mobject.py +19 -14
  50. manim/mobject/svg/brace.py +2 -0
  51. manim/mobject/svg/svg_mobject.py +2 -2
  52. manim/mobject/table.py +0 -1
  53. manim/mobject/text/code_mobject.py +2 -0
  54. manim/mobject/text/numbers.py +2 -0
  55. manim/mobject/text/tex_mobject.py +1 -1
  56. manim/mobject/text/text_mobject.py +43 -6
  57. manim/mobject/three_d/three_d_utils.py +4 -4
  58. manim/mobject/three_d/three_dimensions.py +4 -4
  59. manim/mobject/types/image_mobject.py +5 -1
  60. manim/mobject/types/point_cloud_mobject.py +2 -0
  61. manim/mobject/types/vectorized_mobject.py +124 -29
  62. manim/mobject/value_tracker.py +3 -3
  63. manim/mobject/vector_field.py +3 -1
  64. manim/plugins/__init__.py +15 -1
  65. manim/plugins/plugins_flags.py +11 -5
  66. manim/renderer/cairo_renderer.py +12 -2
  67. manim/renderer/opengl_renderer.py +2 -3
  68. manim/renderer/opengl_renderer_window.py +2 -0
  69. manim/renderer/shader_wrapper.py +2 -0
  70. manim/renderer/vectorized_mobject_rendering.py +5 -0
  71. manim/scene/scene.py +22 -6
  72. manim/scene/scene_file_writer.py +3 -1
  73. manim/scene/section.py +2 -0
  74. manim/scene/three_d_scene.py +5 -6
  75. manim/scene/vector_space_scene.py +21 -5
  76. manim/typing.py +567 -67
  77. manim/utils/bezier.py +9 -18
  78. manim/utils/caching.py +2 -0
  79. manim/utils/color/BS381.py +1 -0
  80. manim/utils/color/XKCD.py +1 -0
  81. manim/utils/color/core.py +31 -13
  82. manim/utils/commands.py +8 -1
  83. manim/utils/debug.py +0 -1
  84. manim/utils/deprecation.py +3 -2
  85. manim/utils/docbuild/__init__.py +17 -0
  86. manim/utils/docbuild/autoaliasattr_directive.py +197 -0
  87. manim/utils/docbuild/autocolor_directive.py +9 -4
  88. manim/utils/docbuild/manim_directive.py +18 -9
  89. manim/utils/docbuild/module_parsing.py +198 -0
  90. manim/utils/exceptions.py +6 -0
  91. manim/utils/family.py +2 -0
  92. manim/utils/family_ops.py +5 -0
  93. manim/utils/file_ops.py +6 -2
  94. manim/utils/hashing.py +2 -0
  95. manim/utils/ipython_magic.py +2 -0
  96. manim/utils/module_ops.py +2 -0
  97. manim/utils/opengl.py +14 -0
  98. manim/utils/parameter_parsing.py +31 -0
  99. manim/utils/paths.py +12 -20
  100. manim/utils/rate_functions.py +6 -8
  101. manim/utils/space_ops.py +81 -36
  102. manim/utils/testing/__init__.py +17 -0
  103. manim/utils/testing/frames_comparison.py +7 -5
  104. manim/utils/tex.py +124 -196
  105. manim/utils/tex_file_writing.py +2 -0
  106. manim/utils/tex_templates.py +1 -0
  107. {manim-0.18.0.post0.dist-info → manim-0.18.1.dist-info}/LICENSE.community +1 -1
  108. {manim-0.18.0.post0.dist-info → manim-0.18.1.dist-info}/METADATA +29 -35
  109. manim-0.18.1.dist-info/RECORD +217 -0
  110. manim/cli/new/__init__.py +0 -0
  111. manim/cli/new/group.py +0 -189
  112. manim/plugins/import_plugins.py +0 -43
  113. manim-0.18.0.post0.dist-info/RECORD +0 -217
  114. {manim-0.18.0.post0.dist-info → manim-0.18.1.dist-info}/LICENSE +0 -0
  115. {manim-0.18.0.post0.dist-info → manim-0.18.1.dist-info}/WHEEL +0 -0
  116. {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
@@ -1,5 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
+ __all__ = [
4
+ "EndSceneEarlyException",
5
+ "RerunSceneException",
6
+ "MultiAnimationOverrideException",
7
+ ]
8
+
3
9
 
4
10
  class EndSceneEarlyException(Exception):
5
11
  pass
manim/utils/family.py CHANGED
@@ -6,6 +6,8 @@ from typing import Iterable
6
6
  from ..mobject.mobject import Mobject
7
7
  from ..utils.iterables import remove_list_redundancies
8
8
 
9
+ __all__ = ["extract_mobject_family_members"]
10
+
9
11
 
10
12
  def extract_mobject_family_members(
11
13
  mobjects: Iterable[Mobject],
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.Popen(commands)
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 = {
@@ -15,6 +15,8 @@ from manim.renderer.shader import shader_program_cache
15
15
 
16
16
  from ..constants import RendererType
17
17
 
18
+ __all__ = ["ManimMagic"]
19
+
18
20
  try:
19
21
  from IPython import get_ipython
20
22
  from IPython.core.interactiveshell import InteractiveShell
manim/utils/module_ops.py CHANGED
@@ -12,6 +12,8 @@ from pathlib import Path
12
12
  from .. import config, console, constants, logger
13
13
  from ..scene.scene_file_writer import SceneFileWriter
14
14
 
15
+ __all__ = ["scene_classes_from_file"]
16
+
15
17
 
16
18
  def get_module(file_name: Path):
17
19
  if str(file_name) == "-":
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 Callable
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
- STRAIGHT_PATH_THRESHOLD = 0.01
21
+ if TYPE_CHECKING:
22
+ from manim.typing import PathFuncType, Vector3D
23
+
23
24
 
24
- PATH_FUNC_TYPE = Callable[[np.ndarray, np.ndarray, float], np.ndarray]
25
+ STRAIGHT_PATH_THRESHOLD = 0.01
25
26
 
26
27
 
27
- # Remove `*args` and the `if` inside the functions when removing deprecation
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: np.ndarray = OUT
84
- ) -> PATH_FUNC_TYPE:
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: np.ndarray = OUT) -> PATH_FUNC_TYPE:
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() -> PATH_FUNC_TYPE:
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() -> PATH_FUNC_TYPE:
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: np.ndarray = OUT) -> PATH_FUNC_TYPE:
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
@@ -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
- return (
183
- 0
184
- if t <= 0
185
- else 35 * t**4 - 84 * t**5 + 70 * t**6 - 20 * t**7
186
- if t < 1
187
- else 1
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
- from manim.typing import Point3D_Array, Vector
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
- List[float]
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(np.cross(axis_z, RIGHT))
277
- axis_x = np.cross(axis_y, axis_z)
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(np.cross(UP, axis_z))
281
- axis_y = -np.cross(axis_x, axis_z)
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: np.ndarray, v2: np.ndarray, tol: float = 1e-6) -> np.ndarray:
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, v2 = (normalize(i) for i in (v1, v2))
380
- cp = np.cross(v1, v2)
381
- cp_norm = np.linalg.norm(cp)
382
- if cp_norm < tol:
383
- # Vectors align, so find a normal to them in the plane shared with the z-axis
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
- return normalize(cp)
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 = (np.cross(*i) for i in padded)
533
- x, y, z = np.cross(line1, line2)
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 = np.cross(v1, np.cross(v0, v1))
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: Sequence[Vector] | Vector, b: Sequence[Vector] | Vector
627
- ) -> Sequence[float] | float:
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(filter(lambda ring: ring[0] <= j < ring[-1], detached_rings))
719
- detached_rings.remove(new_ring)
720
- attached_rings.append(new_ring)
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 = np.cross(p1 - p2, norm_vector)
862
+ direction = cross(p1 - p2, norm_vector)
818
863
  m = midpoint(p1, p2)
819
864
  return [m + direction, m - direction]
@@ -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
+ """