mirror of
https://github.com/3b1b/manim.git
synced 2025-04-13 09:47:07 +00:00
commit
485a4ca33a
19 changed files with 688 additions and 159 deletions
|
@ -579,7 +579,7 @@ class SurfaceExample(Scene):
|
||||||
self.play(light.animate.move_to(3 * IN), run_time=5)
|
self.play(light.animate.move_to(3 * IN), run_time=5)
|
||||||
self.play(light.animate.shift(10 * OUT), run_time=5)
|
self.play(light.animate.shift(10 * OUT), run_time=5)
|
||||||
|
|
||||||
drag_text = Text("Try moving the mouse while pressing d or s")
|
drag_text = Text("Try moving the mouse while pressing d or f")
|
||||||
drag_text.move_to(light_text)
|
drag_text.move_to(light_text)
|
||||||
drag_text.fix_in_frame()
|
drag_text.fix_in_frame()
|
||||||
|
|
||||||
|
|
|
@ -53,6 +53,7 @@ from manimlib.mobject.value_tracker import *
|
||||||
from manimlib.mobject.vector_field import *
|
from manimlib.mobject.vector_field import *
|
||||||
|
|
||||||
from manimlib.scene.scene import *
|
from manimlib.scene.scene import *
|
||||||
|
from manimlib.scene.interactive_scene import *
|
||||||
from manimlib.scene.three_d_scene import *
|
from manimlib.scene.three_d_scene import *
|
||||||
|
|
||||||
from manimlib.utils.bezier import *
|
from manimlib.utils.bezier import *
|
||||||
|
|
|
@ -52,6 +52,7 @@ class Animation(object):
|
||||||
# played. As much initialization as possible,
|
# played. As much initialization as possible,
|
||||||
# especially any mobject copying, should live in
|
# especially any mobject copying, should live in
|
||||||
# this method
|
# this method
|
||||||
|
self.mobject.set_animating_status(True)
|
||||||
self.starting_mobject = self.create_starting_mobject()
|
self.starting_mobject = self.create_starting_mobject()
|
||||||
if self.suspend_mobject_updating:
|
if self.suspend_mobject_updating:
|
||||||
# All calls to self.mobject's internal updaters
|
# All calls to self.mobject's internal updaters
|
||||||
|
@ -66,6 +67,7 @@ class Animation(object):
|
||||||
|
|
||||||
def finish(self) -> None:
|
def finish(self) -> None:
|
||||||
self.interpolate(self.final_alpha_value)
|
self.interpolate(self.final_alpha_value)
|
||||||
|
self.mobject.set_animating_status(False)
|
||||||
if self.suspend_mobject_updating:
|
if self.suspend_mobject_updating:
|
||||||
self.mobject.resume_updating()
|
self.mobject.resume_updating()
|
||||||
|
|
||||||
|
|
|
@ -50,11 +50,13 @@ class AnimationGroup(Animation):
|
||||||
return self.group
|
return self.group
|
||||||
|
|
||||||
def begin(self) -> None:
|
def begin(self) -> None:
|
||||||
|
self.group.set_animating_status(True)
|
||||||
for anim in self.animations:
|
for anim in self.animations:
|
||||||
anim.begin()
|
anim.begin()
|
||||||
# self.init_run_time()
|
# self.init_run_time()
|
||||||
|
|
||||||
def finish(self) -> None:
|
def finish(self) -> None:
|
||||||
|
self.group.set_animating_status(False)
|
||||||
for anim in self.animations:
|
for anim in self.animations:
|
||||||
anim.finish()
|
anim.finish()
|
||||||
|
|
||||||
|
|
|
@ -60,6 +60,15 @@ class CameraFrame(Mobject):
|
||||||
def get_euler_angles(self):
|
def get_euler_angles(self):
|
||||||
return self.get_orientation().as_euler("zxz")[::-1]
|
return self.get_orientation().as_euler("zxz")[::-1]
|
||||||
|
|
||||||
|
def get_theta(self):
|
||||||
|
return self.get_euler_angles()[0]
|
||||||
|
|
||||||
|
def get_phi(self):
|
||||||
|
return self.get_euler_angles()[1]
|
||||||
|
|
||||||
|
def get_gamma(self):
|
||||||
|
return self.get_euler_angles()[2]
|
||||||
|
|
||||||
def get_inverse_camera_rotation_matrix(self):
|
def get_inverse_camera_rotation_matrix(self):
|
||||||
return self.get_orientation().as_matrix().T
|
return self.get_orientation().as_matrix().T
|
||||||
|
|
||||||
|
@ -190,7 +199,10 @@ class Camera(object):
|
||||||
self.init_textures()
|
self.init_textures()
|
||||||
self.init_light_source()
|
self.init_light_source()
|
||||||
self.refresh_perspective_uniforms()
|
self.refresh_perspective_uniforms()
|
||||||
self.static_mobject_to_render_group_list = {}
|
# A cached map from mobjects to their associated list of render groups
|
||||||
|
# so that these render groups are not regenerated unnecessarily for static
|
||||||
|
# mobjects
|
||||||
|
self.mob_to_render_groups = {}
|
||||||
|
|
||||||
def init_frame(self) -> None:
|
def init_frame(self) -> None:
|
||||||
self.frame = CameraFrame(**self.frame_config)
|
self.frame = CameraFrame(**self.frame_config)
|
||||||
|
@ -356,11 +368,21 @@ class Camera(object):
|
||||||
if render_group["single_use"]:
|
if render_group["single_use"]:
|
||||||
self.release_render_group(render_group)
|
self.release_render_group(render_group)
|
||||||
|
|
||||||
def get_render_group_list(self, mobject: Mobject) -> list[dict[str]] | map[dict[str]]:
|
def get_render_group_list(self, mobject: Mobject) -> Iterable[dict[str]]:
|
||||||
try:
|
if mobject.is_changing():
|
||||||
return self.static_mobject_to_render_group_list[id(mobject)]
|
return self.generate_render_group_list(mobject)
|
||||||
except KeyError:
|
|
||||||
return map(self.get_render_group, mobject.get_shader_wrapper_list())
|
# Otherwise, cache result for later use
|
||||||
|
key = id(mobject)
|
||||||
|
if key not in self.mob_to_render_groups:
|
||||||
|
self.mob_to_render_groups[key] = list(self.generate_render_group_list(mobject))
|
||||||
|
return self.mob_to_render_groups[key]
|
||||||
|
|
||||||
|
def generate_render_group_list(self, mobject: Mobject) -> Iterable[dict[str]]:
|
||||||
|
return (
|
||||||
|
self.get_render_group(sw, single_use=mobject.is_changing())
|
||||||
|
for sw in mobject.get_shader_wrapper_list()
|
||||||
|
)
|
||||||
|
|
||||||
def get_render_group(
|
def get_render_group(
|
||||||
self,
|
self,
|
||||||
|
@ -399,19 +421,10 @@ class Camera(object):
|
||||||
if render_group[key] is not None:
|
if render_group[key] is not None:
|
||||||
render_group[key].release()
|
render_group[key].release()
|
||||||
|
|
||||||
def set_mobjects_as_static(self, *mobjects: Mobject) -> None:
|
def refresh_static_mobjects(self) -> None:
|
||||||
# Creates buffer and array objects holding each mobjects shader data
|
for render_group in it.chain(*self.mob_to_render_groups.values()):
|
||||||
for mob in mobjects:
|
self.release_render_group(render_group)
|
||||||
self.static_mobject_to_render_group_list[id(mob)] = [
|
self.mob_to_render_groups = {}
|
||||||
self.get_render_group(sw, single_use=False)
|
|
||||||
for sw in mob.get_shader_wrapper_list()
|
|
||||||
]
|
|
||||||
|
|
||||||
def release_static_mobjects(self) -> None:
|
|
||||||
for rg_list in self.static_mobject_to_render_group_list.values():
|
|
||||||
for render_group in rg_list:
|
|
||||||
self.release_render_group(render_group)
|
|
||||||
self.static_mobject_to_render_group_list = {}
|
|
||||||
|
|
||||||
# Shaders
|
# Shaders
|
||||||
def init_shaders(self) -> None:
|
def init_shaders(self) -> None:
|
||||||
|
|
|
@ -71,6 +71,16 @@ BOLD = "BOLD"
|
||||||
|
|
||||||
DEFAULT_STROKE_WIDTH = 4
|
DEFAULT_STROKE_WIDTH = 4
|
||||||
|
|
||||||
|
# For keyboard interactions
|
||||||
|
CTRL_SYMBOL = 65508
|
||||||
|
SHIFT_SYMBOL = 65505
|
||||||
|
DELETE_SYMBOL = 65288
|
||||||
|
ARROW_SYMBOLS = list(range(65361, 65365))
|
||||||
|
|
||||||
|
SHIFT_MODIFIER = 1
|
||||||
|
CTRL_MODIFIER = 2
|
||||||
|
COMMAND_MODIFIER = 64
|
||||||
|
|
||||||
# Colors
|
# Colors
|
||||||
BLUE_E = "#1C758A"
|
BLUE_E = "#1C758A"
|
||||||
BLUE_D = "#29ABCA"
|
BLUE_D = "#29ABCA"
|
||||||
|
@ -127,6 +137,20 @@ LIGHT_PINK = "#DC75CD"
|
||||||
GREEN_SCREEN = "#00FF00"
|
GREEN_SCREEN = "#00FF00"
|
||||||
ORANGE = "#FF862F"
|
ORANGE = "#FF862F"
|
||||||
|
|
||||||
|
MANIM_COLORS = [
|
||||||
|
BLACK, GREY_E, GREY_D, GREY_C, GREY_B, GREY_A, WHITE,
|
||||||
|
BLUE_E, BLUE_D, BLUE_C, BLUE_B, BLUE_A,
|
||||||
|
TEAL_E, TEAL_D, TEAL_C, TEAL_B, TEAL_A,
|
||||||
|
GREEN_E, GREEN_D, GREEN_C, GREEN_B, GREEN_A,
|
||||||
|
YELLOW_E, YELLOW_D, YELLOW_C, YELLOW_B, YELLOW_A,
|
||||||
|
GOLD_E, GOLD_D, GOLD_C, GOLD_B, GOLD_A,
|
||||||
|
RED_E, RED_D, RED_C, RED_B, RED_A,
|
||||||
|
MAROON_E, MAROON_D, MAROON_C, MAROON_B, MAROON_A,
|
||||||
|
PURPLE_E, PURPLE_D, PURPLE_C, PURPLE_B, PURPLE_A,
|
||||||
|
GREY_BROWN, DARK_BROWN, LIGHT_BROWN,
|
||||||
|
PINK, LIGHT_PINK,
|
||||||
|
]
|
||||||
|
|
||||||
# Abbreviated names for the "median" colors
|
# Abbreviated names for the "median" colors
|
||||||
BLUE = BLUE_C
|
BLUE = BLUE_C
|
||||||
TEAL = TEAL_C
|
TEAL = TEAL_C
|
||||||
|
|
|
@ -320,8 +320,8 @@ class Circle(Arc):
|
||||||
"anchors_span_full_range": False
|
"anchors_span_full_range": False
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, start_angle: float = 0, **kwargs):
|
||||||
Arc.__init__(self, 0, TAU, **kwargs)
|
Arc.__init__(self, start_angle, TAU, **kwargs)
|
||||||
|
|
||||||
def surround(
|
def surround(
|
||||||
self,
|
self,
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import copy
|
|
||||||
import random
|
import random
|
||||||
import itertools as it
|
import itertools as it
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from typing import Iterable, Callable, Union, Sequence
|
from typing import Iterable, Callable, Union, Sequence
|
||||||
|
import pickle
|
||||||
|
import os
|
||||||
|
|
||||||
import colour
|
import colour
|
||||||
import moderngl
|
import moderngl
|
||||||
|
@ -23,7 +24,6 @@ from manimlib.utils.iterables import list_update
|
||||||
from manimlib.utils.iterables import resize_array
|
from manimlib.utils.iterables import resize_array
|
||||||
from manimlib.utils.iterables import resize_preserving_order
|
from manimlib.utils.iterables import resize_preserving_order
|
||||||
from manimlib.utils.iterables import resize_with_interpolation
|
from manimlib.utils.iterables import resize_with_interpolation
|
||||||
from manimlib.utils.iterables import make_even
|
|
||||||
from manimlib.utils.iterables import listify
|
from manimlib.utils.iterables import listify
|
||||||
from manimlib.utils.bezier import interpolate
|
from manimlib.utils.bezier import interpolate
|
||||||
from manimlib.utils.bezier import integer_interpolate
|
from manimlib.utils.bezier import integer_interpolate
|
||||||
|
@ -37,6 +37,7 @@ from manimlib.shader_wrapper import get_colormap_code
|
||||||
from manimlib.event_handler import EVENT_DISPATCHER
|
from manimlib.event_handler import EVENT_DISPATCHER
|
||||||
from manimlib.event_handler.event_listner import EventListner
|
from manimlib.event_handler.event_listner import EventListner
|
||||||
from manimlib.event_handler.event_type import EventType
|
from manimlib.event_handler.event_type import EventType
|
||||||
|
from manimlib.logger import log
|
||||||
|
|
||||||
|
|
||||||
TimeBasedUpdater = Callable[["Mobject", float], None]
|
TimeBasedUpdater = Callable[["Mobject", float], None]
|
||||||
|
@ -71,7 +72,7 @@ class Mobject(object):
|
||||||
# Must match in attributes of vert shader
|
# Must match in attributes of vert shader
|
||||||
"shader_dtype": [
|
"shader_dtype": [
|
||||||
('point', np.float32, (3,)),
|
('point', np.float32, (3,)),
|
||||||
]
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
|
@ -81,6 +82,8 @@ class Mobject(object):
|
||||||
self.family: list[Mobject] = [self]
|
self.family: list[Mobject] = [self]
|
||||||
self.locked_data_keys: set[str] = set()
|
self.locked_data_keys: set[str] = set()
|
||||||
self.needs_new_bounding_box: bool = True
|
self.needs_new_bounding_box: bool = True
|
||||||
|
self._is_animating: bool = False
|
||||||
|
self._is_movable: bool = False
|
||||||
|
|
||||||
self.init_data()
|
self.init_data()
|
||||||
self.init_uniforms()
|
self.init_uniforms()
|
||||||
|
@ -265,15 +268,30 @@ class Mobject(object):
|
||||||
parent.refresh_bounding_box()
|
parent.refresh_bounding_box()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def is_point_touching(
|
def are_points_touching(
|
||||||
self,
|
self,
|
||||||
point: np.ndarray,
|
points: np.ndarray,
|
||||||
buff: float = MED_SMALL_BUFF
|
buff: float = MED_SMALL_BUFF
|
||||||
) -> bool:
|
) -> bool:
|
||||||
bb = self.get_bounding_box()
|
bb = self.get_bounding_box()
|
||||||
mins = (bb[0] - buff)
|
mins = (bb[0] - buff)
|
||||||
maxs = (bb[2] + buff)
|
maxs = (bb[2] + buff)
|
||||||
return (point >= mins).all() and (point <= maxs).all()
|
return ((points >= mins) * (points <= maxs)).all(1)
|
||||||
|
|
||||||
|
def is_point_touching(
|
||||||
|
self,
|
||||||
|
point: np.ndarray,
|
||||||
|
buff: float = MED_SMALL_BUFF
|
||||||
|
) -> bool:
|
||||||
|
return self.are_points_touching(np.array(point, ndmin=2), buff)[0]
|
||||||
|
|
||||||
|
def is_touching(self, mobject: Mobject, buff: float = 1e-2) -> bool:
|
||||||
|
bb1 = self.get_bounding_box()
|
||||||
|
bb2 = mobject.get_bounding_box()
|
||||||
|
return not any((
|
||||||
|
(bb2[2] < bb1[0] - buff).any(), # E.g. Right of mobject is left of self's left
|
||||||
|
(bb2[0] > bb1[2] + buff).any(), # E.g. Left of mobject is right of self's right
|
||||||
|
))
|
||||||
|
|
||||||
# Family matters
|
# Family matters
|
||||||
|
|
||||||
|
@ -421,22 +439,6 @@ class Mobject(object):
|
||||||
self.center()
|
self.center()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def replicate(self, n: int) -> Group:
|
|
||||||
return self.get_group_class()(
|
|
||||||
*(self.copy() for x in range(n))
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_grid(self, n_rows: int, n_cols: int, height: float | None = None, **kwargs):
|
|
||||||
"""
|
|
||||||
Returns a new mobject containing multiple copies of this one
|
|
||||||
arranged in a grid
|
|
||||||
"""
|
|
||||||
grid = self.replicate(n_rows * n_cols)
|
|
||||||
grid.arrange_in_grid(n_rows, n_cols, **kwargs)
|
|
||||||
if height is not None:
|
|
||||||
grid.set_height(height)
|
|
||||||
return grid
|
|
||||||
|
|
||||||
def sort(
|
def sort(
|
||||||
self,
|
self,
|
||||||
point_to_num_func: Callable[[np.ndarray], float] = lambda p: p[0],
|
point_to_num_func: Callable[[np.ndarray], float] = lambda p: p[0],
|
||||||
|
@ -457,67 +459,46 @@ class Mobject(object):
|
||||||
self.assemble_family()
|
self.assemble_family()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
# Creating new Mobjects from this one
|
||||||
|
|
||||||
|
def replicate(self, n: int) -> Group:
|
||||||
|
return self.get_group_class()(
|
||||||
|
*(self.copy() for x in range(n))
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_grid(self, n_rows: int, n_cols: int, height: float | None = None, **kwargs):
|
||||||
|
"""
|
||||||
|
Returns a new mobject containing multiple copies of this one
|
||||||
|
arranged in a grid
|
||||||
|
"""
|
||||||
|
grid = self.replicate(n_rows * n_cols)
|
||||||
|
grid.arrange_in_grid(n_rows, n_cols, **kwargs)
|
||||||
|
if height is not None:
|
||||||
|
grid.set_height(height)
|
||||||
|
return grid
|
||||||
|
|
||||||
# Copying
|
# Copying
|
||||||
|
|
||||||
def copy(self):
|
def copy(self):
|
||||||
# TODO, either justify reason for shallow copy, or
|
|
||||||
# remove this redundancy everywhere
|
|
||||||
# return self.deepcopy()
|
|
||||||
|
|
||||||
parents = self.parents
|
|
||||||
self.parents = []
|
self.parents = []
|
||||||
copy_mobject = copy.copy(self)
|
return pickle.loads(pickle.dumps(self))
|
||||||
self.parents = parents
|
|
||||||
|
|
||||||
copy_mobject.data = dict(self.data)
|
|
||||||
for key in self.data:
|
|
||||||
copy_mobject.data[key] = self.data[key].copy()
|
|
||||||
|
|
||||||
copy_mobject.uniforms = dict(self.uniforms)
|
|
||||||
for key in self.uniforms:
|
|
||||||
if isinstance(self.uniforms[key], np.ndarray):
|
|
||||||
copy_mobject.uniforms[key] = self.uniforms[key].copy()
|
|
||||||
|
|
||||||
copy_mobject.submobjects = []
|
|
||||||
copy_mobject.add(*[sm.copy() for sm in self.submobjects])
|
|
||||||
copy_mobject.match_updaters(self)
|
|
||||||
|
|
||||||
copy_mobject.needs_new_bounding_box = self.needs_new_bounding_box
|
|
||||||
|
|
||||||
# Make sure any mobject or numpy array attributes are copied
|
|
||||||
family = self.get_family()
|
|
||||||
for attr, value in list(self.__dict__.items()):
|
|
||||||
if isinstance(value, Mobject) and value in family and value is not self:
|
|
||||||
setattr(copy_mobject, attr, value.copy())
|
|
||||||
if isinstance(value, np.ndarray):
|
|
||||||
setattr(copy_mobject, attr, value.copy())
|
|
||||||
if isinstance(value, ShaderWrapper):
|
|
||||||
setattr(copy_mobject, attr, value.copy())
|
|
||||||
return copy_mobject
|
|
||||||
|
|
||||||
def deepcopy(self):
|
def deepcopy(self):
|
||||||
parents = self.parents
|
# This used to be different from copy, so is now just here for backward compatibility
|
||||||
self.parents = []
|
return self.copy()
|
||||||
result = copy.deepcopy(self)
|
|
||||||
self.parents = parents
|
|
||||||
return result
|
|
||||||
|
|
||||||
def generate_target(self, use_deepcopy: bool = False):
|
def generate_target(self, use_deepcopy: bool = False):
|
||||||
|
# TODO, remove now pointless use_deepcopy arg
|
||||||
self.target = None # Prevent exponential explosion
|
self.target = None # Prevent exponential explosion
|
||||||
if use_deepcopy:
|
self.target = self.copy()
|
||||||
self.target = self.deepcopy()
|
|
||||||
else:
|
|
||||||
self.target = self.copy()
|
|
||||||
return self.target
|
return self.target
|
||||||
|
|
||||||
def save_state(self, use_deepcopy: bool = False):
|
def save_state(self, use_deepcopy: bool = False):
|
||||||
|
# TODO, remove now pointless use_deepcopy arg
|
||||||
if hasattr(self, "saved_state"):
|
if hasattr(self, "saved_state"):
|
||||||
# Prevent exponential growth of data
|
# Prevent exponential growth of data
|
||||||
self.saved_state = None
|
self.saved_state = None
|
||||||
if use_deepcopy:
|
self.saved_state = self.copy()
|
||||||
self.saved_state = self.deepcopy()
|
|
||||||
else:
|
|
||||||
self.saved_state = self.copy()
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def restore(self):
|
def restore(self):
|
||||||
|
@ -526,6 +507,27 @@ class Mobject(object):
|
||||||
self.become(self.saved_state)
|
self.become(self.saved_state)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def save_to_file(self, file_path):
|
||||||
|
if not file_path.endswith(".mob"):
|
||||||
|
file_path += ".mob"
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
cont = input(f"{file_path} already exists. Overwrite (y/n)? ")
|
||||||
|
if cont != "y":
|
||||||
|
return
|
||||||
|
with open(file_path, "wb") as fp:
|
||||||
|
pickle.dump(self, fp)
|
||||||
|
log.info(f"Saved mobject to {file_path}")
|
||||||
|
return self
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def load(file_path):
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
log.error(f"No file found at {file_path}")
|
||||||
|
sys.exit(2)
|
||||||
|
with open(file_path, "rb") as fp:
|
||||||
|
mobject = pickle.load(fp)
|
||||||
|
return mobject
|
||||||
|
|
||||||
# Updating
|
# Updating
|
||||||
|
|
||||||
def init_updaters(self):
|
def init_updaters(self):
|
||||||
|
@ -575,6 +577,8 @@ class Mobject(object):
|
||||||
updater_list.insert(index, update_function)
|
updater_list.insert(index, update_function)
|
||||||
|
|
||||||
self.refresh_has_updater_status()
|
self.refresh_has_updater_status()
|
||||||
|
for parent in self.parents:
|
||||||
|
parent.has_updaters = True
|
||||||
if call_updater:
|
if call_updater:
|
||||||
self.update(dt=0)
|
self.update(dt=0)
|
||||||
return self
|
return self
|
||||||
|
@ -589,10 +593,10 @@ class Mobject(object):
|
||||||
def clear_updaters(self, recurse: bool = True):
|
def clear_updaters(self, recurse: bool = True):
|
||||||
self.time_based_updaters = []
|
self.time_based_updaters = []
|
||||||
self.non_time_updaters = []
|
self.non_time_updaters = []
|
||||||
self.refresh_has_updater_status()
|
|
||||||
if recurse:
|
if recurse:
|
||||||
for submob in self.submobjects:
|
for submob in self.submobjects:
|
||||||
submob.clear_updaters()
|
submob.clear_updaters()
|
||||||
|
self.refresh_has_updater_status()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def match_updaters(self, mobject: Mobject):
|
def match_updaters(self, mobject: Mobject):
|
||||||
|
@ -623,6 +627,24 @@ class Mobject(object):
|
||||||
self.has_updaters = any(mob.get_updaters() for mob in self.get_family())
|
self.has_updaters = any(mob.get_updaters() for mob in self.get_family())
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
# Check if mark as static or not for camera
|
||||||
|
|
||||||
|
def is_changing(self) -> bool:
|
||||||
|
return self._is_animating or self.has_updaters or self._is_movable
|
||||||
|
|
||||||
|
def set_animating_status(self, is_animating: bool, recurse: bool = True) -> None:
|
||||||
|
for mob in self.get_family(recurse):
|
||||||
|
mob._is_animating = is_animating
|
||||||
|
return self
|
||||||
|
|
||||||
|
def make_movable(self, value: bool = True, recurse: bool = True) -> None:
|
||||||
|
for mob in self.get_family(recurse):
|
||||||
|
mob._is_movable = value
|
||||||
|
return self
|
||||||
|
|
||||||
|
def is_movable(self) -> bool:
|
||||||
|
return self._is_movable
|
||||||
|
|
||||||
# Transforming operations
|
# Transforming operations
|
||||||
|
|
||||||
def shift(self, vector: np.ndarray):
|
def shift(self, vector: np.ndarray):
|
||||||
|
@ -1173,6 +1195,13 @@ class Mobject(object):
|
||||||
def get_corner(self, direction: np.ndarray) -> np.ndarray:
|
def get_corner(self, direction: np.ndarray) -> np.ndarray:
|
||||||
return self.get_bounding_box_point(direction)
|
return self.get_bounding_box_point(direction)
|
||||||
|
|
||||||
|
def get_all_corners(self):
|
||||||
|
bb = self.get_bounding_box()
|
||||||
|
return np.array([
|
||||||
|
[bb[indices[-i + 1]][i] for i in range(3)]
|
||||||
|
for indices in it.product(*3 * [[0, 2]])
|
||||||
|
])
|
||||||
|
|
||||||
def get_center(self) -> np.ndarray:
|
def get_center(self) -> np.ndarray:
|
||||||
return self.get_bounding_box()[1]
|
return self.get_bounding_box()[1]
|
||||||
|
|
||||||
|
@ -1403,7 +1432,7 @@ class Mobject(object):
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def push_self_into_submobjects(self):
|
def push_self_into_submobjects(self):
|
||||||
copy = self.deepcopy()
|
copy = self.copy()
|
||||||
copy.set_submobjects([])
|
copy.set_submobjects([])
|
||||||
self.resize_points(0)
|
self.resize_points(0)
|
||||||
self.add(copy)
|
self.add(copy)
|
||||||
|
|
|
@ -274,6 +274,3 @@ class BarChart(VGroup):
|
||||||
(value / self.max_value) * self.height
|
(value / self.max_value) * self.height
|
||||||
)
|
)
|
||||||
bar.move_to(bar_bottom, DOWN)
|
bar.move_to(bar_bottom, DOWN)
|
||||||
|
|
||||||
def copy(self):
|
|
||||||
return self.deepcopy()
|
|
||||||
|
|
|
@ -123,9 +123,6 @@ class LabelledString(_StringSVG, ABC):
|
||||||
self.group_substrs = self.get_group_substrs()
|
self.group_substrs = self.get_group_substrs()
|
||||||
self.submob_groups = self.get_submob_groups()
|
self.submob_groups = self.get_submob_groups()
|
||||||
|
|
||||||
def copy(self):
|
|
||||||
return self.deepcopy()
|
|
||||||
|
|
||||||
# Toolkits
|
# Toolkits
|
||||||
|
|
||||||
def get_substr(self, span: Span) -> str:
|
def get_substr(self, span: Span) -> str:
|
||||||
|
|
|
@ -681,7 +681,7 @@ class VMobject(Mobject):
|
||||||
self.get_end_anchors(),
|
self.get_end_anchors(),
|
||||||
))))
|
))))
|
||||||
|
|
||||||
def get_points_without_null_curves(self, atol: float=1e-9) -> np.ndarray:
|
def get_points_without_null_curves(self, atol: float = 1e-9) -> np.ndarray:
|
||||||
nppc = self.n_points_per_curve
|
nppc = self.n_points_per_curve
|
||||||
points = self.get_points()
|
points = self.get_points()
|
||||||
distinct_curves = reduce(op.or_, [
|
distinct_curves = reduce(op.or_, [
|
||||||
|
|
|
@ -326,7 +326,7 @@ class ShowPassingFlashWithThinningStrokeWidth(AnimationGroup):
|
||||||
max_time_width = kwargs.pop("time_width", self.time_width)
|
max_time_width = kwargs.pop("time_width", self.time_width)
|
||||||
AnimationGroup.__init__(self, *[
|
AnimationGroup.__init__(self, *[
|
||||||
VShowPassingFlash(
|
VShowPassingFlash(
|
||||||
vmobject.deepcopy().set_stroke(width=stroke_width),
|
vmobject.copy().set_stroke(width=stroke_width),
|
||||||
time_width=time_width,
|
time_width=time_width,
|
||||||
**kwargs
|
**kwargs
|
||||||
)
|
)
|
||||||
|
|
417
manimlib/scene/interactive_scene.py
Normal file
417
manimlib/scene/interactive_scene.py
Normal file
|
@ -0,0 +1,417 @@
|
||||||
|
import numpy as np
|
||||||
|
import itertools as it
|
||||||
|
import pyperclip
|
||||||
|
import os
|
||||||
|
|
||||||
|
from manimlib.animation.fading import FadeIn
|
||||||
|
from manimlib.constants import MANIM_COLORS, WHITE, YELLOW
|
||||||
|
from manimlib.constants import ORIGIN, UP, DOWN, LEFT, RIGHT
|
||||||
|
from manimlib.constants import FRAME_WIDTH, SMALL_BUFF
|
||||||
|
from manimlib.constants import CTRL_SYMBOL, SHIFT_SYMBOL, DELETE_SYMBOL, ARROW_SYMBOLS
|
||||||
|
from manimlib.constants import SHIFT_MODIFIER, COMMAND_MODIFIER
|
||||||
|
from manimlib.mobject.mobject import Mobject
|
||||||
|
from manimlib.mobject.geometry import Rectangle
|
||||||
|
from manimlib.mobject.geometry import Square
|
||||||
|
from manimlib.mobject.mobject import Group
|
||||||
|
from manimlib.mobject.svg.tex_mobject import Tex
|
||||||
|
from manimlib.mobject.svg.text_mobject import Text
|
||||||
|
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||||
|
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||||
|
from manimlib.mobject.types.dot_cloud import DotCloud
|
||||||
|
from manimlib.scene.scene import Scene
|
||||||
|
from manimlib.utils.tex_file_writing import LatexError
|
||||||
|
from manimlib.utils.family_ops import extract_mobject_family_members
|
||||||
|
from manimlib.utils.space_ops import get_norm
|
||||||
|
from manimlib.logger import log
|
||||||
|
|
||||||
|
|
||||||
|
SELECT_KEY = 's'
|
||||||
|
GRAB_KEY = 'g'
|
||||||
|
HORIZONTAL_GRAB_KEY = 'h'
|
||||||
|
VERTICAL_GRAB_KEY = 'v'
|
||||||
|
RESIZE_KEY = 't'
|
||||||
|
COLOR_KEY = 'c'
|
||||||
|
|
||||||
|
|
||||||
|
# Note, a lot of the functionality here is still buggy and very much a work in progress.
|
||||||
|
|
||||||
|
class InteractiveScene(Scene):
|
||||||
|
"""
|
||||||
|
TODO, Document
|
||||||
|
|
||||||
|
To select mobjects on screen, hold ctrl and move the mouse to highlight a region,
|
||||||
|
or just tap ctrl to select the mobject under the cursor.
|
||||||
|
|
||||||
|
Pressing command + t will toggle between modes where you either select top level
|
||||||
|
mobjects part of the scene, or low level pieces.
|
||||||
|
|
||||||
|
Hold 'g' to grab the selection and move it around
|
||||||
|
Hold 'h' to drag it constrained in the horizontal direction
|
||||||
|
Hold 'v' to drag it constrained in the vertical direction
|
||||||
|
Hold 't' to resize selection, adding 'shift' to resize with respect to a corner
|
||||||
|
|
||||||
|
Command + 'c' copies the ids of selections to clipboard
|
||||||
|
Command + 'v' will paste either:
|
||||||
|
- The copied mobject
|
||||||
|
- A Tex mobject based on copied LaTeX
|
||||||
|
- A Text mobject based on copied Text
|
||||||
|
Command + 'z' restores selection back to its original state
|
||||||
|
Command + 's' saves the selected mobjects to file
|
||||||
|
"""
|
||||||
|
corner_dot_config = dict(
|
||||||
|
color=WHITE,
|
||||||
|
radius=0.1,
|
||||||
|
glow_factor=1.0,
|
||||||
|
)
|
||||||
|
selection_rectangle_stroke_color = WHITE
|
||||||
|
selection_rectangle_stroke_width = 1.0
|
||||||
|
colors = MANIM_COLORS
|
||||||
|
selection_nudge_size = 0.05
|
||||||
|
|
||||||
|
def setup(self):
|
||||||
|
self.selection = Group()
|
||||||
|
self.selection_highlight = Group()
|
||||||
|
self.selection_rectangle = self.get_selection_rectangle()
|
||||||
|
self.color_palette = self.get_color_palette()
|
||||||
|
self.unselectables = [
|
||||||
|
self.selection,
|
||||||
|
self.selection_highlight,
|
||||||
|
self.selection_rectangle,
|
||||||
|
self.camera.frame
|
||||||
|
]
|
||||||
|
self.saved_selection_state = []
|
||||||
|
self.select_top_level_mobs = True
|
||||||
|
|
||||||
|
self.is_selecting = False
|
||||||
|
self.add(self.selection_highlight)
|
||||||
|
|
||||||
|
def toggle_selection_mode(self):
|
||||||
|
self.select_top_level_mobs = not self.select_top_level_mobs
|
||||||
|
self.refresh_selection_scope()
|
||||||
|
|
||||||
|
def get_selection_search_set(self):
|
||||||
|
mobs = [m for m in self.mobjects if m not in self.unselectables]
|
||||||
|
if self.select_top_level_mobs:
|
||||||
|
return mobs
|
||||||
|
else:
|
||||||
|
return [
|
||||||
|
submob
|
||||||
|
for mob in mobs
|
||||||
|
for submob in mob.family_members_with_points()
|
||||||
|
]
|
||||||
|
|
||||||
|
def refresh_selection_scope(self):
|
||||||
|
curr = list(self.selection)
|
||||||
|
if self.select_top_level_mobs:
|
||||||
|
self.selection.set_submobjects([
|
||||||
|
mob
|
||||||
|
for mob in self.mobjects
|
||||||
|
if any(sm in mob.get_family() for sm in curr)
|
||||||
|
])
|
||||||
|
self.selection.refresh_bounding_box(recurse_down=True)
|
||||||
|
else:
|
||||||
|
self.selection.set_submobjects(
|
||||||
|
extract_mobject_family_members(
|
||||||
|
curr, exclude_pointless=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.refresh_selection_highlight()
|
||||||
|
|
||||||
|
def get_selection_rectangle(self):
|
||||||
|
rect = Rectangle(
|
||||||
|
stroke_color=self.selection_rectangle_stroke_color,
|
||||||
|
stroke_width=self.selection_rectangle_stroke_width,
|
||||||
|
)
|
||||||
|
rect.fix_in_frame()
|
||||||
|
rect.fixed_corner = ORIGIN
|
||||||
|
rect.add_updater(self.update_selection_rectangle)
|
||||||
|
return rect
|
||||||
|
|
||||||
|
def get_color_palette(self):
|
||||||
|
palette = VGroup(*(
|
||||||
|
Square(fill_color=color, fill_opacity=1, side_length=1)
|
||||||
|
for color in self.colors
|
||||||
|
))
|
||||||
|
palette.set_stroke(width=0)
|
||||||
|
palette.arrange(RIGHT, buff=0.5)
|
||||||
|
palette.set_width(FRAME_WIDTH - 0.5)
|
||||||
|
palette.to_edge(DOWN, buff=SMALL_BUFF)
|
||||||
|
palette.fix_in_frame()
|
||||||
|
return palette
|
||||||
|
|
||||||
|
def get_stroke_highlight(self, vmobject):
|
||||||
|
outline = vmobject.copy()
|
||||||
|
for sm, osm in zip(vmobject.get_family(), outline.get_family()):
|
||||||
|
osm.set_fill(opacity=0)
|
||||||
|
osm.set_stroke(YELLOW, width=sm.get_stroke_width() + 1.5)
|
||||||
|
outline.add_updater(lambda o: o.replace(vmobject))
|
||||||
|
return outline
|
||||||
|
|
||||||
|
def get_corner_dots(self, mobject):
|
||||||
|
dots = DotCloud(**self.corner_dot_config)
|
||||||
|
dots.add_updater(lambda d: d.set_points(mobject.get_all_corners()))
|
||||||
|
dots.scale((dots.get_width() + dots.get_radius()) / dots.get_width())
|
||||||
|
# Since for flat object, all 8 corners really appear as four, dim the dots
|
||||||
|
if mobject.get_depth() < 1e-2:
|
||||||
|
dots.set_opacity(0.5)
|
||||||
|
return dots
|
||||||
|
|
||||||
|
def get_highlight(self, mobject):
|
||||||
|
if isinstance(mobject, VMobject) and mobject.has_points():
|
||||||
|
return self.get_stroke_highlight(mobject)
|
||||||
|
else:
|
||||||
|
return self.get_corner_dots(mobject)
|
||||||
|
|
||||||
|
def refresh_selection_highlight(self):
|
||||||
|
self.selection_highlight.set_submobjects([
|
||||||
|
self.get_highlight(mob)
|
||||||
|
for mob in self.selection
|
||||||
|
])
|
||||||
|
|
||||||
|
def update_selection_rectangle(self, rect):
|
||||||
|
p1 = rect.fixed_corner
|
||||||
|
p2 = self.mouse_point.get_center()
|
||||||
|
rect.set_points_as_corners([
|
||||||
|
p1, [p2[0], p1[1], 0],
|
||||||
|
p2, [p1[0], p2[1], 0],
|
||||||
|
p1,
|
||||||
|
])
|
||||||
|
return rect
|
||||||
|
|
||||||
|
def add_to_selection(self, *mobjects):
|
||||||
|
for mob in mobjects:
|
||||||
|
if mob in self.unselectables:
|
||||||
|
continue
|
||||||
|
if mob not in self.selection:
|
||||||
|
self.selection.add(mob)
|
||||||
|
self.selection_highlight.add(self.get_highlight(mob))
|
||||||
|
self.saved_selection_state = [
|
||||||
|
(mob, mob.copy())
|
||||||
|
for mob in self.selection
|
||||||
|
]
|
||||||
|
|
||||||
|
def toggle_from_selection(self, *mobjects):
|
||||||
|
for mob in mobjects:
|
||||||
|
if mob in self.selection:
|
||||||
|
self.selection.remove(mob)
|
||||||
|
else:
|
||||||
|
self.add_to_selection(mob)
|
||||||
|
self.refresh_selection_highlight()
|
||||||
|
|
||||||
|
def clear_selection(self):
|
||||||
|
self.selection.set_submobjects([])
|
||||||
|
self.selection_highlight.set_submobjects([])
|
||||||
|
|
||||||
|
def add(self, *new_mobjects: Mobject):
|
||||||
|
for mob in new_mobjects:
|
||||||
|
mob.make_movable()
|
||||||
|
super().add(*new_mobjects)
|
||||||
|
|
||||||
|
# Selection operations
|
||||||
|
|
||||||
|
def copy_selection(self):
|
||||||
|
ids = map(id, self.selection)
|
||||||
|
pyperclip.copy(",".join(map(str, ids)))
|
||||||
|
|
||||||
|
def paste_selection(self):
|
||||||
|
clipboard_str = pyperclip.paste()
|
||||||
|
# Try pasting a mobject
|
||||||
|
try:
|
||||||
|
ids = map(int, clipboard_str.split(","))
|
||||||
|
mobs = map(self.id_to_mobject, ids)
|
||||||
|
mob_copies = [m.copy() for m in mobs if m is not None]
|
||||||
|
self.clear_selection()
|
||||||
|
self.add_to_selection(*mob_copies)
|
||||||
|
self.play(*(
|
||||||
|
FadeIn(mc, run_time=0.5, scale=1.5)
|
||||||
|
for mc in mob_copies
|
||||||
|
))
|
||||||
|
return
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
# Otherwise, treat as tex or text
|
||||||
|
if "\\" in clipboard_str: # Proxy to text for LaTeX
|
||||||
|
try:
|
||||||
|
new_mob = Tex(clipboard_str)
|
||||||
|
except LatexError:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
new_mob = Text(clipboard_str)
|
||||||
|
self.clear_selection()
|
||||||
|
self.add(new_mob)
|
||||||
|
self.add_to_selection(new_mob)
|
||||||
|
new_mob.move_to(self.mouse_point)
|
||||||
|
|
||||||
|
def delete_selection(self):
|
||||||
|
self.remove(*self.selection)
|
||||||
|
self.clear_selection()
|
||||||
|
|
||||||
|
def saved_selection_to_file(self):
|
||||||
|
directory = self.file_writer.get_saved_mobject_directory()
|
||||||
|
files = os.listdir(directory)
|
||||||
|
for mob in self.selection:
|
||||||
|
file_name = str(mob) + "_0.mob"
|
||||||
|
index = 0
|
||||||
|
while file_name in files:
|
||||||
|
file_name = file_name.replace(str(index), str(index + 1))
|
||||||
|
index += 1
|
||||||
|
user_name = input(
|
||||||
|
f"Enter mobject file name (default is {file_name}): "
|
||||||
|
)
|
||||||
|
if user_name:
|
||||||
|
file_name = user_name
|
||||||
|
files.append(file_name)
|
||||||
|
self.save_mobect(mob, file_name)
|
||||||
|
|
||||||
|
def undo(self):
|
||||||
|
mobs = []
|
||||||
|
for mob, state in self.saved_selection_state:
|
||||||
|
mob.become(state)
|
||||||
|
mobs.append(mob)
|
||||||
|
if mob not in self.mobjects:
|
||||||
|
self.add(mob)
|
||||||
|
self.selection.set_submobjects(mobs)
|
||||||
|
self.refresh_selection_highlight()
|
||||||
|
|
||||||
|
def prepare_resizing(self, about_corner=False):
|
||||||
|
center = self.selection.get_center()
|
||||||
|
mp = self.mouse_point.get_center()
|
||||||
|
if about_corner:
|
||||||
|
self.scale_about_point = self.selection.get_corner(center - mp)
|
||||||
|
else:
|
||||||
|
self.scale_about_point = center
|
||||||
|
self.scale_ref_vect = mp - self.scale_about_point
|
||||||
|
self.scale_ref_width = self.selection.get_width()
|
||||||
|
|
||||||
|
# Event handlers
|
||||||
|
|
||||||
|
def on_key_press(self, symbol: int, modifiers: int) -> None:
|
||||||
|
super().on_key_press(symbol, modifiers)
|
||||||
|
char = chr(symbol)
|
||||||
|
# Enable selection
|
||||||
|
if char == SELECT_KEY and modifiers == 0:
|
||||||
|
self.is_selecting = True
|
||||||
|
self.add(self.selection_rectangle)
|
||||||
|
self.selection_rectangle.fixed_corner = self.mouse_point.get_center().copy()
|
||||||
|
# Prepare for move
|
||||||
|
elif char in [GRAB_KEY, HORIZONTAL_GRAB_KEY, VERTICAL_GRAB_KEY] and modifiers == 0:
|
||||||
|
mp = self.mouse_point.get_center()
|
||||||
|
self.mouse_to_selection = mp - self.selection.get_center()
|
||||||
|
# Prepare for resizing
|
||||||
|
elif char == RESIZE_KEY and modifiers in [0, SHIFT_MODIFIER]:
|
||||||
|
self.prepare_resizing(about_corner=(modifiers == SHIFT_MODIFIER))
|
||||||
|
elif symbol == SHIFT_SYMBOL:
|
||||||
|
if self.window.is_key_pressed(ord("t")):
|
||||||
|
self.prepare_resizing(about_corner=True)
|
||||||
|
# Show color palette
|
||||||
|
elif char == COLOR_KEY and modifiers == 0:
|
||||||
|
if len(self.selection) == 0:
|
||||||
|
return
|
||||||
|
if self.color_palette not in self.mobjects:
|
||||||
|
self.add(self.color_palette)
|
||||||
|
else:
|
||||||
|
self.remove(self.color_palette)
|
||||||
|
# Command + c -> Copy mobject ids to clipboard
|
||||||
|
elif char == "c" and modifiers == COMMAND_MODIFIER:
|
||||||
|
self.copy_selection()
|
||||||
|
# Command + v -> Paste
|
||||||
|
elif char == "v" and modifiers == COMMAND_MODIFIER:
|
||||||
|
self.paste_selection()
|
||||||
|
# Command + x -> Cut
|
||||||
|
elif char == "x" and modifiers == COMMAND_MODIFIER:
|
||||||
|
# TODO, this copy won't work, because once the objects are removed,
|
||||||
|
# they're not searched for in the pasting.
|
||||||
|
self.copy_selection()
|
||||||
|
self.delete_selection()
|
||||||
|
# Delete
|
||||||
|
elif symbol == DELETE_SYMBOL:
|
||||||
|
self.delete_selection()
|
||||||
|
# Command + a -> Select all
|
||||||
|
elif char == "a" and modifiers == COMMAND_MODIFIER:
|
||||||
|
self.clear_selection()
|
||||||
|
self.add_to_selection(*self.mobjects)
|
||||||
|
# Command + g -> Group selection
|
||||||
|
elif char == "g" and modifiers == COMMAND_MODIFIER:
|
||||||
|
group = self.get_group(*self.selection)
|
||||||
|
self.add(group)
|
||||||
|
self.clear_selection()
|
||||||
|
self.add_to_selection(group)
|
||||||
|
# Command + shift + g -> Ungroup the selection
|
||||||
|
elif char == "g" and modifiers == COMMAND_MODIFIER | SHIFT_MODIFIER:
|
||||||
|
pieces = []
|
||||||
|
for mob in list(self.selection):
|
||||||
|
self.remove(mob)
|
||||||
|
pieces.extend(list(mob))
|
||||||
|
self.clear_selection()
|
||||||
|
self.add(*pieces)
|
||||||
|
self.add_to_selection(*pieces)
|
||||||
|
# Command + t -> Toggle selection mode
|
||||||
|
elif char == "t" and modifiers == COMMAND_MODIFIER:
|
||||||
|
self.toggle_selection_mode()
|
||||||
|
# Command + z -> Restore selection to original state
|
||||||
|
elif char == "z" and modifiers == COMMAND_MODIFIER:
|
||||||
|
self.undo()
|
||||||
|
# Command + s -> Save selections to file
|
||||||
|
elif char == "s" and modifiers == COMMAND_MODIFIER:
|
||||||
|
self.saved_selection_to_file()
|
||||||
|
# Keyboard movements
|
||||||
|
elif symbol in ARROW_SYMBOLS:
|
||||||
|
nudge = self.selection_nudge_size
|
||||||
|
if (modifiers & SHIFT_MODIFIER):
|
||||||
|
nudge *= 10
|
||||||
|
vect = [LEFT, UP, RIGHT, DOWN][ARROW_SYMBOLS.index(symbol)]
|
||||||
|
self.selection.shift(nudge * vect)
|
||||||
|
|
||||||
|
def on_key_release(self, symbol: int, modifiers: int) -> None:
|
||||||
|
super().on_key_release(symbol, modifiers)
|
||||||
|
if chr(symbol) == SELECT_KEY:
|
||||||
|
self.is_selecting = False
|
||||||
|
self.remove(self.selection_rectangle)
|
||||||
|
for mob in reversed(self.get_selection_search_set()):
|
||||||
|
if mob.is_movable() and self.selection_rectangle.is_touching(mob):
|
||||||
|
self.add_to_selection(mob)
|
||||||
|
|
||||||
|
elif symbol == SHIFT_SYMBOL:
|
||||||
|
if self.window.is_key_pressed(ord(RESIZE_KEY)):
|
||||||
|
self.prepare_resizing(about_corner=False)
|
||||||
|
|
||||||
|
def on_mouse_motion(self, point: np.ndarray, d_point: np.ndarray) -> None:
|
||||||
|
super().on_mouse_motion(point, d_point)
|
||||||
|
# Move selection
|
||||||
|
if self.window.is_key_pressed(ord("g")):
|
||||||
|
self.selection.move_to(point - self.mouse_to_selection)
|
||||||
|
# Move selection restricted to horizontal
|
||||||
|
elif self.window.is_key_pressed(ord("h")):
|
||||||
|
self.selection.set_x((point - self.mouse_to_selection)[0])
|
||||||
|
# Move selection restricted to vertical
|
||||||
|
elif self.window.is_key_pressed(ord("v")):
|
||||||
|
self.selection.set_y((point - self.mouse_to_selection)[1])
|
||||||
|
# Scale selection
|
||||||
|
elif self.window.is_key_pressed(ord("t")):
|
||||||
|
# TODO, allow for scaling about the opposite corner
|
||||||
|
vect = point - self.scale_about_point
|
||||||
|
scalar = get_norm(vect) / get_norm(self.scale_ref_vect)
|
||||||
|
self.selection.set_width(
|
||||||
|
scalar * self.scale_ref_width,
|
||||||
|
about_point=self.scale_about_point
|
||||||
|
)
|
||||||
|
|
||||||
|
def on_mouse_release(self, point: np.ndarray, button: int, mods: int) -> None:
|
||||||
|
super().on_mouse_release(point, button, mods)
|
||||||
|
if self.color_palette in self.mobjects:
|
||||||
|
# Search through all mobject on the screne, not just the palette
|
||||||
|
to_search = list(it.chain(*(
|
||||||
|
mobject.family_members_with_points()
|
||||||
|
for mobject in self.mobjects
|
||||||
|
if mobject not in self.unselectables
|
||||||
|
)))
|
||||||
|
mob = self.point_to_mobject(point, to_search)
|
||||||
|
if mob is not None:
|
||||||
|
self.selection.set_color(mob.get_fill_color())
|
||||||
|
self.remove(self.color_palette)
|
||||||
|
elif self.window.is_key_pressed(SHIFT_SYMBOL):
|
||||||
|
mob = self.point_to_mobject(point)
|
||||||
|
if mob is not None:
|
||||||
|
self.toggle_from_selection(mob)
|
||||||
|
else:
|
||||||
|
self.clear_selection()
|
|
@ -4,20 +4,25 @@ import time
|
||||||
import random
|
import random
|
||||||
import inspect
|
import inspect
|
||||||
import platform
|
import platform
|
||||||
import itertools as it
|
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from typing import Iterable, Callable
|
from typing import Iterable, Callable
|
||||||
|
import os
|
||||||
|
|
||||||
from tqdm import tqdm as ProgressDisplay
|
from tqdm import tqdm as ProgressDisplay
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import numpy.typing as npt
|
|
||||||
|
|
||||||
from manimlib.animation.animation import prepare_animation
|
from manimlib.animation.animation import prepare_animation
|
||||||
from manimlib.animation.transform import MoveToTarget
|
from manimlib.animation.transform import MoveToTarget
|
||||||
from manimlib.camera.camera import Camera
|
from manimlib.camera.camera import Camera
|
||||||
|
from manimlib.config import get_custom_config
|
||||||
from manimlib.constants import DEFAULT_WAIT_TIME
|
from manimlib.constants import DEFAULT_WAIT_TIME
|
||||||
|
from manimlib.constants import ARROW_SYMBOLS
|
||||||
|
from manimlib.constants import SHIFT_MODIFIER, CTRL_MODIFIER, COMMAND_MODIFIER
|
||||||
from manimlib.mobject.mobject import Mobject
|
from manimlib.mobject.mobject import Mobject
|
||||||
from manimlib.mobject.mobject import Point
|
from manimlib.mobject.mobject import Point
|
||||||
|
from manimlib.mobject.mobject import Group
|
||||||
|
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||||
|
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||||
from manimlib.scene.scene_file_writer import SceneFileWriter
|
from manimlib.scene.scene_file_writer import SceneFileWriter
|
||||||
from manimlib.utils.config_ops import digest_config
|
from manimlib.utils.config_ops import digest_config
|
||||||
from manimlib.utils.family_ops import extract_mobject_family_members
|
from manimlib.utils.family_ops import extract_mobject_family_members
|
||||||
|
@ -33,6 +38,14 @@ if TYPE_CHECKING:
|
||||||
from manimlib.animation.animation import Animation
|
from manimlib.animation.animation import Animation
|
||||||
|
|
||||||
|
|
||||||
|
PAN_3D_KEY = 'd'
|
||||||
|
FRAME_SHIFT_KEY = 'f'
|
||||||
|
ZOOM_KEY = 'z'
|
||||||
|
RESET_FRAME_KEY = 'r'
|
||||||
|
QUIT_KEY = 'q'
|
||||||
|
EMBED_KEY = 'e'
|
||||||
|
|
||||||
|
|
||||||
class Scene(object):
|
class Scene(object):
|
||||||
CONFIG = {
|
CONFIG = {
|
||||||
"window_config": {},
|
"window_config": {},
|
||||||
|
@ -116,16 +129,17 @@ class Scene(object):
|
||||||
# If there is a window, enter a loop
|
# If there is a window, enter a loop
|
||||||
# which updates the frame while under
|
# which updates the frame while under
|
||||||
# the hood calling the pyglet event loop
|
# the hood calling the pyglet event loop
|
||||||
log.info("Tips: You are now in the interactive mode. Now you can use the keyboard"
|
log.info(
|
||||||
" and the mouse to interact with the scene. Just press `q` if you want to quit.")
|
"Tips: You are now in the interactive mode. Now you can use the keyboard"
|
||||||
|
" and the mouse to interact with the scene. Just press `command + q` or `esc`"
|
||||||
|
" if you want to quit."
|
||||||
|
)
|
||||||
self.quit_interaction = False
|
self.quit_interaction = False
|
||||||
self.lock_static_mobject_data()
|
self.refresh_static_mobjects()
|
||||||
while not (self.window.is_closing or self.quit_interaction):
|
while not (self.window.is_closing or self.quit_interaction):
|
||||||
self.update_frame(1 / self.camera.frame_rate)
|
self.update_frame(1 / self.camera.frame_rate)
|
||||||
if self.window.is_closing:
|
if self.window.is_closing:
|
||||||
self.window.destroy()
|
self.window.destroy()
|
||||||
if self.quit_interaction:
|
|
||||||
self.unlock_mobject_data()
|
|
||||||
|
|
||||||
def embed(self, close_scene_on_exit: bool = True) -> None:
|
def embed(self, close_scene_on_exit: bool = True) -> None:
|
||||||
if not self.preview:
|
if not self.preview:
|
||||||
|
@ -142,15 +156,18 @@ class Scene(object):
|
||||||
from IPython.terminal.embed import InteractiveShellEmbed
|
from IPython.terminal.embed import InteractiveShellEmbed
|
||||||
shell = InteractiveShellEmbed()
|
shell = InteractiveShellEmbed()
|
||||||
# Have the frame update after each command
|
# Have the frame update after each command
|
||||||
|
shell.events.register('post_run_cell', lambda *a, **kw: self.refresh_static_mobjects())
|
||||||
shell.events.register('post_run_cell', lambda *a, **kw: self.update_frame())
|
shell.events.register('post_run_cell', lambda *a, **kw: self.update_frame())
|
||||||
# Use the locals of the caller as the local namespace
|
# Use the locals of the caller as the local namespace
|
||||||
# once embedded, and add a few custom shortcuts
|
# once embedded, and add a few custom shortcuts
|
||||||
local_ns = inspect.currentframe().f_back.f_locals
|
local_ns = inspect.currentframe().f_back.f_locals
|
||||||
local_ns["touch"] = self.interact
|
local_ns["touch"] = self.interact
|
||||||
|
local_ns["i2g"] = self.ids_to_group
|
||||||
for term in ("play", "wait", "add", "remove", "clear", "save_state", "restore"):
|
for term in ("play", "wait", "add", "remove", "clear", "save_state", "restore"):
|
||||||
local_ns[term] = getattr(self, term)
|
local_ns[term] = getattr(self, term)
|
||||||
log.info("Tips: Now the embed iPython terminal is open. But you can't interact with"
|
log.info("Tips: Now the embed iPython terminal is open. But you can't interact with"
|
||||||
" the window directly. To do so, you need to type `touch()` or `self.interact()`")
|
" the window directly. To do so, you need to type `touch()` or `self.interact()`")
|
||||||
|
exec(get_custom_config()["universal_import_line"])
|
||||||
shell(local_ns=local_ns, stack_depth=2)
|
shell(local_ns=local_ns, stack_depth=2)
|
||||||
# End scene when exiting an embed
|
# End scene when exiting an embed
|
||||||
if close_scene_on_exit:
|
if close_scene_on_exit:
|
||||||
|
@ -295,6 +312,25 @@ class Scene(object):
|
||||||
return mobject
|
return mobject
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def get_group(self, *mobjects):
|
||||||
|
if all(isinstance(m, VMobject) for m in mobjects):
|
||||||
|
return VGroup(*mobjects)
|
||||||
|
else:
|
||||||
|
return Group(*mobjects)
|
||||||
|
|
||||||
|
def id_to_mobject(self, id_value):
|
||||||
|
for mob in self.mobjects:
|
||||||
|
for sm in mob.get_family():
|
||||||
|
if id(sm) == id_value:
|
||||||
|
return sm
|
||||||
|
return None
|
||||||
|
|
||||||
|
def ids_to_group(self, *id_values):
|
||||||
|
return self.get_group(*filter(
|
||||||
|
lambda x: x is not None,
|
||||||
|
map(self.id_to_mobject, id_values)
|
||||||
|
))
|
||||||
|
|
||||||
# Related to skipping
|
# Related to skipping
|
||||||
def update_skipping_status(self) -> None:
|
def update_skipping_status(self) -> None:
|
||||||
if self.start_at_animation_number is not None:
|
if self.start_at_animation_number is not None:
|
||||||
|
@ -443,6 +479,7 @@ class Scene(object):
|
||||||
self.real_animation_start_time = time.time()
|
self.real_animation_start_time = time.time()
|
||||||
self.virtual_animation_start_time = self.time
|
self.virtual_animation_start_time = self.time
|
||||||
|
|
||||||
|
self.refresh_static_mobjects()
|
||||||
func(self, *args, **kwargs)
|
func(self, *args, **kwargs)
|
||||||
|
|
||||||
if should_write:
|
if should_write:
|
||||||
|
@ -451,23 +488,8 @@ class Scene(object):
|
||||||
self.num_plays += 1
|
self.num_plays += 1
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
def lock_static_mobject_data(self, *animations: Animation) -> None:
|
def refresh_static_mobjects(self) -> None:
|
||||||
movers = list(it.chain(*[
|
self.camera.refresh_static_mobjects()
|
||||||
anim.mobject.get_family()
|
|
||||||
for anim in animations
|
|
||||||
]))
|
|
||||||
for mobject in self.mobjects:
|
|
||||||
if mobject in movers or mobject.get_family_updaters():
|
|
||||||
continue
|
|
||||||
self.camera.set_mobjects_as_static(mobject)
|
|
||||||
|
|
||||||
def unlock_mobject_data(self) -> None:
|
|
||||||
self.camera.release_static_mobjects()
|
|
||||||
|
|
||||||
def refresh_locked_data(self):
|
|
||||||
self.unlock_mobject_data()
|
|
||||||
self.lock_static_mobject_data()
|
|
||||||
return self
|
|
||||||
|
|
||||||
def begin_animations(self, animations: Iterable[Animation]) -> None:
|
def begin_animations(self, animations: Iterable[Animation]) -> None:
|
||||||
for animation in animations:
|
for animation in animations:
|
||||||
|
@ -507,11 +529,9 @@ class Scene(object):
|
||||||
log.warning("Called Scene.play with no animations")
|
log.warning("Called Scene.play with no animations")
|
||||||
return
|
return
|
||||||
animations = self.anims_from_play_args(*args, **kwargs)
|
animations = self.anims_from_play_args(*args, **kwargs)
|
||||||
self.lock_static_mobject_data(*animations)
|
|
||||||
self.begin_animations(animations)
|
self.begin_animations(animations)
|
||||||
self.progress_through_animations(animations)
|
self.progress_through_animations(animations)
|
||||||
self.finish_animations(animations)
|
self.finish_animations(animations)
|
||||||
self.unlock_mobject_data()
|
|
||||||
|
|
||||||
@handle_play_like_call
|
@handle_play_like_call
|
||||||
def wait(
|
def wait(
|
||||||
|
@ -524,7 +544,6 @@ class Scene(object):
|
||||||
if note:
|
if note:
|
||||||
log.info(note)
|
log.info(note)
|
||||||
self.update_mobjects(dt=0) # Any problems with this?
|
self.update_mobjects(dt=0) # Any problems with this?
|
||||||
self.lock_static_mobject_data()
|
|
||||||
if self.presenter_mode and not self.skip_animations and not ignore_presenter_mode:
|
if self.presenter_mode and not self.skip_animations and not ignore_presenter_mode:
|
||||||
while self.hold_on_wait:
|
while self.hold_on_wait:
|
||||||
self.update_frame(dt=1 / self.camera.frame_rate)
|
self.update_frame(dt=1 / self.camera.frame_rate)
|
||||||
|
@ -538,9 +557,8 @@ class Scene(object):
|
||||||
self.update_frame(dt)
|
self.update_frame(dt)
|
||||||
self.emit_frame()
|
self.emit_frame()
|
||||||
if stop_condition is not None and stop_condition():
|
if stop_condition is not None and stop_condition():
|
||||||
time_progression.close()
|
|
||||||
break
|
break
|
||||||
self.unlock_mobject_data()
|
self.refresh_static_mobjects()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def wait_until(
|
def wait_until(
|
||||||
|
@ -574,22 +592,31 @@ class Scene(object):
|
||||||
|
|
||||||
# Helpers for interactive development
|
# Helpers for interactive development
|
||||||
def save_state(self) -> None:
|
def save_state(self) -> None:
|
||||||
self.saved_state = {
|
self.saved_state = [
|
||||||
"mobjects": self.mobjects,
|
(mob, mob.copy())
|
||||||
"mobject_states": [
|
for mob in self.mobjects
|
||||||
mob.copy()
|
]
|
||||||
for mob in self.mobjects
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
def restore(self) -> None:
|
def restore(self) -> None:
|
||||||
if not hasattr(self, "saved_state"):
|
if not hasattr(self, "saved_state"):
|
||||||
raise Exception("Trying to restore scene without having saved")
|
raise Exception("Trying to restore scene without having saved")
|
||||||
mobjects = self.saved_state["mobjects"]
|
self.mobjects = []
|
||||||
states = self.saved_state["mobject_states"]
|
for mob, mob_state in self.saved_state:
|
||||||
for mob, state in zip(mobjects, states):
|
mob.become(mob_state)
|
||||||
mob.become(state)
|
self.mobjects.append(mob)
|
||||||
self.mobjects = mobjects
|
|
||||||
|
def save_mobect(self, mobject: Mobject, file_name: str):
|
||||||
|
directory = self.file_writer.get_saved_mobject_directory()
|
||||||
|
path = os.path.join(directory, file_name)
|
||||||
|
mobject.save_to_file(path)
|
||||||
|
|
||||||
|
def load_mobject(self, file_name):
|
||||||
|
if os.path.exists(file_name):
|
||||||
|
path = file_name
|
||||||
|
else:
|
||||||
|
directory = self.file_writer.get_saved_mobject_directory()
|
||||||
|
path = os.path.join(directory, file_name)
|
||||||
|
return Mobject.load(path)
|
||||||
|
|
||||||
# Event handling
|
# Event handling
|
||||||
|
|
||||||
|
@ -606,10 +633,12 @@ class Scene(object):
|
||||||
return
|
return
|
||||||
|
|
||||||
frame = self.camera.frame
|
frame = self.camera.frame
|
||||||
if self.window.is_key_pressed(ord("d")):
|
# Handle perspective changes
|
||||||
|
if self.window.is_key_pressed(ord(PAN_3D_KEY)):
|
||||||
frame.increment_theta(-self.pan_sensitivity * d_point[0])
|
frame.increment_theta(-self.pan_sensitivity * d_point[0])
|
||||||
frame.increment_phi(self.pan_sensitivity * d_point[1])
|
frame.increment_phi(self.pan_sensitivity * d_point[1])
|
||||||
elif self.window.is_key_pressed(ord("s")):
|
# Handle frame movements
|
||||||
|
elif self.window.is_key_pressed(ord(FRAME_SHIFT_KEY)):
|
||||||
shift = -d_point
|
shift = -d_point
|
||||||
shift[0] *= frame.get_width() / 2
|
shift[0] *= frame.get_width() / 2
|
||||||
shift[1] *= frame.get_height() / 2
|
shift[1] *= frame.get_height() / 2
|
||||||
|
@ -637,6 +666,7 @@ class Scene(object):
|
||||||
button: int,
|
button: int,
|
||||||
mods: int
|
mods: int
|
||||||
) -> None:
|
) -> None:
|
||||||
|
self.mouse_drag_point.move_to(point)
|
||||||
event_data = {"point": point, "button": button, "mods": mods}
|
event_data = {"point": point, "button": button, "mods": mods}
|
||||||
propagate_event = EVENT_DISPATCHER.dispatch(EventType.MousePressEvent, **event_data)
|
propagate_event = EVENT_DISPATCHER.dispatch(EventType.MousePressEvent, **event_data)
|
||||||
if propagate_event is not None and propagate_event is False:
|
if propagate_event is not None and propagate_event is False:
|
||||||
|
@ -664,9 +694,9 @@ class Scene(object):
|
||||||
return
|
return
|
||||||
|
|
||||||
frame = self.camera.frame
|
frame = self.camera.frame
|
||||||
if self.window.is_key_pressed(ord("z")):
|
if self.window.is_key_pressed(ord(ZOOM_KEY)):
|
||||||
factor = 1 + np.arctan(10 * offset[1])
|
factor = 1 + np.arctan(10 * offset[1])
|
||||||
frame.scale(1/factor, about_point=point)
|
frame.scale(1 / factor, about_point=point)
|
||||||
else:
|
else:
|
||||||
transform = frame.get_inverse_camera_rotation_matrix()
|
transform = frame.get_inverse_camera_rotation_matrix()
|
||||||
shift = np.dot(np.transpose(transform), offset)
|
shift = np.dot(np.transpose(transform), offset)
|
||||||
|
@ -698,13 +728,16 @@ 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 == "r":
|
if char == RESET_FRAME_KEY:
|
||||||
self.camera.frame.to_default_state()
|
self.camera.frame.to_default_state()
|
||||||
elif char == "q":
|
# command + q
|
||||||
|
elif char == QUIT_KEY and modifiers == COMMAND_MODIFIER:
|
||||||
self.quit_interaction = True
|
self.quit_interaction = True
|
||||||
elif char == " " or symbol == 65363: # Space or right arrow
|
# Space or right arrow
|
||||||
|
elif char == " " or symbol == ARROW_SYMBOLS[2]:
|
||||||
self.hold_on_wait = False
|
self.hold_on_wait = False
|
||||||
elif char == "e" and modifiers == 3: # ctrl + shift + e
|
# ctrl + shift + e
|
||||||
|
elif char == EMBED_KEY and modifiers == CTRL_MODIFIER | SHIFT_MODIFIER:
|
||||||
self.embed(close_scene_on_exit=False)
|
self.embed(close_scene_on_exit=False)
|
||||||
|
|
||||||
def on_resize(self, width: int, height: int) -> None:
|
def on_resize(self, width: int, height: int) -> None:
|
||||||
|
|
|
@ -78,6 +78,8 @@ class SceneFileWriter(object):
|
||||||
self.partial_movie_directory = guarantee_existence(os.path.join(
|
self.partial_movie_directory = guarantee_existence(os.path.join(
|
||||||
movie_dir, "partial_movie_files", scene_name,
|
movie_dir, "partial_movie_files", scene_name,
|
||||||
))
|
))
|
||||||
|
# A place to save mobjects
|
||||||
|
self.saved_mobject_directory = os.path.join(out_dir, "mobjects")
|
||||||
|
|
||||||
def get_default_module_directory(self) -> str:
|
def get_default_module_directory(self) -> str:
|
||||||
path, _ = os.path.splitext(self.input_file_path)
|
path, _ = os.path.splitext(self.input_file_path)
|
||||||
|
@ -119,6 +121,12 @@ class SceneFileWriter(object):
|
||||||
def get_movie_file_path(self) -> str:
|
def get_movie_file_path(self) -> str:
|
||||||
return self.movie_file_path
|
return self.movie_file_path
|
||||||
|
|
||||||
|
def get_saved_mobject_directory(self) -> str:
|
||||||
|
return guarantee_existence(os.path.join(
|
||||||
|
self.saved_mobject_directory,
|
||||||
|
str(self.scene),
|
||||||
|
))
|
||||||
|
|
||||||
# Sound
|
# Sound
|
||||||
def init_audio(self) -> None:
|
def init_audio(self) -> None:
|
||||||
self.includes_sound: bool = False
|
self.includes_sound: bool = False
|
||||||
|
@ -220,7 +228,7 @@ class SceneFileWriter(object):
|
||||||
# This is if the background of the exported
|
# This is if the background of the exported
|
||||||
# video should be transparent.
|
# video should be transparent.
|
||||||
command += [
|
command += [
|
||||||
'-vcodec', 'qtrle',
|
'-vcodec', 'prores_ks',
|
||||||
]
|
]
|
||||||
elif self.movie_file_extension == ".gif":
|
elif self.movie_file_extension == ".gif":
|
||||||
command += []
|
command += []
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import itertools as it
|
|
||||||
from typing import Iterable
|
from typing import Iterable
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
@ -11,15 +10,14 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
def extract_mobject_family_members(
|
def extract_mobject_family_members(
|
||||||
mobject_list: Iterable[Mobject],
|
mobject_list: Iterable[Mobject],
|
||||||
only_those_with_points: bool = False
|
exclude_pointless: bool = False
|
||||||
) -> list[Mobject]:
|
) -> list[Mobject]:
|
||||||
result = list(it.chain(*[
|
return [
|
||||||
mob.get_family()
|
sm
|
||||||
for mob in mobject_list
|
for mob in mobject_list
|
||||||
]))
|
for sm in mob.get_family()
|
||||||
if only_those_with_points:
|
if (not exclude_pointless) or sm.has_points()
|
||||||
result = [mob for mob in result if mob.has_points()]
|
]
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def restructure_list_to_exclude_certain_family_members(
|
def restructure_list_to_exclude_certain_family_members(
|
||||||
|
|
|
@ -152,6 +152,8 @@ def angle_between_vectors(v1: np.ndarray, v2: np.ndarray) -> float:
|
||||||
"""
|
"""
|
||||||
n1 = get_norm(v1)
|
n1 = get_norm(v1)
|
||||||
n2 = get_norm(v2)
|
n2 = get_norm(v2)
|
||||||
|
if n1 == 0 or n2 == 0:
|
||||||
|
return 0
|
||||||
cos_angle = np.dot(v1, v2) / np.float64(n1 * n2)
|
cos_angle = np.dot(v1, v2) / np.float64(n1 * n2)
|
||||||
return math.acos(clip(cos_angle, -1, 1))
|
return math.acos(clip(cos_angle, -1, 1))
|
||||||
|
|
||||||
|
|
|
@ -92,7 +92,7 @@ def tex_to_dvi(tex_file):
|
||||||
for line in file.readlines():
|
for line in file.readlines():
|
||||||
if line.startswith("!"):
|
if line.startswith("!"):
|
||||||
log.debug(f"The error could be: `{line[2:-1]}`")
|
log.debug(f"The error could be: `{line[2:-1]}`")
|
||||||
sys.exit(2)
|
raise LatexError()
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@ -134,3 +134,8 @@ def display_during_execution(message):
|
||||||
yield
|
yield
|
||||||
finally:
|
finally:
|
||||||
print(" " * len(to_print), end="\r")
|
print(" " * len(to_print), end="\r")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class LatexError(Exception):
|
||||||
|
pass
|
||||||
|
|
|
@ -11,6 +11,7 @@ moderngl_window
|
||||||
skia-pathops
|
skia-pathops
|
||||||
pydub
|
pydub
|
||||||
pygments
|
pygments
|
||||||
|
pyperclip
|
||||||
pyyaml
|
pyyaml
|
||||||
rich
|
rich
|
||||||
screeninfo
|
screeninfo
|
||||||
|
|
Loading…
Add table
Reference in a new issue