mirror of
https://github.com/3b1b/manim.git
synced 2025-04-13 09:47:07 +00:00
A few bug fixes (#2277)
* 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 * Add arg to clear the cache * Separate out full_tex_to_svg from tex_to_svg And only cache to disk the results of full_tex_to_svg. Otherwise, making edits to the tex_templates would not show up without clearing the cache. * Bug fix in handling BlankScene * Make checkpoint_states an instance variable of CheckpointManager As per https://github.com/3b1b/manim/issues/2272 * Move resizing out of Window.focus, and into Window.init_for_scene * Make default output directory "." instead of "" To address https://github.com/3b1b/manim/issues/2261 * Remove input_file_path arg from SceneFileWriter * Use Dict syntax in place of dict for config more consistently across config.py * Simplify get_output_directory * Swap order of preamble and additional preamble
This commit is contained in:
parent
3d9a0cd25e
commit
f427fc67df
5 changed files with 80 additions and 85 deletions
|
@ -5,6 +5,7 @@ from manimlib import __version__
|
||||||
from manimlib.config import manim_config
|
from manimlib.config import manim_config
|
||||||
from manimlib.config import parse_cli
|
from manimlib.config import parse_cli
|
||||||
import manimlib.extract_scene
|
import manimlib.extract_scene
|
||||||
|
from manimlib.utils.cache import clear_cache
|
||||||
from manimlib.window import Window
|
from manimlib.window import Window
|
||||||
|
|
||||||
|
|
||||||
|
@ -54,6 +55,8 @@ def main():
|
||||||
args = parse_cli()
|
args = parse_cli()
|
||||||
if args.version and args.file is None:
|
if args.version and args.file is None:
|
||||||
return
|
return
|
||||||
|
if args.clear_cache:
|
||||||
|
clear_cache()
|
||||||
|
|
||||||
run_scenes()
|
run_scenes()
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ import inspect
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import yaml
|
import yaml
|
||||||
|
from pathlib import Path
|
||||||
from ast import literal_eval
|
from ast import literal_eval
|
||||||
from addict import Dict
|
from addict import Dict
|
||||||
|
|
||||||
|
@ -31,11 +32,11 @@ def initialize_manim_config() -> Dict:
|
||||||
"""
|
"""
|
||||||
args = parse_cli()
|
args = parse_cli()
|
||||||
global_defaults_file = os.path.join(get_manim_dir(), "manimlib", "default_config.yml")
|
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(global_defaults_file),
|
||||||
load_yaml("custom_config.yml"), # From current working directory
|
load_yaml("custom_config.yml"), # From current working directory
|
||||||
load_yaml(args.config_file) if args.config_file else dict(),
|
load_yaml(args.config_file) if args.config_file else dict(),
|
||||||
)
|
))
|
||||||
|
|
||||||
log.setLevel(args.log_level or config["log_level"])
|
log.setLevel(args.log_level or config["log_level"])
|
||||||
|
|
||||||
|
@ -47,7 +48,7 @@ def initialize_manim_config() -> Dict:
|
||||||
update_run_config(config, args)
|
update_run_config(config, args)
|
||||||
update_embed_config(config, args)
|
update_embed_config(config, args)
|
||||||
|
|
||||||
return Dict(config)
|
return config
|
||||||
|
|
||||||
|
|
||||||
def parse_cli():
|
def parse_cli():
|
||||||
|
@ -211,6 +212,11 @@ def parse_cli():
|
||||||
"--log-level",
|
"--log-level",
|
||||||
help="Level of messages to Display, can be DEBUG / INFO / WARNING / ERROR / CRITICAL"
|
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(
|
parser.add_argument(
|
||||||
"--autoreload",
|
"--autoreload",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
|
@ -225,41 +231,41 @@ def parse_cli():
|
||||||
sys.exit(2)
|
sys.exit(2)
|
||||||
|
|
||||||
|
|
||||||
def update_directory_config(config: dict):
|
def update_directory_config(config: Dict):
|
||||||
dir_config = config["directories"]
|
dir_config = config.directories
|
||||||
base = dir_config['base']
|
base = dir_config.base
|
||||||
for key, subdir in dir_config['subdirs'].items():
|
for key, subdir in dir_config.subdirs.items():
|
||||||
dir_config[key] = os.path.join(base, subdir)
|
dir_config[key] = os.path.join(base, subdir)
|
||||||
|
|
||||||
|
|
||||||
def update_window_config(config: dict, args: Namespace):
|
def update_window_config(config: Dict, args: Namespace):
|
||||||
window_config = config["window"]
|
window_config = config.window
|
||||||
for key in "position", "size":
|
for key in "position", "size":
|
||||||
if window_config.get(key):
|
if window_config.get(key):
|
||||||
window_config[key] = literal_eval(window_config[key])
|
window_config[key] = literal_eval(window_config[key])
|
||||||
if args.full_screen:
|
if args.full_screen:
|
||||||
window_config["full_screen"] = True
|
window_config.full_screen = True
|
||||||
|
|
||||||
|
|
||||||
def update_camera_config(config: dict, args: Namespace):
|
def update_camera_config(config: Dict, args: Namespace):
|
||||||
camera_config = config["camera"]
|
camera_config = config.camera
|
||||||
arg_resolution = get_resolution_from_args(args, config["resolution_options"])
|
arg_resolution = get_resolution_from_args(args, config.resolution_options)
|
||||||
camera_config["resolution"] = arg_resolution or literal_eval(camera_config["resolution"])
|
camera_config.resolution = arg_resolution or literal_eval(camera_config.resolution)
|
||||||
if args.fps:
|
if args.fps:
|
||||||
camera_config["fps"] = args.fps
|
camera_config.fps = args.fps
|
||||||
if args.color:
|
if args.color:
|
||||||
try:
|
try:
|
||||||
camera_config["background_color"] = colour.Color(args.color)
|
camera_config.background_color = colour.Color(args.color)
|
||||||
except Exception:
|
except Exception:
|
||||||
log.error("Please use a valid color")
|
log.error("Please use a valid color")
|
||||||
log.error(err)
|
log.error(err)
|
||||||
sys.exit(2)
|
sys.exit(2)
|
||||||
if args.transparent:
|
if args.transparent:
|
||||||
camera_config["background_opacity"] = 0.0
|
camera_config.background_opacity = 0.0
|
||||||
|
|
||||||
|
|
||||||
def update_file_writer_config(config: dict, args: Namespace):
|
def update_file_writer_config(config: Dict, args: Namespace):
|
||||||
file_writer_config = config["file_writer"]
|
file_writer_config = config.file_writer
|
||||||
file_writer_config.update(
|
file_writer_config.update(
|
||||||
write_to_movie=(not args.skip_animations and args.write_file),
|
write_to_movie=(not args.skip_animations and args.write_file),
|
||||||
subdivide_output=args.subdivide,
|
subdivide_output=args.subdivide,
|
||||||
|
@ -268,26 +274,25 @@ def update_file_writer_config(config: dict, args: Namespace):
|
||||||
movie_file_extension=(get_file_ext(args)),
|
movie_file_extension=(get_file_ext(args)),
|
||||||
output_directory=get_output_directory(args, config),
|
output_directory=get_output_directory(args, config),
|
||||||
file_name=args.file_name,
|
file_name=args.file_name,
|
||||||
input_file_path=args.file or "",
|
|
||||||
open_file_upon_completion=args.open,
|
open_file_upon_completion=args.open,
|
||||||
show_file_location_upon_completion=args.finder,
|
show_file_location_upon_completion=args.finder,
|
||||||
quiet=args.quiet,
|
quiet=args.quiet,
|
||||||
)
|
)
|
||||||
|
|
||||||
if args.vcodec:
|
if args.vcodec:
|
||||||
file_writer_config["video_codec"] = args.vcodec
|
file_writer_config.video_codec = args.vcodec
|
||||||
elif args.transparent:
|
elif args.transparent:
|
||||||
file_writer_config["video_codec"] = 'prores_ks'
|
file_writer_config.video_codec = 'prores_ks'
|
||||||
file_writer_config["pixel_format"] = ''
|
file_writer_config.pixel_format = ''
|
||||||
elif args.gif:
|
elif args.gif:
|
||||||
file_writer_config["video_codec"] = ''
|
file_writer_config.video_codec = ''
|
||||||
|
|
||||||
if args.pix_fmt:
|
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):
|
def update_scene_config(config: Dict, args: Namespace):
|
||||||
scene_config = config["scene"]
|
scene_config = config.scene
|
||||||
start, end = get_animations_numbers(args)
|
start, end = get_animations_numbers(args)
|
||||||
scene_config.update(
|
scene_config.update(
|
||||||
# Note, Scene.__init__ makes use of both manimlib.camera and
|
# 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,
|
presenter_mode=args.presenter_mode,
|
||||||
)
|
)
|
||||||
if args.leave_progress_bars:
|
if args.leave_progress_bars:
|
||||||
scene_config["leave_progress_bars"] = True
|
scene_config.leave_progress_bars = True
|
||||||
if args.show_animation_progress:
|
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):
|
def update_run_config(config: Dict, args: Namespace):
|
||||||
config["run"] = dict(
|
config.run = Dict(
|
||||||
file_name=args.file,
|
file_name=args.file,
|
||||||
embed_line=(int(args.embed) if args.embed is not None else None),
|
embed_line=(int(args.embed) if args.embed is not None else None),
|
||||||
is_reload=False,
|
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:
|
if args.autoreload:
|
||||||
config["embed"]["autoreload"] = True
|
config.embed.autoreload = True
|
||||||
|
|
||||||
|
|
||||||
# Helpers for the functions above
|
# Helpers for the functions above
|
||||||
|
@ -375,17 +380,15 @@ def get_animations_numbers(args: Namespace) -> tuple[int | None, int | None]:
|
||||||
return int(stan), None
|
return int(stan), None
|
||||||
|
|
||||||
|
|
||||||
def get_output_directory(args: Namespace, config: dict) -> str:
|
def get_output_directory(args: Namespace, config: Dict) -> str:
|
||||||
dir_config = config["directories"]
|
dir_config = config.directories
|
||||||
output_directory = args.video_dir or dir_config["output"]
|
out_dir = args.video_dir or dir_config.output
|
||||||
if dir_config["mirror_module_path"] and args.file:
|
if dir_config.mirror_module_path and args.file:
|
||||||
to_cut = dir_config["removed_mirror_prefix"]
|
file_path = Path(args.file).absolute()
|
||||||
ext = os.path.abspath(args.file)
|
rel_path = file_path.relative_to(dir_config.removed_mirror_prefix)
|
||||||
ext = ext.replace(to_cut, "").replace(".py", "")
|
rel_path = Path(str(rel_path).lstrip("_"))
|
||||||
if ext.startswith("_"):
|
out_dir = Path(out_dir, rel_path).with_suffix("")
|
||||||
ext = ext[1:]
|
return out_dir
|
||||||
output_directory = os.path.join(output_directory, ext)
|
|
||||||
return output_directory
|
|
||||||
|
|
||||||
|
|
||||||
# Create global configuration
|
# Create global configuration
|
||||||
|
|
|
@ -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]):
|
def get_scene_classes(module: Optional[Module]):
|
||||||
if module is None:
|
if module is None:
|
||||||
# If no module was passed in, just play the blank scene
|
# If no module was passed in, just play the blank scene
|
||||||
return [BlankScene(**scene_config)]
|
return [BlankScene]
|
||||||
if hasattr(module, "SCENES_IN_ORDER"):
|
if hasattr(module, "SCENES_IN_ORDER"):
|
||||||
return module.SCENES_IN_ORDER
|
return module.SCENES_IN_ORDER
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -34,10 +34,8 @@ class SceneFileWriter(object):
|
||||||
png_mode: str = "RGBA",
|
png_mode: str = "RGBA",
|
||||||
save_last_frame: bool = False,
|
save_last_frame: bool = False,
|
||||||
movie_file_extension: str = ".mp4",
|
movie_file_extension: str = ".mp4",
|
||||||
# What python file is generating this scene
|
|
||||||
input_file_path: str = "",
|
|
||||||
# Where should this be written
|
# Where should this be written
|
||||||
output_directory: str = "",
|
output_directory: str = ".",
|
||||||
file_name: str | None = None,
|
file_name: str | None = None,
|
||||||
open_file_upon_completion: bool = False,
|
open_file_upon_completion: bool = False,
|
||||||
show_file_location_upon_completion: bool = False,
|
show_file_location_upon_completion: bool = False,
|
||||||
|
@ -57,7 +55,6 @@ class SceneFileWriter(object):
|
||||||
self.png_mode = png_mode
|
self.png_mode = png_mode
|
||||||
self.save_last_frame = save_last_frame
|
self.save_last_frame = save_last_frame
|
||||||
self.movie_file_extension = movie_file_extension
|
self.movie_file_extension = movie_file_extension
|
||||||
self.input_file_path = input_file_path
|
|
||||||
self.output_directory = output_directory
|
self.output_directory = output_directory
|
||||||
self.file_name = file_name
|
self.file_name = file_name
|
||||||
self.open_file_upon_completion = open_file_upon_completion
|
self.open_file_upon_completion = open_file_upon_completion
|
||||||
|
|
|
@ -31,22 +31,13 @@ def get_tex_template_config(template_name: str) -> dict[str, str]:
|
||||||
|
|
||||||
|
|
||||||
@lru_cache
|
@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:
|
Returns a compiler and preamble to use for rendering LaTeX
|
||||||
{
|
|
||||||
"template": "default",
|
|
||||||
"compiler": "latex",
|
|
||||||
"preamble": "..."
|
|
||||||
}
|
|
||||||
"""
|
"""
|
||||||
template = template or manim_config.tex.template
|
template = template or manim_config.tex.template
|
||||||
template_config = get_tex_template_config(template)
|
config = get_tex_template_config(template)
|
||||||
return {
|
return config["compiler"], config["preamble"]
|
||||||
"template": template,
|
|
||||||
"compiler": template_config["compiler"],
|
|
||||||
"preamble": template_config["preamble"]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def get_full_tex(content: str, preamble: str = ""):
|
def get_full_tex(content: str, preamble: str = ""):
|
||||||
|
@ -60,7 +51,6 @@ def get_full_tex(content: str, preamble: str = ""):
|
||||||
|
|
||||||
|
|
||||||
@lru_cache(maxsize=128)
|
@lru_cache(maxsize=128)
|
||||||
@cache_on_disk
|
|
||||||
def latex_to_svg(
|
def latex_to_svg(
|
||||||
latex: str,
|
latex: str,
|
||||||
template: str = "",
|
template: str = "",
|
||||||
|
@ -83,14 +73,21 @@ def latex_to_svg(
|
||||||
NotImplementedError: If compiler is not supported
|
NotImplementedError: If compiler is not supported
|
||||||
"""
|
"""
|
||||||
if show_message_during_execution:
|
if show_message_during_execution:
|
||||||
max_message_len = 80
|
message = f"Writing {(short_tex or latex)[:70]}..."
|
||||||
message = f"Writing {short_tex or latex}"
|
else:
|
||||||
if len(message) > max_message_len:
|
message = ""
|
||||||
message = message[:max_message_len - 3] + "..."
|
|
||||||
print(message, end="\r")
|
|
||||||
|
|
||||||
tex_config = get_tex_config(template)
|
compiler, preamble = get_tex_config(template)
|
||||||
compiler = tex_config["compiler"]
|
|
||||||
|
preamble = "\n".join([preamble, additional_preamble])
|
||||||
|
full_tex = get_full_tex(latex, preamble)
|
||||||
|
return full_tex_to_svg(full_tex, compiler, message)
|
||||||
|
|
||||||
|
|
||||||
|
@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":
|
if compiler == "latex":
|
||||||
dvi_ext = ".dvi"
|
dvi_ext = ".dvi"
|
||||||
|
@ -99,17 +96,13 @@ def latex_to_svg(
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError(f"Compiler '{compiler}' is not implemented")
|
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
|
# Write intermediate files to a temporary directory
|
||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
base_path = os.path.join(temp_dir, "working")
|
tex_path = Path(temp_dir, "working").with_suffix(".tex")
|
||||||
tex_path = base_path + ".tex"
|
dvi_path = tex_path.with_suffix(dvi_ext)
|
||||||
dvi_path = base_path + dvi_ext
|
|
||||||
|
|
||||||
# Write tex file
|
# Write tex file
|
||||||
Path(tex_path).write_text(full_tex)
|
tex_path.write_text(full_tex)
|
||||||
|
|
||||||
# Run latex compiler
|
# Run latex compiler
|
||||||
process = subprocess.run(
|
process = subprocess.run(
|
||||||
|
@ -128,13 +121,12 @@ def latex_to_svg(
|
||||||
if process.returncode != 0:
|
if process.returncode != 0:
|
||||||
# Handle error
|
# Handle error
|
||||||
error_str = ""
|
error_str = ""
|
||||||
log_path = base_path + ".log"
|
log_path = tex_path.with_suffix(".log")
|
||||||
if os.path.exists(log_path):
|
if log_path.exists():
|
||||||
with open(log_path, "r", encoding="utf-8") as log_file:
|
content = log_path.read_text()
|
||||||
content = log_file.read()
|
error_match = re.search(r"(?<=\n! ).*\n.*\n", content)
|
||||||
error_match = re.search(r"(?<=\n! ).*\n.*\n", content)
|
if error_match:
|
||||||
if error_match:
|
error_str = error_match.group()
|
||||||
error_str = error_match.group()
|
|
||||||
raise LatexError(error_str or "LaTeX compilation failed")
|
raise LatexError(error_str or "LaTeX compilation failed")
|
||||||
|
|
||||||
# Run dvisvgm and capture output directly
|
# Run dvisvgm and capture output directly
|
||||||
|
@ -152,7 +144,7 @@ def latex_to_svg(
|
||||||
# Return SVG string
|
# Return SVG string
|
||||||
result = process.stdout.decode('utf-8')
|
result = process.stdout.decode('utf-8')
|
||||||
|
|
||||||
if show_message_during_execution:
|
if message:
|
||||||
print(" " * len(message), end="\r")
|
print(" " * len(message), end="\r")
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
Loading…
Add table
Reference in a new issue