diff --git a/manimlib/__init__.py b/manimlib/__init__.py index ecee0ec4..2043738c 100644 --- a/manimlib/__init__.py +++ b/manimlib/__init__.py @@ -4,6 +4,8 @@ __version__ = pkg_resources.get_distribution("manimgl").version from manimlib.constants import * +from manimlib.window import * + from manimlib.animation.animation import * from manimlib.animation.composition import * from manimlib.animation.creation import * @@ -20,17 +22,16 @@ from manimlib.animation.update import * from manimlib.camera.camera import * -from manimlib.window import * - from manimlib.mobject.boolean_ops import * -from manimlib.mobject.coordinate_systems import * from manimlib.mobject.changing import * +from manimlib.mobject.coordinate_systems import * from manimlib.mobject.frame import * from manimlib.mobject.functions import * from manimlib.mobject.geometry import * from manimlib.mobject.interactive import * from manimlib.mobject.matrix import * from manimlib.mobject.mobject import * +from manimlib.mobject.mobject_update_utils import * from manimlib.mobject.number_line import * from manimlib.mobject.numbers import * from manimlib.mobject.probability import * @@ -43,17 +44,16 @@ from manimlib.mobject.svg.svg_mobject import * from manimlib.mobject.svg.tex_mobject import * from manimlib.mobject.svg.text_mobject import * from manimlib.mobject.three_dimensions import * +from manimlib.mobject.types.dot_cloud import * from manimlib.mobject.types.image_mobject import * from manimlib.mobject.types.point_cloud_mobject import * from manimlib.mobject.types.surface import * from manimlib.mobject.types.vectorized_mobject import * -from manimlib.mobject.types.dot_cloud import * -from manimlib.mobject.mobject_update_utils import * from manimlib.mobject.value_tracker import * from manimlib.mobject.vector_field import * -from manimlib.scene.scene import * from manimlib.scene.interactive_scene import * +from manimlib.scene.scene import * from manimlib.scene.three_d_scene import * from manimlib.utils.bezier import * @@ -62,9 +62,9 @@ from manimlib.utils.config_ops import * from manimlib.utils.customization import * from manimlib.utils.debug import * from manimlib.utils.directories import * +from manimlib.utils.file_ops import * from manimlib.utils.images import * from manimlib.utils.iterables import * -from manimlib.utils.file_ops import * from manimlib.utils.paths import * from manimlib.utils.rate_functions import * from manimlib.utils.simple_functions import * diff --git a/manimlib/__main__.py b/manimlib/__main__.py index d6af540d..d5dddc7e 100644 --- a/manimlib/__main__.py +++ b/manimlib/__main__.py @@ -1,9 +1,9 @@ #!/usr/bin/env python -import manimlib.config -import manimlib.logger -import manimlib.extract_scene -import manimlib.utils.init_config from manimlib import __version__ +import manimlib.config +import manimlib.extract_scene +import manimlib.logger +import manimlib.utils.init_config def main(): diff --git a/manimlib/animation/animation.py b/manimlib/animation/animation.py index e587f813..03959329 100644 --- a/manimlib/animation/animation.py +++ b/manimlib/animation/animation.py @@ -1,7 +1,6 @@ from __future__ import annotations from copy import deepcopy -from typing import Callable from manimlib.mobject.mobject import _AnimationBuilder from manimlib.mobject.mobject import Mobject @@ -12,6 +11,8 @@ from manimlib.utils.simple_functions import clip from typing import TYPE_CHECKING if TYPE_CHECKING: + from typing import Callable + from manimlib.scene.scene import Scene diff --git a/manimlib/animation/composition.py b/manimlib/animation/composition.py index 0771ad31..3ab77bd4 100644 --- a/manimlib/animation/composition.py +++ b/manimlib/animation/composition.py @@ -1,9 +1,9 @@ from __future__ import annotations import numpy as np -from typing import Callable -from manimlib.animation.animation import Animation, prepare_animation +from manimlib.animation.animation import Animation +from manimlib.animation.animation import prepare_animation from manimlib.mobject.mobject import Group from manimlib.utils.bezier import integer_interpolate from manimlib.utils.bezier import interpolate @@ -15,8 +15,10 @@ from manimlib.utils.simple_functions import clip from typing import TYPE_CHECKING if TYPE_CHECKING: - from manimlib.scene.scene import Scene + from typing import Callable + from manimlib.mobject.mobject import Mobject + from manimlib.scene.scene import Scene DEFAULT_LAGGED_START_LAG_RATIO = 0.05 diff --git a/manimlib/animation/fading.py b/manimlib/animation/fading.py index 39c149f0..2fc63d43 100644 --- a/manimlib/animation/fading.py +++ b/manimlib/animation/fading.py @@ -4,17 +4,17 @@ import numpy as np from manimlib.animation.animation import Animation from manimlib.animation.transform import Transform -from manimlib.mobject.mobject import Group from manimlib.constants import ORIGIN +from manimlib.mobject.mobject import Group from manimlib.utils.bezier import interpolate from manimlib.utils.rate_functions import there_and_back from typing import TYPE_CHECKING if TYPE_CHECKING: - from manimlib.scene.scene import Scene from manimlib.mobject.mobject import Mobject from manimlib.mobject.types.vectorized_mobject import VMobject + from manimlib.scene.scene import Scene DEFAULT_FADE_LAG_RATIO = 0 diff --git a/manimlib/animation/growing.py b/manimlib/animation/growing.py index 1b3c3cd7..b29982f2 100644 --- a/manimlib/animation/growing.py +++ b/manimlib/animation/growing.py @@ -1,14 +1,15 @@ from __future__ import annotations -from manimlib.constants import PI from manimlib.animation.transform import Transform +from manimlib.constants import PI from typing import TYPE_CHECKING if TYPE_CHECKING: import numpy as np - from manimlib.mobject.mobject import Mobject + from manimlib.mobject.geometry import Arrow + from manimlib.mobject.mobject import Mobject class GrowFromPoint(Transform): diff --git a/manimlib/animation/indication.py b/manimlib/animation/indication.py index 5f210773..160464b3 100644 --- a/manimlib/animation/indication.py +++ b/manimlib/animation/indication.py @@ -1,40 +1,44 @@ from __future__ import annotations import math -from typing import Union, Sequence - import numpy as np -from manimlib.constants import * from manimlib.animation.animation import Animation -from manimlib.animation.movement import Homotopy from manimlib.animation.composition import AnimationGroup from manimlib.animation.composition import Succession from manimlib.animation.creation import ShowCreation from manimlib.animation.creation import ShowPartial from manimlib.animation.fading import FadeOut from manimlib.animation.fading import FadeIn +from manimlib.animation.movement import Homotopy from manimlib.animation.transform import Transform -from manimlib.mobject.types.vectorized_mobject import VMobject +from manimlib.constants import ORIGIN, RIGHT, UP +from manimlib.constants import SMALL_BUFF +from manimlib.constants import TAU +from manimlib.constants import GREY, YELLOW from manimlib.mobject.geometry import Circle from manimlib.mobject.geometry import Dot +from manimlib.mobject.geometry import Line from manimlib.mobject.shape_matchers import SurroundingRectangle from manimlib.mobject.shape_matchers import Underline +from manimlib.mobject.types.vectorized_mobject import VMobject from manimlib.mobject.types.vectorized_mobject import VGroup -from manimlib.mobject.geometry import Line from manimlib.utils.bezier import interpolate from manimlib.utils.config_ops import digest_config -from manimlib.utils.rate_functions import there_and_back -from manimlib.utils.rate_functions import wiggle from manimlib.utils.rate_functions import smooth from manimlib.utils.rate_functions import squish_rate_func +from manimlib.utils.rate_functions import there_and_back +from manimlib.utils.rate_functions import wiggle from typing import TYPE_CHECKING if TYPE_CHECKING: - import colour + from colour import Color + from typing import Union + from manimlib.mobject.mobject import Mobject - ManimColor = Union[str, colour.Color, Sequence[float]] + + ManimColor = Union[str, Color] class FocusOn(Transform): diff --git a/manimlib/animation/movement.py b/manimlib/animation/movement.py index 78cbbee8..0edf38b1 100644 --- a/manimlib/animation/movement.py +++ b/manimlib/animation/movement.py @@ -1,14 +1,15 @@ from __future__ import annotations -from typing import Callable, Sequence - from manimlib.animation.animation import Animation from manimlib.utils.rate_functions import linear from typing import TYPE_CHECKING if TYPE_CHECKING: + from typing import Callable, Sequence + import numpy as np + from manimlib.mobject.mobject import Mobject diff --git a/manimlib/animation/numbers.py b/manimlib/animation/numbers.py index 0a992b39..5b6e9223 100644 --- a/manimlib/animation/numbers.py +++ b/manimlib/animation/numbers.py @@ -1,11 +1,14 @@ from __future__ import annotations -from typing import Callable - from manimlib.animation.animation import Animation from manimlib.mobject.numbers import DecimalNumber from manimlib.utils.bezier import interpolate +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Callable + class ChangingDecimal(Animation): CONFIG = { diff --git a/manimlib/animation/rotation.py b/manimlib/animation/rotation.py index 7993c3cf..058d9066 100644 --- a/manimlib/animation/rotation.py +++ b/manimlib/animation/rotation.py @@ -1,10 +1,8 @@ from __future__ import annotations from manimlib.animation.animation import Animation -from manimlib.constants import OUT -from manimlib.constants import PI -from manimlib.constants import TAU -from manimlib.constants import ORIGIN +from manimlib.constants import ORIGIN, OUT +from manimlib.constants import PI, TAU from manimlib.utils.rate_functions import linear from manimlib.utils.rate_functions import smooth @@ -12,6 +10,7 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: import numpy as np + from manimlib.mobject.mobject import Mobject diff --git a/manimlib/animation/specialized.py b/manimlib/animation/specialized.py index 376e37e2..26f26868 100644 --- a/manimlib/animation/specialized.py +++ b/manimlib/animation/specialized.py @@ -1,15 +1,17 @@ from __future__ import annotations -import numpy as np - from manimlib.animation.composition import LaggedStart from manimlib.animation.transform import Restore -from manimlib.constants import WHITE -from manimlib.constants import BLACK +from manimlib.constants import BLACK, WHITE from manimlib.mobject.geometry import Circle from manimlib.mobject.types.vectorized_mobject import VGroup from manimlib.utils.config_ops import digest_config +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + import numpy as np + class Broadcast(LaggedStart): CONFIG = { diff --git a/manimlib/animation/transform.py b/manimlib/animation/transform.py index a426b21e..37307a0c 100644 --- a/manimlib/animation/transform.py +++ b/manimlib/animation/transform.py @@ -1,15 +1,13 @@ from __future__ import annotations import inspect -from typing import Callable, Union, Sequence import numpy as np -import numpy.typing as npt from manimlib.animation.animation import Animation from manimlib.constants import DEFAULT_POINTWISE_FUNCTION_RUN_TIME -from manimlib.constants import OUT from manimlib.constants import DEGREES +from manimlib.constants import OUT from manimlib.mobject.mobject import Group from manimlib.mobject.mobject import Mobject from manimlib.utils.config_ops import digest_config @@ -21,9 +19,14 @@ from manimlib.utils.rate_functions import squish_rate_func from typing import TYPE_CHECKING if TYPE_CHECKING: - import colour + from colour import Color + from typing import Callable, Union + + import numpy.typing as npt + from manimlib.scene.scene import Scene - ManimColor = Union[str, colour.Color, Sequence[float]] + + ManimColor = Union[str, Color] class Transform(Animation): diff --git a/manimlib/animation/update.py b/manimlib/animation/update.py index 43fafa42..2a929584 100644 --- a/manimlib/animation/update.py +++ b/manimlib/animation/update.py @@ -1,13 +1,12 @@ from __future__ import annotations -import operator as op -from typing import Callable - from manimlib.animation.animation import Animation from typing import TYPE_CHECKING if TYPE_CHECKING: + from typing import Callable + from manimlib.mobject.mobject import Mobject @@ -47,10 +46,7 @@ class MaintainPositionRelativeTo(Animation): **kwargs ): self.tracked_mobject = tracked_mobject - self.diff = op.sub( - mobject.get_center(), - tracked_mobject.get_center(), - ) + self.diff = mobject.get_center() - tracked_mobject.get_center() super().__init__(mobject, **kwargs) def interpolate_mobject(self, alpha: float) -> None: diff --git a/manimlib/camera/camera.py b/manimlib/camera/camera.py index 2884216c..ab1e2d33 100644 --- a/manimlib/camera/camera.py +++ b/manimlib/camera/camera.py @@ -1,19 +1,23 @@ from __future__ import annotations -import moderngl -from colour import Color -import OpenGL.GL as gl +import itertools as it import math -import itertools as it - +import moderngl import numpy as np -from scipy.spatial.transform import Rotation +import OpenGL.GL as gl from PIL import Image +from scipy.spatial.transform import Rotation -from manimlib.constants import * +from manimlib.constants import BLACK +from manimlib.constants import DEGREES, RADIANS +from manimlib.constants import DEFAULT_FRAME_RATE +from manimlib.constants import DEFAULT_PIXEL_HEIGHT, DEFAULT_PIXEL_WIDTH +from manimlib.constants import FRAME_HEIGHT, FRAME_WIDTH +from manimlib.constants import DOWN, LEFT, ORIGIN, OUT, RIGHT, UP from manimlib.mobject.mobject import Mobject from manimlib.mobject.mobject import Point +from manimlib.utils.color import color_to_rgba from manimlib.utils.config_ops import digest_config from manimlib.utils.simple_functions import fdiv from manimlib.utils.space_ops import normalize @@ -189,10 +193,9 @@ class Camera(object): def __init__(self, ctx: moderngl.Context | None = None, **kwargs): digest_config(self, kwargs, locals()) self.rgb_max_val: float = np.iinfo(self.pixel_array_dtype).max - self.background_rgba: list[float] = [ - *Color(self.background_color).get_rgb(), - self.background_opacity - ] + self.background_rgba: list[float] = list(color_to_rgba( + self.background_color, self.background_opacity + )) self.init_frame() self.init_context(ctx) self.init_shaders() diff --git a/manimlib/config.py b/manimlib/config.py index a2e68c51..1f863b8e 100644 --- a/manimlib/config.py +++ b/manimlib/config.py @@ -1,16 +1,16 @@ import argparse import colour -import inspect +from contextlib import contextmanager import importlib +import inspect import os +from screeninfo import get_monitors import sys import yaml -from contextlib import contextmanager -from screeninfo import get_monitors +from manimlib.logger import log from manimlib.utils.config_ops import merge_dicts_recursively from manimlib.utils.init_config import init_customization -from manimlib.logger import log __config_file__ = "custom_config.yml" @@ -117,16 +117,19 @@ def parse_cli(): ) parser.add_argument( "-n", "--start_at_animation_number", - help="Start rendering not from the first animation, but" - "from another, specified by its index. If you pass" - "in two comma separated values, e.g. \"3,6\", it will end" + help="Start rendering not from the first animation, but " + "from another, specified by its index. If you pass " + "in two comma separated values, e.g. \"3,6\", it will end " "the rendering at the second value", ) parser.add_argument( - "-e", "--embed", metavar="LINENO", - help="Takes a line number as an argument, and results" - "in the scene being called as if the line `self.embed()`" - "was inserted into the scene code at that line number." + "-e", "--embed", + nargs="?", + const="", + help="Creates a new file where the line `self.embed` is inserted " + "into the Scenes construct method. " + "If a string is passed in, the line will be inserted below the " + "last line of code including that string." ) parser.add_argument( "-r", "--resolution", @@ -185,22 +188,70 @@ def get_module(file_name): return module +def get_indent(line: str): + return len(line) - len(line.lstrip()) + + @contextmanager -def insert_embed_line(file_name, lineno): +def insert_embed_line(file_name: str, scene_name: str, line_marker: str): + """ + This is hacky, but convenient. When user includes the argument "-e", it will try + to recreate a file that inserts the line `self.embed()` into the end of the scene's + construct method. If there is an argument passed in, it will insert the line after + the last line in the sourcefile which includes that string. + """ with open(file_name, 'r') as fp: lines = fp.readlines() - line = lines[lineno - 1] - n_spaces = len(line) - len(line.lstrip()) - lines.insert(lineno, " " * n_spaces + "self.embed()\n") - - alt_file = file_name.replace(".py", "_inserted_embed.py") - with open(alt_file, 'w') as fp: - fp.writelines(lines) try: - yield alt_file + scene_line_number = next( + i for i, line in enumerate(lines) + if line.startswith(f"class {scene_name}") + ) + except StopIteration: + log.error(f"No scene {scene_name}") + + prev_line_num = None + n_spaces = None + if len(line_marker) == 0: + # Find the end of the construct method + in_construct = False + for index in range(scene_line_number, len(lines) - 1): + line = lines[index] + if line.lstrip().startswith("def construct"): + in_construct = True + n_spaces = get_indent(line) + 4 + elif in_construct: + if len(line.strip()) > 0 and get_indent(line) < n_spaces: + prev_line_num = index - 2 + break + elif line_marker.isdigit(): + # Treat the argument as a line number + prev_line_num = int(line_marker) - 1 + elif len(line_marker) > 0: + # Treat the argument as a string + try: + prev_line_num = next( + i + for i in range(len(lines) - 1, scene_line_number, -1) + if line_marker in lines[i] + ) + except StopIteration: + log.error(f"No lines matching {line_marker}") + sys.exit(2) + + # Insert and write new file + if n_spaces is None: + n_spaces = get_indent(lines[prev_line_num]) + new_lines = list(lines) + new_lines.insert(prev_line_num + 1, " " * n_spaces + "self.embed()\n") + with open(file_name, 'w') as fp: + fp.writelines(new_lines) + try: + yield file_name finally: - os.remove(alt_file) + with open(file_name, 'w') as fp: + fp.writelines(lines) def get_custom_config(): @@ -296,10 +347,10 @@ def get_configuration(args): "quiet": args.quiet, } - if args.embed is None: - module = get_module(args.file) - else: - with insert_embed_line(args.file, int(args.embed)) as alt_file: + module = get_module(args.file) + + if args.embed is not None: + with insert_embed_line(args.file, args.scene_names[0], args.embed) as alt_file: module = get_module(alt_file) config = { diff --git a/manimlib/constants.py b/manimlib/constants.py index cc73c0ac..f0bc3269 100644 --- a/manimlib/constants.py +++ b/manimlib/constants.py @@ -1,5 +1,6 @@ import numpy as np + # Sizes relevant to default camera frame ASPECT_RATIO = 16.0 / 9.0 FRAME_HEIGHT = 8.0 @@ -74,6 +75,7 @@ DEFAULT_STROKE_WIDTH = 4 # For keyboard interactions CTRL_SYMBOL = 65508 SHIFT_SYMBOL = 65505 +COMMAND_SYMBOL = 65517 DELETE_SYMBOL = 65288 ARROW_SYMBOLS = list(range(65361, 65365)) diff --git a/manimlib/event_handler/event_dispatcher.py b/manimlib/event_handler/event_dispatcher.py index 34eb55eb..2ec7c49d 100644 --- a/manimlib/event_handler/event_dispatcher.py +++ b/manimlib/event_handler/event_dispatcher.py @@ -2,8 +2,8 @@ from __future__ import annotations import numpy as np -from manimlib.event_handler.event_type import EventType from manimlib.event_handler.event_listner import EventListner +from manimlib.event_handler.event_type import EventType class EventDispatcher(object): diff --git a/manimlib/event_handler/event_listner.py b/manimlib/event_handler/event_listner.py index 4552cf8c..6f32121c 100644 --- a/manimlib/event_handler/event_listner.py +++ b/manimlib/event_handler/event_listner.py @@ -1,10 +1,13 @@ from __future__ import annotations -from typing import Callable, TYPE_CHECKING +from typing import TYPE_CHECKING if TYPE_CHECKING: - from manimlib.mobject.mobject import Mobject + from typing import Callable + from manimlib.event_handler.event_type import EventType + from manimlib.mobject.mobject import Mobject + class EventListner(object): def __init__( diff --git a/manimlib/extract_scene.py b/manimlib/extract_scene.py index abec96ec..a3686ebc 100644 --- a/manimlib/extract_scene.py +++ b/manimlib/extract_scene.py @@ -1,13 +1,14 @@ +import copy import inspect import sys -import copy -from manimlib.scene.scene import Scene from manimlib.config import get_custom_config from manimlib.logger import log +from manimlib.scene.interactive_scene import InteractiveScene +from manimlib.scene.scene import Scene -class BlankScene(Scene): +class BlankScene(InteractiveScene): def construct(self): exec(get_custom_config()["universal_import_line"]) self.embed() diff --git a/manimlib/logger.py b/manimlib/logger.py index b04ae7ae..71567a1d 100644 --- a/manimlib/logger.py +++ b/manimlib/logger.py @@ -1,4 +1,5 @@ import logging + from rich.logging import RichHandler __all__ = ["log"] diff --git a/manimlib/mobject/changing.py b/manimlib/mobject/changing.py index 76d92bab..7a954708 100644 --- a/manimlib/mobject/changing.py +++ b/manimlib/mobject/changing.py @@ -1,19 +1,18 @@ from __future__ import annotations -from typing import Callable - import numpy as np -from manimlib.constants import BLUE_D -from manimlib.constants import BLUE_B -from manimlib.constants import BLUE_E -from manimlib.constants import GREY_BROWN -from manimlib.constants import WHITE +from manimlib.constants import BLUE_B, BLUE_D, BLUE_E, GREY_BROWN, WHITE from manimlib.mobject.mobject import Mobject -from manimlib.mobject.types.vectorized_mobject import VMobject from manimlib.mobject.types.vectorized_mobject import VGroup +from manimlib.mobject.types.vectorized_mobject import VMobject from manimlib.utils.rate_functions import smooth +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Callable + class AnimatedBoundary(VGroup): CONFIG = { diff --git a/manimlib/mobject/coordinate_systems.py b/manimlib/mobject/coordinate_systems.py index 2d21d9d4..09a84a97 100644 --- a/manimlib/mobject/coordinate_systems.py +++ b/manimlib/mobject/coordinate_systems.py @@ -1,16 +1,20 @@ from __future__ import annotations +from abc import ABC, abstractmethod import numbers -from abc import abstractmethod -from typing import Type, TypeVar, Union, Callable, Iterable, Sequence import numpy as np -from manimlib.constants import * +from manimlib.constants import BLACK, BLUE, BLUE_D, GREEN, GREY_A, WHITE +from manimlib.constants import DEGREES, PI +from manimlib.constants import DL, DOWN, DR, LEFT, ORIGIN, OUT, RIGHT, UP +from manimlib.constants import FRAME_HEIGHT, FRAME_WIDTH +from manimlib.constants import FRAME_X_RADIUS, FRAME_Y_RADIUS +from manimlib.constants import MED_SMALL_BUFF, SMALL_BUFF from manimlib.mobject.functions import ParametricCurve from manimlib.mobject.geometry import Arrow -from manimlib.mobject.geometry import Line from manimlib.mobject.geometry import DashedLine +from manimlib.mobject.geometry import Line from manimlib.mobject.geometry import Rectangle from manimlib.mobject.number_line import NumberLine from manimlib.mobject.svg.tex_mobject import Tex @@ -25,16 +29,19 @@ from manimlib.utils.space_ops import rotate_vector from typing import TYPE_CHECKING if TYPE_CHECKING: - import colour + from colour import Color + from typing import Callable, Iterable, Sequence, Type, TypeVar, Union + from manimlib.mobject.mobject import Mobject + T = TypeVar("T", bound=Mobject) - ManimColor = Union[str, colour.Color, Sequence[float]] + ManimColor = Union[str, Color] EPSILON = 1e-8 -class CoordinateSystem(): +class CoordinateSystem(ABC): """ Abstract class for Axes and NumberPlane """ diff --git a/manimlib/mobject/frame.py b/manimlib/mobject/frame.py index 7523ab51..fc496695 100644 --- a/manimlib/mobject/frame.py +++ b/manimlib/mobject/frame.py @@ -1,4 +1,5 @@ -from manimlib.constants import * +from manimlib.constants import BLACK, GREY_E +from manimlib.constants import FRAME_HEIGHT from manimlib.mobject.geometry import Rectangle from manimlib.utils.config_ops import digest_config diff --git a/manimlib/mobject/functions.py b/manimlib/mobject/functions.py index ec374ed0..9ec232a7 100644 --- a/manimlib/mobject/functions.py +++ b/manimlib/mobject/functions.py @@ -1,13 +1,18 @@ from __future__ import annotations -from typing import Callable, Sequence - from isosurfaces import plot_isoline +import numpy as np -from manimlib.constants import * +from manimlib.constants import FRAME_X_RADIUS, FRAME_Y_RADIUS +from manimlib.constants import YELLOW from manimlib.mobject.types.vectorized_mobject import VMobject from manimlib.utils.config_ops import digest_config +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Callable, Sequence + class ParametricCurve(VMobject): CONFIG = { diff --git a/manimlib/mobject/geometry.py b/manimlib/mobject/geometry.py index 81670fad..eaad71e7 100644 --- a/manimlib/mobject/geometry.py +++ b/manimlib/mobject/geometry.py @@ -2,23 +2,24 @@ from __future__ import annotations import math import numbers -from typing import Sequence, Union -import colour import numpy as np -from manimlib.constants import * +from manimlib.constants import DL, DOWN, DR, LEFT, ORIGIN, OUT, RIGHT, UL, UP, UR +from manimlib.constants import GREY_A, RED, WHITE +from manimlib.constants import MED_SMALL_BUFF +from manimlib.constants import PI, TAU from manimlib.mobject.mobject import Mobject +from manimlib.mobject.types.vectorized_mobject import DashedVMobject from manimlib.mobject.types.vectorized_mobject import VGroup from manimlib.mobject.types.vectorized_mobject import VMobject -from manimlib.mobject.types.vectorized_mobject import DashedVMobject from manimlib.utils.config_ops import digest_config from manimlib.utils.iterables import adjacent_n_tuples from manimlib.utils.iterables import adjacent_pairs -from manimlib.utils.simple_functions import fdiv from manimlib.utils.simple_functions import clip -from manimlib.utils.space_ops import angle_of_vector +from manimlib.utils.simple_functions import fdiv from manimlib.utils.space_ops import angle_between_vectors +from manimlib.utils.space_ops import angle_of_vector from manimlib.utils.space_ops import compass_directions from manimlib.utils.space_ops import find_intersection from manimlib.utils.space_ops import get_norm @@ -26,7 +27,13 @@ from manimlib.utils.space_ops import normalize from manimlib.utils.space_ops import rotate_vector from manimlib.utils.space_ops import rotation_matrix_transpose -ManimColor = Union[str, colour.Color, Sequence[float]] +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from colour import Color + from typing import Union + + ManimColor = Union[str, Color] DEFAULT_DOT_RADIUS = 0.08 @@ -716,8 +723,8 @@ class Arrow(Line): def set_stroke( self, - color: ManimColor | None = None, - width: float | None = None, + color: ManimColor | Iterable[ManimColor] | None = None, + width: float | Iterable[float] | None = None, *args, **kwargs ): super().set_stroke(color=color, width=width, *args, **kwargs) diff --git a/manimlib/mobject/interactive.py b/manimlib/mobject/interactive.py index b50425ef..fd3a3b3b 100644 --- a/manimlib/mobject/interactive.py +++ b/manimlib/mobject/interactive.py @@ -1,22 +1,32 @@ from __future__ import annotations -from typing import Callable - import numpy as np from pyglet.window import key as PygletWindowKeys from manimlib.constants import FRAME_HEIGHT, FRAME_WIDTH -from manimlib.constants import LEFT, RIGHT, UP, DOWN, ORIGIN -from manimlib.constants import SMALL_BUFF, MED_SMALL_BUFF, MED_LARGE_BUFF -from manimlib.constants import BLACK, GREY_A, GREY_C, RED, GREEN, BLUE, WHITE -from manimlib.mobject.mobject import Mobject, Group -from manimlib.mobject.types.vectorized_mobject import VGroup -from manimlib.mobject.geometry import Dot, Line, Square, Rectangle, RoundedRectangle, Circle +from manimlib.constants import DOWN, LEFT, ORIGIN, RIGHT, UP +from manimlib.constants import MED_LARGE_BUFF, MED_SMALL_BUFF, SMALL_BUFF +from manimlib.constants import BLACK, BLUE, GREEN, GREY_A, GREY_C, RED, WHITE +from manimlib.mobject.mobject import Group +from manimlib.mobject.mobject import Mobject +from manimlib.mobject.geometry import Circle +from manimlib.mobject.geometry import Dot +from manimlib.mobject.geometry import Line +from manimlib.mobject.geometry import Rectangle +from manimlib.mobject.geometry import RoundedRectangle +from manimlib.mobject.geometry import Square from manimlib.mobject.svg.text_mobject import Text +from manimlib.mobject.types.vectorized_mobject import VGroup from manimlib.mobject.value_tracker import ValueTracker +from manimlib.utils.color import rgb_to_hex from manimlib.utils.config_ops import digest_config -from manimlib.utils.space_ops import get_norm, get_closest_point_on_line -from manimlib.utils.color import rgb_to_color, color_to_rgba, rgb_to_hex +from manimlib.utils.space_ops import get_closest_point_on_line +from manimlib.utils.space_ops import get_norm + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Callable # Interactive Mobjects @@ -336,7 +346,7 @@ class ColorSliders(Group): g = self.g_slider.get_value() / 255 b = self.b_slider.get_value() / 255 alpha = self.a_slider.get_value() - return color_to_rgba(rgb_to_color((r, g, b)), alpha=alpha) + return np.array((r, g, b, alpha)) def get_picked_color(self) -> str: rgba = self.get_value() diff --git a/manimlib/mobject/matrix.py b/manimlib/mobject/matrix.py index b53cba0a..76688a4d 100644 --- a/manimlib/mobject/matrix.py +++ b/manimlib/mobject/matrix.py @@ -1,12 +1,12 @@ from __future__ import annotations import itertools as it -from typing import Union, Sequence import numpy as np -import numpy.typing as npt -from manimlib.constants import * +from manimlib.constants import DEFAULT_MOBJECT_TO_MOBJECT_BUFFER +from manimlib.constants import DOWN, LEFT, RIGHT, UP +from manimlib.constants import WHITE from manimlib.mobject.numbers import DecimalNumber from manimlib.mobject.numbers import Integer from manimlib.mobject.shape_matchers import BackgroundRectangle @@ -18,9 +18,14 @@ from manimlib.mobject.types.vectorized_mobject import VMobject from typing import TYPE_CHECKING if TYPE_CHECKING: - import colour + from colour import Color + from typing import Union + + import numpy.typing as npt + from manimlib.mobject.mobject import Mobject - ManimColor = Union[str, colour.Color, Sequence[float]] + + ManimColor = Union[str, Color] VECTOR_LABEL_SCALE_FACTOR = 0.8 diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index d09773ed..c074c9f7 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -1,50 +1,61 @@ from __future__ import annotations import copy -import sys -import random -import itertools as it from functools import wraps -from typing import Iterable, Callable, Union, Sequence -import pickle +import itertools as it import os +import pickle +import random +import sys -import colour import moderngl +import numbers import numpy as np -import numpy.typing as npt -from manimlib.constants import * +from manimlib.constants import DEFAULT_MOBJECT_TO_EDGE_BUFFER +from manimlib.constants import DEFAULT_MOBJECT_TO_MOBJECT_BUFFER +from manimlib.constants import DOWN, IN, LEFT, ORIGIN, OUT, RIGHT, UP +from manimlib.constants import FRAME_X_RADIUS, FRAME_Y_RADIUS +from manimlib.constants import MED_SMALL_BUFF +from manimlib.constants import TAU +from manimlib.constants import WHITE +from manimlib.event_handler import EVENT_DISPATCHER +from manimlib.event_handler.event_listner import EventListner +from manimlib.event_handler.event_type import EventType +from manimlib.logger import log +from manimlib.shader_wrapper import get_colormap_code +from manimlib.shader_wrapper import ShaderWrapper from manimlib.utils.color import color_gradient +from manimlib.utils.color import color_to_rgb from manimlib.utils.color import get_colormap_list from manimlib.utils.color import rgb_to_hex -from manimlib.utils.color import color_to_rgb from manimlib.utils.config_ops import digest_config from manimlib.utils.iterables import batch_by_property from manimlib.utils.iterables import list_update +from manimlib.utils.iterables import listify from manimlib.utils.iterables import resize_array from manimlib.utils.iterables import resize_preserving_order from manimlib.utils.iterables import resize_with_interpolation -from manimlib.utils.iterables import listify -from manimlib.utils.bezier import interpolate from manimlib.utils.bezier import integer_interpolate +from manimlib.utils.bezier import interpolate from manimlib.utils.paths import straight_path from manimlib.utils.simple_functions import get_parameters from manimlib.utils.space_ops import angle_of_vector from manimlib.utils.space_ops import get_norm from manimlib.utils.space_ops import rotation_matrix_transpose -from manimlib.shader_wrapper import ShaderWrapper -from manimlib.shader_wrapper import get_colormap_code -from manimlib.event_handler import EVENT_DISPATCHER -from manimlib.event_handler.event_listner import EventListner -from manimlib.event_handler.event_type import EventType -from manimlib.logger import log +from typing import TYPE_CHECKING -TimeBasedUpdater = Callable[["Mobject", float], None] -NonTimeUpdater = Callable[["Mobject"], None] -Updater = Union[TimeBasedUpdater, NonTimeUpdater] -ManimColor = Union[str, colour.Color, Sequence[float]] +if TYPE_CHECKING: + from colour import Color + from typing import Callable, Iterable, Sequence, Union + + import numpy.typing as npt + + TimeBasedUpdater = Callable[["Mobject", float], None] + NonTimeUpdater = Callable[["Mobject"], None] + Updater = Union[TimeBasedUpdater, NonTimeUpdater] + ManimColor = Union[str, Color] class Mobject(object): @@ -84,7 +95,8 @@ class Mobject(object): self.locked_data_keys: set[str] = set() self.needs_new_bounding_box: bool = True self._is_animating: bool = False - self._is_movable: bool = False + self.saved_state = None + self.target = None self.init_data() self.init_uniforms() @@ -136,8 +148,10 @@ class Mobject(object): return self def set_uniforms(self, uniforms: dict): - for key in uniforms: - self.uniforms[key] = uniforms[key] # Copy? + for key, value in uniforms.items(): + if isinstance(value, np.ndarray): + value = value.copy() + self.uniforms[key] = value return self @property @@ -460,66 +474,94 @@ class Mobject(object): self.assemble_family() return self - # Creating new Mobjects from this one + # Copying and serialization - def replicate(self, n: int) -> Group: - return self.get_group_class()( - *(self.copy() for x in range(n)) - ) + def stash_mobject_pointers(func): + @wraps(func) + def wrapper(self, *args, **kwargs): + uncopied_attrs = ["parents", "target", "saved_state"] + stash = dict() + for attr in uncopied_attrs: + if hasattr(self, attr): + value = getattr(self, attr) + stash[attr] = value + null_value = [] if isinstance(value, list) else None + setattr(self, attr, null_value) + result = func(self, *args, **kwargs) + self.__dict__.update(stash) + return result + return wrapper - 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 + @stash_mobject_pointers + def serialize(self): + return pickle.dumps(self) - # Copying + def deserialize(self, data: bytes): + self.become(pickle.loads(data)) + return self - def copy(self): - self.parents = [] + def deepcopy(self): try: + # Often faster than deepcopy return pickle.loads(pickle.dumps(self)) except AttributeError: return copy.deepcopy(self) - def deepcopy(self): - # This used to be different from copy, so is now just here for backward compatibility - return self.copy() + @stash_mobject_pointers + def copy(self, deep: bool = False): + if deep: + return self.deepcopy() + + result = copy.copy(self) + + # The line above is only a shallow copy, so the internal + # data which are numpyu arrays or other mobjects still + # need to be further copied. + result.data = { + key: np.array(value) + for key, value in self.data.items() + } + result.uniforms = { + key: np.array(value) + for key, value in self.uniforms.items() + } + + result.submobjects = [] + result.add(*(sm.copy() for sm in self.submobjects)) + result.match_updaters(self) + + family = self.get_family() + for attr, value in list(self.__dict__.items()): + if isinstance(value, Mobject) and value is not self: + if value in family: + setattr(result, attr, result.family[self.family.index(value)]) + else: + setattr(result, attr, value.copy()) + if isinstance(value, np.ndarray): + setattr(result, attr, value.copy()) + if isinstance(value, ShaderWrapper): + setattr(result, attr, value.copy()) + return result def generate_target(self, use_deepcopy: bool = False): - # TODO, remove now pointless use_deepcopy arg - self.target = None # Prevent exponential explosion - self.target = self.copy() + self.target = self.copy(deep=use_deepcopy) + self.target.saved_state = self.saved_state return self.target def save_state(self, use_deepcopy: bool = False): - # TODO, remove now pointless use_deepcopy arg - if hasattr(self, "saved_state"): - # Prevent exponential growth of data - self.saved_state = None - self.saved_state = self.copy() + self.saved_state = self.copy(deep=use_deepcopy) + self.saved_state.target = self.target return self def restore(self): - if not hasattr(self, "saved_state") or self.save_state is None: + if not hasattr(self, "saved_state") or self.saved_state is None: raise Exception("Trying to restore without having saved") self.become(self.saved_state) 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 + def save_to_file(self, file_path: str, supress_overwrite_warning: bool = False): with open(file_path, "wb") as fp: - pickle.dump(self, fp) + fp.write(self.serialize()) log.info(f"Saved mobject to {file_path}") return self @@ -532,6 +574,39 @@ class Mobject(object): mobject = pickle.load(fp) return mobject + def become(self, mobject: Mobject): + """ + Edit all data and submobjects to be idential + to another mobject + """ + self.align_family(mobject) + for sm1, sm2 in zip(self.get_family(), mobject.get_family()): + sm1.set_data(sm2.data) + sm1.set_uniforms(sm2.uniforms) + sm1.shader_folder = sm2.shader_folder + sm1.texture_paths = sm2.texture_paths + sm1.depth_test = sm2.depth_test + sm1.render_primitive = sm2.render_primitive + self.refresh_bounding_box(recurse_down=True) + return self + + # Creating new Mobjects from this one + + def replicate(self, n: int) -> Group: + group_class = self.get_group_class() + return group_class(*(self.copy() for _ in range(n))) + + def get_grid(self, n_rows: int, n_cols: int, height: float | None = None, **kwargs) -> Group: + """ + 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 + # Updating def init_updaters(self): @@ -634,21 +709,13 @@ class Mobject(object): # 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 + return self._is_animating or self.has_updaters 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 def shift(self, vector: np.ndarray): @@ -675,10 +742,10 @@ class Mobject(object): Otherwise, if about_point is given a value, scaling is done with respect to that point. """ - if isinstance(scale_factor, Iterable): - scale_factor = np.array(scale_factor).clip(min=min_scale_factor) - else: + if isinstance(scale_factor, numbers.Number): scale_factor = max(scale_factor, min_scale_factor) + else: + scale_factor = np.array(scale_factor).clip(min=min_scale_factor) self.apply_points_function( lambda points: scale_factor * points, about_point=about_point, @@ -1064,8 +1131,8 @@ class Mobject(object): def set_rgba_array_by_color( self, - color: ManimColor | None = None, - opacity: float | None = None, + color: ManimColor | Iterable[ManimColor] | None = None, + opacity: float | Iterable[float] | None = None, name: str = "rgbas", recurse: bool = True ): @@ -1087,7 +1154,12 @@ class Mobject(object): mob.data[name][:, 3] = resize_array(opacities, size) return self - def set_color(self, color: ManimColor, opacity: float | None = None, recurse: bool = True): + def set_color( + self, + color: ManimColor | Iterable[ManimColor] | None, + opacity: float | Iterable[float] | None = None, + recurse: bool = True + ): self.set_rgba_array_by_color(color, opacity, recurse=False) # Recurse to submobjects differently from how set_rgba_array_by_color # in case they implement set_color differently @@ -1096,7 +1168,11 @@ class Mobject(object): submob.set_color(color, recurse=True) return self - def set_opacity(self, opacity: float, recurse: bool = True): + def set_opacity( + self, + opacity: float | Iterable[float] | None, + recurse: bool = True + ): self.set_rgba_array_by_color(color=None, opacity=opacity, recurse=False) if recurse: for submob in self.submobjects: @@ -1203,7 +1279,7 @@ class Mobject(object): 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]]) + for indices in it.product([0, 2], repeat=3) ]) def get_center(self) -> np.ndarray: @@ -1519,18 +1595,6 @@ class Mobject(object): """ pass # To implement in subclass - def become(self, mobject: Mobject): - """ - Edit all data and submobjects to be idential - to another mobject - """ - self.align_family(mobject) - for sm1, sm2 in zip(self.get_family(), mobject.get_family()): - sm1.set_data(sm2.data) - sm1.set_uniforms(sm2.uniforms) - self.refresh_bounding_box(recurse_down=True) - return self - # Locking data def lock_data(self, keys: Iterable[str]): diff --git a/manimlib/mobject/mobject_update_utils.py b/manimlib/mobject/mobject_update_utils.py index b32a0ff3..4fdcfecd 100644 --- a/manimlib/mobject/mobject_update_utils.py +++ b/manimlib/mobject/mobject_update_utils.py @@ -1,7 +1,6 @@ from __future__ import annotations import inspect -from typing import Callable from manimlib.constants import DEGREES from manimlib.constants import RIGHT @@ -11,7 +10,10 @@ from manimlib.utils.simple_functions import clip from typing import TYPE_CHECKING if TYPE_CHECKING: + from typing import Callable + import numpy as np + from manimlib.animation.animation import Animation diff --git a/manimlib/mobject/number_line.py b/manimlib/mobject/number_line.py index bc96b55a..e16382e6 100644 --- a/manimlib/mobject/number_line.py +++ b/manimlib/mobject/number_line.py @@ -1,8 +1,10 @@ from __future__ import annotations -from typing import Iterable, Sequence +import numpy as np -from manimlib.constants import * +from manimlib.constants import DOWN, LEFT, RIGHT, UP +from manimlib.constants import GREY_B +from manimlib.constants import MED_SMALL_BUFF from manimlib.mobject.geometry import Line from manimlib.mobject.numbers import DecimalNumber from manimlib.mobject.types.vectorized_mobject import VGroup @@ -12,6 +14,11 @@ from manimlib.utils.config_ops import digest_config from manimlib.utils.config_ops import merge_dicts_recursively from manimlib.utils.simple_functions import fdiv +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Iterable, Sequence + class NumberLine(Line): CONFIG = { diff --git a/manimlib/mobject/numbers.py b/manimlib/mobject/numbers.py index beac837c..6d88c647 100644 --- a/manimlib/mobject/numbers.py +++ b/manimlib/mobject/numbers.py @@ -1,13 +1,18 @@ from __future__ import annotations -from typing import TypeVar, Type +import numpy as np -from manimlib.constants import * +from manimlib.constants import DOWN, LEFT, RIGHT, UP from manimlib.mobject.svg.tex_mobject import SingleStringTex from manimlib.mobject.svg.text_mobject import Text from manimlib.mobject.types.vectorized_mobject import VMobject -T = TypeVar("T", bound=VMobject) +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Type, TypeVar + + T = TypeVar("T", bound=VMobject) class DecimalNumber(VMobject): diff --git a/manimlib/mobject/probability.py b/manimlib/mobject/probability.py index 3edb068b..ac921025 100644 --- a/manimlib/mobject/probability.py +++ b/manimlib/mobject/probability.py @@ -1,9 +1,8 @@ from __future__ import annotations -from typing import Iterable, Union, Sequence -import colour - -from manimlib.constants import * +from manimlib.constants import BLUE, BLUE_E, GREEN_E, GREY_B, GREY_D, MAROON_B, YELLOW +from manimlib.constants import DOWN, LEFT, RIGHT, UP +from manimlib.constants import MED_LARGE_BUFF, MED_SMALL_BUFF, SMALL_BUFF from manimlib.mobject.geometry import Line from manimlib.mobject.geometry import Rectangle from manimlib.mobject.mobject import Mobject @@ -14,7 +13,14 @@ from manimlib.mobject.types.vectorized_mobject import VGroup from manimlib.utils.color import color_gradient from manimlib.utils.iterables import listify -ManimColor = Union[str, colour.Color, Sequence[float]] +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from colour import Color + from typing import Iterable, Union + + ManimColor = Union[str, Color] + EPSILON = 0.0001 diff --git a/manimlib/mobject/shape_matchers.py b/manimlib/mobject/shape_matchers.py index a1ffe5fd..78cc8100 100644 --- a/manimlib/mobject/shape_matchers.py +++ b/manimlib/mobject/shape_matchers.py @@ -1,20 +1,25 @@ from __future__ import annotations -from manimlib.constants import * +from colour import Color + +from manimlib.constants import BLACK, RED, YELLOW +from manimlib.constants import DL, DOWN, DR, LEFT, RIGHT, UL, UR +from manimlib.constants import SMALL_BUFF from manimlib.mobject.geometry import Line from manimlib.mobject.geometry import Rectangle from manimlib.mobject.types.vectorized_mobject import VGroup from manimlib.mobject.types.vectorized_mobject import VMobject -from manimlib.utils.color import Color -from manimlib.utils.customization import get_customization from manimlib.utils.config_ops import digest_config +from manimlib.utils.customization import get_customization from typing import TYPE_CHECKING if TYPE_CHECKING: - from typing import Union, Sequence + from typing import Union + from manimlib.mobject.mobject import Mobject - ManimColor = Union[str, Color, Sequence[float]] + + ManimColor = Union[str, Color] class SurroundingRectangle(Rectangle): diff --git a/manimlib/mobject/svg/brace.py b/manimlib/mobject/svg/brace.py index 659b0604..e49cebaf 100644 --- a/manimlib/mobject/svg/brace.py +++ b/manimlib/mobject/svg/brace.py @@ -2,27 +2,32 @@ from __future__ import annotations import math import copy -from typing import Iterable import numpy as np -from manimlib.constants import * +from manimlib.constants import DEFAULT_MOBJECT_TO_MOBJECT_BUFFER, SMALL_BUFF +from manimlib.constants import DOWN, LEFT, ORIGIN, RIGHT, UP +from manimlib.constants import PI +from manimlib.animation.composition import AnimationGroup from manimlib.animation.fading import FadeIn from manimlib.animation.growing import GrowFromCenter -from manimlib.animation.composition import AnimationGroup -from manimlib.mobject.svg.tex_mobject import Tex from manimlib.mobject.svg.tex_mobject import SingleStringTex +from manimlib.mobject.svg.tex_mobject import Tex from manimlib.mobject.svg.tex_mobject import TexText from manimlib.mobject.svg.text_mobject import Text from manimlib.mobject.types.vectorized_mobject import VMobject from manimlib.utils.config_ops import digest_config +from manimlib.utils.iterables import listify from manimlib.utils.space_ops import get_norm from typing import TYPE_CHECKING if TYPE_CHECKING: - from manimlib.mobject.mobject import Mobject + from typing import Iterable + from manimlib.animation.animation import Animation + from manimlib.mobject.mobject import Mobject + class Brace(SingleStringTex): CONFIG = { @@ -113,8 +118,8 @@ class BraceLabel(VMobject): def __init__( self, - obj: list[VMobject] | Mobject, - text: Iterable[str] | str, + obj: VMobject | list[VMobject], + text: str | Iterable[str], brace_direction: np.ndarray = DOWN, **kwargs ) -> None: @@ -124,12 +129,8 @@ class BraceLabel(VMobject): obj = VMobject(*obj) self.brace = Brace(obj, brace_direction, **kwargs) - if isinstance(text, Iterable): - self.label = self.label_constructor(*text, **kwargs) - else: - self.label = self.label_constructor(str(text)) - if self.label_scale != 1: - self.label.scale(self.label_scale) + self.label = self.label_constructor(*listify(text), **kwargs) + self.label.scale(self.label_scale) self.brace.put_at_tip(self.label, buff=self.label_buff) self.set_submobjects([self.brace, self.label]) @@ -137,11 +138,11 @@ class BraceLabel(VMobject): def creation_anim( self, label_anim: Animation = FadeIn, - brace_anim: Animation=GrowFromCenter + brace_anim: Animation = GrowFromCenter ) -> AnimationGroup: return AnimationGroup(brace_anim(self.brace), label_anim(self.label)) - def shift_brace(self, obj: list[VMobject] | Mobject, **kwargs): + def shift_brace(self, obj: VMobject | list[VMobject], **kwargs): if isinstance(obj, list): obj = VMobject(*obj) self.brace = Brace(obj, self.brace_direction, **kwargs) @@ -158,7 +159,7 @@ class BraceLabel(VMobject): self.submobjects[1] = self.label return self - def change_brace_label(self, obj: list[VMobject] | Mobject, *text: str): + def change_brace_label(self, obj: VMobject | list[VMobject], *text: str): self.shift_brace(obj) self.change_label(*text) return self diff --git a/manimlib/mobject/svg/drawings.py b/manimlib/mobject/svg/drawings.py index 6eee3428..ed2102c3 100644 --- a/manimlib/mobject/svg/drawings.py +++ b/manimlib/mobject/svg/drawings.py @@ -20,8 +20,8 @@ from manimlib.utils.config_ops import digest_config from manimlib.utils.rate_functions import linear from manimlib.utils.space_ops import angle_of_vector from manimlib.utils.space_ops import complex_to_R3 -from manimlib.utils.space_ops import rotate_vector from manimlib.utils.space_ops import midpoint +from manimlib.utils.space_ops import rotate_vector class Checkmark(TexText): diff --git a/manimlib/mobject/svg/svg_mobject.py b/manimlib/mobject/svg/svg_mobject.py index b44c107f..5a918e66 100644 --- a/manimlib/mobject/svg/svg_mobject.py +++ b/manimlib/mobject/svg/svg_mobject.py @@ -1,27 +1,24 @@ from __future__ import annotations -import os import hashlib -import itertools as it -from typing import Callable +import os from xml.etree import ElementTree as ET -import svgelements as se import numpy as np +import svgelements as se from manimlib.constants import RIGHT -from manimlib.mobject.geometry import Line +from manimlib.logger import log from manimlib.mobject.geometry import Circle +from manimlib.mobject.geometry import Line from manimlib.mobject.geometry import Polygon from manimlib.mobject.geometry import Polyline from manimlib.mobject.geometry import Rectangle from manimlib.mobject.geometry import RoundedRectangle from manimlib.mobject.types.vectorized_mobject import VMobject -from manimlib.utils.config_ops import digest_config from manimlib.utils.directories import get_mobject_data_dir from manimlib.utils.images import get_full_vector_image_path from manimlib.utils.iterables import hash_obj -from manimlib.logger import log SVG_HASH_TO_MOB_MAP: dict[int, VMobject] = {} @@ -199,9 +196,9 @@ class SVGMobject(VMobject): ) -> VMobject: mob.set_style( stroke_width=shape.stroke_width, - stroke_color=shape.stroke.hex, + stroke_color=shape.stroke.hexrgb, stroke_opacity=shape.stroke.opacity, - fill_color=shape.fill.hex, + fill_color=shape.fill.hexrgb, fill_opacity=shape.fill.opacity ) return mob diff --git a/manimlib/mobject/svg/tex_mobject.py b/manimlib/mobject/svg/tex_mobject.py index 619f5bc9..fb444608 100644 --- a/manimlib/mobject/svg/tex_mobject.py +++ b/manimlib/mobject/svg/tex_mobject.py @@ -1,21 +1,28 @@ from __future__ import annotations -from typing import Iterable, Sequence, Union from functools import reduce import operator as op -import colour import re -from manimlib.constants import * +from manimlib.constants import BLACK, WHITE +from manimlib.constants import DOWN, LEFT, RIGHT, UP +from manimlib.constants import FRAME_WIDTH +from manimlib.constants import MED_LARGE_BUFF, MED_SMALL_BUFF, SMALL_BUFF from manimlib.mobject.geometry import Line from manimlib.mobject.svg.svg_mobject import SVGMobject from manimlib.mobject.types.vectorized_mobject import VGroup from manimlib.utils.config_ops import digest_config -from manimlib.utils.tex_file_writing import tex_to_svg_file -from manimlib.utils.tex_file_writing import get_tex_config from manimlib.utils.tex_file_writing import display_during_execution +from manimlib.utils.tex_file_writing import get_tex_config +from manimlib.utils.tex_file_writing import tex_to_svg_file -ManimColor = Union[str, colour.Color, Sequence[float]] +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from colour import Color + from typing import Iterable, Union + + ManimColor = Union[str, Color] SCALE_FACTOR_PER_FONT_POINT = 0.001 diff --git a/manimlib/mobject/three_dimensions.py b/manimlib/mobject/three_dimensions.py index c6c2d946..3a1a8010 100644 --- a/manimlib/mobject/three_dimensions.py +++ b/manimlib/mobject/three_dimensions.py @@ -2,19 +2,23 @@ from __future__ import annotations import math -from manimlib.constants import * -from manimlib.mobject.types.surface import Surface +import numpy as np + +from manimlib.constants import BLUE, BLUE_D, BLUE_E +from manimlib.constants import IN, ORIGIN, OUT, RIGHT +from manimlib.constants import PI, TAU from manimlib.mobject.types.surface import SGroup +from manimlib.mobject.types.surface import Surface from manimlib.mobject.types.vectorized_mobject import VGroup from manimlib.mobject.types.vectorized_mobject import VMobject -from manimlib.mobject.geometry import Square from manimlib.mobject.geometry import Polygon +from manimlib.mobject.geometry import Square from manimlib.utils.bezier import interpolate from manimlib.utils.config_ops import digest_config from manimlib.utils.iterables import adjacent_pairs +from manimlib.utils.space_ops import compass_directions from manimlib.utils.space_ops import get_norm from manimlib.utils.space_ops import z_to_vector -from manimlib.utils.space_ops import compass_directions class SurfaceMesh(VGroup): diff --git a/manimlib/mobject/types/dot_cloud.py b/manimlib/mobject/types/dot_cloud.py index 3e48aa8f..5975b3ee 100644 --- a/manimlib/mobject/types/dot_cloud.py +++ b/manimlib/mobject/types/dot_cloud.py @@ -1,15 +1,18 @@ from __future__ import annotations -import numpy as np -import numpy.typing as npt import moderngl +import numpy as np -from manimlib.constants import GREY_C -from manimlib.constants import YELLOW +from manimlib.constants import GREY_C, YELLOW from manimlib.constants import ORIGIN from manimlib.mobject.types.point_cloud_mobject import PMobject from manimlib.utils.iterables import resize_preserving_order +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + import numpy.typing as npt + DEFAULT_DOT_RADIUS = 0.05 DEFAULT_GLOW_DOT_RADIUS = 0.2 diff --git a/manimlib/mobject/types/image_mobject.py b/manimlib/mobject/types/image_mobject.py index 54166d36..dd993319 100644 --- a/manimlib/mobject/types/image_mobject.py +++ b/manimlib/mobject/types/image_mobject.py @@ -3,7 +3,7 @@ from __future__ import annotations import numpy as np from PIL import Image -from manimlib.constants import * +from manimlib.constants import DL, DR, UL, UR from manimlib.mobject.mobject import Mobject from manimlib.utils.bezier import inverse_interpolate from manimlib.utils.images import get_full_raster_image_path diff --git a/manimlib/mobject/types/point_cloud_mobject.py b/manimlib/mobject/types/point_cloud_mobject.py index 5de41173..f05ca8ea 100644 --- a/manimlib/mobject/types/point_cloud_mobject.py +++ b/manimlib/mobject/types/point_cloud_mobject.py @@ -1,19 +1,22 @@ from __future__ import annotations -from typing import Callable, Sequence, Union - -import colour -import numpy.typing as npt - -from manimlib.constants import * +from manimlib.constants import BLACK +from manimlib.constants import ORIGIN from manimlib.mobject.mobject import Mobject from manimlib.utils.color import color_gradient from manimlib.utils.color import color_to_rgba -from manimlib.utils.iterables import resize_with_interpolation from manimlib.utils.iterables import resize_array +from manimlib.utils.iterables import resize_with_interpolation +from typing import TYPE_CHECKING -ManimColor = Union[str, colour.Color, Sequence[float]] +if TYPE_CHECKING: + from colour import Color + from typing import Callable, Union + + import numpy.typing as npt + + ManimColor = Union[str, Color] class PMobject(Mobject): diff --git a/manimlib/mobject/types/surface.py b/manimlib/mobject/types/surface.py index cc3e32d5..339f62f1 100644 --- a/manimlib/mobject/types/surface.py +++ b/manimlib/mobject/types/surface.py @@ -1,12 +1,10 @@ from __future__ import annotations -from typing import Iterable, Callable - import moderngl import numpy as np -import numpy.typing as npt -from manimlib.constants import * +from manimlib.constants import GREY +from manimlib.constants import OUT from manimlib.mobject.mobject import Mobject from manimlib.utils.bezier import integer_interpolate from manimlib.utils.bezier import interpolate @@ -17,6 +15,10 @@ from manimlib.utils.space_ops import normalize_along_axis from typing import TYPE_CHECKING if TYPE_CHECKING: + from typing import Callable, Iterable + + import numpy.typing as npt + from manimlib.camera.camera import Camera diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index 6615f715..92363ae0 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -1,30 +1,36 @@ from __future__ import annotations -import operator as op +from functools import reduce +from functools import wraps import itertools as it -from functools import reduce, wraps -from typing import Iterable, Sequence, Callable, Union +import operator as op -import colour import moderngl -import numpy.typing as npt +import numpy as np -from manimlib.constants import * +from manimlib.constants import GREY_C +from manimlib.constants import GREY_E +from manimlib.constants import BLACK, WHITE +from manimlib.constants import DEFAULT_STROKE_WIDTH +from manimlib.constants import DEGREES +from manimlib.constants import JOINT_TYPE_MAP +from manimlib.constants import ORIGIN, OUT, UP from manimlib.mobject.mobject import Mobject from manimlib.mobject.mobject import Point from manimlib.utils.bezier import bezier -from manimlib.utils.bezier import get_smooth_quadratic_bezier_handle_points -from manimlib.utils.bezier import get_smooth_cubic_bezier_handle_points from manimlib.utils.bezier import get_quadratic_approximation_of_cubic +from manimlib.utils.bezier import get_smooth_cubic_bezier_handle_points +from manimlib.utils.bezier import get_smooth_quadratic_bezier_handle_points +from manimlib.utils.bezier import integer_interpolate from manimlib.utils.bezier import interpolate from manimlib.utils.bezier import inverse_interpolate -from manimlib.utils.bezier import integer_interpolate from manimlib.utils.bezier import partial_quadratic_bezier_points +from manimlib.utils.color import color_gradient from manimlib.utils.color import rgb_to_hex +from manimlib.utils.iterables import listify from manimlib.utils.iterables import make_even from manimlib.utils.iterables import resize_array from manimlib.utils.iterables import resize_with_interpolation -from manimlib.utils.iterables import listify from manimlib.utils.space_ops import angle_between_vectors from manimlib.utils.space_ops import cross2d from manimlib.utils.space_ops import earclip_triangulation @@ -33,8 +39,15 @@ from manimlib.utils.space_ops import get_unit_normal from manimlib.utils.space_ops import z_to_vector from manimlib.shader_wrapper import ShaderWrapper +from typing import TYPE_CHECKING -ManimColor = Union[str, colour.Color, Sequence[float]] +if TYPE_CHECKING: + from colour import Color + from typing import Callable, Iterable, Sequence, Union + + import numpy.typing as npt + + ManimColor = Union[str, Color] class VMobject(Mobject): @@ -130,8 +143,8 @@ class VMobject(Mobject): def set_fill( self, - color: ManimColor | None = None, - opacity: float | None = None, + color: ManimColor | Iterable[ManimColor] | None = None, + opacity: float | Iterable[float] | None = None, recurse: bool = True ): self.set_rgba_array_by_color(color, opacity, 'fill_rgba', recurse) @@ -139,9 +152,9 @@ class VMobject(Mobject): def set_stroke( self, - color: ManimColor | None = None, - width: float | npt.ArrayLike | None = None, - opacity: float | None = None, + color: ManimColor | Iterable[ManimColor] | None = None, + width: float | Iterable[float] | None = None, + opacity: float | Iterable[float] | None = None, background: bool | None = None, recurse: bool = True ): @@ -162,8 +175,8 @@ class VMobject(Mobject): def set_backstroke( self, - color: ManimColor = BLACK, - width: float | npt.ArrayLike = 3, + color: ManimColor | Iterable[ManimColor] = BLACK, + width: float | Iterable[float] = 3, background: bool = True ): self.set_stroke(color, width, background=background) @@ -177,13 +190,13 @@ class VMobject(Mobject): def set_style( self, - fill_color: ManimColor | None = None, - fill_opacity: float | None = None, + fill_color: ManimColor | Iterable[ManimColor] | None = None, + fill_opacity: float | Iterable[float] | None = None, fill_rgba: npt.ArrayLike | None = None, - stroke_color: ManimColor | None = None, - stroke_opacity: float | None = None, + stroke_color: ManimColor | Iterable[ManimColor] | None = None, + stroke_opacity: float | Iterable[float] | None = None, stroke_rgba: npt.ArrayLike | None = None, - stroke_width: float | npt.ArrayLike | None = None, + stroke_width: float | Iterable[float] | None = None, stroke_background: bool = True, reflectiveness: float | None = None, gloss: float | None = None, @@ -247,12 +260,21 @@ class VMobject(Mobject): sm1.match_style(sm2) return self - def set_color(self, color: ManimColor, recurse: bool = True): - self.set_fill(color, recurse=recurse) - self.set_stroke(color, recurse=recurse) + def set_color( + self, + color: ManimColor | Iterable[ManimColor] | None, + opacity: float | Iterable[float] | None = None, + recurse: bool = True + ): + self.set_fill(color, opacity=opacity, recurse=recurse) + self.set_stroke(color, opacity=opacity, recurse=recurse) return self - def set_opacity(self, opacity: float, recurse: bool = True): + def set_opacity( + self, + opacity: float | Iterable[float] | None, + recurse: bool = True + ): self.set_fill(opacity=opacity, recurse=recurse) self.set_stroke(opacity=opacity, recurse=recurse) return self @@ -1174,3 +1196,24 @@ class DashedVMobject(VMobject): # Family is already taken care of by get_subcurve # implementation self.match_style(vmobject, recurse=False) + + +class VHighlight(VGroup): + def __init__( + self, + vmobject: VMobject, + n_layers: int = 3, + color_bounds: tuple[ManimColor] = (GREY_C, GREY_E), + max_stroke_width: float = 10.0, + ): + outline = vmobject.replicate(n_layers) + outline.set_fill(opacity=0) + added_widths = np.linspace(0, max_stroke_width, n_layers + 1)[1:] + colors = color_gradient(color_bounds, n_layers) + for part, added_width, color in zip(reversed(outline), added_widths, colors): + for sm in part.family_members_with_points(): + part.set_stroke( + width=sm.get_stroke_width() + added_width, + color=color, + ) + super().__init__(*outline) diff --git a/manimlib/mobject/vector_field.py b/manimlib/mobject/vector_field.py index e657e84b..a41f2da2 100644 --- a/manimlib/mobject/vector_field.py +++ b/manimlib/mobject/vector_field.py @@ -1,23 +1,21 @@ from __future__ import annotations import itertools as it -import random -from typing import Sequence, TypeVar, Callable, Iterable import numpy as np -import numpy.typing as npt -from manimlib.constants import * +from manimlib.constants import FRAME_HEIGHT, FRAME_WIDTH +from manimlib.constants import WHITE from manimlib.animation.composition import AnimationGroup from manimlib.animation.indication import VShowPassingFlash from manimlib.mobject.geometry import Arrow from manimlib.mobject.types.vectorized_mobject import VGroup from manimlib.mobject.types.vectorized_mobject import VMobject -from manimlib.utils.bezier import inverse_interpolate from manimlib.utils.bezier import interpolate +from manimlib.utils.bezier import inverse_interpolate from manimlib.utils.color import get_colormap_list -from manimlib.utils.config_ops import merge_dicts_recursively from manimlib.utils.config_ops import digest_config +from manimlib.utils.config_ops import merge_dicts_recursively from manimlib.utils.rate_functions import linear from manimlib.utils.simple_functions import sigmoid from manimlib.utils.space_ops import get_norm @@ -25,8 +23,13 @@ from manimlib.utils.space_ops import get_norm from typing import TYPE_CHECKING if TYPE_CHECKING: - from manimlib.mobject.mobject import Mobject + from typing import Callable, Iterable, Sequence, TypeVar + + import numpy.typing as npt + from manimlib.mobject.coordinate_systems import CoordinateSystem + from manimlib.mobject.mobject import Mobject + T = TypeVar("T") @@ -299,7 +302,7 @@ class AnimatedStreamLines(VGroup): **self.line_anim_config, ) line.anim.begin() - line.time = -self.lag_range * random.random() + line.time = -self.lag_range * np.random.random() self.add(line.anim.mobject) self.add_updater(lambda m, dt: m.update(dt)) diff --git a/manimlib/once_useful_constructs/fractals.py b/manimlib/once_useful_constructs/fractals.py index 6285f554..57512262 100644 --- a/manimlib/once_useful_constructs/fractals.py +++ b/manimlib/once_useful_constructs/fractals.py @@ -1,4 +1,5 @@ from functools import reduce +import random from manimlib.constants import * # from manimlib.for_3b1b_videos.pi_creature import PiCreature diff --git a/manimlib/scene/interactive_scene.py b/manimlib/scene/interactive_scene.py index d1cc6d1f..68603add 100644 --- a/manimlib/scene/interactive_scene.py +++ b/manimlib/scene/interactive_scene.py @@ -1,41 +1,43 @@ -import numpy as np import itertools as it +import numpy as np import pyperclip -import os -import platform from manimlib.animation.fading import FadeIn -from manimlib.constants import MANIM_COLORS, WHITE, YELLOW -from manimlib.constants import ORIGIN, UP, DOWN, LEFT, RIGHT, DL, UL, UR, DR +from manimlib.constants import ARROW_SYMBOLS, CTRL_SYMBOL, DELETE_SYMBOL, SHIFT_SYMBOL +from manimlib.constants import COMMAND_MODIFIER, SHIFT_MODIFIER +from manimlib.constants import DL, DOWN, DR, LEFT, ORIGIN, RIGHT, UL, UP, UR from manimlib.constants import FRAME_WIDTH, SMALL_BUFF -from manimlib.constants import SHIFT_SYMBOL, DELETE_SYMBOL, ARROW_SYMBOLS -from manimlib.constants import SHIFT_MODIFIER, COMMAND_MODIFIER -from manimlib.mobject.mobject import Mobject +from manimlib.constants import MANIM_COLORS, WHITE, GREY_C from manimlib.mobject.geometry import Rectangle from manimlib.mobject.geometry import Square from manimlib.mobject.mobject import Group +from manimlib.mobject.mobject import Mobject +from manimlib.mobject.numbers import DecimalNumber 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.mobject.types.vectorized_mobject import VGroup +from manimlib.mobject.types.vectorized_mobject import VHighlight +from manimlib.mobject.types.vectorized_mobject import VMobject 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 +from manimlib.utils.tex_file_writing import LatexError SELECT_KEY = 's' GRAB_KEY = 'g' -HORIZONTAL_GRAB_KEY = 'h' -VERTICAL_GRAB_KEY = 'v' +X_GRAB_KEY = 'h' +Y_GRAB_KEY = 'v' +GRAB_KEYS = [GRAB_KEY, X_GRAB_KEY, Y_GRAB_KEY] RESIZE_KEY = 't' COLOR_KEY = 'c' +CURSOR_LOCATION_KEY = 'l' # Note, a lot of the functionality here is still buggy and very much a work in progress. + class InteractiveScene(Scene): """ To select mobjects on screen, hold ctrl and move the mouse to highlight a region, @@ -66,36 +68,102 @@ class InteractiveScene(Scene): selection_rectangle_stroke_width = 1.0 colors = MANIM_COLORS selection_nudge_size = 0.05 + cursor_location_config = dict( + font_size=14, + fill_color=GREY_C, + num_decimal_places=3, + ) 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.cursor_location_label = self.get_cursor_location_label() self.unselectables = [ self.selection, self.selection_highlight, self.selection_rectangle, + self.cursor_location_label, self.camera.frame ] - self.saved_selection_state = [] self.select_top_level_mobs = True + self.regenerate_selection_search_set() self.is_selecting = False + self.is_grabbing = False self.add(self.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 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 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_cursor_location_label(self): + decimals = VGroup(*( + DecimalNumber(**self.cursor_location_config) + for n in range(3) + )) + + def update_coords(decimals): + for mob, coord in zip(decimals, self.mouse_point.get_location()): + mob.set_value(coord) + decimals.arrange(RIGHT, buff=decimals.get_height()) + decimals.to_corner(DR, buff=SMALL_BUFF) + decimals.fix_in_frame() + return decimals + + decimals.add_updater(update_coords) + return decimals + + # Related to selection + def toggle_selection_mode(self): self.select_top_level_mobs = not self.select_top_level_mobs self.refresh_selection_scope() + self.regenerate_selection_search_set() - def get_selection_search_set(self): - mobs = [m for m in self.mobjects if m not in self.unselectables] + def get_selection_search_set(self) -> list[Mobject]: + return self.selection_search_set + + def regenerate_selection_search_set(self): + selectable = list(filter( + lambda m: m not in self.unselectables, + self.mobjects + )) if self.select_top_level_mobs: - return mobs + self.selection_search_set = selectable else: - return [ + self.selection_search_set = [ submob - for mob in mobs + for mob in selectable for submob in mob.family_members_with_points() ] @@ -116,37 +184,7 @@ class InteractiveScene(Scene): ) 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): + def get_corner_dots(self, mobject: Mobject) -> Mobject: dots = DotCloud(**self.corner_dot_config) radius = self.corner_dot_config["radius"] if mobject.get_depth() < 1e-2: @@ -159,9 +197,11 @@ class InteractiveScene(Scene): ])) return dots - def get_highlight(self, mobject): - if isinstance(mobject, VMobject) and mobject.has_points(): - return self.get_stroke_highlight(mobject) + def get_highlight(self, mobject: Mobject) -> Mobject: + if isinstance(mobject, VMobject) and mobject.has_points() and not self.select_top_level_mobs: + result = VHighlight(mobject) + result.add_updater(lambda m: m.replace(mobject)) + return result else: return self.get_corner_dots(mobject) @@ -171,40 +211,54 @@ class InteractiveScene(Scene): 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): - mobs = list(filter(lambda m: m not in self.unselectables, mobjects)) - self.selection.add(*mobjects) + mobs = list(filter( + lambda m: m not in self.unselectables and m not in self.selection, + mobjects + )) + if len(mobs) == 0: + return + self.selection.add(*mobs) self.selection_highlight.add(*map(self.get_highlight, mobs)) - self.saved_selection_state = [(mob, mob.copy()) for mob in self.selection] + for mob in mobs: + mob.set_animating_status(True) + self.refresh_static_mobjects() def toggle_from_selection(self, *mobjects): for mob in mobjects: if mob in self.selection: self.selection.remove(mob) + mob.set_animating_status(False) else: self.add_to_selection(mob) self.refresh_selection_highlight() def clear_selection(self): + for mob in self.selection: + mob.set_animating_status(False) self.selection.set_submobjects([]) self.selection_highlight.set_submobjects([]) + self.refresh_static_mobjects() def add(self, *new_mobjects: Mobject): - for mob in new_mobjects: - mob.make_movable() super().add(*new_mobjects) + self.regenerate_selection_search_set() - # Selection operations + def remove(self, *mobjects: Mobject): + super().remove(*mobjects) + self.regenerate_selection_search_set() + + def disable_interaction(self, *mobjects: Mobject): + for mob in mobjects: + self.unselectables.append(mob) + self.regenerate_selection_search_set() + + def enable_interaction(self, *mobjects: Mobject): + for mob in mobjects: + if mob in self.unselectables: + self.unselectables.remove(mob) + + # Functions for keyboard actions def copy_selection(self): ids = map(id, self.selection) @@ -218,11 +272,11 @@ class InteractiveScene(Scene): 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 )) + self.add_to_selection(*mob_copies) return except ValueError: pass @@ -242,41 +296,27 @@ class InteractiveScene(Scene): 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 - if platform.system() == 'Darwin': - user_name = os.popen(f""" - osascript -e ' - set chosenfile to (choose file name default name "{file_name}" default location "{directory}") - POSIX path of chosenfile' - """).read() - user_name = user_name.replace("\n", "") - else: - 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) + def restore_state(self, mobject_states: list[tuple[Mobject, Mobject]]): + super().restore_state(mobject_states) self.refresh_selection_highlight() + def enable_selection(self): + self.is_selecting = True + self.add(self.selection_rectangle) + self.selection_rectangle.fixed_corner = self.mouse_point.get_center().copy() + + def gather_new_selection(self): + self.is_selecting = False + self.remove(self.selection_rectangle) + for mob in reversed(self.get_selection_search_set()): + if self.selection_rectangle.is_touching(mob): + self.add_to_selection(mob) + + def prepare_grab(self): + mp = self.mouse_point.get_center() + self.mouse_to_selection = mp - self.selection.get_center() + self.is_grabbing = True + def prepare_resizing(self, about_corner=False): center = self.selection.get_center() mp = self.mouse_point.get_center() @@ -286,136 +326,173 @@ class InteractiveScene(Scene): self.scale_about_point = center self.scale_ref_vect = mp - self.scale_about_point self.scale_ref_width = self.selection.get_width() + self.scale_ref_height = self.selection.get_height() - # Event handlers + def toggle_color_palette(self): + if len(self.selection) == 0: + return + if self.color_palette not in self.mobjects: + self.save_state() + self.add(self.color_palette) + else: + self.remove(self.color_palette) + + def group_selection(self): + group = self.get_group(*self.selection) + self.add(group) + self.clear_selection() + self.add_to_selection(group) + + def ungroup_selection(self): + 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) + + def nudge_selection(self, vect: np.ndarray, large: bool = False): + nudge = self.selection_nudge_size + if large: + nudge *= 10 + self.selection.shift(nudge * vect) + + def save_selection_to_file(self): + if len(self.selection) == 1: + self.save_mobject_to_file(self.selection[0]) + else: + self.save_mobject_to_file(self.selection) 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 + self.enable_selection() + elif char in GRAB_KEYS and modifiers == 0: + self.prepare_grab() 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 + self.toggle_color_palette() + elif char == CURSOR_LOCATION_KEY and modifiers == 0: + self.add(self.cursor_location_label) 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 + self.group_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 + self.ungroup_selection() 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 + self.save_selection_to_file() 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) + self.nudge_selection( + vect=[LEFT, UP, RIGHT, DOWN][ARROW_SYMBOLS.index(symbol)], + large=(modifiers & SHIFT_MODIFIER), + ) + + # Conditions for saving state + if char in [GRAB_KEY, X_GRAB_KEY, Y_GRAB_KEY, RESIZE_KEY]: + self.save_state() 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) + self.gather_new_selection() + if chr(symbol) in GRAB_KEYS: + self.is_grabbing = False + elif chr(symbol) == CURSOR_LOCATION_KEY: + self.remove(self.cursor_location_label) + elif symbol == SHIFT_SYMBOL and self.window.is_key_pressed(ord(RESIZE_KEY)): + self.prepare_resizing(about_corner=False) - elif symbol == SHIFT_SYMBOL: - if self.window.is_key_pressed(ord(RESIZE_KEY)): - self.prepare_resizing(about_corner=False) + # Mouse actions + def handle_grabbing(self, point: np.ndarray): + diff = point - self.mouse_to_selection + if self.window.is_key_pressed(ord(GRAB_KEY)): + self.selection.move_to(diff) + elif self.window.is_key_pressed(ord(X_GRAB_KEY)): + self.selection.set_x(diff[0]) + elif self.window.is_key_pressed(ord(Y_GRAB_KEY)): + self.selection.set_y(diff[1]) - 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 + def handle_resizing(self, point: np.ndarray): + vect = point - self.scale_about_point + if self.window.is_key_pressed(CTRL_SYMBOL): + for i in (0, 1): + scalar = vect[i] / self.scale_ref_vect[i] + self.selection.rescale_to_fit( + scalar * [self.scale_ref_width, self.scale_ref_height][i], + dim=i, + about_point=self.scale_about_point, + stretch=True, + ) + else: 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 handle_sweeping_selection(self, point: np.ndarray): + mob = self.point_to_mobject( + point, search_set=self.get_selection_search_set(), + buff=SMALL_BUFF + ) + if mob is not None: + self.add_to_selection(mob) + + def choose_color(self, point: np.ndarray): + # Search through all mobject on the screen, not just the palette + to_search = [ + sm + for mobject in self.mobjects + for sm in mobject.family_members_with_points() + 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_color()) + self.remove(self.color_palette) + + def toggle_clicked_mobject_from_selection(self, point: np.ndarray): + mob = self.point_to_mobject( + point, + search_set=self.get_selection_search_set(), + buff=SMALL_BUFF + ) + if mob is not None: + self.toggle_from_selection(mob) + + def on_mouse_motion(self, point: np.ndarray, d_point: np.ndarray) -> None: + super().on_mouse_motion(point, d_point) + if self.is_grabbing: + self.handle_grabbing(point) + elif self.window.is_key_pressed(ord(RESIZE_KEY)): + self.handle_resizing(point) + elif self.window.is_key_pressed(ord(SELECT_KEY)) and self.window.is_key_pressed(SHIFT_SYMBOL): + self.handle_sweeping_selection(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) + self.choose_color(point) elif self.window.is_key_pressed(SHIFT_SYMBOL): - mob = self.point_to_mobject(point) - if mob is not None: - self.toggle_from_selection(mob) + self.toggle_clicked_mobject_from_selection(point) else: self.clear_selection() diff --git a/manimlib/scene/sample_space_scene.py b/manimlib/scene/sample_space_scene.py index cc1c73f5..b68c23d9 100644 --- a/manimlib/scene/sample_space_scene.py +++ b/manimlib/scene/sample_space_scene.py @@ -2,10 +2,11 @@ from manimlib.animation.animation import Animation from manimlib.animation.transform import MoveToTarget from manimlib.animation.transform import Transform from manimlib.animation.update import UpdateFromFunc -from manimlib.constants import * -from manimlib.scene.scene import Scene +from manimlib.constants import DOWN, RIGHT +from manimlib.constants import MED_LARGE_BUFF, SMALL_BUFF from manimlib.mobject.probability import SampleSpace from manimlib.mobject.types.vectorized_mobject import VGroup +from manimlib.scene.scene import Scene class SampleSpaceScene(Scene): diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index 01037f62..a67c91e1 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -1,40 +1,42 @@ from __future__ import annotations -import time -import random -import inspect -import platform from functools import wraps -from typing import Iterable, Callable +import inspect import os +import platform +import random +import time -from tqdm import tqdm as ProgressDisplay import numpy as np +from tqdm import tqdm as ProgressDisplay from manimlib.animation.animation import prepare_animation from manimlib.animation.transform import MoveToTarget from manimlib.camera.camera import Camera -from manimlib.config import get_custom_config -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.constants import DEFAULT_WAIT_TIME +from manimlib.constants import COMMAND_MODIFIER +from manimlib.constants import SHIFT_MODIFIER +from manimlib.event_handler import EVENT_DISPATCHER +from manimlib.event_handler.event_type import EventType +from manimlib.logger import log +from manimlib.mobject.mobject import Group from manimlib.mobject.mobject import Mobject 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.mobject.types.vectorized_mobject import VMobject from manimlib.scene.scene_file_writer import SceneFileWriter from manimlib.utils.config_ops import digest_config from manimlib.utils.family_ops import extract_mobject_family_members from manimlib.utils.family_ops import restructure_list_to_exclude_certain_family_members -from manimlib.event_handler.event_type import EventType -from manimlib.event_handler import EVENT_DISPATCHER -from manimlib.logger import log from typing import TYPE_CHECKING if TYPE_CHECKING: + from typing import Callable, Iterable + from PIL.Image import Image + from manimlib.animation.animation import Animation @@ -43,7 +45,6 @@ FRAME_SHIFT_KEY = 'f' ZOOM_KEY = 'z' RESET_FRAME_KEY = 'r' QUIT_KEY = 'q' -EMBED_KEY = 'e' class Scene(object): @@ -62,6 +63,7 @@ class Scene(object): "presenter_mode": False, "linger_after_completion": True, "pan_sensitivity": 3, + "max_num_saved_states": 20, } def __init__(self, **kwargs): @@ -71,12 +73,15 @@ class Scene(object): self.window = Window(scene=self, **self.window_config) self.camera_config["ctx"] = self.window.ctx self.camera_config["frame_rate"] = 30 # Where's that 30 from? + self.undo_stack = [] + self.redo_stack = [] else: self.window = None self.camera: Camera = self.camera_class(**self.camera_config) self.file_writer = SceneFileWriter(self, **self.file_writer_config) self.mobjects: list[Mobject] = [self.camera.frame] + self.id_to_mobject_map: dict[int, Mobject] = dict() self.num_plays: int = 0 self.time: float = 0 self.skip_time: float = 0 @@ -88,12 +93,16 @@ class Scene(object): self.mouse_point = Point() self.mouse_drag_point = Point() self.hold_on_wait = self.presenter_mode + self.inside_embed = False # Much nicer to work with deterministic scenes if self.random_seed is not None: random.seed(self.random_seed) np.random.seed(self.random_seed) + def __str__(self) -> str: + return self.__class__.__name__ + def run(self) -> None: self.virtual_animation_start_time: float = 0 self.real_animation_start_time: float = time.time() @@ -143,40 +152,57 @@ class Scene(object): def embed(self, close_scene_on_exit: bool = True) -> None: if not self.preview: - # If the scene is just being - # written, ignore embed calls + # Ignore embed calls when there is no preview return + self.inside_embed = True self.stop_skipping() self.linger_after_completion = False self.update_frame() - - # Save scene state at the point of embedding self.save_state() - from IPython.terminal.embed import InteractiveShellEmbed - shell = InteractiveShellEmbed() - # 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()) - # Use the locals of the caller as the local namespace - # once embedded, and add a few custom shortcuts + # Configure and launch embedded IPython terminal + from IPython.terminal import embed, pt_inputhooks + shell = embed.InteractiveShellEmbed.instance() + + # Use the locals namespace of the caller local_ns = inspect.currentframe().f_back.f_locals - local_ns["touch"] = self.interact - local_ns["i2g"] = self.ids_to_group - for term in ("play", "wait", "add", "remove", "clear", "save_state", "restore"): - local_ns[term] = getattr(self, term) - 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()`") - exec(get_custom_config()["universal_import_line"]) + # Add a few custom shortcuts + local_ns.update({ + name: getattr(self, name) + for name in [ + "play", "wait", "add", "remove", "clear", + "save_state", "undo", "redo", "i2g", "i2m" + ] + }) + + # Enables gui interactions during the embed + def inputhook(context): + while not context.input_is_ready(): + if self.window.is_closing: + pass + # self.window.destroy() + else: + self.update_frame(dt=0) + + pt_inputhooks.register("manim", inputhook) + shell.enable_gui("manim") + + # Operation to run after each ipython command + def post_cell_func(*args, **kwargs): + self.refresh_static_mobjects() + + shell.events.register("post_run_cell", post_cell_func) + + # Launch shell, with stack_depth=2 indicating we should use caller globals/locals shell(local_ns=local_ns, stack_depth=2) + + self.inside_embed = False # End scene when exiting an embed if close_scene_on_exit: raise EndSceneEarlyException() - def __str__(self) -> str: - return self.__class__.__name__ - # Only these methods should touch the camera + def get_image(self) -> Image: return self.camera.get_image() @@ -207,6 +233,7 @@ class Scene(object): self.file_writer.write_frame(self.camera) # Related to updating + def update_mobjects(self, dt: float) -> None: for mobject in self.mobjects: mobject.update(dt) @@ -225,6 +252,7 @@ class Scene(object): ]) # Related to time + def get_time(self) -> float: return self.time @@ -232,6 +260,7 @@ class Scene(object): self.time += dt # Related to internal mobject organization + def get_top_level_mobjects(self) -> list[Mobject]: # Return only those which are not in the family # of another mobject from the scene @@ -256,6 +285,11 @@ class Scene(object): """ self.remove(*new_mobjects) self.mobjects += new_mobjects + self.id_to_mobject_map.update({ + id(sm): sm + for m in new_mobjects + for sm in m.get_family() + }) return self def add_mobjects_among(self, values: Iterable): @@ -319,11 +353,7 @@ class Scene(object): 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 + return self.id_to_mobject_map[id_value] def ids_to_group(self, *id_values): return self.get_group(*filter( @@ -331,7 +361,14 @@ class Scene(object): map(self.id_to_mobject, id_values) )) + def i2g(self, *id_values): + return self.ids_to_group(*id_values) + + def i2m(self, id_value): + return self.id_to_mobject(id_value) + # Related to skipping + def update_skipping_status(self) -> None: if self.start_at_animation_number is not None: if self.num_plays == self.start_at_animation_number: @@ -347,6 +384,7 @@ class Scene(object): self.skip_animations = False # Methods associated with running animations + def get_time_progression( self, run_time: float, @@ -470,6 +508,8 @@ class Scene(object): def handle_play_like_call(func): @wraps(func) def wrapper(self, *args, **kwargs): + if self.inside_embed: + self.save_state() self.update_skipping_status() should_write = not self.skip_animations if should_write: @@ -591,24 +631,39 @@ class Scene(object): self.file_writer.add_sound(sound_file, time, gain, gain_to_background) # Helpers for interactive development + + def get_state(self) -> list[tuple[Mobject, Mobject]]: + return [(mob, mob.copy()) for mob in self.mobjects] + + def restore_state(self, mobject_states: list[tuple[Mobject, Mobject]]): + self.mobjects = [mob.become(mob_copy) for mob, mob_copy in mobject_states] + def save_state(self) -> None: - self.saved_state = [ - (mob, mob.copy()) - for mob in self.mobjects - ] + if not self.preview: + return + self.redo_stack = [] + self.undo_stack.append(self.get_state()) + if len(self.undo_stack) > self.max_num_saved_states: + self.undo_stack.pop(0) - def restore(self) -> None: - if not hasattr(self, "saved_state"): - raise Exception("Trying to restore scene without having saved") - self.mobjects = [] - for mob, mob_state in self.saved_state: - mob.become(mob_state) - self.mobjects.append(mob) + def undo(self): + if self.undo_stack: + self.redo_stack.append(self.get_state()) + self.restore_state(self.undo_stack.pop()) + self.refresh_static_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 redo(self): + if self.redo_stack: + self.undo_stack.append(self.get_state()) + self.restore_state(self.redo_stack.pop()) + self.refresh_static_mobjects() + + def save_mobject_to_file(self, mobject: Mobject, file_path: str | None = None) -> None: + if file_path is None: + file_path = self.file_writer.get_saved_mobject_path(mobject) + if file_path is None: + return + mobject.save_to_file(file_path) def load_mobject(self, file_name): if os.path.exists(file_name): @@ -730,15 +785,16 @@ class Scene(object): if char == RESET_FRAME_KEY: self.camera.frame.to_default_state() + elif char == "z" and modifiers == COMMAND_MODIFIER: + self.undo() + elif char == "z" and modifiers == COMMAND_MODIFIER | SHIFT_MODIFIER: + self.redo() # command + q elif char == QUIT_KEY and modifiers == COMMAND_MODIFIER: self.quit_interaction = True # Space or right arrow elif char == " " or symbol == ARROW_SYMBOLS[2]: self.hold_on_wait = False - # ctrl + shift + e - elif char == EMBED_KEY and modifiers == CTRL_MODIFIER | SHIFT_MODIFIER: - self.embed(close_scene_on_exit=False) def on_resize(self, width: int, height: int) -> None: self.camera.reset_pixel_shape(width, height) diff --git a/manimlib/scene/scene_file_writer.py b/manimlib/scene/scene_file_writer.py index 869db27b..3a647fe9 100644 --- a/manimlib/scene/scene_file_writer.py +++ b/manimlib/scene/scene_file_writer.py @@ -1,30 +1,32 @@ from __future__ import annotations import os -import sys -import shutil import platform +import shutil import subprocess as sp +import sys import numpy as np from pydub import AudioSegment from tqdm import tqdm as ProgressDisplay from manimlib.constants import FFMPEG_BIN +from manimlib.logger import log +from manimlib.mobject.mobject import Mobject from manimlib.utils.config_ops import digest_config -from manimlib.utils.file_ops import guarantee_existence 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 -from manimlib.logger import log from typing import TYPE_CHECKING if TYPE_CHECKING: - from manimlib.scene.scene import Scene - from manimlib.camera.camera import Camera from PIL.Image import Image + from manimlib.camera.camera import Camera + from manimlib.scene.scene import Scene + class SceneFileWriter(object): CONFIG = { @@ -60,7 +62,7 @@ class SceneFileWriter(object): # Output directories and files def init_output_directories(self) -> None: - out_dir = self.output_directory + out_dir = self.output_directory or "" if self.mirror_module_path: module_dir = self.get_default_module_directory() out_dir = os.path.join(out_dir, module_dir) @@ -127,6 +129,36 @@ class SceneFileWriter(object): str(self.scene), )) + def get_saved_mobject_path(self, mobject: Mobject) -> str | None: + directory = self.get_saved_mobject_directory() + files = os.listdir(directory) + default_name = str(mobject) + "_0.mob" + index = 0 + while default_name in files: + default_name = default_name.replace(str(index), str(index + 1)) + index += 1 + if platform.system() == 'Darwin': + cmds = [ + "osascript", "-e", + f""" + set chosenfile to (choose file name default name "{default_name}" default location "{directory}") + POSIX path of chosenfile + """, + ] + process = sp.Popen(cmds, stdout=sp.PIPE) + file_path = process.stdout.read().decode("utf-8").split("\n")[0] + if not file_path: + return + else: + user_name = input(f"Enter mobject file name (default is {default_name}): ") + file_path = os.path.join(directory, user_name or default_name) + if os.path.exists(file_path) or os.path.exists(file_path + ".mob"): + if input(f"{file_path} already exists. Overwrite (y/n)? ") != "y": + return + if not file_path.endswith(".mob"): + file_path = file_path + ".mob" + return file_path + # Sound def init_audio(self) -> None: self.includes_sound: bool = False diff --git a/manimlib/scene/vector_space_scene.py b/manimlib/scene/vector_space_scene.py index e85c96d2..4b5a2efc 100644 --- a/manimlib/scene/vector_space_scene.py +++ b/manimlib/scene/vector_space_scene.py @@ -8,7 +8,10 @@ from manimlib.animation.growing import GrowArrow from manimlib.animation.transform import ApplyFunction from manimlib.animation.transform import ApplyPointwiseFunction from manimlib.animation.transform import Transform -from manimlib.constants import * +from manimlib.constants import BLACK, BLUE_D, GREEN_C, RED_C, GREY, WHITE, YELLOW +from manimlib.constants import DL, DOWN, ORIGIN, RIGHT, UP +from manimlib.constants import FRAME_WIDTH, FRAME_X_RADIUS, FRAME_Y_RADIUS +from manimlib.constants import SMALL_BUFF from manimlib.mobject.coordinate_systems import Axes from manimlib.mobject.coordinate_systems import NumberPlane from manimlib.mobject.geometry import Arrow @@ -30,6 +33,7 @@ from manimlib.utils.rate_functions import rush_into from manimlib.utils.space_ops import angle_of_vector from manimlib.utils.space_ops import get_norm + X_COLOR = GREEN_C Y_COLOR = RED_C Z_COLOR = BLUE_D diff --git a/manimlib/shader_wrapper.py b/manimlib/shader_wrapper.py index bd32a7ae..5ed2364f 100644 --- a/manimlib/shader_wrapper.py +++ b/manimlib/shader_wrapper.py @@ -1,9 +1,8 @@ from __future__ import annotations +import copy import os import re -import copy -from typing import Iterable import moderngl import numpy as np @@ -11,6 +10,12 @@ import numpy as np from manimlib.utils.directories import get_shader_dir from manimlib.utils.file_ops import find_file +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Iterable + + # Mobjects that should be rendered with # the same shader will be organized and # clumped together based on keeping track diff --git a/manimlib/utils/bezier.py b/manimlib/utils/bezier.py index 71d3d2b9..293b2228 100644 --- a/manimlib/utils/bezier.py +++ b/manimlib/utils/bezier.py @@ -1,19 +1,26 @@ from __future__ import annotations -from typing import Iterable, Callable, TypeVar, Sequence - -from scipy import linalg import numpy as np -import numpy.typing as npt +from scipy import linalg -from manimlib.utils.simple_functions import choose -from manimlib.utils.space_ops import find_intersection -from manimlib.utils.space_ops import cross2d -from manimlib.utils.space_ops import midpoint from manimlib.logger import log +from manimlib.utils.simple_functions import choose +from manimlib.utils.space_ops import cross2d +from manimlib.utils.space_ops import find_intersection +from manimlib.utils.space_ops import midpoint + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Callable, Iterable, Sequence, TypeVar + + import numpy.typing as npt + + T = TypeVar("T") + CLOSED_THRESHOLD = 0.001 -T = TypeVar("T") + def bezier( points: Iterable[float | np.ndarray] diff --git a/manimlib/utils/color.py b/manimlib/utils/color.py index 93cc9577..ae6517e0 100644 --- a/manimlib/utils/color.py +++ b/manimlib/utils/color.py @@ -1,15 +1,24 @@ -import random +from __future__ import annotations from colour import Color +from colour import hex2rgb +from colour import rgb2hex import numpy as np -from manimlib.constants import WHITE from manimlib.constants import COLORMAP_3B1B +from manimlib.constants import WHITE from manimlib.utils.bezier import interpolate from manimlib.utils.iterables import resize_with_interpolation +from typing import TYPE_CHECKING -def color_to_rgb(color): +if TYPE_CHECKING: + from typing import Iterable, Union + + ManimColor = Union[str, Color] + + +def color_to_rgb(color: ManimColor) -> np.ndarray: if isinstance(color, str): return hex_to_rgb(color) elif isinstance(color, Color): @@ -18,55 +27,48 @@ def color_to_rgb(color): raise Exception("Invalid color type") -def color_to_rgba(color, alpha=1): +def color_to_rgba(color: ManimColor, alpha: float = 1.0) -> np.ndarray: return np.array([*color_to_rgb(color), alpha]) -def rgb_to_color(rgb): +def rgb_to_color(rgb: Iterable[float]) -> Color: try: - return Color(rgb=rgb) + return Color(rgb=tuple(rgb)) except ValueError: return Color(WHITE) -def rgba_to_color(rgba): - return rgb_to_color(rgba[:3]) +def rgba_to_color(rgba: Iterable[float]) -> Color: + return rgb_to_color(tuple(rgba)[:3]) -def rgb_to_hex(rgb): - return "#" + "".join( - hex(int_x // 16)[2] + hex(int_x % 16)[2] - for x in rgb - for int_x in [int(255 * x)] - ) +def rgb_to_hex(rgb: Iterable[float]) -> str: + return rgb2hex(rgb, force_long=True).upper() -def hex_to_rgb(hex_code): - hex_part = hex_code[1:] - if len(hex_part) == 3: - hex_part = "".join([2 * c for c in hex_part]) - return np.array([ - int(hex_part[i:i + 2], 16) / 255 - for i in range(0, 6, 2) - ]) +def hex_to_rgb(hex_code: str) -> np.ndarray: + return np.array(hex2rgb(hex_code)) -def invert_color(color): +def invert_color(color: ManimColor) -> Color: return rgb_to_color(1.0 - color_to_rgb(color)) -def color_to_int_rgb(color): +def color_to_int_rgb(color: ManimColor) -> np.ndarray: return (255 * color_to_rgb(color)).astype('uint8') -def color_to_int_rgba(color, opacity=1.0): +def color_to_int_rgba(color: ManimColor, opacity: float = 1.0) -> np.ndarray: alpha = int(255 * opacity) return np.array([*color_to_int_rgb(color), alpha]) -def color_gradient(reference_colors, length_of_output): +def color_gradient( + reference_colors: Iterable[ManimColor], + length_of_output: int +) -> list[Color]: if length_of_output == 0: - return reference_colors[0] + return [] rgbs = list(map(color_to_rgb, reference_colors)) alphas = np.linspace(0, (len(rgbs) - 1), length_of_output) floors = alphas.astype('int') @@ -80,30 +82,33 @@ def color_gradient(reference_colors, length_of_output): ] -def interpolate_color(color1, color2, alpha): +def interpolate_color( + color1: ManimColor, + color2: ManimColor, + alpha: float +) -> Color: rgb = interpolate(color_to_rgb(color1), color_to_rgb(color2), alpha) return rgb_to_color(rgb) -def average_color(*colors): +def average_color(*colors: ManimColor) -> Color: rgbs = np.array(list(map(color_to_rgb, colors))) return rgb_to_color(rgbs.mean(0)) -def random_bright_color(): +def random_color() -> Color: + return Color(rgb=tuple(np.random.random(3))) + + +def random_bright_color() -> Color: color = random_color() - curr_rgb = color_to_rgb(color) - new_rgb = interpolate( - curr_rgb, np.ones(len(curr_rgb)), 0.5 - ) - return Color(rgb=new_rgb) + return average_color(color, Color(WHITE)) -def random_color(): - return Color(rgb=(random.random() for i in range(3))) - - -def get_colormap_list(map_name="viridis", n_colors=9): +def get_colormap_list( + map_name: str = "viridis", + n_colors: int = 9 +) -> np.ndarray: """ Options for map_name: 3b1b_colormap diff --git a/manimlib/utils/customization.py b/manimlib/utils/customization.py index bf79b1b8..94923b43 100644 --- a/manimlib/utils/customization.py +++ b/manimlib/utils/customization.py @@ -4,6 +4,7 @@ import tempfile from manimlib.config import get_custom_config from manimlib.config import get_manim_dir + CUSTOMIZATION = {} diff --git a/manimlib/utils/debug.py b/manimlib/utils/debug.py index 29aa6a3c..f877af8b 100644 --- a/manimlib/utils/debug.py +++ b/manimlib/utils/debug.py @@ -1,17 +1,18 @@ from __future__ import annotations -import time import numpy as np -from typing import Callable +import time from manimlib.constants import BLACK +from manimlib.logger import log from manimlib.mobject.numbers import Integer from manimlib.mobject.types.vectorized_mobject import VGroup -from manimlib.logger import log from typing import TYPE_CHECKING if TYPE_CHECKING: + from typing import Callable + from manimlib.mobject.mobject import Mobject diff --git a/manimlib/utils/directories.py b/manimlib/utils/directories.py index 87970523..daf1714c 100644 --- a/manimlib/utils/directories.py +++ b/manimlib/utils/directories.py @@ -2,8 +2,8 @@ from __future__ import annotations import os -from manimlib.utils.file_ops import guarantee_existence from manimlib.utils.customization import get_customization +from manimlib.utils.file_ops import guarantee_existence def get_directories() -> dict[str, str]: diff --git a/manimlib/utils/family_ops.py b/manimlib/utils/family_ops.py index 8468e74a..1f2e6b29 100644 --- a/manimlib/utils/family_ops.py +++ b/manimlib/utils/family_ops.py @@ -1,10 +1,10 @@ from __future__ import annotations -from typing import Iterable - from typing import TYPE_CHECKING if TYPE_CHECKING: + from typing import Iterable + from manimlib.mobject.mobject import Mobject diff --git a/manimlib/utils/file_ops.py b/manimlib/utils/file_ops.py index a50366bc..e1419e91 100644 --- a/manimlib/utils/file_ops.py +++ b/manimlib/utils/file_ops.py @@ -1,11 +1,15 @@ from __future__ import annotations import os -from typing import Iterable import numpy as np import validators +from typing import TYPE_CHECKING + +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 diff --git a/manimlib/utils/images.py b/manimlib/utils/images.py index cab0a45f..17f9628c 100644 --- a/manimlib/utils/images.py +++ b/manimlib/utils/images.py @@ -1,10 +1,16 @@ +from __future__ import annotations + import numpy as np from PIL import Image -from typing import Iterable -from manimlib.utils.file_ops import find_file from manimlib.utils.directories import get_raster_image_dir from manimlib.utils.directories import get_vector_image_dir +from manimlib.utils.file_ops import find_file + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Iterable def get_full_raster_image_path(image_file_name: str) -> str: diff --git a/manimlib/utils/init_config.py b/manimlib/utils/init_config.py index 36ae9d4b..2e5d4b32 100644 --- a/manimlib/utils/init_config.py +++ b/manimlib/utils/init_config.py @@ -1,16 +1,21 @@ from __future__ import annotations +import importlib +import inspect import os import yaml -import inspect -import importlib -from typing import Any from rich import box +from rich.console import Console +from rich.prompt import Confirm +from rich.prompt import Prompt from rich.rule import Rule from rich.table import Table -from rich.console import Console -from rich.prompt import Prompt, Confirm + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any def get_manim_dir() -> str: diff --git a/manimlib/utils/paths.py b/manimlib/utils/paths.py index 3bbf092d..67192e45 100644 --- a/manimlib/utils/paths.py +++ b/manimlib/utils/paths.py @@ -1,5 +1,6 @@ +from __future__ import annotations + import math -from typing import Callable import numpy as np @@ -8,6 +9,12 @@ from manimlib.utils.bezier import interpolate from manimlib.utils.space_ops import get_norm from manimlib.utils.space_ops import rotation_matrix_transpose +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Callable + + STRAIGHT_PATH_THRESHOLD = 0.01 diff --git a/manimlib/utils/rate_functions.py b/manimlib/utils/rate_functions.py index 79057734..45646760 100644 --- a/manimlib/utils/rate_functions.py +++ b/manimlib/utils/rate_functions.py @@ -1,9 +1,14 @@ -from typing import Callable +from __future__ import annotations import numpy as np from manimlib.utils.bezier import bezier +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Callable + def linear(t: float) -> float: return t diff --git a/manimlib/utils/simple_functions.py b/manimlib/utils/simple_functions.py index c6a7c5d1..1371a744 100644 --- a/manimlib/utils/simple_functions.py +++ b/manimlib/utils/simple_functions.py @@ -1,7 +1,8 @@ -import inspect -import numpy as np -import math from functools import lru_cache +import inspect +import math + +import numpy as np def sigmoid(x): diff --git a/manimlib/utils/sounds.py b/manimlib/utils/sounds.py index 79501284..f34ba435 100644 --- a/manimlib/utils/sounds.py +++ b/manimlib/utils/sounds.py @@ -1,8 +1,10 @@ -from manimlib.utils.file_ops import find_file +from __future__ import annotations + from manimlib.utils.directories import get_sound_dir +from manimlib.utils.file_ops import find_file -def get_full_sound_file_path(sound_file_name) -> str: +def get_full_sound_file_path(sound_file_name: str) -> str: return find_file( sound_file_name, directories=[get_sound_dir()], diff --git a/manimlib/utils/space_ops.py b/manimlib/utils/space_ops.py index e6ecfb13..faeef158 100644 --- a/manimlib/utils/space_ops.py +++ b/manimlib/utils/space_ops.py @@ -1,25 +1,27 @@ from __future__ import annotations +from functools import reduce import math import operator as op -from functools import reduce -from typing import Callable, Iterable, Sequence import platform -import numpy as np -import numpy.typing as npt from mapbox_earcut import triangulate_float32 as earcut +import numpy as np from scipy.spatial.transform import Rotation from tqdm import tqdm as ProgressDisplay -from manimlib.constants import RIGHT -from manimlib.constants import DOWN -from manimlib.constants import OUT -from manimlib.constants import PI -from manimlib.constants import TAU +from manimlib.constants import DOWN, OUT, RIGHT +from manimlib.constants import PI, TAU from manimlib.utils.iterables import adjacent_pairs from manimlib.utils.simple_functions import clip +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Callable, Iterable, Sequence + + import numpy.typing as npt + def cross(v1: np.ndarray, v2: np.ndarray) -> list[np.ndarray]: return [ diff --git a/manimlib/utils/tex_file_writing.py b/manimlib/utils/tex_file_writing.py index b41c2cf1..557d08be 100644 --- a/manimlib/utils/tex_file_writing.py +++ b/manimlib/utils/tex_file_writing.py @@ -1,18 +1,20 @@ -import sys -import os -import hashlib -from contextlib import contextmanager +from __future__ import annotations + +from contextlib import contextmanager +import hashlib +import os +import sys -from manimlib.utils.directories import get_tex_dir -from manimlib.config import get_manim_dir from manimlib.config import get_custom_config +from manimlib.config import get_manim_dir from manimlib.logger import log +from manimlib.utils.directories import get_tex_dir SAVED_TEX_CONFIG = {} -def get_tex_config(): +def get_tex_config() -> dict[str, str]: """ Returns a dict which should look something like this: { @@ -37,13 +39,13 @@ def get_tex_config(): return SAVED_TEX_CONFIG -def tex_hash(tex_file_content): +def tex_hash(tex_file_content: str) -> int: # Truncating at 16 bytes for cleanliness hasher = hashlib.sha256(tex_file_content.encode()) return hasher.hexdigest()[:16] -def tex_to_svg_file(tex_file_content): +def tex_to_svg_file(tex_file_content: str) -> str: svg_file = os.path.join( get_tex_dir(), tex_hash(tex_file_content) + ".svg" ) @@ -53,7 +55,7 @@ def tex_to_svg_file(tex_file_content): return svg_file -def tex_to_svg(tex_file_content, svg_file): +def tex_to_svg(tex_file_content: str, svg_file: str) -> str: tex_file = svg_file.replace(".svg", ".tex") with open(tex_file, "w", encoding="utf-8") as outfile: outfile.write(tex_file_content) @@ -69,7 +71,7 @@ def tex_to_svg(tex_file_content, svg_file): return svg_file -def tex_to_dvi(tex_file): +def tex_to_dvi(tex_file: str) -> str: tex_config = get_tex_config() program = tex_config["executable"] file_type = tex_config["intermediate_filetype"] @@ -96,7 +98,7 @@ def tex_to_dvi(tex_file): return result -def dvi_to_svg(dvi_file, regen_if_exists=False): +def dvi_to_svg(dvi_file: str) -> str: """ Converts a dvi, which potentially has multiple slides, into a directory full of enumerated pngs corresponding with these slides. @@ -123,7 +125,7 @@ def dvi_to_svg(dvi_file, regen_if_exists=False): # TODO, perhaps this should live elsewhere @contextmanager -def display_during_execution(message): +def display_during_execution(message: str) -> None: # Only show top line to_print = message.split("\n")[0] max_characters = os.get_terminal_size().columns - 1 diff --git a/manimlib/window.py b/manimlib/window.py index 0d9d3a47..f124c117 100644 --- a/manimlib/window.py +++ b/manimlib/window.py @@ -1,6 +1,7 @@ from __future__ import annotations import numpy as np + import moderngl_window as mglw from moderngl_window.context.pyglet.window import Window as PygletWindow from moderngl_window.timers.clock import Timer diff --git a/requirements.txt b/requirements.txt index a5225a8b..5c6ae599 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,23 +1,23 @@ colour -numpy -Pillow -scipy -sympy -tqdm +ipython +isosurfaces +manimpango>=0.4.0.post0,<0.5.0 mapbox-earcut matplotlib moderngl moderngl_window -skia-pathops +numpy +Pillow pydub pygments +PyOpenGL pyperclip pyyaml rich +scipy screeninfo -validators -ipython -PyOpenGL -manimpango>=0.4.0.post0,<0.5.0 -isosurfaces +skia-pathops svgelements +sympy +tqdm +validators diff --git a/setup.cfg b/setup.cfg index 934f051c..82c033f9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,27 +30,28 @@ packages = find: include_package_data = True install_requires = colour - numpy - Pillow - scipy - sympy - tqdm + ipython + isosurfaces + manimpango>=0.4.0.post0,<0.5.0 mapbox-earcut matplotlib moderngl moderngl_window - skia-pathops + numpy + Pillow pydub pygments + PyOpenGL + pyperclip pyyaml rich + scipy screeninfo - validators - ipython - PyOpenGL - manimpango>=0.4.0.post0,<0.5.0 - isosurfaces + skia-pathops svgelements + sympy + tqdm + validators [options.entry_points] console_scripts =