manim 0.17.0__py3-none-any.whl → 0.19.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.
Files changed (163) hide show
  1. manim/__init__.py +11 -6
  2. manim/__main__.py +62 -19
  3. manim/_config/__init__.py +10 -9
  4. manim/_config/cli_colors.py +26 -9
  5. manim/_config/default.cfg +1 -3
  6. manim/_config/logger_utils.py +23 -13
  7. manim/_config/utils.py +662 -468
  8. manim/animation/animation.py +164 -18
  9. manim/animation/changing.py +34 -23
  10. manim/animation/composition.py +265 -67
  11. manim/animation/creation.py +208 -26
  12. manim/animation/fading.py +16 -18
  13. manim/animation/growing.py +35 -15
  14. manim/animation/indication.py +150 -76
  15. manim/animation/movement.py +56 -22
  16. manim/animation/numbers.py +64 -6
  17. manim/animation/rotation.py +78 -7
  18. manim/animation/specialized.py +6 -7
  19. manim/animation/speedmodifier.py +13 -10
  20. manim/animation/transform.py +14 -11
  21. manim/animation/transform_matching_parts.py +3 -4
  22. manim/animation/updaters/mobject_update_utils.py +152 -30
  23. manim/animation/updaters/update.py +10 -7
  24. manim/camera/camera.py +182 -118
  25. manim/camera/mapping_camera.py +34 -3
  26. manim/camera/moving_camera.py +95 -74
  27. manim/camera/multi_camera.py +23 -15
  28. manim/camera/three_d_camera.py +70 -52
  29. manim/cli/__init__.py +17 -0
  30. manim/cli/cfg/group.py +76 -44
  31. manim/cli/checkhealth/checks.py +192 -0
  32. manim/cli/checkhealth/commands.py +90 -0
  33. manim/cli/default_group.py +158 -25
  34. manim/cli/init/commands.py +33 -25
  35. manim/cli/plugins/commands.py +16 -3
  36. manim/cli/render/commands.py +72 -60
  37. manim/cli/render/ease_of_access_options.py +4 -3
  38. manim/cli/render/global_options.py +59 -17
  39. manim/cli/render/output_options.py +6 -5
  40. manim/cli/render/render_options.py +98 -33
  41. manim/constants.py +109 -59
  42. manim/data_structures.py +31 -0
  43. manim/mobject/frame.py +8 -5
  44. manim/mobject/geometry/__init__.py +1 -0
  45. manim/mobject/geometry/arc.py +277 -135
  46. manim/mobject/geometry/boolean_ops.py +32 -31
  47. manim/mobject/geometry/labeled.py +376 -0
  48. manim/mobject/geometry/line.py +192 -87
  49. manim/mobject/geometry/polygram.py +224 -58
  50. manim/mobject/geometry/shape_matchers.py +61 -25
  51. manim/mobject/geometry/tips.py +122 -48
  52. manim/mobject/graph.py +1027 -419
  53. manim/mobject/graphing/coordinate_systems.py +533 -278
  54. manim/mobject/graphing/functions.py +53 -32
  55. manim/mobject/graphing/number_line.py +123 -65
  56. manim/mobject/graphing/probability.py +88 -62
  57. manim/mobject/graphing/scale.py +33 -19
  58. manim/mobject/logo.py +118 -28
  59. manim/mobject/matrix.py +87 -83
  60. manim/mobject/mobject.py +912 -442
  61. manim/mobject/opengl/dot_cloud.py +16 -5
  62. manim/mobject/opengl/opengl_compatibility.py +4 -2
  63. manim/mobject/opengl/opengl_geometry.py +254 -153
  64. manim/mobject/opengl/opengl_image_mobject.py +3 -1
  65. manim/mobject/opengl/opengl_mobject.py +779 -482
  66. manim/mobject/opengl/opengl_point_cloud_mobject.py +41 -14
  67. manim/mobject/opengl/opengl_surface.py +14 -92
  68. manim/mobject/opengl/opengl_three_dimensions.py +12 -8
  69. manim/mobject/opengl/opengl_vectorized_mobject.py +98 -100
  70. manim/mobject/svg/brace.py +173 -41
  71. manim/mobject/svg/svg_mobject.py +139 -53
  72. manim/mobject/table.py +61 -68
  73. manim/mobject/text/code_mobject.py +193 -539
  74. manim/mobject/text/numbers.py +81 -34
  75. manim/mobject/text/tex_mobject.py +130 -78
  76. manim/mobject/text/text_mobject.py +288 -164
  77. manim/mobject/three_d/polyhedra.py +111 -13
  78. manim/mobject/three_d/three_d_utils.py +17 -8
  79. manim/mobject/three_d/three_dimensions.py +239 -106
  80. manim/mobject/types/image_mobject.py +50 -30
  81. manim/mobject/types/point_cloud_mobject.py +120 -75
  82. manim/mobject/types/vectorized_mobject.py +841 -408
  83. manim/mobject/value_tracker.py +105 -38
  84. manim/mobject/vector_field.py +50 -31
  85. manim/opengl/__init__.py +3 -3
  86. manim/plugins/__init__.py +14 -1
  87. manim/plugins/plugins_flags.py +10 -14
  88. manim/renderer/cairo_renderer.py +65 -50
  89. manim/renderer/opengl_renderer.py +89 -69
  90. manim/renderer/opengl_renderer_window.py +39 -18
  91. manim/renderer/shader.py +123 -87
  92. manim/renderer/shader_wrapper.py +44 -28
  93. manim/renderer/vectorized_mobject_rendering.py +38 -10
  94. manim/scene/moving_camera_scene.py +32 -3
  95. manim/scene/scene.py +507 -242
  96. manim/scene/scene_file_writer.py +371 -220
  97. manim/scene/section.py +20 -16
  98. manim/scene/three_d_scene.py +14 -22
  99. manim/scene/vector_space_scene.py +223 -129
  100. manim/scene/zoomed_scene.py +46 -41
  101. manim/typing.py +990 -0
  102. manim/utils/bezier.py +1823 -371
  103. manim/utils/caching.py +12 -5
  104. manim/utils/color/AS2700.py +236 -0
  105. manim/utils/color/BS381.py +318 -0
  106. manim/utils/color/DVIPSNAMES.py +96 -0
  107. manim/utils/color/SVGNAMES.py +179 -0
  108. manim/utils/color/X11.py +533 -0
  109. manim/utils/color/XKCD.py +952 -0
  110. manim/utils/color/__init__.py +61 -0
  111. manim/utils/color/core.py +1667 -0
  112. manim/utils/color/manim_colors.py +218 -0
  113. manim/utils/commands.py +48 -20
  114. manim/utils/config_ops.py +39 -19
  115. manim/utils/debug.py +8 -7
  116. manim/utils/deprecation.py +86 -39
  117. manim/utils/docbuild/__init__.py +17 -0
  118. manim/utils/docbuild/autoaliasattr_directive.py +236 -0
  119. manim/utils/docbuild/autocolor_directive.py +99 -0
  120. manim/utils/docbuild/manim_directive.py +94 -41
  121. manim/utils/docbuild/module_parsing.py +245 -0
  122. manim/utils/exceptions.py +6 -0
  123. manim/utils/family.py +5 -3
  124. manim/utils/family_ops.py +17 -4
  125. manim/utils/file_ops.py +27 -17
  126. manim/utils/hashing.py +55 -45
  127. manim/utils/images.py +13 -7
  128. manim/utils/ipython_magic.py +13 -7
  129. manim/utils/iterables.py +163 -120
  130. manim/utils/module_ops.py +66 -24
  131. manim/utils/opengl.py +77 -24
  132. manim/utils/parameter_parsing.py +32 -0
  133. manim/utils/paths.py +30 -33
  134. manim/utils/polylabel.py +235 -0
  135. manim/utils/qhull.py +218 -0
  136. manim/utils/rate_functions.py +98 -32
  137. manim/utils/simple_functions.py +25 -33
  138. manim/utils/sounds.py +7 -1
  139. manim/utils/space_ops.py +188 -115
  140. manim/utils/testing/__init__.py +17 -0
  141. manim/utils/testing/_frames_testers.py +13 -8
  142. manim/utils/testing/_show_diff.py +5 -3
  143. manim/utils/testing/_test_class_makers.py +34 -18
  144. manim/utils/testing/frames_comparison.py +37 -19
  145. manim/utils/tex.py +130 -198
  146. manim/utils/tex_file_writing.py +77 -47
  147. manim/utils/tex_templates.py +2 -1
  148. manim/utils/unit.py +6 -5
  149. {manim-0.17.0.dist-info → manim-0.19.1.dist-info}/METADATA +64 -65
  150. manim-0.19.1.dist-info/RECORD +220 -0
  151. {manim-0.17.0.dist-info → manim-0.19.1.dist-info}/WHEEL +1 -1
  152. manim-0.19.1.dist-info/entry_points.txt +3 -0
  153. {manim-0.17.0.dist-info → manim-0.19.1.dist-info/licenses}/LICENSE.community +1 -1
  154. manim/cli/new/group.py +0 -189
  155. manim/communitycolors.py +0 -9
  156. manim/gui/__init__.py +0 -0
  157. manim/gui/gui.py +0 -82
  158. manim/plugins/import_plugins.py +0 -43
  159. manim/utils/color.py +0 -552
  160. manim-0.17.0.dist-info/RECORD +0 -206
  161. manim-0.17.0.dist-info/entry_points.txt +0 -4
  162. /manim/cli/{new → checkhealth}/__init__.py +0 -0
  163. {manim-0.17.0.dist-info → manim-0.19.1.dist-info/licenses}/LICENSE +0 -0
@@ -4,18 +4,21 @@ from __future__ import annotations
4
4
 
5
5
  __all__ = ["Brace", "BraceLabel", "ArcBrace", "BraceText", "BraceBetweenPoints"]
6
6
 
7
- from typing import Sequence
7
+ from typing import TYPE_CHECKING, Any
8
8
 
9
9
  import numpy as np
10
10
  import svgelements as se
11
+ from typing_extensions import Self
11
12
 
12
13
  from manim._config import config
13
14
  from manim.mobject.geometry.arc import Arc
14
15
  from manim.mobject.geometry.line import Line
15
16
  from manim.mobject.mobject import Mobject
16
17
  from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
17
- from manim.mobject.text.tex_mobject import MathTex, Tex
18
+ from manim.mobject.text.tex_mobject import MathTex, SingleStringMathTex, Tex
19
+ from manim.mobject.text.text_mobject import Text
18
20
 
21
+ from ...animation.animation import Animation
19
22
  from ...animation.composition import AnimationGroup
20
23
  from ...animation.fading import FadeIn
21
24
  from ...animation.growing import GrowFromCenter
@@ -24,6 +27,10 @@ from ...mobject.types.vectorized_mobject import VMobject
24
27
  from ...utils.color import BLACK
25
28
  from ..svg.svg_mobject import VMobjectFromSVGPath
26
29
 
30
+ if TYPE_CHECKING:
31
+ from manim.typing import Point3D, Point3DLike, Vector3D, Vector3DLike
32
+ from manim.utils.color.core import ParsableManimColor
33
+
27
34
 
28
35
  class Brace(VMobjectFromSVGPath):
29
36
  """Takes a mobject and draws a brace adjacent to it.
@@ -63,14 +70,14 @@ class Brace(VMobjectFromSVGPath):
63
70
  def __init__(
64
71
  self,
65
72
  mobject: Mobject,
66
- direction: Sequence[float] | None = DOWN,
67
- buff=0.2,
68
- sharpness=2,
69
- stroke_width=0,
70
- fill_opacity=1.0,
71
- background_stroke_width=0,
72
- background_stroke_color=BLACK,
73
- **kwargs,
73
+ direction: Vector3DLike = DOWN,
74
+ buff: float = 0.2,
75
+ sharpness: float = 2,
76
+ stroke_width: float = 0,
77
+ fill_opacity: float = 1.0,
78
+ background_stroke_width: float = 0,
79
+ background_stroke_color: ParsableManimColor = BLACK,
80
+ **kwargs: Any,
74
81
  ):
75
82
  path_string_template = (
76
83
  "m0.01216 0c-0.01152 0-0.01216 6.103e-4 -0.01216 0.01311v0.007762c0.06776 "
@@ -123,7 +130,20 @@ class Brace(VMobjectFromSVGPath):
123
130
  for mob in mobject, self:
124
131
  mob.rotate(angle, about_point=ORIGIN)
125
132
 
126
- def put_at_tip(self, mob, use_next_to=True, **kwargs):
133
+ def put_at_tip(self, mob: Mobject, use_next_to: bool = True, **kwargs: Any) -> Self:
134
+ """Puts the given mobject at the brace tip.
135
+
136
+ Parameters
137
+ ----------
138
+ mob
139
+ The mobject to be placed at the tip.
140
+ use_next_to
141
+ If true, then :meth:`next_to` is used to place the mobject at the
142
+ tip.
143
+ kwargs
144
+ Any additional keyword arguments are passed to :meth:`next_to` which
145
+ is used to put the mobject next to the brace tip.
146
+ """
127
147
  if use_next_to:
128
148
  mob.next_to(self.get_tip(), np.round(self.get_direction()), **kwargs)
129
149
  else:
@@ -133,80 +153,190 @@ class Brace(VMobjectFromSVGPath):
133
153
  mob.shift(self.get_direction() * shift_distance)
134
154
  return self
135
155
 
136
- def get_text(self, *text, **kwargs):
156
+ def get_text(self, *text: str, **kwargs: Any) -> Tex:
157
+ """Places the text at the brace tip.
158
+
159
+ Parameters
160
+ ----------
161
+ text
162
+ The text to be placed at the brace tip.
163
+ kwargs
164
+ Any additional keyword arguments are passed to :meth:`.put_at_tip` which
165
+ is used to position the text at the brace tip.
166
+
167
+ Returns
168
+ -------
169
+ :class:`~.Tex`
170
+ """
137
171
  text_mob = Tex(*text)
138
172
  self.put_at_tip(text_mob, **kwargs)
139
173
  return text_mob
140
174
 
141
- def get_tex(self, *tex, **kwargs):
175
+ def get_tex(self, *tex: str, **kwargs: Any) -> MathTex:
176
+ """Places the tex at the brace tip.
177
+
178
+ Parameters
179
+ ----------
180
+ tex
181
+ The tex to be placed at the brace tip.
182
+ kwargs
183
+ Any further keyword arguments are passed to :meth:`.put_at_tip` which
184
+ is used to position the tex at the brace tip.
185
+
186
+ Returns
187
+ -------
188
+ :class:`~.MathTex`
189
+ """
142
190
  tex_mob = MathTex(*tex)
143
191
  self.put_at_tip(tex_mob, **kwargs)
144
192
  return tex_mob
145
193
 
146
- def get_tip(self):
194
+ def get_tip(self) -> Point3D:
195
+ """Returns the point at the brace tip."""
147
196
  # Returns the position of the seventh point in the path, which is the tip.
148
197
  if config["renderer"] == "opengl":
149
198
  return self.points[34]
150
199
 
151
200
  return self.points[28] # = 7*4
152
201
 
153
- def get_direction(self):
202
+ def get_direction(self) -> Vector3D:
203
+ """Returns the direction from the center to the brace tip."""
154
204
  vect = self.get_tip() - self.get_center()
155
205
  return vect / np.linalg.norm(vect)
156
206
 
157
207
 
158
208
  class BraceLabel(VMobject, metaclass=ConvertToOpenGL):
209
+ """Create a brace with a label attached.
210
+
211
+ Parameters
212
+ ----------
213
+ obj
214
+ The mobject adjacent to which the brace is placed.
215
+ text
216
+ The label text.
217
+ brace_direction
218
+ The direction of the brace. By default ``DOWN``.
219
+ label_constructor
220
+ A class or function used to construct a mobject representing
221
+ the label. By default :class:`~.MathTex`.
222
+ font_size
223
+ The font size of the label, passed to the ``label_constructor``.
224
+ buff
225
+ The buffer between the mobject and the brace.
226
+ brace_config
227
+ Arguments to be passed to :class:`.Brace`.
228
+ kwargs
229
+ Additional arguments to be passed to :class:`~.VMobject`.
230
+ """
231
+
159
232
  def __init__(
160
233
  self,
161
- obj,
162
- text,
163
- brace_direction=DOWN,
164
- label_constructor=MathTex,
165
- font_size=DEFAULT_FONT_SIZE,
166
- buff=0.2,
167
- **kwargs,
234
+ obj: Mobject,
235
+ text: str,
236
+ brace_direction: Vector3DLike = DOWN,
237
+ label_constructor: type[SingleStringMathTex | Text] = MathTex,
238
+ font_size: float = DEFAULT_FONT_SIZE,
239
+ buff: float = 0.2,
240
+ brace_config: dict[str, Any] | None = None,
241
+ **kwargs: Any,
168
242
  ):
169
243
  self.label_constructor = label_constructor
170
244
  super().__init__(**kwargs)
171
245
 
172
246
  self.brace_direction = brace_direction
173
- self.buff = buff
174
- if isinstance(obj, list):
175
- obj = self.get_group_class()(*obj)
176
- self.brace = Brace(obj, brace_direction, buff, **kwargs)
247
+ if brace_config is None:
248
+ brace_config = {}
249
+ self.brace = Brace(obj, brace_direction, buff, **brace_config)
177
250
 
178
251
  if isinstance(text, (tuple, list)):
179
- self.label = self.label_constructor(font_size=font_size, *text, **kwargs)
252
+ self.label: VMobject = self.label_constructor(
253
+ *text, font_size=font_size, **kwargs
254
+ )
180
255
  else:
181
256
  self.label = self.label_constructor(str(text), font_size=font_size)
182
257
 
183
258
  self.brace.put_at_tip(self.label)
184
259
  self.add(self.brace, self.label)
185
260
 
186
- def creation_anim(self, label_anim=FadeIn, brace_anim=GrowFromCenter):
261
+ def creation_anim(
262
+ self,
263
+ label_anim: type[Animation] = FadeIn,
264
+ brace_anim: type[Animation] = GrowFromCenter,
265
+ ) -> AnimationGroup:
187
266
  return AnimationGroup(brace_anim(self.brace), label_anim(self.label))
188
267
 
189
- def shift_brace(self, obj, **kwargs):
268
+ def shift_brace(self, obj: Mobject, **kwargs: Any) -> Self:
190
269
  if isinstance(obj, list):
191
270
  obj = self.get_group_class()(*obj)
192
271
  self.brace = Brace(obj, self.brace_direction, **kwargs)
193
272
  self.brace.put_at_tip(self.label)
194
273
  return self
195
274
 
196
- def change_label(self, *text, **kwargs):
197
- self.label = self.label_constructor(*text, **kwargs)
198
-
275
+ def change_label(self, *text: str, **kwargs: Any) -> Self:
276
+ self.remove(self.label)
277
+ self.label = self.label_constructor(*text, **kwargs) # type: ignore[arg-type]
199
278
  self.brace.put_at_tip(self.label)
279
+ self.add(self.label)
200
280
  return self
201
281
 
202
- def change_brace_label(self, obj, *text, **kwargs):
282
+ def change_brace_label(self, obj: Mobject, *text: str, **kwargs: Any) -> Self:
203
283
  self.shift_brace(obj)
204
284
  self.change_label(*text, **kwargs)
205
285
  return self
206
286
 
207
287
 
208
288
  class BraceText(BraceLabel):
209
- def __init__(self, obj, text, label_constructor=Tex, **kwargs):
289
+ """Create a brace with a text label attached.
290
+
291
+ Parameters
292
+ ----------
293
+ obj
294
+ The mobject adjacent to which the brace is placed.
295
+ text
296
+ The label text.
297
+ brace_direction
298
+ The direction of the brace. By default ``DOWN``.
299
+ label_constructor
300
+ A class or function used to construct a mobject representing
301
+ the label. By default :class:`~.Text`.
302
+ font_size
303
+ The font size of the label, passed to the ``label_constructor``.
304
+ buff
305
+ The buffer between the mobject and the brace.
306
+ brace_config
307
+ Arguments to be passed to :class:`.Brace`.
308
+ kwargs
309
+ Additional arguments to be passed to :class:`~.VMobject`.
310
+
311
+
312
+ Examples
313
+ --------
314
+ .. manim:: BraceTextExample
315
+ :save_last_frame:
316
+
317
+ class BraceTextExample(Scene):
318
+ def construct(self):
319
+ s1 = Square().move_to(2*LEFT)
320
+ self.add(s1)
321
+ br1 = BraceText(s1, "Label")
322
+ self.add(br1)
323
+
324
+ s2 = Square().move_to(2*RIGHT)
325
+ self.add(s2)
326
+ br2 = BraceText(s2, "Label")
327
+
328
+ br2.change_label("new")
329
+ self.add(br2)
330
+ self.wait(0.1)
331
+ """
332
+
333
+ def __init__(
334
+ self,
335
+ obj: Mobject,
336
+ text: str,
337
+ label_constructor: type[SingleStringMathTex | Text] = Text,
338
+ **kwargs: Any,
339
+ ):
210
340
  super().__init__(obj, text, label_constructor=label_constructor, **kwargs)
211
341
 
212
342
 
@@ -244,10 +374,10 @@ class BraceBetweenPoints(Brace):
244
374
 
245
375
  def __init__(
246
376
  self,
247
- point_1: Sequence[float] | None,
248
- point_2: Sequence[float] | None,
249
- direction: Sequence[float] | None = ORIGIN,
250
- **kwargs,
377
+ point_1: Point3DLike,
378
+ point_2: Point3DLike,
379
+ direction: Vector3DLike = ORIGIN,
380
+ **kwargs: Any,
251
381
  ):
252
382
  if all(direction == ORIGIN):
253
383
  line_vector = np.array(point_2) - np.array(point_1)
@@ -312,10 +442,12 @@ class ArcBrace(Brace):
312
442
 
313
443
  def __init__(
314
444
  self,
315
- arc: Arc = Arc(start_angle=-1, angle=2, radius=1),
316
- direction: Sequence[float] = RIGHT,
317
- **kwargs,
445
+ arc: Arc | None = None,
446
+ direction: Vector3DLike = RIGHT,
447
+ **kwargs: Any,
318
448
  ):
449
+ if arc is None:
450
+ arc = Arc(start_angle=-1, angle=2, radius=1)
319
451
  arc_end_angle = arc.start_angle + arc.angle
320
452
  line = Line(UP * arc.start_angle, UP * arc_end_angle)
321
453
  scale_shift = RIGHT * np.log(arc.radius)
@@ -4,14 +4,17 @@ from __future__ import annotations
4
4
 
5
5
  import os
6
6
  from pathlib import Path
7
+ from typing import Any
7
8
  from xml.etree import ElementTree as ET
8
9
 
9
10
  import numpy as np
10
11
  import svgelements as se
11
12
 
12
13
  from manim import config, logger
14
+ from manim.utils.color import ManimColor, ParsableManimColor
13
15
 
14
16
  from ...constants import RIGHT
17
+ from ...utils.bezier import get_quadratic_approximation_of_cubic
15
18
  from ...utils.images import get_full_vector_image_path
16
19
  from ...utils.iterables import hash_obj
17
20
  from ..geometry.arc import Circle
@@ -81,6 +84,12 @@ class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
81
84
  A dictionary with keyword arguments passed to
82
85
  :class:`.VMobjectFromSVGPath` used for importing path elements.
83
86
  If ``None`` (the default), no additional arguments are passed.
87
+ use_svg_cache
88
+ If True (default), the svg inputs (e.g. file_name, settings)
89
+ will be used as a key and a copy of the created mobject will
90
+ be saved using that key to be quickly retrieved if the same
91
+ inputs need be processed later. For large SVGs which are used
92
+ only once, this can be omitted to improve performance.
84
93
  kwargs
85
94
  Further arguments passed to the parent class.
86
95
  """
@@ -91,16 +100,17 @@ class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
91
100
  should_center: bool = True,
92
101
  height: float | None = 2,
93
102
  width: float | None = None,
94
- color: str | None = None,
103
+ color: ParsableManimColor | None = None,
95
104
  opacity: float | None = None,
96
- fill_color: str | None = None,
105
+ fill_color: ParsableManimColor | None = None,
97
106
  fill_opacity: float | None = None,
98
- stroke_color: str | None = None,
107
+ stroke_color: ParsableManimColor | None = None,
99
108
  stroke_opacity: float | None = None,
100
109
  stroke_width: float | None = None,
101
110
  svg_default: dict | None = None,
102
111
  path_string_config: dict | None = None,
103
- **kwargs,
112
+ use_svg_cache: bool = True,
113
+ **kwargs: Any,
104
114
  ):
105
115
  super().__init__(color=None, stroke_color=None, fill_color=None, **kwargs)
106
116
 
@@ -110,13 +120,15 @@ class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
110
120
  self.should_center = should_center
111
121
  self.svg_height = height
112
122
  self.svg_width = width
113
- self.color = color
123
+ self.color = ManimColor(color)
114
124
  self.opacity = opacity
115
125
  self.fill_color = fill_color
116
- self.fill_opacity = fill_opacity
126
+ self.fill_opacity = fill_opacity # type: ignore[assignment]
117
127
  self.stroke_color = stroke_color
118
- self.stroke_opacity = stroke_opacity
119
- self.stroke_width = stroke_width
128
+ self.stroke_opacity = stroke_opacity # type: ignore[assignment]
129
+ self.stroke_width = stroke_width # type: ignore[assignment]
130
+ if self.stroke_width is None:
131
+ self.stroke_width = 0
120
132
 
121
133
  if svg_default is None:
122
134
  svg_default = {
@@ -134,7 +146,7 @@ class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
134
146
  path_string_config = {}
135
147
  self.path_string_config = path_string_config
136
148
 
137
- self.init_svg_mobject()
149
+ self.init_svg_mobject(use_svg_cache=use_svg_cache)
138
150
 
139
151
  self.set_style(
140
152
  fill_color=fill_color,
@@ -145,7 +157,7 @@ class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
145
157
  )
146
158
  self.move_into_position()
147
159
 
148
- def init_svg_mobject(self) -> None:
160
+ def init_svg_mobject(self, use_svg_cache: bool) -> None:
149
161
  """Checks whether the SVG has already been imported and
150
162
  generates it if not.
151
163
 
@@ -153,14 +165,16 @@ class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
153
165
  --------
154
166
  :meth:`.SVGMobject.generate_mobject`
155
167
  """
156
- hash_val = hash_obj(self.hash_seed)
157
- if hash_val in SVG_HASH_TO_MOB_MAP:
158
- mob = SVG_HASH_TO_MOB_MAP[hash_val].copy()
159
- self.add(*mob)
160
- return
168
+ if use_svg_cache:
169
+ hash_val = hash_obj(self.hash_seed)
170
+ if hash_val in SVG_HASH_TO_MOB_MAP:
171
+ mob = SVG_HASH_TO_MOB_MAP[hash_val].copy()
172
+ self.add(*mob)
173
+ return
161
174
 
162
175
  self.generate_mobject()
163
- SVG_HASH_TO_MOB_MAP[hash_val] = self.copy()
176
+ if use_svg_cache:
177
+ SVG_HASH_TO_MOB_MAP[hash_val] = self.copy()
164
178
 
165
179
  @property
166
180
  def hash_seed(self) -> tuple:
@@ -181,7 +195,7 @@ class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
181
195
  """Parse the SVG and translate its elements to submobjects."""
182
196
  file_path = self.get_file_path()
183
197
  element_tree = ET.parse(file_path)
184
- new_tree = self.modify_xml_tree(element_tree)
198
+ new_tree = self.modify_xml_tree(element_tree) # type: ignore[arg-type]
185
199
  # Create a temporary svg file to dump modified svg to be parsed
186
200
  modified_file_path = file_path.with_name(f"{file_path.stem}_{file_path.suffix}")
187
201
  new_tree.write(modified_file_path)
@@ -218,12 +232,12 @@ class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
218
232
  "style",
219
233
  )
220
234
  root = element_tree.getroot()
221
- root_style_dict = {k: v for k, v in root.attrib.items() if k in style_keys}
235
+ root_style_dict = {k: v for k, v in root.attrib.items() if k in style_keys} # type: ignore[union-attr]
222
236
 
223
237
  new_root = ET.Element("svg", {})
224
238
  config_style_node = ET.SubElement(new_root, "g", config_style_dict)
225
239
  root_style_node = ET.SubElement(config_style_node, "g", root_style_dict)
226
- root_style_node.extend(root)
240
+ root_style_node.extend(root) # type: ignore[arg-type]
227
241
  return ET.ElementTree(new_root)
228
242
 
229
243
  def generate_config_style_dict(self) -> dict[str, str]:
@@ -252,12 +266,13 @@ class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
252
266
  svg
253
267
  The parsed SVG file.
254
268
  """
255
- result = []
269
+ result: list[VMobject] = []
256
270
  for shape in svg.elements():
257
- if isinstance(shape, se.Group):
271
+ # can we combine the two continue cases into one?
272
+ if isinstance(shape, se.Group): # noqa: SIM114
258
273
  continue
259
274
  elif isinstance(shape, se.Path):
260
- mob = self.path_to_mobject(shape)
275
+ mob: VMobject = self.path_to_mobject(shape)
261
276
  elif isinstance(shape, se.SimpleLine):
262
277
  mob = self.line_to_mobject(shape)
263
278
  elif isinstance(shape, se.Rect):
@@ -270,7 +285,7 @@ class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
270
285
  mob = self.polyline_to_mobject(shape)
271
286
  elif isinstance(shape, se.Text):
272
287
  mob = self.text_to_mobject(shape)
273
- elif isinstance(shape, se.Use) or type(shape) == se.SVGElement:
288
+ elif isinstance(shape, se.Use) or type(shape) is se.SVGElement:
274
289
  continue
275
290
  else:
276
291
  logger.warning(f"Unsupported element type: {type(shape)}")
@@ -411,7 +426,7 @@ class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
411
426
  return vmobject_class().set_points_as_corners(points)
412
427
 
413
428
  @staticmethod
414
- def text_to_mobject(text: se.Text):
429
+ def text_to_mobject(text: se.Text) -> VMobject:
415
430
  """Convert a text element to a vectorized mobject.
416
431
 
417
432
  .. warning::
@@ -424,16 +439,16 @@ class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
424
439
  The parsed SVG text.
425
440
  """
426
441
  logger.warning(f"Unsupported element type: {type(text)}")
427
- return
442
+ return # type: ignore[return-value]
428
443
 
429
444
  def move_into_position(self) -> None:
430
445
  """Scale and move the generated mobject into position."""
431
446
  if self.should_center:
432
447
  self.center()
433
448
  if self.svg_height is not None:
434
- self.set_height(self.svg_height)
449
+ self.set(height=self.svg_height)
435
450
  if self.svg_width is not None:
436
- self.set_width(self.svg_width)
451
+ self.set(width=self.svg_width)
437
452
 
438
453
 
439
454
  class VMobjectFromSVGPath(VMobject, metaclass=ConvertToOpenGL):
@@ -469,7 +484,7 @@ class VMobjectFromSVGPath(VMobject, metaclass=ConvertToOpenGL):
469
484
  long_lines: bool = False,
470
485
  should_subdivide_sharp_curves: bool = False,
471
486
  should_remove_null_curves: bool = False,
472
- **kwargs,
487
+ **kwargs: Any,
473
488
  ):
474
489
  # Get rid of arcs
475
490
  path_obj.approximate_arcs_with_quads()
@@ -481,7 +496,7 @@ class VMobjectFromSVGPath(VMobject, metaclass=ConvertToOpenGL):
481
496
 
482
497
  super().__init__(**kwargs)
483
498
 
484
- def init_points(self) -> None:
499
+ def generate_points(self) -> None:
485
500
  # TODO: cache mobject in a re-importable way
486
501
 
487
502
  self.handle_commands()
@@ -494,31 +509,102 @@ class VMobjectFromSVGPath(VMobject, metaclass=ConvertToOpenGL):
494
509
  # Get rid of any null curves
495
510
  self.set_points(self.get_points_without_null_curves())
496
511
 
497
- generate_points = init_points
512
+ def init_points(self) -> None:
513
+ self.generate_points()
498
514
 
499
515
  def handle_commands(self) -> None:
500
- segment_class_to_func_map = {
501
- se.Move: (self.start_new_path, ("end",)),
502
- se.Close: (self.close_path, ()),
503
- se.Line: (self.add_line_to, ("end",)),
504
- se.QuadraticBezier: (
505
- self.add_quadratic_bezier_curve_to,
506
- ("control", "end"),
507
- ),
508
- se.CubicBezier: (
509
- self.add_cubic_bezier_curve_to,
510
- ("control1", "control2", "end"),
511
- ),
512
- }
516
+ all_points: list[np.ndarray] = []
517
+ last_move: np.ndarray = None
518
+ curve_start = None
519
+ last_true_move = None
520
+
521
+ def move_pen(pt: np.ndarray, *, true_move: bool = False) -> None:
522
+ nonlocal last_move, curve_start, last_true_move
523
+ last_move = pt
524
+ if curve_start is None:
525
+ curve_start = last_move
526
+ if true_move:
527
+ last_true_move = last_move
528
+
529
+ if self.n_points_per_curve == 4:
530
+
531
+ def add_cubic(
532
+ start: np.ndarray, cp1: np.ndarray, cp2: np.ndarray, end: np.ndarray
533
+ ) -> None:
534
+ nonlocal all_points
535
+ assert len(all_points) % 4 == 0, len(all_points)
536
+ all_points += [start, cp1, cp2, end]
537
+ move_pen(end)
538
+
539
+ def add_quad(start: np.ndarray, cp: np.ndarray, end: np.ndarray) -> None:
540
+ add_cubic(start, (start + cp + cp) / 3, (cp + cp + end) / 3, end)
541
+ move_pen(end)
542
+
543
+ def add_line(start: np.ndarray, end: np.ndarray) -> None:
544
+ add_cubic(
545
+ start, (start + start + end) / 3, (start + end + end) / 3, end
546
+ )
547
+ move_pen(end)
548
+
549
+ else:
550
+
551
+ def add_cubic(
552
+ start: np.ndarray, cp1: np.ndarray, cp2: np.ndarray, end: np.ndarray
553
+ ) -> None:
554
+ nonlocal all_points
555
+ assert len(all_points) % 3 == 0, len(all_points)
556
+ two_quads = get_quadratic_approximation_of_cubic(
557
+ start,
558
+ cp1,
559
+ cp2,
560
+ end,
561
+ )
562
+ all_points += two_quads[:3].tolist()
563
+ all_points += two_quads[3:].tolist()
564
+ move_pen(end)
565
+
566
+ def add_quad(start: np.ndarray, cp: np.ndarray, end: np.ndarray) -> None:
567
+ nonlocal all_points
568
+ assert len(all_points) % 3 == 0, len(all_points)
569
+ all_points += [start, cp, end]
570
+ move_pen(end)
571
+
572
+ def add_line(start: np.ndarray, end: np.ndarray) -> None:
573
+ add_quad(start, (start + end) / 2, end)
574
+ move_pen(end)
575
+
513
576
  for segment in self.path_obj:
514
577
  segment_class = segment.__class__
515
- func, attr_names = segment_class_to_func_map[segment_class]
516
- points = [
517
- _convert_point_to_3d(*segment.__getattribute__(attr_name))
518
- for attr_name in attr_names
519
- ]
520
- func(*points)
521
-
522
- # Get rid of the side effect of trailing "Z M" commands.
523
- if self.has_new_path_started():
524
- self.resize_points(self.get_num_points() - 1)
578
+ if segment_class == se.Move:
579
+ move_pen(_convert_point_to_3d(*segment.end), true_move=True)
580
+ elif segment_class == se.Line:
581
+ add_line(last_move, _convert_point_to_3d(*segment.end))
582
+ elif segment_class == se.QuadraticBezier:
583
+ add_quad(
584
+ last_move,
585
+ _convert_point_to_3d(*segment.control),
586
+ _convert_point_to_3d(*segment.end),
587
+ )
588
+ elif segment_class == se.CubicBezier:
589
+ add_cubic(
590
+ last_move,
591
+ _convert_point_to_3d(*segment.control1),
592
+ _convert_point_to_3d(*segment.control2),
593
+ _convert_point_to_3d(*segment.end),
594
+ )
595
+ elif segment_class == se.Close:
596
+ # If the SVG path naturally ends at the beginning of the curve,
597
+ # we do *not* need to draw a closing line. To account for floating
598
+ # point precision, we use a small value to compare the two points.
599
+ if abs(np.linalg.norm(last_move - last_true_move)) > 0.0001:
600
+ add_line(last_move, last_true_move)
601
+ curve_start = None
602
+ else:
603
+ raise AssertionError(f"Not implemented: {segment_class}")
604
+
605
+ self.points = np.array(all_points, ndmin=2, dtype="float64")
606
+ # If we have no points, make sure the array is shaped properly
607
+ # (0 rows tall by 3 columns wide) so future operations can
608
+ # add or remove points correctly.
609
+ if len(all_points) == 0:
610
+ self.points = np.reshape(self.points, (0, 3))