2024-12-06 12:24:16 -07:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2022-04-12 19:19:59 +08:00
|
|
|
import copy
|
2018-03-31 15:11:35 -07:00
|
|
|
import inspect
|
2018-12-22 14:27:22 -08:00
|
|
|
import sys
|
2015-05-06 17:58:34 -07:00
|
|
|
|
2024-12-06 12:24:16 -07:00
|
|
|
from manimlib.module_loader import ModuleLoader
|
|
|
|
|
2024-12-11 09:50:17 -06:00
|
|
|
from manimlib.config import manim_config
|
2021-10-07 17:37:10 +08:00
|
|
|
from manimlib.logger import log
|
2022-04-23 09:03:53 -07:00
|
|
|
from manimlib.scene.interactive_scene import InteractiveScene
|
2022-04-12 19:19:59 +08:00
|
|
|
from manimlib.scene.scene import Scene
|
2015-05-06 17:58:34 -07:00
|
|
|
|
2024-12-06 12:24:16 -07:00
|
|
|
from typing import TYPE_CHECKING
|
2024-12-28 20:48:32 +05:30
|
|
|
|
2024-12-06 12:24:16 -07:00
|
|
|
if TYPE_CHECKING:
|
|
|
|
Module = importlib.util.types.ModuleType
|
|
|
|
from typing import Optional
|
2024-12-12 10:39:54 -06:00
|
|
|
from addict import Dict
|
2024-12-06 12:24:16 -07:00
|
|
|
|
2015-10-10 18:48:54 -07:00
|
|
|
|
2022-04-22 08:33:18 -07:00
|
|
|
class BlankScene(InteractiveScene):
|
2021-01-02 22:20:13 -08:00
|
|
|
def construct(self):
|
2024-12-11 09:50:17 -06:00
|
|
|
exec(manim_config.universal_import_line)
|
2021-01-02 22:20:13 -08:00
|
|
|
self.embed()
|
|
|
|
|
|
|
|
|
2018-12-25 19:51:03 -08:00
|
|
|
def is_child_scene(obj, module):
|
2018-04-06 13:58:59 -07:00
|
|
|
if not inspect.isclass(obj):
|
|
|
|
return False
|
|
|
|
if not issubclass(obj, Scene):
|
|
|
|
return False
|
|
|
|
if obj == Scene:
|
|
|
|
return False
|
2019-07-24 18:01:12 -05:00
|
|
|
if not obj.__module__.startswith(module.__name__):
|
|
|
|
return False
|
2018-04-06 13:58:59 -07:00
|
|
|
return True
|
|
|
|
|
2015-05-07 21:28:02 -07:00
|
|
|
|
2019-01-25 11:03:14 -08:00
|
|
|
def prompt_user_for_choice(scene_classes):
|
2021-01-02 22:20:13 -08:00
|
|
|
name_to_class = {}
|
2021-02-05 13:09:03 +05:30
|
|
|
max_digits = len(str(len(scene_classes)))
|
|
|
|
for idx, scene_class in enumerate(scene_classes, start=1):
|
2019-01-25 11:03:14 -08:00
|
|
|
name = scene_class.__name__
|
2021-02-05 13:09:03 +05:30
|
|
|
print(f"{str(idx).zfill(max_digits)}: {name}")
|
2021-01-02 22:20:13 -08:00
|
|
|
name_to_class[name] = scene_class
|
2018-04-06 13:58:59 -07:00
|
|
|
try:
|
2024-12-12 10:39:54 -06:00
|
|
|
user_input = input("\nSelect which scene to render (by name or number): ")
|
2018-04-06 13:58:59 -07:00
|
|
|
return [
|
2021-11-30 11:41:33 -08:00
|
|
|
name_to_class[split_str] if not split_str.isnumeric() else scene_classes[int(split_str) - 1]
|
2021-02-05 13:09:03 +05:30
|
|
|
for split_str in user_input.replace(" ", "").split(",")
|
2018-04-06 13:58:59 -07:00
|
|
|
]
|
2021-10-07 17:37:10 +08:00
|
|
|
except IndexError:
|
|
|
|
log.error("Invalid scene number")
|
|
|
|
sys.exit(2)
|
2018-12-22 14:27:22 -08:00
|
|
|
except KeyError:
|
2021-10-07 17:37:10 +08:00
|
|
|
log.error("Invalid scene name")
|
2018-12-25 19:51:03 -08:00
|
|
|
sys.exit(2)
|
|
|
|
except EOFError:
|
|
|
|
sys.exit(1)
|
2018-04-06 13:58:59 -07:00
|
|
|
|
2015-10-20 21:55:46 -07:00
|
|
|
|
2023-02-03 11:45:47 -08:00
|
|
|
def compute_total_frames(scene_class, scene_config):
|
2021-11-30 11:41:33 -08:00
|
|
|
"""
|
|
|
|
When a scene is being written to file, a copy of the scene is run with
|
|
|
|
skip_animations set to true so as to count how many frames it will require.
|
|
|
|
This allows for a total progress bar on rendering, and also allows runtime
|
2023-01-04 16:36:25 -08:00
|
|
|
errors to be exposed preemptively for long running scenes.
|
2021-11-30 11:41:33 -08:00
|
|
|
"""
|
|
|
|
pre_config = copy.deepcopy(scene_config)
|
|
|
|
pre_config["file_writer_config"]["write_to_movie"] = False
|
2023-01-04 16:36:25 -08:00
|
|
|
pre_config["file_writer_config"]["save_last_frame"] = False
|
2021-12-07 10:07:15 -08:00
|
|
|
pre_config["file_writer_config"]["quiet"] = True
|
2021-11-30 11:41:33 -08:00
|
|
|
pre_config["skip_animations"] = True
|
|
|
|
pre_scene = scene_class(**pre_config)
|
|
|
|
pre_scene.run()
|
2021-12-07 10:07:15 -08:00
|
|
|
total_time = pre_scene.time - pre_scene.skip_time
|
2024-12-11 09:50:17 -06:00
|
|
|
return int(total_time * manim_config.camera.fps)
|
2021-11-30 11:41:33 -08:00
|
|
|
|
|
|
|
|
2024-12-12 10:39:54 -06:00
|
|
|
def scene_from_class(scene_class, scene_config: Dict, run_config: Dict):
|
2024-12-11 09:50:17 -06:00
|
|
|
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)
|
2023-02-03 11:45:47 -08:00
|
|
|
return scene_class(**scene_config)
|
|
|
|
|
|
|
|
|
2024-12-12 10:39:54 -06:00
|
|
|
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")
|
2023-02-03 11:45:47 -08:00
|
|
|
|
|
|
|
|
2024-12-12 10:39:54 -06:00
|
|
|
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
|
2021-01-02 22:20:13 -08:00
|
|
|
else:
|
2024-12-12 10:39:54 -06:00
|
|
|
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:
|
2023-02-03 11:45:47 -08:00
|
|
|
classes_to_run = prompt_user_for_choice(all_scene_classes)
|
|
|
|
|
|
|
|
return [
|
2024-12-05 14:36:21 -06:00
|
|
|
scene_from_class(scene_class, scene_config, run_config)
|
2023-02-03 11:45:47 -08:00
|
|
|
for scene_class in classes_to_run
|
|
|
|
]
|
2019-01-25 11:03:14 -08:00
|
|
|
|
|
|
|
|
2024-12-12 10:39:54 -06:00
|
|
|
def get_scene_classes(module: Optional[Module]):
|
|
|
|
if module is None:
|
|
|
|
# If no module was passed in, just play the blank scene
|
2024-12-12 20:45:34 -06:00
|
|
|
return [BlankScene]
|
2019-07-24 18:01:12 -05:00
|
|
|
if hasattr(module, "SCENES_IN_ORDER"):
|
|
|
|
return module.SCENES_IN_ORDER
|
|
|
|
else:
|
|
|
|
return [
|
|
|
|
member[1]
|
|
|
|
for member in inspect.getmembers(
|
|
|
|
module,
|
|
|
|
lambda x: is_child_scene(x, module)
|
|
|
|
)
|
|
|
|
]
|
2018-04-06 13:58:59 -07:00
|
|
|
|
2015-10-28 16:03:33 -07:00
|
|
|
|
2024-12-06 12:24:16 -07:00
|
|
|
def get_indent(code_lines: list[str], line_number: int) -> str:
|
2024-12-09 09:49:48 -06:00
|
|
|
"""
|
|
|
|
Find the indent associated with a given line of python code,
|
|
|
|
as a string of spaces
|
|
|
|
"""
|
|
|
|
# Find most recent non-empty line
|
|
|
|
try:
|
2024-12-09 11:59:16 -06:00
|
|
|
line = next(filter(lambda line: line.strip(), code_lines[line_number - 1::-1]))
|
2024-12-09 09:49:48 -06:00
|
|
|
except StopIteration:
|
|
|
|
return ""
|
|
|
|
|
|
|
|
# Either return its leading spaces, or add for if it ends with colon
|
|
|
|
n_spaces = len(line) - len(line.lstrip())
|
|
|
|
if line.endswith(":"):
|
|
|
|
n_spaces += 4
|
|
|
|
return n_spaces * " "
|
2024-12-06 12:24:16 -07:00
|
|
|
|
|
|
|
|
2025-02-04 17:47:54 +01:00
|
|
|
def insert_embed_line_to_module_exec(module: Module, run_config: Dict) -> None:
|
2024-12-06 12:24:16 -07:00
|
|
|
"""
|
2025-02-04 17:47:54 +01:00
|
|
|
Loads the user code, inserts a self.embed() line at the given line_number
|
|
|
|
and executes the code.
|
|
|
|
|
2024-12-06 12:24:16 -07:00
|
|
|
This is hacky, but convenient. When user includes the argument "-e", it will try
|
|
|
|
to recreate a file that inserts the line `self.embed()` into the end of the scene's
|
|
|
|
construct method. If there is an argument passed in, it will insert the line after
|
|
|
|
the last line in the sourcefile which includes that string.
|
|
|
|
"""
|
|
|
|
lines = inspect.getsource(module).splitlines()
|
2024-12-28 20:48:32 +05:30
|
|
|
line_number = run_config.embed_line
|
2024-12-06 12:24:16 -07:00
|
|
|
|
|
|
|
# Add the relevant embed line to the code
|
|
|
|
indent = get_indent(lines, line_number)
|
|
|
|
lines.insert(line_number, indent + "self.embed()")
|
|
|
|
new_code = "\n".join(lines)
|
|
|
|
|
2024-12-28 20:48:32 +05:30
|
|
|
# When the user executes the `-e <line_number>` command
|
|
|
|
# without specifying scene_names, the nearest class name above
|
|
|
|
# `<line_number>` will be automatically used as 'scene_names'.
|
|
|
|
|
|
|
|
if not run_config.scene_names:
|
|
|
|
classes = list(filter(lambda line: line.startswith("class"), lines[:line_number]))
|
|
|
|
if classes:
|
|
|
|
from re import search
|
|
|
|
|
|
|
|
scene_name = search(r"(\w+)\(", classes[-1])
|
|
|
|
run_config.update(scene_names=[scene_name.group(1)])
|
|
|
|
else:
|
|
|
|
log.error(f"No 'class' found above {line_number}!")
|
|
|
|
|
2024-12-09 09:49:48 -06:00
|
|
|
# Execute the code, which presumably redefines the user's
|
|
|
|
# scene to include this embed line, within the relevant module.
|
2025-02-04 17:46:53 +01:00
|
|
|
# Note that we add the user-module to sys.modules to please Python builtins
|
|
|
|
# that rely on cls.__module__ to be not None (which would be the case if
|
|
|
|
# the module was not in sys.modules). See #2307.
|
|
|
|
if module.__name__ in sys.modules:
|
|
|
|
log.error(
|
|
|
|
"Module name is already used by Manim itself, "
|
|
|
|
"please use a different name"
|
|
|
|
)
|
|
|
|
sys.exit(2)
|
|
|
|
sys.modules[module.__name__] = module
|
|
|
|
code_object = compile(new_code, module.__name__, "exec")
|
2024-12-06 12:24:16 -07:00
|
|
|
exec(code_object, module.__dict__)
|
|
|
|
|
|
|
|
|
2024-12-28 20:48:32 +05:30
|
|
|
def get_module(run_config: Dict) -> Module:
|
|
|
|
module = ModuleLoader.get_module(run_config.file_name, run_config.is_reload)
|
|
|
|
if run_config.embed_line:
|
2025-02-04 17:47:54 +01:00
|
|
|
insert_embed_line_to_module_exec(module, run_config)
|
2024-12-06 12:24:16 -07:00
|
|
|
return module
|
|
|
|
|
|
|
|
|
2024-12-12 10:39:54 -06:00
|
|
|
def main(scene_config: Dict, run_config: Dict):
|
2024-12-28 20:48:32 +05:30
|
|
|
module = get_module(run_config)
|
2024-12-12 10:39:54 -06:00
|
|
|
all_scene_classes = get_scene_classes(module)
|
2024-12-10 14:43:10 -06:00
|
|
|
scenes = get_scenes_to_render(all_scene_classes, scene_config, run_config)
|
|
|
|
if len(scenes) == 0:
|
|
|
|
print("No scenes found to run")
|
|
|
|
return scenes
|