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