mirror of
https://github.com/3b1b/manim.git
synced 2025-04-13 09:47:07 +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
|
PI = np.pi
|
||||||
TAU = 2 * PI
|
TAU = 2 * PI
|
||||||
DEGREES = TAU / 360
|
DEG = TAU / 360
|
||||||
|
|
||||||
Text
|
Text
|
||||||
----
|
----
|
||||||
|
|
|
@ -34,7 +34,7 @@ InteractiveDevlopment
|
||||||
self.play(ReplacementTransform(square, circle))
|
self.play(ReplacementTransform(square, circle))
|
||||||
self.wait()
|
self.wait()
|
||||||
self.play(circle.animate.stretch(4, 0))
|
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))
|
self.play(circle.animate.shift(2 * RIGHT).scale(0.25))
|
||||||
|
|
||||||
text = Text("""
|
text = Text("""
|
||||||
|
@ -221,7 +221,7 @@ TexTransformExample
|
||||||
self.play(
|
self.play(
|
||||||
TransformMatchingTex(
|
TransformMatchingTex(
|
||||||
lines[0].copy(), lines[1],
|
lines[0].copy(), lines[1],
|
||||||
path_arc=90 * DEGREES,
|
path_arc=90 * DEG,
|
||||||
),
|
),
|
||||||
**play_kw
|
**play_kw
|
||||||
)
|
)
|
||||||
|
@ -599,8 +599,8 @@ SurfaceExample
|
||||||
# Set perspective
|
# Set perspective
|
||||||
frame = self.camera.frame
|
frame = self.camera.frame
|
||||||
frame.set_euler_angles(
|
frame.set_euler_angles(
|
||||||
theta=-30 * DEGREES,
|
theta=-30 * DEG,
|
||||||
phi=70 * DEGREES,
|
phi=70 * DEG,
|
||||||
)
|
)
|
||||||
|
|
||||||
surface = surfaces[0]
|
surface = surfaces[0]
|
||||||
|
@ -624,8 +624,8 @@ SurfaceExample
|
||||||
self.play(
|
self.play(
|
||||||
Transform(surface, surfaces[2]),
|
Transform(surface, surfaces[2]),
|
||||||
# Move camera frame during the transition
|
# Move camera frame during the transition
|
||||||
frame.animate.increment_phi(-10 * DEGREES),
|
frame.animate.increment_phi(-10 * DEG),
|
||||||
frame.animate.increment_theta(-20 * DEGREES),
|
frame.animate.increment_theta(-20 * DEG),
|
||||||
run_time=3
|
run_time=3
|
||||||
)
|
)
|
||||||
# Add ambient rotation
|
# Add ambient rotation
|
||||||
|
|
|
@ -190,7 +190,7 @@ class TexTransformExample(Scene):
|
||||||
# to go to a non-equal substring from the target,
|
# to go to a non-equal substring from the target,
|
||||||
# use the key map.
|
# use the key map.
|
||||||
key_map={"+": "-"},
|
key_map={"+": "-"},
|
||||||
path_arc=90 * DEGREES,
|
path_arc=90 * DEG,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
self.wait()
|
self.wait()
|
||||||
|
@ -203,7 +203,7 @@ class TexTransformExample(Scene):
|
||||||
TransformMatchingStrings(
|
TransformMatchingStrings(
|
||||||
lines[2].copy(), lines[3],
|
lines[2].copy(), lines[3],
|
||||||
key_map={"2": R"\sqrt"},
|
key_map={"2": R"\sqrt"},
|
||||||
path_arc=-30 * DEGREES,
|
path_arc=-30 * DEG,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
self.wait(2)
|
self.wait(2)
|
||||||
|
@ -616,8 +616,8 @@ class SurfaceExample(ThreeDScene):
|
||||||
self.play(
|
self.play(
|
||||||
Transform(surface, surfaces[2]),
|
Transform(surface, surfaces[2]),
|
||||||
# Move camera frame during the transition
|
# Move camera frame during the transition
|
||||||
self.frame.animate.increment_phi(-10 * DEGREES),
|
self.frame.animate.increment_phi(-10 * DEG),
|
||||||
self.frame.animate.increment_theta(-20 * DEGREES),
|
self.frame.animate.increment_theta(-20 * DEG),
|
||||||
run_time=3
|
run_time=3
|
||||||
)
|
)
|
||||||
# Add ambient rotation
|
# Add ambient rotation
|
||||||
|
@ -666,7 +666,7 @@ class InteractiveDevelopment(Scene):
|
||||||
self.play(ReplacementTransform(square, circle))
|
self.play(ReplacementTransform(square, circle))
|
||||||
self.wait()
|
self.wait()
|
||||||
self.play(circle.animate.stretch(4, 0))
|
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))
|
self.play(circle.animate.shift(2 * RIGHT).scale(0.25))
|
||||||
|
|
||||||
text = Text("""
|
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 FRAME_X_RADIUS, FRAME_Y_RADIUS
|
||||||
from manimlib.constants import ORIGIN, RIGHT, UP
|
from manimlib.constants import ORIGIN, RIGHT, UP
|
||||||
from manimlib.constants import SMALL_BUFF
|
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 TAU
|
||||||
from manimlib.constants import GREY, YELLOW
|
from manimlib.constants import GREY, YELLOW
|
||||||
from manimlib.mobject.geometry import Circle
|
from manimlib.mobject.geometry import Circle
|
||||||
|
@ -395,7 +395,7 @@ class WiggleOutThenIn(Animation):
|
||||||
|
|
||||||
|
|
||||||
class TurnInsideOut(Transform):
|
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)
|
super().__init__(mobject, path_arc=path_arc, **kwargs)
|
||||||
|
|
||||||
def create_target(self) -> Mobject:
|
def create_target(self) -> Mobject:
|
||||||
|
|
|
@ -5,7 +5,7 @@ import inspect
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from manimlib.animation.animation import Animation
|
from manimlib.animation.animation import Animation
|
||||||
from manimlib.constants import DEGREES
|
from manimlib.constants import DEG
|
||||||
from manimlib.constants import OUT
|
from manimlib.constants import OUT
|
||||||
from manimlib.mobject.mobject import Group
|
from manimlib.mobject.mobject import Group
|
||||||
from manimlib.mobject.mobject import Mobject
|
from manimlib.mobject.mobject import Mobject
|
||||||
|
@ -314,7 +314,7 @@ class ApplyComplexFunction(ApplyMethod):
|
||||||
|
|
||||||
|
|
||||||
class CyclicReplace(Transform):
|
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)
|
super().__init__(Group(*mobjects), path_arc=path_arc, **kwargs)
|
||||||
|
|
||||||
def create_target(self) -> Mobject:
|
def create_target(self) -> Mobject:
|
||||||
|
|
|
@ -6,7 +6,7 @@ import warnings
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from scipy.spatial.transform import Rotation
|
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 FRAME_SHAPE
|
||||||
from manimlib.constants import DOWN, LEFT, ORIGIN, OUT, RIGHT, UP
|
from manimlib.constants import DOWN, LEFT, ORIGIN, OUT, RIGHT, UP
|
||||||
from manimlib.constants import PI
|
from manimlib.constants import PI
|
||||||
|
@ -26,7 +26,7 @@ class CameraFrame(Mobject):
|
||||||
frame_shape: tuple[float, float] = FRAME_SHAPE,
|
frame_shape: tuple[float, float] = FRAME_SHAPE,
|
||||||
center_point: Vect3 = ORIGIN,
|
center_point: Vect3 = ORIGIN,
|
||||||
# Field of view in the y direction
|
# Field of view in the y direction
|
||||||
fovy: float = 45 * DEGREES,
|
fovy: float = 45 * DEG,
|
||||||
euler_axes: str = "zxz",
|
euler_axes: str = "zxz",
|
||||||
# This keeps it ordered first in a scene
|
# This keeps it ordered first in a scene
|
||||||
z_index=-1,
|
z_index=-1,
|
||||||
|
@ -181,7 +181,7 @@ class CameraFrame(Mobject):
|
||||||
Shortcut for set_euler_angles, defaulting to taking
|
Shortcut for set_euler_angles, defaulting to taking
|
||||||
in angles in degrees
|
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:
|
if center is not None:
|
||||||
self.move_to(np.array(center))
|
self.move_to(np.array(center))
|
||||||
if height is not None:
|
if height is not None:
|
||||||
|
@ -209,7 +209,7 @@ class CameraFrame(Mobject):
|
||||||
self.increment_euler_angles(dgamma=dgamma, units=units)
|
self.increment_euler_angles(dgamma=dgamma, units=units)
|
||||||
return self
|
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))
|
self.add_updater(lambda m, dt: m.increment_theta(angular_speed * dt))
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
|
@ -105,11 +105,6 @@ def parse_cli():
|
||||||
help="Scene will stay paused during wait calls until " + \
|
help="Scene will stay paused during wait calls until " + \
|
||||||
"space bar or right arrow is hit, like a slide show"
|
"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(
|
parser.add_argument(
|
||||||
"-i", "--gif",
|
"-i", "--gif",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
|
@ -148,6 +143,12 @@ def parse_cli():
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="Show the output file in finder",
|
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(
|
parser.add_argument(
|
||||||
"--file_name",
|
"--file_name",
|
||||||
help="Name for the movie or image file",
|
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 = 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,
|
||||||
save_last_frame=(args.skip_animations and args.write_file),
|
save_last_frame=(args.skip_animations and args.write_file),
|
||||||
save_pngs=args.save_pngs,
|
|
||||||
png_mode=("RGBA" if args.transparent else "RGB"),
|
png_mode=("RGBA" if args.transparent else "RGB"),
|
||||||
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),
|
||||||
|
|
|
@ -61,9 +61,10 @@ RIGHT_SIDE: Vect3 = FRAME_X_RADIUS * RIGHT
|
||||||
# Angles
|
# Angles
|
||||||
PI: float = np.pi
|
PI: float = np.pi
|
||||||
TAU: float = 2 * 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
|
# Nice to have a constant for readability
|
||||||
# when juxtaposed with expressions like 30 * DEGREES
|
# when juxtaposed with expressions like 30 * DEG
|
||||||
RADIANS: float = 1
|
RADIANS: float = 1
|
||||||
|
|
||||||
# Related to Text
|
# Related to Text
|
||||||
|
|
|
@ -52,13 +52,6 @@ camera:
|
||||||
fps: 30
|
fps: 30
|
||||||
background_opacity: 1.0
|
background_opacity: 1.0
|
||||||
file_writer:
|
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
|
# What command to use for ffmpeg
|
||||||
ffmpeg_bin: "ffmpeg"
|
ffmpeg_bin: "ffmpeg"
|
||||||
# Parameters to pass into ffmpeg
|
# Parameters to pass into ffmpeg
|
||||||
|
@ -71,6 +64,9 @@ file_writer:
|
||||||
scene:
|
scene:
|
||||||
show_animation_progress: False
|
show_animation_progress: False
|
||||||
leave_progress_bars: 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
|
# How long does a scene pause on Scene.wait calls
|
||||||
default_wait_time: 1.0
|
default_wait_time: 1.0
|
||||||
vmobject:
|
vmobject:
|
||||||
|
@ -104,6 +100,20 @@ sizes:
|
||||||
# Default buffers used in Mobject.next_to or Mobject.to_edge
|
# Default buffers used in Mobject.next_to or Mobject.to_edge
|
||||||
default_mobject_to_edge_buff: 0.5
|
default_mobject_to_edge_buff: 0.5
|
||||||
default_mobject_to_mobject_buff: 0.25
|
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:
|
colors:
|
||||||
blue_e: "#1C758A"
|
blue_e: "#1C758A"
|
||||||
blue_d: "#29ABCA"
|
blue_d: "#29ABCA"
|
||||||
|
|
|
@ -15,6 +15,7 @@ from typing import TYPE_CHECKING
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
Module = importlib.util.types.ModuleType
|
Module = importlib.util.types.ModuleType
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from addict import Dict
|
||||||
|
|
||||||
|
|
||||||
class BlankScene(InteractiveScene):
|
class BlankScene(InteractiveScene):
|
||||||
|
@ -43,11 +44,7 @@ def prompt_user_for_choice(scene_classes):
|
||||||
print(f"{str(idx).zfill(max_digits)}: {name}")
|
print(f"{str(idx).zfill(max_digits)}: {name}")
|
||||||
name_to_class[name] = scene_class
|
name_to_class[name] = scene_class
|
||||||
try:
|
try:
|
||||||
user_input = input(
|
user_input = input("\nSelect which scene to render (by name or number): ")
|
||||||
"\nThat module has multiple scenes, " + \
|
|
||||||
"which ones would you like to render?" + \
|
|
||||||
"\nScene Name or Number: "
|
|
||||||
)
|
|
||||||
return [
|
return [
|
||||||
name_to_class[split_str] if not split_str.isnumeric() else scene_classes[int(split_str) - 1]
|
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(",")
|
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)
|
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
|
fw_config = manim_config.file_writer
|
||||||
if fw_config.write_to_movie and run_config.prerun:
|
if fw_config.write_to_movie and run_config.prerun:
|
||||||
scene_config.file_writer_config.total_frames = compute_total_frames(scene_class, scene_config)
|
scene_config.file_writer_config.total_frames = compute_total_frames(scene_class, scene_config)
|
||||||
return scene_class(**scene_config)
|
return scene_class(**scene_config)
|
||||||
|
|
||||||
|
|
||||||
def get_scenes_to_render(all_scene_classes, scene_config, run_config):
|
def note_missing_scenes(arg_names, module_names):
|
||||||
if run_config["write_all"]:
|
for name in arg_names:
|
||||||
return [sc(**scene_config) for sc in all_scene_classes]
|
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):
|
def get_scenes_to_render(all_scene_classes: list, scene_config: Dict, run_config: Dict):
|
||||||
log.error(f"No scene named {name} found")
|
if run_config["write_all"] or len(all_scene_classes) == 1:
|
||||||
scene_names.remove(name)
|
classes_to_run = all_scene_classes
|
||||||
|
|
||||||
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]]
|
|
||||||
else:
|
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)
|
classes_to_run = prompt_user_for_choice(all_scene_classes)
|
||||||
|
|
||||||
return [
|
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"):
|
if hasattr(module, "SCENES_IN_ORDER"):
|
||||||
return module.SCENES_IN_ORDER
|
return module.SCENES_IN_ORDER
|
||||||
else:
|
else:
|
||||||
|
@ -162,24 +162,16 @@ def insert_embed_line_to_module(module: Module, line_number: int):
|
||||||
exec(code_object, module.__dict__)
|
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)
|
module = ModuleLoader.get_module(file_name, is_reload)
|
||||||
if embed_line:
|
if embed_line:
|
||||||
insert_embed_line_to_module(module, embed_line)
|
insert_embed_line_to_module(module, embed_line)
|
||||||
return module
|
return module
|
||||||
|
|
||||||
|
|
||||||
def main(scene_config, run_config):
|
def main(scene_config: Dict, run_config: Dict):
|
||||||
module = get_scene_module(
|
module = get_module(run_config.file_name, run_config.embed_line, run_config.is_reload)
|
||||||
run_config["file_name"],
|
all_scene_classes = get_scene_classes(module)
|
||||||
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)
|
|
||||||
scenes = get_scenes_to_render(all_scene_classes, scene_config, run_config)
|
scenes = get_scenes_to_render(all_scene_classes, scene_config, run_config)
|
||||||
if len(scenes) == 0:
|
if len(scenes) == 0:
|
||||||
print("No scenes found to run")
|
print("No scenes found to run")
|
||||||
|
|
|
@ -7,7 +7,7 @@ import numpy as np
|
||||||
import itertools as it
|
import itertools as it
|
||||||
|
|
||||||
from manimlib.constants import BLACK, BLUE, BLUE_D, BLUE_E, GREEN, GREY_A, WHITE, RED
|
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 DL, UL, DOWN, DR, LEFT, ORIGIN, OUT, RIGHT, UP
|
||||||
from manimlib.constants import FRAME_X_RADIUS, FRAME_Y_RADIUS
|
from manimlib.constants import FRAME_X_RADIUS, FRAME_Y_RADIUS
|
||||||
from manimlib.constants import MED_SMALL_BUFF, SMALL_BUFF
|
from manimlib.constants import MED_SMALL_BUFF, SMALL_BUFF
|
||||||
|
@ -307,7 +307,7 @@ class CoordinateSystem(ABC):
|
||||||
|
|
||||||
point = self.input_to_graph_point(x, graph)
|
point = self.input_to_graph_point(x, graph)
|
||||||
angle = self.angle_of_tangent(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:
|
if normal[1] < 0:
|
||||||
normal *= -1
|
normal *= -1
|
||||||
label.next_to(point, normal, buff=buff)
|
label.next_to(point, normal, buff=buff)
|
||||||
|
@ -474,7 +474,7 @@ class Axes(VGroup, CoordinateSystem):
|
||||||
),
|
),
|
||||||
length=height,
|
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
|
# Add as a separate group in case various other
|
||||||
# mobjects are added to self, as for example in
|
# mobjects are added to self, as for example in
|
||||||
# NumberPlane below
|
# 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 DL, DOWN, DR, LEFT, ORIGIN, OUT, RIGHT, UL, UP, UR
|
||||||
from manimlib.constants import GREY_A, RED, WHITE, BLACK
|
from manimlib.constants import GREY_A, RED, WHITE, BLACK
|
||||||
from manimlib.constants import MED_SMALL_BUFF, SMALL_BUFF
|
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.mobject import Mobject
|
||||||
from manimlib.mobject.types.vectorized_mobject import DashedVMobject
|
from manimlib.mobject.types.vectorized_mobject import DashedVMobject
|
||||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||||
|
@ -983,7 +983,7 @@ class RegularPolygon(Polygon):
|
||||||
):
|
):
|
||||||
# Defaults to 0 for odd, 90 for even
|
# Defaults to 0 for odd, 90 for even
|
||||||
if start_angle is None:
|
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)
|
start_vect = rotate_vector(radius * RIGHT, start_angle)
|
||||||
vertices = compass_directions(n, start_vect)
|
vertices = compass_directions(n, start_vect)
|
||||||
super().__init__(*vertices, **kwargs)
|
super().__init__(*vertices, **kwargs)
|
||||||
|
|
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from manimlib.constants import DOWN, LEFT, RIGHT, ORIGIN
|
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.numbers import DecimalNumber
|
||||||
from manimlib.mobject.svg.tex_mobject import Tex
|
from manimlib.mobject.svg.tex_mobject import Tex
|
||||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||||
|
@ -196,7 +196,7 @@ class Matrix(VMobject):
|
||||||
dots.set_width(hdots_width)
|
dots.set_width(hdots_width)
|
||||||
self.swap_entry_for_dots(row[col_index], dots)
|
self.swap_entry_for_dots(row[col_index], dots)
|
||||||
if use_vdots and use_hdots:
|
if use_vdots and use_hdots:
|
||||||
rows[row_index][col_index].rotate(-45 * DEGREES)
|
rows[row_index][col_index].rotate(-45 * DEG)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def get_mob_matrix(self) -> VMobjectMatrixType:
|
def get_mob_matrix(self) -> VMobjectMatrixType:
|
||||||
|
|
|
@ -2,7 +2,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
|
|
||||||
from manimlib.constants import DEGREES
|
from manimlib.constants import DEG
|
||||||
from manimlib.constants import RIGHT
|
from manimlib.constants import RIGHT
|
||||||
from manimlib.mobject.mobject import Mobject
|
from manimlib.mobject.mobject import Mobject
|
||||||
from manimlib.utils.simple_functions import clip
|
from manimlib.utils.simple_functions import clip
|
||||||
|
@ -71,7 +71,7 @@ def always_shift(
|
||||||
|
|
||||||
def always_rotate(
|
def always_rotate(
|
||||||
mobject: Mobject,
|
mobject: Mobject,
|
||||||
rate: float = 20 * DEGREES,
|
rate: float = 20 * DEG,
|
||||||
**kwargs
|
**kwargs
|
||||||
) -> Mobject:
|
) -> Mobject:
|
||||||
mobject.add_updater(
|
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.cache import cache_on_disk
|
||||||
from manimlib.utils.color import color_to_hex
|
from manimlib.utils.color import color_to_hex
|
||||||
from manimlib.utils.color import int_to_hex
|
from manimlib.utils.color import int_to_hex
|
||||||
|
from manimlib.utils.simple_functions import hash_string
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
@ -74,34 +75,29 @@ def markup_to_svg(
|
||||||
pango_width = line_width / FRAME_WIDTH * DEFAULT_PIXEL_WIDTH
|
pango_width = line_width / FRAME_WIDTH * DEFAULT_PIXEL_WIDTH
|
||||||
|
|
||||||
# Write the result to a temporary svg file, and return it's contents.
|
# 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
|
temp_file = Path(tempfile.gettempdir(), hash_string(markup_str)).with_suffix(".svg")
|
||||||
#
|
manimpango.MarkupUtils.text2svg(
|
||||||
# To avoid CAIRO_STATUS_WRITE_ERROR: b'error while writing to
|
text=markup_str,
|
||||||
# output stream' on Windows, we need to pass 'delete=False'.
|
font="", # Already handled
|
||||||
with tempfile.NamedTemporaryFile(suffix='.svg', mode='r+', delete=False) as tmp:
|
slant="NORMAL", # Already handled
|
||||||
manimpango.MarkupUtils.text2svg(
|
weight="NORMAL", # Already handled
|
||||||
text=markup_str,
|
size=1, # Already handled
|
||||||
font="", # Already handled
|
_=0, # Empty parameter
|
||||||
slant="NORMAL", # Already handled
|
disable_liga=False,
|
||||||
weight="NORMAL", # Already handled
|
file_name=str(temp_file),
|
||||||
size=1, # Already handled
|
START_X=0,
|
||||||
_=0, # Empty parameter
|
START_Y=0,
|
||||||
disable_liga=False,
|
width=DEFAULT_CANVAS_WIDTH,
|
||||||
file_name=tmp.name,
|
height=DEFAULT_CANVAS_HEIGHT,
|
||||||
START_X=0,
|
justify=justify,
|
||||||
START_Y=0,
|
indent=indent,
|
||||||
width=DEFAULT_CANVAS_WIDTH,
|
line_spacing=None, # Already handled
|
||||||
height=DEFAULT_CANVAS_HEIGHT,
|
alignment=alignment,
|
||||||
justify=justify,
|
pango_width=pango_width
|
||||||
indent=indent,
|
)
|
||||||
line_spacing=None, # Already handled
|
result = temp_file.read_text()
|
||||||
alignment=alignment,
|
os.remove(temp_file)
|
||||||
pango_width=pango_width
|
return result
|
||||||
)
|
|
||||||
|
|
||||||
# Read the contents
|
|
||||||
tmp.seek(0)
|
|
||||||
return tmp.read()
|
|
||||||
|
|
||||||
|
|
||||||
class MarkupText(StringMobject):
|
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 GREY_A, GREY_C, GREY_E
|
||||||
from manimlib.constants import BLACK
|
from manimlib.constants import BLACK
|
||||||
from manimlib.constants import DEFAULT_STROKE_WIDTH
|
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 ORIGIN, OUT
|
||||||
from manimlib.constants import PI
|
from manimlib.constants import PI
|
||||||
from manimlib.constants import TAU
|
from manimlib.constants import TAU
|
||||||
|
@ -490,7 +490,7 @@ class VMobject(Mobject):
|
||||||
v1 = handle1 - last
|
v1 = handle1 - last
|
||||||
v2 = anchor - handle2
|
v2 = anchor - handle2
|
||||||
angle = angle_between_vectors(v1, v2)
|
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]
|
quad_approx = [last, find_intersection(last, v1, anchor, -v2), anchor]
|
||||||
else:
|
else:
|
||||||
quad_approx = get_quadratic_approximation_of_cubic(
|
quad_approx = get_quadratic_approximation_of_cubic(
|
||||||
|
@ -616,7 +616,7 @@ class VMobject(Mobject):
|
||||||
|
|
||||||
def subdivide_sharp_curves(
|
def subdivide_sharp_curves(
|
||||||
self,
|
self,
|
||||||
angle_threshold: float = 30 * DEGREES,
|
angle_threshold: float = 30 * DEG,
|
||||||
recurse: bool = True
|
recurse: bool = True
|
||||||
) -> Self:
|
) -> Self:
|
||||||
def tuple_to_subdivisions(b0, b1, b2):
|
def tuple_to_subdivisions(b0, b1, b2):
|
||||||
|
@ -656,7 +656,7 @@ class VMobject(Mobject):
|
||||||
self.make_smooth(approx=approx)
|
self.make_smooth(approx=approx)
|
||||||
return self
|
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])
|
angles = np.abs(self.get_joint_angles()[0::2])
|
||||||
return (angles < angle_tol).all()
|
return (angles < angle_tol).all()
|
||||||
|
|
||||||
|
|
|
@ -7,10 +7,11 @@ from IPython.core.getipython import get_ipython
|
||||||
from pyglet.window import key as PygletWindowKeys
|
from pyglet.window import key as PygletWindowKeys
|
||||||
|
|
||||||
from manimlib.animation.fading import FadeIn
|
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 DL, DOWN, DR, LEFT, ORIGIN, RIGHT, UL, UP, UR
|
||||||
from manimlib.constants import FRAME_WIDTH, FRAME_HEIGHT, SMALL_BUFF
|
from manimlib.constants import FRAME_WIDTH, FRAME_HEIGHT, SMALL_BUFF
|
||||||
from manimlib.constants import PI
|
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.constants import MANIM_COLORS, WHITE, GREY_A, GREY_C
|
||||||
from manimlib.mobject.geometry import Line
|
from manimlib.mobject.geometry import Line
|
||||||
from manimlib.mobject.geometry import Rectangle
|
from manimlib.mobject.geometry import Rectangle
|
||||||
|
@ -36,17 +37,16 @@ if TYPE_CHECKING:
|
||||||
from manimlib.typing import Vect3
|
from manimlib.typing import Vect3
|
||||||
|
|
||||||
|
|
||||||
SELECT_KEY = 's'
|
SELECT_KEY = manim_config.key_bindings.select
|
||||||
UNSELECT_KEY = 'u'
|
UNSELECT_KEY = manim_config.key_bindings.unselect
|
||||||
GRAB_KEY = 'g'
|
GRAB_KEY = manim_config.key_bindings.grab
|
||||||
X_GRAB_KEY = 'h'
|
X_GRAB_KEY = manim_config.key_bindings.x_grab
|
||||||
Y_GRAB_KEY = 'v'
|
Y_GRAB_KEY = manim_config.key_bindings.y_grab
|
||||||
GRAB_KEYS = [GRAB_KEY, X_GRAB_KEY, Y_GRAB_KEY]
|
GRAB_KEYS = [GRAB_KEY, X_GRAB_KEY, Y_GRAB_KEY]
|
||||||
RESIZE_KEY = 't'
|
RESIZE_KEY = manim_config.key_bindings.resize # TODO
|
||||||
COLOR_KEY = 'c'
|
COLOR_KEY = manim_config.key_bindings.color
|
||||||
INFORMATION_KEY = 'i'
|
INFORMATION_KEY = manim_config.key_bindings.information
|
||||||
CURSOR_KEY = 'k'
|
CURSOR_KEY = manim_config.key_bindings.cursor
|
||||||
COPY_FRAME_POSITION_KEY = 'p'
|
|
||||||
|
|
||||||
# For keyboard interactions
|
# For keyboard interactions
|
||||||
|
|
||||||
|
@ -625,7 +625,7 @@ class InteractiveScene(Scene):
|
||||||
angles = frame.get_euler_angles()
|
angles = frame.get_euler_angles()
|
||||||
|
|
||||||
call = f"reorient("
|
call = f"reorient("
|
||||||
theta, phi, gamma = (angles / DEGREES).astype(int)
|
theta, phi, gamma = (angles / DEG).astype(int)
|
||||||
call += f"{theta}, {phi}, {gamma}"
|
call += f"{theta}, {phi}, {gamma}"
|
||||||
if any(center != 0):
|
if any(center != 0):
|
||||||
call += f", {tuple(np.round(center, 2))}"
|
call += f", {tuple(np.round(center, 2))}"
|
||||||
|
|
|
@ -48,12 +48,6 @@ if TYPE_CHECKING:
|
||||||
from manimlib.animation.animation import Animation
|
from manimlib.animation.animation import Animation
|
||||||
|
|
||||||
|
|
||||||
PAN_3D_KEY = 'd'
|
|
||||||
FRAME_SHIFT_KEY = 'f'
|
|
||||||
RESET_FRAME_KEY = 'r'
|
|
||||||
QUIT_KEY = 'q'
|
|
||||||
|
|
||||||
|
|
||||||
class Scene(object):
|
class Scene(object):
|
||||||
random_seed: int = 0
|
random_seed: int = 0
|
||||||
pan_sensitivity: float = 0.5
|
pan_sensitivity: float = 0.5
|
||||||
|
@ -77,6 +71,7 @@ class Scene(object):
|
||||||
end_at_animation_number: int | None = None,
|
end_at_animation_number: int | None = None,
|
||||||
show_animation_progress: bool = False,
|
show_animation_progress: bool = False,
|
||||||
leave_progress_bars: bool = False,
|
leave_progress_bars: bool = False,
|
||||||
|
preview_while_skipping: bool = True,
|
||||||
presenter_mode: bool = False,
|
presenter_mode: bool = False,
|
||||||
default_wait_time: float = 1.0,
|
default_wait_time: float = 1.0,
|
||||||
):
|
):
|
||||||
|
@ -86,6 +81,7 @@ class Scene(object):
|
||||||
self.end_at_animation_number = end_at_animation_number
|
self.end_at_animation_number = end_at_animation_number
|
||||||
self.show_animation_progress = show_animation_progress
|
self.show_animation_progress = show_animation_progress
|
||||||
self.leave_progress_bars = leave_progress_bars
|
self.leave_progress_bars = leave_progress_bars
|
||||||
|
self.preview_while_skipping = preview_while_skipping
|
||||||
self.presenter_mode = presenter_mode
|
self.presenter_mode = presenter_mode
|
||||||
self.default_wait_time = default_wait_time
|
self.default_wait_time = default_wait_time
|
||||||
|
|
||||||
|
@ -531,7 +527,7 @@ class Scene(object):
|
||||||
if not self.skip_animations:
|
if not self.skip_animations:
|
||||||
self.file_writer.end_animation()
|
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
|
# Show some quick frames along the way
|
||||||
self.update_frame(dt=0, force_draw=True)
|
self.update_frame(dt=0, force_draw=True)
|
||||||
|
|
||||||
|
@ -746,13 +742,13 @@ class Scene(object):
|
||||||
|
|
||||||
frame = self.camera.frame
|
frame = self.camera.frame
|
||||||
# Handle perspective changes
|
# 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 = frame.to_fixed_frame_point(d_point, relative=True)
|
||||||
ff_d_point *= self.pan_sensitivity
|
ff_d_point *= self.pan_sensitivity
|
||||||
frame.increment_theta(-ff_d_point[0])
|
frame.increment_theta(-ff_d_point[0])
|
||||||
frame.increment_phi(ff_d_point[1])
|
frame.increment_phi(ff_d_point[1])
|
||||||
# Handle frame movements
|
# 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)
|
frame.shift(-d_point)
|
||||||
|
|
||||||
def on_mouse_drag(
|
def on_mouse_drag(
|
||||||
|
@ -838,14 +834,14 @@ class Scene(object):
|
||||||
if propagate_event is not None and propagate_event is False:
|
if propagate_event is not None and propagate_event is False:
|
||||||
return
|
return
|
||||||
|
|
||||||
if char == RESET_FRAME_KEY:
|
if char == manim_config.key_bindings.reset:
|
||||||
self.play(self.camera.frame.animate.to_default_state())
|
self.play(self.camera.frame.animate.to_default_state())
|
||||||
elif char == "z" and (modifiers & (PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_CTRL)):
|
elif char == "z" and (modifiers & (PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_CTRL)):
|
||||||
self.undo()
|
self.undo()
|
||||||
elif char == "z" and (modifiers & (PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_CTRL | PygletWindowKeys.MOD_SHIFT)):
|
elif char == "z" and (modifiers & (PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_CTRL | PygletWindowKeys.MOD_SHIFT)):
|
||||||
self.redo()
|
self.redo()
|
||||||
# command + q
|
# 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
|
self.quit_interaction = True
|
||||||
# Space or right arrow
|
# Space or right arrow
|
||||||
elif char == " " or symbol == PygletWindowKeys.RIGHT:
|
elif char == " " or symbol == PygletWindowKeys.RIGHT:
|
||||||
|
|
|
@ -102,7 +102,7 @@ class InteractiveSceneEmbed:
|
||||||
"""Flash border, and potentially play sound, on exceptions"""
|
"""Flash border, and potentially play sound, on exceptions"""
|
||||||
def custom_exc(shell, etype, evalue, tb, tb_offset=None):
|
def custom_exc(shell, etype, evalue, tb, tb_offset=None):
|
||||||
# Show the error don't just swallow it
|
# 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 = FullScreenRectangle().set_stroke(RED, 30).set_fill(opacity=0)
|
||||||
rect.fix_in_frame()
|
rect.fix_in_frame()
|
||||||
self.scene.play(VFadeInThenOut(rect, run_time=0.5))
|
self.scene.play(VFadeInThenOut(rect, run_time=0.5))
|
||||||
|
|
|
@ -13,8 +13,6 @@ from pathlib import Path
|
||||||
|
|
||||||
from manimlib.logger import log
|
from manimlib.logger import log
|
||||||
from manimlib.mobject.mobject import Mobject
|
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.file_ops import guarantee_existence
|
||||||
from manimlib.utils.sounds import get_full_sound_file_path
|
from manimlib.utils.sounds import get_full_sound_file_path
|
||||||
|
|
||||||
|
@ -32,17 +30,15 @@ class SceneFileWriter(object):
|
||||||
self,
|
self,
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
write_to_movie: bool = False,
|
write_to_movie: bool = False,
|
||||||
break_into_partial_movies: bool = False,
|
subdivide_output: bool = False,
|
||||||
save_pngs: bool = False, # TODO, this currently does nothing
|
|
||||||
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
|
# What python file is generating this scene
|
||||||
input_file_path: str = "",
|
input_file_path: str = "",
|
||||||
# Where should this be written
|
# Where should this be written
|
||||||
output_directory: str | None = None,
|
output_directory: str = "",
|
||||||
file_name: str | None = None,
|
file_name: str | None = None,
|
||||||
subdirectory_for_videos: bool = False,
|
|
||||||
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,
|
||||||
quiet: bool = False,
|
quiet: bool = False,
|
||||||
|
@ -57,8 +53,7 @@ class SceneFileWriter(object):
|
||||||
):
|
):
|
||||||
self.scene: Scene = scene
|
self.scene: Scene = scene
|
||||||
self.write_to_movie = write_to_movie
|
self.write_to_movie = write_to_movie
|
||||||
self.break_into_partial_movies = break_into_partial_movies
|
self.subdivide_output = subdivide_output
|
||||||
self.save_pngs = save_pngs
|
|
||||||
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
|
||||||
|
@ -66,7 +61,6 @@ class SceneFileWriter(object):
|
||||||
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
|
||||||
self.subdirectory_for_videos = subdirectory_for_videos
|
|
||||||
self.show_file_location_upon_completion = show_file_location_upon_completion
|
self.show_file_location_upon_completion = show_file_location_upon_completion
|
||||||
self.quiet = quiet
|
self.quiet = quiet
|
||||||
self.total_frames = total_frames
|
self.total_frames = total_frames
|
||||||
|
@ -81,40 +75,39 @@ class SceneFileWriter(object):
|
||||||
self.writing_process: sp.Popen | None = None
|
self.writing_process: sp.Popen | None = None
|
||||||
self.progress_display: ProgressDisplay | None = None
|
self.progress_display: ProgressDisplay | None = None
|
||||||
self.ended_with_interrupt: bool = False
|
self.ended_with_interrupt: bool = False
|
||||||
|
|
||||||
self.init_output_directories()
|
self.init_output_directories()
|
||||||
self.init_audio()
|
self.init_audio()
|
||||||
|
|
||||||
# Output directories and files
|
# Output directories and files
|
||||||
def init_output_directories(self) -> None:
|
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:
|
if self.save_last_frame:
|
||||||
image_dir = guarantee_existence(os.path.join(out_dir, "images"))
|
self.image_file_path = self.init_image_file_path()
|
||||||
image_file = add_extension_if_not_present(scene_name, ".png")
|
|
||||||
self.image_file_path = os.path.join(image_dir, image_file)
|
|
||||||
if self.write_to_movie:
|
if self.write_to_movie:
|
||||||
if self.subdirectory_for_videos:
|
self.movie_file_path = self.init_movie_file_path()
|
||||||
movie_dir = guarantee_existence(os.path.join(out_dir, "videos"))
|
if self.subdivide_output:
|
||||||
else:
|
self.partial_movie_directory = self.init_partial_movie_directory()
|
||||||
movie_dir = guarantee_existence(out_dir)
|
|
||||||
movie_file = add_extension_if_not_present(scene_name, self.movie_file_extension)
|
def init_image_file_path(self) -> Path:
|
||||||
self.movie_file_path = os.path.join(movie_dir, movie_file)
|
return self.get_output_file_rootname().with_suffix(".png")
|
||||||
if self.break_into_partial_movies:
|
|
||||||
self.partial_movie_directory = guarantee_existence(os.path.join(
|
def init_movie_file_path(self) -> Path:
|
||||||
movie_dir, "partial_movie_files", scene_name,
|
return self.get_output_file_rootname().with_suffix(self.movie_file_extension)
|
||||||
))
|
|
||||||
# A place to save mobjects
|
def init_partial_movie_directory(self):
|
||||||
self.saved_mobject_directory = os.path.join(
|
return guarantee_existence(self.get_output_file_rootname())
|
||||||
out_dir, "mobjects", str(self.scene)
|
|
||||||
|
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:
|
def get_output_file_name(self) -> str:
|
||||||
path, _ = os.path.splitext(self.input_file_path)
|
if self.file_name:
|
||||||
if path.startswith("_"):
|
return self.file_name
|
||||||
path = path[1:]
|
# Otherwise, use the name of the scene, potentially
|
||||||
return path
|
# appending animation numbers
|
||||||
|
|
||||||
def get_default_scene_name(self) -> str:
|
|
||||||
name = str(self.scene)
|
name = str(self.scene)
|
||||||
saan = self.scene.start_at_animation_number
|
saan = self.scene.start_at_animation_number
|
||||||
eaan = self.scene.end_at_animation_number
|
eaan = self.scene.end_at_animation_number
|
||||||
|
@ -124,26 +117,13 @@ class SceneFileWriter(object):
|
||||||
name += f"_{eaan}"
|
name += f"_{eaan}"
|
||||||
return name
|
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
|
# Directory getters
|
||||||
def get_image_file_path(self) -> str:
|
def get_image_file_path(self) -> str:
|
||||||
return self.image_file_path
|
return self.image_file_path
|
||||||
|
|
||||||
def get_next_partial_movie_path(self) -> str:
|
def get_next_partial_movie_path(self) -> str:
|
||||||
result = os.path.join(
|
result = Path(self.partial_movie_directory, f"{self.scene.num_plays:05}")
|
||||||
self.partial_movie_directory,
|
return result.with_suffix(self.movie_file_extension)
|
||||||
"{:05}{}".format(
|
|
||||||
self.scene.num_plays,
|
|
||||||
self.movie_file_extension,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return result
|
|
||||||
|
|
||||||
def get_movie_file_path(self) -> str:
|
def get_movie_file_path(self) -> str:
|
||||||
return self.movie_file_path
|
return self.movie_file_path
|
||||||
|
@ -199,23 +179,20 @@ class SceneFileWriter(object):
|
||||||
|
|
||||||
# Writers
|
# Writers
|
||||||
def begin(self) -> None:
|
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())
|
self.open_movie_pipe(self.get_movie_file_path())
|
||||||
|
|
||||||
def begin_animation(self) -> None:
|
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())
|
self.open_movie_pipe(self.get_next_partial_movie_path())
|
||||||
|
|
||||||
def end_animation(self) -> None:
|
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()
|
self.close_movie_pipe()
|
||||||
|
|
||||||
def finish(self) -> None:
|
def finish(self) -> None:
|
||||||
if self.write_to_movie:
|
if not self.subdivide_output and self.write_to_movie:
|
||||||
if self.break_into_partial_movies:
|
self.close_movie_pipe()
|
||||||
self.combine_movie_files()
|
|
||||||
else:
|
|
||||||
self.close_movie_pipe()
|
|
||||||
if self.includes_sound:
|
if self.includes_sound:
|
||||||
self.add_sound_to_video()
|
self.add_sound_to_video()
|
||||||
self.print_file_ready_message(self.get_movie_file_path())
|
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()
|
width, height = self.scene.camera.get_pixel_shape()
|
||||||
|
|
||||||
vf_arg = 'vflip'
|
vf_arg = 'vflip'
|
||||||
# if self.pixel_format.startswith("yuv"):
|
|
||||||
vf_arg += f',eq=saturation={self.saturation}:gamma={self.gamma}'
|
vf_arg += f',eq=saturation={self.saturation}:gamma={self.gamma}'
|
||||||
|
|
||||||
command = [
|
command = [
|
||||||
|
@ -246,7 +222,7 @@ class SceneFileWriter(object):
|
||||||
'-r', str(fps), # frames per second
|
'-r', str(fps), # frames per second
|
||||||
'-i', '-', # The input comes from a pipe
|
'-i', '-', # The input comes from a pipe
|
||||||
'-vf', vf_arg,
|
'-vf', vf_arg,
|
||||||
'-an', # Tells FFMPEG not to expect any audio
|
'-an', # Tells ffmpeg not to expect any audio
|
||||||
'-loglevel', 'error',
|
'-loglevel', 'error',
|
||||||
]
|
]
|
||||||
if self.video_codec:
|
if self.video_codec:
|
||||||
|
@ -273,8 +249,8 @@ class SceneFileWriter(object):
|
||||||
movie_path = Path(self.get_movie_file_path())
|
movie_path = Path(self.get_movie_file_path())
|
||||||
scene_name = movie_path.stem
|
scene_name = movie_path.stem
|
||||||
insert_dir = Path(movie_path.parent, "inserts")
|
insert_dir = Path(movie_path.parent, "inserts")
|
||||||
guarantee_existence(str(insert_dir))
|
guarantee_existence(insert_dir)
|
||||||
return Path(insert_dir, f"{scene_name}_{index}{movie_path.suffix}")
|
return Path(insert_dir, f"{scene_name}_{index}").with_suffix(self.movie_file_extension)
|
||||||
|
|
||||||
def begin_insert(self):
|
def begin_insert(self):
|
||||||
# Begin writing process
|
# Begin writing process
|
||||||
|
@ -283,7 +259,7 @@ class SceneFileWriter(object):
|
||||||
index = 0
|
index = 0
|
||||||
while (insert_path := self.get_insert_file_path(index)).exists():
|
while (insert_path := self.get_insert_file_path(index)).exists():
|
||||||
index += 1
|
index += 1
|
||||||
self.inserted_file_path = str(insert_path)
|
self.inserted_file_path = insert_path
|
||||||
self.open_movie_pipe(self.inserted_file_path)
|
self.open_movie_pipe(self.inserted_file_path)
|
||||||
|
|
||||||
def end_insert(self):
|
def end_insert(self):
|
||||||
|
@ -327,54 +303,6 @@ class SceneFileWriter(object):
|
||||||
else:
|
else:
|
||||||
self.movie_file_path = self.temp_file_path
|
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:
|
def add_sound_to_video(self) -> None:
|
||||||
movie_file_path = self.get_movie_file_path()
|
movie_file_path = self.get_movie_file_path()
|
||||||
stem, ext = os.path.splitext(movie_file_path)
|
stem, ext = os.path.splitext(movie_file_path)
|
||||||
|
|
|
@ -20,29 +20,3 @@ def merge_dicts_recursively(*dicts):
|
||||||
else:
|
else:
|
||||||
result[key] = value
|
result[key] = value
|
||||||
return result
|
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
|
from typing import Iterable
|
||||||
|
|
||||||
|
|
||||||
def add_extension_if_not_present(file_name: str, extension: str) -> str:
|
def guarantee_existence(path: str | Path) -> Path:
|
||||||
# This could conceivably be smarter about handling existing differing extensions
|
path = Path(path)
|
||||||
if(file_name[-len(extension):] != extension):
|
path.mkdir(parents=True, exist_ok=True)
|
||||||
return file_name + extension
|
return path.absolute()
|
||||||
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 find_file(
|
def find_file(
|
||||||
file_name: str,
|
file_name: str,
|
||||||
directories: Iterable[str] | None = None,
|
directories: Iterable[str] | None = None,
|
||||||
extensions: Iterable[str] | None = None
|
extensions: Iterable[str] | None = None
|
||||||
) -> str:
|
) -> Path:
|
||||||
# Check if this is a file online first, and if so, download
|
# Check if this is a file online first, and if so, download
|
||||||
# it to a temporary directory
|
# it to a temporary directory
|
||||||
if validators.url(file_name):
|
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
|
# Check if what was passed in is already a valid path to a file
|
||||||
if os.path.exists(file_name):
|
if os.path.exists(file_name):
|
||||||
return file_name
|
return Path(file_name)
|
||||||
|
|
||||||
# Otherwise look in local file system
|
# Otherwise look in local file system
|
||||||
directories = directories or [""]
|
directories = directories or [""]
|
||||||
extensions = extensions or [""]
|
extensions = extensions or [""]
|
||||||
possible_paths = (
|
possible_paths = (
|
||||||
os.path.join(directory, file_name + extension)
|
Path(directory, file_name + extension)
|
||||||
for directory in directories
|
for directory in directories
|
||||||
for extension in extensions
|
for extension in extensions
|
||||||
)
|
)
|
||||||
for path in possible_paths:
|
for path in possible_paths:
|
||||||
if os.path.exists(path):
|
if path.exists():
|
||||||
return path
|
return path
|
||||||
raise IOError(f"{file_name} not Found")
|
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]:
|
def get_parameters(function: Callable) -> Iterable[str]:
|
||||||
return inspect.signature(function).parameters.keys()
|
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:
|
def clip(a: float, min_a: float, max_a: float) -> float:
|
||||||
if a < min_a:
|
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:
|
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:
|
if zero_over_zero_value is not None:
|
||||||
out = np.full_like(a, zero_over_zero_value)
|
out = np.full_like(a, zero_over_zero_value)
|
||||||
where = np.logical_or(a != 0, b != 0)
|
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)
|
return np.true_divide(a, b, out=out, where=where)
|
||||||
|
|
||||||
|
|
||||||
def binary_search(function: Callable[[float], float],
|
def binary_search(
|
||||||
target: float,
|
function: Callable[[float], float],
|
||||||
lower_bound: float,
|
target: float,
|
||||||
upper_bound: float,
|
lower_bound: float,
|
||||||
tolerance:float = 1e-4) -> float | None:
|
upper_bound: float,
|
||||||
|
tolerance:float = 1e-4
|
||||||
|
) -> float | None:
|
||||||
lh = lower_bound
|
lh = lower_bound
|
||||||
rh = upper_bound
|
rh = upper_bound
|
||||||
mh = (lh + rh) / 2
|
mh = (lh + rh) / 2
|
||||||
|
|
|
@ -1,40 +1,38 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
from functools import lru_cache
|
||||||
|
|
||||||
from manimlib.utils.tex_to_symbol_count import TEX_TO_SYMBOL_COUNT
|
from manimlib.utils.tex_to_symbol_count import TEX_TO_SYMBOL_COUNT
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache
|
||||||
def num_tex_symbols(tex: str) -> int:
|
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
|
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]
|
if match.group("sqrt"):
|
||||||
for substr in re.findall(r"\\sqrt\[[0-9]+\]", tex):
|
total += len(match.group()) - 5
|
||||||
total += len(substr) - 5 # e.g. \sqrt[3] is 3 symbols
|
else:
|
||||||
tex = tex.replace(substr, " ")
|
total += TEX_TO_SYMBOL_COUNT.get(match.group(), 1)
|
||||||
|
pos = match.end()
|
||||||
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, " ")
|
|
||||||
|
|
||||||
# Count remaining characters
|
# 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
|
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