Merge branch 'refactor-tests' of https://github.com/Darylgolden/manim into refactor-tests

This commit is contained in:
Darylgolden 2022-04-01 09:52:25 +08:00
commit f68dcbdab5
31 changed files with 521 additions and 3727 deletions

View file

@ -45,7 +45,7 @@ jobs:
- name: Install Manim
run: |
poetry install -E webgl_renderer
poetry install
- name: Run tests
run: |
@ -202,7 +202,7 @@ jobs:
- name: Install manim
run: |
poetry config experimental.new-installer false
poetry install -E webgl_renderer
poetry install
- name: Run tests
run: |

View file

@ -56,7 +56,7 @@ repos:
flake8-docstrings==1.6.0, flake8-rst-docstrings==0.2.3,
flake8-pytest-style==1.5.0, flake8-simplify==0.14.1, flake8-comprehensions>=3.6.1]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: 'v0.941'
rev: 'v0.942'
hooks:
- id: mypy
additional_dependencies: [types-decorator, types-docutils, types-requests, types-setuptools]

View file

@ -30,7 +30,7 @@ RUN wget -O /tmp/install-tl-unx.tar.gz http://mirror.ctan.org/systems/texlive/tl
# clone and build manim
COPY . /opt/manim
WORKDIR /opt/manim
RUN pip install --no-cache .[jupyterlab,webgl_renderer]
RUN pip install --no-cache .[jupyterlab]
RUN pip install -r docs/requirements.txt

View file

@ -360,7 +360,6 @@ A list of all config options
'progress_bar', 'quality', 'right_side', 'save_as_gif', 'save_last_frame',
'save_pngs', 'scene_names', 'show_in_file_browser', 'sound', 'tex_dir',
'tex_template', 'tex_template_file', 'text_dir', 'top', 'transparent',
'upto_animation_number', 'use_opengl_renderer', 'use_webgl_renderer',
'verbosity', 'video_dir', 'webgl_renderer_path', 'window_position',
'window_monitor', 'window_size', 'write_all', 'write_to_movie', 'enable_wireframe',
'force_window']
'upto_animation_number', 'use_opengl_renderer', 'verbosity', 'video_dir',
'window_position', 'window_monitor', 'window_size', 'write_all', 'write_to_movie',
'enable_wireframe', 'force_window']

View file

@ -27,8 +27,6 @@ for i, arg in enumerate(sys.argv):
config.renderer = parsed_renderer
elif arg == "--use_opengl_renderer":
config.renderer = "opengl"
elif arg == "--use_webgl_renderer":
config.renderer = "webgl"
# many scripts depend on this -> has to be loaded first
from .utils.commands import * # isort:skip

View file

@ -93,15 +93,9 @@ tex_dir = {media_dir}/Tex
text_dir = {media_dir}/texts
partial_movie_dir = {video_dir}/partial_movie_files/{scene_name}
# --renderer [cairo|opengl|webgl]
# --renderer [cairo|opengl]
renderer = cairo
# --use_webgl_renderer
use_webgl_renderer = False
# --webgl_renderer_path
webgl_renderer_path =
# --use_opengl_renderer
use_opengl_renderer = False

View file

@ -15,7 +15,6 @@ import configparser
import copy
import json
import logging
import os
import sys
from typing import TYPE_CHECKING
@ -26,7 +25,7 @@ from rich.logging import RichHandler
from rich.theme import Theme
if TYPE_CHECKING:
from manim._config.utils import ManimConfig
from pathlib import Path
HIGHLIGHTED_KEYWORDS = [ # these keywords are highlighted specially
"Played",
"animations",
@ -142,7 +141,7 @@ def parse_theme(parser: configparser.ConfigParser) -> Theme:
return custom_theme
def set_file_logger(config: ManimConfig, verbosity: str) -> None:
def set_file_logger(scene_name: str, module_name: str, log_dir: Path) -> None:
"""Add a file handler to manim logger.
The path to the file is built using ``config.log_dir``.
@ -152,28 +151,13 @@ def set_file_logger(config: ManimConfig, verbosity: str) -> None:
config : :class:`ManimConfig`
The global config, used to determine the log file path.
verbosity : :class:`str`
The verbosity level of the logger.
Notes
-----
Calling this function changes the verbosity of all handlers assigned to
manim logger.
"""
# Note: The log file name will be
# <name_of_animation_file>_<name_of_scene>.log, gotten from config. So it
# can differ from the real name of the scene. <name_of_scene> would only
# appear if scene name was provided when manim was called.
scene_name_suffix = "".join(config["scene_names"])
scene_file_name = os.path.basename(config["input_file"]).split(".")[0]
log_file_name = (
f"{scene_file_name}_{scene_name_suffix}.log"
if scene_name_suffix
else f"{scene_file_name}.log"
)
log_file_path = config.get_dir("log_dir") / log_file_name
log_file_path.parent.mkdir(parents=True, exist_ok=True)
log_file_name = f"{module_name}_{scene_name}.log"
log_file_path = log_dir / log_file_name
file_handler = logging.FileHandler(log_file_path, mode="w")
file_handler.setFormatter(JSONFormatter())
@ -182,9 +166,6 @@ def set_file_logger(config: ManimConfig, verbosity: str) -> None:
logger.addHandler(file_handler)
logger.info("Log file will be saved in %(logpath)s", {"logpath": log_file_path})
config.verbosity = verbosity
logger.setLevel(verbosity)
class JSONFormatter(logging.Formatter):
"""A formatter that outputs logs in a custom JSON format.

View file

@ -19,9 +19,9 @@ import logging
import os
import re
import sys
import typing
from collections.abc import Mapping, MutableMapping
from pathlib import Path
from typing import Any, Iterable, Iterator
import colour
import numpy as np
@ -29,7 +29,6 @@ import numpy as np
from .. import constants
from ..utils.tex import TexTemplate, TexTemplateFromFile
from ..utils.tex_templates import TexTemplateLibrary
from .logger_utils import set_file_logger
def config_file_paths() -> list[Path]:
@ -255,7 +254,6 @@ class ManimConfig(MutableMapping):
"input_file",
"media_embed",
"media_width",
"webgl_renderer_path",
"log_dir",
"log_to_file",
"max_files_cached",
@ -282,7 +280,6 @@ class ManimConfig(MutableMapping):
"upto_animation_number",
"renderer",
"use_opengl_renderer",
"use_webgl_renderer",
"enable_gui",
"gui_location",
"use_projection_fill_shaders",
@ -304,7 +301,7 @@ class ManimConfig(MutableMapping):
self._d = {k: None for k in self._OPTS}
# behave like a dict
def __iter__(self) -> typing.Iterator[str]:
def __iter__(self) -> Iterator[str]:
return iter(self._d)
def __len__(self) -> int:
@ -317,10 +314,10 @@ class ManimConfig(MutableMapping):
except AttributeError:
return False
def __getitem__(self, key) -> typing.Any:
def __getitem__(self, key) -> Any:
return getattr(self, key)
def __setitem__(self, key: str, val: typing.Any) -> None:
def __setitem__(self, key: str, val: Any) -> None:
getattr(ManimConfig, key).fset(self, val) # fset is the property's setter
def update(self, obj: ManimConfig | dict) -> None:
@ -395,7 +392,7 @@ class ManimConfig(MutableMapping):
"""See ManimConfig.copy()."""
return copy.deepcopy(self)
def __deepcopy__(self, memo: dict[str, typing.Any]) -> ManimConfig:
def __deepcopy__(self, memo: dict[str, Any]) -> ManimConfig:
"""See ManimConfig.copy()."""
c = ManimConfig()
# Deepcopying the underlying dict is enough because all properties
@ -405,14 +402,14 @@ class ManimConfig(MutableMapping):
return c
# helper type-checking methods
def _set_from_list(self, key: str, val: typing.Any, values: list) -> None:
def _set_from_list(self, key: str, val: Any, values: list) -> None:
"""Set ``key`` to ``val`` if ``val`` is contained in ``values``."""
if val in values:
self._d[key] = val
else:
raise ValueError(f"attempted to set {key} to {val}; must be in {values}")
def _set_boolean(self, key: str | int, val: typing.Any) -> None:
def _set_boolean(self, key: str | int, val: Any) -> None:
"""Set ``key`` to ``val`` if ``val`` is Boolean."""
if val in [True, False]:
self._d[key] = val
@ -425,7 +422,7 @@ class ManimConfig(MutableMapping):
else:
raise ValueError(f"{key} must be tuple")
def _set_str(self, key: str, val: typing.Any) -> None:
def _set_str(self, key: str, val: Any) -> None:
"""Set ``key`` to ``val`` if ``val`` is a string."""
if isinstance(val, str):
self._d[key] = val
@ -537,7 +534,6 @@ class ManimConfig(MutableMapping):
"flush_cache",
"custom_folders",
"use_opengl_renderer",
"use_webgl_renderer",
"enable_gui",
"fullscreen",
"use_projection_fill_shaders",
@ -577,7 +573,6 @@ class ManimConfig(MutableMapping):
"movie_file_extension",
"background_color",
"renderer",
"webgl_renderer_path",
"window_position",
]:
setattr(self, key, parser["CLI"].get(key, fallback="", raw=True))
@ -710,7 +705,6 @@ class ManimConfig(MutableMapping):
"renderer",
"background_color",
"use_opengl_renderer",
"use_webgl_renderer",
"enable_gui",
"fullscreen",
"use_projection_fill_shaders",
@ -835,8 +829,7 @@ class ManimConfig(MutableMapping):
filename,
)
if filename:
return self.digest_parser(make_config_parser(filename))
return self.digest_parser(make_config_parser(filename))
# config options are properties
preview = property(
@ -861,19 +854,11 @@ class ManimConfig(MutableMapping):
doc="Whether to show progress bars while rendering animations.",
)
@property
def log_to_file(self):
"""Whether to save logs to a file."""
return self._d["log_to_file"]
@log_to_file.setter
def log_to_file(self, val: str) -> None:
self._set_boolean("log_to_file", val)
if val:
log_dir = self.get_dir("log_dir")
if not os.path.exists(log_dir):
os.makedirs(log_dir)
set_file_logger(self, self["verbosity"])
log_to_file = property(
lambda self: self._d["log_to_file"],
lambda self, val: self._set_boolean("log_to_file", val),
doc="Whether to save logs to a file.",
)
notify_outdated_version = property(
lambda self: self._d["notify_outdated_version"],
@ -929,12 +914,6 @@ class ManimConfig(MutableMapping):
doc="Set to force window when using the opengl renderer",
)
dry_run = property(
lambda self: self._d["dry_run"],
lambda self, val: self._set_boolean("dry_run", val),
doc="Enable dry_run so that no output files are generated and window is disabled.",
)
@property
def verbosity(self):
"""Logger verbosity; "DEBUG", "INFO", "WARNING", "ERROR", or "CRITICAL" (-v)."""
@ -1181,7 +1160,7 @@ class ManimConfig(MutableMapping):
@property
def renderer(self):
"""Renderer: "cairo", "opengl", "webgl"""
"""Renderer: "cairo", "opengl"""
return self._d["renderer"]
@renderer.setter
@ -1221,7 +1200,7 @@ class ManimConfig(MutableMapping):
self._set_from_list(
"renderer",
val,
["cairo", "opengl", "webgl"],
["cairo", "opengl"],
)
@property
@ -1236,31 +1215,9 @@ class ManimConfig(MutableMapping):
self._set_from_list(
"renderer",
"opengl",
["cairo", "opengl", "webgl"],
["cairo", "opengl"],
)
@property
def use_webgl_renderer(self):
"""Whether or not to use WebGL renderer."""
return self._d["use_webgl_renderer"]
@use_webgl_renderer.setter
def use_webgl_renderer(self, val: bool) -> None:
self._d["use_webgl_renderer"] = val
if val:
self._set_from_list(
"webgl",
"renderer",
["cairo", "opengl", "webgl"],
)
self["disable_caching"] = True
webgl_renderer_path = property(
lambda self: self._d["webgl_renderer_path"],
lambda self, val: self._d.__setitem__("webgl_renderer_path", val),
doc="Path to WebGL renderer.",
)
media_dir = property(
lambda self: self._d["media_dir"],
lambda self, val: self._set_dir("media_dir", val),
@ -1637,7 +1594,7 @@ class ManimFrame(Mapping):
self.__dict__["_c"] = c
# there are required by parent class Mapping to behave like a dict
def __getitem__(self, key: str | int) -> typing.Any:
def __getitem__(self, key: str | int) -> Any:
if key in self._OPTS:
return self._c[key]
elif key in self._CONSTANTS:
@ -1645,7 +1602,7 @@ class ManimFrame(Mapping):
else:
raise KeyError(key)
def __iter__(self) -> typing.Iterable:
def __iter__(self) -> Iterable:
return iter(list(self._OPTS) + list(self._CONSTANTS))
def __len__(self) -> int:

View file

@ -1,35 +0,0 @@
from __future__ import annotations
import copy
from .camera import Camera
class WebGLCamera(Camera):
def __init__(self, **kwargs):
super().__init__(self, **kwargs)
self.serialized_frame = []
self.pixel_array = None
def display_multiple_non_background_colored_vmobjects(self, vmobjects, _):
for vmobject in vmobjects:
# TODO: Store a proto instead of JSON.
needs_redraw = False
point_hash = hash(tuple(vmobject.points.flatten()))
if vmobject.point_hash != point_hash:
vmobject.point_hash = point_hash
needs_redraw = True
self.serialized_frame.append(
{
"points": vmobject.points.tolist(),
"style": vmobject.get_style(simple=True),
"id": id(vmobject),
"needs_redraw": needs_redraw,
},
)
def reset(self):
self.serialized_frame = []
def set_frame_to_background(self, background):
self.serialized_frame = copy.deepcopy(background)

View file

@ -50,16 +50,6 @@ def render(
)
args["renderer"] = "opengl"
if args["use_webgl_renderer"]:
logger.warning(
"--use_webgl_renderer is deprecated, please use --renderer=webgl instead!",
)
args["renderer"] = "webgl"
if args["use_webgl_renderer"] and args["use_opengl_renderer"]:
logger.warning("You may select only one renderer!")
sys.exit()
if args["save_as_gif"]:
logger.warning("--save_as_gif is deprecated, please use --format=gif instead!")
args["format"] = "gif"
@ -120,20 +110,6 @@ def render(
except Exception:
error_console.print_exception()
sys.exit(1)
elif config.renderer == "webgl":
try:
from manim.grpc.impl import frame_server_impl
server = frame_server_impl.get(file)
server.start()
server.wait_for_termination()
except ModuleNotFoundError:
console.print(
"Dependencies for the WebGL render are missing. Run "
"pip install manim[webgl_renderer] to install them.",
)
error_console.print_exception()
sys.exit(1)
else:
for SceneClass in scene_classes_from_file(file):
try:

View file

@ -95,7 +95,7 @@ render_options = option_group(
),
option(
"--renderer",
type=click.Choice(["cairo", "opengl", "webgl"], case_sensitive=False),
type=click.Choice(["cairo", "opengl"], case_sensitive=False),
help="Select a renderer for your Scene.",
default=None,
),
@ -105,18 +105,6 @@ render_options = option_group(
help="Render scenes using OpenGL (Deprecated).",
default=None,
),
option(
"--use_webgl_renderer",
is_flag=True,
help="Render scenes using the WebGL frontend (Deprecated).",
default=None,
),
option(
"--webgl_renderer_path",
default=None,
type=click.Path(),
help="The path to the WebGL frontend.",
),
option(
"-g",
"--save_pngs",

View file

@ -67,7 +67,6 @@ __all__ = [
"GIF_FILE_EXTENSION",
"FFMPEG_VERBOSITY_MAP",
"VERBOSITY_CHOICES",
"WEBGL_RENDERER_INFO",
"QUALITIES",
"DEFAULT_QUALITY",
"DEFAULT_QUALITY_SHORT",
@ -216,12 +215,6 @@ FFMPEG_VERBOSITY_MAP: dict[str, str] = {
"CRITICAL": "fatal",
}
VERBOSITY_CHOICES = FFMPEG_VERBOSITY_MAP.keys()
WEBGL_RENDERER_INFO: str = (
"The Electron frontend to Manim is hosted at "
"https://github.com/ManimCommunity/manim-renderer. After cloning and building it, "
"you can either start it prior to running Manim or specify the path to the "
"executable with the --webgl_renderer_path flag."
)
# Video qualities
QUALITIES: dict[str, dict[str, str | int | None]] = {

View file

View file

@ -1,8 +0,0 @@
# https://github.com/protocolbuffers/protobuf/issues/1491#issuecomment-547504972
from __future__ import annotations
import sys
from pathlib import Path
sys.path.append(str(Path(__file__).parent))

File diff suppressed because it is too large Load diff

View file

@ -1,171 +0,0 @@
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
"""Client and server classes corresponding to protobuf-defined services."""
from __future__ import annotations
import frameserver_pb2 as frameserver__pb2
import grpc
class FrameServerStub:
"""Missing associated documentation comment in .proto file."""
def __init__(self, channel):
"""Constructor.
Args:
channel: A grpc.Channel.
"""
self.GetFrameAtTime = channel.unary_unary(
"/frameserver.FrameServer/GetFrameAtTime",
request_serializer=frameserver__pb2.FrameRequest.SerializeToString,
response_deserializer=frameserver__pb2.FrameResponse.FromString,
)
self.FetchSceneData = channel.unary_unary(
"/frameserver.FrameServer/FetchSceneData",
request_serializer=frameserver__pb2.EmptyRequest.SerializeToString,
response_deserializer=frameserver__pb2.FetchSceneDataResponse.FromString,
)
self.ScriptUpdated = channel.unary_unary(
"/frameserver.FrameServer/ScriptUpdated",
request_serializer=frameserver__pb2.EmptyRequest.SerializeToString,
response_deserializer=frameserver__pb2.EmptyResponse.FromString,
)
class FrameServerServicer:
"""Missing associated documentation comment in .proto file."""
def GetFrameAtTime(self, request, context):
"""Returns a serialization of the scene at the specified time."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details("Method not implemented!")
raise NotImplementedError("Method not implemented!")
def FetchSceneData(self, request, context):
"""Returns a list of the names and durations of all animations in the scene."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details("Method not implemented!")
raise NotImplementedError("Method not implemented!")
def ScriptUpdated(self, request, context):
"""Returns when the manim script changes"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details("Method not implemented!")
raise NotImplementedError("Method not implemented!")
def add_FrameServerServicer_to_server(servicer, server):
rpc_method_handlers = {
"GetFrameAtTime": grpc.unary_unary_rpc_method_handler(
servicer.GetFrameAtTime,
request_deserializer=frameserver__pb2.FrameRequest.FromString,
response_serializer=frameserver__pb2.FrameResponse.SerializeToString,
),
"FetchSceneData": grpc.unary_unary_rpc_method_handler(
servicer.FetchSceneData,
request_deserializer=frameserver__pb2.EmptyRequest.FromString,
response_serializer=frameserver__pb2.FetchSceneDataResponse.SerializeToString,
),
"ScriptUpdated": grpc.unary_unary_rpc_method_handler(
servicer.ScriptUpdated,
request_deserializer=frameserver__pb2.EmptyRequest.FromString,
response_serializer=frameserver__pb2.EmptyResponse.SerializeToString,
),
}
generic_handler = grpc.method_handlers_generic_handler(
"frameserver.FrameServer",
rpc_method_handlers,
)
server.add_generic_rpc_handlers((generic_handler,))
# This class is part of an EXPERIMENTAL API.
class FrameServer:
"""Missing associated documentation comment in .proto file."""
@staticmethod
def GetFrameAtTime(
request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None,
):
return grpc.experimental.unary_unary(
request,
target,
"/frameserver.FrameServer/GetFrameAtTime",
frameserver__pb2.FrameRequest.SerializeToString,
frameserver__pb2.FrameResponse.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
)
@staticmethod
def FetchSceneData(
request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None,
):
return grpc.experimental.unary_unary(
request,
target,
"/frameserver.FrameServer/FetchSceneData",
frameserver__pb2.EmptyRequest.SerializeToString,
frameserver__pb2.FetchSceneDataResponse.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
)
@staticmethod
def ScriptUpdated(
request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None,
):
return grpc.experimental.unary_unary(
request,
target,
"/frameserver.FrameServer/ScriptUpdated",
frameserver__pb2.EmptyRequest.SerializeToString,
frameserver__pb2.EmptyResponse.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
)

View file

@ -1,376 +0,0 @@
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: renderserver.proto
"""Generated protocol buffer code."""
from __future__ import annotations
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from google.protobuf import reflection as _reflection
from google.protobuf import symbol_database as _symbol_database
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor.FileDescriptor(
name="renderserver.proto",
package="renderserver",
syntax="proto3",
serialized_options=None,
create_key=_descriptor._internal_create_key,
serialized_pb=b'\n\x12renderserver.proto\x12\x0crenderserver"f\n\x16UpdateSceneDataRequest\x12"\n\x05scene\x18\x01 \x01(\x0b\x32\x13.renderserver.Scene\x12\x11\n\texception\x18\x02 \x01(\t\x12\x15\n\rhas_exception\x18\x03 \x01(\x08"\\\n\x05Scene\x12\x0c\n\x04name\x18\x01 \x01(\t\x12+\n\nanimations\x18\x02 \x03(\x0b\x32\x17.renderserver.Animation\x12\x18\n\x10\x62\x61\x63kground_color\x18\x03 \x01(\t"+\n\tAnimation\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x10\n\x08\x64uration\x18\x02 \x01(\x02"\x0e\n\x0c\x45mptyRequest"\x0f\n\rEmptyResponse2d\n\x0cRenderServer\x12T\n\x0fUpdateSceneData\x12$.renderserver.UpdateSceneDataRequest\x1a\x1b.renderserver.EmptyResponseb\x06proto3',
)
_UPDATESCENEDATAREQUEST = _descriptor.Descriptor(
name="UpdateSceneDataRequest",
full_name="renderserver.UpdateSceneDataRequest",
filename=None,
file=DESCRIPTOR,
containing_type=None,
create_key=_descriptor._internal_create_key,
fields=[
_descriptor.FieldDescriptor(
name="scene",
full_name="renderserver.UpdateSceneDataRequest.scene",
index=0,
number=1,
type=11,
cpp_type=10,
label=1,
has_default_value=False,
default_value=None,
message_type=None,
enum_type=None,
containing_type=None,
is_extension=False,
extension_scope=None,
serialized_options=None,
file=DESCRIPTOR,
create_key=_descriptor._internal_create_key,
),
_descriptor.FieldDescriptor(
name="exception",
full_name="renderserver.UpdateSceneDataRequest.exception",
index=1,
number=2,
type=9,
cpp_type=9,
label=1,
has_default_value=False,
default_value=b"".decode("utf-8"),
message_type=None,
enum_type=None,
containing_type=None,
is_extension=False,
extension_scope=None,
serialized_options=None,
file=DESCRIPTOR,
create_key=_descriptor._internal_create_key,
),
_descriptor.FieldDescriptor(
name="has_exception",
full_name="renderserver.UpdateSceneDataRequest.has_exception",
index=2,
number=3,
type=8,
cpp_type=7,
label=1,
has_default_value=False,
default_value=False,
message_type=None,
enum_type=None,
containing_type=None,
is_extension=False,
extension_scope=None,
serialized_options=None,
file=DESCRIPTOR,
create_key=_descriptor._internal_create_key,
),
],
extensions=[],
nested_types=[],
enum_types=[],
serialized_options=None,
is_extendable=False,
syntax="proto3",
extension_ranges=[],
oneofs=[],
serialized_start=36,
serialized_end=138,
)
_SCENE = _descriptor.Descriptor(
name="Scene",
full_name="renderserver.Scene",
filename=None,
file=DESCRIPTOR,
containing_type=None,
create_key=_descriptor._internal_create_key,
fields=[
_descriptor.FieldDescriptor(
name="name",
full_name="renderserver.Scene.name",
index=0,
number=1,
type=9,
cpp_type=9,
label=1,
has_default_value=False,
default_value=b"".decode("utf-8"),
message_type=None,
enum_type=None,
containing_type=None,
is_extension=False,
extension_scope=None,
serialized_options=None,
file=DESCRIPTOR,
create_key=_descriptor._internal_create_key,
),
_descriptor.FieldDescriptor(
name="animations",
full_name="renderserver.Scene.animations",
index=1,
number=2,
type=11,
cpp_type=10,
label=3,
has_default_value=False,
default_value=[],
message_type=None,
enum_type=None,
containing_type=None,
is_extension=False,
extension_scope=None,
serialized_options=None,
file=DESCRIPTOR,
create_key=_descriptor._internal_create_key,
),
_descriptor.FieldDescriptor(
name="background_color",
full_name="renderserver.Scene.background_color",
index=2,
number=3,
type=9,
cpp_type=9,
label=1,
has_default_value=False,
default_value=b"".decode("utf-8"),
message_type=None,
enum_type=None,
containing_type=None,
is_extension=False,
extension_scope=None,
serialized_options=None,
file=DESCRIPTOR,
create_key=_descriptor._internal_create_key,
),
],
extensions=[],
nested_types=[],
enum_types=[],
serialized_options=None,
is_extendable=False,
syntax="proto3",
extension_ranges=[],
oneofs=[],
serialized_start=140,
serialized_end=232,
)
_ANIMATION = _descriptor.Descriptor(
name="Animation",
full_name="renderserver.Animation",
filename=None,
file=DESCRIPTOR,
containing_type=None,
create_key=_descriptor._internal_create_key,
fields=[
_descriptor.FieldDescriptor(
name="name",
full_name="renderserver.Animation.name",
index=0,
number=1,
type=9,
cpp_type=9,
label=1,
has_default_value=False,
default_value=b"".decode("utf-8"),
message_type=None,
enum_type=None,
containing_type=None,
is_extension=False,
extension_scope=None,
serialized_options=None,
file=DESCRIPTOR,
create_key=_descriptor._internal_create_key,
),
_descriptor.FieldDescriptor(
name="duration",
full_name="renderserver.Animation.duration",
index=1,
number=2,
type=2,
cpp_type=6,
label=1,
has_default_value=False,
default_value=float(0),
message_type=None,
enum_type=None,
containing_type=None,
is_extension=False,
extension_scope=None,
serialized_options=None,
file=DESCRIPTOR,
create_key=_descriptor._internal_create_key,
),
],
extensions=[],
nested_types=[],
enum_types=[],
serialized_options=None,
is_extendable=False,
syntax="proto3",
extension_ranges=[],
oneofs=[],
serialized_start=234,
serialized_end=277,
)
_EMPTYREQUEST = _descriptor.Descriptor(
name="EmptyRequest",
full_name="renderserver.EmptyRequest",
filename=None,
file=DESCRIPTOR,
containing_type=None,
create_key=_descriptor._internal_create_key,
fields=[],
extensions=[],
nested_types=[],
enum_types=[],
serialized_options=None,
is_extendable=False,
syntax="proto3",
extension_ranges=[],
oneofs=[],
serialized_start=279,
serialized_end=293,
)
_EMPTYRESPONSE = _descriptor.Descriptor(
name="EmptyResponse",
full_name="renderserver.EmptyResponse",
filename=None,
file=DESCRIPTOR,
containing_type=None,
create_key=_descriptor._internal_create_key,
fields=[],
extensions=[],
nested_types=[],
enum_types=[],
serialized_options=None,
is_extendable=False,
syntax="proto3",
extension_ranges=[],
oneofs=[],
serialized_start=295,
serialized_end=310,
)
_UPDATESCENEDATAREQUEST.fields_by_name["scene"].message_type = _SCENE
_SCENE.fields_by_name["animations"].message_type = _ANIMATION
DESCRIPTOR.message_types_by_name["UpdateSceneDataRequest"] = _UPDATESCENEDATAREQUEST
DESCRIPTOR.message_types_by_name["Scene"] = _SCENE
DESCRIPTOR.message_types_by_name["Animation"] = _ANIMATION
DESCRIPTOR.message_types_by_name["EmptyRequest"] = _EMPTYREQUEST
DESCRIPTOR.message_types_by_name["EmptyResponse"] = _EMPTYRESPONSE
_sym_db.RegisterFileDescriptor(DESCRIPTOR)
UpdateSceneDataRequest = _reflection.GeneratedProtocolMessageType(
"UpdateSceneDataRequest",
(_message.Message,),
{
"DESCRIPTOR": _UPDATESCENEDATAREQUEST,
"__module__": "renderserver_pb2",
# @@protoc_insertion_point(class_scope:renderserver.UpdateSceneDataRequest)
},
)
_sym_db.RegisterMessage(UpdateSceneDataRequest)
Scene = _reflection.GeneratedProtocolMessageType(
"Scene",
(_message.Message,),
{
"DESCRIPTOR": _SCENE,
"__module__": "renderserver_pb2",
# @@protoc_insertion_point(class_scope:renderserver.Scene)
},
)
_sym_db.RegisterMessage(Scene)
Animation = _reflection.GeneratedProtocolMessageType(
"Animation",
(_message.Message,),
{
"DESCRIPTOR": _ANIMATION,
"__module__": "renderserver_pb2",
# @@protoc_insertion_point(class_scope:renderserver.Animation)
},
)
_sym_db.RegisterMessage(Animation)
EmptyRequest = _reflection.GeneratedProtocolMessageType(
"EmptyRequest",
(_message.Message,),
{
"DESCRIPTOR": _EMPTYREQUEST,
"__module__": "renderserver_pb2",
# @@protoc_insertion_point(class_scope:renderserver.EmptyRequest)
},
)
_sym_db.RegisterMessage(EmptyRequest)
EmptyResponse = _reflection.GeneratedProtocolMessageType(
"EmptyResponse",
(_message.Message,),
{
"DESCRIPTOR": _EMPTYRESPONSE,
"__module__": "renderserver_pb2",
# @@protoc_insertion_point(class_scope:renderserver.EmptyResponse)
},
)
_sym_db.RegisterMessage(EmptyResponse)
_RENDERSERVER = _descriptor.ServiceDescriptor(
name="RenderServer",
full_name="renderserver.RenderServer",
file=DESCRIPTOR,
index=0,
serialized_options=None,
create_key=_descriptor._internal_create_key,
serialized_start=312,
serialized_end=412,
methods=[
_descriptor.MethodDescriptor(
name="UpdateSceneData",
full_name="renderserver.RenderServer.UpdateSceneData",
index=0,
containing_service=None,
input_type=_UPDATESCENEDATAREQUEST,
output_type=_EMPTYRESPONSE,
serialized_options=None,
create_key=_descriptor._internal_create_key,
),
],
)
_sym_db.RegisterServiceDescriptor(_RENDERSERVER)
DESCRIPTOR.services_by_name["RenderServer"] = _RENDERSERVER
# @@protoc_insertion_point(module_scope)

View file

@ -1,81 +0,0 @@
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
"""Client and server classes corresponding to protobuf-defined services."""
from __future__ import annotations
import grpc
import renderserver_pb2 as renderserver__pb2
class RenderServerStub:
"""Missing associated documentation comment in .proto file."""
def __init__(self, channel):
"""Constructor.
Args:
channel: A grpc.Channel.
"""
self.UpdateSceneData = channel.unary_unary(
"/renderserver.RenderServer/UpdateSceneData",
request_serializer=renderserver__pb2.UpdateSceneDataRequest.SerializeToString,
response_deserializer=renderserver__pb2.EmptyResponse.FromString,
)
class RenderServerServicer:
"""Missing associated documentation comment in .proto file."""
def UpdateSceneData(self, request, context):
"""Called from Manim when a scene has been newly rendered."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details("Method not implemented!")
raise NotImplementedError("Method not implemented!")
def add_RenderServerServicer_to_server(servicer, server):
rpc_method_handlers = {
"UpdateSceneData": grpc.unary_unary_rpc_method_handler(
servicer.UpdateSceneData,
request_deserializer=renderserver__pb2.UpdateSceneDataRequest.FromString,
response_serializer=renderserver__pb2.EmptyResponse.SerializeToString,
),
}
generic_handler = grpc.method_handlers_generic_handler(
"renderserver.RenderServer",
rpc_method_handlers,
)
server.add_generic_rpc_handlers((generic_handler,))
# This class is part of an EXPERIMENTAL API.
class RenderServer:
"""Missing associated documentation comment in .proto file."""
@staticmethod
def UpdateSceneData(
request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None,
):
return grpc.experimental.unary_unary(
request,
target,
"/renderserver.RenderServer/UpdateSceneData",
renderserver__pb2.UpdateSceneDataRequest.SerializeToString,
renderserver__pb2.EmptyResponse.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
)

View file

@ -1,409 +0,0 @@
from __future__ import annotations
import copy
import subprocess as sp
import traceback
import types
from concurrent import futures
import grpc
from manim.mobject.value_tracker import ValueTracker
from ... import config, logger
from ...constants import WEBGL_RENDERER_INFO
from ...mobject.types.image_mobject import ImageMobject
from ...mobject.types.vectorized_mobject import VMobject
from ...renderer.webgl_renderer import WebGLRenderer
from ...utils.family import extract_mobject_family_members
from ...utils.module_ops import scene_classes_from_file
from ..gen import (
frameserver_pb2,
frameserver_pb2_grpc,
renderserver_pb2,
renderserver_pb2_grpc,
)
class FrameServer(frameserver_pb2_grpc.FrameServerServicer):
def __init__(self, server, input_file_path):
self.server = server
self.input_file_path = input_file_path
self.exception = None
self.load_scene_module()
try:
self.update_renderer_scene_data()
except grpc._channel._InactiveRpcError:
logger.warning("No frontend was detected at localhost:50052.")
try:
sp.Popen(config["webgl_renderer_path"])
except PermissionError:
logger.info(WEBGL_RENDERER_INFO)
self.server.stop(None)
return
def GetFrameAtTime(self, request, context):
try:
requested_scene_index = request.animation_index
# Find the requested scene.
scene_finished = False
if requested_scene_index == request.end_index:
scene_finished = True
if (
request.animation_offset
<= self.keyframes[requested_scene_index].duration
):
animation_offset = request.animation_offset
else:
if requested_scene_index + 1 < request.end_index:
requested_scene_index += 1
animation_offset = 0
else:
scene_finished = True
animation_offset = self.keyframes[requested_scene_index].duration
if requested_scene_index == self.previous_scene_index:
requested_scene = self.previous_scene
update_previous_scene = False
else:
requested_scene = copy.deepcopy(self.keyframes[requested_scene_index])
update_previous_scene = True
requested_scene.update_to_time(animation_offset)
ids_to_remove = []
mobjects_to_add = []
animations = []
updaters = []
update_data = []
# TODO: Only remove/add changed mobjects rather than all of them.
if self.previous_scene is not None and (
request.first_request or self.previous_scene != requested_scene
):
previous_mobjects = extract_mobject_family_members(
self.previous_scene.mobjects,
only_those_with_points=True,
)
# Remove everything from the previous scene.
ids_to_remove = [
mob.original_id
for mob in previous_mobjects
if not isinstance(mob, ValueTracker)
]
if request.first_request or self.previous_scene != requested_scene:
# Add everything from the requested scene.
mobjects_to_add = [
serialize_mobject(mobject)
for mobject in extract_mobject_family_members(
requested_scene.mobjects,
only_those_with_points=True,
)
if not isinstance(mobject, ValueTracker)
]
# Send animation and updater info.
all_animations_tweened = True
for animation in requested_scene.animations:
attribute_tween_data = generate_attribute_tween_data(animation)
mobject_tween_data_list = []
flickered_mobject_ids = []
if attribute_tween_data is None:
all_animations_tweened = False
flickered_mobject_ids = [
mob.original_id
for mob in extract_mobject_family_members(
animation.mobject,
only_those_with_points=True,
)
]
else:
if animation.mobject is not None:
# Add offset vector to submobjects.
root_mobject_center = animation.mobject.get_center()
for updated_mobject in extract_mobject_family_members(
animation.mobject,
only_those_with_points=True,
):
mobject_tween_data_list.append(
frameserver_pb2.Animation.MobjectTweenData(
id=updated_mobject.original_id,
root_mobject_offset=updated_mobject.get_center()
- root_mobject_center,
),
)
animation_proto = frameserver_pb2.Animation(
name=animation.__class__.__name__,
duration=requested_scene.duration,
easing_function=animation.rate_func.__name__,
attribute_tween_data=attribute_tween_data,
mobject_tween_data=mobject_tween_data_list,
flickered_mobject_ids=flickered_mobject_ids,
)
animations.append(animation_proto)
for (
updated_mobject,
updater_list,
) in requested_scene.mobject_updater_lists:
all_updaters_tweened = True
for updater in updater_list:
attribute_tween_data = generate_attribute_tween_data(updater)
if attribute_tween_data is None:
all_animations_tweened = False
all_updaters_tweened = False
updaters.append(
frameserver_pb2.Updater(
flickered_mobject_ids=[
mob.original_id
for mob in extract_mobject_family_members(
updated_mobject,
only_those_with_points=True,
)
],
),
)
break
else:
raise NotImplementedError("Add tween data for updaters.")
if all_updaters_tweened:
# Append an updater with tween data.
pass
else:
all_animations_tweened = False
for animation in requested_scene.animations:
# Only send update data for animations that don't have tween data.
if generate_attribute_tween_data(animation) is None:
update_data.extend(
[
serialize_mobject(mobject)
for mobject in extract_mobject_family_members(
animation.mobject,
only_those_with_points=True,
)
if not isinstance(mobject, ValueTracker)
],
)
for (
updated_mobject,
updater_list,
) in requested_scene.mobject_updater_lists:
for updater in updater_list:
# Only send update data for updaters that don't have tween data.
if generate_attribute_tween_data(updater) is None:
update_data.extend(
[
serialize_mobject(mobject)
for mobject in extract_mobject_family_members(
updated_mobject,
only_those_with_points=True,
)
if not isinstance(mobject, ValueTracker)
],
)
resp = frameserver_pb2.FrameResponse(
frame_data=frameserver_pb2.FrameData(
remove=ids_to_remove,
add=mobjects_to_add,
update=update_data,
),
scene_finished=scene_finished,
animations=animations,
updaters=updaters,
animation_index=requested_scene_index,
animation_offset=animation_offset,
all_animations_tweened=all_animations_tweened,
)
if update_previous_scene:
self.previous_scene = requested_scene
self.previous_scene_index = requested_scene_index
return resp
except Exception:
traceback.print_exc()
def FetchSceneData(self, request, context):
try:
request = frameserver_pb2.FetchSceneDataResponse(
scene=frameserver_pb2.Scene(
name=str(self.scene),
animations=[
frameserver_pb2.Animation(
name=animations_to_name(scene.animations),
duration=scene.duration,
)
for scene in self.keyframes
],
),
path=str(self.input_file_path),
)
if hasattr(self.scene.camera, "background_color"):
request.scene.background_color = self.scene.camera.background_color
else:
request.scene.background_color = "#000000"
return request
except Exception:
traceback.print_exc()
def ScriptUpdated(self, request, context):
self.load_scene_module()
with grpc.insecure_channel("localhost:50052") as channel:
stub = renderserver_pb2_grpc.RenderServerStub(channel)
try:
self.update_renderer_scene_data()
except grpc._channel._InactiveRpcError:
logger.warning("No frontend was detected at localhost:50052.")
sp.Popen(config["js_renderer_path"])
return frameserver_pb2.EmptyResponse()
def load_scene_module(self):
self.exception = None
try:
self.scene_class = scene_classes_from_file(
self.input_file_path,
require_single_scene=True,
)
self.generate_keyframe_data()
except Exception as e:
self.exception = e
def generate_keyframe_data(self):
self.keyframes = []
self.previous_scene_index = None
self.previous_scene = None
self.renderer = WebGLRenderer(self)
self.scene = self.scene_class(self.renderer)
self.scene.render()
def update_renderer_scene_data(self):
# If a javascript renderer is running, notify it of the scene being served. If
# not, spawn one and it will request the scene when it starts.
with grpc.insecure_channel("localhost:50052") as channel:
stub = renderserver_pb2_grpc.RenderServerStub(channel)
if not self.exception:
request = renderserver_pb2.UpdateSceneDataRequest(
scene=renderserver_pb2.Scene(
name=str(self.scene),
animations=[
renderserver_pb2.Animation(
name=animations_to_name(scene.animations),
duration=scene.duration,
)
for scene in self.keyframes
],
),
)
if hasattr(self.scene.camera, "background_color"):
request.scene.background_color = self.scene.camera.background_color
else:
request.scene.background_color = "#000000"
else:
lines = traceback.format_exception(
None,
self.exception,
self.exception.__traceback__,
)
request = renderserver_pb2.UpdateSceneDataRequest(
has_exception=True,
exception="\n".join(lines),
)
stub.UpdateSceneData(request)
def generate_attribute_tween_data(animation):
if isinstance(animation, types.FunctionType):
return None
animation_name = animation.__class__.__name__
if animation_name == "_MethodAnimation":
tween_data_array = []
for method in animation.methods:
if method.__name__ in ["shift", "to_edge"]:
tween_data_array.append(
frameserver_pb2.Animation.AttributeTweenData(
attribute="position",
start_data=animation.starting_mobject.get_center(),
end_data=animation.target_mobject.get_center(),
),
)
else:
return None
return tween_data_array
elif animation_name == "FadeIn":
return [
frameserver_pb2.Animation.AttributeTweenData(
attribute="fill_opacity",
start_data=[animation.starting_mobject.fill_opacity],
end_data=[animation.target_copy.fill_opacity],
),
frameserver_pb2.Animation.AttributeTweenData(
attribute="stroke_opacity",
start_data=[animation.starting_mobject.stroke_opacity],
end_data=[animation.target_copy.stroke_opacity],
),
]
elif animation_name == "Wait":
return []
else:
return None
def animations_to_name(animations):
if len(animations) == 1:
return str(animations[0].__class__.__name__)
return f"{str(animations[0].__class__.__name__)}..."
def serialize_mobject(mobject):
mob_proto = frameserver_pb2.MobjectData(id=mobject.original_id)
if isinstance(mobject, VMobject):
needs_redraw = False
point_hash = hash(tuple(mobject.points.flatten()))
if mobject.point_hash != point_hash:
mobject.point_hash = point_hash
needs_redraw = True
mob_proto.vectorized_mobject_data.needs_redraw = needs_redraw
for point in mobject.points:
point_proto = mob_proto.vectorized_mobject_data.points.add()
point_proto.x = point[0]
point_proto.y = point[1]
point_proto.z = point[2]
mob_style = mobject.get_style(simple=True)
mob_proto.style.fill_color = mob_style["fill_color"]
mob_proto.style.fill_opacity = float(mob_style["fill_opacity"])
mob_proto.style.stroke_color = mob_style["stroke_color"]
mob_proto.style.stroke_opacity = float(mob_style["stroke_opacity"])
mob_proto.style.stroke_width = float(mob_style["stroke_width"])
elif isinstance(mobject, ImageMobject):
mob_proto.type = frameserver_pb2.MobjectData.MobjectType.IMAGE_MOBJECT
mob_style = mobject.get_style()
mob_proto.style.fill_color = mob_style["fill_color"]
mob_proto.style.fill_opacity = float(mob_style["fill_opacity"])
assets_dir_path = str(config.get_dir("assets_dir"))
if mobject.path.startswith(assets_dir_path):
mob_proto.image_mobject_data.path = mobject.path[len(assets_dir_path) + 1 :]
else:
logger.info(
f"Expected path {mobject.path} to be under the assets dir ({assets_dir_path})",
)
mob_proto.image_mobject_data.height = mobject.height
mob_proto.image_mobject_data.width = mobject.width
mob_center = mobject.get_center()
mob_proto.image_mobject_data.center.x = mob_center[0]
mob_proto.image_mobject_data.center.y = mob_center[1]
mob_proto.image_mobject_data.center.z = mob_center[2]
return mob_proto
def get(input_file_path):
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
frameserver_pb2_grpc.add_FrameServerServicer_to_server(
FrameServer(server, input_file_path),
server,
)
server.add_insecure_port("localhost:50051")
return server

View file

@ -1,114 +0,0 @@
syntax = "proto3";
package frameserver;
service FrameServer {
// Returns a serialization of the scene at the specified time.
rpc GetFrameAtTime (FrameRequest) returns (FrameResponse);
// Returns a list of the names and durations of all animations in the scene.
rpc FetchSceneData (EmptyRequest) returns (FetchSceneDataResponse);
// Returns when the manim script changes
rpc ScriptUpdated (EmptyRequest) returns (EmptyResponse);
}
message FetchSceneDataResponse {
Scene scene = 1;
string path = 2;
}
message Scene {
string name = 1;
repeated Animation animations = 2;
string background_color = 3;
}
message Animation {
string name = 1;
float duration = 2;
string easing_function = 3;
message AttributeTweenData {
string attribute = 1;
repeated float start_data = 2;
repeated float end_data = 3;
}
repeated AttributeTweenData attribute_tween_data = 4;
message MobjectTweenData {
string id = 1;
repeated float root_mobject_offset = 2;
}
repeated MobjectTweenData mobject_tween_data = 5;
repeated string flickered_mobject_ids = 6;
}
message Updater {
repeated string flickered_mobject_ids = 1;
}
message FrameRequest {
int32 end_index = 1;
bool first_request = 2;
int32 animation_index = 3;
float animation_offset = 4;
}
message Style {
string fill_color = 1;
float fill_opacity = 2;
string stroke_color = 3;
float stroke_opacity = 4;
float stroke_width = 5;
}
message Point {
float x = 1;
float y = 2;
float z = 3;
}
message MobjectData {
string id = 1;
Style style = 2;
enum MobjectType {
VMOBJECT = 0;
IMAGE_MOBJECT = 1;
}
MobjectType type = 3;
VMobjectData vectorized_mobject_data = 4;
ImageMobjectData image_mobject_data = 5;
repeated float root_mobject_offset = 6;
}
message VMobjectData {
repeated Point points = 1;
bool needs_redraw = 2;
}
message ImageMobjectData {
string path = 1;
float height = 2;
float width = 3;
Point center = 4;
}
message FrameData {
repeated string remove = 1;
repeated MobjectData add = 2;
repeated MobjectData update = 3;
}
message FrameResponse {
FrameData frame_data = 1;
bool scene_finished = 2;
repeated Animation animations = 3;
repeated Updater updaters = 4;
int32 animation_index = 5;
float animation_offset = 6;
bool all_animations_tweened = 7;
}
message EmptyRequest {}
message EmptyResponse {}

View file

@ -1,28 +0,0 @@
syntax = "proto3";
package renderserver;
service RenderServer {
// Called from Manim when a scene has been newly rendered.
rpc UpdateSceneData (UpdateSceneDataRequest) returns (EmptyResponse);
}
message UpdateSceneDataRequest {
Scene scene = 1;
string exception = 2;
bool has_exception = 3;
}
message Scene {
string name = 1;
repeated Animation animations = 2;
string background_color = 3;
}
message Animation {
string name = 1;
float duration = 2;
}
message EmptyRequest {}
message EmptyResponse {}

View file

@ -1,67 +0,0 @@
from __future__ import annotations
import copy
from manim import config
from ..utils.family import extract_mobject_family_members
class WebGLRenderer:
def __init__(self, frame_server):
self.skip_animations = True
self.frame_server = frame_server
self.camera = WebGLCamera()
self.num_plays = 0
def init_scene(self, scene):
pass
def scene_finished(self, scene):
pass
def play(self, scene, *args, **kwargs):
self.num_plays += 1
# If the scene contains an updater it must be updated frame by frame.
for mob in extract_mobject_family_members(scene.mobjects):
if len(mob.updaters) > 0:
self.skip_animations = False
break
s = scene.compile_animation_data(*args, skip_rendering=True, **kwargs)
scene.begin_animations()
self.skip_animations = True
scene_copy = copy.deepcopy(scene)
scene_copy.renderer = self
self.frame_server.keyframes.append(scene_copy)
if s is None:
# Nothing happens in this animation, so there's no need to update it.
scene_copy.is_static = True
else:
scene_copy.is_static = False
scene.play_internal(skip_rendering=True)
def update_frame( # TODO Description in Docstring
self,
scene,
mobjects=None,
include_submobjects=True,
ignore_skipping=True,
**kwargs,
):
pass
def save_static_frame_data(self, scene, static_mobjects):
pass
def add_frame(self, frame, num_frames=1):
pass
def get_frame(self):
pass
class WebGLCamera:
def __init__(self, use_z_index=True):
self.use_z_index = use_z_index
self.frame_rate = config["frame_rate"]

View file

@ -19,6 +19,7 @@ from pydub import AudioSegment
from manim import __version__
from .. import config, logger
from .._config.logger_utils import set_file_logger
from ..constants import FFMPEG_BIN, GIF_FILE_EXTENSION
from ..utils.file_ops import (
add_extension_if_not_present,
@ -158,6 +159,12 @@ class SceneFileWriter:
),
)
if config["log_to_file"]:
log_dir = guarantee_existence(config.get_dir("log_dir"))
set_file_logger(
scene_name=scene_name, module_name=module_name, log_dir=log_dir
)
def finish_last_section(self) -> None:
"""Delete current section if it is empty."""
if len(self.sections) and self.sections[-1].is_empty():

View file

@ -24,6 +24,10 @@ import subprocess as sp
import time
from pathlib import Path
from shutil import copyfile
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from ..scene.scene_file_writer import SceneFileWriter
from manim import __version__, config, logger
@ -117,33 +121,35 @@ def write_to_movie() -> bool:
)
def add_extension_if_not_present(file_name, extension):
def add_extension_if_not_present(file_name: Path, extension: str) -> Path:
if file_name.suffix != extension:
return file_name.with_suffix(extension)
else:
return file_name
def add_version_before_extension(file_name):
def add_version_before_extension(file_name: Path) -> Path:
file_name = Path(file_name)
path, name, suffix = file_name.parent, file_name.stem, file_name.suffix
return Path(path, f"{name}_ManimCE_v{__version__}{suffix}")
def guarantee_existence(path):
def guarantee_existence(path: Path) -> Path:
if not os.path.exists(path):
os.makedirs(path)
return os.path.abspath(path)
return Path(os.path.abspath(path))
def guarantee_empty_existence(path):
def guarantee_empty_existence(path: Path) -> Path:
if os.path.exists(path):
shutil.rmtree(path)
os.makedirs(path)
return os.path.abspath(path)
return Path(os.path.abspath(path))
def seek_full_path_from_defaults(file_name, default_dir, extensions):
def seek_full_path_from_defaults(
file_name: Path, default_dir: str, extensions: str
) -> Path:
possible_paths = [file_name]
possible_paths += [
Path(default_dir) / f"{file_name}{extension}" for extension in ["", *extensions]
@ -155,7 +161,7 @@ def seek_full_path_from_defaults(file_name, default_dir, extensions):
raise OSError(error)
def modify_atime(file_path):
def modify_atime(file_path) -> None:
"""Will manually change the accessed time (called `atime`) of the file, as on a lot of OS the accessed time refresh is disabled by default.
Parameters
@ -188,7 +194,7 @@ def open_file(file_path, in_browser=False):
sp.Popen(commands)
def open_media_file(file_writer):
def open_media_file(file_writer: SceneFileWriter) -> None:
file_paths = []
if config["save_last_frame"]:
@ -207,7 +213,7 @@ def open_media_file(file_writer):
logger.info(f"Previewed File at: '{file_path}'")
def get_template_names():
def get_template_names() -> list[str]:
"""Returns template names from the templates directory.
Returns
@ -218,7 +224,7 @@ def get_template_names():
return [template_name.stem for template_name in template_path.glob("*.mtp")]
def get_template_path():
def get_template_path() -> Path:
"""Returns the Path of templates directory.
Returns
@ -242,7 +248,9 @@ def add_import_statement(file):
f.write(import_line.rstrip("\r\n") + "\n" + content)
def copy_template_files(project_dir=Path("."), template_name="Default"):
def copy_template_files(
project_dir: Path = Path("."), template_name: str = "Default"
) -> None:
"""Copies template files from templates dir to project_dir.
Parameters

1031
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -35,14 +35,12 @@ Pillow = ">=8.4,<10.0"
scipy = "^1.7.3"
tqdm = "^4.62.3"
pydub = "^0.25.1"
rich = ">=6.0"
rich = ">=6.0,!=12.0.0"
pycairo = "^1.19"
manimpango = "^0.4.0.post0"
networkx = "^2.5"
decorator = "^5.0.7"
importlib-metadata = { version = "^4.10.0", python = "<3.8" }
grpcio = { version = "^1.43.0", optional = true }
grpcio-tools = { version = "^1.43.0", optional = true }
watchdog = "^2.1.6"
jupyterlab = { version = "^3.0", optional = true }
moderngl = "^5.6.3"
@ -59,7 +57,6 @@ Pygments = "^2.10.0"
"backports.cached-property" = "^1.0.1"
[tool.poetry.extras]
webgl_renderer = ["grpcio","grpcio-tools"]
jupyterlab = ["jupyterlab"]
gui = ["dearpygui"]

View file

@ -1,19 +0,0 @@
#!/usr/bin/env python3
from __future__ import annotations
import sys
from http.server import HTTPServer, SimpleHTTPRequestHandler, test # type: ignore
class CORSRequestHandler(SimpleHTTPRequestHandler):
def end_headers(self):
self.send_header("Access-Control-Allow-Origin", "*")
super().end_headers()
if __name__ == "__main__":
test(
CORSRequestHandler,
HTTPServer,
port=int(sys.argv[1]) if len(sys.argv) > 1 else 8000,
)

View file

@ -1,19 +0,0 @@
#!/usr/bin/env python
"""
This is intended to be run from manim/grpc
"""
from __future__ import annotations
import os
CMD_STRING = """
poetry run python \
-m grpc_tools.protoc \
-I./proto \
--python_out=./gen \
--grpc_python_out=./gen \
./proto/frameserver.proto \
./proto/renderserver.proto
"""
os.system(CMD_STRING)

View file

@ -98,6 +98,7 @@ def test_custom_dirs(tmp_path):
{
"media_dir": tmp_path,
"save_sections": True,
"log_to_file": True,
"frame_rate": 15,
"pixel_height": 854,
"pixel_width": 480,
@ -108,6 +109,7 @@ def test_custom_dirs(tmp_path):
"images_dir": "{media_dir}/test_images",
"text_dir": "{media_dir}/test_text",
"tex_dir": "{media_dir}/test_tex",
"log_dir": "{media_dir}/test_log",
}
):
scene = MyScene()
@ -131,7 +133,7 @@ def test_custom_dirs(tmp_path):
assert_dir_filled(os.path.join(tmp_path, "test_text"))
assert_dir_filled(os.path.join(tmp_path, "test_tex"))
# TODO: testing the log dir would be nice but it doesn't get generated for some reason and test crashes when setting "log_to_file" to True
assert_dir_filled(os.path.join(tmp_path, "test_log"))
def test_frame_size(tmp_path):

View file

@ -35,32 +35,6 @@ def test_logging_to_file(tmp_path, python_version):
assert exitcode == 0, err
@logs_comparison(
"BasicSceneLoggingTest.txt",
os.path.join("logs", "basic_scenes_square_to_circle.log"),
)
def test_logging_when_scene_is_not_specified(tmp_path, python_version):
path_basic_scene = os.path.join(
"tests",
"test_logging",
"basic_scenes_square_to_circle.py",
)
command = [
python_version,
"-m",
"manim",
"-ql",
"-v",
"DEBUG",
"--log_to_file",
"--media_dir",
str(tmp_path),
path_basic_scene,
]
_, err, exitcode = capture(command)
assert exitcode == 0, err
def test_error_logging(tmp_path, python_version):
path_error_scene = Path("tests/test_logging/basic_scenes_error.py")