diff --git a/manimlib/__main__.py b/manimlib/__main__.py index d2d5cf8967..3fb944d65a 100644 --- a/manimlib/__main__.py +++ b/manimlib/__main__.py @@ -5,6 +5,7 @@ from manimlib.config import manim_config from manimlib.config import parse_cli import manimlib.extract_scene +from manimlib.utils.cache import clear_cache from manimlib.window import Window @@ -54,6 +55,8 @@ def main(): args = parse_cli() if args.version and args.file is None: return + if args.clear_cache: + clear_cache() run_scenes() diff --git a/manimlib/config.py b/manimlib/config.py index 37872ce80b..ef9a3afc42 100644 --- a/manimlib/config.py +++ b/manimlib/config.py @@ -7,6 +7,7 @@ import os import sys import yaml +from pathlib import Path from ast import literal_eval from addict import Dict @@ -31,11 +32,11 @@ def initialize_manim_config() -> Dict: """ args = parse_cli() global_defaults_file = os.path.join(get_manim_dir(), "manimlib", "default_config.yml") - config = merge_dicts_recursively( + config = Dict(merge_dicts_recursively( load_yaml(global_defaults_file), load_yaml("custom_config.yml"), # From current working directory load_yaml(args.config_file) if args.config_file else dict(), - ) + )) log.setLevel(args.log_level or config["log_level"]) @@ -47,7 +48,7 @@ def initialize_manim_config() -> Dict: update_run_config(config, args) update_embed_config(config, args) - return Dict(config) + return config def parse_cli(): @@ -211,6 +212,11 @@ def parse_cli(): "--log-level", help="Level of messages to Display, can be DEBUG / INFO / WARNING / ERROR / CRITICAL" ) + parser.add_argument( + "--clear-cache", + action="store_true", + help="Erase the cache used for Tex and Text Mobjects" + ) parser.add_argument( "--autoreload", action="store_true", @@ -225,41 +231,41 @@ def parse_cli(): sys.exit(2) -def update_directory_config(config: dict): - dir_config = config["directories"] - base = dir_config['base'] - for key, subdir in dir_config['subdirs'].items(): +def update_directory_config(config: Dict): + dir_config = config.directories + base = dir_config.base + for key, subdir in dir_config.subdirs.items(): dir_config[key] = os.path.join(base, subdir) -def update_window_config(config: dict, args: Namespace): - window_config = config["window"] +def update_window_config(config: Dict, args: Namespace): + window_config = config.window for key in "position", "size": if window_config.get(key): window_config[key] = literal_eval(window_config[key]) if args.full_screen: - window_config["full_screen"] = True + window_config.full_screen = True -def update_camera_config(config: dict, args: Namespace): - camera_config = config["camera"] - arg_resolution = get_resolution_from_args(args, config["resolution_options"]) - camera_config["resolution"] = arg_resolution or literal_eval(camera_config["resolution"]) +def update_camera_config(config: Dict, args: Namespace): + camera_config = config.camera + arg_resolution = get_resolution_from_args(args, config.resolution_options) + camera_config.resolution = arg_resolution or literal_eval(camera_config.resolution) if args.fps: - camera_config["fps"] = args.fps + camera_config.fps = args.fps if args.color: try: - camera_config["background_color"] = colour.Color(args.color) + camera_config.background_color = colour.Color(args.color) except Exception: log.error("Please use a valid color") log.error(err) sys.exit(2) if args.transparent: - camera_config["background_opacity"] = 0.0 + camera_config.background_opacity = 0.0 -def update_file_writer_config(config: dict, args: Namespace): - file_writer_config = config["file_writer"] +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, @@ -268,26 +274,25 @@ def update_file_writer_config(config: dict, args: Namespace): movie_file_extension=(get_file_ext(args)), output_directory=get_output_directory(args, config), file_name=args.file_name, - input_file_path=args.file or "", open_file_upon_completion=args.open, show_file_location_upon_completion=args.finder, quiet=args.quiet, ) if args.vcodec: - file_writer_config["video_codec"] = args.vcodec + file_writer_config.video_codec = args.vcodec elif args.transparent: - file_writer_config["video_codec"] = 'prores_ks' - file_writer_config["pixel_format"] = '' + file_writer_config.video_codec = 'prores_ks' + file_writer_config.pixel_format = '' elif args.gif: - file_writer_config["video_codec"] = '' + file_writer_config.video_codec = '' if args.pix_fmt: - file_writer_config["pixel_format"] = args.pix_fmt + file_writer_config.pixel_format = args.pix_fmt -def update_scene_config(config: dict, args: Namespace): - scene_config = config["scene"] +def update_scene_config(config: Dict, args: Namespace): + scene_config = config.scene start, end = get_animations_numbers(args) scene_config.update( # Note, Scene.__init__ makes use of both manimlib.camera and @@ -301,13 +306,13 @@ def update_scene_config(config: dict, args: Namespace): presenter_mode=args.presenter_mode, ) if args.leave_progress_bars: - scene_config["leave_progress_bars"] = True + scene_config.leave_progress_bars = True if args.show_animation_progress: - scene_config["show_animation_progress"] = True + scene_config.show_animation_progress = True -def update_run_config(config: dict, args: Namespace): - config["run"] = dict( +def update_run_config(config: Dict, args: Namespace): + config.run = Dict( file_name=args.file, embed_line=(int(args.embed) if args.embed is not None else None), is_reload=False, @@ -319,9 +324,9 @@ def update_run_config(config: dict, args: Namespace): ) -def update_embed_config(config: dict, args: Namespace): +def update_embed_config(config: Dict, args: Namespace): if args.autoreload: - config["embed"]["autoreload"] = True + config.embed.autoreload = True # Helpers for the functions above @@ -375,17 +380,15 @@ def get_animations_numbers(args: Namespace) -> tuple[int | None, int | None]: return int(stan), None -def get_output_directory(args: Namespace, config: dict) -> str: - dir_config = config["directories"] - output_directory = args.video_dir or dir_config["output"] - if dir_config["mirror_module_path"] and args.file: - to_cut = dir_config["removed_mirror_prefix"] - ext = os.path.abspath(args.file) - ext = ext.replace(to_cut, "").replace(".py", "") - if ext.startswith("_"): - ext = ext[1:] - output_directory = os.path.join(output_directory, ext) - return output_directory +def get_output_directory(args: Namespace, config: Dict) -> str: + dir_config = config.directories + out_dir = args.video_dir or dir_config.output + if dir_config.mirror_module_path and args.file: + file_path = Path(args.file).absolute() + rel_path = file_path.relative_to(dir_config.removed_mirror_prefix) + rel_path = Path(str(rel_path).lstrip("_")) + out_dir = Path(out_dir, rel_path).with_suffix("") + return out_dir # Create global configuration diff --git a/manimlib/extract_scene.py b/manimlib/extract_scene.py index 6ebb48884a..5509778b05 100644 --- a/manimlib/extract_scene.py +++ b/manimlib/extract_scene.py @@ -111,7 +111,7 @@ def get_scenes_to_render(all_scene_classes: list, scene_config: Dict, run_config 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)] + return [BlankScene] if hasattr(module, "SCENES_IN_ORDER"): return module.SCENES_IN_ORDER else: diff --git a/manimlib/scene/scene_file_writer.py b/manimlib/scene/scene_file_writer.py index a29b347f10..479e3b4c8a 100644 --- a/manimlib/scene/scene_file_writer.py +++ b/manimlib/scene/scene_file_writer.py @@ -34,10 +34,8 @@ def __init__( 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 = "", + output_directory: str = ".", file_name: str | None = None, open_file_upon_completion: bool = False, show_file_location_upon_completion: bool = False, @@ -57,7 +55,6 @@ def __init__( self.png_mode = png_mode self.save_last_frame = save_last_frame self.movie_file_extension = movie_file_extension - self.input_file_path = input_file_path self.output_directory = output_directory self.file_name = file_name self.open_file_upon_completion = open_file_upon_completion diff --git a/manimlib/utils/tex_file_writing.py b/manimlib/utils/tex_file_writing.py index 7726312a1f..71872ce030 100644 --- a/manimlib/utils/tex_file_writing.py +++ b/manimlib/utils/tex_file_writing.py @@ -31,22 +31,13 @@ def get_tex_template_config(template_name: str) -> dict[str, str]: @lru_cache -def get_tex_config(template: str = "") -> dict[str, str]: +def get_tex_config(template: str = "") -> tuple[str, str]: """ - Returns a dict which should look something like this: - { - "template": "default", - "compiler": "latex", - "preamble": "..." - } + Returns a compiler and preamble to use for rendering LaTeX """ template = template or manim_config.tex.template - template_config = get_tex_template_config(template) - return { - "template": template, - "compiler": template_config["compiler"], - "preamble": template_config["preamble"] - } + config = get_tex_template_config(template) + return config["compiler"], config["preamble"] def get_full_tex(content: str, preamble: str = ""): @@ -60,7 +51,6 @@ def get_full_tex(content: str, preamble: str = ""): @lru_cache(maxsize=128) -@cache_on_disk def latex_to_svg( latex: str, template: str = "", @@ -83,14 +73,21 @@ def latex_to_svg( NotImplementedError: If compiler is not supported """ if show_message_during_execution: - max_message_len = 80 - message = f"Writing {short_tex or latex}" - if len(message) > max_message_len: - message = message[:max_message_len - 3] + "..." - print(message, end="\r") + message = f"Writing {(short_tex or latex)[:70]}..." + else: + message = "" + + compiler, preamble = get_tex_config(template) + + preamble = "\n".join([preamble, additional_preamble]) + full_tex = get_full_tex(latex, preamble) + return full_tex_to_svg(full_tex, compiler, message) - tex_config = get_tex_config(template) - compiler = tex_config["compiler"] + +@cache_on_disk +def full_tex_to_svg(full_tex: str, compiler: str = "latex", message: str = ""): + if message: + print(message, end="\r") if compiler == "latex": dvi_ext = ".dvi" @@ -99,17 +96,13 @@ def latex_to_svg( else: raise NotImplementedError(f"Compiler '{compiler}' is not implemented") - preamble = tex_config["preamble"] + "\n" + additional_preamble - full_tex = get_full_tex(latex, preamble) - # Write intermediate files to a temporary directory with tempfile.TemporaryDirectory() as temp_dir: - base_path = os.path.join(temp_dir, "working") - tex_path = base_path + ".tex" - dvi_path = base_path + dvi_ext + tex_path = Path(temp_dir, "working").with_suffix(".tex") + dvi_path = tex_path.with_suffix(dvi_ext) # Write tex file - Path(tex_path).write_text(full_tex) + tex_path.write_text(full_tex) # Run latex compiler process = subprocess.run( @@ -128,13 +121,12 @@ def latex_to_svg( if process.returncode != 0: # Handle error error_str = "" - log_path = base_path + ".log" - if os.path.exists(log_path): - with open(log_path, "r", encoding="utf-8") as log_file: - content = log_file.read() - error_match = re.search(r"(?<=\n! ).*\n.*\n", content) - if error_match: - error_str = error_match.group() + log_path = tex_path.with_suffix(".log") + if log_path.exists(): + content = log_path.read_text() + error_match = re.search(r"(?<=\n! ).*\n.*\n", content) + if error_match: + error_str = error_match.group() raise LatexError(error_str or "LaTeX compilation failed") # Run dvisvgm and capture output directly @@ -152,7 +144,7 @@ def latex_to_svg( # Return SVG string result = process.stdout.decode('utf-8') - if show_message_during_execution: + if message: print(" " * len(message), end="\r") return result