mirror of
				https://github.com/3b1b/manim.git
				synced 2025-11-01 15:08:59 +00:00 
			
		
		
		
	Misc. clean up (#2269)
* Comment tweak * Directly print traceback Since the shell.showtraceback is giving some issues * Make InteracrtiveSceneEmbed into a class This way it can keep track of it's internal shell; use of get_ipython has a finicky relationship with reloading. * Move remaining checkpoint_paste logic into scene_embed.py This involved making a few context managers for Scene: temp_record, temp_skip, temp_progress_bar, which seem useful in and of themselves. * Change null key to be the empty string * Ensure temporary svg paths for Text are deleted * Remove unused dict_ops.py functions * Remove break_into_partial_movies from file_writer configuration * Rewrite guarantee_existence using Path * Clean up SceneFileWriter It had a number of vestigial functions no longer used, and some setup that could be made more organized. * Remove --save_pngs CLI arg (which did nothing) * Add --subdivide CLI arg * Remove add_extension_if_not_present * Remove get_sorted_integer_files * Have find_file return Path * Minor clean up * Clean up num_tex_symbols * Fix find_file * Minor cleanup for extract_scene.py * Add preview_frame_while_skipping option to scene config * Use shell.showtraceback function * Move keybindings to config, instead of in-place constants * Replace DEGREES -> DEG
This commit is contained in:
		
							parent
							
								
									00b34f2020
								
							
						
					
					
						commit
						744e695340
					
				
					 24 changed files with 209 additions and 353 deletions
				
			
		| 
						 | 
				
			
			@ -74,7 +74,7 @@ Mathematical constant
 | 
			
		|||
 | 
			
		||||
   PI = np.pi
 | 
			
		||||
   TAU = 2 * PI
 | 
			
		||||
   DEGREES = TAU / 360
 | 
			
		||||
   DEG = TAU / 360
 | 
			
		||||
 | 
			
		||||
Text
 | 
			
		||||
----
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -34,7 +34,7 @@ InteractiveDevlopment
 | 
			
		|||
            self.play(ReplacementTransform(square, circle))
 | 
			
		||||
            self.wait()
 | 
			
		||||
            self.play(circle.animate.stretch(4, 0))
 | 
			
		||||
            self.play(Rotate(circle, 90 * DEGREES))
 | 
			
		||||
            self.play(Rotate(circle, 90 * DEG))
 | 
			
		||||
            self.play(circle.animate.shift(2 * RIGHT).scale(0.25))
 | 
			
		||||
 | 
			
		||||
            text = Text("""
 | 
			
		||||
| 
						 | 
				
			
			@ -221,7 +221,7 @@ TexTransformExample
 | 
			
		|||
            self.play(
 | 
			
		||||
                TransformMatchingTex(
 | 
			
		||||
                    lines[0].copy(), lines[1],
 | 
			
		||||
                    path_arc=90 * DEGREES,
 | 
			
		||||
                    path_arc=90 * DEG,
 | 
			
		||||
                ),
 | 
			
		||||
                **play_kw
 | 
			
		||||
            )
 | 
			
		||||
| 
						 | 
				
			
			@ -599,8 +599,8 @@ SurfaceExample
 | 
			
		|||
            # Set perspective
 | 
			
		||||
            frame = self.camera.frame
 | 
			
		||||
            frame.set_euler_angles(
 | 
			
		||||
                theta=-30 * DEGREES,
 | 
			
		||||
                phi=70 * DEGREES,
 | 
			
		||||
                theta=-30 * DEG,
 | 
			
		||||
                phi=70 * DEG,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            surface = surfaces[0]
 | 
			
		||||
| 
						 | 
				
			
			@ -624,8 +624,8 @@ SurfaceExample
 | 
			
		|||
            self.play(
 | 
			
		||||
                Transform(surface, surfaces[2]),
 | 
			
		||||
                # Move camera frame during the transition
 | 
			
		||||
                frame.animate.increment_phi(-10 * DEGREES),
 | 
			
		||||
                frame.animate.increment_theta(-20 * DEGREES),
 | 
			
		||||
                frame.animate.increment_phi(-10 * DEG),
 | 
			
		||||
                frame.animate.increment_theta(-20 * DEG),
 | 
			
		||||
                run_time=3
 | 
			
		||||
            )
 | 
			
		||||
            # Add ambient rotation
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -190,7 +190,7 @@ class TexTransformExample(Scene):
 | 
			
		|||
                # to go to a non-equal substring from the target,
 | 
			
		||||
                # use the key map.
 | 
			
		||||
                key_map={"+": "-"},
 | 
			
		||||
                path_arc=90 * DEGREES,
 | 
			
		||||
                path_arc=90 * DEG,
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
        self.wait()
 | 
			
		||||
| 
						 | 
				
			
			@ -203,7 +203,7 @@ class TexTransformExample(Scene):
 | 
			
		|||
            TransformMatchingStrings(
 | 
			
		||||
                lines[2].copy(), lines[3],
 | 
			
		||||
                key_map={"2": R"\sqrt"},
 | 
			
		||||
                path_arc=-30 * DEGREES,
 | 
			
		||||
                path_arc=-30 * DEG,
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
        self.wait(2)
 | 
			
		||||
| 
						 | 
				
			
			@ -616,8 +616,8 @@ class SurfaceExample(ThreeDScene):
 | 
			
		|||
        self.play(
 | 
			
		||||
            Transform(surface, surfaces[2]),
 | 
			
		||||
            # Move camera frame during the transition
 | 
			
		||||
            self.frame.animate.increment_phi(-10 * DEGREES),
 | 
			
		||||
            self.frame.animate.increment_theta(-20 * DEGREES),
 | 
			
		||||
            self.frame.animate.increment_phi(-10 * DEG),
 | 
			
		||||
            self.frame.animate.increment_theta(-20 * DEG),
 | 
			
		||||
            run_time=3
 | 
			
		||||
        )
 | 
			
		||||
        # Add ambient rotation
 | 
			
		||||
| 
						 | 
				
			
			@ -666,7 +666,7 @@ class InteractiveDevelopment(Scene):
 | 
			
		|||
        self.play(ReplacementTransform(square, circle))
 | 
			
		||||
        self.wait()
 | 
			
		||||
        self.play(circle.animate.stretch(4, 0))
 | 
			
		||||
        self.play(Rotate(circle, 90 * DEGREES))
 | 
			
		||||
        self.play(Rotate(circle, 90 * DEG))
 | 
			
		||||
        self.play(circle.animate.shift(2 * RIGHT).scale(0.25))
 | 
			
		||||
 | 
			
		||||
        text = Text("""
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,7 +14,7 @@ from manimlib.animation.transform import Transform
 | 
			
		|||
from manimlib.constants import FRAME_X_RADIUS, FRAME_Y_RADIUS
 | 
			
		||||
from manimlib.constants import ORIGIN, RIGHT, UP
 | 
			
		||||
from manimlib.constants import SMALL_BUFF
 | 
			
		||||
from manimlib.constants import DEGREES
 | 
			
		||||
from manimlib.constants import DEG
 | 
			
		||||
from manimlib.constants import TAU
 | 
			
		||||
from manimlib.constants import GREY, YELLOW
 | 
			
		||||
from manimlib.mobject.geometry import Circle
 | 
			
		||||
| 
						 | 
				
			
			@ -395,7 +395,7 @@ class WiggleOutThenIn(Animation):
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
class TurnInsideOut(Transform):
 | 
			
		||||
    def __init__(self, mobject: Mobject, path_arc: float = 90 * DEGREES, **kwargs):
 | 
			
		||||
    def __init__(self, mobject: Mobject, path_arc: float = 90 * DEG, **kwargs):
 | 
			
		||||
        super().__init__(mobject, path_arc=path_arc, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def create_target(self) -> Mobject:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,7 +5,7 @@ import inspect
 | 
			
		|||
import numpy as np
 | 
			
		||||
 | 
			
		||||
from manimlib.animation.animation import Animation
 | 
			
		||||
from manimlib.constants import DEGREES
 | 
			
		||||
from manimlib.constants import DEG
 | 
			
		||||
from manimlib.constants import OUT
 | 
			
		||||
from manimlib.mobject.mobject import Group
 | 
			
		||||
from manimlib.mobject.mobject import Mobject
 | 
			
		||||
| 
						 | 
				
			
			@ -314,7 +314,7 @@ class ApplyComplexFunction(ApplyMethod):
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
class CyclicReplace(Transform):
 | 
			
		||||
    def __init__(self, *mobjects: Mobject, path_arc=90 * DEGREES, **kwargs):
 | 
			
		||||
    def __init__(self, *mobjects: Mobject, path_arc=90 * DEG, **kwargs):
 | 
			
		||||
        super().__init__(Group(*mobjects), path_arc=path_arc, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def create_target(self) -> Mobject:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,7 +6,7 @@ import warnings
 | 
			
		|||
import numpy as np
 | 
			
		||||
from scipy.spatial.transform import Rotation
 | 
			
		||||
 | 
			
		||||
from manimlib.constants import DEGREES, RADIANS
 | 
			
		||||
from manimlib.constants import DEG, RADIANS
 | 
			
		||||
from manimlib.constants import FRAME_SHAPE
 | 
			
		||||
from manimlib.constants import DOWN, LEFT, ORIGIN, OUT, RIGHT, UP
 | 
			
		||||
from manimlib.constants import PI
 | 
			
		||||
| 
						 | 
				
			
			@ -26,7 +26,7 @@ class CameraFrame(Mobject):
 | 
			
		|||
        frame_shape: tuple[float, float] = FRAME_SHAPE,
 | 
			
		||||
        center_point: Vect3 = ORIGIN,
 | 
			
		||||
        # Field of view in the y direction
 | 
			
		||||
        fovy: float = 45 * DEGREES,
 | 
			
		||||
        fovy: float = 45 * DEG,
 | 
			
		||||
        euler_axes: str = "zxz",
 | 
			
		||||
        # This keeps it ordered first in a scene
 | 
			
		||||
        z_index=-1,
 | 
			
		||||
| 
						 | 
				
			
			@ -181,7 +181,7 @@ class CameraFrame(Mobject):
 | 
			
		|||
        Shortcut for set_euler_angles, defaulting to taking
 | 
			
		||||
        in angles in degrees
 | 
			
		||||
        """
 | 
			
		||||
        self.set_euler_angles(theta_degrees, phi_degrees, gamma_degrees, units=DEGREES)
 | 
			
		||||
        self.set_euler_angles(theta_degrees, phi_degrees, gamma_degrees, units=DEG)
 | 
			
		||||
        if center is not None:
 | 
			
		||||
            self.move_to(np.array(center))
 | 
			
		||||
        if height is not None:
 | 
			
		||||
| 
						 | 
				
			
			@ -209,7 +209,7 @@ class CameraFrame(Mobject):
 | 
			
		|||
        self.increment_euler_angles(dgamma=dgamma, units=units)
 | 
			
		||||
        return self
 | 
			
		||||
 | 
			
		||||
    def add_ambient_rotation(self, angular_speed=1 * DEGREES):
 | 
			
		||||
    def add_ambient_rotation(self, angular_speed=1 * DEG):
 | 
			
		||||
        self.add_updater(lambda m, dt: m.increment_theta(angular_speed * dt))
 | 
			
		||||
        return self
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -105,11 +105,6 @@ def parse_cli():
 | 
			
		|||
            help="Scene will stay paused during wait calls until " + \
 | 
			
		||||
                 "space bar or right arrow is hit, like a slide show"
 | 
			
		||||
        )
 | 
			
		||||
        parser.add_argument(
 | 
			
		||||
            "-g", "--save_pngs",
 | 
			
		||||
            action="store_true",
 | 
			
		||||
            help="Save each frame as a png",
 | 
			
		||||
        )
 | 
			
		||||
        parser.add_argument(
 | 
			
		||||
            "-i", "--gif",
 | 
			
		||||
            action="store_true",
 | 
			
		||||
| 
						 | 
				
			
			@ -148,6 +143,12 @@ def parse_cli():
 | 
			
		|||
            action="store_true",
 | 
			
		||||
            help="Show the output file in finder",
 | 
			
		||||
        )
 | 
			
		||||
        parser.add_argument(
 | 
			
		||||
            "--subdivide",
 | 
			
		||||
            action="store_true",
 | 
			
		||||
            help="Divide the output animation into individual movie files " +
 | 
			
		||||
                 "for each animation",
 | 
			
		||||
        )
 | 
			
		||||
        parser.add_argument(
 | 
			
		||||
            "--file_name",
 | 
			
		||||
            help="Name for the movie or image file",
 | 
			
		||||
| 
						 | 
				
			
			@ -261,8 +262,8 @@ def update_file_writer_config(config: dict, args: Namespace):
 | 
			
		|||
    file_writer_config = config["file_writer"]
 | 
			
		||||
    file_writer_config.update(
 | 
			
		||||
        write_to_movie=(not args.skip_animations and args.write_file),
 | 
			
		||||
        subdivide_output=args.subdivide,
 | 
			
		||||
        save_last_frame=(args.skip_animations and args.write_file),
 | 
			
		||||
        save_pngs=args.save_pngs,
 | 
			
		||||
        png_mode=("RGBA" if args.transparent else "RGB"),
 | 
			
		||||
        movie_file_extension=(get_file_ext(args)),
 | 
			
		||||
        output_directory=get_output_directory(args, config),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -61,9 +61,10 @@ RIGHT_SIDE: Vect3 = FRAME_X_RADIUS * RIGHT
 | 
			
		|||
# Angles
 | 
			
		||||
PI: float = np.pi
 | 
			
		||||
TAU: float = 2 * PI
 | 
			
		||||
DEGREES: float = TAU / 360
 | 
			
		||||
DEG: float = TAU / 360
 | 
			
		||||
DEGREES = DEG  # Many older animations use teh full name
 | 
			
		||||
# Nice to have a constant for readability
 | 
			
		||||
# when juxtaposed with expressions like 30 * DEGREES
 | 
			
		||||
# when juxtaposed with expressions like 30 * DEG
 | 
			
		||||
RADIANS: float = 1
 | 
			
		||||
 | 
			
		||||
# Related to Text
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -52,13 +52,6 @@ camera:
 | 
			
		|||
  fps: 30
 | 
			
		||||
  background_opacity: 1.0
 | 
			
		||||
file_writer:
 | 
			
		||||
  # If break_into_partial_movies is set to True, then many small
 | 
			
		||||
  # files will be written corresponding to each Scene.play and
 | 
			
		||||
  # Scene.wait call, and these files will then be combined
 | 
			
		||||
  # to form the full scene.  Sometimes video-editing is made
 | 
			
		||||
  # easier when working with the broken up scene, which
 | 
			
		||||
  # effectively has cuts at all the places you might want.
 | 
			
		||||
  break_into_partial_movies: False
 | 
			
		||||
  # What command to use for ffmpeg
 | 
			
		||||
  ffmpeg_bin: "ffmpeg"
 | 
			
		||||
  # Parameters to pass into ffmpeg
 | 
			
		||||
| 
						 | 
				
			
			@ -71,6 +64,9 @@ file_writer:
 | 
			
		|||
scene:
 | 
			
		||||
  show_animation_progress: False
 | 
			
		||||
  leave_progress_bars: False
 | 
			
		||||
  # When skipping animations, should a single frame be rendered
 | 
			
		||||
  # at the end of each play call?
 | 
			
		||||
  preview_while_skipping: True
 | 
			
		||||
  # How long does a scene pause on Scene.wait calls
 | 
			
		||||
  default_wait_time: 1.0
 | 
			
		||||
vmobject:
 | 
			
		||||
| 
						 | 
				
			
			@ -104,6 +100,20 @@ sizes:
 | 
			
		|||
  # Default buffers used in Mobject.next_to or Mobject.to_edge
 | 
			
		||||
  default_mobject_to_edge_buff: 0.5
 | 
			
		||||
  default_mobject_to_mobject_buff: 0.25
 | 
			
		||||
key_bindings:
 | 
			
		||||
  pan_3d: 'd'
 | 
			
		||||
  pan: 'f'
 | 
			
		||||
  reset: 'r'
 | 
			
		||||
  quit: 'q'  # Together with command
 | 
			
		||||
  select: 's'
 | 
			
		||||
  unselect: 'u'
 | 
			
		||||
  grab: 'g'
 | 
			
		||||
  x_grab: 'h'
 | 
			
		||||
  y_grab: 'v'
 | 
			
		||||
  resize: 't'
 | 
			
		||||
  color: 'c'
 | 
			
		||||
  information: 'i'
 | 
			
		||||
  cursor: 'k'
 | 
			
		||||
colors:
 | 
			
		||||
  blue_e: "#1C758A"
 | 
			
		||||
  blue_d: "#29ABCA"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,6 +15,7 @@ from typing import TYPE_CHECKING
 | 
			
		|||
if TYPE_CHECKING:
 | 
			
		||||
    Module = importlib.util.types.ModuleType
 | 
			
		||||
    from typing import Optional
 | 
			
		||||
    from addict import Dict
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BlankScene(InteractiveScene):
 | 
			
		||||
| 
						 | 
				
			
			@ -43,11 +44,7 @@ def prompt_user_for_choice(scene_classes):
 | 
			
		|||
        print(f"{str(idx).zfill(max_digits)}: {name}")
 | 
			
		||||
        name_to_class[name] = scene_class
 | 
			
		||||
    try:
 | 
			
		||||
        user_input = input(
 | 
			
		||||
            "\nThat module has multiple scenes, " + \
 | 
			
		||||
            "which ones would you like to render?" + \
 | 
			
		||||
            "\nScene Name or Number: "
 | 
			
		||||
        )
 | 
			
		||||
        user_input = input("\nSelect which scene to render (by name or number): ")
 | 
			
		||||
        return [
 | 
			
		||||
            name_to_class[split_str] if not split_str.isnumeric() else scene_classes[int(split_str) - 1]
 | 
			
		||||
            for split_str in user_input.replace(" ", "").split(",")
 | 
			
		||||
| 
						 | 
				
			
			@ -80,29 +77,29 @@ def compute_total_frames(scene_class, scene_config):
 | 
			
		|||
    return int(total_time * manim_config.camera.fps)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def scene_from_class(scene_class, scene_config, run_config):
 | 
			
		||||
def scene_from_class(scene_class, scene_config: Dict, run_config: Dict):
 | 
			
		||||
    fw_config = manim_config.file_writer
 | 
			
		||||
    if fw_config.write_to_movie and run_config.prerun:
 | 
			
		||||
        scene_config.file_writer_config.total_frames = compute_total_frames(scene_class, scene_config)
 | 
			
		||||
    return scene_class(**scene_config)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_scenes_to_render(all_scene_classes, scene_config, run_config):
 | 
			
		||||
    if run_config["write_all"]:
 | 
			
		||||
        return [sc(**scene_config) for sc in all_scene_classes]
 | 
			
		||||
def note_missing_scenes(arg_names, module_names):
 | 
			
		||||
    for name in arg_names:
 | 
			
		||||
        if name not in module_names:
 | 
			
		||||
            log.error(f"No scene named {name} found")
 | 
			
		||||
 | 
			
		||||
    names_to_classes = {sc.__name__: sc for sc in all_scene_classes}
 | 
			
		||||
    scene_names = run_config["scene_names"]
 | 
			
		||||
 | 
			
		||||
    for name in set.difference(set(scene_names), names_to_classes):
 | 
			
		||||
        log.error(f"No scene named {name} found")
 | 
			
		||||
        scene_names.remove(name)
 | 
			
		||||
 | 
			
		||||
    if scene_names:
 | 
			
		||||
        classes_to_run = [names_to_classes[name] for name in scene_names]
 | 
			
		||||
    elif len(all_scene_classes) == 1:
 | 
			
		||||
        classes_to_run = [all_scene_classes[0]]
 | 
			
		||||
def get_scenes_to_render(all_scene_classes: list, scene_config: Dict, run_config: Dict):
 | 
			
		||||
    if run_config["write_all"] or len(all_scene_classes) == 1:
 | 
			
		||||
        classes_to_run = all_scene_classes
 | 
			
		||||
    else:
 | 
			
		||||
        name_to_class = {sc.__name__: sc for sc in all_scene_classes}
 | 
			
		||||
        classes_to_run = [name_to_class.get(name) for name in run_config.scene_names]
 | 
			
		||||
        classes_to_run = list(filter(lambda x: x, classes_to_run))  # Remove Nones
 | 
			
		||||
        note_missing_scenes(run_config.scene_names, name_to_class.keys())
 | 
			
		||||
 | 
			
		||||
    if len(classes_to_run) == 0:
 | 
			
		||||
        classes_to_run = prompt_user_for_choice(all_scene_classes)
 | 
			
		||||
 | 
			
		||||
    return [
 | 
			
		||||
| 
						 | 
				
			
			@ -111,7 +108,10 @@ def get_scenes_to_render(all_scene_classes, scene_config, run_config):
 | 
			
		|||
    ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_scene_classes_from_module(module):
 | 
			
		||||
def get_scene_classes(module: Optional[Module]):
 | 
			
		||||
    if module is None:
 | 
			
		||||
        # If no module was passed in, just play the blank scene
 | 
			
		||||
        return [BlankScene(**scene_config)]
 | 
			
		||||
    if hasattr(module, "SCENES_IN_ORDER"):
 | 
			
		||||
        return module.SCENES_IN_ORDER
 | 
			
		||||
    else:
 | 
			
		||||
| 
						 | 
				
			
			@ -162,24 +162,16 @@ def insert_embed_line_to_module(module: Module, line_number: int):
 | 
			
		|||
    exec(code_object, module.__dict__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_scene_module(file_name: Optional[str], embed_line: Optional[int], is_reload: bool = False) -> Module:
 | 
			
		||||
def get_module(file_name: Optional[str], embed_line: Optional[int], is_reload: bool = False) -> Module:
 | 
			
		||||
    module = ModuleLoader.get_module(file_name, is_reload)
 | 
			
		||||
    if embed_line:
 | 
			
		||||
        insert_embed_line_to_module(module, embed_line)
 | 
			
		||||
    return module
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def main(scene_config, run_config):
 | 
			
		||||
    module = get_scene_module(
 | 
			
		||||
        run_config["file_name"],
 | 
			
		||||
        run_config["embed_line"],
 | 
			
		||||
        run_config["is_reload"]
 | 
			
		||||
    )
 | 
			
		||||
    if module is None:
 | 
			
		||||
        # If no module was passed in, just play the blank scene
 | 
			
		||||
        return [BlankScene(**scene_config)]
 | 
			
		||||
 | 
			
		||||
    all_scene_classes = get_scene_classes_from_module(module)
 | 
			
		||||
def main(scene_config: Dict, run_config: Dict):
 | 
			
		||||
    module = get_module(run_config.file_name, run_config.embed_line, run_config.is_reload)
 | 
			
		||||
    all_scene_classes = get_scene_classes(module)
 | 
			
		||||
    scenes = get_scenes_to_render(all_scene_classes, scene_config, run_config)
 | 
			
		||||
    if len(scenes) == 0:
 | 
			
		||||
        print("No scenes found to run")
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,7 +7,7 @@ import numpy as np
 | 
			
		|||
import itertools as it
 | 
			
		||||
 | 
			
		||||
from manimlib.constants import BLACK, BLUE, BLUE_D, BLUE_E, GREEN, GREY_A, WHITE, RED
 | 
			
		||||
from manimlib.constants import DEGREES, PI
 | 
			
		||||
from manimlib.constants import DEG, PI
 | 
			
		||||
from manimlib.constants import DL, UL, DOWN, DR, LEFT, ORIGIN, OUT, RIGHT, UP
 | 
			
		||||
from manimlib.constants import FRAME_X_RADIUS, FRAME_Y_RADIUS
 | 
			
		||||
from manimlib.constants import MED_SMALL_BUFF, SMALL_BUFF
 | 
			
		||||
| 
						 | 
				
			
			@ -307,7 +307,7 @@ class CoordinateSystem(ABC):
 | 
			
		|||
 | 
			
		||||
        point = self.input_to_graph_point(x, graph)
 | 
			
		||||
        angle = self.angle_of_tangent(x, graph)
 | 
			
		||||
        normal = rotate_vector(RIGHT, angle + 90 * DEGREES)
 | 
			
		||||
        normal = rotate_vector(RIGHT, angle + 90 * DEG)
 | 
			
		||||
        if normal[1] < 0:
 | 
			
		||||
            normal *= -1
 | 
			
		||||
        label.next_to(point, normal, buff=buff)
 | 
			
		||||
| 
						 | 
				
			
			@ -474,7 +474,7 @@ class Axes(VGroup, CoordinateSystem):
 | 
			
		|||
            ),
 | 
			
		||||
            length=height,
 | 
			
		||||
        )
 | 
			
		||||
        self.y_axis.rotate(90 * DEGREES, about_point=ORIGIN)
 | 
			
		||||
        self.y_axis.rotate(90 * DEG, about_point=ORIGIN)
 | 
			
		||||
        # Add as a separate group in case various other
 | 
			
		||||
        # mobjects are added to self, as for example in
 | 
			
		||||
        # NumberPlane below
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,7 +7,7 @@ import numpy as np
 | 
			
		|||
from manimlib.constants import DL, DOWN, DR, LEFT, ORIGIN, OUT, RIGHT, UL, UP, UR
 | 
			
		||||
from manimlib.constants import GREY_A, RED, WHITE, BLACK
 | 
			
		||||
from manimlib.constants import MED_SMALL_BUFF, SMALL_BUFF
 | 
			
		||||
from manimlib.constants import DEGREES, PI, TAU
 | 
			
		||||
from manimlib.constants import DEG, PI, TAU
 | 
			
		||||
from manimlib.mobject.mobject import Mobject
 | 
			
		||||
from manimlib.mobject.types.vectorized_mobject import DashedVMobject
 | 
			
		||||
from manimlib.mobject.types.vectorized_mobject import VGroup
 | 
			
		||||
| 
						 | 
				
			
			@ -983,7 +983,7 @@ class RegularPolygon(Polygon):
 | 
			
		|||
    ):
 | 
			
		||||
        # Defaults to 0 for odd, 90 for even
 | 
			
		||||
        if start_angle is None:
 | 
			
		||||
            start_angle = (n % 2) * 90 * DEGREES
 | 
			
		||||
            start_angle = (n % 2) * 90 * DEG
 | 
			
		||||
        start_vect = rotate_vector(radius * RIGHT, start_angle)
 | 
			
		||||
        vertices = compass_directions(n, start_vect)
 | 
			
		||||
        super().__init__(*vertices, **kwargs)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,7 +3,7 @@ from __future__ import annotations
 | 
			
		|||
import numpy as np
 | 
			
		||||
 | 
			
		||||
from manimlib.constants import DOWN, LEFT, RIGHT, ORIGIN
 | 
			
		||||
from manimlib.constants import DEGREES
 | 
			
		||||
from manimlib.constants import DEG
 | 
			
		||||
from manimlib.mobject.numbers import DecimalNumber
 | 
			
		||||
from manimlib.mobject.svg.tex_mobject import Tex
 | 
			
		||||
from manimlib.mobject.types.vectorized_mobject import VGroup
 | 
			
		||||
| 
						 | 
				
			
			@ -196,7 +196,7 @@ class Matrix(VMobject):
 | 
			
		|||
                dots.set_width(hdots_width)
 | 
			
		||||
                self.swap_entry_for_dots(row[col_index], dots)
 | 
			
		||||
        if use_vdots and use_hdots:
 | 
			
		||||
            rows[row_index][col_index].rotate(-45 * DEGREES)
 | 
			
		||||
            rows[row_index][col_index].rotate(-45 * DEG)
 | 
			
		||||
        return self
 | 
			
		||||
 | 
			
		||||
    def get_mob_matrix(self) -> VMobjectMatrixType:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,7 @@ from __future__ import annotations
 | 
			
		|||
 | 
			
		||||
import inspect
 | 
			
		||||
 | 
			
		||||
from manimlib.constants import DEGREES
 | 
			
		||||
from manimlib.constants import DEG
 | 
			
		||||
from manimlib.constants import RIGHT
 | 
			
		||||
from manimlib.mobject.mobject import Mobject
 | 
			
		||||
from manimlib.utils.simple_functions import clip
 | 
			
		||||
| 
						 | 
				
			
			@ -71,7 +71,7 @@ def always_shift(
 | 
			
		|||
 | 
			
		||||
def always_rotate(
 | 
			
		||||
    mobject: Mobject,
 | 
			
		||||
    rate: float = 20 * DEGREES,
 | 
			
		||||
    rate: float = 20 * DEG,
 | 
			
		||||
    **kwargs
 | 
			
		||||
) -> Mobject:
 | 
			
		||||
    mobject.add_updater(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,6 +20,7 @@ from manimlib.mobject.svg.string_mobject import StringMobject
 | 
			
		|||
from manimlib.utils.cache import cache_on_disk
 | 
			
		||||
from manimlib.utils.color import color_to_hex
 | 
			
		||||
from manimlib.utils.color import int_to_hex
 | 
			
		||||
from manimlib.utils.simple_functions import hash_string
 | 
			
		||||
 | 
			
		||||
from typing import TYPE_CHECKING
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -74,34 +75,29 @@ def markup_to_svg(
 | 
			
		|||
        pango_width = line_width / FRAME_WIDTH * DEFAULT_PIXEL_WIDTH
 | 
			
		||||
 | 
			
		||||
    # Write the result to a temporary svg file, and return it's contents.
 | 
			
		||||
    # TODO, better would be to have this not write to file at all
 | 
			
		||||
    #
 | 
			
		||||
    # To avoid CAIRO_STATUS_WRITE_ERROR: b'error while writing to
 | 
			
		||||
    # output stream' on Windows, we need to pass 'delete=False'.
 | 
			
		||||
    with tempfile.NamedTemporaryFile(suffix='.svg', mode='r+', delete=False) as tmp:
 | 
			
		||||
        manimpango.MarkupUtils.text2svg(
 | 
			
		||||
            text=markup_str,
 | 
			
		||||
            font="",                     # Already handled
 | 
			
		||||
            slant="NORMAL",              # Already handled
 | 
			
		||||
            weight="NORMAL",             # Already handled
 | 
			
		||||
            size=1,                      # Already handled
 | 
			
		||||
            _=0,                         # Empty parameter
 | 
			
		||||
            disable_liga=False,
 | 
			
		||||
            file_name=tmp.name,
 | 
			
		||||
            START_X=0,
 | 
			
		||||
            START_Y=0,
 | 
			
		||||
            width=DEFAULT_CANVAS_WIDTH,
 | 
			
		||||
            height=DEFAULT_CANVAS_HEIGHT,
 | 
			
		||||
            justify=justify,
 | 
			
		||||
            indent=indent,
 | 
			
		||||
            line_spacing=None,           # Already handled
 | 
			
		||||
            alignment=alignment,
 | 
			
		||||
            pango_width=pango_width
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # Read the contents
 | 
			
		||||
        tmp.seek(0)
 | 
			
		||||
        return tmp.read()
 | 
			
		||||
    temp_file = Path(tempfile.gettempdir(), hash_string(markup_str)).with_suffix(".svg")
 | 
			
		||||
    manimpango.MarkupUtils.text2svg(
 | 
			
		||||
        text=markup_str,
 | 
			
		||||
        font="",                     # Already handled
 | 
			
		||||
        slant="NORMAL",              # Already handled
 | 
			
		||||
        weight="NORMAL",             # Already handled
 | 
			
		||||
        size=1,                      # Already handled
 | 
			
		||||
        _=0,                         # Empty parameter
 | 
			
		||||
        disable_liga=False,
 | 
			
		||||
        file_name=str(temp_file),
 | 
			
		||||
        START_X=0,
 | 
			
		||||
        START_Y=0,
 | 
			
		||||
        width=DEFAULT_CANVAS_WIDTH,
 | 
			
		||||
        height=DEFAULT_CANVAS_HEIGHT,
 | 
			
		||||
        justify=justify,
 | 
			
		||||
        indent=indent,
 | 
			
		||||
        line_spacing=None,           # Already handled
 | 
			
		||||
        alignment=alignment,
 | 
			
		||||
        pango_width=pango_width
 | 
			
		||||
    )
 | 
			
		||||
    result = temp_file.read_text()
 | 
			
		||||
    os.remove(temp_file)
 | 
			
		||||
    return result
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class MarkupText(StringMobject):
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,7 +7,7 @@ import numpy as np
 | 
			
		|||
from manimlib.constants import GREY_A, GREY_C, GREY_E
 | 
			
		||||
from manimlib.constants import BLACK
 | 
			
		||||
from manimlib.constants import DEFAULT_STROKE_WIDTH
 | 
			
		||||
from manimlib.constants import DEGREES
 | 
			
		||||
from manimlib.constants import DEG
 | 
			
		||||
from manimlib.constants import ORIGIN, OUT
 | 
			
		||||
from manimlib.constants import PI
 | 
			
		||||
from manimlib.constants import TAU
 | 
			
		||||
| 
						 | 
				
			
			@ -490,7 +490,7 @@ class VMobject(Mobject):
 | 
			
		|||
        v1 = handle1 - last
 | 
			
		||||
        v2 = anchor - handle2
 | 
			
		||||
        angle = angle_between_vectors(v1, v2)
 | 
			
		||||
        if self.use_simple_quadratic_approx and angle < 45 * DEGREES:
 | 
			
		||||
        if self.use_simple_quadratic_approx and angle < 45 * DEG:
 | 
			
		||||
            quad_approx = [last, find_intersection(last, v1, anchor, -v2), anchor]
 | 
			
		||||
        else:
 | 
			
		||||
            quad_approx = get_quadratic_approximation_of_cubic(
 | 
			
		||||
| 
						 | 
				
			
			@ -616,7 +616,7 @@ class VMobject(Mobject):
 | 
			
		|||
 | 
			
		||||
    def subdivide_sharp_curves(
 | 
			
		||||
        self,
 | 
			
		||||
        angle_threshold: float = 30 * DEGREES,
 | 
			
		||||
        angle_threshold: float = 30 * DEG,
 | 
			
		||||
        recurse: bool = True
 | 
			
		||||
    ) -> Self:
 | 
			
		||||
        def tuple_to_subdivisions(b0, b1, b2):
 | 
			
		||||
| 
						 | 
				
			
			@ -656,7 +656,7 @@ class VMobject(Mobject):
 | 
			
		|||
        self.make_smooth(approx=approx)
 | 
			
		||||
        return self
 | 
			
		||||
 | 
			
		||||
    def is_smooth(self, angle_tol=1 * DEGREES) -> bool:
 | 
			
		||||
    def is_smooth(self, angle_tol=1 * DEG) -> bool:
 | 
			
		||||
        angles = np.abs(self.get_joint_angles()[0::2])
 | 
			
		||||
        return (angles < angle_tol).all()
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,10 +7,11 @@ from IPython.core.getipython import get_ipython
 | 
			
		|||
from pyglet.window import key as PygletWindowKeys
 | 
			
		||||
 | 
			
		||||
from manimlib.animation.fading import FadeIn
 | 
			
		||||
from manimlib.config import manim_config
 | 
			
		||||
from manimlib.constants import DL, DOWN, DR, LEFT, ORIGIN, RIGHT, UL, UP, UR
 | 
			
		||||
from manimlib.constants import FRAME_WIDTH, FRAME_HEIGHT, SMALL_BUFF
 | 
			
		||||
from manimlib.constants import PI
 | 
			
		||||
from manimlib.constants import DEGREES
 | 
			
		||||
from manimlib.constants import DEG
 | 
			
		||||
from manimlib.constants import MANIM_COLORS, WHITE, GREY_A, GREY_C
 | 
			
		||||
from manimlib.mobject.geometry import Line
 | 
			
		||||
from manimlib.mobject.geometry import Rectangle
 | 
			
		||||
| 
						 | 
				
			
			@ -36,17 +37,16 @@ if TYPE_CHECKING:
 | 
			
		|||
    from manimlib.typing import Vect3
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
SELECT_KEY = 's'
 | 
			
		||||
UNSELECT_KEY = 'u'
 | 
			
		||||
GRAB_KEY = 'g'
 | 
			
		||||
X_GRAB_KEY = 'h'
 | 
			
		||||
Y_GRAB_KEY = 'v'
 | 
			
		||||
SELECT_KEY = manim_config.key_bindings.select
 | 
			
		||||
UNSELECT_KEY = manim_config.key_bindings.unselect
 | 
			
		||||
GRAB_KEY = manim_config.key_bindings.grab
 | 
			
		||||
X_GRAB_KEY = manim_config.key_bindings.x_grab
 | 
			
		||||
Y_GRAB_KEY = manim_config.key_bindings.y_grab
 | 
			
		||||
GRAB_KEYS = [GRAB_KEY, X_GRAB_KEY, Y_GRAB_KEY]
 | 
			
		||||
RESIZE_KEY = 't'
 | 
			
		||||
COLOR_KEY = 'c'
 | 
			
		||||
INFORMATION_KEY = 'i'
 | 
			
		||||
CURSOR_KEY = 'k'
 | 
			
		||||
COPY_FRAME_POSITION_KEY = 'p'
 | 
			
		||||
RESIZE_KEY = manim_config.key_bindings.resize  # TODO
 | 
			
		||||
COLOR_KEY = manim_config.key_bindings.color
 | 
			
		||||
INFORMATION_KEY = manim_config.key_bindings.information
 | 
			
		||||
CURSOR_KEY = manim_config.key_bindings.cursor
 | 
			
		||||
 | 
			
		||||
# For keyboard interactions
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -625,7 +625,7 @@ class InteractiveScene(Scene):
 | 
			
		|||
        angles = frame.get_euler_angles()
 | 
			
		||||
 | 
			
		||||
        call = f"reorient("
 | 
			
		||||
        theta, phi, gamma = (angles / DEGREES).astype(int)
 | 
			
		||||
        theta, phi, gamma = (angles / DEG).astype(int)
 | 
			
		||||
        call += f"{theta}, {phi}, {gamma}"
 | 
			
		||||
        if any(center != 0):
 | 
			
		||||
            call += f", {tuple(np.round(center, 2))}"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -48,12 +48,6 @@ if TYPE_CHECKING:
 | 
			
		|||
    from manimlib.animation.animation import Animation
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
PAN_3D_KEY = 'd'
 | 
			
		||||
FRAME_SHIFT_KEY = 'f'
 | 
			
		||||
RESET_FRAME_KEY = 'r'
 | 
			
		||||
QUIT_KEY = 'q'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Scene(object):
 | 
			
		||||
    random_seed: int = 0
 | 
			
		||||
    pan_sensitivity: float = 0.5
 | 
			
		||||
| 
						 | 
				
			
			@ -77,6 +71,7 @@ class Scene(object):
 | 
			
		|||
        end_at_animation_number: int | None = None,
 | 
			
		||||
        show_animation_progress: bool = False,
 | 
			
		||||
        leave_progress_bars: bool = False,
 | 
			
		||||
        preview_while_skipping: bool = True,
 | 
			
		||||
        presenter_mode: bool = False,
 | 
			
		||||
        default_wait_time: float = 1.0,
 | 
			
		||||
    ):
 | 
			
		||||
| 
						 | 
				
			
			@ -86,6 +81,7 @@ class Scene(object):
 | 
			
		|||
        self.end_at_animation_number = end_at_animation_number
 | 
			
		||||
        self.show_animation_progress = show_animation_progress
 | 
			
		||||
        self.leave_progress_bars = leave_progress_bars
 | 
			
		||||
        self.preview_while_skipping = preview_while_skipping
 | 
			
		||||
        self.presenter_mode = presenter_mode
 | 
			
		||||
        self.default_wait_time = default_wait_time
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -531,7 +527,7 @@ class Scene(object):
 | 
			
		|||
        if not self.skip_animations:
 | 
			
		||||
            self.file_writer.end_animation()
 | 
			
		||||
 | 
			
		||||
        if self.skip_animations and self.window is not None:
 | 
			
		||||
        if self.preview_while_skipping and self.skip_animations and self.window is not None:
 | 
			
		||||
            # Show some quick frames along the way
 | 
			
		||||
            self.update_frame(dt=0, force_draw=True)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -746,13 +742,13 @@ class Scene(object):
 | 
			
		|||
 | 
			
		||||
        frame = self.camera.frame
 | 
			
		||||
        # Handle perspective changes
 | 
			
		||||
        if self.window.is_key_pressed(ord(PAN_3D_KEY)):
 | 
			
		||||
        if self.window.is_key_pressed(ord(manim_config.key_bindings.pan_3d)):
 | 
			
		||||
            ff_d_point = frame.to_fixed_frame_point(d_point, relative=True)
 | 
			
		||||
            ff_d_point *= self.pan_sensitivity
 | 
			
		||||
            frame.increment_theta(-ff_d_point[0])
 | 
			
		||||
            frame.increment_phi(ff_d_point[1])
 | 
			
		||||
        # Handle frame movements
 | 
			
		||||
        elif self.window.is_key_pressed(ord(FRAME_SHIFT_KEY)):
 | 
			
		||||
        elif self.window.is_key_pressed(ord(manim_config.key_bindings.pan)):
 | 
			
		||||
            frame.shift(-d_point)
 | 
			
		||||
 | 
			
		||||
    def on_mouse_drag(
 | 
			
		||||
| 
						 | 
				
			
			@ -838,14 +834,14 @@ class Scene(object):
 | 
			
		|||
        if propagate_event is not None and propagate_event is False:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        if char == RESET_FRAME_KEY:
 | 
			
		||||
        if char == manim_config.key_bindings.reset:
 | 
			
		||||
            self.play(self.camera.frame.animate.to_default_state())
 | 
			
		||||
        elif char == "z" and (modifiers & (PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_CTRL)):
 | 
			
		||||
            self.undo()
 | 
			
		||||
        elif char == "z" and (modifiers & (PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_CTRL | PygletWindowKeys.MOD_SHIFT)):
 | 
			
		||||
            self.redo()
 | 
			
		||||
        # command + q
 | 
			
		||||
        elif char == QUIT_KEY and (modifiers & (PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_CTRL)):
 | 
			
		||||
        elif char == manim_config.key_bindings.quit and (modifiers & (PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_CTRL)):
 | 
			
		||||
            self.quit_interaction = True
 | 
			
		||||
        # Space or right arrow
 | 
			
		||||
        elif char == " " or symbol == PygletWindowKeys.RIGHT:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -102,7 +102,7 @@ class InteractiveSceneEmbed:
 | 
			
		|||
        """Flash border, and potentially play sound, on exceptions"""
 | 
			
		||||
        def custom_exc(shell, etype, evalue, tb, tb_offset=None):
 | 
			
		||||
            # Show the error don't just swallow it
 | 
			
		||||
            print(''.join(traceback.format_exception(etype, evalue, tb)))
 | 
			
		||||
            shell.showtraceback((etype, evalue, tb), tb_offset=tb_offset)
 | 
			
		||||
            rect = FullScreenRectangle().set_stroke(RED, 30).set_fill(opacity=0)
 | 
			
		||||
            rect.fix_in_frame()
 | 
			
		||||
            self.scene.play(VFadeInThenOut(rect, run_time=0.5))
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,8 +13,6 @@ from pathlib import Path
 | 
			
		|||
 | 
			
		||||
from manimlib.logger import log
 | 
			
		||||
from manimlib.mobject.mobject import Mobject
 | 
			
		||||
from manimlib.utils.file_ops import add_extension_if_not_present
 | 
			
		||||
from manimlib.utils.file_ops import get_sorted_integer_files
 | 
			
		||||
from manimlib.utils.file_ops import guarantee_existence
 | 
			
		||||
from manimlib.utils.sounds import get_full_sound_file_path
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -32,17 +30,15 @@ class SceneFileWriter(object):
 | 
			
		|||
        self,
 | 
			
		||||
        scene: Scene,
 | 
			
		||||
        write_to_movie: bool = False,
 | 
			
		||||
        break_into_partial_movies: bool = False,
 | 
			
		||||
        save_pngs: bool = False,  # TODO, this currently does nothing
 | 
			
		||||
        subdivide_output: bool = False,
 | 
			
		||||
        png_mode: str = "RGBA",
 | 
			
		||||
        save_last_frame: bool = False,
 | 
			
		||||
        movie_file_extension: str = ".mp4",
 | 
			
		||||
        # What python file is generating this scene
 | 
			
		||||
        input_file_path: str = "",
 | 
			
		||||
        # Where should this be written
 | 
			
		||||
        output_directory: str | None = None,
 | 
			
		||||
        output_directory: str = "",
 | 
			
		||||
        file_name: str | None = None,
 | 
			
		||||
        subdirectory_for_videos: bool = False,
 | 
			
		||||
        open_file_upon_completion: bool = False,
 | 
			
		||||
        show_file_location_upon_completion: bool = False,
 | 
			
		||||
        quiet: bool = False,
 | 
			
		||||
| 
						 | 
				
			
			@ -57,8 +53,7 @@ class SceneFileWriter(object):
 | 
			
		|||
    ):
 | 
			
		||||
        self.scene: Scene = scene
 | 
			
		||||
        self.write_to_movie = write_to_movie
 | 
			
		||||
        self.break_into_partial_movies = break_into_partial_movies
 | 
			
		||||
        self.save_pngs = save_pngs
 | 
			
		||||
        self.subdivide_output = subdivide_output
 | 
			
		||||
        self.png_mode = png_mode
 | 
			
		||||
        self.save_last_frame = save_last_frame
 | 
			
		||||
        self.movie_file_extension = movie_file_extension
 | 
			
		||||
| 
						 | 
				
			
			@ -66,7 +61,6 @@ class SceneFileWriter(object):
 | 
			
		|||
        self.output_directory = output_directory
 | 
			
		||||
        self.file_name = file_name
 | 
			
		||||
        self.open_file_upon_completion = open_file_upon_completion
 | 
			
		||||
        self.subdirectory_for_videos = subdirectory_for_videos
 | 
			
		||||
        self.show_file_location_upon_completion = show_file_location_upon_completion
 | 
			
		||||
        self.quiet = quiet
 | 
			
		||||
        self.total_frames = total_frames
 | 
			
		||||
| 
						 | 
				
			
			@ -81,40 +75,39 @@ class SceneFileWriter(object):
 | 
			
		|||
        self.writing_process: sp.Popen | None = None
 | 
			
		||||
        self.progress_display: ProgressDisplay | None = None
 | 
			
		||||
        self.ended_with_interrupt: bool = False
 | 
			
		||||
 | 
			
		||||
        self.init_output_directories()
 | 
			
		||||
        self.init_audio()
 | 
			
		||||
 | 
			
		||||
    # Output directories and files
 | 
			
		||||
    def init_output_directories(self) -> None:
 | 
			
		||||
        out_dir = self.output_directory or ""
 | 
			
		||||
        scene_name = self.file_name or self.get_default_scene_name()
 | 
			
		||||
        if self.save_last_frame:
 | 
			
		||||
            image_dir = guarantee_existence(os.path.join(out_dir, "images"))
 | 
			
		||||
            image_file = add_extension_if_not_present(scene_name, ".png")
 | 
			
		||||
            self.image_file_path = os.path.join(image_dir, image_file)
 | 
			
		||||
            self.image_file_path = self.init_image_file_path()
 | 
			
		||||
        if self.write_to_movie:
 | 
			
		||||
            if self.subdirectory_for_videos:
 | 
			
		||||
                movie_dir = guarantee_existence(os.path.join(out_dir, "videos"))
 | 
			
		||||
            else:
 | 
			
		||||
                movie_dir = guarantee_existence(out_dir)
 | 
			
		||||
            movie_file = add_extension_if_not_present(scene_name, self.movie_file_extension)
 | 
			
		||||
            self.movie_file_path = os.path.join(movie_dir, movie_file)
 | 
			
		||||
            if self.break_into_partial_movies:
 | 
			
		||||
                self.partial_movie_directory = guarantee_existence(os.path.join(
 | 
			
		||||
                    movie_dir, "partial_movie_files", scene_name,
 | 
			
		||||
                ))
 | 
			
		||||
        # A place to save mobjects
 | 
			
		||||
        self.saved_mobject_directory = os.path.join(
 | 
			
		||||
            out_dir, "mobjects", str(self.scene)
 | 
			
		||||
            self.movie_file_path = self.init_movie_file_path()
 | 
			
		||||
        if self.subdivide_output:
 | 
			
		||||
            self.partial_movie_directory = self.init_partial_movie_directory()
 | 
			
		||||
 | 
			
		||||
    def init_image_file_path(self) -> Path:
 | 
			
		||||
        return self.get_output_file_rootname().with_suffix(".png")
 | 
			
		||||
 | 
			
		||||
    def init_movie_file_path(self) -> Path:
 | 
			
		||||
        return self.get_output_file_rootname().with_suffix(self.movie_file_extension)
 | 
			
		||||
 | 
			
		||||
    def init_partial_movie_directory(self):
 | 
			
		||||
        return guarantee_existence(self.get_output_file_rootname())
 | 
			
		||||
 | 
			
		||||
    def get_output_file_rootname(self) -> Path:
 | 
			
		||||
        return Path(
 | 
			
		||||
            guarantee_existence(self.output_directory),
 | 
			
		||||
            self.get_output_file_name()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def get_default_module_directory(self) -> str:
 | 
			
		||||
        path, _ = os.path.splitext(self.input_file_path)
 | 
			
		||||
        if path.startswith("_"):
 | 
			
		||||
            path = path[1:]
 | 
			
		||||
        return path
 | 
			
		||||
 | 
			
		||||
    def get_default_scene_name(self) -> str:
 | 
			
		||||
    def get_output_file_name(self) -> str:
 | 
			
		||||
        if self.file_name:
 | 
			
		||||
            return self.file_name
 | 
			
		||||
        # Otherwise, use the name of the scene, potentially
 | 
			
		||||
        # appending animation numbers
 | 
			
		||||
        name = str(self.scene)
 | 
			
		||||
        saan = self.scene.start_at_animation_number
 | 
			
		||||
        eaan = self.scene.end_at_animation_number
 | 
			
		||||
| 
						 | 
				
			
			@ -124,26 +117,13 @@ class SceneFileWriter(object):
 | 
			
		|||
            name += f"_{eaan}"
 | 
			
		||||
        return name
 | 
			
		||||
 | 
			
		||||
    def get_resolution_directory(self) -> str:
 | 
			
		||||
        pixel_height = self.scene.camera.pixel_height
 | 
			
		||||
        fps = self.scene.camera.fps
 | 
			
		||||
        return "{}p{}".format(
 | 
			
		||||
            pixel_height, fps
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    # Directory getters
 | 
			
		||||
    def get_image_file_path(self) -> str:
 | 
			
		||||
        return self.image_file_path
 | 
			
		||||
 | 
			
		||||
    def get_next_partial_movie_path(self) -> str:
 | 
			
		||||
        result = os.path.join(
 | 
			
		||||
            self.partial_movie_directory,
 | 
			
		||||
            "{:05}{}".format(
 | 
			
		||||
                self.scene.num_plays,
 | 
			
		||||
                self.movie_file_extension,
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
        return result
 | 
			
		||||
        result = Path(self.partial_movie_directory, f"{self.scene.num_plays:05}")
 | 
			
		||||
        return result.with_suffix(self.movie_file_extension)
 | 
			
		||||
 | 
			
		||||
    def get_movie_file_path(self) -> str:
 | 
			
		||||
        return self.movie_file_path
 | 
			
		||||
| 
						 | 
				
			
			@ -199,23 +179,20 @@ class SceneFileWriter(object):
 | 
			
		|||
 | 
			
		||||
    # Writers
 | 
			
		||||
    def begin(self) -> None:
 | 
			
		||||
        if not self.break_into_partial_movies and self.write_to_movie:
 | 
			
		||||
        if not self.subdivide_output and self.write_to_movie:
 | 
			
		||||
            self.open_movie_pipe(self.get_movie_file_path())
 | 
			
		||||
 | 
			
		||||
    def begin_animation(self) -> None:
 | 
			
		||||
        if self.break_into_partial_movies and self.write_to_movie:
 | 
			
		||||
        if self.subdivide_output and self.write_to_movie:
 | 
			
		||||
            self.open_movie_pipe(self.get_next_partial_movie_path())
 | 
			
		||||
 | 
			
		||||
    def end_animation(self) -> None:
 | 
			
		||||
        if self.break_into_partial_movies and self.write_to_movie:
 | 
			
		||||
        if self.subdivide_output and self.write_to_movie:
 | 
			
		||||
            self.close_movie_pipe()
 | 
			
		||||
 | 
			
		||||
    def finish(self) -> None:
 | 
			
		||||
        if self.write_to_movie:
 | 
			
		||||
            if self.break_into_partial_movies:
 | 
			
		||||
                self.combine_movie_files()
 | 
			
		||||
            else:
 | 
			
		||||
                self.close_movie_pipe()
 | 
			
		||||
        if not self.subdivide_output and self.write_to_movie:
 | 
			
		||||
            self.close_movie_pipe()
 | 
			
		||||
            if self.includes_sound:
 | 
			
		||||
                self.add_sound_to_video()
 | 
			
		||||
            self.print_file_ready_message(self.get_movie_file_path())
 | 
			
		||||
| 
						 | 
				
			
			@ -234,7 +211,6 @@ class SceneFileWriter(object):
 | 
			
		|||
        width, height = self.scene.camera.get_pixel_shape()
 | 
			
		||||
 | 
			
		||||
        vf_arg = 'vflip'
 | 
			
		||||
        # if self.pixel_format.startswith("yuv"):
 | 
			
		||||
        vf_arg += f',eq=saturation={self.saturation}:gamma={self.gamma}'
 | 
			
		||||
 | 
			
		||||
        command = [
 | 
			
		||||
| 
						 | 
				
			
			@ -246,7 +222,7 @@ class SceneFileWriter(object):
 | 
			
		|||
            '-r', str(fps),  # frames per second
 | 
			
		||||
            '-i', '-',  # The input comes from a pipe
 | 
			
		||||
            '-vf', vf_arg,
 | 
			
		||||
            '-an',  # Tells FFMPEG not to expect any audio
 | 
			
		||||
            '-an',  # Tells ffmpeg not to expect any audio
 | 
			
		||||
            '-loglevel', 'error',
 | 
			
		||||
        ]
 | 
			
		||||
        if self.video_codec:
 | 
			
		||||
| 
						 | 
				
			
			@ -273,8 +249,8 @@ class SceneFileWriter(object):
 | 
			
		|||
        movie_path = Path(self.get_movie_file_path())
 | 
			
		||||
        scene_name = movie_path.stem
 | 
			
		||||
        insert_dir = Path(movie_path.parent, "inserts")
 | 
			
		||||
        guarantee_existence(str(insert_dir))
 | 
			
		||||
        return Path(insert_dir, f"{scene_name}_{index}{movie_path.suffix}")
 | 
			
		||||
        guarantee_existence(insert_dir)
 | 
			
		||||
        return Path(insert_dir, f"{scene_name}_{index}").with_suffix(self.movie_file_extension)
 | 
			
		||||
 | 
			
		||||
    def begin_insert(self):
 | 
			
		||||
        # Begin writing process
 | 
			
		||||
| 
						 | 
				
			
			@ -283,7 +259,7 @@ class SceneFileWriter(object):
 | 
			
		|||
        index = 0
 | 
			
		||||
        while (insert_path := self.get_insert_file_path(index)).exists():
 | 
			
		||||
            index += 1
 | 
			
		||||
        self.inserted_file_path = str(insert_path)
 | 
			
		||||
        self.inserted_file_path = insert_path
 | 
			
		||||
        self.open_movie_pipe(self.inserted_file_path)
 | 
			
		||||
 | 
			
		||||
    def end_insert(self):
 | 
			
		||||
| 
						 | 
				
			
			@ -327,54 +303,6 @@ class SceneFileWriter(object):
 | 
			
		|||
        else:
 | 
			
		||||
            self.movie_file_path = self.temp_file_path
 | 
			
		||||
 | 
			
		||||
    def combine_movie_files(self) -> None:
 | 
			
		||||
        kwargs = {
 | 
			
		||||
            "remove_non_integer_files": True,
 | 
			
		||||
            "extension": self.movie_file_extension,
 | 
			
		||||
        }
 | 
			
		||||
        if self.scene.start_at_animation_number is not None:
 | 
			
		||||
            kwargs["min_index"] = self.scene.start_at_animation_number
 | 
			
		||||
        if self.scene.end_at_animation_number is not None:
 | 
			
		||||
            kwargs["max_index"] = self.scene.end_at_animation_number
 | 
			
		||||
        else:
 | 
			
		||||
            kwargs["remove_indices_greater_than"] = self.scene.num_plays - 1
 | 
			
		||||
        partial_movie_files = get_sorted_integer_files(
 | 
			
		||||
            self.partial_movie_directory,
 | 
			
		||||
            **kwargs
 | 
			
		||||
        )
 | 
			
		||||
        if len(partial_movie_files) == 0:
 | 
			
		||||
            log.warning("No animations in this scene")
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        # Write a file partial_file_list.txt containing all
 | 
			
		||||
        # partial movie files
 | 
			
		||||
        file_list = os.path.join(
 | 
			
		||||
            self.partial_movie_directory,
 | 
			
		||||
            "partial_movie_file_list.txt"
 | 
			
		||||
        )
 | 
			
		||||
        with open(file_list, 'w') as fp:
 | 
			
		||||
            for pf_path in partial_movie_files:
 | 
			
		||||
                if os.name == 'nt':
 | 
			
		||||
                    pf_path = pf_path.replace('\\', '/')
 | 
			
		||||
                fp.write(f"file \'{pf_path}\'\n")
 | 
			
		||||
 | 
			
		||||
        movie_file_path = self.get_movie_file_path()
 | 
			
		||||
        commands = [
 | 
			
		||||
            self.ffmpeg_bin,
 | 
			
		||||
            '-y',  # overwrite output file if it exists
 | 
			
		||||
            '-f', 'concat',
 | 
			
		||||
            '-safe', '0',
 | 
			
		||||
            '-i', file_list,
 | 
			
		||||
            '-loglevel', 'error',
 | 
			
		||||
            '-c', 'copy',
 | 
			
		||||
            movie_file_path
 | 
			
		||||
        ]
 | 
			
		||||
        if not self.includes_sound:
 | 
			
		||||
            commands.insert(-1, '-an')
 | 
			
		||||
 | 
			
		||||
        combine_process = sp.Popen(commands)
 | 
			
		||||
        combine_process.wait()
 | 
			
		||||
 | 
			
		||||
    def add_sound_to_video(self) -> None:
 | 
			
		||||
        movie_file_path = self.get_movie_file_path()
 | 
			
		||||
        stem, ext = os.path.splitext(movie_file_path)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,29 +20,3 @@ def merge_dicts_recursively(*dicts):
 | 
			
		|||
        else:
 | 
			
		||||
            result[key] = value
 | 
			
		||||
    return result
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def soft_dict_update(d1, d2):
 | 
			
		||||
    """
 | 
			
		||||
    Adds key values pairs of d2 to d1 only when d1 doesn't
 | 
			
		||||
    already have that key
 | 
			
		||||
    """
 | 
			
		||||
    for key, value in list(d2.items()):
 | 
			
		||||
        if key not in d1:
 | 
			
		||||
            d1[key] = value
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def dict_eq(d1, d2):
 | 
			
		||||
    if len(d1) != len(d2):
 | 
			
		||||
        return False
 | 
			
		||||
    for key in d1:
 | 
			
		||||
        value1 = d1[key]
 | 
			
		||||
        value2 = d2[key]
 | 
			
		||||
        if type(value1) != type(value2):
 | 
			
		||||
            return False
 | 
			
		||||
        if type(d1[key]) == np.ndarray:
 | 
			
		||||
            if any(d1[key] != d2[key]):
 | 
			
		||||
                return False
 | 
			
		||||
        elif d1[key] != d2[key]:
 | 
			
		||||
            return False
 | 
			
		||||
    return True
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,25 +17,17 @@ if TYPE_CHECKING:
 | 
			
		|||
    from typing import Iterable
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def add_extension_if_not_present(file_name: str, extension: str) -> str:
 | 
			
		||||
    # This could conceivably be smarter about handling existing differing extensions
 | 
			
		||||
    if(file_name[-len(extension):] != extension):
 | 
			
		||||
        return file_name + extension
 | 
			
		||||
    else:
 | 
			
		||||
        return file_name
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def guarantee_existence(path: str) -> str:
 | 
			
		||||
    if not os.path.exists(path):
 | 
			
		||||
        os.makedirs(path)
 | 
			
		||||
    return os.path.abspath(path)
 | 
			
		||||
def guarantee_existence(path: str | Path) -> Path:
 | 
			
		||||
    path = Path(path)
 | 
			
		||||
    path.mkdir(parents=True, exist_ok=True)
 | 
			
		||||
    return path.absolute()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def find_file(
 | 
			
		||||
    file_name: str,
 | 
			
		||||
    directories: Iterable[str] | None = None,
 | 
			
		||||
    extensions: Iterable[str] | None = None
 | 
			
		||||
) -> str:
 | 
			
		||||
) -> Path:
 | 
			
		||||
    # Check if this is a file online first, and if so, download
 | 
			
		||||
    # it to a temporary directory
 | 
			
		||||
    if validators.url(file_name):
 | 
			
		||||
| 
						 | 
				
			
			@ -49,49 +41,17 @@ def find_file(
 | 
			
		|||
 | 
			
		||||
    # Check if what was passed in is already a valid path to a file
 | 
			
		||||
    if os.path.exists(file_name):
 | 
			
		||||
        return file_name
 | 
			
		||||
        return Path(file_name)
 | 
			
		||||
 | 
			
		||||
    # Otherwise look in local file system
 | 
			
		||||
    directories = directories or [""]
 | 
			
		||||
    extensions = extensions or [""]
 | 
			
		||||
    possible_paths = (
 | 
			
		||||
        os.path.join(directory, file_name + extension)
 | 
			
		||||
        Path(directory, file_name + extension)
 | 
			
		||||
        for directory in directories
 | 
			
		||||
        for extension in extensions
 | 
			
		||||
    )
 | 
			
		||||
    for path in possible_paths:
 | 
			
		||||
        if os.path.exists(path):
 | 
			
		||||
        if path.exists():
 | 
			
		||||
            return path
 | 
			
		||||
    raise IOError(f"{file_name} not Found")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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):
 | 
			
		||||
        if '.' in file:
 | 
			
		||||
            index_str = file[:file.index('.')]
 | 
			
		||||
        else:
 | 
			
		||||
            index_str = file
 | 
			
		||||
 | 
			
		||||
        full_path = os.path.join(directory, file)
 | 
			
		||||
        if index_str.isdigit():
 | 
			
		||||
            index = int(index_str)
 | 
			
		||||
            if remove_indices_greater_than is not None:
 | 
			
		||||
                if 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 list(map(lambda p: os.path.join(directory, p[1]), indexed_files))
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -36,12 +36,6 @@ def get_num_args(function: Callable) -> int:
 | 
			
		|||
def get_parameters(function: Callable) -> Iterable[str]:
 | 
			
		||||
    return inspect.signature(function).parameters.keys()
 | 
			
		||||
 | 
			
		||||
# Just to have a less heavyweight name for this extremely common operation
 | 
			
		||||
#
 | 
			
		||||
# We may wish to have more fine-grained control over division by zero behavior
 | 
			
		||||
# in the future (separate specifiable values for 0/0 and x/0 with x != 0),
 | 
			
		||||
# but for now, we just allow the option to handle indeterminate 0/0.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def clip(a: float, min_a: float, max_a: float) -> float:
 | 
			
		||||
    if a < min_a:
 | 
			
		||||
| 
						 | 
				
			
			@ -58,6 +52,10 @@ def arr_clip(arr: np.ndarray, min_a: float, max_a: float) -> np.ndarray:
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
def fdiv(a: Scalable, b: Scalable, zero_over_zero_value: Scalable | None = None) -> Scalable:
 | 
			
		||||
    """
 | 
			
		||||
    Less heavyweight name for np.true_divide, enabling
 | 
			
		||||
    default behavior for 0/0
 | 
			
		||||
    """
 | 
			
		||||
    if zero_over_zero_value is not None:
 | 
			
		||||
        out = np.full_like(a, zero_over_zero_value)
 | 
			
		||||
        where = np.logical_or(a != 0, b != 0)
 | 
			
		||||
| 
						 | 
				
			
			@ -68,11 +66,13 @@ def fdiv(a: Scalable, b: Scalable, zero_over_zero_value: Scalable | None = None)
 | 
			
		|||
    return np.true_divide(a, b, out=out, where=where)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def binary_search(function: Callable[[float], float],
 | 
			
		||||
                  target: float,
 | 
			
		||||
                  lower_bound: float,
 | 
			
		||||
                  upper_bound: float,
 | 
			
		||||
                  tolerance:float = 1e-4) -> float | None:
 | 
			
		||||
def binary_search(
 | 
			
		||||
    function: Callable[[float], float],
 | 
			
		||||
    target: float,
 | 
			
		||||
    lower_bound: float,
 | 
			
		||||
    upper_bound: float,
 | 
			
		||||
    tolerance:float = 1e-4
 | 
			
		||||
) -> float | None:
 | 
			
		||||
    lh = lower_bound
 | 
			
		||||
    rh = upper_bound
 | 
			
		||||
    mh = (lh + rh) / 2
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,40 +1,38 @@
 | 
			
		|||
from __future__ import annotations
 | 
			
		||||
 | 
			
		||||
import re
 | 
			
		||||
from functools import lru_cache
 | 
			
		||||
 | 
			
		||||
from manimlib.utils.tex_to_symbol_count import TEX_TO_SYMBOL_COUNT
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@lru_cache
 | 
			
		||||
def num_tex_symbols(tex: str) -> int:
 | 
			
		||||
    tex = remove_tex_environments(tex)
 | 
			
		||||
    commands_pattern = r"""
 | 
			
		||||
        (?P<sqrt>\\sqrt\[[0-9]+\])|    # Special sqrt with number
 | 
			
		||||
        (?P<cmd>\\[a-zA-Z!,-/:;<>]+)   # Regular commands
 | 
			
		||||
    """
 | 
			
		||||
    This function attempts to estimate the number of symbols that
 | 
			
		||||
    a given string of tex would produce.
 | 
			
		||||
 | 
			
		||||
    Warning, it may not behave perfectly
 | 
			
		||||
    """
 | 
			
		||||
    # First, remove patterns like \begin{align}, \phantom{thing},
 | 
			
		||||
    # \begin{array}{cc}, etc.
 | 
			
		||||
    pattern = "|".join(
 | 
			
		||||
        rf"(\\{s})" + r"(\{\w+\})?(\{\w+\})?(\[\w+\])?"
 | 
			
		||||
        for s in ["begin", "end", "phantom"]
 | 
			
		||||
    )
 | 
			
		||||
    tex = re.sub(pattern, "", tex)
 | 
			
		||||
 | 
			
		||||
    # Progressively count the symbols associated with certain tex commands,
 | 
			
		||||
    # and remove those commands from the string, adding the number of symbols
 | 
			
		||||
    # that command creates
 | 
			
		||||
    total = 0
 | 
			
		||||
    pos = 0
 | 
			
		||||
    for match in re.finditer(commands_pattern, tex, re.VERBOSE):
 | 
			
		||||
        # Count normal characters up to this command
 | 
			
		||||
        total += sum(1 for c in tex[pos:match.start()] if c not in "^{} \n\t_$\\&")
 | 
			
		||||
 | 
			
		||||
    # Start with the special case \sqrt[number]
 | 
			
		||||
    for substr in re.findall(r"\\sqrt\[[0-9]+\]", tex):
 | 
			
		||||
        total += len(substr) - 5  # e.g. \sqrt[3] is 3 symbols
 | 
			
		||||
        tex = tex.replace(substr, " ")
 | 
			
		||||
 | 
			
		||||
    general_command = r"\\[a-zA-Z!,-/:;<>]+"
 | 
			
		||||
    for substr in re.findall(general_command, tex):
 | 
			
		||||
        total += TEX_TO_SYMBOL_COUNT.get(substr, 1)
 | 
			
		||||
        tex = tex.replace(substr, " ")
 | 
			
		||||
        if match.group("sqrt"):
 | 
			
		||||
            total += len(match.group()) - 5
 | 
			
		||||
        else:
 | 
			
		||||
            total += TEX_TO_SYMBOL_COUNT.get(match.group(), 1)
 | 
			
		||||
        pos = match.end()
 | 
			
		||||
 | 
			
		||||
    # Count remaining characters
 | 
			
		||||
    total += sum(map(lambda c: c not in "^{} \n\t_$\\&", tex))
 | 
			
		||||
    total += sum(1 for c in tex[pos:] if c not in "^{} \n\t_$\\&")
 | 
			
		||||
    return total
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def remove_tex_environments(tex: str) -> str:
 | 
			
		||||
    # Handle \phantom{...} with any content
 | 
			
		||||
    tex = re.sub(r"\\phantom\{[^}]*\}", "", tex)
 | 
			
		||||
    # Handle other environment commands
 | 
			
		||||
    tex = re.sub(r"\\(begin|end)(\{\w+\})?(\{\w+\})?(\[\w+\])?", "", tex)
 | 
			
		||||
    return tex
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue